mirror of
https://github.com/grafana/grafana.git
synced 2025-09-20 21:26:46 +08:00
Alerting: Add label and state filters to alert instance components (#42550)
This commit is contained in:
@ -16,6 +16,7 @@ export interface RadioButtonProps {
|
|||||||
active: boolean;
|
active: boolean;
|
||||||
id: string;
|
id: string;
|
||||||
onChange: () => void;
|
onChange: () => void;
|
||||||
|
onClick: () => void;
|
||||||
fullWidth?: boolean;
|
fullWidth?: boolean;
|
||||||
'aria-label'?: StringSelector;
|
'aria-label'?: StringSelector;
|
||||||
children?: React.ReactNode;
|
children?: React.ReactNode;
|
||||||
@ -29,6 +30,7 @@ export const RadioButton = React.forwardRef<HTMLInputElement, RadioButtonProps>(
|
|||||||
disabled = false,
|
disabled = false,
|
||||||
size = 'md',
|
size = 'md',
|
||||||
onChange,
|
onChange,
|
||||||
|
onClick,
|
||||||
id,
|
id,
|
||||||
name = undefined,
|
name = undefined,
|
||||||
description,
|
description,
|
||||||
@ -46,6 +48,7 @@ export const RadioButton = React.forwardRef<HTMLInputElement, RadioButtonProps>(
|
|||||||
type="radio"
|
type="radio"
|
||||||
className={styles.radio}
|
className={styles.radio}
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
|
onClick={onClick}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
id={id}
|
id={id}
|
||||||
checked={active}
|
checked={active}
|
||||||
|
@ -13,6 +13,7 @@ export interface RadioButtonGroupProps<T> {
|
|||||||
disabledOptions?: T[];
|
disabledOptions?: T[];
|
||||||
options: Array<SelectableValue<T>>;
|
options: Array<SelectableValue<T>>;
|
||||||
onChange?: (value: T) => void;
|
onChange?: (value: T) => void;
|
||||||
|
onClick?: (value: T) => void;
|
||||||
size?: RadioButtonSize;
|
size?: RadioButtonSize;
|
||||||
fullWidth?: boolean;
|
fullWidth?: boolean;
|
||||||
className?: string;
|
className?: string;
|
||||||
@ -23,6 +24,7 @@ export function RadioButtonGroup<T>({
|
|||||||
options,
|
options,
|
||||||
value,
|
value,
|
||||||
onChange,
|
onChange,
|
||||||
|
onClick,
|
||||||
disabled,
|
disabled,
|
||||||
disabledOptions,
|
disabledOptions,
|
||||||
size = 'md',
|
size = 'md',
|
||||||
@ -40,6 +42,16 @@ export function RadioButtonGroup<T>({
|
|||||||
},
|
},
|
||||||
[onChange]
|
[onChange]
|
||||||
);
|
);
|
||||||
|
const handleOnClick = useCallback(
|
||||||
|
(option: SelectableValue) => {
|
||||||
|
return () => {
|
||||||
|
if (onClick) {
|
||||||
|
onClick(option.value);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
},
|
||||||
|
[onClick]
|
||||||
|
);
|
||||||
const id = uniqueId('radiogroup-');
|
const id = uniqueId('radiogroup-');
|
||||||
const groupName = useRef(id);
|
const groupName = useRef(id);
|
||||||
const styles = useStyles2(getStyles);
|
const styles = useStyles2(getStyles);
|
||||||
@ -63,6 +75,7 @@ export function RadioButtonGroup<T>({
|
|||||||
key={`o.label-${i}`}
|
key={`o.label-${i}`}
|
||||||
aria-label={o.ariaLabel}
|
aria-label={o.ariaLabel}
|
||||||
onChange={handleOnChange(o)}
|
onChange={handleOnChange(o)}
|
||||||
|
onClick={handleOnClick(o)}
|
||||||
id={`option-${o.value}-${id}`}
|
id={`option-${o.value}-${id}`}
|
||||||
name={groupName.current}
|
name={groupName.current}
|
||||||
description={o.description}
|
description={o.description}
|
||||||
|
@ -15,6 +15,7 @@ export const MatcherFilter = ({ className, onFilterChange, queryString }: Props)
|
|||||||
const target = e.target as HTMLInputElement;
|
const target = e.target as HTMLInputElement;
|
||||||
onFilterChange(target.value);
|
onFilterChange(target.value);
|
||||||
};
|
};
|
||||||
|
const searchIcon = <Icon name={'search'} />;
|
||||||
return (
|
return (
|
||||||
<div className={className}>
|
<div className={className}>
|
||||||
<Label>
|
<Label>
|
||||||
@ -35,6 +36,8 @@ export const MatcherFilter = ({ className, onFilterChange, queryString }: Props)
|
|||||||
defaultValue={queryString}
|
defaultValue={queryString}
|
||||||
onChange={handleSearchChange}
|
onChange={handleSearchChange}
|
||||||
data-testid="search-query-input"
|
data-testid="search-query-input"
|
||||||
|
prefix={searchIcon}
|
||||||
|
className={styles.inputWidth}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@ -44,4 +47,8 @@ const getStyles = (theme: GrafanaTheme2) => ({
|
|||||||
icon: css`
|
icon: css`
|
||||||
margin-right: ${theme.spacing(0.5)};
|
margin-right: ${theme.spacing(0.5)};
|
||||||
`,
|
`,
|
||||||
|
inputWidth: css`
|
||||||
|
width: 340px;
|
||||||
|
flex-grow: 0;
|
||||||
|
`,
|
||||||
});
|
});
|
||||||
|
@ -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 (
|
||||||
|
<div className={className}>
|
||||||
|
<Label>State</Label>
|
||||||
|
<RadioButtonGroup
|
||||||
|
options={stateOptions}
|
||||||
|
value={stateFilter}
|
||||||
|
onChange={onStateFilterChange}
|
||||||
|
onClick={(v) => {
|
||||||
|
if (v === stateFilter) {
|
||||||
|
onStateFilterChange(undefined);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
@ -16,15 +16,12 @@ type AlertTableColumnProps = DynamicTableColumnProps<Alert>;
|
|||||||
type AlertTableItemProps = DynamicTableItemProps<Alert>;
|
type AlertTableItemProps = DynamicTableItemProps<Alert>;
|
||||||
|
|
||||||
export const AlertInstancesTable: FC<Props> = ({ instances }) => {
|
export const AlertInstancesTable: FC<Props> = ({ instances }) => {
|
||||||
// add key & sort instance. API returns instances in random order, different every time.
|
|
||||||
const items = useMemo(
|
const items = useMemo(
|
||||||
(): AlertTableItemProps[] =>
|
(): AlertTableItemProps[] =>
|
||||||
instances
|
instances.map((instance) => ({
|
||||||
.map((instance) => ({
|
|
||||||
data: instance,
|
data: instance,
|
||||||
id: alertInstanceKey(instance),
|
id: alertInstanceKey(instance),
|
||||||
}))
|
})),
|
||||||
.sort((a, b) => a.id.localeCompare(b.id)),
|
|
||||||
[instances]
|
[instances]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -1,8 +1,17 @@
|
|||||||
import { Rule } from 'app/types/unified-alerting';
|
import { Alert, Rule } from 'app/types/unified-alerting';
|
||||||
import React from 'react';
|
import React, { useMemo, useState } from 'react';
|
||||||
import { isAlertingRule } from '../../utils/rules';
|
import { isAlertingRule } from '../../utils/rules';
|
||||||
import { DetailsField } from '../DetailsField';
|
import { DetailsField } from '../DetailsField';
|
||||||
import { AlertInstancesTable } from './AlertInstancesTable';
|
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 = {
|
type Props = {
|
||||||
promRule?: Rule;
|
promRule?: Rule;
|
||||||
@ -11,13 +20,84 @@ type Props = {
|
|||||||
export function RuleDetailsMatchingInstances(props: Props): JSX.Element | null {
|
export function RuleDetailsMatchingInstances(props: Props): JSX.Element | null {
|
||||||
const { promRule } = props;
|
const { promRule } = props;
|
||||||
|
|
||||||
if (!isAlertingRule(promRule) || !promRule.alerts?.length) {
|
const [queryString, setQueryString] = useState<string>();
|
||||||
|
const [alertState, setAlertState] = useState<GrafanaAlertState>();
|
||||||
|
|
||||||
|
// This key is used to force a rerender on the inputs when the filters are cleared
|
||||||
|
const [filterKey] = useState<number>(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 null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DetailsField label="Matching instances" horizontal={true}>
|
<DetailsField label="Matching instances" horizontal={true}>
|
||||||
<AlertInstancesTable instances={promRule.alerts} />
|
<div className={cx(styles.flexRow, styles.spaceBetween)}>
|
||||||
|
<div className={styles.flexRow}>
|
||||||
|
<MatcherFilter
|
||||||
|
className={styles.rowChild}
|
||||||
|
key={queryStringKey}
|
||||||
|
queryString={queryString}
|
||||||
|
onFilterChange={(value) => setQueryString(value)}
|
||||||
|
/>
|
||||||
|
<AlertInstanceStateFilter
|
||||||
|
className={styles.rowChild}
|
||||||
|
stateFilter={alertState}
|
||||||
|
onStateFilterChange={setAlertState}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<AlertInstancesTable instances={alerts} />
|
||||||
</DetailsField>
|
</DetailsField>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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};
|
||||||
|
`,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
@ -88,6 +88,13 @@ describe('Alertmanager utils', () => {
|
|||||||
{ name: 'bar', value: 'bazz', isEqual: true, isRegex: false },
|
{ 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<Matcher[]>([
|
||||||
|
{ name: 'foo.bar-baz', value: 'bar', isRegex: false, isEqual: true },
|
||||||
|
{ name: 'baz-bar.foo', value: 'bazz', isEqual: true, isRegex: false },
|
||||||
|
]);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('labelsMatchMatchers', () => {
|
describe('labelsMatchMatchers', () => {
|
||||||
|
@ -136,7 +136,7 @@ export function parseMatcher(matcher: string): Matcher {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function parseMatchers(matcherQueryString: 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[] = [];
|
const matchers: Matcher[] = [];
|
||||||
|
|
||||||
matcherQueryString.replace(matcherRegExp, (_, key, operator, value) => {
|
matcherQueryString.replace(matcherRegExp, (_, key, operator, value) => {
|
||||||
|
70
public/app/features/alerting/unified/utils/misc.test.ts
Normal file
70
public/app/features/alerting/unified/utils/misc.test.ts
Normal file
@ -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]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
@ -1,9 +1,13 @@
|
|||||||
import { urlUtil, UrlQueryMap } from '@grafana/data';
|
import { urlUtil, UrlQueryMap } from '@grafana/data';
|
||||||
import { config } from '@grafana/runtime';
|
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 { ALERTMANAGER_NAME_QUERY_KEY } from './constants';
|
||||||
import { getRulesSourceName } from './datasource';
|
import { getRulesSourceName } from './datasource';
|
||||||
import * as ruleId from './rule-id';
|
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 {
|
export function createViewLink(ruleSource: RulesSource, rule: CombinedRule, returnTo: string): string {
|
||||||
const sourceName = getRulesSourceName(ruleSource);
|
const sourceName = getRulesSourceName(ruleSource);
|
||||||
@ -83,3 +87,37 @@ export function retryWhile<T, E = Error>(
|
|||||||
});
|
});
|
||||||
return makeAttempt();
|
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;
|
||||||
|
}
|
||||||
|
@ -2,35 +2,31 @@ import React, { useEffect, useMemo, useState } from 'react';
|
|||||||
import pluralize from 'pluralize';
|
import pluralize from 'pluralize';
|
||||||
import { Icon, useStyles2 } from '@grafana/ui';
|
import { Icon, useStyles2 } from '@grafana/ui';
|
||||||
import { Alert, PromRuleWithLocation } from 'app/types/unified-alerting';
|
import { Alert, PromRuleWithLocation } from 'app/types/unified-alerting';
|
||||||
import { AlertLabels } from 'app/features/alerting/unified/components/AlertLabels';
|
import { GrafanaTheme2, PanelProps } from '@grafana/data';
|
||||||
import { AlertStateTag } from 'app/features/alerting/unified/components/rules/AlertStateTag';
|
|
||||||
import { dateTime, GrafanaTheme2 } from '@grafana/data';
|
|
||||||
import { css } from '@emotion/css';
|
import { css } from '@emotion/css';
|
||||||
import { PromAlertingRuleState } from 'app/types/unified-alerting-dto';
|
import { GrafanaAlertState, PromAlertingRuleState } from 'app/types/unified-alerting-dto';
|
||||||
import { omit } from 'lodash';
|
import { UnifiedAlertListOptions } from './types';
|
||||||
import { alertInstanceKey } from 'app/features/alerting/unified/utils/rules';
|
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 {
|
interface Props {
|
||||||
ruleWithLocation: PromRuleWithLocation;
|
ruleWithLocation: PromRuleWithLocation;
|
||||||
showInstances: boolean;
|
options: PanelProps<UnifiedAlertListOptions>['options'];
|
||||||
}
|
}
|
||||||
|
|
||||||
export const AlertInstances = ({ ruleWithLocation, showInstances }: Props) => {
|
export const AlertInstances = ({ ruleWithLocation, options }: Props) => {
|
||||||
const { rule } = ruleWithLocation;
|
const { rule } = ruleWithLocation;
|
||||||
const [displayInstances, setDisplayInstances] = useState<boolean>(showInstances);
|
const [displayInstances, setDisplayInstances] = useState<boolean>(options.showInstances);
|
||||||
const styles = useStyles2(getStyles);
|
const styles = useStyles2(getStyles);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setDisplayInstances(showInstances);
|
setDisplayInstances(options.showInstances);
|
||||||
}, [showInstances]);
|
}, [options.showInstances]);
|
||||||
|
|
||||||
// sort instances, because API returns them in random order every time
|
const alerts = useMemo(
|
||||||
const sortedAlerts = useMemo(
|
(): Alert[] => (displayInstances ? filterAlerts(options, sortAlerts(options.sortOrder, rule.alerts)) : []),
|
||||||
(): Alert[] =>
|
[rule, options, displayInstances]
|
||||||
displayInstances
|
|
||||||
? rule.alerts.slice().sort((a, b) => alertInstanceKey(a).localeCompare(alertInstanceKey(b)))
|
|
||||||
: [],
|
|
||||||
[rule, displayInstances]
|
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -42,37 +38,34 @@ export const AlertInstances = ({ ruleWithLocation, showInstances }: Props) => {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{!!sortedAlerts.length && (
|
{!!alerts.length && <AlertInstancesTable instances={alerts} />}
|
||||||
<ol className={styles.list}>
|
|
||||||
{sortedAlerts.map((alert, index) => {
|
|
||||||
return (
|
|
||||||
<li className={styles.listItem} key={`${alert.activeAt}-${index}`}>
|
|
||||||
<div>
|
|
||||||
<AlertStateTag state={alert.state} />
|
|
||||||
<span className={styles.date}>{dateTime(alert.activeAt).format('YYYY-MM-DD HH:mm:ss')}</span>
|
|
||||||
</div>
|
|
||||||
<AlertLabels labels={omit(alert.labels, 'alertname')} />
|
|
||||||
</li>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</ol>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const getStyles = (theme: GrafanaTheme2) => ({
|
function filterAlerts(options: PanelProps<UnifiedAlertListOptions>['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`
|
instance: css`
|
||||||
cursor: pointer;
|
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)};
|
|
||||||
`,
|
|
||||||
});
|
});
|
||||||
|
@ -100,7 +100,7 @@ export function UnifiedAlertList(props: PanelProps<UnifiedAlertListOptions>) {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<AlertInstances ruleWithLocation={ruleWithLocation} showInstances={props.options.showInstances} />
|
<AlertInstances ruleWithLocation={ruleWithLocation} options={props.options} />
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
);
|
);
|
||||||
|
@ -154,12 +154,14 @@ const unifiedAlertList = new PanelPlugin<UnifiedAlertListOptions>(UnifiedAlertLi
|
|||||||
.addNumberInput({
|
.addNumberInput({
|
||||||
name: 'Max items',
|
name: 'Max items',
|
||||||
path: 'maxItems',
|
path: 'maxItems',
|
||||||
|
description: 'Maximum alerts to display',
|
||||||
defaultValue: 20,
|
defaultValue: 20,
|
||||||
category: ['Options'],
|
category: ['Options'],
|
||||||
})
|
})
|
||||||
.addSelect({
|
.addSelect({
|
||||||
name: 'Sort order',
|
name: 'Sort order',
|
||||||
path: 'sortOrder',
|
path: 'sortOrder',
|
||||||
|
description: 'Sort order of alerts and alert instances',
|
||||||
settings: {
|
settings: {
|
||||||
options: [
|
options: [
|
||||||
{ label: 'Alphabetical (asc)', value: SortOrder.AlphaAsc },
|
{ label: 'Alphabetical (asc)', value: SortOrder.AlphaAsc },
|
||||||
@ -175,24 +177,35 @@ const unifiedAlertList = new PanelPlugin<UnifiedAlertListOptions>(UnifiedAlertLi
|
|||||||
.addBooleanSwitch({
|
.addBooleanSwitch({
|
||||||
path: 'dashboardAlerts',
|
path: 'dashboardAlerts',
|
||||||
name: 'Alerts from this dashboard',
|
name: 'Alerts from this dashboard',
|
||||||
|
description: 'Show alerts from this dashboard',
|
||||||
defaultValue: false,
|
defaultValue: false,
|
||||||
category: ['Options'],
|
category: ['Options'],
|
||||||
})
|
})
|
||||||
.addBooleanSwitch({
|
.addBooleanSwitch({
|
||||||
path: 'showInstances',
|
path: 'showInstances',
|
||||||
name: 'Show alert instances',
|
name: 'Show alert instances',
|
||||||
|
description: 'Show individual alert instances for multi-dimensional rules',
|
||||||
defaultValue: false,
|
defaultValue: false,
|
||||||
category: ['Options'],
|
category: ['Options'],
|
||||||
})
|
})
|
||||||
.addTextInput({
|
.addTextInput({
|
||||||
path: 'alertName',
|
path: 'alertName',
|
||||||
name: 'Alert name',
|
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: '',
|
defaultValue: '',
|
||||||
category: ['Filter'],
|
category: ['Filter'],
|
||||||
})
|
})
|
||||||
.addCustomEditor({
|
.addCustomEditor({
|
||||||
path: 'folder',
|
path: 'folder',
|
||||||
name: 'Folder',
|
name: 'Folder',
|
||||||
|
description: 'Filter for alerts in the selected folder',
|
||||||
id: 'folder',
|
id: 'folder',
|
||||||
defaultValue: null,
|
defaultValue: null,
|
||||||
editor: function RenderFolderPicker(props) {
|
editor: function RenderFolderPicker(props) {
|
||||||
@ -212,19 +225,49 @@ const unifiedAlertList = new PanelPlugin<UnifiedAlertListOptions>(UnifiedAlertLi
|
|||||||
path: 'stateFilter.firing',
|
path: 'stateFilter.firing',
|
||||||
name: 'Alerting',
|
name: 'Alerting',
|
||||||
defaultValue: true,
|
defaultValue: true,
|
||||||
category: ['State filter'],
|
category: ['Alert state filter'],
|
||||||
})
|
})
|
||||||
.addBooleanSwitch({
|
.addBooleanSwitch({
|
||||||
path: 'stateFilter.pending',
|
path: 'stateFilter.pending',
|
||||||
name: 'Pending',
|
name: 'Pending',
|
||||||
defaultValue: true,
|
defaultValue: true,
|
||||||
category: ['State filter'],
|
category: ['Alert state filter'],
|
||||||
})
|
})
|
||||||
.addBooleanSwitch({
|
.addBooleanSwitch({
|
||||||
path: 'stateFilter.inactive',
|
path: 'stateFilter.inactive',
|
||||||
name: 'Inactive',
|
name: 'Inactive',
|
||||||
defaultValue: false,
|
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'],
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { PromAlertingRuleState } from 'app/types/unified-alerting-dto';
|
import { GrafanaAlertState, PromAlertingRuleState } from 'app/types/unified-alerting-dto';
|
||||||
|
|
||||||
export enum SortOrder {
|
export enum SortOrder {
|
||||||
AlphaAsc = 1,
|
AlphaAsc = 1,
|
||||||
@ -42,4 +42,8 @@ export interface UnifiedAlertListOptions {
|
|||||||
stateFilter: {
|
stateFilter: {
|
||||||
[K in PromAlertingRuleState]: boolean;
|
[K in PromAlertingRuleState]: boolean;
|
||||||
};
|
};
|
||||||
|
alertInstanceLabelFilter: string;
|
||||||
|
alertInstanceStateFilter: {
|
||||||
|
[K in GrafanaAlertState]: boolean;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user