mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Alerting: Create alert link from dashboard alerting panel (#63648)
* WIP * feat: update CSS for long names also adds broken href, to fix later * Create correct link using CombinedRules * Use link instead of button for alert link * Updates from PR review * Handle loading,haveResults and dispatched state for both promRules and rulerRules --------- Co-authored-by: Gilles De Mey <gilles.de.mey@gmail.com>
This commit is contained in:
parent
eca0a9f487
commit
a41e9b2dc7
@ -1,7 +1,7 @@
|
|||||||
import { createAsyncThunk, AsyncThunk } from '@reduxjs/toolkit';
|
import { AsyncThunk, createAsyncThunk } from '@reduxjs/toolkit';
|
||||||
import { isEmpty } from 'lodash';
|
import { isEmpty } from 'lodash';
|
||||||
|
|
||||||
import { locationService, config } from '@grafana/runtime';
|
import { config, locationService } from '@grafana/runtime';
|
||||||
import {
|
import {
|
||||||
AlertmanagerAlert,
|
AlertmanagerAlert,
|
||||||
AlertManagerCortexConfig,
|
AlertManagerCortexConfig,
|
||||||
@ -33,7 +33,7 @@ import {
|
|||||||
|
|
||||||
import { contextSrv } from '../../../../core/core';
|
import { contextSrv } from '../../../../core/core';
|
||||||
import { backendSrv } from '../../../../core/services/backend_srv';
|
import { backendSrv } from '../../../../core/services/backend_srv';
|
||||||
import { logInfo, LogMessages, withPerformanceLogging, trackNewAlerRuleFormSaved } from '../Analytics';
|
import { logInfo, LogMessages, trackNewAlerRuleFormSaved, withPerformanceLogging } from '../Analytics';
|
||||||
import {
|
import {
|
||||||
addAlertManagers,
|
addAlertManagers,
|
||||||
createOrUpdateSilence,
|
createOrUpdateSilence,
|
||||||
|
@ -6,6 +6,7 @@ import {
|
|||||||
AlertingRule,
|
AlertingRule,
|
||||||
CloudRuleIdentifier,
|
CloudRuleIdentifier,
|
||||||
CombinedRuleGroup,
|
CombinedRuleGroup,
|
||||||
|
CombinedRuleWithLocation,
|
||||||
GrafanaRuleIdentifier,
|
GrafanaRuleIdentifier,
|
||||||
PrometheusRuleIdentifier,
|
PrometheusRuleIdentifier,
|
||||||
PromRuleWithLocation,
|
PromRuleWithLocation,
|
||||||
@ -26,10 +27,12 @@ import {
|
|||||||
RulerRuleDTO,
|
RulerRuleDTO,
|
||||||
} from 'app/types/unified-alerting-dto';
|
} from 'app/types/unified-alerting-dto';
|
||||||
|
|
||||||
|
import { CombinedRuleNamespace } from '../../../../types/unified-alerting';
|
||||||
import { State } from '../components/StateTag';
|
import { State } from '../components/StateTag';
|
||||||
import { RuleHealth } from '../search/rulesSearchParser';
|
import { RuleHealth } from '../search/rulesSearchParser';
|
||||||
|
|
||||||
import { RULER_NOT_SUPPORTED_MSG } from './constants';
|
import { RULER_NOT_SUPPORTED_MSG } from './constants';
|
||||||
|
import { getRulesSourceName } from './datasource';
|
||||||
import { AsyncRequestState } from './redux';
|
import { AsyncRequestState } from './redux';
|
||||||
|
|
||||||
export function isAlertingRule(rule: Rule | undefined): rule is AlertingRule {
|
export function isAlertingRule(rule: Rule | undefined): rule is AlertingRule {
|
||||||
@ -112,6 +115,22 @@ export const flattenRules = (rules: RuleNamespace[]) => {
|
|||||||
}, []);
|
}, []);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const getAlertingRule = (rule: CombinedRuleWithLocation) =>
|
||||||
|
isAlertingRule(rule.promRule) ? rule.promRule : null;
|
||||||
|
|
||||||
|
export const flattenCombinedRules = (rules: CombinedRuleNamespace[]) => {
|
||||||
|
return rules.reduce<CombinedRuleWithLocation[]>((acc, { rulesSource, name: namespaceName, groups }) => {
|
||||||
|
groups.forEach(({ name: groupName, rules }) => {
|
||||||
|
rules.forEach((rule) => {
|
||||||
|
if (rule.promRule && isAlertingRule(rule.promRule)) {
|
||||||
|
acc.push({ dataSourceName: getRulesSourceName(rulesSource), namespaceName, groupName, ...rule });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
return acc;
|
||||||
|
}, []);
|
||||||
|
};
|
||||||
|
|
||||||
export function alertStateToState(state: PromAlertingRuleState | GrafanaAlertStateWithReason | AlertState): State {
|
export function alertStateToState(state: PromAlertingRuleState | GrafanaAlertStateWithReason | AlertState): State {
|
||||||
let key: PromAlertingRuleState | GrafanaAlertState | AlertState;
|
let key: PromAlertingRuleState | GrafanaAlertState | AlertState;
|
||||||
if (Object.values(AlertState).includes(state as AlertState)) {
|
if (Object.values(AlertState).includes(state as AlertState)) {
|
||||||
@ -140,8 +159,8 @@ const alertStateToStateMap: Record<PromAlertingRuleState | GrafanaAlertState | A
|
|||||||
[AlertState.Unknown]: 'info',
|
[AlertState.Unknown]: 'info',
|
||||||
};
|
};
|
||||||
|
|
||||||
export function getFirstActiveAt(promRule: AlertingRule) {
|
export function getFirstActiveAt(promRule?: AlertingRule) {
|
||||||
if (!promRule.alerts) {
|
if (!promRule?.alerts) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
return promRule.alerts.reduce((prev, alert) => {
|
return promRule.alerts.reduce((prev, alert) => {
|
||||||
|
@ -18,8 +18,9 @@ import {
|
|||||||
import { config } from 'app/core/config';
|
import { config } from 'app/core/config';
|
||||||
import { contextSrv } from 'app/core/services/context_srv';
|
import { contextSrv } from 'app/core/services/context_srv';
|
||||||
import alertDef from 'app/features/alerting/state/alertDef';
|
import alertDef from 'app/features/alerting/state/alertDef';
|
||||||
|
import { useCombinedRuleNamespaces } from 'app/features/alerting/unified/hooks/useCombinedRuleNamespaces';
|
||||||
import { useUnifiedAlertingSelector } from 'app/features/alerting/unified/hooks/useUnifiedAlertingSelector';
|
import { useUnifiedAlertingSelector } from 'app/features/alerting/unified/hooks/useUnifiedAlertingSelector';
|
||||||
import { fetchAllPromRulesAction } from 'app/features/alerting/unified/state/actions';
|
import { fetchAllPromAndRulerRulesAction } from 'app/features/alerting/unified/state/actions';
|
||||||
import { labelsMatchMatchers, parseMatchers } from 'app/features/alerting/unified/utils/alertmanager';
|
import { labelsMatchMatchers, parseMatchers } from 'app/features/alerting/unified/utils/alertmanager';
|
||||||
import { Annotation } from 'app/features/alerting/unified/utils/constants';
|
import { Annotation } from 'app/features/alerting/unified/utils/constants';
|
||||||
import {
|
import {
|
||||||
@ -27,13 +28,16 @@ import {
|
|||||||
GRAFANA_DATASOURCE_NAME,
|
GRAFANA_DATASOURCE_NAME,
|
||||||
GRAFANA_RULES_SOURCE_NAME,
|
GRAFANA_RULES_SOURCE_NAME,
|
||||||
} from 'app/features/alerting/unified/utils/datasource';
|
} from 'app/features/alerting/unified/utils/datasource';
|
||||||
import { flattenRules, getFirstActiveAt } from 'app/features/alerting/unified/utils/rules';
|
import { initialAsyncRequestState } from 'app/features/alerting/unified/utils/redux';
|
||||||
|
import { flattenCombinedRules, getFirstActiveAt } from 'app/features/alerting/unified/utils/rules';
|
||||||
import { getDashboardSrv } from 'app/features/dashboard/services/DashboardSrv';
|
import { getDashboardSrv } from 'app/features/dashboard/services/DashboardSrv';
|
||||||
import { DashboardModel } from 'app/features/dashboard/state';
|
import { DashboardModel } from 'app/features/dashboard/state';
|
||||||
import { useDispatch, AccessControlAction } from 'app/types';
|
import { AccessControlAction, useDispatch } from 'app/types';
|
||||||
import { PromRuleWithLocation } from 'app/types/unified-alerting';
|
|
||||||
import { PromAlertingRuleState } from 'app/types/unified-alerting-dto';
|
import { PromAlertingRuleState } from 'app/types/unified-alerting-dto';
|
||||||
|
|
||||||
|
import { getAlertingRule } from '../../../features/alerting/unified/utils/rules';
|
||||||
|
import { AlertingRule, CombinedRuleWithLocation } from '../../../types/unified-alerting';
|
||||||
|
|
||||||
import { GroupMode, SortOrder, UnifiedAlertListOptions, ViewMode } from './types';
|
import { GroupMode, SortOrder, UnifiedAlertListOptions, ViewMode } from './types';
|
||||||
import GroupedModeView from './unified-alerting/GroupedView';
|
import GroupedModeView from './unified-alerting/GroupedView';
|
||||||
import UngroupedModeView from './unified-alerting/UngroupedView';
|
import UngroupedModeView from './unified-alerting/UngroupedView';
|
||||||
@ -58,33 +62,38 @@ export function UnifiedAlertList(props: PanelProps<UnifiedAlertListOptions>) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
dispatch(fetchAllPromRulesAction());
|
//we need promRules and rulerRules for getting the uid when creating the alert link in panel in case of being a rulerRule.
|
||||||
const sub = dashboard?.events.subscribe(TimeRangeUpdatedEvent, () => dispatch(fetchAllPromRulesAction()));
|
dispatch(fetchAllPromAndRulerRulesAction());
|
||||||
|
const sub = dashboard?.events.subscribe(TimeRangeUpdatedEvent, () => dispatch(fetchAllPromAndRulerRulesAction()));
|
||||||
return () => {
|
return () => {
|
||||||
sub?.unsubscribe();
|
sub?.unsubscribe();
|
||||||
};
|
};
|
||||||
}, [dispatch, dashboard]);
|
}, [dispatch, dashboard]);
|
||||||
|
|
||||||
const promRulesRequests = useUnifiedAlertingSelector((state) => state.promRules);
|
const { prom, ruler } = useUnifiedAlertingSelector((state) => ({
|
||||||
|
prom: state.promRules[GRAFANA_RULES_SOURCE_NAME] || initialAsyncRequestState,
|
||||||
|
ruler: state.rulerRules[GRAFANA_RULES_SOURCE_NAME] || initialAsyncRequestState,
|
||||||
|
}));
|
||||||
|
|
||||||
const dispatched = rulesDataSourceNames.some((name) => promRulesRequests[name]?.dispatched);
|
const loading = prom.loading || ruler.loading;
|
||||||
const loading = rulesDataSourceNames.some((name) => promRulesRequests[name]?.loading);
|
const haveResults = !!prom.result || !!ruler.result;
|
||||||
const haveResults = rulesDataSourceNames.some(
|
|
||||||
(name) => promRulesRequests[name]?.result?.length && !promRulesRequests[name]?.error
|
const promRulesRequests = useUnifiedAlertingSelector((state) => state.promRules);
|
||||||
);
|
const rulerRulesRequests = useUnifiedAlertingSelector((state) => state.rulerRules);
|
||||||
|
const combinedRules = useCombinedRuleNamespaces();
|
||||||
|
|
||||||
|
const somePromRulesDispatched = rulesDataSourceNames.some((name) => promRulesRequests[name]?.dispatched);
|
||||||
|
const someRulerRulesDispatched = rulesDataSourceNames.some((name) => rulerRulesRequests[name]?.dispatched);
|
||||||
|
const dispatched = somePromRulesDispatched || someRulerRulesDispatched;
|
||||||
|
|
||||||
const styles = useStyles2(getStyles);
|
const styles = useStyles2(getStyles);
|
||||||
|
|
||||||
|
const flattenedCombinedRules = flattenCombinedRules(combinedRules);
|
||||||
|
const order = props.options.sortOrder;
|
||||||
|
|
||||||
const rules = useMemo(
|
const rules = useMemo(
|
||||||
() =>
|
() => filterRules(props, sortRules(order, flattenedCombinedRules)),
|
||||||
filterRules(
|
[flattenedCombinedRules, order, props]
|
||||||
props,
|
|
||||||
sortRules(
|
|
||||||
props.options.sortOrder,
|
|
||||||
Object.values(promRulesRequests).flatMap(({ result = [] }) => flattenRules(result))
|
|
||||||
)
|
|
||||||
),
|
|
||||||
[props, promRulesRequests]
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const noAlertsMessage = rules.length === 0 ? 'No alerts matching filters' : undefined;
|
const noAlertsMessage = rules.length === 0 ? 'No alerts matching filters' : undefined;
|
||||||
@ -127,16 +136,24 @@ export function UnifiedAlertList(props: PanelProps<UnifiedAlertListOptions>) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function sortRules(sortOrder: SortOrder, rules: PromRuleWithLocation[]) {
|
function sortRules(sortOrder: SortOrder, rules: CombinedRuleWithLocation[]) {
|
||||||
if (sortOrder === SortOrder.Importance) {
|
if (sortOrder === SortOrder.Importance) {
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
return sortBy(rules, (rule) => alertDef.alertStateSortScore[rule.state]);
|
return sortBy(rules, (rule) => alertDef.alertStateSortScore[rule.state]);
|
||||||
} else if (sortOrder === SortOrder.TimeAsc) {
|
} else if (sortOrder === SortOrder.TimeAsc) {
|
||||||
return sortBy(rules, (rule) => getFirstActiveAt(rule.rule) || new Date());
|
return sortBy(rules, (rule) => {
|
||||||
|
//at this point rules are all AlertingRule, this check is only needed for Typescript checks
|
||||||
|
const alertingRule: AlertingRule | undefined = getAlertingRule(rule) ?? undefined;
|
||||||
|
return getFirstActiveAt(alertingRule) || new Date();
|
||||||
|
});
|
||||||
} else if (sortOrder === SortOrder.TimeDesc) {
|
} else if (sortOrder === SortOrder.TimeDesc) {
|
||||||
return sortBy(rules, (rule) => getFirstActiveAt(rule.rule) || new Date()).reverse();
|
return sortBy(rules, (rule) => {
|
||||||
|
//at this point rules are all AlertingRule, this check is only needed for Typescript checks
|
||||||
|
const alertingRule: AlertingRule | undefined = getAlertingRule(rule) ?? undefined;
|
||||||
|
return getFirstActiveAt(alertingRule) || new Date();
|
||||||
|
}).reverse();
|
||||||
}
|
}
|
||||||
const result = sortBy(rules, (rule) => rule.rule.name.toLowerCase());
|
const result = sortBy(rules, (rule) => rule.name.toLowerCase());
|
||||||
if (sortOrder === SortOrder.AlphaDesc) {
|
if (sortOrder === SortOrder.AlphaDesc) {
|
||||||
result.reverse();
|
result.reverse();
|
||||||
}
|
}
|
||||||
@ -144,39 +161,46 @@ function sortRules(sortOrder: SortOrder, rules: PromRuleWithLocation[]) {
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
function filterRules(props: PanelProps<UnifiedAlertListOptions>, rules: PromRuleWithLocation[]) {
|
function filterRules(props: PanelProps<UnifiedAlertListOptions>, rules: CombinedRuleWithLocation[]) {
|
||||||
const { options, replaceVariables } = props;
|
const { options, replaceVariables } = props;
|
||||||
|
|
||||||
let filteredRules = [...rules];
|
let filteredRules = [...rules];
|
||||||
if (options.dashboardAlerts) {
|
if (options.dashboardAlerts) {
|
||||||
const dashboardUid = getDashboardSrv().getCurrent()?.uid;
|
const dashboardUid = getDashboardSrv().getCurrent()?.uid;
|
||||||
filteredRules = filteredRules.filter(({ rule: { annotations = {} } }) =>
|
filteredRules = filteredRules.filter(({ annotations = {} }) =>
|
||||||
Object.entries(annotations).some(([key, value]) => key === Annotation.dashboardUID && value === dashboardUid)
|
Object.entries(annotations).some(([key, value]) => key === Annotation.dashboardUID && value === dashboardUid)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (options.alertName) {
|
if (options.alertName) {
|
||||||
const replacedName = replaceVariables(options.alertName);
|
const replacedName = replaceVariables(options.alertName);
|
||||||
filteredRules = filteredRules.filter(({ rule: { name } }) =>
|
filteredRules = filteredRules.filter(({ name }) =>
|
||||||
name.toLocaleLowerCase().includes(replacedName.toLocaleLowerCase())
|
name.toLocaleLowerCase().includes(replacedName.toLocaleLowerCase())
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
filteredRules = filteredRules.filter((rule) => {
|
filteredRules = filteredRules.filter((rule) => {
|
||||||
|
const alertingRule = getAlertingRule(rule);
|
||||||
|
if (!alertingRule) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
return (
|
return (
|
||||||
(options.stateFilter.firing && rule.rule.state === PromAlertingRuleState.Firing) ||
|
(options.stateFilter.firing && alertingRule.state === PromAlertingRuleState.Firing) ||
|
||||||
(options.stateFilter.pending && rule.rule.state === PromAlertingRuleState.Pending) ||
|
(options.stateFilter.pending && alertingRule.state === PromAlertingRuleState.Pending) ||
|
||||||
(options.stateFilter.normal && rule.rule.state === PromAlertingRuleState.Inactive)
|
(options.stateFilter.normal && alertingRule.state === PromAlertingRuleState.Inactive)
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
if (options.alertInstanceLabelFilter) {
|
if (options.alertInstanceLabelFilter) {
|
||||||
const replacedLabelFilter = replaceVariables(options.alertInstanceLabelFilter);
|
const replacedLabelFilter = replaceVariables(options.alertInstanceLabelFilter);
|
||||||
const matchers = parseMatchers(replacedLabelFilter);
|
const matchers = parseMatchers(replacedLabelFilter);
|
||||||
|
|
||||||
// Reduce rules and instances to only those that match
|
// Reduce rules and instances to only those that match
|
||||||
filteredRules = filteredRules.reduce<PromRuleWithLocation[]>((rules, rule) => {
|
filteredRules = filteredRules.reduce<CombinedRuleWithLocation[]>((rules, rule) => {
|
||||||
const filteredAlerts = (rule.rule.alerts ?? []).filter(({ labels }) => labelsMatchMatchers(labels, matchers));
|
const alertingRule = getAlertingRule(rule);
|
||||||
|
const filteredAlerts = (alertingRule?.alerts ?? []).filter(({ labels }) => labelsMatchMatchers(labels, matchers));
|
||||||
if (filteredAlerts.length) {
|
if (filteredAlerts.length) {
|
||||||
rules.push({ ...rule, rule: { ...rule.rule, alerts: filteredAlerts } });
|
const alertRule: AlertingRule | null = getAlertingRule(rule);
|
||||||
|
alertRule && rules.push({ ...rule, promRule: { ...alertRule, alerts: filteredAlerts } });
|
||||||
}
|
}
|
||||||
return rules;
|
return rules;
|
||||||
}, []);
|
}, []);
|
||||||
@ -200,8 +224,9 @@ function filterRules(props: PanelProps<UnifiedAlertListOptions>, rules: PromRule
|
|||||||
// Remove rules having 0 instances
|
// Remove rules having 0 instances
|
||||||
// AlertInstances filters instances and we need to prevent situation
|
// AlertInstances filters instances and we need to prevent situation
|
||||||
// when we display a rule with 0 instances
|
// when we display a rule with 0 instances
|
||||||
filteredRules = filteredRules.reduce<PromRuleWithLocation[]>((rules, rule) => {
|
filteredRules = filteredRules.reduce<CombinedRuleWithLocation[]>((rules, rule) => {
|
||||||
const filteredAlerts = filterAlerts(options, rule.rule.alerts ?? []);
|
const alertingRule = getAlertingRule(rule);
|
||||||
|
const filteredAlerts = alertingRule ? filterAlerts(options, alertingRule.alerts ?? []) : [];
|
||||||
if (filteredAlerts.length) {
|
if (filteredAlerts.length) {
|
||||||
// We intentionally don't set alerts to filteredAlerts
|
// We intentionally don't set alerts to filteredAlerts
|
||||||
// because later we couldn't display that some alerts are hidden (ref AlertInstances filtering)
|
// because later we couldn't display that some alerts are hidden (ref AlertInstances filtering)
|
||||||
@ -239,13 +264,23 @@ export const getStyles = (theme: GrafanaTheme2) => ({
|
|||||||
border-radius: ${theme.shape.borderRadius(2)};
|
border-radius: ${theme.shape.borderRadius(2)};
|
||||||
margin-bottom: ${theme.spacing(0.5)};
|
margin-bottom: ${theme.spacing(0.5)};
|
||||||
|
|
||||||
& > * {
|
gap: ${theme.spacing(2)};
|
||||||
margin-right: ${theme.spacing(1)};
|
|
||||||
}
|
|
||||||
`,
|
`,
|
||||||
alertName: css`
|
alertName: css`
|
||||||
font-size: ${theme.typography.h6.fontSize};
|
font-size: ${theme.typography.h6.fontSize};
|
||||||
font-weight: ${theme.typography.fontWeightBold};
|
font-weight: ${theme.typography.fontWeightBold};
|
||||||
|
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
`,
|
||||||
|
alertNameWrapper: css`
|
||||||
|
display: flex;
|
||||||
|
flex: 1;
|
||||||
|
flex-wrap: nowrap;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
min-width: 100px;
|
||||||
`,
|
`,
|
||||||
alertLabels: css`
|
alertLabels: css`
|
||||||
> * {
|
> * {
|
||||||
@ -290,4 +325,8 @@ export const getStyles = (theme: GrafanaTheme2) => ({
|
|||||||
customGroupDetails: css`
|
customGroupDetails: css`
|
||||||
margin-bottom: ${theme.spacing(0.5)};
|
margin-bottom: ${theme.spacing(0.5)};
|
||||||
`,
|
`,
|
||||||
|
link: css`
|
||||||
|
word-break: break-all;
|
||||||
|
color: ${theme.colors.primary.text};
|
||||||
|
`,
|
||||||
});
|
});
|
||||||
|
@ -2,15 +2,17 @@ import React, { FC, useMemo } from 'react';
|
|||||||
|
|
||||||
import { useStyles2 } from '@grafana/ui';
|
import { useStyles2 } from '@grafana/ui';
|
||||||
import { AlertLabel } from 'app/features/alerting/unified/components/AlertLabel';
|
import { AlertLabel } from 'app/features/alerting/unified/components/AlertLabel';
|
||||||
import { Alert, PromRuleWithLocation } from 'app/types/unified-alerting';
|
import { getAlertingRule } from 'app/features/alerting/unified/utils/rules';
|
||||||
|
import { Alert } from 'app/types/unified-alerting';
|
||||||
|
|
||||||
|
import { AlertingRule, CombinedRuleWithLocation } from '../../../../types/unified-alerting';
|
||||||
import { AlertInstances } from '../AlertInstances';
|
import { AlertInstances } from '../AlertInstances';
|
||||||
import { getStyles } from '../UnifiedAlertList';
|
import { getStyles } from '../UnifiedAlertList';
|
||||||
import { GroupedRules, UnifiedAlertListOptions } from '../types';
|
import { GroupedRules, UnifiedAlertListOptions } from '../types';
|
||||||
import { filterAlerts } from '../util';
|
import { filterAlerts } from '../util';
|
||||||
|
|
||||||
type GroupedModeProps = {
|
type GroupedModeProps = {
|
||||||
rules: PromRuleWithLocation[];
|
rules: CombinedRuleWithLocation[];
|
||||||
options: UnifiedAlertListOptions;
|
options: UnifiedAlertListOptions;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -22,12 +24,13 @@ const GroupedModeView: FC<GroupedModeProps> = ({ rules, options }) => {
|
|||||||
const groupedRules = useMemo<GroupedRules>(() => {
|
const groupedRules = useMemo<GroupedRules>(() => {
|
||||||
const groupedRules = new Map<string, Alert[]>();
|
const groupedRules = new Map<string, Alert[]>();
|
||||||
|
|
||||||
const hasInstancesWithMatchingLabels = (rule: PromRuleWithLocation) =>
|
const hasInstancesWithMatchingLabels = (rule: CombinedRuleWithLocation) =>
|
||||||
groupBy ? alertHasEveryLabel(rule, groupBy) : true;
|
groupBy ? alertHasEveryLabelForCombinedRules(rule, groupBy) : true;
|
||||||
|
|
||||||
const matchingRules = rules.filter(hasInstancesWithMatchingLabels);
|
const matchingRules = rules.filter(hasInstancesWithMatchingLabels);
|
||||||
matchingRules.forEach((rule: PromRuleWithLocation) => {
|
matchingRules.forEach((rule: CombinedRuleWithLocation) => {
|
||||||
(rule.rule.alerts ?? []).forEach((alert) => {
|
const alertingRule: AlertingRule | null = getAlertingRule(rule);
|
||||||
|
(alertingRule?.alerts ?? []).forEach((alert) => {
|
||||||
const mapKey = createMapKey(groupBy, alert.labels);
|
const mapKey = createMapKey(groupBy, alert.labels);
|
||||||
const existingAlerts = groupedRules.get(mapKey) ?? [];
|
const existingAlerts = groupedRules.get(mapKey) ?? [];
|
||||||
groupedRules.set(mapKey, [...existingAlerts, alert]);
|
groupedRules.set(mapKey, [...existingAlerts, alert]);
|
||||||
@ -75,9 +78,10 @@ function parseMapKey(key: string): Array<[string, string]> {
|
|||||||
return [...new URLSearchParams(key)];
|
return [...new URLSearchParams(key)];
|
||||||
}
|
}
|
||||||
|
|
||||||
function alertHasEveryLabel(rule: PromRuleWithLocation, groupByKeys: string[]) {
|
function alertHasEveryLabelForCombinedRules(rule: CombinedRuleWithLocation, groupByKeys: string[]) {
|
||||||
|
const alertingRule: AlertingRule | null = getAlertingRule(rule);
|
||||||
return groupByKeys.every((key) => {
|
return groupByKeys.every((key) => {
|
||||||
return (rule.rule.alerts ?? []).some((alert) => alert.labels[key]);
|
return (alertingRule?.alerts ?? []).some((alert) => alert.labels[key]);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,25 +1,36 @@
|
|||||||
import { css } from '@emotion/css';
|
import { css } from '@emotion/css';
|
||||||
import React, { FC } from 'react';
|
import React, { FC } from 'react';
|
||||||
|
import { useLocation } from 'react-use';
|
||||||
|
|
||||||
import { GrafanaTheme2, intervalToAbbreviatedDurationString } from '@grafana/data';
|
import { GrafanaTheme2, intervalToAbbreviatedDurationString } from '@grafana/data';
|
||||||
|
import { Stack } from '@grafana/experimental';
|
||||||
import { Icon, useStyles2 } from '@grafana/ui';
|
import { Icon, useStyles2 } from '@grafana/ui';
|
||||||
import alertDef from 'app/features/alerting/state/alertDef';
|
import alertDef from 'app/features/alerting/state/alertDef';
|
||||||
import { alertStateToReadable, alertStateToState, getFirstActiveAt } from 'app/features/alerting/unified/utils/rules';
|
import { Spacer } from 'app/features/alerting/unified/components/Spacer';
|
||||||
import { PromRuleWithLocation } from 'app/types/unified-alerting';
|
import { fromCombinedRule, stringifyIdentifier } from 'app/features/alerting/unified/utils/rule-id';
|
||||||
|
import {
|
||||||
|
alertStateToReadable,
|
||||||
|
alertStateToState,
|
||||||
|
getFirstActiveAt,
|
||||||
|
isAlertingRule,
|
||||||
|
} from 'app/features/alerting/unified/utils/rules';
|
||||||
|
import { createUrl } from 'app/features/alerting/unified/utils/url';
|
||||||
import { PromAlertingRuleState } from 'app/types/unified-alerting-dto';
|
import { PromAlertingRuleState } from 'app/types/unified-alerting-dto';
|
||||||
|
|
||||||
|
import { AlertingRule, CombinedRuleWithLocation } from '../../../../types/unified-alerting';
|
||||||
import { AlertInstances } from '../AlertInstances';
|
import { AlertInstances } from '../AlertInstances';
|
||||||
import { getStyles } from '../UnifiedAlertList';
|
import { getStyles } from '../UnifiedAlertList';
|
||||||
import { UnifiedAlertListOptions } from '../types';
|
import { UnifiedAlertListOptions } from '../types';
|
||||||
|
|
||||||
type UngroupedModeProps = {
|
type UngroupedModeProps = {
|
||||||
rules: PromRuleWithLocation[];
|
rules: CombinedRuleWithLocation[];
|
||||||
options: UnifiedAlertListOptions;
|
options: UnifiedAlertListOptions;
|
||||||
};
|
};
|
||||||
|
|
||||||
const UngroupedModeView: FC<UngroupedModeProps> = ({ rules, options }) => {
|
const UngroupedModeView: FC<UngroupedModeProps> = ({ rules, options }) => {
|
||||||
const styles = useStyles2(getStyles);
|
const styles = useStyles2(getStyles);
|
||||||
const stateStyle = useStyles2(getStateTagStyles);
|
const stateStyle = useStyles2(getStateTagStyles);
|
||||||
|
const { href: returnTo } = useLocation();
|
||||||
|
|
||||||
const rulesToDisplay = rules.length <= options.maxItems ? rules : rules.slice(0, options.maxItems);
|
const rulesToDisplay = rules.length <= options.maxItems ? rules : rules.slice(0, options.maxItems);
|
||||||
|
|
||||||
@ -27,43 +38,71 @@ const UngroupedModeView: FC<UngroupedModeProps> = ({ rules, options }) => {
|
|||||||
<>
|
<>
|
||||||
<ol className={styles.alertRuleList}>
|
<ol className={styles.alertRuleList}>
|
||||||
{rulesToDisplay.map((ruleWithLocation, index) => {
|
{rulesToDisplay.map((ruleWithLocation, index) => {
|
||||||
const { rule, namespaceName, groupName } = ruleWithLocation;
|
const { namespaceName, groupName, dataSourceName } = ruleWithLocation;
|
||||||
const firstActiveAt = getFirstActiveAt(rule);
|
const alertingRule: AlertingRule | undefined = isAlertingRule(ruleWithLocation.promRule)
|
||||||
return (
|
? ruleWithLocation.promRule
|
||||||
<li className={styles.alertRuleItem} key={`alert-${namespaceName}-${groupName}-${rule.name}-${index}`}>
|
: undefined;
|
||||||
<div className={stateStyle.icon}>
|
const firstActiveAt = getFirstActiveAt(alertingRule);
|
||||||
<Icon
|
const indentifier = fromCombinedRule(ruleWithLocation.dataSourceName, ruleWithLocation);
|
||||||
name={alertDef.getStateDisplayModel(rule.state).iconClass}
|
const strIndentifier = stringifyIdentifier(indentifier);
|
||||||
className={stateStyle[alertStateToState(rule.state)]}
|
|
||||||
size={'lg'}
|
const href = createUrl(
|
||||||
/>
|
`/alerting/${encodeURIComponent(dataSourceName)}/${encodeURIComponent(strIndentifier)}/view`,
|
||||||
</div>
|
{ returnTo: returnTo ?? '' }
|
||||||
<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)]}>
|
|
||||||
{alertStateToReadable(rule.state)}
|
|
||||||
</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>
|
|
||||||
);
|
);
|
||||||
|
if (alertingRule) {
|
||||||
|
return (
|
||||||
|
<li
|
||||||
|
className={styles.alertRuleItem}
|
||||||
|
key={`alert-${namespaceName}-${groupName}-${ruleWithLocation.name}-${index}`}
|
||||||
|
>
|
||||||
|
<div className={stateStyle.icon}>
|
||||||
|
<Icon
|
||||||
|
name={alertDef.getStateDisplayModel(alertingRule.state).iconClass}
|
||||||
|
className={stateStyle[alertStateToState(alertingRule.state)]}
|
||||||
|
size={'lg'}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className={styles.alertNameWrapper}>
|
||||||
|
<div className={styles.instanceDetails}>
|
||||||
|
<Stack direction="row" gap={1} wrap={false}>
|
||||||
|
<div className={styles.alertName} title={ruleWithLocation.name}>
|
||||||
|
{ruleWithLocation.name}
|
||||||
|
</div>
|
||||||
|
<Spacer />
|
||||||
|
{href && (
|
||||||
|
<a href={href} target="__blank" className={styles.link} rel="noopener">
|
||||||
|
<Stack alignItems="center" gap={1}>
|
||||||
|
View alert rule
|
||||||
|
<Icon name={'external-link-alt'} size="sm" />
|
||||||
|
</Stack>
|
||||||
|
</a>
|
||||||
|
)}
|
||||||
|
</Stack>
|
||||||
|
<div className={styles.alertDuration}>
|
||||||
|
<span className={stateStyle[alertStateToState(alertingRule.state)]}>
|
||||||
|
{alertStateToReadable(alertingRule.state)}
|
||||||
|
</span>{' '}
|
||||||
|
{firstActiveAt && alertingRule.state !== PromAlertingRuleState.Inactive && (
|
||||||
|
<>
|
||||||
|
for{' '}
|
||||||
|
<span>
|
||||||
|
{intervalToAbbreviatedDurationString({
|
||||||
|
start: firstActiveAt,
|
||||||
|
end: Date.now(),
|
||||||
|
})}
|
||||||
|
</span>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<AlertInstances alerts={alertingRule.alerts ?? []} options={options} />
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
})}
|
})}
|
||||||
</ol>
|
</ol>
|
||||||
</>
|
</>
|
||||||
|
@ -3,15 +3,15 @@
|
|||||||
import { AlertState, DataSourceInstanceSettings } from '@grafana/data';
|
import { AlertState, DataSourceInstanceSettings } from '@grafana/data';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
Annotations,
|
||||||
|
GrafanaAlertState,
|
||||||
|
GrafanaAlertStateWithReason,
|
||||||
|
Labels,
|
||||||
|
mapStateWithReasonToBaseState,
|
||||||
PromAlertingRuleState,
|
PromAlertingRuleState,
|
||||||
PromRuleType,
|
PromRuleType,
|
||||||
RulerRuleDTO,
|
RulerRuleDTO,
|
||||||
Labels,
|
|
||||||
Annotations,
|
|
||||||
RulerRuleGroupDTO,
|
RulerRuleGroupDTO,
|
||||||
GrafanaAlertState,
|
|
||||||
GrafanaAlertStateWithReason,
|
|
||||||
mapStateWithReasonToBaseState,
|
|
||||||
} from './unified-alerting-dto';
|
} from './unified-alerting-dto';
|
||||||
|
|
||||||
export type Alert = {
|
export type Alert = {
|
||||||
@ -111,6 +111,12 @@ export interface RuleWithLocation<T = RulerRuleDTO> {
|
|||||||
rule: T;
|
rule: T;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface CombinedRuleWithLocation extends CombinedRule {
|
||||||
|
dataSourceName: string;
|
||||||
|
namespaceName: string;
|
||||||
|
groupName: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface PromRuleWithLocation {
|
export interface PromRuleWithLocation {
|
||||||
rule: AlertingRule;
|
rule: AlertingRule;
|
||||||
dataSourceName: string;
|
dataSourceName: string;
|
||||||
|
Loading…
Reference in New Issue
Block a user