mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Alerting: add custom grouping to Alert Panel (#44559)
Co-authored-by: Peter Holmberg <peterholmberg@users.noreply.github.com>
This commit is contained in:
parent
c5211f848d
commit
c1a0c2664c
@ -91,7 +91,7 @@ export const DynamicTable = <T extends object>({
|
|||||||
{items.map((item, index) => {
|
{items.map((item, index) => {
|
||||||
const isItemExpanded = isExpanded ? isExpanded(item) : expandedIds.includes(item.id);
|
const isItemExpanded = isExpanded ? isExpanded(item) : expandedIds.includes(item.id);
|
||||||
return (
|
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)}
|
{renderPrefixCell && renderPrefixCell(item, index, items)}
|
||||||
{isExpandable && (
|
{isExpandable && (
|
||||||
<div className={cx(styles.cell, styles.expandCell)}>
|
<div className={cx(styles.cell, styles.expandCell)}>
|
||||||
|
@ -161,3 +161,19 @@ export function messageFromError(e: Error | FetchError | SerializedError): strin
|
|||||||
}
|
}
|
||||||
return (e as Error)?.message || String(e);
|
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;
|
||||||
|
}
|
||||||
|
@ -1,67 +1,47 @@
|
|||||||
import React, { useEffect, useMemo, useState } from 'react';
|
import React, { FC, useCallback, 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 } from 'app/types/unified-alerting';
|
||||||
import { GrafanaTheme2, PanelProps } from '@grafana/data';
|
import { GrafanaTheme2, PanelProps } from '@grafana/data';
|
||||||
import { css } from '@emotion/css';
|
import { css } from '@emotion/css';
|
||||||
import { GrafanaAlertState, PromAlertingRuleState } from 'app/types/unified-alerting-dto';
|
import { GroupMode, UnifiedAlertListOptions } from './types';
|
||||||
import { UnifiedAlertListOptions } from './types';
|
|
||||||
import { AlertInstancesTable } from 'app/features/alerting/unified/components/rules/AlertInstancesTable';
|
import { AlertInstancesTable } from 'app/features/alerting/unified/components/rules/AlertInstancesTable';
|
||||||
import { sortAlerts } from 'app/features/alerting/unified/utils/misc';
|
import { sortAlerts } from 'app/features/alerting/unified/utils/misc';
|
||||||
|
import { filterAlerts } from './util';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
ruleWithLocation: PromRuleWithLocation;
|
alerts: Alert[];
|
||||||
options: PanelProps<UnifiedAlertListOptions>['options'];
|
options: PanelProps<UnifiedAlertListOptions>['options'];
|
||||||
}
|
}
|
||||||
|
|
||||||
export const AlertInstances = ({ ruleWithLocation, options }: Props) => {
|
export const AlertInstances: FC<Props> = ({ alerts, options }) => {
|
||||||
const { rule } = ruleWithLocation;
|
// when custom grouping is enabled, we will always uncollapse the list of alert instances
|
||||||
const [displayInstances, setDisplayInstances] = useState<boolean>(options.showInstances);
|
const defaultShowInstances = options.groupMode === GroupMode.Custom ? true : options.showInstances;
|
||||||
|
const [displayInstances, setDisplayInstances] = useState<boolean>(defaultShowInstances);
|
||||||
const styles = useStyles2(getStyles);
|
const styles = useStyles2(getStyles);
|
||||||
|
|
||||||
useEffect(() => {
|
const toggleDisplayInstances = useCallback(() => {
|
||||||
setDisplayInstances(options.showInstances);
|
setDisplayInstances((display) => !display);
|
||||||
}, [options.showInstances]);
|
}, []);
|
||||||
|
|
||||||
const alerts = useMemo(
|
const filteredAlerts = useMemo(
|
||||||
(): Alert[] => (displayInstances ? filterAlerts(options, sortAlerts(options.sortOrder, rule.alerts)) : []),
|
(): Alert[] => filterAlerts(options, sortAlerts(options.sortOrder, alerts)) ?? [],
|
||||||
[rule, options, displayInstances]
|
[alerts, options]
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
{rule.state !== PromAlertingRuleState.Inactive && (
|
{options.groupMode === GroupMode.Default && (
|
||||||
<div className={styles.instance} onClick={() => setDisplayInstances(!displayInstances)}>
|
<div className={styles.instance} onClick={() => toggleDisplayInstances()}>
|
||||||
<Icon name={displayInstances ? 'angle-down' : 'angle-right'} size={'md'} />
|
<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>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
{displayInstances && <AlertInstancesTable instances={filteredAlerts} />}
|
||||||
{!!alerts.length && <AlertInstancesTable instances={alerts} />}
|
|
||||||
</div>
|
</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) => ({
|
const getStyles = (_: GrafanaTheme2) => ({
|
||||||
instance: css`
|
instance: css`
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
75
public/app/plugins/panel/alertlist/GroupByWithLoading.tsx
Normal file
75
public/app/plugins/panel/alertlist/GroupByWithLoading.tsx
Normal 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}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
@ -1,15 +1,14 @@
|
|||||||
import React, { useEffect, useMemo } from 'react';
|
import React, { useEffect, useMemo } from 'react';
|
||||||
import { sortBy } from 'lodash';
|
import { sortBy } from 'lodash';
|
||||||
import { useDispatch } from 'react-redux';
|
import { useDispatch } from 'react-redux';
|
||||||
import { GrafanaTheme, GrafanaTheme2, intervalToAbbreviatedDurationString, PanelProps } from '@grafana/data';
|
import { GrafanaTheme2, PanelProps } from '@grafana/data';
|
||||||
import { CustomScrollbar, Icon, IconName, LoadingPlaceholder, useStyles, useStyles2 } from '@grafana/ui';
|
import { CustomScrollbar, LoadingPlaceholder, useStyles2 } from '@grafana/ui';
|
||||||
import { css } from '@emotion/css';
|
import { css } from '@emotion/css';
|
||||||
|
|
||||||
import { AlertInstances } from './AlertInstances';
|
|
||||||
import alertDef from 'app/features/alerting/state/alertDef';
|
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 { PromRuleWithLocation } from 'app/types/unified-alerting';
|
||||||
import { fetchAllPromRulesAction } from 'app/features/alerting/unified/state/actions';
|
import { fetchAllPromRulesAction } from 'app/features/alerting/unified/state/actions';
|
||||||
import { useUnifiedAlertingSelector } from 'app/features/alerting/unified/hooks/useUnifiedAlertingSelector';
|
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 { Annotation, RULE_LIST_POLL_INTERVAL_MS } from 'app/features/alerting/unified/utils/constants';
|
||||||
import { PromAlertingRuleState } from 'app/types/unified-alerting-dto';
|
import { PromAlertingRuleState } from 'app/types/unified-alerting-dto';
|
||||||
import { labelsMatchMatchers, parseMatchers } from 'app/features/alerting/unified/utils/alertmanager';
|
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>) {
|
export function UnifiedAlertList(props: PanelProps<UnifiedAlertListOptions>) {
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
@ -43,8 +44,7 @@ export function UnifiedAlertList(props: PanelProps<UnifiedAlertListOptions>) {
|
|||||||
(name) => promRulesRequests[name]?.result?.length && !promRulesRequests[name]?.error
|
(name) => promRulesRequests[name]?.result?.length && !promRulesRequests[name]?.error
|
||||||
);
|
);
|
||||||
|
|
||||||
const styles = useStyles(getStyles);
|
const styles = useStyles2(getStyles);
|
||||||
const stateStyle = useStyles2(getStateTagStyles);
|
|
||||||
|
|
||||||
const rules = useMemo(
|
const rules = useMemo(
|
||||||
() =>
|
() =>
|
||||||
@ -58,8 +58,6 @@ export function UnifiedAlertList(props: PanelProps<UnifiedAlertListOptions>) {
|
|||||||
[props.options, promRulesRequests]
|
[props.options, promRulesRequests]
|
||||||
);
|
);
|
||||||
|
|
||||||
const rulesToDisplay = rules.length <= props.options.maxItems ? rules : rules.slice(0, props.options.maxItems);
|
|
||||||
|
|
||||||
const noAlertsMessage = rules.length ? '' : 'No alerts';
|
const noAlertsMessage = rules.length ? '' : 'No alerts';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -68,49 +66,12 @@ export function UnifiedAlertList(props: PanelProps<UnifiedAlertListOptions>) {
|
|||||||
{dispatched && loading && !haveResults && <LoadingPlaceholder text="Loading..." />}
|
{dispatched && loading && !haveResults && <LoadingPlaceholder text="Loading..." />}
|
||||||
{noAlertsMessage && <div className={styles.noAlertsMessage}>{noAlertsMessage}</div>}
|
{noAlertsMessage && <div className={styles.noAlertsMessage}>{noAlertsMessage}</div>}
|
||||||
<section>
|
<section>
|
||||||
<ol className={styles.alertRuleList}>
|
{props.options.groupMode === GroupMode.Custom && haveResults && (
|
||||||
{haveResults &&
|
<GroupedModeView rules={rules} options={props.options} />
|
||||||
rulesToDisplay.map((ruleWithLocation, index) => {
|
)}
|
||||||
const { rule, namespaceName, groupName } = ruleWithLocation;
|
{props.options.groupMode === GroupMode.Default && haveResults && (
|
||||||
const firstActiveAt = getFirstActiveAt(rule);
|
<UngroupedModeView rules={rules} options={props.options} />
|
||||||
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>
|
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
</CustomScrollbar>
|
</CustomScrollbar>
|
||||||
@ -160,7 +121,7 @@ function filterRules(options: PanelProps<UnifiedAlertListOptions>['options'], ru
|
|||||||
const matchers = parseMatchers(options.alertInstanceLabelFilter);
|
const matchers = parseMatchers(options.alertInstanceLabelFilter);
|
||||||
// Reduce rules and instances to only those that match
|
// Reduce rules and instances to only those that match
|
||||||
filteredRules = filteredRules.reduce((rules, rule) => {
|
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) {
|
if (filteredAlerts.length) {
|
||||||
rules.push({ ...rule, rule: { ...rule.rule, alerts: filteredAlerts } });
|
rules.push({ ...rule, rule: { ...rule.rule, alerts: filteredAlerts } });
|
||||||
}
|
}
|
||||||
@ -185,10 +146,10 @@ function filterRules(options: PanelProps<UnifiedAlertListOptions>['options'], ru
|
|||||||
return filteredRules;
|
return filteredRules;
|
||||||
}
|
}
|
||||||
|
|
||||||
const getStyles = (theme: GrafanaTheme) => ({
|
export const getStyles = (theme: GrafanaTheme2) => ({
|
||||||
cardContainer: css`
|
cardContainer: css`
|
||||||
padding: ${theme.spacing.xs} 0 ${theme.spacing.xxs} 0;
|
padding: ${theme.v1.spacing.xs} 0 ${theme.v1.spacing.xxs} 0;
|
||||||
line-height: ${theme.typography.lineHeight.md};
|
line-height: ${theme.v1.typography.lineHeight.md};
|
||||||
margin-bottom: 0px;
|
margin-bottom: 0px;
|
||||||
`,
|
`,
|
||||||
container: css`
|
container: css`
|
||||||
@ -206,29 +167,34 @@ const getStyles = (theme: GrafanaTheme) => ({
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
background: ${theme.colors.bg2};
|
background: ${theme.v1.colors.bg2};
|
||||||
padding: ${theme.spacing.xs} ${theme.spacing.sm};
|
padding: ${theme.v1.spacing.xs} ${theme.v1.spacing.sm};
|
||||||
border-radius: ${theme.border.radius.md};
|
border-radius: ${theme.v1.border.radius.md};
|
||||||
margin-bottom: ${theme.spacing.xs};
|
margin-bottom: ${theme.v1.spacing.xs};
|
||||||
|
|
||||||
& > * {
|
& > * {
|
||||||
margin-right: ${theme.spacing.sm};
|
margin-right: ${theme.v1.spacing.sm};
|
||||||
}
|
}
|
||||||
`,
|
`,
|
||||||
alertName: css`
|
alertName: css`
|
||||||
font-size: ${theme.typography.size.md};
|
font-size: ${theme.v1.typography.size.md};
|
||||||
font-weight: ${theme.typography.weight.bold};
|
font-weight: ${theme.v1.typography.weight.bold};
|
||||||
|
`,
|
||||||
|
alertLabels: css`
|
||||||
|
> * {
|
||||||
|
margin-right: ${theme.v1.spacing.xs};
|
||||||
|
}
|
||||||
`,
|
`,
|
||||||
alertDuration: css`
|
alertDuration: css`
|
||||||
font-size: ${theme.typography.size.sm};
|
font-size: ${theme.v1.typography.size.sm};
|
||||||
`,
|
`,
|
||||||
alertRuleItemText: css`
|
alertRuleItemText: css`
|
||||||
font-weight: ${theme.typography.weight.bold};
|
font-weight: ${theme.v1.typography.weight.bold};
|
||||||
font-size: ${theme.typography.size.sm};
|
font-size: ${theme.v1.typography.size.sm};
|
||||||
margin: 0;
|
margin: 0;
|
||||||
`,
|
`,
|
||||||
alertRuleItemTime: css`
|
alertRuleItemTime: css`
|
||||||
color: ${theme.colors.textWeak};
|
color: ${theme.v1.colors.textWeak};
|
||||||
font-weight: normal;
|
font-weight: normal;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
`,
|
`,
|
||||||
@ -246,7 +212,7 @@ const getStyles = (theme: GrafanaTheme) => ({
|
|||||||
height: 100%;
|
height: 100%;
|
||||||
`,
|
`,
|
||||||
alertIcon: css`
|
alertIcon: css`
|
||||||
margin-right: ${theme.spacing.xs};
|
margin-right: ${theme.v1.spacing.xs};
|
||||||
`,
|
`,
|
||||||
instanceDetails: css`
|
instanceDetails: css`
|
||||||
min-width: 1px;
|
min-width: 1px;
|
||||||
@ -254,68 +220,7 @@ const getStyles = (theme: GrafanaTheme) => ({
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
`,
|
`,
|
||||||
});
|
customGroupDetails: css`
|
||||||
|
margin-bottom: ${theme.v1.spacing.xs};
|
||||||
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};
|
|
||||||
`,
|
`,
|
||||||
});
|
});
|
||||||
|
@ -3,7 +3,7 @@ import { PanelPlugin } from '@grafana/data';
|
|||||||
import { TagsInput } from '@grafana/ui';
|
import { TagsInput } from '@grafana/ui';
|
||||||
import { AlertList } from './AlertList';
|
import { AlertList } from './AlertList';
|
||||||
import { UnifiedAlertList } from './UnifiedAlertList';
|
import { UnifiedAlertList } from './UnifiedAlertList';
|
||||||
import { AlertListOptions, ShowOption, SortOrder, UnifiedAlertListOptions } from './types';
|
import { AlertListOptions, GroupMode, ShowOption, SortOrder, UnifiedAlertListOptions } from './types';
|
||||||
import { alertListPanelMigrationHandler } from './AlertListMigrationHandler';
|
import { alertListPanelMigrationHandler } from './AlertListMigrationHandler';
|
||||||
import { config, DataSourcePicker } from '@grafana/runtime';
|
import { config, DataSourcePicker } from '@grafana/runtime';
|
||||||
import { RuleFolderPicker } from 'app/features/alerting/unified/components/rule-editor/RuleFolderPicker';
|
import { RuleFolderPicker } from 'app/features/alerting/unified/components/rule-editor/RuleFolderPicker';
|
||||||
@ -13,6 +13,7 @@ import {
|
|||||||
ReadonlyFolderPicker,
|
ReadonlyFolderPicker,
|
||||||
} from '../../../core/components/Select/ReadonlyFolderPicker/ReadonlyFolderPicker';
|
} from '../../../core/components/Select/ReadonlyFolderPicker/ReadonlyFolderPicker';
|
||||||
import { AlertListSuggestionsSupplier } from './suggestions';
|
import { AlertListSuggestionsSupplier } from './suggestions';
|
||||||
|
import { GroupBy } from './GroupByWithLoading';
|
||||||
|
|
||||||
function showIfCurrentState(options: AlertListOptions) {
|
function showIfCurrentState(options: AlertListOptions) {
|
||||||
return options.showOptions === ShowOption.Current;
|
return options.showOptions === ShowOption.Current;
|
||||||
@ -151,6 +152,37 @@ const alertList = new PanelPlugin<AlertListOptions>(AlertList)
|
|||||||
|
|
||||||
const unifiedAlertList = new PanelPlugin<UnifiedAlertListOptions>(UnifiedAlertList).setPanelOptions((builder) => {
|
const unifiedAlertList = new PanelPlugin<UnifiedAlertListOptions>(UnifiedAlertList).setPanelOptions((builder) => {
|
||||||
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({
|
.addNumberInput({
|
||||||
name: 'Max items',
|
name: 'Max items',
|
||||||
path: 'maxItems',
|
path: 'maxItems',
|
||||||
@ -181,13 +213,6 @@ const unifiedAlertList = new PanelPlugin<UnifiedAlertListOptions>(UnifiedAlertLi
|
|||||||
defaultValue: false,
|
defaultValue: false,
|
||||||
category: ['Options'],
|
category: ['Options'],
|
||||||
})
|
})
|
||||||
.addBooleanSwitch({
|
|
||||||
path: 'showInstances',
|
|
||||||
name: 'Show alert instances',
|
|
||||||
description: 'Show individual alert instances for multi-dimensional rules',
|
|
||||||
defaultValue: false,
|
|
||||||
category: ['Options'],
|
|
||||||
})
|
|
||||||
.addTextInput({
|
.addTextInput({
|
||||||
path: 'alertName',
|
path: 'alertName',
|
||||||
name: 'Alert name',
|
name: 'Alert name',
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
import { Alert } from 'app/types/unified-alerting';
|
||||||
|
|
||||||
export enum SortOrder {
|
export enum SortOrder {
|
||||||
AlphaAsc = 1,
|
AlphaAsc = 1,
|
||||||
AlphaDesc,
|
AlphaDesc,
|
||||||
@ -11,6 +13,11 @@ export enum ShowOption {
|
|||||||
RecentChanges = 'changes',
|
RecentChanges = 'changes',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export enum GroupMode {
|
||||||
|
Default = 'default',
|
||||||
|
Custom = 'custom',
|
||||||
|
}
|
||||||
|
|
||||||
export interface AlertListOptions {
|
export interface AlertListOptions {
|
||||||
showOptions: ShowOption;
|
showOptions: ShowOption;
|
||||||
maxItems: number;
|
maxItems: number;
|
||||||
@ -43,6 +50,8 @@ export interface UnifiedAlertListOptions {
|
|||||||
maxItems: number;
|
maxItems: number;
|
||||||
sortOrder: SortOrder;
|
sortOrder: SortOrder;
|
||||||
dashboardAlerts: boolean;
|
dashboardAlerts: boolean;
|
||||||
|
groupMode: GroupMode;
|
||||||
|
groupBy: string[];
|
||||||
alertName: string;
|
alertName: string;
|
||||||
showInstances: boolean;
|
showInstances: boolean;
|
||||||
folder: { id: number; title: string };
|
folder: { id: number; title: string };
|
||||||
@ -50,3 +59,5 @@ export interface UnifiedAlertListOptions {
|
|||||||
alertInstanceLabelFilter: string;
|
alertInstanceLabelFilter: string;
|
||||||
datasource: string;
|
datasource: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type GroupedRules = Map<string, Alert[]>;
|
||||||
|
@ -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;
|
@ -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;
|
30
public/app/plugins/panel/alertlist/util.ts
Normal file
30
public/app/plugins/panel/alertlist/util.ts
Normal 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('__'));
|
||||||
|
}
|
@ -28,7 +28,7 @@ interface RuleBase {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface AlertingRule extends RuleBase {
|
export interface AlertingRule extends RuleBase {
|
||||||
alerts: Alert[];
|
alerts?: Alert[];
|
||||||
labels: {
|
labels: {
|
||||||
[key: string]: string;
|
[key: string]: string;
|
||||||
};
|
};
|
||||||
|
Loading…
Reference in New Issue
Block a user