diff --git a/public/app/features/alerting/unified/components/rules/EditRuleGroupModal.test.tsx b/public/app/features/alerting/unified/components/rules/EditRuleGroupModal.test.tsx
index 79c54e4023f..77631b55177 100644
--- a/public/app/features/alerting/unified/components/rules/EditRuleGroupModal.test.tsx
+++ b/public/app/features/alerting/unified/components/rules/EditRuleGroupModal.test.tsx
@@ -60,7 +60,7 @@ describe('EditGroupModal', () => {
const namespace = mockCombinedRuleNamespace({
name: 'my-alerts',
rulesSource: mockDataSource(),
- groups: [{ name: 'default-group', interval: '90s', rules: [] }],
+ groups: [{ name: 'default-group', interval: '90s', rules: [], totals: {} }],
});
const group = namespace.groups[0];
@@ -100,7 +100,9 @@ describe('EditGroupModal component on cloud alert rules', () => {
const promNs = mockCombinedRuleNamespace({
name: 'prometheus-ns',
rulesSource: promDsSettings,
- groups: [{ name: 'default-group', interval: '90s', rules: [alertingRule, recordingRule1, recordingRule2] }],
+ groups: [
+ { name: 'default-group', interval: '90s', rules: [alertingRule, recordingRule1, recordingRule2], totals: {} },
+ ],
});
const group = promNs.groups[0];
@@ -121,7 +123,7 @@ describe('EditGroupModal component on cloud alert rules', () => {
const promNs = mockCombinedRuleNamespace({
name: 'prometheus-ns',
rulesSource: promDsSettings,
- groups: [{ name: 'default-group', interval: '90s', rules: [recordingRule1, recordingRule2] }],
+ groups: [{ name: 'default-group', interval: '90s', rules: [recordingRule1, recordingRule2], totals: {} }],
});
const group = promNs.groups[0];
@@ -154,6 +156,7 @@ describe('EditGroupModal component on grafana-managed alert rules', () => {
rulerRule: mockRulerAlertingRule({ alert: 'high-memory' }),
}),
],
+ totals: {},
},
],
};
diff --git a/public/app/features/alerting/unified/components/rules/RuleDetails.tsx b/public/app/features/alerting/unified/components/rules/RuleDetails.tsx
index 7899ac4ac30..c38efc5bd45 100644
--- a/public/app/features/alerting/unified/components/rules/RuleDetails.tsx
+++ b/public/app/features/alerting/unified/components/rules/RuleDetails.tsx
@@ -25,7 +25,7 @@ interface Props {
// The limit is set to 15 in order to upkeep the good performance
// and to encourage users to go to the rule details page to see the rest of the instances
// We don't want to paginate the instances list on the alert list page
-const INSTANCES_DISPLAY_LIMIT = 15;
+export const INSTANCES_DISPLAY_LIMIT = 15;
export const RuleDetails = ({ rule }: Props) => {
const styles = useStyles2(getStyles);
diff --git a/public/app/features/alerting/unified/components/rules/RuleDetailsMatchingInstances.test.tsx b/public/app/features/alerting/unified/components/rules/RuleDetailsMatchingInstances.test.tsx
index 4c7cfd599b7..d3d5b197067 100644
--- a/public/app/features/alerting/unified/components/rules/RuleDetailsMatchingInstances.test.tsx
+++ b/public/app/features/alerting/unified/components/rules/RuleDetailsMatchingInstances.test.tsx
@@ -32,7 +32,7 @@ describe('RuleDetailsMatchingInstances', () => {
it('For Grafana Managed rules instances filter should contain five states', () => {
const rule = mockCombinedRule();
- render(
);
+ render(
);
const stateFilter = ui.stateFilter.get();
expect(stateFilter).toBeInTheDocument();
@@ -69,7 +69,7 @@ describe('RuleDetailsMatchingInstances', () => {
[GrafanaAlertState.Error]: ui.grafanaStateButton.error,
};
- render(
);
+ render(
);
await userEvent.click(buttons[state].get());
@@ -82,7 +82,7 @@ describe('RuleDetailsMatchingInstances', () => {
namespace: mockPromNamespace(),
});
- render(
);
+ render(
);
const stateFilter = ui.stateFilter.get();
expect(stateFilter).toBeInTheDocument();
@@ -108,7 +108,7 @@ describe('RuleDetailsMatchingInstances', () => {
}),
});
- render(
);
+ render(
);
await userEvent.click(ui.cloudStateButton[state].get());
@@ -122,7 +122,7 @@ describe('RuleDetailsMatchingInstances', () => {
function mockPromNamespace(): CombinedRuleNamespace {
return {
rulesSource: mockDataSource(),
- groups: [{ name: 'Prom rules group', rules: [] }],
+ groups: [{ name: 'Prom rules group', rules: [], totals: {} }],
name: 'Prometheus-test',
};
}
diff --git a/public/app/features/alerting/unified/components/rules/RuleDetailsMatchingInstances.tsx b/public/app/features/alerting/unified/components/rules/RuleDetailsMatchingInstances.tsx
index f9bbf24de4b..c52170a4d6c 100644
--- a/public/app/features/alerting/unified/components/rules/RuleDetailsMatchingInstances.tsx
+++ b/public/app/features/alerting/unified/components/rules/RuleDetailsMatchingInstances.tsx
@@ -1,5 +1,5 @@
import { css, cx } from '@emotion/css';
-import { countBy } from 'lodash';
+import { countBy, sum } from 'lodash';
import React, { useMemo, useState } from 'react';
import { GrafanaTheme2 } from '@grafana/data';
@@ -20,11 +20,13 @@ import { isAlertingRule } from '../../utils/rules';
import { DetailsField } from '../DetailsField';
import { AlertInstancesTable } from './AlertInstancesTable';
+import { getComponentsFromStats } from './RuleStats';
interface Props {
rule: CombinedRule;
pagination?: PaginationProps;
itemsDisplayLimit?: number;
+ enableFiltering?: boolean;
}
interface ShowMoreStats {
@@ -52,9 +54,10 @@ function ShowMoreInstances(props: { ruleViewPageLink: string; stats: ShowMoreSta
export function RuleDetailsMatchingInstances(props: Props): JSX.Element | null {
const {
- rule: { promRule, namespace },
+ rule: { promRule, namespace, instanceTotals },
itemsDisplayLimit = Number.POSITIVE_INFINITY,
pagination,
+ enableFiltering = false,
} = props;
const [queryString, setQueryString] = useState
();
@@ -82,40 +85,45 @@ export function RuleDetailsMatchingInstances(props: Props): JSX.Element | 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));
- const hiddenItemsCount = alerts.length - visibleInstances.length;
+ const totalInstancesCount = sum(Object.values(instanceTotals));
+ const hiddenInstancesCount = totalInstancesCount - visibleInstances.length;
const stats: ShowMoreStats = {
- totalItemsCount: alerts.length,
+ totalItemsCount: totalInstancesCount,
visibleItemsCount: visibleInstances.length,
};
const ruleViewPageLink = createViewLink(namespace.rulesSource, props.rule, location.pathname + location.search);
+ const statsComponents = getComponentsFromStats(instanceTotals);
- const footerRow = hiddenItemsCount ? (
+ const footerRow = hiddenInstancesCount ? (
) : undefined;
return (
-
-
-
setQueryString(value)}
- />
-
+ {enableFiltering && (
+
+
+
setQueryString(value)}
+ />
+
+
-
-
+ )}
+ {!enableFiltering &&
{statsComponents}
}
);
@@ -164,5 +172,13 @@ const getStyles = (theme: GrafanaTheme2) => {
align-items: center;
width: 100%;
`,
+ instancesContainer: css`
+ margin-bottom: ${theme.spacing(2)};
+ `,
+ stats: css`
+ display: flex;
+ gap: ${theme.spacing(1)};
+ padding: ${theme.spacing(1, 0)};
+ `,
};
};
diff --git a/public/app/features/alerting/unified/components/rules/RuleListGroupView.test.tsx b/public/app/features/alerting/unified/components/rules/RuleListGroupView.test.tsx
index 82ffb120c84..9d0c05fac86 100644
--- a/public/app/features/alerting/unified/components/rules/RuleListGroupView.test.tsx
+++ b/public/app/features/alerting/unified/components/rules/RuleListGroupView.test.tsx
@@ -116,6 +116,7 @@ function getGrafanaNamespace(): CombinedRuleNamespace {
{
name: 'default',
rules: [mockCombinedRule()],
+ totals: {},
},
],
};
@@ -129,6 +130,7 @@ function getCloudNamespace(): CombinedRuleNamespace {
{
name: 'Prom group',
rules: [mockCombinedRule()],
+ totals: {},
},
],
};
diff --git a/public/app/features/alerting/unified/components/rules/RuleState.tsx b/public/app/features/alerting/unified/components/rules/RuleState.tsx
index a16c8324ead..11d95a99982 100644
--- a/public/app/features/alerting/unified/components/rules/RuleState.tsx
+++ b/public/app/features/alerting/unified/components/rules/RuleState.tsx
@@ -31,7 +31,7 @@ export const RuleState = ({ rule, isDeleting, isCreating, isPaused }: Props) =>
promRule.state !== PromAlertingRuleState.Inactive
) {
// find earliest alert
- const firstActiveAt = getFirstActiveAt(promRule);
+ const firstActiveAt = promRule.activeAt ? new Date(promRule.activeAt) : getFirstActiveAt(promRule);
// calculate time elapsed from earliest alert
if (firstActiveAt) {
diff --git a/public/app/features/alerting/unified/components/rules/RuleStats.tsx b/public/app/features/alerting/unified/components/rules/RuleStats.tsx
index 11fb219cef5..46148dee699 100644
--- a/public/app/features/alerting/unified/components/rules/RuleStats.tsx
+++ b/public/app/features/alerting/unified/components/rules/RuleStats.tsx
@@ -1,121 +1,77 @@
+import { isUndefined, omitBy, sum } from 'lodash';
import pluralize from 'pluralize';
-import React, { Fragment, useState } from 'react';
-import { useDebounce } from 'react-use';
+import React, { Fragment } from 'react';
import { Stack } from '@grafana/experimental';
import { Badge } from '@grafana/ui';
-import { CombinedRule, CombinedRuleGroup, CombinedRuleNamespace } from 'app/types/unified-alerting';
+import {
+ AlertGroupTotals,
+ AlertInstanceTotalState,
+ CombinedRuleGroup,
+ CombinedRuleNamespace,
+} from 'app/types/unified-alerting';
import { PromAlertingRuleState } from 'app/types/unified-alerting-dto';
-import { isAlertingRule, isRecordingRule, isRecordingRulerRule, isGrafanaRulerRulePaused } from '../../utils/rules';
-
interface Props {
- includeTotal?: boolean;
- group?: CombinedRuleGroup;
- namespaces?: CombinedRuleNamespace[];
+ namespaces: CombinedRuleNamespace[];
}
-const emptyStats = {
- total: 0,
+// All available states for a rule need to be initialized to prevent NaN values when adding a number and undefined
+const emptyStats: Required
= {
recording: 0,
- [PromAlertingRuleState.Firing]: 0,
+ alerting: 0,
[PromAlertingRuleState.Pending]: 0,
[PromAlertingRuleState.Inactive]: 0,
paused: 0,
error: 0,
-} as const;
+ nodata: 0,
+};
-export const RuleStats = ({ group, namespaces, includeTotal }: Props) => {
- const evaluationInterval = group?.interval;
- const [calculated, setCalculated] = useState(emptyStats);
+export const RuleStats = ({ namespaces }: Props) => {
+ const stats = { ...emptyStats };
- // Performance optimization allowing reducing number of stats calculation
- // The problem occurs when we load many data sources.
- // Then redux store gets updated multiple times in a pretty short period, triggering calculating stats many times.
- // debounce allows to skip calculations which results would be abandoned in milliseconds
- useDebounce(
- () => {
- const stats = { ...emptyStats };
-
- const calcRule = (rule: CombinedRule) => {
- if (rule.promRule && isAlertingRule(rule.promRule)) {
- if (isGrafanaRulerRulePaused(rule)) {
- stats.paused += 1;
- }
- stats[rule.promRule.state] += 1;
- }
- if (ruleHasError(rule)) {
- stats.error += 1;
- }
- if (
- (rule.promRule && isRecordingRule(rule.promRule)) ||
- (rule.rulerRule && isRecordingRulerRule(rule.rulerRule))
- ) {
- stats.recording += 1;
- }
- stats.total += 1;
- };
-
- if (group) {
- group.rules.forEach(calcRule);
+ // 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];
}
+ });
+ });
- if (namespaces) {
- namespaces.forEach((namespace) => namespace.groups.forEach((group) => group.rules.forEach(calcRule)));
- }
+ const statsComponents = getComponentsFromStats(stats);
+ const hasStats = Boolean(statsComponents.length);
- setCalculated(stats);
- },
- 400,
- [group, namespaces]
+ const total = sum(Object.values(stats));
+
+ statsComponents.unshift(
+
+ {total} {pluralize('rule', total)}
+
);
- const statsComponents: React.ReactNode[] = [];
+ return (
+
+ {hasStats && (
+
+ {statsComponents}
+
+ )}
+
+ );
+};
- if (includeTotal) {
- statsComponents.push(
-
- {calculated.total} {pluralize('rule', calculated.total)}
-
- );
- }
+interface RuleGroupStatsProps {
+ group: CombinedRuleGroup;
+}
- if (calculated[PromAlertingRuleState.Firing]) {
- statsComponents.push(
-
- );
- }
-
- if (calculated.error) {
- statsComponents.push();
- }
-
- if (calculated[PromAlertingRuleState.Pending]) {
- statsComponents.push(
-
- );
- }
-
- if (calculated[PromAlertingRuleState.Inactive] && calculated.paused) {
- statsComponents.push(
-
- );
- }
-
- if (calculated[PromAlertingRuleState.Inactive] && !calculated.paused) {
- statsComponents.push(
-
- );
- }
-
- if (calculated.recording) {
- statsComponents.push();
- }
+export const RuleGroupStats = ({ group }: RuleGroupStatsProps) => {
+ const stats = group.totals;
+ const evaluationInterval = group?.interval;
+ const statsComponents = getComponentsFromStats(stats);
const hasStats = Boolean(statsComponents.length);
return (
@@ -135,6 +91,48 @@ export const RuleStats = ({ group, namespaces, includeTotal }: Props) => {
);
};
-function ruleHasError(rule: CombinedRule) {
- return rule.promRule?.health === 'err' || rule.promRule?.health === 'error';
+export function getComponentsFromStats(
+ stats: Partial>
+) {
+ const statsComponents: React.ReactNode[] = [];
+
+ if (stats[AlertInstanceTotalState.Alerting]) {
+ statsComponents.push();
+ }
+
+ if (stats.error) {
+ statsComponents.push();
+ }
+
+ if (stats.nodata) {
+ statsComponents.push();
+ }
+
+ if (stats[AlertInstanceTotalState.Pending]) {
+ statsComponents.push(
+
+ );
+ }
+
+ if (stats[AlertInstanceTotalState.Normal] && stats.paused) {
+ statsComponents.push(
+
+ );
+ }
+
+ if (stats[AlertInstanceTotalState.Normal] && !stats.paused) {
+ statsComponents.push(
+
+ );
+ }
+
+ if (stats.recording) {
+ statsComponents.push();
+ }
+
+ return statsComponents;
}
diff --git a/public/app/features/alerting/unified/components/rules/RulesGroup.test.tsx b/public/app/features/alerting/unified/components/rules/RulesGroup.test.tsx
index 0d4f7277d13..fd6fcdb9112 100644
--- a/public/app/features/alerting/unified/components/rules/RulesGroup.test.tsx
+++ b/public/app/features/alerting/unified/components/rules/RulesGroup.test.tsx
@@ -62,6 +62,7 @@ describe('Rules group tests', () => {
const group: CombinedRuleGroup = {
name: 'TestGroup',
rules: [mockCombinedRule()],
+ totals: {},
};
const namespace: CombinedRuleNamespace = {
@@ -89,6 +90,7 @@ describe('Rules group tests', () => {
const group: CombinedRuleGroup = {
name: 'TestGroup',
rules: [mockCombinedRule()],
+ totals: {},
};
const namespace: CombinedRuleNamespace = {
@@ -147,6 +149,7 @@ describe('Rules group tests', () => {
const group: CombinedRuleGroup = {
name: 'TestGroup',
rules: [mockCombinedRule()],
+ totals: {},
};
const namespace: CombinedRuleNamespace = {
diff --git a/public/app/features/alerting/unified/components/rules/RulesGroup.tsx b/public/app/features/alerting/unified/components/rules/RulesGroup.tsx
index 3e6a5b51493..b0cd0856f07 100644
--- a/public/app/features/alerting/unified/components/rules/RulesGroup.tsx
+++ b/public/app/features/alerting/unified/components/rules/RulesGroup.tsx
@@ -23,7 +23,7 @@ import { RuleLocation } from '../RuleLocation';
import { ActionIcon } from './ActionIcon';
import { EditCloudGroupModal } from './EditRuleGroupModal';
import { ReorderCloudGroupModal } from './ReorderRuleGroupModal';
-import { RuleStats } from './RuleStats';
+import { RuleGroupStats } from './RuleStats';
import { RulesTable } from './RulesTable';
type ViewMode = 'grouped' | 'list';
@@ -217,7 +217,7 @@ export const RulesGroup = React.memo(({ group, namespace, expandAll, viewMode }:
}
-
+
{isProvisioned && (
<>
diff --git a/public/app/features/alerting/unified/hooks/useCombinedRuleNamespaces.test.ts b/public/app/features/alerting/unified/hooks/useCombinedRuleNamespaces.test.ts
index 544b2837855..bbffb07382b 100644
--- a/public/app/features/alerting/unified/hooks/useCombinedRuleNamespaces.test.ts
+++ b/public/app/features/alerting/unified/hooks/useCombinedRuleNamespaces.test.ts
@@ -8,22 +8,26 @@ describe('flattenGrafanaManagedRules', () => {
const ungroupedGroup1 = {
name: 'my-rule',
rules: [{ name: 'my-rule' }],
+ totals: {},
} as CombinedRuleGroup;
const ungroupedGroup2 = {
name: 'another-rule',
rules: [{ name: 'another-rule' }],
+ totals: {},
} as CombinedRuleGroup;
// the rules from both these groups should go in their own group name
const group1 = {
name: 'group1',
rules: [{ name: 'rule-1' }, { name: 'rule-2' }],
+ totals: {},
} as CombinedRuleGroup;
const group2 = {
name: 'group2',
rules: [{ name: 'rule-1' }, { name: 'rule-2' }],
+ totals: {},
} as CombinedRuleGroup;
const namespace1 = {
@@ -45,6 +49,7 @@ describe('flattenGrafanaManagedRules', () => {
{
name: 'default',
rules: sortRulesByName([...ungroupedGroup1.rules, ...ungroupedGroup2.rules, ...group1.rules, ...group2.rules]),
+ totals: {},
},
]);
@@ -52,6 +57,7 @@ describe('flattenGrafanaManagedRules', () => {
{
name: 'default',
rules: ungroupedGroup1.rules,
+ totals: {},
},
]);
});
diff --git a/public/app/features/alerting/unified/hooks/useCombinedRuleNamespaces.ts b/public/app/features/alerting/unified/hooks/useCombinedRuleNamespaces.ts
index 6a463c7a451..9a903261b26 100644
--- a/public/app/features/alerting/unified/hooks/useCombinedRuleNamespaces.ts
+++ b/public/app/features/alerting/unified/hooks/useCombinedRuleNamespaces.ts
@@ -1,7 +1,11 @@
-import { isEqual } from 'lodash';
+import { countBy, isEqual } from 'lodash';
import { useMemo, useRef } from 'react';
import {
+ AlertGroupTotals,
+ AlertingRule,
+ AlertInstanceTotals,
+ AlertInstanceTotalState,
CombinedRule,
CombinedRuleGroup,
CombinedRuleNamespace,
@@ -10,7 +14,12 @@ import {
RuleNamespace,
RulesSource,
} from 'app/types/unified-alerting';
-import { RulerRuleDTO, RulerRuleGroupDTO, RulerRulesConfigDTO } from 'app/types/unified-alerting-dto';
+import {
+ PromAlertingRuleState,
+ RulerRuleDTO,
+ RulerRuleGroupDTO,
+ RulerRulesConfigDTO,
+} from 'app/types/unified-alerting-dto';
import {
getAllRulesSources,
@@ -18,7 +27,13 @@ import {
isCloudRulesSource,
isGrafanaRulesSource,
} from '../utils/datasource';
-import { isAlertingRule, isAlertingRulerRule, isRecordingRulerRule } from '../utils/rules';
+import {
+ isAlertingRule,
+ isAlertingRulerRule,
+ isGrafanaRulerRule,
+ isRecordingRule,
+ isRecordingRulerRule,
+} from '../utils/rules';
import { useUnifiedAlertingSelector } from './useUnifiedAlertingSelector';
@@ -104,6 +119,7 @@ export function flattenGrafanaManagedRules(namespaces: CombinedRuleNamespace[])
newNamespace.groups.push({
name: 'default',
rules: sortRulesByName(namespace.groups.flatMap((group) => group.rules)),
+ totals: calculateAllGroupsTotals(namespace.groups),
});
return newNamespace;
@@ -116,11 +132,18 @@ export function sortRulesByName(rules: CombinedRule[]) {
function addRulerGroupsToCombinedNamespace(namespace: CombinedRuleNamespace, groups: RulerRuleGroupDTO[] = []): void {
namespace.groups = groups.map((group) => {
+ const numRecordingRules = group.rules.filter((rule) => isRecordingRulerRule(rule)).length;
+ const numPaused = group.rules.filter((rule) => isGrafanaRulerRule(rule) && rule.grafana_alert.is_paused).length;
+
const combinedGroup: CombinedRuleGroup = {
name: group.name,
interval: group.interval,
source_tenants: group.source_tenants,
rules: [],
+ totals: {
+ paused: numPaused,
+ recording: numRecordingRules,
+ },
};
combinedGroup.rules = group.rules.map((rule) => rulerRuleToCombinedRule(rule, namespace, combinedGroup));
return combinedGroup;
@@ -137,11 +160,18 @@ function addPromGroupsToCombinedNamespace(namespace: CombinedRuleNamespace, grou
combinedGroup = {
name: group.name,
rules: [],
+ totals: calculateGroupTotals(group),
};
namespace.groups.push(combinedGroup);
existingGroupsByName.set(group.name, combinedGroup);
}
+ // combine totals from ruler with totals from prometheus state API
+ combinedGroup.totals = {
+ ...combinedGroup.totals,
+ ...calculateGroupTotals(group),
+ };
+
const combinedRulesByName = new Map();
combinedGroup!.rules.forEach((r) => {
// Prometheus rules do not have to be unique by name
@@ -153,6 +183,8 @@ function addPromGroupsToCombinedNamespace(namespace: CombinedRuleNamespace, grou
const existingRule = getExistingRuleInGroup(rule, combinedRulesByName, namespace.rulesSource);
if (existingRule) {
existingRule.promRule = rule;
+ existingRule.instanceTotals = isAlertingRule(rule) ? calculateRuleTotals(rule) : {};
+ existingRule.filteredInstanceTotals = isAlertingRule(rule) ? calculateRuleFilteredTotals(rule) : {};
} else {
combinedGroup!.rules.push(promRuleToCombinedRule(rule, namespace, combinedGroup!));
}
@@ -160,6 +192,74 @@ function addPromGroupsToCombinedNamespace(namespace: CombinedRuleNamespace, grou
});
}
+export function calculateRuleTotals(rule: Pick): AlertInstanceTotals {
+ const result = countBy(rule.alerts, 'state');
+
+ if (rule.totals) {
+ const { normal, ...totals } = rule.totals;
+ return { ...totals, inactive: normal };
+ }
+
+ return {
+ alerting: result[AlertInstanceTotalState.Alerting],
+ pending: result[AlertInstanceTotalState.Pending],
+ inactive: result[AlertInstanceTotalState.Normal],
+ nodata: result[AlertInstanceTotalState.NoData],
+ error: result[AlertInstanceTotalState.Error] + result['err'], // Prometheus uses "err" instead of "error"
+ };
+}
+
+export function calculateRuleFilteredTotals(
+ rule: Pick
+): AlertInstanceTotals {
+ if (rule.totalsFiltered) {
+ const { normal, ...totals } = rule.totalsFiltered;
+ return { ...totals, inactive: normal };
+ }
+ return {};
+}
+
+export function calculateGroupTotals(group: Pick): AlertGroupTotals {
+ if (group.totals) {
+ const { firing, ...totals } = group.totals;
+
+ return {
+ ...totals,
+ alerting: firing,
+ };
+ }
+
+ const countsByState = countBy(group.rules, (rule) => isAlertingRule(rule) && rule.state);
+ const countsByHealth = countBy(group.rules, (rule) => rule.health);
+ const recordingCount = group.rules.filter((rule) => isRecordingRule(rule)).length;
+
+ return {
+ alerting: countsByState[PromAlertingRuleState.Firing],
+ error: countsByHealth.error,
+ nodata: countsByHealth.nodata,
+ inactive: countsByState[PromAlertingRuleState.Inactive],
+ pending: countsByState[PromAlertingRuleState.Pending],
+ recording: recordingCount,
+ };
+}
+
+function calculateAllGroupsTotals(groups: CombinedRuleGroup[]): AlertGroupTotals {
+ const totals: Record = {};
+
+ groups.forEach((group) => {
+ const groupTotals = group.totals;
+ Object.entries(groupTotals).forEach(([key, value]) => {
+ if (!totals[key]) {
+ totals[key] = 0;
+ }
+
+ totals[key] += value;
+ });
+ });
+
+ return totals;
+}
+
function promRuleToCombinedRule(rule: Rule, namespace: CombinedRuleNamespace, group: CombinedRuleGroup): CombinedRule {
return {
name: rule.name,
@@ -169,6 +269,8 @@ function promRuleToCombinedRule(rule: Rule, namespace: CombinedRuleNamespace, gr
promRule: rule,
namespace: namespace,
group,
+ instanceTotals: isAlertingRule(rule) ? calculateRuleTotals(rule) : {},
+ filteredInstanceTotals: isAlertingRule(rule) ? calculateRuleFilteredTotals(rule) : {},
};
}
@@ -186,6 +288,8 @@ function rulerRuleToCombinedRule(
rulerRule: rule,
namespace,
group,
+ instanceTotals: {},
+ filteredInstanceTotals: {},
}
: isRecordingRulerRule(rule)
? {
@@ -196,6 +300,8 @@ function rulerRuleToCombinedRule(
rulerRule: rule,
namespace,
group,
+ instanceTotals: {},
+ filteredInstanceTotals: {},
}
: {
name: rule.grafana_alert.title,
@@ -205,6 +311,8 @@ function rulerRuleToCombinedRule(
rulerRule: rule,
namespace,
group,
+ instanceTotals: {},
+ filteredInstanceTotals: {},
};
}
diff --git a/public/app/features/alerting/unified/hooks/useFilteredRules.ts b/public/app/features/alerting/unified/hooks/useFilteredRules.ts
index 2979c6a78cc..aea4ba65c3e 100644
--- a/public/app/features/alerting/unified/hooks/useFilteredRules.ts
+++ b/public/app/features/alerting/unified/hooks/useFilteredRules.ts
@@ -5,7 +5,7 @@ import { useCallback, useEffect, useMemo } from 'react';
import { getDataSourceSrv } from '@grafana/runtime';
import { Matcher } from 'app/plugins/datasource/alertmanager/types';
-import { CombinedRuleGroup, CombinedRuleNamespace } from 'app/types/unified-alerting';
+import { CombinedRuleGroup, CombinedRuleNamespace, Rule } from 'app/types/unified-alerting';
import { isPromAlertingRuleState, PromRuleType, RulerGrafanaRuleDTO } from 'app/types/unified-alerting-dto';
import { applySearchFilterToQuery, getSearchFilterFromQuery, RulesFilter } from '../search/rulesSearchParser';
@@ -13,6 +13,7 @@ import { labelsMatchMatchers, matcherToMatcherField, parseMatcher, parseMatchers
import { isCloudRulesSource } from '../utils/datasource';
import { getRuleHealth, isAlertingRule, isGrafanaRulerRule, isPromRuleType } from '../utils/rules';
+import { calculateGroupTotals, calculateRuleFilteredTotals, calculateRuleTotals } from './useCombinedRuleNamespaces';
import { useURLSearchParams } from './useURLSearchParams';
export function useRulesFilter() {
@@ -74,7 +75,27 @@ export function useRulesFilter() {
}
export const useFilteredRules = (namespaces: CombinedRuleNamespace[], filterState: RulesFilter) => {
- return useMemo(() => filterRules(namespaces, filterState), [namespaces, filterState]);
+ return useMemo(() => {
+ const filteredRules = filterRules(namespaces, filterState);
+
+ // Totals recalculation is a workaround for the lack of server-side filtering
+ filteredRules.forEach((namespace) => {
+ namespace.groups.forEach((group) => {
+ group.rules.forEach((rule) => {
+ if (isAlertingRule(rule.promRule)) {
+ rule.instanceTotals = calculateRuleTotals(rule.promRule);
+ rule.filteredInstanceTotals = calculateRuleFilteredTotals(rule.promRule);
+ }
+ });
+
+ group.totals = calculateGroupTotals({
+ rules: group.rules.map((r) => r.promRule).filter((r): r is Rule => !!r),
+ });
+ });
+ });
+
+ return filteredRules;
+ }, [namespaces, filterState]);
};
// Options details can be found here https://github.com/leeoniya/uFuzzy#options
diff --git a/public/app/features/alerting/unified/mocks.ts b/public/app/features/alerting/unified/mocks.ts
index 9a8bc0a82bd..34007712684 100644
--- a/public/app/features/alerting/unified/mocks.ts
+++ b/public/app/features/alerting/unified/mocks.ts
@@ -170,6 +170,7 @@ export const mockPromAlertingRule = (partial: Partial = {}): Alert
},
state: PromAlertingRuleState.Firing,
health: 'OK',
+ totalsFiltered: { alerting: 1 },
...partial,
};
};
@@ -512,16 +513,19 @@ export const mockCombinedRule = (partial?: Partial): CombinedRule
group: {
name: 'mockCombinedRuleGroup',
rules: [],
+ totals: {},
},
namespace: {
name: 'mockCombinedNamespace',
- groups: [{ name: 'mockCombinedRuleGroup', rules: [] }],
+ groups: [{ name: 'mockCombinedRuleGroup', rules: [], totals: {} }],
rulesSource: 'grafana',
},
labels: {},
annotations: {},
promRule: mockPromAlertingRule(),
rulerRule: mockRulerAlertingRule(),
+ instanceTotals: {},
+ filteredInstanceTotals: {},
...partial,
});
@@ -596,7 +600,7 @@ export function mockAlertQuery(query: Partial): AlertQuery {
}
export function mockCombinedRuleGroup(name: string, rules: CombinedRule[]): CombinedRuleGroup {
- return { name, rules };
+ return { name, rules, totals: {} };
}
export function mockCombinedRuleNamespace(namespace: Partial): CombinedRuleNamespace {
diff --git a/public/app/features/alerting/unified/state/actions.ts b/public/app/features/alerting/unified/state/actions.ts
index be215b06619..e2835e7aff6 100644
--- a/public/app/features/alerting/unified/state/actions.ts
+++ b/public/app/features/alerting/unified/state/actions.ts
@@ -8,6 +8,7 @@ import {
AlertmanagerGroup,
ExternalAlertmanagerConfig,
ExternalAlertmanagersResponse,
+ Matcher,
Receiver,
Silence,
SilenceCreatePayload,
@@ -101,7 +102,19 @@ function getDataSourceRulerConfig(getState: () => unknown, rulesSourceName: stri
export const fetchPromRulesAction = createAsyncThunk(
'unifiedalerting/fetchPromRules',
async (
- { rulesSourceName, filter }: { rulesSourceName: string; filter?: FetchPromRulesFilter },
+ {
+ rulesSourceName,
+ filter,
+ limitAlerts,
+ matcher,
+ state,
+ }: {
+ rulesSourceName: string;
+ filter?: FetchPromRulesFilter;
+ limitAlerts?: number;
+ matcher?: Matcher[];
+ state?: string[];
+ },
thunkAPI
): Promise => {
await thunkAPI.dispatch(fetchRulesSourceBuildInfoAction({ rulesSourceName }));
@@ -111,7 +124,7 @@ export const fetchPromRulesAction = createAsyncThunk(
thunk: 'unifiedalerting/fetchPromRules',
});
- return await withSerializedError(fetchRulesWithLogging(rulesSourceName, filter));
+ return await withSerializedError(fetchRulesWithLogging(rulesSourceName, filter, limitAlerts, matcher, state));
}
);
@@ -339,7 +352,17 @@ export const fetchRulesSourceBuildInfoAction = createAsyncThunk(
}
);
-export function fetchAllPromAndRulerRulesAction(force = false): ThunkResult> {
+interface FetchPromRulesRulesActionProps {
+ filter?: FetchPromRulesFilter;
+ limitAlerts?: number;
+ matcher?: Matcher[];
+ state?: string[];
+}
+
+export function fetchAllPromAndRulerRulesAction(
+ force = false,
+ options: FetchPromRulesRulesActionProps = {}
+): ThunkResult> {
return async (dispatch, getStore) => {
const allStartLoadingTs = performance.now();
@@ -359,7 +382,7 @@ export function fetchAllPromAndRulerRulesAction(force = false): ThunkResult {
group: {
name: 'Prom up alert',
rules: [],
+ totals: {},
},
namespace: {
rulesSource: GRAFANA_RULES_SOURCE_NAME,
@@ -28,6 +29,8 @@ describe('alertRuleToQueries', () => {
labels: {},
grafana_alert: grafanaAlert,
},
+ instanceTotals: {},
+ filteredInstanceTotals: {},
};
const result = alertRuleToQueries(combinedRule);
@@ -43,6 +46,7 @@ describe('alertRuleToQueries', () => {
group: {
name: 'test',
rules: [],
+ totals: {},
},
namespace: {
name: 'prom test alerts',
@@ -58,6 +62,8 @@ describe('alertRuleToQueries', () => {
readOnly: false,
},
},
+ instanceTotals: {},
+ filteredInstanceTotals: {},
};
const result = alertRuleToQueries(combinedRule);
diff --git a/public/app/plugins/panel/alertlist/AlertInstances.tsx b/public/app/plugins/panel/alertlist/AlertInstances.tsx
index 6c1edec5eb3..7437268bcad 100644
--- a/public/app/plugins/panel/alertlist/AlertInstances.tsx
+++ b/public/app/plugins/panel/alertlist/AlertInstances.tsx
@@ -4,8 +4,9 @@ import pluralize from 'pluralize';
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { GrafanaTheme2, PanelProps } from '@grafana/data';
-import { clearButtonStyles, Icon, useStyles2 } from '@grafana/ui';
+import { Button, clearButtonStyles, Icon, useStyles2 } from '@grafana/ui';
import { AlertInstancesTable } from 'app/features/alerting/unified/components/rules/AlertInstancesTable';
+import { INSTANCES_DISPLAY_LIMIT } from 'app/features/alerting/unified/components/rules/RuleDetails';
import { sortAlerts } from 'app/features/alerting/unified/utils/misc';
import { Alert } from 'app/types/unified-alerting';
@@ -17,9 +18,20 @@ import { filterAlerts } from './util';
interface Props {
alerts: Alert[];
options: PanelProps['options'];
+ grafanaTotalInstances?: number;
+ grafanaFilteredInstancesTotal?: number;
+ handleInstancesLimit?: (limit: boolean) => void;
+ limitInstances?: boolean;
}
-export const AlertInstances = ({ alerts, options }: Props) => {
+export const AlertInstances = ({
+ alerts,
+ options,
+ grafanaTotalInstances,
+ handleInstancesLimit,
+ limitInstances,
+ grafanaFilteredInstancesTotal,
+}: Props) => {
// when custom grouping is enabled, we will always uncollapse the list of alert instances
const defaultShowInstances = options.groupMode === GroupMode.Custom ? true : options.showInstances;
const [displayInstances, setDisplayInstances] = useState(defaultShowInstances);
@@ -36,8 +48,13 @@ export const AlertInstances = ({ alerts, options }: Props) => {
(): Alert[] => filterAlerts(options, sortAlerts(options.sortOrder, alerts)) ?? [],
[alerts, options]
);
+ const isGrafanaAlert = grafanaTotalInstances !== undefined;
- const hiddenInstances = alerts.length - filteredAlerts.length;
+ const hiddenInstancesForGrafanaAlerts =
+ grafanaTotalInstances && grafanaFilteredInstancesTotal ? grafanaTotalInstances - grafanaFilteredInstancesTotal : 0;
+ const hiddenInstancesForNonGrafanaAlerts = alerts.length - filteredAlerts.length;
+
+ const hiddenInstances = isGrafanaAlert ? hiddenInstancesForGrafanaAlerts : hiddenInstancesForNonGrafanaAlerts;
const uncollapsible = filteredAlerts.length > 0;
const toggleShowInstances = uncollapsible ? toggleDisplayInstances : noop;
@@ -48,6 +65,49 @@ export const AlertInstances = ({ alerts, options }: Props) => {
}
}, [filteredAlerts]);
+ const onShowAllClick = async () => {
+ if (!handleInstancesLimit) {
+ return;
+ }
+ handleInstancesLimit(false);
+ setDisplayInstances(true);
+ };
+
+ const onShowLimitedClick = async () => {
+ if (!handleInstancesLimit) {
+ return;
+ }
+ handleInstancesLimit(true);
+ setDisplayInstances(true);
+ };
+ const totalInstancesNumber = limitInstances ? grafanaFilteredInstancesTotal : filteredAlerts.length;
+ const limitStatus = limitInstances
+ ? `Showing ${INSTANCES_DISPLAY_LIMIT} of ${grafanaTotalInstances} instances`
+ : `Showing all ${grafanaTotalInstances} instances`;
+
+ const limitButtonLabel = limitInstances
+ ? 'View all instances'
+ : `Limit the result to ${INSTANCES_DISPLAY_LIMIT} instances`;
+
+ const instancesLimitedAndOverflowed =
+ grafanaTotalInstances &&
+ INSTANCES_DISPLAY_LIMIT === filteredAlerts.length &&
+ grafanaTotalInstances > filteredAlerts.length;
+ const instancesNotLimitedAndoverflowed =
+ grafanaTotalInstances && INSTANCES_DISPLAY_LIMIT < filteredAlerts.length && !limitInstances;
+
+ const footerRow =
+ instancesLimitedAndOverflowed || instancesNotLimitedAndoverflowed ? (
+
+
{limitStatus}
+ {
+
+ }
+
+ ) : undefined;
+
return (
{options.groupMode === GroupMode.Default && (
@@ -56,7 +116,7 @@ export const AlertInstances = ({ alerts, options }: Props) => {
onClick={() => toggleShowInstances()}
>
{uncollapsible &&
}
-
{`${filteredAlerts.length} ${pluralize('instance', filteredAlerts.length)}`}
+
{`${totalInstancesNumber} ${pluralize('instance', totalInstancesNumber)}`}
{hiddenInstances > 0 &&
, {`${hiddenInstances} hidden by filters`}}
)}
@@ -64,14 +124,23 @@ export const AlertInstances = ({ alerts, options }: Props) => {
)}
);
};
-const getStyles = (_: GrafanaTheme2) => ({
+const getStyles = (theme: GrafanaTheme2) => ({
clickable: css`
cursor: pointer;
`,
+ footerRow: css`
+ display: flex;
+ flex-direction: column;
+ gap: ${theme.spacing(1)};
+ justify-content: space-between;
+ align-items: center;
+ width: 100%;
+ `,
});
diff --git a/public/app/plugins/panel/alertlist/UnifiedAlertList.tsx b/public/app/plugins/panel/alertlist/UnifiedAlertList.tsx
index dbfed7f94a9..da19f2c8352 100644
--- a/public/app/plugins/panel/alertlist/UnifiedAlertList.tsx
+++ b/public/app/plugins/panel/alertlist/UnifiedAlertList.tsx
@@ -1,7 +1,7 @@
import { css } from '@emotion/css';
import { sortBy } from 'lodash';
import React, { useEffect, useMemo } from 'react';
-import { useEffectOnce } from 'react-use';
+import { useEffectOnce, useToggle } from 'react-use';
import { GrafanaTheme2, PanelProps } from '@grafana/data';
import { TimeRangeUpdatedEvent } from '@grafana/runtime';
@@ -18,9 +18,11 @@ import {
import { config } from 'app/core/config';
import { contextSrv } from 'app/core/services/context_srv';
import alertDef from 'app/features/alerting/state/alertDef';
+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 } from 'app/features/alerting/unified/state/actions';
+import { parseMatchers } from 'app/features/alerting/unified/utils/alertmanager';
import { Annotation } from 'app/features/alerting/unified/utils/constants';
import {
getAllRulesSourceNames,
@@ -37,14 +39,26 @@ 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, 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, []);
+}
+
export function UnifiedAlertList(props: PanelProps) {
const dispatch = useDispatch();
const rulesDataSourceNames = useMemo(getAllRulesSourceNames, []);
+ const [limitInstances, toggleLimit] = useToggle(true);
// backwards compat for "Inactive" state filter
useEffect(() => {
@@ -60,14 +74,74 @@ export function UnifiedAlertList(props: PanelProps) {
dashboard = getDashboardSrv().getCurrent();
});
+ const stateList = useMemo(() => getStateList(props.options.stateFilter), [props.options.stateFilter]);
+ const { options, replaceVariables } = props;
+ const parsedOptions: UnifiedAlertListOptions = {
+ ...props.options,
+ alertName: replaceVariables(options.alertName),
+ alertInstanceLabelFilter: replaceVariables(options.alertInstanceLabelFilter),
+ };
+
+ const matcherList = useMemo(
+ () => parseMatchers(parsedOptions.alertInstanceLabelFilter),
+ [parsedOptions.alertInstanceLabelFilter]
+ );
+
+ useEffect(() => {
+ if (props.options.groupMode === GroupMode.Default) {
+ dispatch(
+ fetchAllPromAndRulerRulesAction(false, {
+ limitAlerts: limitInstances ? INSTANCES_DISPLAY_LIMIT : undefined,
+ matcher: matcherList,
+ state: stateList,
+ })
+ );
+ }
+ }, [props.options.groupMode, limitInstances, dispatch, matcherList, stateList]);
+
useEffect(() => {
//we need promRules and rulerRules for getting the uid when creating the alert link in panel in case of being a rulerRule.
- dispatch(fetchAllPromAndRulerRulesAction());
- const sub = dashboard?.events.subscribe(TimeRangeUpdatedEvent, () => dispatch(fetchAllPromAndRulerRulesAction()));
+ dispatch(
+ fetchAllPromAndRulerRulesAction(false, {
+ limitAlerts: limitInstances ? INSTANCES_DISPLAY_LIMIT : undefined,
+ matcher: matcherList,
+ state: stateList,
+ })
+ );
+ const sub = dashboard?.events.subscribe(TimeRangeUpdatedEvent, () =>
+ dispatch(
+ fetchAllPromAndRulerRulesAction(false, {
+ limitAlerts: limitInstances ? INSTANCES_DISPLAY_LIMIT : undefined,
+ matcher: matcherList,
+ state: stateList,
+ })
+ )
+ );
return () => {
sub?.unsubscribe();
};
- }, [dispatch, dashboard]);
+ }, [dispatch, dashboard, matcherList, stateList, toggleLimit, limitInstances]);
+
+ const handleInstancesLimit = (limit: boolean) => {
+ if (limit) {
+ dispatch(
+ fetchAllPromAndRulerRulesAction(false, {
+ limitAlerts: INSTANCES_DISPLAY_LIMIT,
+ matcher: matcherList,
+ state: stateList,
+ })
+ );
+ toggleLimit(true);
+ } else {
+ dispatch(
+ fetchAllPromAndRulerRulesAction(false, {
+ matcher: matcherList,
+ state: stateList,
+ })
+ );
+ toggleLimit(false);
+ }
+ };
const { prom, ruler } = useUnifiedAlertingSelector((state) => ({
prom: state.promRules[GRAFANA_RULES_SOURCE_NAME] || initialAsyncRequestState,
@@ -106,13 +180,6 @@ export function UnifiedAlertList(props: PanelProps) {
);
}
- const { options, replaceVariables } = props;
- const parsedOptions: UnifiedAlertListOptions = {
- ...props.options,
- alertName: replaceVariables(options.alertName),
- alertInstanceLabelFilter: replaceVariables(options.alertInstanceLabelFilter),
- };
-
return (
@@ -134,7 +201,12 @@ export function UnifiedAlertList(props: PanelProps) {
)}
{props.options.viewMode === ViewMode.List && props.options.groupMode === GroupMode.Default && haveResults && (
-
+
)}
diff --git a/public/app/plugins/panel/alertlist/UnifiedalertList.test.tsx b/public/app/plugins/panel/alertlist/UnifiedalertList.test.tsx
index 2ae4648c602..1ff3972f11c 100644
--- a/public/app/plugins/panel/alertlist/UnifiedalertList.test.tsx
+++ b/public/app/plugins/panel/alertlist/UnifiedalertList.test.tsx
@@ -1,10 +1,10 @@
-import { render } from '@testing-library/react';
+import { render, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import React from 'react';
import { Provider } from 'react-redux';
import { byRole, byText } from 'testing-library-selector';
-import { getDefaultTimeRange, LoadingState, PanelProps, FieldConfigSource } from '@grafana/data';
+import { FieldConfigSource, getDefaultTimeRange, LoadingState, PanelProps } from '@grafana/data';
import { TimeRangeUpdatedEvent } from '@grafana/runtime';
import { DashboardSrv, setDashboardSrv } from 'app/features/dashboard/services/DashboardSrv';
@@ -19,7 +19,7 @@ import {
import { GRAFANA_RULES_SOURCE_NAME } from '../../../features/alerting/unified/utils/datasource';
import { UnifiedAlertList } from './UnifiedAlertList';
-import { UnifiedAlertListOptions, SortOrder, GroupMode, ViewMode } from './types';
+import { GroupMode, SortOrder, UnifiedAlertListOptions, ViewMode } from './types';
import * as utils from './util';
jest.mock('app/features/alerting/unified/api/alertmanager');
@@ -88,6 +88,8 @@ const renderPanel = (options: Partial = defaultOptions)
mockPromAlertingRule({
name: 'rule1',
alerts: [mockPromAlert({ labels: { severity: 'critical' } })],
+ totals: { alerting: 1 },
+ totalsFiltered: { alerting: 1 },
}),
],
}),
@@ -112,6 +114,7 @@ const renderPanel = (options: Partial = defaultOptions)
describe('UnifiedAlertList', () => {
it('subscribes to the dashboard refresh interval', async () => {
+ jest.spyOn(defaultProps, 'replaceVariables').mockReturnValue('severity=critical');
await renderPanel();
expect(dashboard.events.subscribe).toHaveBeenCalledTimes(1);
expect(dashboard.events.subscribe.mock.calls[0][0]).toEqual(TimeRangeUpdatedEvent);
@@ -125,7 +128,7 @@ describe('UnifiedAlertList', () => {
const user = userEvent.setup();
- renderPanel({
+ await renderPanel({
alertInstanceLabelFilter: '$label',
dashboardAlerts: false,
alertName: '',
@@ -135,6 +138,10 @@ describe('UnifiedAlertList', () => {
expect(byText('rule1').get()).toBeInTheDocument();
+ await waitFor(() => {
+ expect(screen.getByText('1 instance')).toBeInTheDocument();
+ });
+
const expandElement = byText('1 instance').get();
await user.click(expandElement);
diff --git a/public/app/plugins/panel/alertlist/types.ts b/public/app/plugins/panel/alertlist/types.ts
index c94f5de97e1..a0e07fd4516 100644
--- a/public/app/plugins/panel/alertlist/types.ts
+++ b/public/app/plugins/panel/alertlist/types.ts
@@ -42,7 +42,7 @@ export interface AlertListOptions {
folderId: number;
}
-interface StateFilter {
+export interface StateFilter {
firing: boolean;
pending: boolean;
inactive?: boolean; // backwards compat
diff --git a/public/app/plugins/panel/alertlist/unified-alerting/UngroupedView.tsx b/public/app/plugins/panel/alertlist/unified-alerting/UngroupedView.tsx
index 7a0c0ecffad..882b8c810e6 100644
--- a/public/app/plugins/panel/alertlist/unified-alerting/UngroupedView.tsx
+++ b/public/app/plugins/panel/alertlist/unified-alerting/UngroupedView.tsx
@@ -17,7 +17,8 @@ import {
import { createUrl } from 'app/features/alerting/unified/utils/url';
import { PromAlertingRuleState } from 'app/types/unified-alerting-dto';
-import { AlertingRule, CombinedRuleWithLocation } from '../../../../types/unified-alerting';
+import { GRAFANA_RULES_SOURCE_NAME } from '../../../../features/alerting/unified/utils/datasource';
+import { AlertingRule, AlertInstanceTotalState, CombinedRuleWithLocation } from '../../../../types/unified-alerting';
import { AlertInstances } from '../AlertInstances';
import { getStyles } from '../UnifiedAlertList';
import { UnifiedAlertListOptions } from '../types';
@@ -25,9 +26,17 @@ import { UnifiedAlertListOptions } from '../types';
type Props = {
rules: CombinedRuleWithLocation[];
options: UnifiedAlertListOptions;
+ handleInstancesLimit?: (limit: boolean) => void;
+ limitInstances: boolean;
};
-const UngroupedModeView = ({ rules, options }: Props) => {
+function getGrafanaInstancesTotal(totals: Partial>) {
+ return Object.values(totals)
+ .filter((total) => total !== undefined)
+ .reduce((total, currentTotal) => total + currentTotal, 0);
+}
+
+const UngroupedModeView = ({ rules, options, handleInstancesLimit, limitInstances }: Props) => {
const styles = useStyles2(getStyles);
const stateStyle = useStyles2(getStateTagStyles);
const { href: returnTo } = useLocation();
@@ -46,6 +55,15 @@ const UngroupedModeView = ({ rules, options }: Props) => {
const indentifier = fromCombinedRule(ruleWithLocation.dataSourceName, ruleWithLocation);
const strIndentifier = stringifyIdentifier(indentifier);
+ const grafanaInstancesTotal =
+ ruleWithLocation.dataSourceName === GRAFANA_RULES_SOURCE_NAME
+ ? getGrafanaInstancesTotal(ruleWithLocation.instanceTotals)
+ : undefined;
+ const grafanaFilteredInstancesTotal =
+ ruleWithLocation.dataSourceName === GRAFANA_RULES_SOURCE_NAME
+ ? getGrafanaInstancesTotal(ruleWithLocation.filteredInstanceTotals)
+ : undefined;
+
const href = createUrl(
`/alerting/${encodeURIComponent(dataSourceName)}/${encodeURIComponent(strIndentifier)}/view`,
{ returnTo: returnTo ?? '' }
@@ -96,7 +114,14 @@ const UngroupedModeView = ({ rules, options }: Props) => {
)}
-