The open and composable observability and data visualization platform. Visualize metrics, logs, and traces from multiple sources like Prometheus, Loki, Elasticsearch, InfluxDB, Postgres and many more.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 
grafana/public/app/plugins/panel/alertlist/UnifiedAlertList.tsx

470 lines
16 KiB

import { css } from '@emotion/css';
import { sortBy } from 'lodash';
import { useEffect, useMemo } from 'react';
import { useEffectOnce, useToggle } from 'react-use';
import { GrafanaTheme2, PanelProps } from '@grafana/data';
import { Trans, useTranslate } from '@grafana/i18n';
import { TimeRangeUpdatedEvent } from '@grafana/runtime';
import {
Alert,
BigValue,
BigValueGraphMode,
BigValueJustifyMode,
BigValueTextMode,
LoadingPlaceholder,
ScrollContainer,
useStyles2,
} from '@grafana/ui';
import { config } from 'app/core/config';
import alertDef from 'app/features/alerting/state/alertDef';
import { alertRuleApi } from 'app/features/alerting/unified/api/alertRuleApi';
import { INSTANCES_DISPLAY_LIMIT } from 'app/features/alerting/unified/components/rules/RuleDetails';
import { useCombinedRuleNamespaces } from 'app/features/alerting/unified/hooks/useCombinedRuleNamespaces';
import { useUnifiedAlertingSelector } from 'app/features/alerting/unified/hooks/useUnifiedAlertingSelector';
import {
fetchAllPromAndRulerRulesAction,
fetchPromAndRulerRulesAction,
} from 'app/features/alerting/unified/state/actions';
import { Annotation } from 'app/features/alerting/unified/utils/constants';
import { GRAFANA_DATASOURCE_NAME, GRAFANA_RULES_SOURCE_NAME } from 'app/features/alerting/unified/utils/datasource';
import { parsePromQLStyleMatcherLooseSafe } from 'app/features/alerting/unified/utils/matchers';
import {
isAsyncRequestMapSlicePartiallyDispatched,
isAsyncRequestMapSlicePartiallyFulfilled,
isAsyncRequestMapSlicePending,
} 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 { DashboardModel } from 'app/features/dashboard/state/DashboardModel';
import { Matcher } from 'app/plugins/datasource/alertmanager/types';
import { ThunkDispatch, useDispatch } from 'app/types';
import { PromAlertingRuleState } from 'app/types/unified-alerting-dto';
import { AlertingAction, useAlertingAbility } from '../../../features/alerting/unified/hooks/useAbilities';
import { getAlertingRule } from '../../../features/alerting/unified/utils/rules';
import { AlertingRule, CombinedRuleWithLocation } from '../../../types/unified-alerting';
import { GroupMode, SortOrder, StateFilter, UnifiedAlertListOptions, ViewMode } from './types';
import GroupedModeView from './unified-alerting/GroupedView';
import UngroupedModeView from './unified-alerting/UngroupedView';
import { filterAlerts } from './util';
function getStateList(state: StateFilter) {
const reducer = (list: string[], [stateKey, value]: [string, boolean]) => {
if (Boolean(value)) {
return [...list, stateKey];
} else {
return list;
}
};
return Object.entries(state).reduce(reducer, []);
}
const fetchPromAndRuler = ({
dispatch,
limitInstances,
matcherList,
dataSourceName,
stateList,
}: {
dispatch: ThunkDispatch;
limitInstances: boolean;
matcherList?: Matcher[] | undefined;
dataSourceName?: string;
stateList: string[];
}) => {
if (dataSourceName) {
dispatch(
fetchPromAndRulerRulesAction({
rulesSourceName: dataSourceName,
limitAlerts: limitInstances ? INSTANCES_DISPLAY_LIMIT : undefined,
matcher: matcherList,
state: stateList,
})
);
} else {
dispatch(
fetchAllPromAndRulerRulesAction(false, {
limitAlerts: limitInstances ? INSTANCES_DISPLAY_LIMIT : undefined,
matcher: matcherList,
state: stateList,
})
);
}
};
function UnifiedAlertList(props: PanelProps<UnifiedAlertListOptions>) {
const { t } = useTranslate();
const dispatch = useDispatch();
const [limitInstances, toggleLimit] = useToggle(true);
const [, gmaViewAllowed] = useAlertingAbility(AlertingAction.ViewAlertRule);
const { usePrometheusRulesByNamespaceQuery } = alertRuleApi;
const promRulesRequests = useUnifiedAlertingSelector((state) => state.promRules);
const rulerRulesRequests = useUnifiedAlertingSelector((state) => state.rulerRules);
const somePromRulesDispatched = isAsyncRequestMapSlicePartiallyDispatched(promRulesRequests);
const hideViewRuleLinkText = props.width < 320;
// backwards compat for "Inactive" state filter
useEffect(() => {
if (props.options.stateFilter.inactive === true) {
props.options.stateFilter.normal = true; // enable the normal filter
}
props.options.stateFilter.inactive = undefined; // now disable inactive
}, [props.options.stateFilter]);
let dashboard: DashboardModel | undefined = undefined;
useEffectOnce(() => {
dashboard = getDashboardSrv().getCurrent();
});
const stateList = useMemo(() => getStateList(props.options.stateFilter), [props.options.stateFilter]);
const { options, replaceVariables } = props;
const dataSourceName =
options.datasource === GRAFANA_DATASOURCE_NAME ? GRAFANA_RULES_SOURCE_NAME : options.datasource;
const parsedOptions: UnifiedAlertListOptions = {
...props.options,
alertName: replaceVariables(options.alertName),
alertInstanceLabelFilter: replaceVariables(options.alertInstanceLabelFilter),
};
const matcherList = useMemo(
() => parsePromQLStyleMatcherLooseSafe(parsedOptions.alertInstanceLabelFilter),
[parsedOptions.alertInstanceLabelFilter]
);
// If the datasource is not defined we should NOT skip the query
// Undefined dataSourceName means that there is no datasource filter applied and we should fetch all the rules
const shouldFetchGrafanaRules = (!dataSourceName || dataSourceName === GRAFANA_RULES_SOURCE_NAME) && gmaViewAllowed;
//For grafana managed rules, get the result using RTK Query to avoid the need of using the redux store
//See https://github.com/grafana/grafana/pull/70482
const {
currentData: grafanaPromRules = [],
isLoading: grafanaRulesLoading,
refetch: refetchGrafanaPromRules,
} = usePrometheusRulesByNamespaceQuery(
{
limitAlerts: limitInstances ? INSTANCES_DISPLAY_LIMIT : undefined,
matcher: matcherList,
state: stateList,
},
{ skip: !shouldFetchGrafanaRules }
);
useEffect(() => {
//we need promRules and rulerRules for getting the uid when creating the alert link in panel in case of being a rulerRule.
if (!promRulesRequests.loading) {
fetchPromAndRuler({ dispatch, limitInstances, matcherList, dataSourceName, stateList });
}
const sub = dashboard?.events.subscribe(TimeRangeUpdatedEvent, () => {
if (shouldFetchGrafanaRules) {
refetchGrafanaPromRules();
}
if (!dataSourceName || dataSourceName !== GRAFANA_RULES_SOURCE_NAME) {
fetchPromAndRuler({ dispatch, limitInstances, matcherList, dataSourceName, stateList });
}
});
return () => {
sub?.unsubscribe();
};
}, [
dispatch,
dashboard,
matcherList,
stateList,
limitInstances,
dataSourceName,
refetchGrafanaPromRules,
shouldFetchGrafanaRules,
promRulesRequests.loading,
]);
const handleInstancesLimit = (limit: boolean) => {
if (limit) {
fetchPromAndRuler({ dispatch, limitInstances, matcherList, dataSourceName, stateList });
toggleLimit(true);
} else {
fetchPromAndRuler({ dispatch, limitInstances: false, matcherList, dataSourceName, stateList });
toggleLimit(false);
}
};
const combinedRules = useCombinedRuleNamespaces(undefined, grafanaPromRules);
const someRulerRulesDispatched = isAsyncRequestMapSlicePartiallyDispatched(rulerRulesRequests);
const haveResults = isAsyncRequestMapSlicePartiallyFulfilled(promRulesRequests);
const dispatched = somePromRulesDispatched || someRulerRulesDispatched;
const loading = isAsyncRequestMapSlicePending(promRulesRequests);
const styles = useStyles2(getStyles);
const flattenedCombinedRules = flattenCombinedRules(combinedRules);
const order = props.options.sortOrder;
const rules = useMemo(
() => filterRules(props, sortRules(order, flattenedCombinedRules)),
[flattenedCombinedRules, order, props]
);
const noAlertsMessage = rules.length === 0 ? 'No alerts matching filters' : undefined;
const renderLoading = grafanaRulesLoading || (dispatched && loading && !haveResults);
const havePreviousResults = Object.values(promRulesRequests).some((state) => state.result);
return (
<ScrollContainer minHeight="100%">
{havePreviousResults && noAlertsMessage && <div className={styles.noAlertsMessage}>{noAlertsMessage}</div>}
{havePreviousResults && (
<section>
{props.options.viewMode === ViewMode.Stat && (
<BigValue
width={props.width}
height={props.height}
graphMode={BigValueGraphMode.None}
textMode={BigValueTextMode.Auto}
justifyMode={BigValueJustifyMode.Auto}
theme={config.theme2}
value={{ text: `${rules.length}`, numeric: rules.length }}
/>
)}
{props.options.viewMode === ViewMode.List && props.options.groupMode === GroupMode.Custom && (
<GroupedModeView rules={rules} options={parsedOptions} />
)}
{props.options.viewMode === ViewMode.List && props.options.groupMode === GroupMode.Default && (
<UngroupedModeView
rules={rules}
options={parsedOptions}
handleInstancesLimit={handleInstancesLimit}
limitInstances={limitInstances}
hideViewRuleLinkText={hideViewRuleLinkText}
/>
)}
</section>
)}
{/* loading moved here to avoid twitching */}
{renderLoading && <LoadingPlaceholder text={t('alertlist.unified-alert-list.text-loading', 'Loading...')} />}
</ScrollContainer>
);
}
function sortRules(sortOrder: SortOrder, rules: CombinedRuleWithLocation[]) {
if (sortOrder === SortOrder.Importance) {
// @ts-ignore
return sortBy(rules, (rule) => alertDef.alertStateSortScore[rule.state]);
} else if (sortOrder === SortOrder.TimeAsc) {
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) {
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.name.toLowerCase());
if (sortOrder === SortOrder.AlphaDesc) {
result.reverse();
}
return result;
}
function filterRules(props: PanelProps<UnifiedAlertListOptions>, rules: CombinedRuleWithLocation[]) {
const { options, replaceVariables } = props;
let filteredRules = [...rules];
if (options.dashboardAlerts) {
const dashboardUid = getDashboardSrv().getCurrent()?.uid;
filteredRules = filteredRules.filter(({ annotations = {} }) =>
Object.entries(annotations).some(([key, value]) => key === Annotation.dashboardUID && value === dashboardUid)
);
}
if (options.alertName) {
const replacedName = replaceVariables(options.alertName);
filteredRules = filteredRules.filter(({ name }) =>
name.toLocaleLowerCase().includes(replacedName.toLocaleLowerCase())
);
}
filteredRules = filteredRules.filter((rule) => {
const alertingRule = getAlertingRule(rule);
if (!alertingRule) {
return false;
}
return (
(options.stateFilter.firing && alertingRule.state === PromAlertingRuleState.Firing) ||
(options.stateFilter.pending && alertingRule.state === PromAlertingRuleState.Pending) ||
(options.stateFilter.normal && alertingRule.state === PromAlertingRuleState.Inactive) ||
(options.stateFilter.recovering && alertingRule.state === PromAlertingRuleState.Recovering)
);
});
if (options.folder && options.folder.uid) {
filteredRules = filteredRules.filter((rule) => {
return rule.namespace.uid === options.folder.uid;
});
}
if (options.datasource) {
const isGrafanaDS = options.datasource === GRAFANA_DATASOURCE_NAME;
filteredRules = filteredRules.filter(
isGrafanaDS
? ({ dataSourceName }) => dataSourceName === GRAFANA_RULES_SOURCE_NAME
: ({ dataSourceName }) => dataSourceName === options.datasource
);
}
// Remove rules having 0 instances unless explicitly configured
// AlertInstances filters instances and we need to prevent situation
// when we display a rule with 0 instances
filteredRules = filteredRules.reduce<CombinedRuleWithLocation[]>((rules, rule) => {
const alertingRule = getAlertingRule(rule);
const filteredAlerts = alertingRule
? filterAlerts(
{
stateFilter: options.stateFilter,
alertInstanceLabelFilter: replaceVariables(options.alertInstanceLabelFilter),
},
alertingRule.alerts ?? []
)
: [];
if (
filteredAlerts.length ||
(alertingRule?.state === PromAlertingRuleState.Inactive &&
options.showInactiveAlerts &&
!options.alertInstanceLabelFilter.length)
) {
// We intentionally don't set alerts to filteredAlerts
// because later we couldn't display that some alerts are hidden (ref AlertInstances filtering)
rules.push(rule);
}
return rules;
}, []);
return filteredRules;
}
export const getStyles = (theme: GrafanaTheme2) => ({
cardContainer: css({
padding: theme.spacing(0.5, 0, 0.25, 0),
lineHeight: theme.typography.body.lineHeight,
marginBottom: 0,
}),
alertRuleList: css({
display: 'flex',
flexWrap: 'wrap',
justifyContent: 'space-between',
listStyleType: 'none',
}),
alertRuleItem: css({
display: 'flex',
alignItems: 'center',
width: '100%',
height: '100%',
background: theme.colors.background.secondary,
padding: theme.spacing(0.5, 1),
borderRadius: theme.shape.radius.default,
marginBottom: theme.spacing(0.5),
gap: theme.spacing(2),
}),
alertName: css({
fontSize: theme.typography.h6.fontSize,
fontWeight: theme.typography.fontWeightBold,
whiteSpace: 'nowrap',
overflow: 'hidden',
textOverflow: 'ellipsis',
}),
alertNameWrapper: css({
display: 'flex',
flex: 1,
flexWrap: 'nowrap',
flexDirection: 'column',
minWidth: '100px',
}),
alertLabels: css({
'> *': {
marginRight: theme.spacing(0.5),
},
}),
alertDuration: css({
fontSize: theme.typography.bodySmall.fontSize,
}),
alertRuleItemText: css({
fontWeight: theme.typography.fontWeightBold,
fontSize: theme.typography.bodySmall.fontSize,
margin: 0,
}),
alertRuleItemTime: css({
color: theme.colors.text.secondary,
fontWeight: 'normal',
whiteSpace: 'nowrap',
}),
alertRuleItemInfo: css({
fontWeight: 'normal',
flexGrow: 2,
display: 'flex',
alignItems: 'flex-end',
}),
noAlertsMessage: css({
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
width: '100%',
height: '100%',
}),
alertIcon: css({
marginRight: theme.spacing(0.5),
}),
instanceDetails: css({
minWidth: '1px',
whiteSpace: 'nowrap',
overflow: 'hidden',
textOverflow: 'ellipsis',
}),
customGroupDetails: css({
marginBottom: theme.spacing(0.5),
}),
link: css({
wordBreak: 'break-all',
color: theme.colors.primary.text,
display: 'flex',
alignItems: 'center',
gap: theme.spacing(1),
}),
hidden: css({
display: 'none',
}),
});
export function UnifiedAlertListPanel(props: PanelProps<UnifiedAlertListOptions>) {
const { t } = useTranslate();
const [, gmaReadAllowed] = useAlertingAbility(AlertingAction.ViewAlertRule);
const [, externalReadAllowed] = useAlertingAbility(AlertingAction.ViewExternalAlertRule);
if (!gmaReadAllowed && !externalReadAllowed) {
return (
<Alert title={t('alertlist.unified-alert-list-panel.title-permission-required', 'Permission required')}>
<Trans i18nKey="alertlist.unified-alert-list-panel.body-permission-required">
Sorry, you do not have the required permissions to read alert rules.
</Trans>
</Alert>
);
}
return <UnifiedAlertList {...props} />;
}