Alerting: New alert list panel component (#34614)

pull/34990/head
Nathan Rodman 4 years ago committed by GitHub
parent 7c25465b3a
commit 7dd5a065ba
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 2
      packages/grafana-ui/src/types/icon.ts
  2. 20
      public/app/features/alerting/state/alertDef.ts
  3. 16
      public/app/features/alerting/unified/api/prometheus.ts
  4. 16
      public/app/features/alerting/unified/components/rules/AlertStateTag.tsx
  5. 14
      public/app/features/alerting/unified/components/rules/RuleState.tsx
  6. 11
      public/app/features/alerting/unified/state/actions.ts
  7. 42
      public/app/features/alerting/unified/utils/rules.ts
  8. 78
      public/app/plugins/panel/alertlist/AlertInstances.tsx
  9. 296
      public/app/plugins/panel/alertlist/UnifiedAlertList.tsx
  10. 88
      public/app/plugins/panel/alertlist/module.tsx
  11. 14
      public/app/plugins/panel/alertlist/types.ts
  12. 7
      public/app/types/unified-alerting.ts

@ -67,6 +67,7 @@ export const getAvailableIcons = () =>
'file-copy-alt',
'filter',
'folder',
'fire',
'folder-open',
'folder-plus',
'folder-upload',
@ -86,6 +87,7 @@ export const getAvailableIcons = () =>
'heart-break',
'history',
'home-alt',
'hourglass',
'import',
'info-circle',
'key-skeleton-alt',

@ -19,10 +19,12 @@ const conditionTypes = [{ text: 'Query', value: 'query' }];
const alertStateSortScore = {
alerting: 1,
firing: 1,
no_data: 2,
pending: 3,
ok: 4,
paused: 5,
inactive: 5,
};
export enum EvalFunction {
@ -111,7 +113,7 @@ function getStateDisplayModel(state: string) {
case 'pending': {
return {
text: 'PENDING',
iconClass: 'exclamation-triangle',
iconClass: 'hourglass',
stateClass: 'alert-state-warning',
};
}
@ -122,6 +124,22 @@ function getStateDisplayModel(state: string) {
stateClass: 'alert-state-paused',
};
}
case 'firing': {
return {
text: 'FIRING',
iconClass: 'fire',
stateClass: '',
};
}
case 'inactive': {
return {
text: 'INACTIVE',
iconClass: 'check',
stateClass: '',
};
}
}
throw { message: 'Unknown alert state' };

@ -1,7 +1,7 @@
import { getBackendSrv } from '@grafana/runtime';
import { RuleNamespace } from 'app/types/unified-alerting';
import { PromRulesResponse } from 'app/types/unified-alerting-dto';
import { getDatasourceAPIId } from '../utils/datasource';
import { getAllRulesSourceNames, getDatasourceAPIId } from '../utils/datasource';
export async function fetchRules(dataSourceName: string): Promise<RuleNamespace[]> {
const response = await getBackendSrv()
@ -30,3 +30,17 @@ export async function fetchRules(dataSourceName: string): Promise<RuleNamespace[
return Object.values(nsMap);
}
export async function fetchAllRules(): Promise<RuleNamespace[]> {
const namespaces = [] as Array<Promise<RuleNamespace[]>>;
getAllRulesSourceNames().forEach(async (name) => {
namespaces.push(
fetchRules(name).catch((e) => {
return [];
// TODO add error comms
})
);
});
const promises = await Promise.all(namespaces);
return promises.flat();
}

@ -1,19 +1,7 @@
import { GrafanaAlertState, PromAlertingRuleState } from 'app/types/unified-alerting-dto';
import React, { FC } from 'react';
import { alertStateToReadable } from '../../utils/rules';
import { State, StateTag } from '../StateTag';
const alertStateToState: Record<PromAlertingRuleState | GrafanaAlertState, State> = {
[PromAlertingRuleState.Inactive]: 'good',
[PromAlertingRuleState.Firing]: 'bad',
[PromAlertingRuleState.Pending]: 'warning',
[GrafanaAlertState.Alerting]: 'bad',
[GrafanaAlertState.Error]: 'bad',
[GrafanaAlertState.NoData]: 'info',
[GrafanaAlertState.Normal]: 'good',
[GrafanaAlertState.Pending]: 'warning',
};
import { alertStateToReadable, alertStateToState } from '../../utils/rules';
import { StateTag } from '../StateTag';
interface Props {
state: PromAlertingRuleState | GrafanaAlertState;
}

@ -2,9 +2,9 @@ import { css } from '@emotion/css';
import { GrafanaTheme2, intervalToAbbreviatedDurationString } from '@grafana/data';
import { HorizontalGroup, Spinner, useStyles2 } from '@grafana/ui';
import { CombinedRule } from 'app/types/unified-alerting';
import { GrafanaAlertState, PromAlertingRuleState } from 'app/types/unified-alerting-dto';
import { PromAlertingRuleState } from 'app/types/unified-alerting-dto';
import React, { FC, useMemo } from 'react';
import { isAlertingRule, isRecordingRule } from '../../utils/rules';
import { isAlertingRule, isRecordingRule, getFirstActiveAt } from '../../utils/rules';
import { AlertStateTag } from './AlertStateTag';
interface Props {
@ -26,15 +26,7 @@ export const RuleState: FC<Props> = ({ rule, isDeleting, isCreating }) => {
promRule.state !== PromAlertingRuleState.Inactive
) {
// find earliest alert
const firstActiveAt = promRule.alerts.reduce((prev, alert) => {
if (alert.activeAt && alert.state !== GrafanaAlertState.Normal) {
const activeAt = new Date(alert.activeAt);
if (prev === null || prev.getTime() > activeAt.getTime()) {
return activeAt;
}
}
return prev;
}, null as Date | null);
const firstActiveAt = getFirstActiveAt(promRule);
// calculate time elapsed from earliest alert
if (firstActiveAt) {

@ -98,6 +98,17 @@ export function fetchAllPromAndRulerRulesAction(force = false): ThunkResult<void
};
}
export function fetchAllPromRulesAction(force = false): ThunkResult<void> {
return (dispatch, getStore) => {
const { promRules } = getStore().unifiedAlerting;
getAllRulesSourceNames().map((name) => {
if (force || !promRules[name]?.loading) {
dispatch(fetchPromRulesAction(name));
}
});
};
}
async function findExistingRule(ruleIdentifier: RuleIdentifier): Promise<RuleWithLocation | null> {
if (isGrafanaRuleIdentifier(ruleIdentifier)) {
const namespaces = await fetchRulerRules(GRAFANA_RULES_SOURCE_NAME);

@ -14,15 +14,18 @@ import {
AlertingRule,
CloudRuleIdentifier,
GrafanaRuleIdentifier,
PromRuleWithLocation,
RecordingRule,
Rule,
RuleIdentifier,
RuleNamespace,
RuleWithLocation,
} from 'app/types/unified-alerting';
import { AsyncRequestState } from './redux';
import { RULER_NOT_SUPPORTED_MSG } from './constants';
import { hash } from './misc';
import { capitalize } from 'lodash';
import { State } from '../components/StateTag';
export function isAlertingRule(rule: Rule): rule is AlertingRule {
return rule.type === PromRuleType.Alerting;
@ -142,3 +145,42 @@ export function alertStateToReadable(state: PromAlertingRuleState | GrafanaAlert
}
return capitalize(state);
}
export const flattenRules = (rules: RuleNamespace[]) => {
return rules.reduce<PromRuleWithLocation[]>((acc, { dataSourceName, name: namespaceName, groups }) => {
groups.forEach(({ name: groupName, rules }) => {
rules.forEach((rule) => {
if (isAlertingRule(rule)) {
acc.push({ dataSourceName, namespaceName, groupName, rule });
}
});
});
return acc;
}, []);
};
export const alertStateToState: Record<PromAlertingRuleState | GrafanaAlertState, State> = {
[PromAlertingRuleState.Inactive]: 'good',
[PromAlertingRuleState.Firing]: 'bad',
[PromAlertingRuleState.Pending]: 'warning',
[GrafanaAlertState.Alerting]: 'bad',
[GrafanaAlertState.Error]: 'bad',
[GrafanaAlertState.NoData]: 'info',
[GrafanaAlertState.Normal]: 'good',
[GrafanaAlertState.Pending]: 'warning',
};
export function getFirstActiveAt(promRule: AlertingRule) {
if (!promRule.alerts) {
return null;
}
return promRule.alerts.reduce((prev, alert) => {
if (alert.activeAt && alert.state !== GrafanaAlertState.Normal) {
const activeAt = new Date(alert.activeAt);
if (prev === null || prev.getTime() > activeAt.getTime()) {
return activeAt;
}
}
return prev;
}, null as Date | null);
}

@ -0,0 +1,78 @@
import React, { useEffect, useMemo, useState } from 'react';
import pluralize from 'pluralize';
import { Icon, useStyles2 } from '@grafana/ui';
import { Alert, PromRuleWithLocation } from 'app/types/unified-alerting';
import { AlertLabels } from 'app/features/alerting/unified/components/AlertLabels';
import { AlertStateTag } from 'app/features/alerting/unified/components/rules/AlertStateTag';
import { dateTime, GrafanaTheme2 } from '@grafana/data';
import { css } from '@emotion/css';
import { PromAlertingRuleState } from 'app/types/unified-alerting-dto';
import { omit } from 'lodash';
import { alertInstanceKey } from 'app/features/alerting/unified/utils/rules';
interface Props {
ruleWithLocation: PromRuleWithLocation;
showInstances: boolean;
}
export const AlertInstances = ({ ruleWithLocation, showInstances }: Props) => {
const { rule } = ruleWithLocation;
const [displayInstances, setDisplayInstances] = useState<boolean>(showInstances);
const styles = useStyles2(getStyles);
useEffect(() => {
setDisplayInstances(showInstances);
}, [showInstances]);
// sort instances, because API returns them in random order every time
const sortedAlerts = useMemo(
(): Alert[] =>
displayInstances
? rule.alerts.slice().sort((a, b) => alertInstanceKey(a).localeCompare(alertInstanceKey(b)))
: [],
[rule, displayInstances]
);
return (
<div>
{rule.state !== PromAlertingRuleState.Inactive && (
<div className={styles.instance} onClick={() => setDisplayInstances(!displayInstances)}>
<Icon name={displayInstances ? 'angle-down' : 'angle-right'} size={'md'} />
<span>{`${rule.alerts.length} ${pluralize('instance', rule.alerts.length)}`}</span>
</div>
)}
{!!sortedAlerts.length && (
<ol className={styles.list}>
{sortedAlerts.map((alert, index) => {
return (
<li className={styles.listItem} key={`${alert.activeAt}-${index}`}>
<div>
<AlertStateTag state={alert.state} />
<span className={styles.date}>{dateTime(alert.activeAt).format('YYYY-MM-DD HH:mm:ss')}</span>
</div>
<AlertLabels labels={omit(alert.labels, 'alertname')} />
</li>
);
})}
</ol>
)}
</div>
);
};
const getStyles = (theme: GrafanaTheme2) => ({
instance: css`
cursor: pointer;
`,
list: css`
list-style-type: none;
`,
listItem: css`
margin-top: ${theme.spacing(1)};
`,
date: css`
font-size: ${theme.typography.bodySmall.fontSize};
padding-left: ${theme.spacing(0.5)};
`,
});

@ -0,0 +1,296 @@
import React, { useEffect, useMemo } from 'react';
import { sortBy } from 'lodash';
import { useDispatch } from 'react-redux';
import { GrafanaTheme, GrafanaTheme2, intervalToAbbreviatedDurationString, PanelProps } from '@grafana/data';
import { CustomScrollbar, Icon, IconName, LoadingPlaceholder, useStyles, useStyles2 } from '@grafana/ui';
import { css } from '@emotion/css';
import { AlertInstances } from './AlertInstances';
import alertDef from 'app/features/alerting/state/alertDef';
import { SortOrder, UnifiedAlertListOptions } from './types';
import { flattenRules, alertStateToState, getFirstActiveAt } from 'app/features/alerting/unified/utils/rules';
import { PromRuleWithLocation } from 'app/types/unified-alerting';
import { fetchAllPromRulesAction } from 'app/features/alerting/unified/state/actions';
import { useUnifiedAlertingSelector } from 'app/features/alerting/unified/hooks/useUnifiedAlertingSelector';
import { getAllRulesSourceNames } from 'app/features/alerting/unified/utils/datasource';
import { getDashboardSrv } from 'app/features/dashboard/services/DashboardSrv';
import { Annotation, RULE_LIST_POLL_INTERVAL_MS } from 'app/features/alerting/unified/utils/constants';
import { PromAlertingRuleState } from 'app/types/unified-alerting-dto';
export function UnifiedAlertList(props: PanelProps<UnifiedAlertListOptions>) {
const dispatch = useDispatch();
const rulesDataSourceNames = useMemo(getAllRulesSourceNames, []);
useEffect(() => {
dispatch(fetchAllPromRulesAction());
const interval = setInterval(() => dispatch(fetchAllPromRulesAction()), RULE_LIST_POLL_INTERVAL_MS);
return () => {
clearInterval(interval);
};
}, [dispatch]);
const promRulesRequests = useUnifiedAlertingSelector((state) => state.promRules);
const dispatched = rulesDataSourceNames.some((name) => promRulesRequests[name]?.dispatched);
const loading = rulesDataSourceNames.some((name) => promRulesRequests[name]?.loading);
const haveResults = rulesDataSourceNames.some(
(name) => promRulesRequests[name]?.result?.length && !promRulesRequests[name]?.error
);
const styles = useStyles(getStyles);
const stateStyle = useStyles2(getStateTagStyles);
const rules = useMemo(
() =>
filterRules(
props.options,
sortRules(
props.options.sortOrder,
Object.values(promRulesRequests).flatMap(({ result = [] }) => flattenRules(result))
)
),
[props.options, promRulesRequests]
);
const rulesToDisplay = rules.length <= props.options.maxItems ? rules : rules.slice(0, props.options.maxItems);
const noAlertsMessage = rules.length ? '' : 'No alerts';
return (
<CustomScrollbar autoHeightMin="100%" autoHeightMax="100%">
<div className={styles.container}>
{dispatched && loading && !haveResults && <LoadingPlaceholder text="Loading..." />}
{noAlertsMessage && <div className={styles.noAlertsMessage}>{noAlertsMessage}</div>}
<section>
<ol className={styles.alertRuleList}>
{haveResults &&
rulesToDisplay.map((ruleWithLocation, index) => {
const { rule, namespaceName, groupName } = ruleWithLocation;
const firstActiveAt = getFirstActiveAt(rule);
return (
<li
className={styles.alertRuleItem}
key={`alert-${namespaceName}-${groupName}-${rule.name}-${index}`}
>
<div className={stateStyle.icon}>
<Icon
name={alertDef.getStateDisplayModel(rule.state).iconClass as IconName}
className={stateStyle[alertStateToState[rule.state]]}
size={'lg'}
/>
</div>
<div>
<div className={styles.instanceDetails}>
<div className={styles.alertName} title={rule.name}>
{rule.name}
</div>
<div className={styles.alertDuration}>
<span className={stateStyle[alertStateToState[rule.state]]}>{rule.state.toUpperCase()}</span>{' '}
{firstActiveAt && rule.state !== PromAlertingRuleState.Inactive && (
<>
for{' '}
<span>
{intervalToAbbreviatedDurationString({
start: firstActiveAt,
end: Date.now(),
})}
</span>
</>
)}
</div>
</div>
<AlertInstances ruleWithLocation={ruleWithLocation} showInstances={props.options.showInstances} />
</div>
</li>
);
})}
</ol>
</section>
</div>
</CustomScrollbar>
);
}
function sortRules(sortOrder: SortOrder, rules: PromRuleWithLocation[]) {
if (sortOrder === SortOrder.Importance) {
// @ts-ignore
return sortBy(rules, (rule) => alertDef.alertStateSortScore[rule.state]);
} else if (sortOrder === SortOrder.TimeAsc) {
return sortBy(rules, (rule) => getFirstActiveAt(rule.rule) || new Date());
} else if (sortOrder === SortOrder.TimeDesc) {
return sortBy(rules, (rule) => getFirstActiveAt(rule.rule) || new Date()).reverse();
}
const result = sortBy(rules, (rule) => rule.rule.name.toLowerCase());
if (sortOrder === SortOrder.AlphaDesc) {
result.reverse();
}
return result;
}
function filterRules(options: PanelProps<UnifiedAlertListOptions>['options'], rules: PromRuleWithLocation[]) {
let filteredRules = [...rules];
if (options.dashboardAlerts) {
const dashboardUid = getDashboardSrv().getCurrent()?.uid;
filteredRules = filteredRules.filter(({ rule: { annotations = {} } }) =>
Object.entries(annotations).some(([key, value]) => key === Annotation.dashboardUID && value === dashboardUid)
);
}
if (options.alertName) {
filteredRules = filteredRules.filter(({ rule: { name } }) =>
name.toLocaleLowerCase().includes(options.alertName.toLocaleLowerCase())
);
}
if (Object.values(options.stateFilter).some((value) => value)) {
filteredRules = filteredRules.filter((rule) => {
return (
(options.stateFilter.firing && rule.rule.state === PromAlertingRuleState.Firing) ||
(options.stateFilter.pending && rule.rule.state === PromAlertingRuleState.Pending) ||
(options.stateFilter.inactive && rule.rule.state === PromAlertingRuleState.Inactive)
);
});
}
if (options.folder) {
filteredRules = filteredRules.filter((rule) => {
return rule.namespaceName === options.folder.title;
});
}
return filteredRules;
}
const getStyles = (theme: GrafanaTheme) => ({
cardContainer: css`
padding: ${theme.spacing.xs} 0 ${theme.spacing.xxs} 0;
line-height: ${theme.typography.lineHeight.md};
margin-bottom: 0px;
`,
container: css`
overflow-y: auto;
height: 100%;
`,
alertRuleList: css`
display: flex;
flex-wrap: wrap;
justify-content: space-between;
list-style-type: none;
`,
alertRuleItem: css`
display: flex;
align-items: center;
width: 100%;
height: 100%;
background: ${theme.colors.bg2};
padding: ${theme.spacing.xs} ${theme.spacing.sm};
border-radius: ${theme.border.radius.md};
margin-bottom: ${theme.spacing.xs};
& > * {
margin-right: ${theme.spacing.sm};
}
`,
alertName: css`
font-size: ${theme.typography.size.md};
font-weight: ${theme.typography.weight.bold};
`,
alertDuration: css`
font-size: ${theme.typography.size.sm};
`,
alertRuleItemText: css`
font-weight: ${theme.typography.weight.bold};
font-size: ${theme.typography.size.sm};
margin: 0;
`,
alertRuleItemTime: css`
color: ${theme.colors.textWeak};
font-weight: normal;
white-space: nowrap;
`,
alertRuleItemInfo: css`
font-weight: normal;
flex-grow: 2;
display: flex;
align-items: flex-end;
`,
noAlertsMessage: css`
display: flex;
align-items: center;
justify-content: center;
width: 100%;
height: 100%;
`,
alertIcon: css`
margin-right: ${theme.spacing.xs};
`,
instanceDetails: css`
min-width: 1px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
`,
});
const getStateTagStyles = (theme: GrafanaTheme2) => ({
common: css`
width: 70px;
text-align: center;
align-self: stretch;
display: inline-block;
color: white;
border-radius: ${theme.shape.borderRadius()};
font-size: ${theme.typography.size.sm};
/* padding: ${theme.spacing(2, 0)}; */
text-transform: capitalize;
line-height: 1.2;
flex-shrink: 0;
display: flex;
flex-direction: column;
justify-content: center;
`,
icon: css`
margin-top: ${theme.spacing(2.5)};
align-self: flex-start;
`,
// good: css`
// background-color: ${theme.colors.success.main};
// border: solid 1px ${theme.colors.success.main};
// color: ${theme.colors.success.contrastText};
// `,
// warning: css`
// background-color: ${theme.colors.warning.main};
// border: solid 1px ${theme.colors.warning.main};
// color: ${theme.colors.warning.contrastText};
// `,
// bad: css`
// background-color: ${theme.colors.error.main};
// border: solid 1px ${theme.colors.error.main};
// color: ${theme.colors.error.contrastText};
// `,
// neutral: css`
// background-color: ${theme.colors.secondary.main};
// border: solid 1px ${theme.colors.secondary.main};
// `,
// info: css`
// background-color: ${theme.colors.primary.main};
// border: solid 1px ${theme.colors.primary.main};
// color: ${theme.colors.primary.contrastText};
// `,
good: css`
color: ${theme.colors.success.main};
`,
bad: css`
color: ${theme.colors.error.main};
`,
warning: css`
color: ${theme.colors.warning.main};
`,
neutral: css`
color: ${theme.colors.secondary.main};
`,
info: css`
color: ${theme.colors.primary.main};
`,
});

@ -2,15 +2,18 @@ import React from 'react';
import { PanelPlugin } from '@grafana/data';
import { TagsInput } from '@grafana/ui';
import { AlertList } from './AlertList';
import { UnifiedAlertList } from './UnifiedAlertList';
import { FolderPicker } from 'app/core/components/Select/FolderPicker';
import { AlertListOptions, ShowOption, SortOrder } from './types';
import { AlertListOptions, UnifiedAlertListOptions, ShowOption, SortOrder } from './types';
import { alertListPanelMigrationHandler } from './AlertListMigrationHandler';
import { config } from '@grafana/runtime';
import { RuleFolderPicker } from 'app/features/alerting/unified/components/rule-editor/RuleFolderPicker';
function showIfCurrentState(options: AlertListOptions) {
return options.showOptions === ShowOption.Current;
}
export const plugin = new PanelPlugin<AlertListOptions>(AlertList)
const alertList = new PanelPlugin<AlertListOptions>(AlertList)
.setPanelOptions((builder) => {
builder
.addSelect({
@ -140,3 +143,84 @@ export const plugin = new PanelPlugin<AlertListOptions>(AlertList)
});
})
.setMigrationHandler(alertListPanelMigrationHandler);
const unifiedAlertList = new PanelPlugin<UnifiedAlertListOptions>(UnifiedAlertList).setPanelOptions((builder) => {
builder
.addNumberInput({
name: 'Max items',
path: 'maxItems',
defaultValue: 20,
category: ['Options'],
})
.addSelect({
name: 'Sort order',
path: 'sortOrder',
settings: {
options: [
{ label: 'Alphabetical (asc)', value: SortOrder.AlphaAsc },
{ label: 'Alphabetical (desc)', value: SortOrder.AlphaDesc },
{ label: 'Importance', value: SortOrder.Importance },
{ label: 'Time (asc)', value: SortOrder.TimeAsc },
{ label: 'Time (desc)', value: SortOrder.TimeDesc },
],
},
defaultValue: SortOrder.AlphaAsc,
category: ['Options'],
})
.addBooleanSwitch({
path: 'dashboardAlerts',
name: 'Alerts from this dashboard',
defaultValue: false,
category: ['Options'],
})
.addBooleanSwitch({
path: 'showInstances',
name: 'Show alert instances',
defaultValue: false,
category: ['Options'],
})
.addTextInput({
path: 'alertName',
name: 'Alert name',
defaultValue: '',
category: ['Filter'],
})
.addCustomEditor({
path: 'folder',
name: 'Folder',
id: 'folder',
defaultValue: null,
editor: function RenderFolderPicker(props) {
return (
<RuleFolderPicker
{...props}
enableReset={true}
onChange={({ title, id }) => {
return props.onChange({ title, id });
}}
/>
);
},
category: ['Filter'],
})
.addBooleanSwitch({
path: 'stateFilter.firing',
name: 'Alerting',
defaultValue: true,
category: ['State filter'],
})
.addBooleanSwitch({
path: 'stateFilter.pending',
name: 'Pending',
defaultValue: true,
category: ['State filter'],
})
.addBooleanSwitch({
path: 'stateFilter.inactive',
name: 'Inactive',
defaultValue: false,
category: ['State filter'],
});
});
export const plugin = config.featureToggles.ngalert ? unifiedAlertList : alertList;

@ -1,3 +1,5 @@
import { PromAlertingRuleState } from 'app/types/unified-alerting-dto';
export enum SortOrder {
AlphaAsc = 1,
AlphaDesc,
@ -29,3 +31,15 @@ export interface AlertListOptions {
};
folderId: number;
}
export interface UnifiedAlertListOptions {
maxItems: number;
sortOrder: SortOrder;
dashboardAlerts: boolean;
alertName: string;
showInstances: boolean;
folder: { id: number; title: string };
stateFilter: {
[K in PromAlertingRuleState]: boolean;
};
}

@ -101,6 +101,13 @@ export interface RuleWithLocation {
rule: RulerRuleDTO;
}
export interface PromRuleWithLocation {
rule: AlertingRule;
dataSourceName: string;
namespaceName: string;
groupName: string;
}
export interface CloudRuleIdentifier {
ruleSourceName: string;
namespace: string;

Loading…
Cancel
Save