grafana_bak/public/app/features/alerting/unified/components/rules/RuleDetailsMatchingInstances.tsx
2025-04-01 10:38:02 +09:00

196 lines
6.5 KiB
TypeScript

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<typeof LinkButton>['onClick'];
href?: React.ComponentProps<typeof LinkButton>['href'];
}
function ShowMoreInstances({ stats, onClick, href }: ShowMoreInstancesProps) {
const styles = useStyles2(getStyles);
return (
<div className={styles.footerRow}>
<div>
Showing {stats.visibleItemsCount} out of {stats.totalItemsCount} instances
</div>
<LinkButton size="sm" variant="secondary" data-testid="show-all" onClick={onClick} href={href}>
Show all {stats.totalItemsCount} alert instances
</LinkButton>
</div>
);
}
export function RuleDetailsMatchingInstances(props: Props) {
const { rule, itemsDisplayLimit = Number.POSITIVE_INFINITY, pagination, enableFiltering = false } = props;
const { promRule, namespace, instanceTotals } = rule;
const [queryString, setQueryString] = useState<string>();
const [alertState, setAlertState] = useState<InstanceStateFilter>();
// This key is used to force a rerender on the inputs when the filters are cleared
const [filterKey] = useState<number>(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 ? (
<ShowMoreInstances
stats={stats}
onClick={enableFiltering ? resetFilter : undefined}
href={!enableFiltering ? ruleViewPageLink : undefined}
/>
) : undefined;
return (
<>
{enableFiltering && (
<div className={cx(styles.flexRow, styles.spaceBetween)}>
<div className={styles.flexRow}>
<MatcherFilter
key={queryStringKey}
defaultQueryString={queryString}
onFilterChange={(value) => setQueryString(value)}
/>
<AlertInstanceStateFilter
filterType={stateFilterType}
stateFilter={alertState}
onStateFilterChange={setAlertState}
itemPerStateStats={countAllByState}
/>
</div>
</div>
)}
{!enableFiltering && <div className={styles.stats}>{statsComponents}</div>}
<AlertInstancesTable rule={rule} instances={visibleInstances} pagination={pagination} footerRow={footerRow} />
</>
);
}
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),
}),
};
};