Alerting: Update UI of instance counts on triage page (#113660)
* Alerting: Update UI of instance counts on triage page * make boxes full height * resolve PR comments- use grafana-ui components * refactoring
This commit is contained in:
@@ -247,7 +247,7 @@ export const getStyles = (theme: GrafanaTheme2) => {
|
||||
overflow: 'hidden', // Let AutoSizer handle the overflow
|
||||
}),
|
||||
summaryContainer: css({
|
||||
gridTemplateRows: summaryHeight,
|
||||
minHeight: summaryHeight,
|
||||
marginBottom: theme.spacing(2),
|
||||
}),
|
||||
headerContainer: css({
|
||||
|
||||
@@ -4,31 +4,37 @@ import { PromAlertingRuleState } from 'app/types/unified-alerting-dto';
|
||||
import { RuleFrame, countRules, parseAlertstateFilter } from './SummaryStats';
|
||||
|
||||
describe('parseAlertstateFilter', () => {
|
||||
it('should return "firing" when filter contains alertstate="firing"', () => {
|
||||
expect(parseAlertstateFilter('alertstate="firing"')).toBe(PromAlertingRuleState.Firing);
|
||||
it('should return array with "firing" when filter contains alertstate="firing"', () => {
|
||||
expect(parseAlertstateFilter('alertstate="firing"')).toEqual([PromAlertingRuleState.Firing]);
|
||||
});
|
||||
|
||||
it('should return "pending" when filter contains alertstate="pending"', () => {
|
||||
expect(parseAlertstateFilter('alertstate="pending"')).toBe(PromAlertingRuleState.Pending);
|
||||
it('should return array with "pending" when filter contains alertstate="pending"', () => {
|
||||
expect(parseAlertstateFilter('alertstate="pending"')).toEqual([PromAlertingRuleState.Pending]);
|
||||
});
|
||||
|
||||
it('should return null when filter contains both firing and pending', () => {
|
||||
expect(parseAlertstateFilter('alertstate=~"firing|pending"')).toBe(null);
|
||||
it('should return both states when filter contains both firing and pending', () => {
|
||||
expect(parseAlertstateFilter('alertstate=~"firing|pending"')).toEqual([
|
||||
PromAlertingRuleState.Firing,
|
||||
PromAlertingRuleState.Pending,
|
||||
]);
|
||||
});
|
||||
|
||||
it('should return null when no alertstate filter', () => {
|
||||
expect(parseAlertstateFilter('')).toBe(null);
|
||||
expect(parseAlertstateFilter('namespace="default"')).toBe(null);
|
||||
it('should return both states when no alertstate filter', () => {
|
||||
expect(parseAlertstateFilter('')).toEqual([PromAlertingRuleState.Firing, PromAlertingRuleState.Pending]);
|
||||
expect(parseAlertstateFilter('namespace="default"')).toEqual([
|
||||
PromAlertingRuleState.Firing,
|
||||
PromAlertingRuleState.Pending,
|
||||
]);
|
||||
});
|
||||
|
||||
it('should handle regex operator =~', () => {
|
||||
expect(parseAlertstateFilter('alertstate=~"firing"')).toBe(PromAlertingRuleState.Firing);
|
||||
expect(parseAlertstateFilter('alertstate=~"pending"')).toBe(PromAlertingRuleState.Pending);
|
||||
expect(parseAlertstateFilter('alertstate=~"firing"')).toEqual([PromAlertingRuleState.Firing]);
|
||||
expect(parseAlertstateFilter('alertstate=~"pending"')).toEqual([PromAlertingRuleState.Pending]);
|
||||
});
|
||||
|
||||
it('should handle whitespace', () => {
|
||||
expect(parseAlertstateFilter('alertstate = "firing"')).toBe(PromAlertingRuleState.Firing);
|
||||
expect(parseAlertstateFilter('alertstate =~ "pending"')).toBe(PromAlertingRuleState.Pending);
|
||||
expect(parseAlertstateFilter('alertstate = "firing"')).toEqual([PromAlertingRuleState.Firing]);
|
||||
expect(parseAlertstateFilter('alertstate =~ "pending"')).toEqual([PromAlertingRuleState.Pending]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -58,7 +64,7 @@ describe('countRules', () => {
|
||||
{ ruleUID: 'rule3', alertstate: PromAlertingRuleState.Pending },
|
||||
]);
|
||||
|
||||
const result = countRules(ruleDfv, null);
|
||||
const result = countRules(ruleDfv, [PromAlertingRuleState.Firing, PromAlertingRuleState.Pending]);
|
||||
|
||||
expect(result.firing).toBe(2);
|
||||
expect(result.pending).toBe(1);
|
||||
@@ -71,13 +77,13 @@ describe('countRules', () => {
|
||||
{ ruleUID: 'rule3', alertstate: PromAlertingRuleState.Firing },
|
||||
]);
|
||||
|
||||
const result = countRules(ruleDfv, null);
|
||||
const result = countRules(ruleDfv, [PromAlertingRuleState.Firing, PromAlertingRuleState.Pending]);
|
||||
|
||||
expect(result.firing).toBe(1);
|
||||
expect(result.pending).toBe(2);
|
||||
});
|
||||
|
||||
it('should count rules with BOTH firing and pending as firing (firing takes precedence)', () => {
|
||||
it('should count rules with BOTH firing and pending in both counts', () => {
|
||||
const ruleDfv = createMockRuleDfv([
|
||||
{ ruleUID: 'rule1', alertstate: PromAlertingRuleState.Firing },
|
||||
{ ruleUID: 'rule1', alertstate: PromAlertingRuleState.Pending }, // Same rule, both states
|
||||
@@ -85,13 +91,13 @@ describe('countRules', () => {
|
||||
{ ruleUID: 'rule3', alertstate: PromAlertingRuleState.Pending }, // Only pending
|
||||
]);
|
||||
|
||||
const result = countRules(ruleDfv, null);
|
||||
const result = countRules(ruleDfv, [PromAlertingRuleState.Firing, PromAlertingRuleState.Pending]);
|
||||
|
||||
// rule1 should be counted as firing (firing takes precedence)
|
||||
// rule2 should be counted as firing
|
||||
// rule3 should be counted as pending
|
||||
// rule1 has both states, so counted in both
|
||||
// rule2 counted only in firing
|
||||
// rule3 counted only in pending
|
||||
expect(result.firing).toBe(2); // rule1 and rule2
|
||||
expect(result.pending).toBe(1); // only rule3
|
||||
expect(result.pending).toBe(2); // rule1 and rule3
|
||||
});
|
||||
|
||||
it('should handle multiple instances of the same rule with same state', () => {
|
||||
@@ -102,7 +108,7 @@ describe('countRules', () => {
|
||||
{ ruleUID: 'rule2', alertstate: PromAlertingRuleState.Pending }, // Same rule, duplicate entry
|
||||
]);
|
||||
|
||||
const result = countRules(ruleDfv, null);
|
||||
const result = countRules(ruleDfv, [PromAlertingRuleState.Firing, PromAlertingRuleState.Pending]);
|
||||
|
||||
// Each rule should only be counted once despite multiple entries
|
||||
expect(result.firing).toBe(1);
|
||||
@@ -112,7 +118,7 @@ describe('countRules', () => {
|
||||
it('should return 0 for both counts when no rules', () => {
|
||||
const ruleDfv = createMockRuleDfv([]);
|
||||
|
||||
const result = countRules(ruleDfv, null);
|
||||
const result = countRules(ruleDfv, [PromAlertingRuleState.Firing, PromAlertingRuleState.Pending]);
|
||||
|
||||
expect(result.firing).toBe(0);
|
||||
expect(result.pending).toBe(0);
|
||||
@@ -128,7 +134,7 @@ describe('countRules', () => {
|
||||
{ ruleUID: 'rule3', alertstate: PromAlertingRuleState.Firing }, // Only firing
|
||||
]);
|
||||
|
||||
const result = countRules(ruleDfv, PromAlertingRuleState.Pending);
|
||||
const result = countRules(ruleDfv, [PromAlertingRuleState.Pending]);
|
||||
|
||||
// Should count rule1 and rule2 (both have pending instances)
|
||||
expect(result.pending).toBe(2);
|
||||
@@ -144,7 +150,7 @@ describe('countRules', () => {
|
||||
{ ruleUID: 'rule3', alertstate: PromAlertingRuleState.Pending },
|
||||
]);
|
||||
|
||||
const result = countRules(ruleDfv, PromAlertingRuleState.Pending);
|
||||
const result = countRules(ruleDfv, [PromAlertingRuleState.Pending]);
|
||||
|
||||
// Should count all three rules (all have pending instances)
|
||||
expect(result.pending).toBe(3);
|
||||
@@ -158,14 +164,13 @@ describe('countRules', () => {
|
||||
{ ruleUID: 'rule2', alertstate: PromAlertingRuleState.Pending },
|
||||
]);
|
||||
|
||||
const noFilterResult = countRules(ruleDfv, null);
|
||||
const pendingFilterResult = countRules(ruleDfv, PromAlertingRuleState.Pending);
|
||||
const noFilterResult = countRules(ruleDfv, [PromAlertingRuleState.Firing, PromAlertingRuleState.Pending]);
|
||||
const pendingFilterResult = countRules(ruleDfv, [PromAlertingRuleState.Pending]);
|
||||
|
||||
// With pending filter: should count rule1 and rule2 = 2
|
||||
// Without filter: should count only rule2 = 1 (rule1 is counted as firing, not pending)
|
||||
expect(pendingFilterResult.pending).toBeGreaterThanOrEqual(noFilterResult.pending);
|
||||
// Without filter: should also count both rules = 2 (both have pending instances)
|
||||
expect(pendingFilterResult.pending).toBe(2);
|
||||
expect(noFilterResult.pending).toBe(1); // Only rule2 (rule1 is firing)
|
||||
expect(noFilterResult.pending).toBe(2); // Both rule1 and rule2 have pending instances
|
||||
expect(noFilterResult.firing).toBe(1); // rule1
|
||||
});
|
||||
});
|
||||
@@ -179,7 +184,7 @@ describe('countRules', () => {
|
||||
{ ruleUID: 'rule3', alertstate: PromAlertingRuleState.Pending }, // Only pending
|
||||
]);
|
||||
|
||||
const result = countRules(ruleDfv, PromAlertingRuleState.Firing);
|
||||
const result = countRules(ruleDfv, [PromAlertingRuleState.Firing]);
|
||||
|
||||
// Should count rule1 and rule2 (both have firing instances)
|
||||
expect(result.firing).toBe(2);
|
||||
@@ -208,18 +213,18 @@ describe('countRules', () => {
|
||||
{ ruleUID: 'ruleE', alertstate: PromAlertingRuleState.Pending },
|
||||
]);
|
||||
|
||||
// No filter: Firing takes precedence
|
||||
const noFilter = countRules(ruleDfv, null);
|
||||
expect(noFilter.firing).toBe(3); // Rule A, C, and D (C has firing so it's counted as firing)
|
||||
expect(noFilter.pending).toBe(2); // Rule B and E (only those with NO firing)
|
||||
// No filter: Count all rules with at least one instance in each state
|
||||
const noFilter = countRules(ruleDfv, [PromAlertingRuleState.Firing, PromAlertingRuleState.Pending]);
|
||||
expect(noFilter.firing).toBe(3); // Rule A, C, and D (all have at least one firing instance)
|
||||
expect(noFilter.pending).toBe(3); // Rule B, C, and E (all have at least one pending instance)
|
||||
|
||||
// With pending filter: Count all rules with ANY pending instances
|
||||
const pendingFilter = countRules(ruleDfv, PromAlertingRuleState.Pending);
|
||||
const pendingFilter = countRules(ruleDfv, [PromAlertingRuleState.Pending]);
|
||||
expect(pendingFilter.pending).toBe(3); // Rule B, C, and E
|
||||
expect(pendingFilter.firing).toBe(0);
|
||||
|
||||
// With firing filter: Count all rules with ANY firing instances
|
||||
const firingFilter = countRules(ruleDfv, PromAlertingRuleState.Firing);
|
||||
const firingFilter = countRules(ruleDfv, [PromAlertingRuleState.Firing]);
|
||||
expect(firingFilter.firing).toBe(3); // Rule A, C, and D
|
||||
expect(firingFilter.pending).toBe(0);
|
||||
});
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import { DataFrameView } from '@grafana/data';
|
||||
import { css } from '@emotion/css';
|
||||
|
||||
import { DataFrameView, GrafanaTheme2 } from '@grafana/data';
|
||||
import { Trans } from '@grafana/i18n';
|
||||
import { SceneObjectBase, SceneObjectState } from '@grafana/scenes';
|
||||
import { useQueryRunner } from '@grafana/scenes-react';
|
||||
import { ErrorBoundaryAlert, Stack, Text } from '@grafana/ui';
|
||||
import { Box, ErrorBoundaryAlert, Grid, useStyles2 } from '@grafana/ui';
|
||||
import { PromAlertingRuleState } from 'app/types/unified-alerting-dto';
|
||||
|
||||
import { Spacer } from '../../components/Spacer';
|
||||
import { METRIC_NAME } from '../constants';
|
||||
|
||||
import { getDataQuery, useQueryFilter } from './utils';
|
||||
@@ -25,63 +26,44 @@ export interface RuleFrame {
|
||||
Value: number;
|
||||
}
|
||||
|
||||
export function parseAlertstateFilter(filter: string): AlertState | null {
|
||||
const firingMatch = filter.match(/alertstate\s*=~?\s*"firing"/);
|
||||
const pendingMatch = filter.match(/alertstate\s*=~?\s*"pending"/);
|
||||
export function parseAlertstateFilter(filter: string): AlertState[] {
|
||||
const hasFiring = filter.match(/alertstate\s*=~?\s*"firing"/);
|
||||
const hasPending = filter.match(/alertstate\s*=~?\s*"pending"/);
|
||||
|
||||
if (firingMatch && pendingMatch) {
|
||||
return null;
|
||||
const states: AlertState[] = [];
|
||||
|
||||
// If both or neither match, include both states
|
||||
if ((hasFiring && hasPending) || (!hasFiring && !hasPending)) {
|
||||
return [PromAlertingRuleState.Firing, PromAlertingRuleState.Pending];
|
||||
}
|
||||
|
||||
if (firingMatch) {
|
||||
return PromAlertingRuleState.Firing;
|
||||
if (hasFiring) {
|
||||
states.push(PromAlertingRuleState.Firing);
|
||||
}
|
||||
if (hasPending) {
|
||||
states.push(PromAlertingRuleState.Pending);
|
||||
}
|
||||
|
||||
if (pendingMatch) {
|
||||
return PromAlertingRuleState.Pending;
|
||||
}
|
||||
|
||||
return null;
|
||||
return states;
|
||||
}
|
||||
|
||||
export function countRules(ruleDfv: DataFrameView<RuleFrame>, alertstateFilter: AlertState | null) {
|
||||
const rulesWithFiring = new Set<string>();
|
||||
const rulesWithPending = new Set<string>();
|
||||
export function countRules(ruleDfv: DataFrameView<RuleFrame>, alertstateFilter: AlertState[]) {
|
||||
const counts = {
|
||||
[PromAlertingRuleState.Firing]: new Set<string>(),
|
||||
[PromAlertingRuleState.Pending]: new Set<string>(),
|
||||
};
|
||||
|
||||
// Only count rules for states we're interested in
|
||||
ruleDfv.fields.grafana_rule_uid.values.forEach((ruleUID, i) => {
|
||||
const alertstate = ruleDfv.fields.alertstate.values[i];
|
||||
if (alertstate === PromAlertingRuleState.Firing) {
|
||||
rulesWithFiring.add(ruleUID);
|
||||
}
|
||||
if (alertstate === PromAlertingRuleState.Pending) {
|
||||
rulesWithPending.add(ruleUID);
|
||||
if (alertstateFilter.includes(alertstate)) {
|
||||
counts[alertstate].add(ruleUID);
|
||||
}
|
||||
});
|
||||
|
||||
// When filtering by pending, count all rules with pending instances (may also have firing)
|
||||
if (alertstateFilter === PromAlertingRuleState.Pending) {
|
||||
return {
|
||||
firing: 0,
|
||||
pending: rulesWithPending.size,
|
||||
};
|
||||
}
|
||||
|
||||
// When filtering by firing, count all rules with firing instances (may also have pending)
|
||||
if (alertstateFilter === PromAlertingRuleState.Firing) {
|
||||
return {
|
||||
firing: rulesWithFiring.size,
|
||||
pending: 0,
|
||||
};
|
||||
}
|
||||
|
||||
// When no filter: firing takes precedence
|
||||
// A rule is "firing" if it has ANY firing instances (even if it also has pending)
|
||||
// A rule is "pending" ONLY if it has pending instances but NO firing instances
|
||||
const onlyPending = new Set([...rulesWithPending].filter((uid) => !rulesWithFiring.has(uid)));
|
||||
|
||||
return {
|
||||
firing: rulesWithFiring.size, // ALL rules with firing instances
|
||||
pending: onlyPending.size, // ONLY rules with no firing instances
|
||||
firing: counts[PromAlertingRuleState.Firing].size,
|
||||
pending: counts[PromAlertingRuleState.Pending].size,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -93,23 +75,57 @@ function countInstances(instanceDfv: DataFrameView<Frame>) {
|
||||
return { firing: getValue(PromAlertingRuleState.Firing), pending: getValue(PromAlertingRuleState.Pending) };
|
||||
}
|
||||
|
||||
interface StatRowProps {
|
||||
interface StatBoxProps {
|
||||
i18nKey: string;
|
||||
value: number;
|
||||
color: 'error' | 'warning';
|
||||
values: Record<string, number>;
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
function StatRow({ i18nKey, color, values, children }: StatRowProps) {
|
||||
function StatBox({ i18nKey, value, color, children }: StatBoxProps) {
|
||||
const styles = useStyles2(getStatBoxStyles);
|
||||
const colorClass = color === 'error' ? styles.errorColor : styles.warningColor;
|
||||
|
||||
return (
|
||||
<Text color={color}>
|
||||
<Trans i18nKey={i18nKey} values={values}>
|
||||
{children}
|
||||
</Trans>
|
||||
</Text>
|
||||
<Box
|
||||
display="flex"
|
||||
direction="column"
|
||||
justifyContent="center"
|
||||
alignItems="center"
|
||||
padding={2}
|
||||
backgroundColor="secondary"
|
||||
borderRadius="default"
|
||||
gap={1}
|
||||
height="100%"
|
||||
>
|
||||
<div className={styles.label}>{children}</div>
|
||||
<div className={`${styles.value} ${colorClass}`}>{value}</div>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
const getStatBoxStyles = (theme: GrafanaTheme2) => ({
|
||||
label: css({
|
||||
fontSize: theme.typography.bodySmall.fontSize,
|
||||
color: theme.colors.text.primary,
|
||||
wordWrap: 'break-word',
|
||||
whiteSpace: 'normal',
|
||||
textAlign: 'center',
|
||||
}),
|
||||
value: css({
|
||||
fontSize: theme.typography.h1.fontSize,
|
||||
fontWeight: theme.typography.fontWeightMedium,
|
||||
lineHeight: 1.2,
|
||||
textAlign: 'center',
|
||||
}),
|
||||
errorColor: css({
|
||||
color: theme.colors.error.text,
|
||||
}),
|
||||
warningColor: css({
|
||||
color: theme.colors.warning.text,
|
||||
}),
|
||||
});
|
||||
|
||||
function SummaryStatsContent() {
|
||||
const filter = useQueryFilter();
|
||||
const alertstateFilter = parseAlertstateFilter(filter);
|
||||
@@ -161,65 +177,28 @@ function SummaryStatsContent() {
|
||||
const rules = countRules(ruleDfv, alertstateFilter);
|
||||
|
||||
return (
|
||||
<Stack direction="column" alignItems="flex-end" gap={0}>
|
||||
<Spacer />
|
||||
{alertstateFilter === PromAlertingRuleState.Firing && (
|
||||
<>
|
||||
<StatRow i18nKey="alerting.triage.firing-rules-count" color="error" values={{ count: rules.firing }}>
|
||||
{'{{count}} firing alert rules'}
|
||||
</StatRow>
|
||||
<StatRow
|
||||
i18nKey="alerting.triage.firing-instances-count"
|
||||
color="error"
|
||||
values={{ firingCount: instances.firing }}
|
||||
>
|
||||
{'{{firingCount}} firing instances'}
|
||||
</StatRow>
|
||||
</>
|
||||
<Grid gap={2}>
|
||||
{alertstateFilter.includes(PromAlertingRuleState.Firing) && (
|
||||
<Grid columns={2} gap={2}>
|
||||
<StatBox i18nKey="alerting.triage.firing-instances-count" value={instances.firing} color="error">
|
||||
<Trans i18nKey="alerting.triage.firing-instances-count">Firing alert instances</Trans>
|
||||
</StatBox>
|
||||
<StatBox i18nKey="alerting.triage.firing-rules-count" value={rules.firing} color="error">
|
||||
<Trans i18nKey="alerting.triage.rules-with-firing-instances">Alert rules with firing instances</Trans>
|
||||
</StatBox>
|
||||
</Grid>
|
||||
)}
|
||||
{alertstateFilter === PromAlertingRuleState.Pending && (
|
||||
<>
|
||||
<StatRow
|
||||
i18nKey="alerting.triage.rules-with-pending-instances"
|
||||
color="warning"
|
||||
values={{ count: rules.pending }}
|
||||
>
|
||||
{'{{count}} rules with pending instances'}
|
||||
</StatRow>
|
||||
<StatRow
|
||||
i18nKey="alerting.triage.pending-instances-count"
|
||||
color="warning"
|
||||
values={{ pendingCount: instances.pending }}
|
||||
>
|
||||
{'{{pendingCount}} pending instances'}
|
||||
</StatRow>
|
||||
</>
|
||||
{alertstateFilter.includes(PromAlertingRuleState.Pending) && (
|
||||
<Grid columns={2} gap={2}>
|
||||
<StatBox i18nKey="alerting.triage.pending-instances-count" value={instances.pending} color="warning">
|
||||
<Trans i18nKey="alerting.triage.pending-instances-count">Pending alert instances</Trans>
|
||||
</StatBox>
|
||||
<StatBox i18nKey="alerting.triage.rules-with-pending-instances" value={rules.pending} color="warning">
|
||||
<Trans i18nKey="alerting.triage.rules-with-pending-instances">Alert rules with pending instances</Trans>
|
||||
</StatBox>
|
||||
</Grid>
|
||||
)}
|
||||
{!alertstateFilter && (
|
||||
<>
|
||||
<StatRow i18nKey="alerting.triage.firing-rules-count" color="error" values={{ count: rules.firing }}>
|
||||
{'{{count}} firing alert rules'}
|
||||
</StatRow>
|
||||
<StatRow
|
||||
i18nKey="alerting.triage.firing-instances-count"
|
||||
color="error"
|
||||
values={{ firingCount: instances.firing }}
|
||||
>
|
||||
{'{{firingCount}} firing instances'}
|
||||
</StatRow>
|
||||
<StatRow i18nKey="alerting.triage.pending-rules-count" color="warning" values={{ count: rules.pending }}>
|
||||
{'{{count}} pending alert rules'}
|
||||
</StatRow>
|
||||
<StatRow
|
||||
i18nKey="alerting.triage.pending-instances-count"
|
||||
color="warning"
|
||||
values={{ pendingCount: instances.pending }}
|
||||
>
|
||||
{'{{pendingCount}} pending instances'}
|
||||
</StatRow>
|
||||
</>
|
||||
)}
|
||||
</Stack>
|
||||
</Grid>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -2945,6 +2945,7 @@
|
||||
"triage": {
|
||||
"alert-instances": "Alert instances",
|
||||
"error-loading-rule": "Error loading rule",
|
||||
"firing-instances-count": "Firing alert instances",
|
||||
"instance-details-drawer": {
|
||||
"instance-details": "Instance details"
|
||||
},
|
||||
@@ -2954,6 +2955,7 @@
|
||||
"no-matching-instances-with-filters": "No alert instances match your current set of filters for the selected time range.",
|
||||
"open-in-sidebar": "Open in sidebar",
|
||||
"open-rule-details": "Open rule details",
|
||||
"pending-instances-count": "Pending alert instances",
|
||||
"rule-details": {
|
||||
"subtitle": "Rule details and conditions",
|
||||
"title": "Rule Details"
|
||||
@@ -2961,7 +2963,9 @@
|
||||
"rule-not-found": {
|
||||
"description": "The requested rule could not be found.",
|
||||
"title": "Rule not found"
|
||||
}
|
||||
},
|
||||
"rules-with-firing-instances": "Alert rules with firing instances",
|
||||
"rules-with-pending-instances": "Alert rules with pending instances"
|
||||
},
|
||||
"type-selector-button": {
|
||||
"add-expression": "Add expression"
|
||||
|
||||
Reference in New Issue
Block a user