mirror of https://github.com/grafana/grafana
Alerting: Update silences creation to support `__alert_rule_uid__` and move into drawer (#87320)
parent
e7d5622969
commit
9e29c215c3
@ -0,0 +1,52 @@ |
||||
import React from 'react'; |
||||
|
||||
import { Divider, Drawer, Stack } from '@grafana/ui'; |
||||
import { AlertManagerPicker } from 'app/features/alerting/unified/components/AlertManagerPicker'; |
||||
import { GrafanaAlertmanagerDeliveryWarning } from 'app/features/alerting/unified/components/GrafanaAlertmanagerDeliveryWarning'; |
||||
import { SilencesEditor } from 'app/features/alerting/unified/components/silences/SilencesEditor'; |
||||
import { getDefaultSilenceFormValues } from 'app/features/alerting/unified/components/silences/utils'; |
||||
import { useAlertmanager } from 'app/features/alerting/unified/state/AlertmanagerContext'; |
||||
import { RulerGrafanaRuleDTO } from 'app/types/unified-alerting-dto'; |
||||
|
||||
type Props = { |
||||
rulerRule: RulerGrafanaRuleDTO; |
||||
onClose: () => void; |
||||
}; |
||||
|
||||
/** |
||||
* For a given Grafana managed rule, renders a drawer containing silences editor and Alertmanager selection |
||||
*/ |
||||
const SilenceGrafanaRuleDrawer = ({ rulerRule, onClose }: Props) => { |
||||
const { uid } = rulerRule.grafana_alert; |
||||
|
||||
const formValues = getDefaultSilenceFormValues(); |
||||
const { selectedAlertmanager } = useAlertmanager(); |
||||
|
||||
return ( |
||||
<Drawer |
||||
title="Silence alert rule" |
||||
subtitle="Configure silences to stop notifications from a particular alert rule." |
||||
onClose={onClose} |
||||
size="md" |
||||
> |
||||
<Stack direction={'column'}> |
||||
<GrafanaAlertmanagerDeliveryWarning currentAlertmanager={selectedAlertmanager!} /> |
||||
|
||||
<div> |
||||
<AlertManagerPicker showOnlyReceivingGrafanaAlerts /> |
||||
<Divider /> |
||||
</div> |
||||
|
||||
<SilencesEditor |
||||
ruleUid={uid} |
||||
formValues={formValues} |
||||
alertManagerSourceName={selectedAlertmanager!} |
||||
onSilenceCreated={onClose} |
||||
onCancel={onClose} |
||||
/> |
||||
</Stack> |
||||
</Drawer> |
||||
); |
||||
}; |
||||
|
||||
export default SilenceGrafanaRuleDrawer; |
@ -0,0 +1,77 @@ |
||||
import { DefaultTimeZone, addDurationToDate, dateTime, intervalToAbbreviatedDurationString } from '@grafana/data'; |
||||
import { config } from '@grafana/runtime'; |
||||
import { SilenceFormFields } from 'app/features/alerting/unified/types/silence-form'; |
||||
import { matcherToMatcherField } from 'app/features/alerting/unified/utils/alertmanager'; |
||||
import { parseQueryParamMatchers } from 'app/features/alerting/unified/utils/matchers'; |
||||
import { MatcherOperator, Silence } from 'app/plugins/datasource/alertmanager/types'; |
||||
|
||||
/** |
||||
* Parse query params and return default silence form values |
||||
*/ |
||||
export const defaultsFromQuery = (searchParams: URLSearchParams): Partial<SilenceFormFields> => { |
||||
const defaults: Partial<SilenceFormFields> = {}; |
||||
|
||||
const comment = searchParams.get('comment'); |
||||
const matchers = searchParams.getAll('matcher'); |
||||
|
||||
const formMatchers = parseQueryParamMatchers(matchers); |
||||
if (formMatchers.length) { |
||||
defaults.matchers = formMatchers.map(matcherToMatcherField); |
||||
} |
||||
|
||||
if (comment) { |
||||
defaults.comment = comment; |
||||
} |
||||
|
||||
return defaults; |
||||
}; |
||||
|
||||
/** |
||||
* |
||||
*/ |
||||
export const getFormFieldsForSilence = (silence: Silence): SilenceFormFields => { |
||||
const now = new Date(); |
||||
const isExpired = Date.parse(silence.endsAt) < Date.now(); |
||||
const interval = isExpired |
||||
? { |
||||
start: now, |
||||
end: addDurationToDate(now, { hours: 2 }), |
||||
} |
||||
: { start: new Date(silence.startsAt), end: new Date(silence.endsAt) }; |
||||
return { |
||||
id: silence.id, |
||||
startsAt: interval.start.toISOString(), |
||||
endsAt: interval.end.toISOString(), |
||||
comment: silence.comment, |
||||
createdBy: silence.createdBy, |
||||
duration: intervalToAbbreviatedDurationString(interval), |
||||
isRegex: false, |
||||
matchers: silence.matchers?.map(matcherToMatcherField) || [], |
||||
matcherName: '', |
||||
matcherValue: '', |
||||
timeZone: DefaultTimeZone, |
||||
}; |
||||
}; |
||||
|
||||
/** |
||||
* Generate default silence form values |
||||
*/ |
||||
export const getDefaultSilenceFormValues = (partial?: Partial<SilenceFormFields>): SilenceFormFields => { |
||||
const now = new Date(); |
||||
|
||||
const endsAt = addDurationToDate(now, { hours: 2 }); // Default time period is now + 2h
|
||||
return { |
||||
id: '', |
||||
startsAt: now.toISOString(), |
||||
endsAt: endsAt.toISOString(), |
||||
comment: `created ${dateTime().format('YYYY-MM-DD HH:mm')}`, |
||||
createdBy: config.bootData.user.name, |
||||
duration: '2h', |
||||
isRegex: false, |
||||
matcherName: '', |
||||
matcherValue: '', |
||||
timeZone: DefaultTimeZone, |
||||
matchers: [{ name: '', value: '', operator: MatcherOperator.equal }], |
||||
...partial, |
||||
}; |
||||
}; |
@ -0,0 +1,16 @@ |
||||
import { http, HttpResponse } from 'msw'; |
||||
|
||||
export const MOCK_GRAFANA_ALERT_RULE_TITLE = 'Test alert'; |
||||
|
||||
const alertRuleDetailsHandler = () => |
||||
http.get<{ folderUid: string }>(`/api/ruler/:ruler/api/v1/rule/:uid`, () => { |
||||
// TODO: Scaffold out alert rule response logic as this endpoint is used more in tests
|
||||
return HttpResponse.json({ |
||||
grafana_alert: { |
||||
title: MOCK_GRAFANA_ALERT_RULE_TITLE, |
||||
}, |
||||
}); |
||||
}); |
||||
|
||||
const handlers = [alertRuleDetailsHandler()]; |
||||
export default handlers; |
@ -1,15 +1,31 @@ |
||||
import { http, HttpResponse } from 'msw'; |
||||
|
||||
import { PluginMeta } from '@grafana/data'; |
||||
import { config } from '@grafana/runtime'; |
||||
import { plugins } from 'app/features/alerting/unified/testSetup/plugins'; |
||||
|
||||
export const getPluginsHandler = (pluginsArray: PluginMeta[] = plugins) => |
||||
http.get<{ pluginId: string }>(`/api/plugins/:pluginId/settings`, ({ params: { pluginId } }) => { |
||||
/** |
||||
* Returns a handler that maps from plugin ID to PluginMeta, and additionally sets up necessary |
||||
* config side effects that are expected to come along with this API behaviour |
||||
*/ |
||||
export const getPluginsHandler = (pluginsArray: PluginMeta[] = plugins) => { |
||||
plugins.forEach(({ id, baseUrl, info, angular }) => { |
||||
config.apps[id] = { |
||||
id, |
||||
path: baseUrl, |
||||
preload: true, |
||||
version: info.version, |
||||
angular: angular ?? { detected: false, hideDeprecation: false }, |
||||
}; |
||||
}); |
||||
|
||||
return http.get<{ pluginId: string }>(`/api/plugins/:pluginId/settings`, ({ params: { pluginId } }) => { |
||||
const matchingPlugin = pluginsArray.find((plugin) => plugin.id === pluginId); |
||||
return matchingPlugin |
||||
? HttpResponse.json<PluginMeta>(matchingPlugin) |
||||
: HttpResponse.json({ message: 'Plugin not found, no installed plugin with that id' }, { status: 404 }); |
||||
}); |
||||
}; |
||||
|
||||
const handlers = [getPluginsHandler()]; |
||||
export default handlers; |
||||
|
Loading…
Reference in new issue