Files
Sonia Aguilar 64ee42d01e Alerting: Add limits and move state and label matching filters to the BE (#66267)
* WIP

* Add instance totals to combined rule. Use totals to display instances stats in the UI

* WIP

* add global summaries, fix TS errors

* fix useCombined test

* fix test

* use activeAt from rule when available

* Fix NaN in global stats

* Add no data total to global summary

* Add totals recalculation for filtered rules

* Fix instances totals, remove instances filtering from alert list view

* Update tests

* Fetch alerts considering filtering label matchers

* WIP - Fetch alerts appending state filter to endpoint

* Fix multiple values for state in request being applyied

* fix test

* Calculate hidden by for grafana managed alerts

* Use INSTANCES_DISPLAY_LIMIT constant for limiting alert instances instead of 1

* Rename matchers parameter according to API changes

* Fix calculating total number of grafana instances

* Rename matcher prop after previous change

* Display button to remove max instances limit

* Change matcher query param to be an array of strings

* Add test for paramsWithMatcherAndState method

* Refactor matcher to be an string array to be consistent with state

* Use matcher query string as matcher object type (encoded JSON)

* Avoind encoding matcher parameters twice

* fix tests

* Enable toggle for the limit/show all button and restore limit and filters when we come back from custom view

* Move getMatcherListFromString method to utils/alertmanager.ts

* Fix limit toggle button being shown when it's not necessary

* Use filteredTotals from be response to calculate hidden by count

* Fix variables not being replaced correctly

* Fix total shown to be all the instances filtered without limits

* Adress some PR review comments

* Move paramsWithMatcherAndState inside prometheusUrlBuilder method

---------

Co-authored-by: Gilles De Mey <gilles.de.mey@gmail.com>
Co-authored-by: Konrad Lalik <konrad.lalik@grafana.com>
Co-authored-by: Virginia Cepeda <virginia.cepeda@grafana.com>
2023-04-25 11:19:20 +02:00

139 lines
3.6 KiB
TypeScript

import { isUndefined, omitBy, sum } from 'lodash';
import pluralize from 'pluralize';
import React, { Fragment } from 'react';
import { Stack } from '@grafana/experimental';
import { Badge } from '@grafana/ui';
import {
AlertGroupTotals,
AlertInstanceTotalState,
CombinedRuleGroup,
CombinedRuleNamespace,
} from 'app/types/unified-alerting';
import { PromAlertingRuleState } from 'app/types/unified-alerting-dto';
interface Props {
namespaces: CombinedRuleNamespace[];
}
// All available states for a rule need to be initialized to prevent NaN values when adding a number and undefined
const emptyStats: Required<AlertGroupTotals> = {
recording: 0,
alerting: 0,
[PromAlertingRuleState.Pending]: 0,
[PromAlertingRuleState.Inactive]: 0,
paused: 0,
error: 0,
nodata: 0,
};
export const RuleStats = ({ namespaces }: Props) => {
const stats = { ...emptyStats };
// sum all totals for all namespaces
namespaces.forEach(({ groups }) => {
groups.forEach((group) => {
const groupTotals = omitBy(group.totals, isUndefined);
for (let key in groupTotals) {
// @ts-ignore
stats[key] += groupTotals[key];
}
});
});
const statsComponents = getComponentsFromStats(stats);
const hasStats = Boolean(statsComponents.length);
const total = sum(Object.values(stats));
statsComponents.unshift(
<Fragment key="total">
{total} {pluralize('rule', total)}
</Fragment>
);
return (
<Stack direction="row">
{hasStats && (
<div>
<Stack gap={0.5}>{statsComponents}</Stack>
</div>
)}
</Stack>
);
};
interface RuleGroupStatsProps {
group: CombinedRuleGroup;
}
export const RuleGroupStats = ({ group }: RuleGroupStatsProps) => {
const stats = group.totals;
const evaluationInterval = group?.interval;
const statsComponents = getComponentsFromStats(stats);
const hasStats = Boolean(statsComponents.length);
return (
<Stack direction="row">
{hasStats && (
<div>
<Stack gap={0.5}>{statsComponents}</Stack>
</div>
)}
{evaluationInterval && (
<>
<div>|</div>
<Badge text={evaluationInterval} icon="clock-nine" color={'blue'} />
</>
)}
</Stack>
);
};
export function getComponentsFromStats(
stats: Partial<Record<AlertInstanceTotalState | 'paused' | 'recording', number>>
) {
const statsComponents: React.ReactNode[] = [];
if (stats[AlertInstanceTotalState.Alerting]) {
statsComponents.push(<Badge color="red" key="firing" text={`${stats[AlertInstanceTotalState.Alerting]} firing`} />);
}
if (stats.error) {
statsComponents.push(<Badge color="red" key="errors" text={`${stats.error} errors`} />);
}
if (stats.nodata) {
statsComponents.push(<Badge color="blue" key="nodata" text={`${stats.nodata} no data`} />);
}
if (stats[AlertInstanceTotalState.Pending]) {
statsComponents.push(
<Badge color={'orange'} key="pending" text={`${stats[AlertInstanceTotalState.Pending]} pending`} />
);
}
if (stats[AlertInstanceTotalState.Normal] && stats.paused) {
statsComponents.push(
<Badge
color="green"
key="paused"
text={`${stats[AlertInstanceTotalState.Normal]} normal (${stats.paused} paused)`}
/>
);
}
if (stats[AlertInstanceTotalState.Normal] && !stats.paused) {
statsComponents.push(
<Badge color="green" key="inactive" text={`${stats[AlertInstanceTotalState.Normal]} normal`} />
);
}
if (stats.recording) {
statsComponents.push(<Badge color="purple" key="recording" text={`${stats.recording} recording`} />);
}
return statsComponents;
}