import { css, cx } from '@emotion/css'; import { countBy, sum } from 'lodash'; import { useMemo, useState } from 'react'; import * as React from 'react'; import { GrafanaTheme2 } from '@grafana/data'; import { LinkButton, useStyles2 } from '@grafana/ui'; import { MatcherFilter } from 'app/features/alerting/unified/components/alert-groups/MatcherFilter'; import { AlertInstanceStateFilter, InstanceStateFilter, } from 'app/features/alerting/unified/components/rules/AlertInstanceStateFilter'; import { labelsMatchMatchers } from 'app/features/alerting/unified/utils/alertmanager'; import { createViewLink, sortAlerts } from 'app/features/alerting/unified/utils/misc'; import { SortOrder } from 'app/plugins/panel/alertlist/types'; import { Alert, CombinedRule, PaginationProps } from 'app/types/unified-alerting'; import { mapStateWithReasonToBaseState } from 'app/types/unified-alerting-dto'; import { GRAFANA_RULES_SOURCE_NAME, isGrafanaRulesSource } from '../../utils/datasource'; import { parsePromQLStyleMatcherLooseSafe } from '../../utils/matchers'; import { isAlertingRule } from '../../utils/rules'; import { AlertInstancesTable } from './AlertInstancesTable'; import { getComponentsFromStats } from './RuleStats'; interface Props { rule: CombinedRule; pagination?: PaginationProps; itemsDisplayLimit?: number; enableFiltering?: boolean; } interface ShowMoreStats { totalItemsCount: number; visibleItemsCount: number; } interface ShowMoreInstancesProps { stats: ShowMoreStats; onClick?: React.ComponentProps['onClick']; href?: React.ComponentProps['href']; } function ShowMoreInstances({ stats, onClick, href }: ShowMoreInstancesProps) { const styles = useStyles2(getStyles); return (
Showing {stats.visibleItemsCount} out of {stats.totalItemsCount} instances
Show all {stats.totalItemsCount} alert instances
); } export function RuleDetailsMatchingInstances(props: Props) { const { rule, itemsDisplayLimit = Number.POSITIVE_INFINITY, pagination, enableFiltering = false } = props; const { promRule, namespace, instanceTotals } = rule; const [queryString, setQueryString] = useState(); const [alertState, setAlertState] = useState(); // This key is used to force a rerender on the inputs when the filters are cleared const [filterKey] = useState(Math.floor(Math.random() * 100)); const queryStringKey = `queryString-${filterKey}`; const styles = useStyles2(getStyles); const stateFilterType = isGrafanaRulesSource(namespace.rulesSource) ? GRAFANA_RULES_SOURCE_NAME : 'prometheus'; const alerts = useMemo( (): Alert[] => isAlertingRule(promRule) && promRule.alerts?.length ? filterAlerts(queryString, alertState, sortAlerts(SortOrder.Importance, promRule.alerts)) : [], [promRule, alertState, queryString] ); if (!isAlertingRule(promRule)) { return null; } const visibleInstances = alerts.slice(0, itemsDisplayLimit); // Count All By State is used only when filtering is enabled and we have access to all instances const countAllByState = countBy(promRule.alerts, (alert) => mapStateWithReasonToBaseState(alert.state)); // error state is not a separate state const totalInstancesCount = sum([ instanceTotals.alerting, instanceTotals.inactive, instanceTotals.pending, instanceTotals.nodata, ]); const hiddenInstancesCount = totalInstancesCount - visibleInstances.length; const stats: ShowMoreStats = { totalItemsCount: totalInstancesCount, visibleItemsCount: visibleInstances.length, }; // createViewLink returns a link containing the app subpath prefix hence cannot be used // in locationService.push as it will result in a double prefix const ruleViewPageLink = createViewLink(namespace.rulesSource, props.rule, location.pathname + location.search); const statsComponents = getComponentsFromStats(instanceTotals); const resetFilter = () => setAlertState(undefined); const footerRow = hiddenInstancesCount ? ( ) : undefined; return ( <> {enableFiltering && (
setQueryString(value)} />
)} {!enableFiltering &&
{statsComponents}
} ); } function filterAlerts( alertInstanceLabel: string | undefined, alertInstanceState: InstanceStateFilter | undefined, alerts: Alert[] ): Alert[] { let filteredAlerts = [...alerts]; if (alertInstanceLabel) { const matchers = alertInstanceLabel ? parsePromQLStyleMatcherLooseSafe(alertInstanceLabel) : []; filteredAlerts = filteredAlerts.filter(({ labels }) => labelsMatchMatchers(labels, matchers)); } if (alertInstanceState) { filteredAlerts = filteredAlerts.filter((alert) => { return mapStateWithReasonToBaseState(alert.state) === alertInstanceState; }); } return filteredAlerts; } const getStyles = (theme: GrafanaTheme2) => { return { flexRow: css({ display: 'flex', flexDirection: 'row', alignItems: 'flex-end', width: '100%', flexWrap: 'wrap', marginBottom: theme.spacing(1), gap: theme.spacing(1), }), spaceBetween: css({ justifyContent: 'space-between', }), footerRow: css({ display: 'flex', flexDirection: 'column', gap: theme.spacing(1), justifyContent: 'space-between', alignItems: 'center', width: '100%', }), instancesContainer: css({ marginBottom: theme.spacing(2), }), stats: css({ display: 'flex', gap: theme.spacing(1), padding: theme.spacing(1, 0), }), }; };