Allow custom silence period for alert groups + display how much left is for a silenced AG (#4375)
# What this PR does https://github.com/grafana/oncall/issues/2333 - Allow custom input for silence period (either by duration or by setting a future date in the calendar, both inputs are syncedd/editable) - Show how much time is left for a silenced alert group ## Which issue(s) this PR closes Closes https://github.com/grafana/oncall/issues/2333
This commit is contained in:
parent
f061d26a7d
commit
dfea60e736
9 changed files with 300 additions and 86 deletions
|
|
@ -2,9 +2,20 @@ import React, { FC, ReactNode } from 'react';
|
|||
|
||||
interface RenderConditionallyProps {
|
||||
shouldRender?: boolean;
|
||||
children: ReactNode;
|
||||
children?: ReactNode;
|
||||
render?: () => ReactNode;
|
||||
backupChildren?: ReactNode;
|
||||
}
|
||||
|
||||
export const RenderConditionally: FC<RenderConditionallyProps> = ({ shouldRender, children, backupChildren = null }) =>
|
||||
shouldRender ? <>{children}</> : <>{backupChildren}</>;
|
||||
export const RenderConditionally: FC<RenderConditionallyProps> = ({
|
||||
shouldRender,
|
||||
children,
|
||||
render,
|
||||
backupChildren = null,
|
||||
}) => {
|
||||
if (render) {
|
||||
return shouldRender ? <>{render()}</> : <>{backupChildren}</>;
|
||||
}
|
||||
|
||||
return shouldRender ? <>{children}</> : <>{backupChildren}</>;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ export class AlertGroupStore {
|
|||
rootStore: RootStore;
|
||||
alerts = new Map<string, ApiSchemas['AlertGroup']>();
|
||||
bulkActions: any = [];
|
||||
silenceOptions: any;
|
||||
silenceOptions: Array<ApiSchemas['AlertGroupSilenceOptions']>;
|
||||
searchResult: { [key: string]: Array<ApiSchemas['AlertGroup']['pk']> } = {};
|
||||
incidentFilters: any;
|
||||
initialQuery = qs.parse(window.location.search);
|
||||
|
|
|
|||
|
|
@ -36,6 +36,7 @@ import {
|
|||
initErrorDataState,
|
||||
} from 'components/PageErrorHandlingWrapper/PageErrorHandlingWrapper.helpers';
|
||||
import { PluginLink } from 'components/PluginLink/PluginLink';
|
||||
import { RenderConditionally } from 'components/RenderConditionally/RenderConditionally';
|
||||
import { SourceCode } from 'components/SourceCode/SourceCode';
|
||||
import { Text } from 'components/Text/Text';
|
||||
import { TooltipBadge } from 'components/TooltipBadge/TooltipBadge';
|
||||
|
|
@ -49,7 +50,8 @@ import { AlertGroupHelper } from 'models/alertgroup/alertgroup.helpers';
|
|||
import { AlertAction, TimeLineItem, TimeLineRealm, GroupedAlert } from 'models/alertgroup/alertgroup.types';
|
||||
import { ResolutionNoteSourceTypesToDisplayName } from 'models/resolution_note/resolution_note.types';
|
||||
import { ApiSchemas } from 'network/oncall-api/api.types';
|
||||
import { IncidentDropdown } from 'pages/incidents/parts/IncidentDropdown';
|
||||
import { CUSTOM_SILENCE_VALUE, IncidentDropdown } from 'pages/incidents/parts/IncidentDropdown';
|
||||
import { IncidentSilenceModal } from 'pages/incidents/parts/IncidentSilenceModal';
|
||||
import { AppFeature } from 'state/features';
|
||||
import { PageProps, WithStoreProps } from 'state/types';
|
||||
import { useStore } from 'state/useStore';
|
||||
|
|
@ -73,6 +75,7 @@ interface IncidentPageState extends PageBaseState {
|
|||
showAttachIncidentForm?: boolean;
|
||||
timelineFilter: string;
|
||||
resolutionNoteText: string;
|
||||
silenceModalData: { incident: ApiSchemas['AlertGroup'] };
|
||||
}
|
||||
|
||||
@observer
|
||||
|
|
@ -81,6 +84,7 @@ class _IncidentPage extends React.Component<IncidentPageProps, IncidentPageState
|
|||
timelineFilter: 'all',
|
||||
resolutionNoteText: '',
|
||||
errorData: initErrorDataState(),
|
||||
silenceModalData: undefined,
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
|
|
@ -127,7 +131,7 @@ class _IncidentPage extends React.Component<IncidentPageProps, IncidentPageState
|
|||
},
|
||||
} = this.props;
|
||||
|
||||
const { errorData, showIntegrationSettings, showAttachIncidentForm } = this.state;
|
||||
const { errorData, showIntegrationSettings, showAttachIncidentForm, silenceModalData } = this.state;
|
||||
const { isNotFoundError, isWrongTeamError, isUnknownError } = errorData;
|
||||
const { alerts } = store.alertGroupStore;
|
||||
|
||||
|
|
@ -143,7 +147,7 @@ class _IncidentPage extends React.Component<IncidentPageProps, IncidentPageState
|
|||
onUnacknowledge: this.getOnActionButtonClick(id, AlertAction.unAcknowledge),
|
||||
onUnresolve: this.getOnActionButtonClick(id, AlertAction.unResolve),
|
||||
onAcknowledge: this.getOnActionButtonClick(id, AlertAction.Acknowledge),
|
||||
onSilence: this.getSilenceClickHandler(id),
|
||||
onSilence: this.getSilenceClickHandler(incident),
|
||||
onUnsilence: this.getUnsilenceClickHandler(id),
|
||||
},
|
||||
true
|
||||
|
|
@ -241,6 +245,23 @@ class _IncidentPage extends React.Component<IncidentPageProps, IncidentPageState
|
|||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Modal where users can input their custom duration for silencing an alert group */}
|
||||
<RenderConditionally
|
||||
shouldRender={Boolean(silenceModalData?.incident)}
|
||||
render={() => (
|
||||
<IncidentSilenceModal
|
||||
alertGroupID={silenceModalData.incident.pk}
|
||||
alertGroupName={silenceModalData.incident.render_for_web?.title}
|
||||
isOpen
|
||||
onDismiss={() => this.setState({ silenceModalData: undefined })}
|
||||
onSave={(duration: number) => {
|
||||
this.setState({ silenceModalData: undefined });
|
||||
store.alertGroupStore.doIncidentAction(silenceModalData.incident.pk, AlertAction.Silence, duration);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</PageErrorHandlingWrapper>
|
||||
|
|
@ -342,7 +363,7 @@ class _IncidentPage extends React.Component<IncidentPageProps, IncidentPageState
|
|||
onUnacknowledge={this.getOnActionButtonClick(incident.pk, AlertAction.unAcknowledge)}
|
||||
onUnresolve={this.getOnActionButtonClick(incident.pk, AlertAction.unResolve)}
|
||||
onAcknowledge={this.getOnActionButtonClick(incident.pk, AlertAction.Acknowledge)}
|
||||
onSilence={this.getSilenceClickHandler(incident.pk)}
|
||||
onSilence={this.getSilenceClickHandler(incident)}
|
||||
onUnsilence={this.getUnsilenceClickHandler(incident.pk)}
|
||||
/>
|
||||
</div>
|
||||
|
|
@ -437,7 +458,7 @@ class _IncidentPage extends React.Component<IncidentPageProps, IncidentPageState
|
|||
onUnacknowledge: this.getOnActionButtonClick(incident.pk, AlertAction.unAcknowledge),
|
||||
onUnresolve: this.getOnActionButtonClick(incident.pk, AlertAction.unResolve),
|
||||
onAcknowledge: this.getOnActionButtonClick(incident.pk, AlertAction.Acknowledge),
|
||||
onSilence: this.getSilenceClickHandler(incident.pk),
|
||||
onSilence: this.getSilenceClickHandler(incident),
|
||||
onUnsilence: this.getUnsilenceClickHandler(incident.pk),
|
||||
})}
|
||||
<ExtensionLinkDropdown
|
||||
|
|
@ -632,11 +653,15 @@ class _IncidentPage extends React.Component<IncidentPageProps, IncidentPageState
|
|||
};
|
||||
};
|
||||
|
||||
getSilenceClickHandler = (incidentId: ApiSchemas['AlertGroup']['pk']) => {
|
||||
getSilenceClickHandler = (incident: ApiSchemas['AlertGroup']) => {
|
||||
const { store } = this.props;
|
||||
|
||||
return (value: number) => {
|
||||
return store.alertGroupStore.doIncidentAction(incidentId, AlertAction.Silence, value);
|
||||
return (value: number): Promise<void> => {
|
||||
if (value === CUSTOM_SILENCE_VALUE) {
|
||||
this.setState({ silenceModalData: { incident } });
|
||||
return Promise.resolve(); // awaited by other component
|
||||
}
|
||||
return store.alertGroupStore.doIncidentAction(incident.pk, AlertAction.Silence, value);
|
||||
};
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -184,6 +184,7 @@ class _IncidentsPage extends React.Component<IncidentsPageProps, IncidentsPageSt
|
|||
{this.renderIncidentFilters()}
|
||||
{this.renderTable()}
|
||||
</div>
|
||||
|
||||
{showAddAlertGroupForm && (
|
||||
<ManualAlertGroup
|
||||
onHide={() => {
|
||||
|
|
|
|||
|
|
@ -1,7 +1,8 @@
|
|||
import React, { FC, SyntheticEvent, useRef, useState } from 'react';
|
||||
|
||||
import { cx } from '@emotion/css';
|
||||
import { Icon, LoadingPlaceholder, useStyles2 } from '@grafana/ui';
|
||||
import { intervalToAbbreviatedDurationString } from '@grafana/data';
|
||||
import { Icon, LoadingPlaceholder, Tooltip, useStyles2 } from '@grafana/ui';
|
||||
import { getUtilStyles } from 'styles/utils.styles';
|
||||
|
||||
import { Tag, TagColor } from 'components/Tag/Tag';
|
||||
|
|
@ -13,6 +14,7 @@ import { ApiSchemas } from 'network/oncall-api/api.types';
|
|||
import { UserActions } from 'utils/authorization/authorization';
|
||||
|
||||
import { getIncidentDropdownStyles } from './IncidentDropdown.styles';
|
||||
import { IncidentSilenceModal } from './IncidentSilenceModal';
|
||||
import { SilenceSelect } from './SilenceSelect';
|
||||
|
||||
const getIncidentTagColor = (alert: ApiSchemas['AlertGroup']) => {
|
||||
|
|
@ -55,6 +57,8 @@ function IncidentStatusTag({
|
|||
);
|
||||
}
|
||||
|
||||
export const CUSTOM_SILENCE_VALUE = -100;
|
||||
|
||||
export const IncidentDropdown: FC<{
|
||||
alert: ApiSchemas['AlertGroup'];
|
||||
onResolve: (e: SyntheticEvent) => Promise<void>;
|
||||
|
|
@ -67,6 +71,7 @@ export const IncidentDropdown: FC<{
|
|||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [currentLoadingAction, setCurrentActionLoading] = useState<IncidentStatus>(undefined);
|
||||
const [forcedOpenAction, setForcedOpenAction] = useState<string>(undefined);
|
||||
const [isSilenceModalOpen, setIsSilenceModalOpen] = useState(false);
|
||||
|
||||
const styles = useStyles2(getIncidentDropdownStyles);
|
||||
const utilStyles = useStyles2(getUtilStyles);
|
||||
|
|
@ -160,60 +165,79 @@ export const IncidentDropdown: FC<{
|
|||
|
||||
if (alert.status === IncidentStatus.Firing) {
|
||||
return (
|
||||
<WithContextMenu
|
||||
forceIsOpen={forcedOpenAction === AlertAction.unResolve}
|
||||
renderMenuItems={() => (
|
||||
<div className={cx(styles.incidentOptions, { [utilStyles.disabled]: isLoading })}>
|
||||
<WithPermissionControlTooltip userAction={UserActions.AlertGroupsWrite}>
|
||||
<div
|
||||
className={cx(styles.incidentOptionItem)}
|
||||
onClick={(e) => onClickFn(e, AlertAction.unResolve, onAcknowledge, IncidentStatus.Acknowledged)}
|
||||
>
|
||||
Acknowledge{' '}
|
||||
{currentLoadingAction === IncidentStatus.Acknowledged && isLoading && (
|
||||
<span className={cx(styles.incidentOptionEl)}>
|
||||
<LoadingPlaceholder text="" />
|
||||
</span>
|
||||
)}
|
||||
<>
|
||||
<WithContextMenu
|
||||
forceIsOpen={forcedOpenAction === AlertAction.unResolve}
|
||||
renderMenuItems={() => (
|
||||
<div className={cx(styles.incidentOptions, { [utilStyles.disabled]: isLoading })}>
|
||||
<WithPermissionControlTooltip userAction={UserActions.AlertGroupsWrite}>
|
||||
<div
|
||||
className={cx(styles.incidentOptionItem)}
|
||||
onClick={(e) => onClickFn(e, AlertAction.unResolve, onAcknowledge, IncidentStatus.Acknowledged)}
|
||||
>
|
||||
Acknowledge{' '}
|
||||
{currentLoadingAction === IncidentStatus.Acknowledged && isLoading && (
|
||||
<span className={cx(styles.incidentOptionEl)}>
|
||||
<LoadingPlaceholder text="" />
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</WithPermissionControlTooltip>
|
||||
<WithPermissionControlTooltip userAction={UserActions.AlertGroupsWrite}>
|
||||
<div
|
||||
className={cx(styles.incidentOptionItem)}
|
||||
onClick={(e) => onClickFn(e, AlertAction.unResolve, onResolve, IncidentStatus.Resolved)}
|
||||
>
|
||||
Resolve{' '}
|
||||
{currentLoadingAction === IncidentStatus.Resolved && isLoading && (
|
||||
<span className={cx(styles.incidentOptionEl)}>
|
||||
<LoadingPlaceholder text="" />
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</WithPermissionControlTooltip>
|
||||
|
||||
<div className={cx(styles.incidentOptionItem)}>
|
||||
<SilenceSelect
|
||||
customValueNum={CUSTOM_SILENCE_VALUE}
|
||||
placeholder={
|
||||
currentLoadingAction === IncidentStatus.Silenced && isLoading ? 'Loading...' : 'Silence for'
|
||||
}
|
||||
onSelect={async (value) => {
|
||||
if (value === CUSTOM_SILENCE_VALUE) {
|
||||
return setIsSilenceModalOpen(true);
|
||||
}
|
||||
|
||||
setIsLoading(true);
|
||||
setForcedOpenAction(AlertAction.unResolve);
|
||||
setCurrentActionLoading(IncidentStatus.Silenced);
|
||||
|
||||
await onSilence(value);
|
||||
|
||||
setIsLoading(false);
|
||||
setForcedOpenAction(undefined);
|
||||
setCurrentActionLoading(undefined);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</WithPermissionControlTooltip>
|
||||
<WithPermissionControlTooltip userAction={UserActions.AlertGroupsWrite}>
|
||||
<div
|
||||
className={cx(styles.incidentOptionItem)}
|
||||
onClick={(e) => onClickFn(e, AlertAction.unResolve, onResolve, IncidentStatus.Resolved)}
|
||||
>
|
||||
Resolve{' '}
|
||||
{currentLoadingAction === IncidentStatus.Resolved && isLoading && (
|
||||
<span className={cx(styles.incidentOptionEl)}>
|
||||
<LoadingPlaceholder text="" />
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</WithPermissionControlTooltip>
|
||||
|
||||
<div className={cx(styles.incidentOptionItem)}>
|
||||
<SilenceSelect
|
||||
placeholder={
|
||||
currentLoadingAction === IncidentStatus.Silenced && isLoading ? 'Loading...' : 'Silence for'
|
||||
}
|
||||
onSelect={async (value) => {
|
||||
setIsLoading(true);
|
||||
setForcedOpenAction(AlertAction.unResolve);
|
||||
setCurrentActionLoading(IncidentStatus.Silenced);
|
||||
|
||||
await onSilence(value);
|
||||
|
||||
setIsLoading(false);
|
||||
setForcedOpenAction(undefined);
|
||||
setCurrentActionLoading(undefined);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
>
|
||||
{({ openMenu }) => <IncidentStatusTag alert={alert} openMenu={openMenu} />}
|
||||
</WithContextMenu>
|
||||
)}
|
||||
>
|
||||
{({ openMenu }) => <IncidentStatusTag alert={alert} openMenu={openMenu} />}
|
||||
</WithContextMenu>
|
||||
<IncidentSilenceModal
|
||||
alertGroupID={alert.pk}
|
||||
alertGroupName={alert.render_for_web?.title}
|
||||
isOpen={isSilenceModalOpen}
|
||||
onDismiss={() => setIsSilenceModalOpen(false)}
|
||||
onSave={async (value) => {
|
||||
setIsSilenceModalOpen(false);
|
||||
setIsLoading(true);
|
||||
await onSilence(value);
|
||||
setIsLoading(false);
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -265,7 +289,27 @@ export const IncidentDropdown: FC<{
|
|||
</div>
|
||||
)}
|
||||
>
|
||||
{({ openMenu }) => <IncidentStatusTag alert={alert} openMenu={openMenu} />}
|
||||
{({ openMenu }) => (
|
||||
<Tooltip content={getSilencedTooltip(alert)} placement={'bottom'}>
|
||||
<span>
|
||||
<IncidentStatusTag alert={alert} openMenu={openMenu} />
|
||||
</span>
|
||||
</Tooltip>
|
||||
)}
|
||||
</WithContextMenu>
|
||||
);
|
||||
};
|
||||
|
||||
function getSilencedTooltip(alert: ApiSchemas['AlertGroup']) {
|
||||
if (alert.silenced_until === null) {
|
||||
return `Silenced forever`;
|
||||
}
|
||||
return `Silence ends in ${getSilencedUntilInDuration(alert.silenced_until)}`;
|
||||
}
|
||||
|
||||
function getSilencedUntilInDuration(date: string) {
|
||||
return intervalToAbbreviatedDurationString({
|
||||
start: new Date(),
|
||||
end: new Date(date),
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,111 @@
|
|||
import React, { useState } from 'react';
|
||||
|
||||
import { css } from '@emotion/css';
|
||||
import {
|
||||
DateTime,
|
||||
addDurationToDate,
|
||||
dateTime,
|
||||
durationToMilliseconds,
|
||||
intervalToAbbreviatedDurationString,
|
||||
isValidDuration,
|
||||
parseDuration,
|
||||
} from '@grafana/data';
|
||||
import { Button, DateTimePicker, Field, HorizontalGroup, Input, Modal, useStyles2 } from '@grafana/ui';
|
||||
|
||||
import { useDebouncedCallback } from 'utils/hooks';
|
||||
|
||||
interface IncidentSilenceModalProps {
|
||||
isOpen: boolean;
|
||||
alertGroupID: string;
|
||||
alertGroupName: string;
|
||||
|
||||
onDismiss: () => void;
|
||||
onSave: (value: number) => void;
|
||||
}
|
||||
|
||||
const IncidentSilenceModal: React.FC<IncidentSilenceModalProps> = ({
|
||||
isOpen,
|
||||
alertGroupID,
|
||||
alertGroupName,
|
||||
|
||||
onDismiss,
|
||||
onSave,
|
||||
}) => {
|
||||
const [date, setDate] = useState<DateTime>(dateTime('2021-05-05 12:00:00'));
|
||||
const [duration, setDuration] = useState<string>('');
|
||||
const debouncedUpdateDateTime = useDebouncedCallback(updateDateTime, 500);
|
||||
|
||||
const styles = useStyles2(getStyles);
|
||||
const isDurationValid = isValidDuration(duration);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
onDismiss={onDismiss}
|
||||
closeOnBackdropClick={false}
|
||||
isOpen={isOpen}
|
||||
title={`Silence alert group #${alertGroupID} ${alertGroupName}`}
|
||||
className={styles.root}
|
||||
>
|
||||
<div className={styles.container}>
|
||||
<Field key={'SilencePicker'} label={'Silence End'} className={styles.containerChild}>
|
||||
<DateTimePicker label="Date" date={date} onChange={onDateChange} minDate={new Date()} />
|
||||
</Field>
|
||||
|
||||
<Field key={'Duration'} label={'Duration'} className={styles.containerChild} invalid={!isDurationValid}>
|
||||
<Input value={duration} onChange={onDurationChange} placeholder="Enter duration (2h 30m)" />
|
||||
</Field>
|
||||
</div>
|
||||
|
||||
<HorizontalGroup justify="flex-end">
|
||||
<Button variant={'secondary'} onClick={onDismiss}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button variant={'primary'} onClick={onSubmit} disabled={!isDurationValid}>
|
||||
Add
|
||||
</Button>
|
||||
</HorizontalGroup>
|
||||
</Modal>
|
||||
);
|
||||
|
||||
function onDateChange(date: DateTime) {
|
||||
setDate(date);
|
||||
const duration = intervalToAbbreviatedDurationString({
|
||||
start: new Date(),
|
||||
end: new Date(date.toDate()),
|
||||
});
|
||||
setDuration(duration);
|
||||
}
|
||||
|
||||
function onDurationChange(event: React.SyntheticEvent<HTMLInputElement>) {
|
||||
const newDuration = event.currentTarget.value;
|
||||
if (newDuration !== duration) {
|
||||
setDuration(newDuration);
|
||||
debouncedUpdateDateTime(newDuration);
|
||||
}
|
||||
}
|
||||
|
||||
function updateDateTime(newDuration: string) {
|
||||
setDate(dateTime(addDurationToDate(new Date(), parseDuration(newDuration))));
|
||||
}
|
||||
|
||||
function onSubmit() {
|
||||
onSave(durationToMilliseconds(parseDuration(duration)) / 1000);
|
||||
}
|
||||
};
|
||||
|
||||
const getStyles = () => ({
|
||||
root: css`
|
||||
width: 600px;
|
||||
`,
|
||||
|
||||
container: css`
|
||||
width: 100%;
|
||||
display: flex;
|
||||
column-gap: 16px;
|
||||
`,
|
||||
containerChild: css`
|
||||
flex-grow: 1;
|
||||
`,
|
||||
});
|
||||
|
||||
export { IncidentSilenceModal };
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
import React from 'react';
|
||||
|
||||
import { ButtonCascader, ComponentSize } from '@grafana/ui';
|
||||
import { ButtonCascader, CascaderOption, ComponentSize } from '@grafana/ui';
|
||||
import { observer } from 'mobx-react';
|
||||
|
||||
import { WithPermissionControlTooltip } from 'containers/WithPermissionControl/WithPermissionControlTooltip';
|
||||
|
|
@ -8,6 +8,8 @@ import { SelectOption } from 'state/types';
|
|||
import { useStore } from 'state/useStore';
|
||||
import { UserActions } from 'utils/authorization/authorization';
|
||||
|
||||
import { CUSTOM_SILENCE_VALUE } from './IncidentDropdown';
|
||||
|
||||
interface SilenceButtonCascaderProps {
|
||||
className?: string;
|
||||
disabled?: boolean;
|
||||
|
|
@ -38,10 +40,15 @@ export const SilenceButtonCascader = observer((props: SilenceButtonCascaderProps
|
|||
</WithPermissionControlTooltip>
|
||||
);
|
||||
|
||||
function getOptions() {
|
||||
return silenceOptions.map((silenceOption: SelectOption) => ({
|
||||
value: silenceOption.value,
|
||||
label: silenceOption.display_name,
|
||||
}));
|
||||
function getOptions(): CascaderOption[] {
|
||||
return silenceOptions
|
||||
.map((silenceOption: SelectOption) => ({
|
||||
value: silenceOption.value,
|
||||
label: silenceOption.display_name,
|
||||
}))
|
||||
.concat({
|
||||
value: CUSTOM_SILENCE_VALUE,
|
||||
label: 'Custom',
|
||||
}) as CascaderOption[];
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -10,12 +10,13 @@ import { UserActions } from 'utils/authorization/authorization';
|
|||
|
||||
interface SilenceSelectProps {
|
||||
placeholder?: string;
|
||||
customValueNum: number;
|
||||
|
||||
onSelect: (value: number) => void;
|
||||
}
|
||||
|
||||
export const SilenceSelect = observer((props: SilenceSelectProps) => {
|
||||
const { placeholder = 'Silence for', onSelect } = props;
|
||||
const { customValueNum, placeholder = 'Silence for', onSelect } = props;
|
||||
|
||||
const store = useStore();
|
||||
|
||||
|
|
@ -24,21 +25,31 @@ export const SilenceSelect = observer((props: SilenceSelectProps) => {
|
|||
const silenceOptions = alertGroupStore.silenceOptions || [];
|
||||
|
||||
return (
|
||||
<WithPermissionControlTooltip key="silence" userAction={UserActions.AlertGroupsWrite}>
|
||||
<Select
|
||||
menuShouldPortal
|
||||
placeholder={placeholder}
|
||||
value={undefined}
|
||||
onChange={({ value }) => onSelect(Number(value))}
|
||||
options={getOptions()}
|
||||
/>
|
||||
</WithPermissionControlTooltip>
|
||||
<>
|
||||
{' '}
|
||||
<WithPermissionControlTooltip key="silence" userAction={UserActions.AlertGroupsWrite}>
|
||||
<Select
|
||||
menuShouldPortal
|
||||
placeholder={placeholder}
|
||||
value={undefined}
|
||||
onChange={({ value }) => {
|
||||
onSelect(Number(value));
|
||||
}}
|
||||
options={getOptions()}
|
||||
/>
|
||||
</WithPermissionControlTooltip>
|
||||
</>
|
||||
);
|
||||
|
||||
function getOptions() {
|
||||
return silenceOptions.map((silenceOption: SelectOption) => ({
|
||||
value: silenceOption.value,
|
||||
label: silenceOption.display_name,
|
||||
}));
|
||||
return silenceOptions
|
||||
.map((silenceOption: SelectOption) => ({
|
||||
value: silenceOption.value,
|
||||
label: silenceOption.display_name,
|
||||
}))
|
||||
.concat({
|
||||
value: customValueNum,
|
||||
label: 'Custom',
|
||||
});
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -4,6 +4,10 @@ import tinycolor from 'tinycolor2';
|
|||
|
||||
export const getUtilStyles = (theme: GrafanaTheme2) => {
|
||||
return {
|
||||
flex: css`
|
||||
display: flex;
|
||||
`,
|
||||
|
||||
width100: css`
|
||||
width: 100%;
|
||||
`,
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue