From 62c20ebd77e267314bca4861fc7caa7c049918f3 Mon Sep 17 00:00:00 2001 From: Matthew Jacobson Date: Mon, 13 Dec 2021 05:54:36 -0500 Subject: [PATCH] Alerting: Add label and state filters to alert instance components (#42550) --- .../Forms/RadioButtonGroup/RadioButton.tsx | 3 + .../RadioButtonGroup/RadioButtonGroup.tsx | 13 +++ .../components/alert-groups/MatcherFilter.tsx | 7 ++ .../rules/AlertInstanceStateFilter.tsx | 32 +++++++ .../components/rules/AlertInstancesTable.tsx | 11 +-- .../rules/RuleDetailsMatchingInstances.tsx | 88 ++++++++++++++++++- .../unified/utils/alertmanager.test.ts | 7 ++ .../alerting/unified/utils/alertmanager.ts | 2 +- .../alerting/unified/utils/misc.test.ts | 70 +++++++++++++++ .../features/alerting/unified/utils/misc.ts | 40 ++++++++- .../panel/alertlist/AlertInstances.tsx | 81 ++++++++--------- .../panel/alertlist/UnifiedAlertList.tsx | 2 +- public/app/plugins/panel/alertlist/module.tsx | 49 ++++++++++- public/app/plugins/panel/alertlist/types.ts | 6 +- 14 files changed, 349 insertions(+), 62 deletions(-) create mode 100644 public/app/features/alerting/unified/components/rules/AlertInstanceStateFilter.tsx create mode 100644 public/app/features/alerting/unified/utils/misc.test.ts diff --git a/packages/grafana-ui/src/components/Forms/RadioButtonGroup/RadioButton.tsx b/packages/grafana-ui/src/components/Forms/RadioButtonGroup/RadioButton.tsx index 2ea6a1a8525..c72688619be 100644 --- a/packages/grafana-ui/src/components/Forms/RadioButtonGroup/RadioButton.tsx +++ b/packages/grafana-ui/src/components/Forms/RadioButtonGroup/RadioButton.tsx @@ -16,6 +16,7 @@ export interface RadioButtonProps { active: boolean; id: string; onChange: () => void; + onClick: () => void; fullWidth?: boolean; 'aria-label'?: StringSelector; children?: React.ReactNode; @@ -29,6 +30,7 @@ export const RadioButton = React.forwardRef( disabled = false, size = 'md', onChange, + onClick, id, name = undefined, description, @@ -46,6 +48,7 @@ export const RadioButton = React.forwardRef( type="radio" className={styles.radio} onChange={onChange} + onClick={onClick} disabled={disabled} id={id} checked={active} diff --git a/packages/grafana-ui/src/components/Forms/RadioButtonGroup/RadioButtonGroup.tsx b/packages/grafana-ui/src/components/Forms/RadioButtonGroup/RadioButtonGroup.tsx index d54371fe95a..dacd001a8a1 100644 --- a/packages/grafana-ui/src/components/Forms/RadioButtonGroup/RadioButtonGroup.tsx +++ b/packages/grafana-ui/src/components/Forms/RadioButtonGroup/RadioButtonGroup.tsx @@ -13,6 +13,7 @@ export interface RadioButtonGroupProps { disabledOptions?: T[]; options: Array>; onChange?: (value: T) => void; + onClick?: (value: T) => void; size?: RadioButtonSize; fullWidth?: boolean; className?: string; @@ -23,6 +24,7 @@ export function RadioButtonGroup({ options, value, onChange, + onClick, disabled, disabledOptions, size = 'md', @@ -40,6 +42,16 @@ export function RadioButtonGroup({ }, [onChange] ); + const handleOnClick = useCallback( + (option: SelectableValue) => { + return () => { + if (onClick) { + onClick(option.value); + } + }; + }, + [onClick] + ); const id = uniqueId('radiogroup-'); const groupName = useRef(id); const styles = useStyles2(getStyles); @@ -63,6 +75,7 @@ export function RadioButtonGroup({ key={`o.label-${i}`} aria-label={o.ariaLabel} onChange={handleOnChange(o)} + onClick={handleOnClick(o)} id={`option-${o.value}-${id}`} name={groupName.current} description={o.description} diff --git a/public/app/features/alerting/unified/components/alert-groups/MatcherFilter.tsx b/public/app/features/alerting/unified/components/alert-groups/MatcherFilter.tsx index e0652394d76..2db2ee9418f 100644 --- a/public/app/features/alerting/unified/components/alert-groups/MatcherFilter.tsx +++ b/public/app/features/alerting/unified/components/alert-groups/MatcherFilter.tsx @@ -15,6 +15,7 @@ export const MatcherFilter = ({ className, onFilterChange, queryString }: Props) const target = e.target as HTMLInputElement; onFilterChange(target.value); }; + const searchIcon = ; return (
); @@ -44,4 +47,8 @@ const getStyles = (theme: GrafanaTheme2) => ({ icon: css` margin-right: ${theme.spacing(0.5)}; `, + inputWidth: css` + width: 340px; + flex-grow: 0; + `, }); diff --git a/public/app/features/alerting/unified/components/rules/AlertInstanceStateFilter.tsx b/public/app/features/alerting/unified/components/rules/AlertInstanceStateFilter.tsx new file mode 100644 index 00000000000..c1152853582 --- /dev/null +++ b/public/app/features/alerting/unified/components/rules/AlertInstanceStateFilter.tsx @@ -0,0 +1,32 @@ +import React from 'react'; +import { RadioButtonGroup, Label } from '@grafana/ui'; +import { GrafanaAlertState } from 'app/types/unified-alerting-dto'; + +interface Props { + className?: string; + stateFilter?: GrafanaAlertState; + onStateFilterChange: (value: GrafanaAlertState | undefined) => void; +} + +export const AlertInstanceStateFilter = ({ className, onStateFilterChange, stateFilter }: Props) => { + const stateOptions = Object.values(GrafanaAlertState).map((value) => ({ + label: value, + value, + })); + + return ( +
+ + { + if (v === stateFilter) { + onStateFilterChange(undefined); + } + }} + /> +
+ ); +}; diff --git a/public/app/features/alerting/unified/components/rules/AlertInstancesTable.tsx b/public/app/features/alerting/unified/components/rules/AlertInstancesTable.tsx index 4a2a34ed4e4..ab128527592 100644 --- a/public/app/features/alerting/unified/components/rules/AlertInstancesTable.tsx +++ b/public/app/features/alerting/unified/components/rules/AlertInstancesTable.tsx @@ -16,15 +16,12 @@ type AlertTableColumnProps = DynamicTableColumnProps; type AlertTableItemProps = DynamicTableItemProps; export const AlertInstancesTable: FC = ({ instances }) => { - // add key & sort instance. API returns instances in random order, different every time. const items = useMemo( (): AlertTableItemProps[] => - instances - .map((instance) => ({ - data: instance, - id: alertInstanceKey(instance), - })) - .sort((a, b) => a.id.localeCompare(b.id)), + instances.map((instance) => ({ + data: instance, + id: alertInstanceKey(instance), + })), [instances] ); diff --git a/public/app/features/alerting/unified/components/rules/RuleDetailsMatchingInstances.tsx b/public/app/features/alerting/unified/components/rules/RuleDetailsMatchingInstances.tsx index 5e864813baf..db039e769b7 100644 --- a/public/app/features/alerting/unified/components/rules/RuleDetailsMatchingInstances.tsx +++ b/public/app/features/alerting/unified/components/rules/RuleDetailsMatchingInstances.tsx @@ -1,8 +1,17 @@ -import { Rule } from 'app/types/unified-alerting'; -import React from 'react'; +import { Alert, Rule } from 'app/types/unified-alerting'; +import React, { useMemo, useState } from 'react'; import { isAlertingRule } from '../../utils/rules'; import { DetailsField } from '../DetailsField'; import { AlertInstancesTable } from './AlertInstancesTable'; +import { SortOrder } from 'app/plugins/panel/alertlist/types'; +import { GrafanaAlertState } from 'app/types/unified-alerting-dto'; +import { GrafanaTheme } from '@grafana/data'; +import { useStyles } from '@grafana/ui'; +import { css, cx } from '@emotion/css'; +import { labelsMatchMatchers, parseMatchers } from 'app/features/alerting/unified/utils/alertmanager'; +import { sortAlerts } from 'app/features/alerting/unified/utils/misc'; +import { MatcherFilter } from 'app/features/alerting/unified/components/alert-groups/MatcherFilter'; +import { AlertInstanceStateFilter } from 'app/features/alerting/unified/components/rules/AlertInstanceStateFilter'; type Props = { promRule?: Rule; @@ -11,13 +20,84 @@ type Props = { export function RuleDetailsMatchingInstances(props: Props): JSX.Element | null { const { promRule } = props; - if (!isAlertingRule(promRule) || !promRule.alerts?.length) { + const [queryString, setQueryString] = useState(); + const [alertState, setAlertState] = useState(); + + // This key is used to force a rerender on the inputs when the filters are cleared + const [filterKey] = useState(Math.floor(Math.random() * 100)); + const queryStringKey = `queryString-${filterKey}`; + + const styles = useStyles(getStyles); + + const alerts = useMemo( + (): Alert[] => + isAlertingRule(promRule) && promRule.alerts?.length + ? filterAlerts(queryString, alertState, sortAlerts(SortOrder.Importance, promRule.alerts)) + : [], + [promRule, alertState, queryString] + ); + + if (!isAlertingRule(promRule)) { return null; } return ( - +
+
+ setQueryString(value)} + /> + +
+
+ +
); } + +function filterAlerts( + alertInstanceLabel: string | undefined, + alertInstanceState: GrafanaAlertState | undefined, + alerts: Alert[] +): Alert[] { + let filteredAlerts = [...alerts]; + if (alertInstanceLabel) { + const matchers = parseMatchers(alertInstanceLabel || ''); + filteredAlerts = filteredAlerts.filter(({ labels }) => labelsMatchMatchers(labels, matchers)); + } + if (alertInstanceState) { + filteredAlerts = filteredAlerts.filter((alert) => { + return alert.state === alertInstanceState; + }); + } + + return filteredAlerts; +} + +const getStyles = (theme: GrafanaTheme) => { + return { + flexRow: css` + display: flex; + flex-direction: row; + align-items: flex-end; + width: 100%; + flex-wrap: wrap; + margin-bottom: ${theme.spacing.sm}; + `, + spaceBetween: css` + justify-content: space-between; + `, + rowChild: css` + margin-right: ${theme.spacing.sm}; + `, + }; +}; diff --git a/public/app/features/alerting/unified/utils/alertmanager.test.ts b/public/app/features/alerting/unified/utils/alertmanager.test.ts index a349e14960c..c318fdb7eea 100644 --- a/public/app/features/alerting/unified/utils/alertmanager.test.ts +++ b/public/app/features/alerting/unified/utils/alertmanager.test.ts @@ -88,6 +88,13 @@ describe('Alertmanager utils', () => { { name: 'bar', value: 'bazz', isEqual: true, isRegex: false }, ]); }); + + it('should parse matchers for key with special characters', () => { + expect(parseMatchers('foo.bar-baz="bar",baz-bar.foo=bazz')).toEqual([ + { name: 'foo.bar-baz', value: 'bar', isRegex: false, isEqual: true }, + { name: 'baz-bar.foo', value: 'bazz', isEqual: true, isRegex: false }, + ]); + }); }); describe('labelsMatchMatchers', () => { diff --git a/public/app/features/alerting/unified/utils/alertmanager.ts b/public/app/features/alerting/unified/utils/alertmanager.ts index 117ec24750d..917a77654ef 100644 --- a/public/app/features/alerting/unified/utils/alertmanager.ts +++ b/public/app/features/alerting/unified/utils/alertmanager.ts @@ -136,7 +136,7 @@ export function parseMatcher(matcher: string): Matcher { } export function parseMatchers(matcherQueryString: string): Matcher[] { - const matcherRegExp = /\b(\w+)(=~|!=|!~|=(?="?\w))"?([^"\n,]*)"?/g; + const matcherRegExp = /\b([\w.-]+)(=~|!=|!~|=(?="?\w))"?([^"\n,]*)"?/g; const matchers: Matcher[] = []; matcherQueryString.replace(matcherRegExp, (_, key, operator, value) => { diff --git a/public/app/features/alerting/unified/utils/misc.test.ts b/public/app/features/alerting/unified/utils/misc.test.ts new file mode 100644 index 00000000000..c69888b374f --- /dev/null +++ b/public/app/features/alerting/unified/utils/misc.test.ts @@ -0,0 +1,70 @@ +import { sortAlerts } from 'app/features/alerting/unified/utils/misc'; +import { GrafanaAlertState } from 'app/types/unified-alerting-dto'; +import { Alert } from 'app/types/unified-alerting'; +import { SortOrder } from 'app/plugins/panel/alertlist/types'; + +function withState(state: GrafanaAlertState, labels?: {}): Alert { + return { activeAt: '', annotations: {}, labels: labels || {}, state: state, value: '' }; +} + +function withDate(activeAt?: string, labels?: {}): Alert { + return { + activeAt: activeAt || '', + annotations: {}, + labels: labels || {}, + state: GrafanaAlertState.Alerting, + value: '', + }; +} + +function permute(inputArray: any[]): any[] { + return inputArray.reduce(function permute(res, item, key, arr) { + return res.concat( + (arr.length > 1 && + arr + .slice(0, key) + .concat(arr.slice(key + 1)) + .reduce(permute, []) + .map(function (perm: any) { + return [item].concat(perm); + })) || + item + ); + }, []); +} + +describe('Unified Altering misc', () => { + describe('sortAlerts', () => { + describe('when using any sortOrder with a list of alert instances', () => { + it.each` + alerts | sortOrder | expected + ${[withState(GrafanaAlertState.Pending), withState(GrafanaAlertState.Alerting), withState(GrafanaAlertState.Normal)]} | ${SortOrder.Importance} | ${[withState(GrafanaAlertState.Alerting), withState(GrafanaAlertState.Pending), withState(GrafanaAlertState.Normal)]} + ${[withState(GrafanaAlertState.Pending), withState(GrafanaAlertState.Alerting), withState(GrafanaAlertState.NoData)]} | ${SortOrder.Importance} | ${[withState(GrafanaAlertState.Alerting), withState(GrafanaAlertState.Pending), withState(GrafanaAlertState.NoData)]} + ${[withState(GrafanaAlertState.Pending), withState(GrafanaAlertState.Error), withState(GrafanaAlertState.Normal)]} | ${SortOrder.Importance} | ${[withState(GrafanaAlertState.Error), withState(GrafanaAlertState.Pending), withState(GrafanaAlertState.Normal)]} + ${[withDate('2021-11-29T14:10:07-05:00'), withDate('2021-11-29T15:10:07-05:00'), withDate('2021-11-29T13:10:07-05:00')]} | ${SortOrder.TimeAsc} | ${[withDate('2021-11-29T13:10:07-05:00'), withDate('2021-11-29T14:10:07-05:00'), withDate('2021-11-29T15:10:07-05:00')]} + ${[withDate('2021-11-29T14:10:07-05:00'), withDate('2021-11-29T15:10:07-05:00'), withDate('2021-11-29T13:10:07-05:00')]} | ${SortOrder.TimeDesc} | ${[withDate('2021-11-29T15:10:07-05:00'), withDate('2021-11-29T14:10:07-05:00'), withDate('2021-11-29T13:10:07-05:00')]} + ${[withDate('', { mno: 'pqr' }), withDate('', { abc: 'def' }), withDate('', { ghi: 'jkl' })]} | ${SortOrder.AlphaAsc} | ${[withDate('', { abc: 'def' }), withDate('', { ghi: 'jkl' }), withDate('', { mno: 'pqr' })]} + ${[withDate('', { mno: 'pqr' }), withDate('', { abc: 'def' }), withDate('', { ghi: 'jkl' })]} | ${SortOrder.AlphaDesc} | ${[withDate('', { mno: 'pqr' }), withDate('', { ghi: 'jkl' }), withDate('', { abc: 'def' })]} + `('then it should sort the alerts correctly', ({ alerts, sortOrder, expected }) => { + const result = sortAlerts(sortOrder, alerts); + + expect(result).toEqual(expected); + }); + }); + + describe('when sorting ties', () => { + it.each` + alerts | sortOrder + ${[withState(GrafanaAlertState.Alerting, { ghi: 'jkl' }), withState(GrafanaAlertState.Alerting, { abc: 'def' }), withState(GrafanaAlertState.Alerting)]} | ${SortOrder.Importance} + ${[withDate('2021-11-29T13:10:07-05:00', { ghi: 'jkl' }), withDate('2021-11-29T13:10:07-05:00'), withDate('2021-11-29T13:10:07-05:00', { abc: 'def' })]} | ${SortOrder.TimeAsc} + ${[withDate('2021-11-29T13:10:07-05:00', { ghi: 'jkl' }), withDate('2021-11-29T13:10:07-05:00'), withDate('2021-11-29T13:10:07-05:00', { abc: 'def' })]} | ${SortOrder.TimeDesc} + `('then tie order should be deterministic', ({ alerts, sortOrder }) => { + // All input permutations should result in the same sorted order + const sortedPermutations = permute(alerts).map((a) => sortAlerts(sortOrder, a)); + sortedPermutations.forEach((p) => { + expect(p).toEqual(sortedPermutations[0]); + }); + }); + }); + }); +}); diff --git a/public/app/features/alerting/unified/utils/misc.ts b/public/app/features/alerting/unified/utils/misc.ts index 813c3143f0c..5aecdce9140 100644 --- a/public/app/features/alerting/unified/utils/misc.ts +++ b/public/app/features/alerting/unified/utils/misc.ts @@ -1,9 +1,13 @@ import { urlUtil, UrlQueryMap } from '@grafana/data'; import { config } from '@grafana/runtime'; -import { CombinedRule, FilterState, RulesSource, SilenceFilterState } from 'app/types/unified-alerting'; +import { Alert, CombinedRule, FilterState, RulesSource, SilenceFilterState } from 'app/types/unified-alerting'; import { ALERTMANAGER_NAME_QUERY_KEY } from './constants'; import { getRulesSourceName } from './datasource'; import * as ruleId from './rule-id'; +import { SortOrder } from 'app/plugins/panel/alertlist/types'; +import { alertInstanceKey } from 'app/features/alerting/unified/utils/rules'; +import { sortBy } from 'lodash'; +import { GrafanaAlertState, PromAlertingRuleState } from 'app/types/unified-alerting-dto'; export function createViewLink(ruleSource: RulesSource, rule: CombinedRule, returnTo: string): string { const sourceName = getRulesSourceName(ruleSource); @@ -83,3 +87,37 @@ export function retryWhile( }); return makeAttempt(); } + +const alertStateSortScore = { + [GrafanaAlertState.Alerting]: 1, + [PromAlertingRuleState.Firing]: 1, + [GrafanaAlertState.Error]: 1, + [GrafanaAlertState.Pending]: 2, + [PromAlertingRuleState.Pending]: 2, + [PromAlertingRuleState.Inactive]: 2, + [GrafanaAlertState.NoData]: 3, + [GrafanaAlertState.Normal]: 4, +}; + +export function sortAlerts(sortOrder: SortOrder, alerts: Alert[]): Alert[] { + // Make sure to handle tie-breaks because API returns alert instances in random order every time + if (sortOrder === SortOrder.Importance) { + return sortBy(alerts, (alert) => [alertStateSortScore[alert.state], alertInstanceKey(alert).toLocaleLowerCase()]); + } else if (sortOrder === SortOrder.TimeAsc) { + return sortBy(alerts, (alert) => [ + new Date(alert.activeAt) || new Date(), + alertInstanceKey(alert).toLocaleLowerCase(), + ]); + } else if (sortOrder === SortOrder.TimeDesc) { + return sortBy(alerts, (alert) => [ + new Date(alert.activeAt) || new Date(), + alertInstanceKey(alert).toLocaleLowerCase(), + ]).reverse(); + } + const result = sortBy(alerts, (alert) => alertInstanceKey(alert).toLocaleLowerCase()); + if (sortOrder === SortOrder.AlphaDesc) { + result.reverse(); + } + + return result; +} diff --git a/public/app/plugins/panel/alertlist/AlertInstances.tsx b/public/app/plugins/panel/alertlist/AlertInstances.tsx index 51729730c82..01a6f8f494f 100644 --- a/public/app/plugins/panel/alertlist/AlertInstances.tsx +++ b/public/app/plugins/panel/alertlist/AlertInstances.tsx @@ -2,35 +2,31 @@ import React, { useEffect, useMemo, useState } from 'react'; import pluralize from 'pluralize'; import { Icon, useStyles2 } from '@grafana/ui'; import { Alert, PromRuleWithLocation } from 'app/types/unified-alerting'; -import { AlertLabels } from 'app/features/alerting/unified/components/AlertLabels'; -import { AlertStateTag } from 'app/features/alerting/unified/components/rules/AlertStateTag'; -import { dateTime, GrafanaTheme2 } from '@grafana/data'; +import { GrafanaTheme2, PanelProps } from '@grafana/data'; import { css } from '@emotion/css'; -import { PromAlertingRuleState } from 'app/types/unified-alerting-dto'; -import { omit } from 'lodash'; -import { alertInstanceKey } from 'app/features/alerting/unified/utils/rules'; +import { GrafanaAlertState, PromAlertingRuleState } from 'app/types/unified-alerting-dto'; +import { UnifiedAlertListOptions } from './types'; +import { AlertInstancesTable } from 'app/features/alerting/unified/components/rules/AlertInstancesTable'; +import { sortAlerts } from 'app/features/alerting/unified/utils/misc'; +import { labelsMatchMatchers, parseMatchers } from 'app/features/alerting/unified/utils/alertmanager'; interface Props { ruleWithLocation: PromRuleWithLocation; - showInstances: boolean; + options: PanelProps['options']; } -export const AlertInstances = ({ ruleWithLocation, showInstances }: Props) => { +export const AlertInstances = ({ ruleWithLocation, options }: Props) => { const { rule } = ruleWithLocation; - const [displayInstances, setDisplayInstances] = useState(showInstances); + const [displayInstances, setDisplayInstances] = useState(options.showInstances); const styles = useStyles2(getStyles); useEffect(() => { - setDisplayInstances(showInstances); - }, [showInstances]); + setDisplayInstances(options.showInstances); + }, [options.showInstances]); - // sort instances, because API returns them in random order every time - const sortedAlerts = useMemo( - (): Alert[] => - displayInstances - ? rule.alerts.slice().sort((a, b) => alertInstanceKey(a).localeCompare(alertInstanceKey(b))) - : [], - [rule, displayInstances] + const alerts = useMemo( + (): Alert[] => (displayInstances ? filterAlerts(options, sortAlerts(options.sortOrder, rule.alerts)) : []), + [rule, options, displayInstances] ); return ( @@ -42,37 +38,34 @@ export const AlertInstances = ({ ruleWithLocation, showInstances }: Props) => { )} - {!!sortedAlerts.length && ( -
    - {sortedAlerts.map((alert, index) => { - return ( -
  1. -
    - - {dateTime(alert.activeAt).format('YYYY-MM-DD HH:mm:ss')} -
    - -
  2. - ); - })} -
- )} + {!!alerts.length && } ); }; -const getStyles = (theme: GrafanaTheme2) => ({ +function filterAlerts(options: PanelProps['options'], alerts: Alert[]): Alert[] { + let filteredAlerts = [...alerts]; + if (options.alertInstanceLabelFilter) { + const matchers = parseMatchers(options.alertInstanceLabelFilter || ''); + filteredAlerts = filteredAlerts.filter(({ labels }) => labelsMatchMatchers(labels, matchers)); + } + if (Object.values(options.alertInstanceStateFilter).some((value) => value)) { + filteredAlerts = filteredAlerts.filter((alert) => { + return ( + (options.alertInstanceStateFilter.Alerting && alert.state === GrafanaAlertState.Alerting) || + (options.alertInstanceStateFilter.Pending && alert.state === GrafanaAlertState.Pending) || + (options.alertInstanceStateFilter.NoData && alert.state === GrafanaAlertState.NoData) || + (options.alertInstanceStateFilter.Normal && alert.state === GrafanaAlertState.Normal) || + (options.alertInstanceStateFilter.Error && alert.state === GrafanaAlertState.Error) + ); + }); + } + + return filteredAlerts; +} + +const getStyles = (_: GrafanaTheme2) => ({ instance: css` cursor: pointer; `, - list: css` - list-style-type: none; - `, - listItem: css` - margin-top: ${theme.spacing(1)}; - `, - date: css` - font-size: ${theme.typography.bodySmall.fontSize}; - padding-left: ${theme.spacing(0.5)}; - `, }); diff --git a/public/app/plugins/panel/alertlist/UnifiedAlertList.tsx b/public/app/plugins/panel/alertlist/UnifiedAlertList.tsx index f475c490535..9f23e309470 100644 --- a/public/app/plugins/panel/alertlist/UnifiedAlertList.tsx +++ b/public/app/plugins/panel/alertlist/UnifiedAlertList.tsx @@ -100,7 +100,7 @@ export function UnifiedAlertList(props: PanelProps) { )} - + ); diff --git a/public/app/plugins/panel/alertlist/module.tsx b/public/app/plugins/panel/alertlist/module.tsx index 44bdb8bd748..e52d9a145ca 100644 --- a/public/app/plugins/panel/alertlist/module.tsx +++ b/public/app/plugins/panel/alertlist/module.tsx @@ -154,12 +154,14 @@ const unifiedAlertList = new PanelPlugin(UnifiedAlertLi .addNumberInput({ name: 'Max items', path: 'maxItems', + description: 'Maximum alerts to display', defaultValue: 20, category: ['Options'], }) .addSelect({ name: 'Sort order', path: 'sortOrder', + description: 'Sort order of alerts and alert instances', settings: { options: [ { label: 'Alphabetical (asc)', value: SortOrder.AlphaAsc }, @@ -175,24 +177,35 @@ const unifiedAlertList = new PanelPlugin(UnifiedAlertLi .addBooleanSwitch({ path: 'dashboardAlerts', name: 'Alerts from this dashboard', + description: 'Show alerts from this dashboard', defaultValue: false, category: ['Options'], }) .addBooleanSwitch({ path: 'showInstances', name: 'Show alert instances', + description: 'Show individual alert instances for multi-dimensional rules', defaultValue: false, category: ['Options'], }) .addTextInput({ path: 'alertName', name: 'Alert name', + description: 'Filter for alerts containing this text', + defaultValue: '', + category: ['Filter'], + }) + .addTextInput({ + path: 'alertInstanceLabelFilter', + name: 'Alert instance label', + description: 'Filter alert instances using label querying, ex: {severity="critical", instance=~"cluster-us-.+"}', defaultValue: '', category: ['Filter'], }) .addCustomEditor({ path: 'folder', name: 'Folder', + description: 'Filter for alerts in the selected folder', id: 'folder', defaultValue: null, editor: function RenderFolderPicker(props) { @@ -212,19 +225,49 @@ const unifiedAlertList = new PanelPlugin(UnifiedAlertLi path: 'stateFilter.firing', name: 'Alerting', defaultValue: true, - category: ['State filter'], + category: ['Alert state filter'], }) .addBooleanSwitch({ path: 'stateFilter.pending', name: 'Pending', defaultValue: true, - category: ['State filter'], + category: ['Alert state filter'], }) .addBooleanSwitch({ path: 'stateFilter.inactive', name: 'Inactive', defaultValue: false, - category: ['State filter'], + category: ['Alert state filter'], + }) + .addBooleanSwitch({ + path: 'alertInstanceStateFilter.Alerting', + name: 'Alerting', + defaultValue: true, + category: ['Alert instance state filter'], + }) + .addBooleanSwitch({ + path: 'alertInstanceStateFilter.Pending', + name: 'Pending', + defaultValue: true, + category: ['Alert instance state filter'], + }) + .addBooleanSwitch({ + path: 'alertInstanceStateFilter.NoData', + name: 'No Data', + defaultValue: false, + category: ['Alert instance state filter'], + }) + .addBooleanSwitch({ + path: 'alertInstanceStateFilter.Normal', + name: 'Normal', + defaultValue: false, + category: ['Alert instance state filter'], + }) + .addBooleanSwitch({ + path: 'alertInstanceStateFilter.Error', + name: 'Error', + defaultValue: true, + category: ['Alert instance state filter'], }); }); diff --git a/public/app/plugins/panel/alertlist/types.ts b/public/app/plugins/panel/alertlist/types.ts index 17d8e6699d3..1721c578405 100644 --- a/public/app/plugins/panel/alertlist/types.ts +++ b/public/app/plugins/panel/alertlist/types.ts @@ -1,4 +1,4 @@ -import { PromAlertingRuleState } from 'app/types/unified-alerting-dto'; +import { GrafanaAlertState, PromAlertingRuleState } from 'app/types/unified-alerting-dto'; export enum SortOrder { AlphaAsc = 1, @@ -42,4 +42,8 @@ export interface UnifiedAlertListOptions { stateFilter: { [K in PromAlertingRuleState]: boolean; }; + alertInstanceLabelFilter: string; + alertInstanceStateFilter: { + [K in GrafanaAlertState]: boolean; + }; }