chore: back merge irm (#5111)

# What this PR does

Back merge irm

## Checklist

- [ ] Unit, integration, and e2e (if applicable) tests updated
- [x] Documentation added (or `pr:no public docs` PR label added if not
required)
- [x] Added the relevant release notes label (see labels prefixed w/
`release:`). These labels dictate how your PR will
    show up in the autogenerated release notes.
This commit is contained in:
Dominik Broj 2024-10-02 12:34:21 +02:00 committed by GitHub
parent e9d94ebc1e
commit 612c0e5a2e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
14 changed files with 130 additions and 8 deletions

View file

@ -1,5 +1,17 @@
# Changelog
## [1.9.28](https://github.com/grafana/irm/compare/grafana-oncall-app-v1.9.27...grafana-oncall-app-v1.9.28) (2024-10-01)
### Bug Fixes
* disallow oncall schedule rotation layer/overrides CUD form submissions more than once ([#193](https://github.com/grafana/irm/issues/193)) ([73ae1c7](https://github.com/grafana/irm/commit/73ae1c7d78474b42b9eb4305416828afeb04fa3a))
### Miscellaneous Chores
* implement merged IRM module.tsx ([#182](https://github.com/grafana/irm/issues/182)) ([995b573](https://github.com/grafana/irm/commit/995b5732493aabc226cd62b9ca52a1e582ef5878))
## [1.9.27](https://github.com/grafana/irm/compare/grafana-oncall-app-v1.9.26...grafana-oncall-app-v1.9.27) (2024-09-26)

View file

@ -1,6 +1,6 @@
{
"name": "grafana-oncall-app",
"version": "1.9.27",
"version": "1.9.28",
"description": "Grafana OnCall Plugin",
"scripts": {
"lint": "eslint --ext .js,.jsx,.ts,.tsx --max-warnings=20 ./src ./e2e-tests",

View file

@ -131,6 +131,8 @@ class _EscalationPolicy extends React.Component<EscalationPolicyProps, any> {
return this.renderNumAlertsInWindow();
case 'num_minutes_in_window':
return this.renderNumMinutesInWindowOptions();
case 'severity':
return this.renderSeverities();
default:
console.warn('Unknown escalation step placeholder');
return '';
@ -248,6 +250,34 @@ class _EscalationPolicy extends React.Component<EscalationPolicyProps, any> {
);
}
renderSeverities() {
const {
data,
isDisabled,
theme,
store: { escalationPolicyStore },
} = this.props;
const styles = getEscalationPolicyStyles(theme);
const { severity } = data;
return (
<WithPermissionControlTooltip key="" userAction={UserActions.EscalationChainsWrite}>
<Select
menuShouldPortal
disabled={isDisabled}
placeholder="Severity"
className={cx(styles.select, styles.control)}
value={severity}
onChange={this.getOnSelectChangeHandler('severity')}
options={escalationPolicyStore.severityChoices.map((severity_choice) => ({
value: severity_choice.value,
label: severity_choice.display_name,
}))}
/>
</WithPermissionControlTooltip>
);
}
renderTimeRange() {
const { data, isDisabled, theme } = this.props;
const styles = getEscalationPolicyStyles(theme);

View file

@ -45,6 +45,7 @@ export const EscalationChainSteps = observer((props: EscalationChainStepsProps)
useEffect(() => {
escalationPolicyStore.updateWebEscalationPolicyOptions();
escalationPolicyStore.updateSeverityOptions();
}, []);
const handleSortEnd = useCallback(

View file

@ -16,7 +16,7 @@ import {
} from '@grafana/ui';
import dayjs from 'dayjs';
import { GRAFANA_HEADER_HEIGHT, StackSize } from 'helpers/consts';
import { useDebouncedCallback, useResize } from 'helpers/hooks';
import { useDebouncedCallback, useIsLoading, useResize } from 'helpers/hooks';
import { observer } from 'mobx-react';
import Draggable, { DraggableData, DraggableEvent } from 'react-draggable';
@ -51,6 +51,7 @@ import { DeletionModal } from 'containers/RotationForm/parts/DeletionModal';
import { TimeUnitSelector } from 'containers/RotationForm/parts/TimeUnitSelector';
import { UserItem } from 'containers/RotationForm/parts/UserItem';
import { calculateScheduleFormOffset } from 'containers/Rotations/Rotations.helpers';
import { ActionKey } from 'models/loader/action-keys';
import { getShiftName } from 'models/schedule/schedule.helpers';
import { Schedule, Shift } from 'models/schedule/schedule.types';
import { ApiSchemas } from 'network/oncall-api/api.types';
@ -114,6 +115,10 @@ export const RotationForm = observer((props: RotationFormProps) => {
const [startRotationFromUserIndex, setStartRotationFromUserIndex] = useState(0);
const isCreating = useIsLoading(ActionKey.CREATE_ONCALL_SHIFT);
const isUpdating = useIsLoading(ActionKey.UPDATE_ONCALL_SHIFT);
const isSubmitting = isCreating || isUpdating;
const [errors, setErrors] = useState<{ [key: string]: string[] }>({});
const [bounds, setDraggableBounds] = useState<{ left: number; right: number; top: number; bottom: number }>(
undefined
@ -526,7 +531,7 @@ export const RotationForm = observer((props: RotationFormProps) => {
const hasUpdatedShift = shift && shift.updated_shift;
const ended = shift && shift.until && getDateTime(shift.until).isBefore(dayjs());
const disabled = hasUpdatedShift || ended;
const disabled = hasUpdatedShift || ended || isSubmitting;
return (
<>

View file

@ -4,7 +4,7 @@ import { cx } from '@emotion/css';
import { IconButton, Stack, Field, Button, useTheme2, useStyles2 } from '@grafana/ui';
import dayjs from 'dayjs';
import { StackSize } from 'helpers/consts';
import { useDebouncedCallback, useResize } from 'helpers/hooks';
import { useDebouncedCallback, useIsLoading, useResize } from 'helpers/hooks';
import Draggable, { DraggableData, DraggableEvent } from 'react-draggable';
import { Modal } from 'components/Modal/Modal';
@ -13,6 +13,7 @@ import { Text } from 'components/Text/Text';
import { UserGroups } from 'components/UserGroups/UserGroups';
import { WithConfirm } from 'components/WithConfirm/WithConfirm';
import { calculateScheduleFormOffset } from 'containers/Rotations/Rotations.helpers';
import { ActionKey } from 'models/loader/action-keys';
import { getShiftName } from 'models/schedule/schedule.helpers';
import { Schedule, Shift } from 'models/schedule/schedule.types';
import { ApiSchemas } from 'network/oncall-api/api.types';
@ -64,6 +65,11 @@ export const ScheduleOverrideForm: FC<RotationFormProps> = (props) => {
const [offsetTop, setOffsetTop] = useState<number>(0);
const isCreating = useIsLoading(ActionKey.CREATE_ONCALL_SHIFT);
const isUpdating = useIsLoading(ActionKey.UPDATE_ONCALL_SHIFT);
const isDeleting = useIsLoading(ActionKey.DELETE_ONCALL_SHIFT);
const isSubmitting = isCreating || isUpdating || isDeleting;
const [isOpen, setIsOpen] = useState<boolean>(false);
const [errors, setErrors] = useState<{ [key: string]: string[] }>({});
@ -197,7 +203,7 @@ export const ScheduleOverrideForm: FC<RotationFormProps> = (props) => {
const isFormValid = useMemo(() => !Object.keys(errors).length, [errors]);
const ended = shift && shift.until && getDateTime(shift.until).isBefore(dayjs());
const disabled = ended;
const disabled = ended || isSubmitting;
return (
<Modal
@ -230,7 +236,13 @@ export const ScheduleOverrideForm: FC<RotationFormProps> = (props) => {
<Stack>
{shiftId !== 'new' && (
<WithConfirm title="Are you sure you want to delete override?">
<IconButton variant="secondary" tooltip="Delete" name="trash-alt" onClick={handleDeleteClick} />
<IconButton
variant="secondary"
tooltip="Delete"
name="trash-alt"
onClick={handleDeleteClick}
disabled={isSubmitting}
/>
</WithConfirm>
)}
<IconButton aria-label="Drag" variant="secondary" className="drag-handler" name="draggabledots" />

View file

@ -2,9 +2,11 @@ import React, { ChangeEvent, useCallback, useState } from 'react';
import { Stack, Modal as GrafanaModal, Button, InlineSwitch, useStyles2 } from '@grafana/ui';
import { StackSize } from 'helpers/consts';
import { useIsLoading } from 'helpers/hooks';
import { Text } from 'components/Text/Text';
import { getRotationFormStyles } from 'containers/RotationForm/RotationForm.styles';
import { ActionKey } from 'models/loader/action-keys';
interface DeletionModalProps {
onHide: () => void;
@ -13,6 +15,7 @@ interface DeletionModalProps {
export const DeletionModal = ({ onHide, onConfirm }: DeletionModalProps) => {
const [isForceDelete, setIsForceDelete] = useState<boolean>(false);
const isDeleting = useIsLoading(ActionKey.DELETE_ONCALL_SHIFT);
const styles = useStyles2(getRotationFormStyles);
@ -46,7 +49,7 @@ export const DeletionModal = ({ onHide, onConfirm }: DeletionModalProps) => {
<Button variant="secondary" onClick={onHide}>
Cancel
</Button>
<Button variant="destructive" onClick={handleConfirmClick}>
<Button variant="destructive" onClick={handleConfirmClick} disabled={isDeleting}>
Delete
</Button>
</Stack>

View file

@ -25,6 +25,10 @@ export enum TimeLineRealm {
export interface TimeLineItem {
action: string;
author: ApiSchemas['User'] | null;
escalation_chain: TimelineLink | null;
incident: DeclaredIncident | null;
schedule: TimelineLink | null;
webhook: TimelineLink | null;
created_at: string;
realm: TimeLineRealm;
time: string;
@ -55,3 +59,13 @@ interface RenderForWeb {
image_url: string;
source_link: string;
}
interface DeclaredIncident {
incident_link: string;
incident_title: string;
}
interface TimelineLink {
pk: string;
title: string;
}

View file

@ -6,6 +6,7 @@ import { EscalationPolicy } from 'models/escalation_policy/escalation_policy.typ
import { makeRequest } from 'network/network';
import { move } from 'state/helpers';
import { RootStore } from 'state/rootStore';
import { SelectOption } from 'state/types';
export class EscalationPolicyStore extends BaseStore {
@observable.shallow
@ -19,6 +20,9 @@ export class EscalationPolicyStore extends BaseStore {
@observable
escalationChoices: any = [];
@observable
severityChoices: SelectOption[] = [];
@observable
webEscalationChoices: any = [];
@ -30,6 +34,15 @@ export class EscalationPolicyStore extends BaseStore {
this.path = '/escalation_policies/';
}
@action.bound
async updateSeverityOptions() {
const response = await makeRequest<SelectOption[]>('/escalation_policies/severity_options/', {});
runInAction(() => {
this.severityChoices = response;
});
}
@action.bound
async updateWebEscalationPolicyOptions() {
const response = await makeRequest('/escalation_policies/escalation_options/', {});

View file

@ -24,6 +24,7 @@ export interface EscalationPolicy {
important: boolean | null;
num_alerts_in_window: number;
num_minutes_in_window: number;
severity: string | null;
}
export interface EscalationPolicyOption {

View file

@ -21,4 +21,7 @@ export enum ActionKey {
FETCH_INTEGRATIONS_AVAILABLE_FOR_CONNECTION = 'FETCH_INTEGRATIONS_AVAILABLE_FOR_CONNECTION',
FETCH_WEBHOOKS = 'FETCH_WEBHOOKS',
TRIGGER_MANUAL_WEBHOOK = 'TRIGGER_MANUAL_WEBHOOK',
CREATE_ONCALL_SHIFT = 'CREATE_ONCALL_SHIFT',
UPDATE_ONCALL_SHIFT = 'UPDATE_ONCALL_SHIFT',
DELETE_ONCALL_SHIFT = 'DELETE_ONCALL_SHIFT',
}

View file

@ -263,6 +263,7 @@ export class ScheduleStore extends BaseStore {
// ------- NEW SCHEDULES API ENDPOINTS ---------
@action.bound
@AutoLoadingState(ActionKey.CREATE_ONCALL_SHIFT)
async createRotation(scheduleId: Schedule['id'], isOverride: boolean, params: Partial<Shift>) {
const type = isOverride ? 3 : 2;
@ -364,6 +365,7 @@ export class ScheduleStore extends BaseStore {
}
@action.bound
@AutoLoadingState(ActionKey.UPDATE_ONCALL_SHIFT)
async updateRotation(shiftId: Shift['id'], params: Partial<Shift>) {
const response = await makeRequest(`/oncall_shifts/${shiftId}`, {
params: { force: true },
@ -382,6 +384,7 @@ export class ScheduleStore extends BaseStore {
}
@action.bound
@AutoLoadingState(ActionKey.UPDATE_ONCALL_SHIFT)
async updateRotationAsNew(shiftId: Shift['id'], params: Partial<Shift>) {
const response = await makeRequest(`/oncall_shifts/${shiftId}`, {
data: { ...params },
@ -489,6 +492,7 @@ export class ScheduleStore extends BaseStore {
return response;
}
@AutoLoadingState(ActionKey.DELETE_ONCALL_SHIFT)
async deleteOncallShift(shiftId: Shift['id'], force?: boolean) {
try {
return await makeRequest(`/oncall_shifts/${shiftId}`, {

View file

@ -661,6 +661,30 @@ class _IncidentPage extends React.Component<IncidentPageProps, IncidentPageState
<Text underline>{entity.author?.username}</Text>
</a>
);
case 'escalation_chain':
return (
<a href={`${PLUGIN_ROOT}/escalations/${entity.escalation_chain?.pk}`} target="_blank" rel="noopener noreferrer">
<Text underline>{entity.escalation_chain?.title}</Text>
</a>
);
case 'related_incident':
return (
<a href={entity.incident?.incident_link} target="_blank" rel="noopener noreferrer">
<Text underline>{entity.incident?.incident_title}</Text>
</a>
);
case 'schedule':
return (
<a href={`${PLUGIN_ROOT}/schedules/${entity.schedule?.pk}`} target="_blank" rel="noopener noreferrer">
<Text underline>{entity.schedule?.title}</Text>
</a>
);
case 'webhook':
return (
<a href={`${PLUGIN_ROOT}/outgoing_webhooks/${entity.webhook?.pk}`} target="_blank" rel="noopener noreferrer">
<Text underline>{entity.webhook?.title}</Text>
</a>
);
default:
return '{{' + match + '}}';
}

View file

@ -2,4 +2,4 @@
export const GIT_COMMIT = 'dev';
// Declare a constant that will be updated by release-please action
export const CURRENT_VERSION = '1.9.27' as string; // x-release-please-version
export const CURRENT_VERSION = '1.9.28' as string; // x-release-please-version