Alerting: add custom grouping to Alert Panel (#44559)

Co-authored-by: Peter Holmberg <peterholmberg@users.noreply.github.com>
This commit is contained in:
Gilles De Mey 2022-02-03 19:07:27 +01:00 committed by GitHub
parent c5211f848d
commit c1a0c2664c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 400 additions and 180 deletions

View File

@ -91,7 +91,7 @@ export const DynamicTable = <T extends object>({
{items.map((item, index) => {
const isItemExpanded = isExpanded ? isExpanded(item) : expandedIds.includes(item.id);
return (
<div className={styles.row} key={item.id} data-testid={testIdGenerator?.(item, index) ?? 'row'}>
<div className={styles.row} key={`${item.id}-${index}`} data-testid={testIdGenerator?.(item, index) ?? 'row'}>
{renderPrefixCell && renderPrefixCell(item, index, items)}
{isExpandable && (
<div className={cx(styles.cell, styles.expandCell)}>

View File

@ -161,3 +161,19 @@ export function messageFromError(e: Error | FetchError | SerializedError): strin
}
return (e as Error)?.message || String(e);
}
export function isAsyncRequestMapSliceFulfilled<T>(slice: AsyncRequestMapSlice<T>): boolean {
return Object.values(slice).every(isAsyncRequestStateFulfilled);
}
export function isAsyncRequestStateFulfilled<T>(state: AsyncRequestState<T>): boolean {
return state.dispatched && !state.loading && !state.error;
}
export function isAsyncRequestMapSlicePending<T>(slice: AsyncRequestMapSlice<T>): boolean {
return Object.values(slice).some(isAsyncRequestStatePending);
}
export function isAsyncRequestStatePending<T>(state: AsyncRequestState<T>): boolean {
return state.dispatched && state.loading;
}

View File

@ -1,67 +1,47 @@
import React, { useEffect, useMemo, useState } from 'react';
import React, { FC, useCallback, useMemo, useState } from 'react';
import pluralize from 'pluralize';
import { Icon, useStyles2 } from '@grafana/ui';
import { Alert, PromRuleWithLocation } from 'app/types/unified-alerting';
import { Alert } from 'app/types/unified-alerting';
import { GrafanaTheme2, PanelProps } from '@grafana/data';
import { css } from '@emotion/css';
import { GrafanaAlertState, PromAlertingRuleState } from 'app/types/unified-alerting-dto';
import { UnifiedAlertListOptions } from './types';
import { GroupMode, UnifiedAlertListOptions } from './types';
import { AlertInstancesTable } from 'app/features/alerting/unified/components/rules/AlertInstancesTable';
import { sortAlerts } from 'app/features/alerting/unified/utils/misc';
import { filterAlerts } from './util';
interface Props {
ruleWithLocation: PromRuleWithLocation;
alerts: Alert[];
options: PanelProps<UnifiedAlertListOptions>['options'];
}
export const AlertInstances = ({ ruleWithLocation, options }: Props) => {
const { rule } = ruleWithLocation;
const [displayInstances, setDisplayInstances] = useState<boolean>(options.showInstances);
export const AlertInstances: FC<Props> = ({ alerts, options }) => {
// when custom grouping is enabled, we will always uncollapse the list of alert instances
const defaultShowInstances = options.groupMode === GroupMode.Custom ? true : options.showInstances;
const [displayInstances, setDisplayInstances] = useState<boolean>(defaultShowInstances);
const styles = useStyles2(getStyles);
useEffect(() => {
setDisplayInstances(options.showInstances);
}, [options.showInstances]);
const toggleDisplayInstances = useCallback(() => {
setDisplayInstances((display) => !display);
}, []);
const alerts = useMemo(
(): Alert[] => (displayInstances ? filterAlerts(options, sortAlerts(options.sortOrder, rule.alerts)) : []),
[rule, options, displayInstances]
const filteredAlerts = useMemo(
(): Alert[] => filterAlerts(options, sortAlerts(options.sortOrder, alerts)) ?? [],
[alerts, options]
);
return (
<div>
{rule.state !== PromAlertingRuleState.Inactive && (
<div className={styles.instance} onClick={() => setDisplayInstances(!displayInstances)}>
{options.groupMode === GroupMode.Default && (
<div className={styles.instance} onClick={() => toggleDisplayInstances()}>
<Icon name={displayInstances ? 'angle-down' : 'angle-right'} size={'md'} />
<span>{`${rule.alerts.length} ${pluralize('instance', rule.alerts.length)}`}</span>
<span>{`${filteredAlerts.length} ${pluralize('instance', filteredAlerts.length)}`}</span>
</div>
)}
{!!alerts.length && <AlertInstancesTable instances={alerts} />}
{displayInstances && <AlertInstancesTable instances={filteredAlerts} />}
</div>
);
};
function filterAlerts(options: PanelProps<UnifiedAlertListOptions>['options'], alerts: Alert[]): Alert[] {
const hasAlertState = Object.values(options.stateFilter).some((value) => value);
let filteredAlerts = [...alerts];
if (hasAlertState) {
filteredAlerts = filteredAlerts.filter((alert) => {
return (
(options.stateFilter.firing &&
(alert.state === GrafanaAlertState.Alerting || alert.state === PromAlertingRuleState.Firing)) ||
(options.stateFilter.pending &&
(alert.state === GrafanaAlertState.Pending || alert.state === PromAlertingRuleState.Pending)) ||
(options.stateFilter.noData && alert.state === GrafanaAlertState.NoData) ||
(options.stateFilter.normal && alert.state === GrafanaAlertState.Normal) ||
(options.stateFilter.error && alert.state === GrafanaAlertState.Error) ||
(options.stateFilter.inactive && alert.state === PromAlertingRuleState.Inactive)
);
});
}
return filteredAlerts;
}
const getStyles = (_: GrafanaTheme2) => ({
instance: css`
cursor: pointer;

View File

@ -0,0 +1,75 @@
import React, { FC, useEffect, useMemo } from 'react';
import { isEmpty, uniq } from 'lodash';
import { Icon, MultiSelect } from '@grafana/ui';
import { SelectableValue } from '@grafana/data';
import { useDispatch } from 'react-redux';
import { fetchAllPromRulesAction } from 'app/features/alerting/unified/state/actions';
import { useUnifiedAlertingSelector } from 'app/features/alerting/unified/hooks/useUnifiedAlertingSelector';
import { getAllRulesSourceNames } from 'app/features/alerting/unified/utils/datasource';
import { PromRuleType } from 'app/types/unified-alerting-dto';
import { AlertingRule } from 'app/types/unified-alerting';
import { isPrivateLabel } from './util';
import {
isAsyncRequestMapSliceFulfilled,
isAsyncRequestMapSlicePending,
} from 'app/features/alerting/unified/utils/redux';
interface Props {
id: string;
defaultValue: SelectableValue<string>;
onChange: (keys: string[]) => void;
}
export const GroupBy: FC<Props> = (props) => {
const { onChange, id, defaultValue } = props;
const dispatch = useDispatch();
useEffect(() => {
dispatch(fetchAllPromRulesAction());
}, [dispatch]);
const promRulesByDatasource = useUnifiedAlertingSelector((state) => state.promRules);
const rulesDataSourceNames = useMemo(getAllRulesSourceNames, []);
const allRequestsReady = isAsyncRequestMapSliceFulfilled(promRulesByDatasource);
const loading = isAsyncRequestMapSlicePending(promRulesByDatasource);
const labels = useMemo(() => {
if (isEmpty(promRulesByDatasource)) {
return [];
}
if (!allRequestsReady) {
return [];
}
const allLabels = rulesDataSourceNames
.flatMap((datasource) => promRulesByDatasource[datasource].result ?? [])
.flatMap((rules) => rules.groups)
.flatMap((group) => group.rules.filter((rule): rule is AlertingRule => rule.type === PromRuleType.Alerting))
.flatMap((rule) => rule.alerts ?? [])
.map((alert) => Object.keys(alert.labels ?? {}))
.flatMap((labels) => labels.filter(isPrivateLabel));
return uniq(allLabels);
}, [allRequestsReady, promRulesByDatasource, rulesDataSourceNames]);
return (
<MultiSelect<string>
id={id}
isLoading={loading}
defaultValue={defaultValue}
aria-label={'group by label keys'}
placeholder="Group by"
prefix={<Icon name={'tag-alt'} />}
onChange={(items) => {
onChange(items.map((item) => item.value ?? ''));
}}
options={labels.map<SelectableValue>((key) => ({
label: key,
value: key,
}))}
menuShouldPortal={true}
/>
);
};

View File

@ -1,15 +1,14 @@
import React, { useEffect, useMemo } from 'react';
import { sortBy } from 'lodash';
import { useDispatch } from 'react-redux';
import { GrafanaTheme, GrafanaTheme2, intervalToAbbreviatedDurationString, PanelProps } from '@grafana/data';
import { CustomScrollbar, Icon, IconName, LoadingPlaceholder, useStyles, useStyles2 } from '@grafana/ui';
import { GrafanaTheme2, PanelProps } from '@grafana/data';
import { CustomScrollbar, LoadingPlaceholder, useStyles2 } from '@grafana/ui';
import { css } from '@emotion/css';
import { AlertInstances } from './AlertInstances';
import alertDef from 'app/features/alerting/state/alertDef';
import { SortOrder, UnifiedAlertListOptions } from './types';
import { GroupMode, SortOrder, UnifiedAlertListOptions } from './types';
import { flattenRules, alertStateToState, getFirstActiveAt } from 'app/features/alerting/unified/utils/rules';
import { flattenRules, getFirstActiveAt } from 'app/features/alerting/unified/utils/rules';
import { PromRuleWithLocation } from 'app/types/unified-alerting';
import { fetchAllPromRulesAction } from 'app/features/alerting/unified/state/actions';
import { useUnifiedAlertingSelector } from 'app/features/alerting/unified/hooks/useUnifiedAlertingSelector';
@ -22,6 +21,8 @@ import { getDashboardSrv } from 'app/features/dashboard/services/DashboardSrv';
import { Annotation, RULE_LIST_POLL_INTERVAL_MS } from 'app/features/alerting/unified/utils/constants';
import { PromAlertingRuleState } from 'app/types/unified-alerting-dto';
import { labelsMatchMatchers, parseMatchers } from 'app/features/alerting/unified/utils/alertmanager';
import UngroupedModeView from './unified-alerting/UngroupedView';
import GroupedModeView from './unified-alerting/GroupedView';
export function UnifiedAlertList(props: PanelProps<UnifiedAlertListOptions>) {
const dispatch = useDispatch();
@ -43,8 +44,7 @@ export function UnifiedAlertList(props: PanelProps<UnifiedAlertListOptions>) {
(name) => promRulesRequests[name]?.result?.length && !promRulesRequests[name]?.error
);
const styles = useStyles(getStyles);
const stateStyle = useStyles2(getStateTagStyles);
const styles = useStyles2(getStyles);
const rules = useMemo(
() =>
@ -58,8 +58,6 @@ export function UnifiedAlertList(props: PanelProps<UnifiedAlertListOptions>) {
[props.options, promRulesRequests]
);
const rulesToDisplay = rules.length <= props.options.maxItems ? rules : rules.slice(0, props.options.maxItems);
const noAlertsMessage = rules.length ? '' : 'No alerts';
return (
@ -68,49 +66,12 @@ export function UnifiedAlertList(props: PanelProps<UnifiedAlertListOptions>) {
{dispatched && loading && !haveResults && <LoadingPlaceholder text="Loading..." />}
{noAlertsMessage && <div className={styles.noAlertsMessage}>{noAlertsMessage}</div>}
<section>
<ol className={styles.alertRuleList}>
{haveResults &&
rulesToDisplay.map((ruleWithLocation, index) => {
const { rule, namespaceName, groupName } = ruleWithLocation;
const firstActiveAt = getFirstActiveAt(rule);
return (
<li
className={styles.alertRuleItem}
key={`alert-${namespaceName}-${groupName}-${rule.name}-${index}`}
>
<div className={stateStyle.icon}>
<Icon
name={alertDef.getStateDisplayModel(rule.state).iconClass as IconName}
className={stateStyle[alertStateToState[rule.state]]}
size={'lg'}
/>
</div>
<div>
<div className={styles.instanceDetails}>
<div className={styles.alertName} title={rule.name}>
{rule.name}
</div>
<div className={styles.alertDuration}>
<span className={stateStyle[alertStateToState[rule.state]]}>{rule.state.toUpperCase()}</span>{' '}
{firstActiveAt && rule.state !== PromAlertingRuleState.Inactive && (
<>
for{' '}
<span>
{intervalToAbbreviatedDurationString({
start: firstActiveAt,
end: Date.now(),
})}
</span>
</>
)}
</div>
</div>
<AlertInstances ruleWithLocation={ruleWithLocation} options={props.options} />
</div>
</li>
);
})}
</ol>
{props.options.groupMode === GroupMode.Custom && haveResults && (
<GroupedModeView rules={rules} options={props.options} />
)}
{props.options.groupMode === GroupMode.Default && haveResults && (
<UngroupedModeView rules={rules} options={props.options} />
)}
</section>
</div>
</CustomScrollbar>
@ -160,7 +121,7 @@ function filterRules(options: PanelProps<UnifiedAlertListOptions>['options'], ru
const matchers = parseMatchers(options.alertInstanceLabelFilter);
// Reduce rules and instances to only those that match
filteredRules = filteredRules.reduce((rules, rule) => {
const filteredAlerts = rule.rule.alerts.filter(({ labels }) => labelsMatchMatchers(labels, matchers));
const filteredAlerts = (rule.rule.alerts ?? []).filter(({ labels }) => labelsMatchMatchers(labels, matchers));
if (filteredAlerts.length) {
rules.push({ ...rule, rule: { ...rule.rule, alerts: filteredAlerts } });
}
@ -185,10 +146,10 @@ function filterRules(options: PanelProps<UnifiedAlertListOptions>['options'], ru
return filteredRules;
}
const getStyles = (theme: GrafanaTheme) => ({
export const getStyles = (theme: GrafanaTheme2) => ({
cardContainer: css`
padding: ${theme.spacing.xs} 0 ${theme.spacing.xxs} 0;
line-height: ${theme.typography.lineHeight.md};
padding: ${theme.v1.spacing.xs} 0 ${theme.v1.spacing.xxs} 0;
line-height: ${theme.v1.typography.lineHeight.md};
margin-bottom: 0px;
`,
container: css`
@ -206,29 +167,34 @@ const getStyles = (theme: GrafanaTheme) => ({
align-items: center;
width: 100%;
height: 100%;
background: ${theme.colors.bg2};
padding: ${theme.spacing.xs} ${theme.spacing.sm};
border-radius: ${theme.border.radius.md};
margin-bottom: ${theme.spacing.xs};
background: ${theme.v1.colors.bg2};
padding: ${theme.v1.spacing.xs} ${theme.v1.spacing.sm};
border-radius: ${theme.v1.border.radius.md};
margin-bottom: ${theme.v1.spacing.xs};
& > * {
margin-right: ${theme.spacing.sm};
margin-right: ${theme.v1.spacing.sm};
}
`,
alertName: css`
font-size: ${theme.typography.size.md};
font-weight: ${theme.typography.weight.bold};
font-size: ${theme.v1.typography.size.md};
font-weight: ${theme.v1.typography.weight.bold};
`,
alertLabels: css`
> * {
margin-right: ${theme.v1.spacing.xs};
}
`,
alertDuration: css`
font-size: ${theme.typography.size.sm};
font-size: ${theme.v1.typography.size.sm};
`,
alertRuleItemText: css`
font-weight: ${theme.typography.weight.bold};
font-size: ${theme.typography.size.sm};
font-weight: ${theme.v1.typography.weight.bold};
font-size: ${theme.v1.typography.size.sm};
margin: 0;
`,
alertRuleItemTime: css`
color: ${theme.colors.textWeak};
color: ${theme.v1.colors.textWeak};
font-weight: normal;
white-space: nowrap;
`,
@ -246,7 +212,7 @@ const getStyles = (theme: GrafanaTheme) => ({
height: 100%;
`,
alertIcon: css`
margin-right: ${theme.spacing.xs};
margin-right: ${theme.v1.spacing.xs};
`,
instanceDetails: css`
min-width: 1px;
@ -254,68 +220,7 @@ const getStyles = (theme: GrafanaTheme) => ({
overflow: hidden;
text-overflow: ellipsis;
`,
});
const getStateTagStyles = (theme: GrafanaTheme2) => ({
common: css`
width: 70px;
text-align: center;
align-self: stretch;
display: inline-block;
color: white;
border-radius: ${theme.shape.borderRadius()};
font-size: ${theme.typography.size.sm};
/* padding: ${theme.spacing(2, 0)}; */
text-transform: capitalize;
line-height: 1.2;
flex-shrink: 0;
display: flex;
flex-direction: column;
justify-content: center;
`,
icon: css`
margin-top: ${theme.spacing(2.5)};
align-self: flex-start;
`,
// good: css`
// background-color: ${theme.colors.success.main};
// border: solid 1px ${theme.colors.success.main};
// color: ${theme.colors.success.contrastText};
// `,
// warning: css`
// background-color: ${theme.colors.warning.main};
// border: solid 1px ${theme.colors.warning.main};
// color: ${theme.colors.warning.contrastText};
// `,
// bad: css`
// background-color: ${theme.colors.error.main};
// border: solid 1px ${theme.colors.error.main};
// color: ${theme.colors.error.contrastText};
// `,
// neutral: css`
// background-color: ${theme.colors.secondary.main};
// border: solid 1px ${theme.colors.secondary.main};
// `,
// info: css`
// background-color: ${theme.colors.primary.main};
// border: solid 1px ${theme.colors.primary.main};
// color: ${theme.colors.primary.contrastText};
// `,
good: css`
color: ${theme.colors.success.main};
`,
bad: css`
color: ${theme.colors.error.main};
`,
warning: css`
color: ${theme.colors.warning.main};
`,
neutral: css`
color: ${theme.colors.secondary.main};
`,
info: css`
color: ${theme.colors.primary.main};
customGroupDetails: css`
margin-bottom: ${theme.v1.spacing.xs};
`,
});

View File

@ -3,7 +3,7 @@ import { PanelPlugin } from '@grafana/data';
import { TagsInput } from '@grafana/ui';
import { AlertList } from './AlertList';
import { UnifiedAlertList } from './UnifiedAlertList';
import { AlertListOptions, ShowOption, SortOrder, UnifiedAlertListOptions } from './types';
import { AlertListOptions, GroupMode, ShowOption, SortOrder, UnifiedAlertListOptions } from './types';
import { alertListPanelMigrationHandler } from './AlertListMigrationHandler';
import { config, DataSourcePicker } from '@grafana/runtime';
import { RuleFolderPicker } from 'app/features/alerting/unified/components/rule-editor/RuleFolderPicker';
@ -13,6 +13,7 @@ import {
ReadonlyFolderPicker,
} from '../../../core/components/Select/ReadonlyFolderPicker/ReadonlyFolderPicker';
import { AlertListSuggestionsSupplier } from './suggestions';
import { GroupBy } from './GroupByWithLoading';
function showIfCurrentState(options: AlertListOptions) {
return options.showOptions === ShowOption.Current;
@ -151,6 +152,37 @@ const alertList = new PanelPlugin<AlertListOptions>(AlertList)
const unifiedAlertList = new PanelPlugin<UnifiedAlertListOptions>(UnifiedAlertList).setPanelOptions((builder) => {
builder
.addRadio({
path: 'groupMode',
name: 'Group mode',
description: 'How alert instances should be grouped',
defaultValue: GroupMode.Default,
settings: {
options: [
{ value: GroupMode.Default, label: 'Default grouping' },
{ value: GroupMode.Custom, label: 'Custom grouping' },
],
},
category: ['Options'],
})
.addCustomEditor({
path: 'groupBy',
name: 'Group by',
description: 'Filter alerts using label querying',
id: 'groupBy',
defaultValue: [],
showIf: (options) => options.groupMode === GroupMode.Custom,
category: ['Options'],
editor: (props) => {
return (
<GroupBy
id={props.id ?? 'groupBy'}
defaultValue={props.value.map((value: string) => ({ label: value, value }))}
onChange={props.onChange}
/>
);
},
})
.addNumberInput({
name: 'Max items',
path: 'maxItems',
@ -181,13 +213,6 @@ const unifiedAlertList = new PanelPlugin<UnifiedAlertListOptions>(UnifiedAlertLi
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',

View File

@ -1,3 +1,5 @@
import { Alert } from 'app/types/unified-alerting';
export enum SortOrder {
AlphaAsc = 1,
AlphaDesc,
@ -11,6 +13,11 @@ export enum ShowOption {
RecentChanges = 'changes',
}
export enum GroupMode {
Default = 'default',
Custom = 'custom',
}
export interface AlertListOptions {
showOptions: ShowOption;
maxItems: number;
@ -43,6 +50,8 @@ export interface UnifiedAlertListOptions {
maxItems: number;
sortOrder: SortOrder;
dashboardAlerts: boolean;
groupMode: GroupMode;
groupBy: string[];
alertName: string;
showInstances: boolean;
folder: { id: number; title: string };
@ -50,3 +59,5 @@ export interface UnifiedAlertListOptions {
alertInstanceLabelFilter: string;
datasource: string;
}
export type GroupedRules = Map<string, Alert[]>;

View File

@ -0,0 +1,70 @@
import React, { FC, useMemo } from 'react';
import { useStyles2 } from '@grafana/ui';
import { AlertLabel } from 'app/features/alerting/unified/components/AlertLabel';
import { AlertInstances } from '../AlertInstances';
import { GroupedRules, UnifiedAlertListOptions } from '../types';
import { getStyles } from '../UnifiedAlertList';
import { PromRuleWithLocation } from 'app/types/unified-alerting';
type GroupedModeProps = {
rules: PromRuleWithLocation[];
options: UnifiedAlertListOptions;
};
const GroupedModeView: FC<GroupedModeProps> = ({ rules, options }) => {
const styles = useStyles2(getStyles);
const groupBy = options.groupBy;
const groupedRules = useMemo<GroupedRules>(() => {
const groupedRules = new Map();
const hasInstancesWithMatchingLabels = (rule: PromRuleWithLocation) =>
groupBy ? alertHasEveryLabel(rule, groupBy) : true;
const matchingRules = rules.filter(hasInstancesWithMatchingLabels);
matchingRules.forEach((rule: PromRuleWithLocation) => {
(rule.rule.alerts ?? []).forEach((alert) => {
const mapKey = createMapKey(groupBy, alert.labels);
const existingAlerts = groupedRules.get(mapKey) ?? [];
groupedRules.set(mapKey, [...existingAlerts, alert]);
});
});
return groupedRules;
}, [groupBy, rules]);
return (
<>
{Array.from(groupedRules).map(([key, alerts]) => (
<li className={styles.alertRuleItem} key={key}>
<div>
<div className={styles.customGroupDetails}>
<div className={styles.alertLabels}>
{key && parseMapKey(key).map(([key, value]) => <AlertLabel key={key} labelKey={key} value={value} />)}
{!key && 'No grouping'}
</div>
</div>
<AlertInstances alerts={alerts} options={options} />
</div>
</li>
))}
</>
);
};
function createMapKey(groupBy: string[], labels: Record<string, string>): string {
return new URLSearchParams(groupBy.map((key) => [key, labels[key]])).toString();
}
function parseMapKey(key: string): Array<[string, string]> {
return [...new URLSearchParams(key)];
}
function alertHasEveryLabel(rule: PromRuleWithLocation, groupByKeys: string[]) {
return groupByKeys.every((key) => {
return (rule.rule.alerts ?? []).some((alert) => alert.labels[key]);
});
}
export default GroupedModeView;

View File

@ -0,0 +1,108 @@
import { css } from '@emotion/css';
import { GrafanaTheme2, intervalToAbbreviatedDurationString } from '@grafana/data';
import { Icon, IconName, useStyles2 } from '@grafana/ui';
import alertDef from 'app/features/alerting/state/alertDef';
import { alertStateToState, getFirstActiveAt } from 'app/features/alerting/unified/utils/rules';
import { PromRuleWithLocation } from 'app/types/unified-alerting';
import { PromAlertingRuleState } from 'app/types/unified-alerting-dto';
import React, { FC } from 'react';
import { AlertInstances } from '../AlertInstances';
import { UnifiedAlertListOptions } from '../types';
import { getStyles } from '../UnifiedAlertList';
type UngroupedModeProps = {
rules: PromRuleWithLocation[];
options: UnifiedAlertListOptions;
};
const UngroupedModeView: FC<UngroupedModeProps> = ({ rules, options }) => {
const styles = useStyles2(getStyles);
const stateStyle = useStyles2(getStateTagStyles);
const rulesToDisplay = rules.length <= options.maxItems ? rules : rules.slice(0, options.maxItems);
return (
<>
<ol className={styles.alertRuleList}>
{rulesToDisplay.map((ruleWithLocation, index) => {
const { rule, namespaceName, groupName } = ruleWithLocation;
const firstActiveAt = getFirstActiveAt(rule);
return (
<li className={styles.alertRuleItem} key={`alert-${namespaceName}-${groupName}-${rule.name}-${index}`}>
<div className={stateStyle.icon}>
<Icon
name={alertDef.getStateDisplayModel(rule.state).iconClass as IconName}
className={stateStyle[alertStateToState[rule.state]]}
size={'lg'}
/>
</div>
<div>
<div className={styles.instanceDetails}>
<div className={styles.alertName} title={rule.name}>
{rule.name}
</div>
<div className={styles.alertDuration}>
<span className={stateStyle[alertStateToState[rule.state]]}>{rule.state.toUpperCase()}</span>{' '}
{firstActiveAt && rule.state !== PromAlertingRuleState.Inactive && (
<>
for{' '}
<span>
{intervalToAbbreviatedDurationString({
start: firstActiveAt,
end: Date.now(),
})}
</span>
</>
)}
</div>
</div>
<AlertInstances alerts={ruleWithLocation.rule.alerts ?? []} options={options} />
</div>
</li>
);
})}
</ol>
</>
);
};
const getStateTagStyles = (theme: GrafanaTheme2) => ({
common: css`
width: 70px;
text-align: center;
align-self: stretch;
display: inline-block;
color: white;
border-radius: ${theme.shape.borderRadius()};
font-size: ${theme.v1.typography.size.sm};
text-transform: capitalize;
line-height: 1.2;
flex-shrink: 0;
display: flex;
flex-direction: column;
justify-content: center;
`,
icon: css`
margin-top: ${theme.spacing(2.5)};
align-self: flex-start;
`,
good: css`
color: ${theme.colors.success.main};
`,
bad: css`
color: ${theme.colors.error.main};
`,
warning: css`
color: ${theme.colors.warning.main};
`,
neutral: css`
color: ${theme.colors.secondary.main};
`,
info: css`
color: ${theme.colors.primary.main};
`,
});
export default UngroupedModeView;

View File

@ -0,0 +1,30 @@
import { PanelProps } from '@grafana/data';
import { Alert } from 'app/types/unified-alerting';
import { GrafanaAlertState, PromAlertingRuleState } from 'app/types/unified-alerting-dto';
import { isEmpty } from 'lodash';
import { UnifiedAlertListOptions } from './types';
export function filterAlerts(options: PanelProps<UnifiedAlertListOptions>['options'], alerts: Alert[]): Alert[] {
const { stateFilter } = options;
if (isEmpty(stateFilter)) {
return alerts;
}
return alerts.filter((alert) => {
return (
(stateFilter.firing &&
(alert.state === GrafanaAlertState.Alerting || alert.state === PromAlertingRuleState.Firing)) ||
(stateFilter.pending &&
(alert.state === GrafanaAlertState.Pending || alert.state === PromAlertingRuleState.Pending)) ||
(stateFilter.noData && alert.state === GrafanaAlertState.NoData) ||
(stateFilter.normal && alert.state === GrafanaAlertState.Normal) ||
(stateFilter.error && alert.state === GrafanaAlertState.Error) ||
(stateFilter.inactive && alert.state === PromAlertingRuleState.Inactive)
);
});
}
export function isPrivateLabel(label: string) {
return !(label.startsWith('__') && label.endsWith('__'));
}

View File

@ -28,7 +28,7 @@ interface RuleBase {
}
export interface AlertingRule extends RuleBase {
alerts: Alert[];
alerts?: Alert[];
labels: {
[key: string]: string;
};