mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Alerting: alert list state view (#33020)
This commit is contained in:
parent
826d82fe95
commit
19c6a02f49
@ -135,6 +135,7 @@ export type IconName =
|
||||
| 'user'
|
||||
| 'users-alt'
|
||||
| 'wrap-text'
|
||||
| 'heart-rate'
|
||||
| 'x';
|
||||
|
||||
export const getAvailableIcons = (): IconName[] => [
|
||||
|
@ -1,32 +1,41 @@
|
||||
import { DataSourceInstanceSettings, GrafanaTheme } from '@grafana/data';
|
||||
import { Icon, InfoBox, useStyles, Button } from '@grafana/ui';
|
||||
import { DataSourceInstanceSettings, GrafanaTheme, urlUtil } from '@grafana/data';
|
||||
import { Icon, InfoBox, useStyles, Button, ButtonGroup, ToolbarButton } from '@grafana/ui';
|
||||
import { SerializedError } from '@reduxjs/toolkit';
|
||||
import React, { FC, useEffect, useMemo } from 'react';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { AlertingPageWrapper } from './components/AlertingPageWrapper';
|
||||
import { NoRulesSplash } from './components/rules/NoRulesCTA';
|
||||
import { SystemOrApplicationRules } from './components/rules/SystemOrApplicationRules';
|
||||
import { useUnifiedAlertingSelector } from './hooks/useUnifiedAlertingSelector';
|
||||
import { useFilteredRules } from './hooks/useFilteredRules';
|
||||
import { fetchAllPromAndRulerRulesAction } from './state/actions';
|
||||
import {
|
||||
getAllRulesSourceNames,
|
||||
getRulesDataSources,
|
||||
GRAFANA_RULES_SOURCE_NAME,
|
||||
isCloudRulesSource,
|
||||
} from './utils/datasource';
|
||||
import { getAllRulesSourceNames, getRulesDataSources, GRAFANA_RULES_SOURCE_NAME } from './utils/datasource';
|
||||
import { css } from '@emotion/css';
|
||||
import { ThresholdRules } from './components/rules/ThresholdRules';
|
||||
import { useCombinedRuleNamespaces } from './hooks/useCombinedRuleNamespaces';
|
||||
import { RULE_LIST_POLL_INTERVAL_MS } from './utils/constants';
|
||||
import { isRulerNotSupportedResponse } from './utils/rules';
|
||||
import RulesFilter from './components/rules/RulesFilter';
|
||||
import { RuleListGroupView } from './components/rules/RuleListGroupView';
|
||||
import { RuleListStateView } from './components/rules/RuleListStateView';
|
||||
import { useQueryParams } from 'app/core/hooks/useQueryParams';
|
||||
|
||||
const VIEWS = {
|
||||
groups: RuleListGroupView,
|
||||
state: RuleListStateView,
|
||||
};
|
||||
|
||||
export const RuleList: FC = () => {
|
||||
const dispatch = useDispatch();
|
||||
const styles = useStyles(getStyles);
|
||||
const rulesDataSourceNames = useMemo(getAllRulesSourceNames, []);
|
||||
|
||||
const [queryParams] = useQueryParams();
|
||||
|
||||
const view = VIEWS[queryParams['view'] as keyof typeof VIEWS]
|
||||
? (queryParams['view'] as keyof typeof VIEWS)
|
||||
: 'groups';
|
||||
|
||||
const ViewComponent = VIEWS[view];
|
||||
|
||||
// fetch rules, then poll every RULE_LIST_POLL_INTERVAL_MS
|
||||
useEffect(() => {
|
||||
dispatch(fetchAllPromAndRulerRulesAction());
|
||||
@ -75,19 +84,6 @@ export const RuleList: FC = () => {
|
||||
|
||||
const combinedNamespaces = useCombinedRuleNamespaces();
|
||||
const filteredNamespaces = useFilteredRules(combinedNamespaces);
|
||||
const [thresholdNamespaces, systemNamespaces] = useMemo(() => {
|
||||
const sorted = filteredNamespaces
|
||||
.map((namespace) => ({
|
||||
...namespace,
|
||||
groups: namespace.groups.sort((a, b) => a.name.localeCompare(b.name)),
|
||||
}))
|
||||
.sort((a, b) => a.name.localeCompare(b.name));
|
||||
return [
|
||||
sorted.filter((ns) => ns.rulesSource === GRAFANA_RULES_SOURCE_NAME),
|
||||
sorted.filter((ns) => isCloudRulesSource(ns.rulesSource)),
|
||||
];
|
||||
}, [filteredNamespaces]);
|
||||
|
||||
return (
|
||||
<AlertingPageWrapper pageId="alert-list" isLoading={loading && !haveResults}>
|
||||
{(promReqeustErrors.length || rulerRequestErrors.length || grafanaPromError) && (
|
||||
@ -126,6 +122,18 @@ export const RuleList: FC = () => {
|
||||
<RulesFilter />
|
||||
<div className={styles.break} />
|
||||
<div className={styles.buttonsContainer}>
|
||||
<ButtonGroup>
|
||||
<a href={urlUtil.renderUrl('/alerting/list', { ...queryParams, view: 'group' })}>
|
||||
<ToolbarButton variant={view === 'groups' ? 'active' : 'default'} icon="folder">
|
||||
Groups
|
||||
</ToolbarButton>
|
||||
</a>
|
||||
<a href={urlUtil.renderUrl('/alerting/list', { ...queryParams, view: 'state' })}>
|
||||
<ToolbarButton variant={view === 'state' ? 'active' : 'default'} icon="heart-rate">
|
||||
State
|
||||
</ToolbarButton>
|
||||
</a>
|
||||
</ButtonGroup>
|
||||
<div />
|
||||
<a href="/alerting/new">
|
||||
<Button icon="plus">New alert rule</Button>
|
||||
@ -134,8 +142,7 @@ export const RuleList: FC = () => {
|
||||
</>
|
||||
)}
|
||||
{showNewAlertSplash && <NoRulesSplash />}
|
||||
{haveResults && <ThresholdRules namespaces={thresholdNamespaces} />}
|
||||
{haveResults && <SystemOrApplicationRules namespaces={systemNamespaces} />}
|
||||
{haveResults && <ViewComponent namespaces={filteredNamespaces} />}
|
||||
</AlertingPageWrapper>
|
||||
);
|
||||
};
|
||||
|
@ -0,0 +1,31 @@
|
||||
import { CombinedRuleNamespace } from 'app/types/unified-alerting';
|
||||
import React, { FC, useMemo } from 'react';
|
||||
import { isCloudRulesSource, isGrafanaRulesSource } from '../../utils/datasource';
|
||||
import { SystemOrApplicationRules } from './SystemOrApplicationRules';
|
||||
import { ThresholdRules } from './ThresholdRules';
|
||||
|
||||
interface Props {
|
||||
namespaces: CombinedRuleNamespace[];
|
||||
}
|
||||
|
||||
export const RuleListGroupView: FC<Props> = ({ namespaces }) => {
|
||||
const [thresholdNamespaces, systemNamespaces] = useMemo(() => {
|
||||
const sorted = namespaces
|
||||
.map((namespace) => ({
|
||||
...namespace,
|
||||
groups: namespace.groups.sort((a, b) => a.name.localeCompare(b.name)),
|
||||
}))
|
||||
.sort((a, b) => a.name.localeCompare(b.name));
|
||||
return [
|
||||
sorted.filter((ns) => isGrafanaRulesSource(ns.rulesSource)),
|
||||
sorted.filter((ns) => isCloudRulesSource(ns.rulesSource)),
|
||||
];
|
||||
}, [namespaces]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<ThresholdRules namespaces={thresholdNamespaces} />
|
||||
<SystemOrApplicationRules namespaces={systemNamespaces} />
|
||||
</>
|
||||
);
|
||||
};
|
@ -0,0 +1,43 @@
|
||||
import { css } from '@emotion/css';
|
||||
import { GrafanaTheme } from '@grafana/data';
|
||||
import { useStyles } from '@grafana/ui';
|
||||
import { CombinedRule } from 'app/types/unified-alerting';
|
||||
import { PromAlertingRuleState } from 'app/types/unified-alerting-dto';
|
||||
import { capitalize } from 'lodash';
|
||||
import React, { FC, useState } from 'react';
|
||||
import { CollapseToggle } from '../CollapseToggle';
|
||||
import { RulesTable } from './RulesTable';
|
||||
|
||||
interface Props {
|
||||
rules: CombinedRule[];
|
||||
state: PromAlertingRuleState;
|
||||
defaultCollapsed?: boolean;
|
||||
}
|
||||
|
||||
export const RuleListStateSection: FC<Props> = ({ rules, state, defaultCollapsed = false }) => {
|
||||
const [collapsed, setCollapsed] = useState(defaultCollapsed);
|
||||
const styles = useStyles(getStyles);
|
||||
return (
|
||||
<>
|
||||
<h4 className={styles.header}>
|
||||
<CollapseToggle
|
||||
className={styles.collapseToggle}
|
||||
size="xxl"
|
||||
isCollapsed={collapsed}
|
||||
onToggle={() => setCollapsed(!collapsed)}
|
||||
/>
|
||||
{capitalize(state)} ({rules.length})
|
||||
</h4>
|
||||
{!collapsed && <RulesTable rules={rules} showGroupColumn={true} />}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const getStyles = (theme: GrafanaTheme) => ({
|
||||
collapseToggle: css`
|
||||
vertical-align: middle;
|
||||
`,
|
||||
header: css`
|
||||
margin-top: ${theme.spacing.md};
|
||||
`,
|
||||
});
|
@ -0,0 +1,46 @@
|
||||
import { CombinedRule, CombinedRuleNamespace } from 'app/types/unified-alerting';
|
||||
import { PromAlertingRuleState } from 'app/types/unified-alerting-dto';
|
||||
import React, { FC, useMemo } from 'react';
|
||||
import { isAlertingRule } from '../../utils/rules';
|
||||
import { RuleListStateSection } from './RuleListSateSection';
|
||||
|
||||
interface Props {
|
||||
namespaces: CombinedRuleNamespace[];
|
||||
}
|
||||
|
||||
type GroupedRules = Record<PromAlertingRuleState, CombinedRule[]>;
|
||||
|
||||
export const RuleListStateView: FC<Props> = ({ namespaces }) => {
|
||||
const groupedRules = useMemo(() => {
|
||||
const result: GroupedRules = {
|
||||
[PromAlertingRuleState.Firing]: [],
|
||||
[PromAlertingRuleState.Inactive]: [],
|
||||
[PromAlertingRuleState.Pending]: [],
|
||||
};
|
||||
|
||||
namespaces.forEach((namespace) =>
|
||||
namespace.groups.forEach((group) =>
|
||||
group.rules.forEach((rule) => {
|
||||
if (rule.promRule && isAlertingRule(rule.promRule)) {
|
||||
result[rule.promRule.state].push(rule);
|
||||
}
|
||||
})
|
||||
)
|
||||
);
|
||||
|
||||
Object.values(result).forEach((rules) => rules.sort((a, b) => a.name.localeCompare(b.name)));
|
||||
|
||||
return result;
|
||||
}, [namespaces]);
|
||||
return (
|
||||
<>
|
||||
<RuleListStateSection state={PromAlertingRuleState.Firing} rules={groupedRules[PromAlertingRuleState.Firing]} />
|
||||
<RuleListStateSection state={PromAlertingRuleState.Pending} rules={groupedRules[PromAlertingRuleState.Pending]} />
|
||||
<RuleListStateSection
|
||||
defaultCollapsed={true}
|
||||
state={PromAlertingRuleState.Inactive}
|
||||
rules={groupedRules[PromAlertingRuleState.Inactive]}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
@ -1,4 +1,4 @@
|
||||
import { CombinedRuleGroup, RulesSource } from 'app/types/unified-alerting';
|
||||
import { CombinedRuleGroup, CombinedRuleNamespace } from 'app/types/unified-alerting';
|
||||
import React, { FC, useMemo, useState, Fragment } from 'react';
|
||||
import { Icon, Tooltip, useStyles } from '@grafana/ui';
|
||||
import { GrafanaTheme } from '@grafana/data';
|
||||
@ -13,17 +13,17 @@ import { ActionIcon } from './ActionIcon';
|
||||
import pluralize from 'pluralize';
|
||||
import { useHasRuler } from '../../hooks/useHasRuler';
|
||||
interface Props {
|
||||
namespace: string;
|
||||
rulesSource: RulesSource;
|
||||
namespace: CombinedRuleNamespace;
|
||||
group: CombinedRuleGroup;
|
||||
}
|
||||
|
||||
export const RulesGroup: FC<Props> = React.memo(({ group, namespace, rulesSource }) => {
|
||||
export const RulesGroup: FC<Props> = React.memo(({ group, namespace }) => {
|
||||
const { rulesSource } = namespace;
|
||||
const styles = useStyles(getStyles);
|
||||
|
||||
const [isCollapsed, setIsCollapsed] = useState(true);
|
||||
|
||||
const hasRuler = useHasRuler(rulesSource);
|
||||
const hasRuler = useHasRuler();
|
||||
|
||||
const stats = useMemo(
|
||||
(): Record<PromAlertingRuleState, number> =>
|
||||
@ -60,14 +60,14 @@ export const RulesGroup: FC<Props> = React.memo(({ group, namespace, rulesSource
|
||||
}
|
||||
|
||||
const actionIcons: React.ReactNode[] = [];
|
||||
if (hasRuler) {
|
||||
if (hasRuler(rulesSource)) {
|
||||
actionIcons.push(<ActionIcon key="edit" icon="pen" tooltip="edit" />);
|
||||
}
|
||||
if (rulesSource === GRAFANA_RULES_SOURCE_NAME) {
|
||||
actionIcons.push(<ActionIcon key="manage-perms" icon="lock" tooltip="manage permissions" />);
|
||||
}
|
||||
|
||||
const groupName = isCloudRulesSource(rulesSource) ? `${namespace} > ${group.name}` : namespace;
|
||||
const groupName = isCloudRulesSource(rulesSource) ? `${namespace.name} > ${group.name}` : namespace.name;
|
||||
|
||||
return (
|
||||
<div className={styles.wrapper} data-testid="rule-group">
|
||||
@ -105,7 +105,7 @@ export const RulesGroup: FC<Props> = React.memo(({ group, namespace, rulesSource
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
{!isCollapsed && <RulesTable rulesSource={rulesSource} namespace={namespace} group={group} />}
|
||||
{!isCollapsed && <RulesTable showGuidelines={true} rules={group.rules} />}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
@ -1,40 +1,43 @@
|
||||
import { GrafanaTheme, rangeUtil } from '@grafana/data';
|
||||
import { GrafanaTheme } from '@grafana/data';
|
||||
import { ConfirmModal, useStyles } from '@grafana/ui';
|
||||
import { CombinedRuleGroup, RulesSource } from 'app/types/unified-alerting';
|
||||
import React, { FC, Fragment, useState } from 'react';
|
||||
import { getRuleIdentifier, isAlertingRule, stringifyRuleIdentifier } from '../../utils/rules';
|
||||
import { CollapseToggle } from '../CollapseToggle';
|
||||
import { css, cx } from '@emotion/css';
|
||||
import { TimeToNow } from '../TimeToNow';
|
||||
import { StateTag } from '../StateTag';
|
||||
import { RuleDetails } from './RuleDetails';
|
||||
import { getAlertTableStyles } from '../../styles/table';
|
||||
import { ActionIcon } from './ActionIcon';
|
||||
import { createExploreLink } from '../../utils/misc';
|
||||
import { getRulesSourceName, isCloudRulesSource } from '../../utils/datasource';
|
||||
import { RulerRuleDTO } from 'app/types/unified-alerting-dto';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { deleteRuleAction } from '../../state/actions';
|
||||
import { useHasRuler } from '../../hooks/useHasRuler';
|
||||
import { CombinedRule } from 'app/types/unified-alerting';
|
||||
|
||||
interface Props {
|
||||
namespace: string;
|
||||
group: CombinedRuleGroup;
|
||||
rulesSource: RulesSource;
|
||||
rules: CombinedRule[];
|
||||
showGuidelines?: boolean;
|
||||
showGroupColumn?: boolean;
|
||||
emptyMessage?: string;
|
||||
}
|
||||
|
||||
export const RulesTable: FC<Props> = ({ group, rulesSource, namespace }) => {
|
||||
const { rules } = group;
|
||||
export const RulesTable: FC<Props> = ({
|
||||
rules,
|
||||
showGuidelines = false,
|
||||
emptyMessage = 'No rules found.',
|
||||
showGroupColumn = false,
|
||||
}) => {
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const hasRuler = useHasRuler(rulesSource);
|
||||
const hasRuler = useHasRuler();
|
||||
|
||||
const styles = useStyles(getStyles);
|
||||
const tableStyles = useStyles(getAlertTableStyles);
|
||||
|
||||
const [expandedKeys, setExpandedKeys] = useState<string[]>([]);
|
||||
|
||||
const [ruleToDelete, setRuleToDelete] = useState<RulerRuleDTO>();
|
||||
const [ruleToDelete, setRuleToDelete] = useState<CombinedRule>();
|
||||
|
||||
const toggleExpandedState = (ruleKey: string) =>
|
||||
setExpandedKeys(
|
||||
@ -42,20 +45,29 @@ export const RulesTable: FC<Props> = ({ group, rulesSource, namespace }) => {
|
||||
);
|
||||
|
||||
const deleteRule = () => {
|
||||
if (ruleToDelete) {
|
||||
if (ruleToDelete && ruleToDelete.rulerRule) {
|
||||
dispatch(
|
||||
deleteRuleAction(getRuleIdentifier(getRulesSourceName(rulesSource), namespace, group.name, ruleToDelete))
|
||||
deleteRuleAction(
|
||||
getRuleIdentifier(
|
||||
getRulesSourceName(ruleToDelete.namespace.rulesSource),
|
||||
ruleToDelete.namespace.name,
|
||||
ruleToDelete.group.name,
|
||||
ruleToDelete.rulerRule
|
||||
)
|
||||
)
|
||||
);
|
||||
setRuleToDelete(undefined);
|
||||
}
|
||||
};
|
||||
|
||||
const wrapperClass = cx(styles.wrapper, { [styles.wrapperMargin]: showGuidelines });
|
||||
|
||||
if (!rules.length) {
|
||||
return <div className={styles.wrapper}>Folder is empty.</div>;
|
||||
return <div className={wrapperClass}>{emptyMessage}</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={styles.wrapper}>
|
||||
<div className={wrapperClass}>
|
||||
<table className={tableStyles.table} data-testid="rules-table">
|
||||
<colgroup>
|
||||
<col className={styles.colExpand} />
|
||||
@ -64,16 +76,17 @@ export const RulesTable: FC<Props> = ({ group, rulesSource, namespace }) => {
|
||||
<col />
|
||||
<col />
|
||||
<col />
|
||||
{showGroupColumn && <col />}
|
||||
</colgroup>
|
||||
<thead>
|
||||
<tr>
|
||||
<th className={styles.relative}>
|
||||
<div className={cx(styles.headerGuideline, styles.guideline)} />
|
||||
{showGuidelines && <div className={cx(styles.headerGuideline, styles.guideline)} />}
|
||||
</th>
|
||||
<th>State</th>
|
||||
<th>Name</th>
|
||||
{showGroupColumn && <th>Group</th>}
|
||||
<th>Status</th>
|
||||
<th>Evaluation</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
@ -81,6 +94,8 @@ export const RulesTable: FC<Props> = ({ group, rulesSource, namespace }) => {
|
||||
{(() => {
|
||||
const seenKeys: string[] = [];
|
||||
return rules.map((rule, ruleIdx) => {
|
||||
const { namespace, group } = rule;
|
||||
const { rulesSource } = namespace;
|
||||
let key = JSON.stringify([rule.promRule?.type, rule.labels, rule.query, rule.name, rule.annotations]);
|
||||
if (seenKeys.includes(key)) {
|
||||
key += `-${ruleIdx}`;
|
||||
@ -90,16 +105,20 @@ export const RulesTable: FC<Props> = ({ group, rulesSource, namespace }) => {
|
||||
const { promRule, rulerRule } = rule;
|
||||
const statuses = [
|
||||
promRule?.health,
|
||||
hasRuler && promRule && !rulerRule ? 'deleting' : '',
|
||||
hasRuler && rulerRule && !promRule ? 'creating' : '',
|
||||
hasRuler(rulesSource) && promRule && !rulerRule ? 'deleting' : '',
|
||||
hasRuler(rulesSource) && rulerRule && !promRule ? 'creating' : '',
|
||||
].filter((x) => !!x);
|
||||
return (
|
||||
<Fragment key={key}>
|
||||
<tr className={ruleIdx % 2 === 0 ? tableStyles.evenRow : undefined}>
|
||||
<td className={styles.relative}>
|
||||
<div className={cx(styles.ruleTopGuideline, styles.guideline)} />
|
||||
{!(ruleIdx === rules.length - 1) && (
|
||||
<div className={cx(styles.ruleBottomGuideline, styles.guideline)} />
|
||||
{showGuidelines && (
|
||||
<>
|
||||
<div className={cx(styles.ruleTopGuideline, styles.guideline)} />
|
||||
{!(ruleIdx === rules.length - 1) && (
|
||||
<div className={cx(styles.ruleBottomGuideline, styles.guideline)} />
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
<CollapseToggle
|
||||
isCollapsed={!isExpanded}
|
||||
@ -109,21 +128,14 @@ export const RulesTable: FC<Props> = ({ group, rulesSource, namespace }) => {
|
||||
</td>
|
||||
<td>{promRule && isAlertingRule(promRule) ? <StateTag status={promRule.state} /> : 'n/a'}</td>
|
||||
<td>{rule.name}</td>
|
||||
{showGroupColumn && (
|
||||
<td>{isCloudRulesSource(rulesSource) ? `${namespace.name} > ${group.name}` : namespace.name}</td>
|
||||
)}
|
||||
<td>{statuses.join(', ') || 'n/a'}</td>
|
||||
<td>
|
||||
{promRule?.lastEvaluation && promRule.evaluationTime ? (
|
||||
<>
|
||||
<TimeToNow date={promRule.lastEvaluation} />, for{' '}
|
||||
{rangeUtil.secondsToHms(promRule.evaluationTime)}
|
||||
</>
|
||||
) : (
|
||||
'n/a'
|
||||
)}
|
||||
</td>
|
||||
<td className={styles.actionsCell}>
|
||||
{isCloudRulesSource(rulesSource) && (
|
||||
<ActionIcon
|
||||
icon="compass"
|
||||
icon="chart-line"
|
||||
tooltip="view in explore"
|
||||
target="__blank"
|
||||
href={createExploreLink(rulesSource.name, rule.query)}
|
||||
@ -135,24 +147,24 @@ export const RulesTable: FC<Props> = ({ group, rulesSource, namespace }) => {
|
||||
tooltip="edit rule"
|
||||
href={`/alerting/${encodeURIComponent(
|
||||
stringifyRuleIdentifier(
|
||||
getRuleIdentifier(getRulesSourceName(rulesSource), namespace, group.name, rulerRule)
|
||||
getRuleIdentifier(getRulesSourceName(rulesSource), namespace.name, group.name, rulerRule)
|
||||
)
|
||||
)}/edit`}
|
||||
/>
|
||||
)}
|
||||
{!!rulerRule && (
|
||||
<ActionIcon icon="trash-alt" tooltip="delete rule" onClick={() => setRuleToDelete(rulerRule)} />
|
||||
<ActionIcon icon="trash-alt" tooltip="delete rule" onClick={() => setRuleToDelete(rule)} />
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
{isExpanded && (
|
||||
<tr className={ruleIdx % 2 === 0 ? tableStyles.evenRow : undefined}>
|
||||
<td className={styles.relative}>
|
||||
{!(ruleIdx === rules.length - 1) && (
|
||||
{!(ruleIdx === rules.length - 1) && showGuidelines && (
|
||||
<div className={cx(styles.ruleContentGuideline, styles.guideline)} />
|
||||
)}
|
||||
</td>
|
||||
<td colSpan={5}>
|
||||
<td colSpan={showGroupColumn ? 5 : 4}>
|
||||
<RuleDetails rulesSource={rulesSource} rule={rule} />
|
||||
</td>
|
||||
</tr>
|
||||
@ -179,9 +191,11 @@ export const RulesTable: FC<Props> = ({ group, rulesSource, namespace }) => {
|
||||
};
|
||||
|
||||
export const getStyles = (theme: GrafanaTheme) => ({
|
||||
wrapperMargin: css`
|
||||
margin-left: 36px;
|
||||
`,
|
||||
wrapper: css`
|
||||
margin-top: ${theme.spacing.md};
|
||||
margin-left: 36px;
|
||||
width: auto;
|
||||
padding: ${theme.spacing.sm};
|
||||
background-color: ${theme.colors.bg2};
|
||||
|
@ -36,17 +36,17 @@ export const SystemOrApplicationRules: FC<Props> = ({ namespaces }) => {
|
||||
)}
|
||||
</div>
|
||||
|
||||
{namespaces?.map(({ rulesSource, name, groups }) =>
|
||||
groups.map((group) => (
|
||||
{namespaces.map((namespace) => {
|
||||
const { groups, rulesSource } = namespace;
|
||||
return groups.map((group) => (
|
||||
<RulesGroup
|
||||
group={group}
|
||||
key={`${getRulesSourceName(rulesSource)}-${name}-${group.name}`}
|
||||
namespace={name}
|
||||
rulesSource={rulesSource}
|
||||
namespace={namespace}
|
||||
/>
|
||||
))
|
||||
)}
|
||||
{namespaces?.length === 0 && !dataSourcesLoading.length && !!rulesDataSources.length && <p>No rules found.</p>}
|
||||
));
|
||||
})}
|
||||
{namespaces?.length === 0 && !!rulesDataSources.length && <p>No rules found.</p>}
|
||||
{!rulesDataSources.length && <p>There are no Prometheus or Loki datas sources configured.</p>}
|
||||
</section>
|
||||
);
|
||||
|
@ -27,12 +27,7 @@ export const ThresholdRules: FC<Props> = ({ namespaces }) => {
|
||||
|
||||
{namespaces?.map((namespace) =>
|
||||
namespace.groups.map((group) => (
|
||||
<RulesGroup
|
||||
group={group}
|
||||
key={`${namespace.name}-${group.name}`}
|
||||
namespace={namespace.name}
|
||||
rulesSource={GRAFANA_RULES_SOURCE_NAME}
|
||||
/>
|
||||
<RulesGroup group={group} key={`${namespace.name}-${group.name}`} namespace={namespace} />
|
||||
))
|
||||
)}
|
||||
{namespaces?.length === 0 && <p>No rules found.</p>}
|
||||
|
@ -3,10 +3,11 @@ import {
|
||||
CombinedRuleGroup,
|
||||
CombinedRuleNamespace,
|
||||
Rule,
|
||||
RuleGroup,
|
||||
RuleNamespace,
|
||||
RulesSource,
|
||||
} from 'app/types/unified-alerting';
|
||||
import { RulerRulesConfigDTO } from 'app/types/unified-alerting-dto';
|
||||
import { RulerRuleDTO, RulerRuleGroupDTO, RulerRulesConfigDTO } from 'app/types/unified-alerting-dto';
|
||||
import { useMemo, useRef } from 'react';
|
||||
import { getAllRulesSources, isCloudRulesSource, isGrafanaRulesSource } from '../utils/datasource';
|
||||
import { isAlertingRule, isAlertingRulerRule, isRecordingRulerRule } from '../utils/rules';
|
||||
@ -26,109 +27,141 @@ export function useCombinedRuleNamespaces(): CombinedRuleNamespace[] {
|
||||
// cache results per rules source, so we only recalculate those for which results have actually changed
|
||||
const cache = useRef<Record<string, CacheValue>>({});
|
||||
|
||||
return useMemo(() => {
|
||||
const retv = getAllRulesSources()
|
||||
.map((rulesSource): CombinedRuleNamespace[] => {
|
||||
const rulesSourceName = isCloudRulesSource(rulesSource) ? rulesSource.name : rulesSource;
|
||||
const promRules = promRulesResponses[rulesSourceName]?.result;
|
||||
const rulerRules = rulerRulesResponses[rulesSourceName]?.result;
|
||||
return useMemo(
|
||||
() =>
|
||||
getAllRulesSources()
|
||||
.map((rulesSource): CombinedRuleNamespace[] => {
|
||||
const rulesSourceName = isCloudRulesSource(rulesSource) ? rulesSource.name : rulesSource;
|
||||
const promRules = promRulesResponses[rulesSourceName]?.result;
|
||||
const rulerRules = rulerRulesResponses[rulesSourceName]?.result;
|
||||
|
||||
const cached = cache.current[rulesSourceName];
|
||||
if (cached && cached.promRules === promRules && cached.rulerRules === rulerRules) {
|
||||
return cached.result;
|
||||
}
|
||||
const namespaces: Record<string, CombinedRuleNamespace> = {};
|
||||
const cached = cache.current[rulesSourceName];
|
||||
if (cached && cached.promRules === promRules && cached.rulerRules === rulerRules) {
|
||||
return cached.result;
|
||||
}
|
||||
const namespaces: Record<string, CombinedRuleNamespace> = {};
|
||||
|
||||
// first get all the ruler rules in
|
||||
Object.entries(rulerRules || {}).forEach(([namespaceName, groups]) => {
|
||||
namespaces[namespaceName] = {
|
||||
rulesSource,
|
||||
name: namespaceName,
|
||||
groups: groups.map((group) => ({
|
||||
name: group.name,
|
||||
rules: group.rules.map(
|
||||
(rule): CombinedRule =>
|
||||
isAlertingRulerRule(rule)
|
||||
? {
|
||||
name: rule.alert,
|
||||
query: rule.expr,
|
||||
labels: rule.labels || {},
|
||||
annotations: rule.annotations || {},
|
||||
rulerRule: rule,
|
||||
}
|
||||
: isRecordingRulerRule(rule)
|
||||
? {
|
||||
name: rule.record,
|
||||
query: rule.expr,
|
||||
labels: rule.labels || {},
|
||||
annotations: {},
|
||||
rulerRule: rule,
|
||||
}
|
||||
: {
|
||||
name: rule.grafana_alert.title,
|
||||
query: '',
|
||||
labels: rule.grafana_alert.labels || {},
|
||||
annotations: rule.grafana_alert.annotations || {},
|
||||
rulerRule: rule,
|
||||
}
|
||||
),
|
||||
})),
|
||||
};
|
||||
});
|
||||
|
||||
// then correlate with prometheus rules
|
||||
promRules?.forEach(({ name: namespaceName, groups }) => {
|
||||
const ns = (namespaces[namespaceName] = namespaces[namespaceName] || {
|
||||
rulesSource,
|
||||
name: namespaceName,
|
||||
groups: [],
|
||||
// first get all the ruler rules in
|
||||
Object.entries(rulerRules || {}).forEach(([namespaceName, groups]) => {
|
||||
const namespace: CombinedRuleNamespace = {
|
||||
rulesSource,
|
||||
name: namespaceName,
|
||||
groups: [],
|
||||
};
|
||||
namespaces[namespaceName] = namespace;
|
||||
addRulerGroupsToCombinedNamespace(namespace, groups);
|
||||
});
|
||||
|
||||
groups.forEach((group) => {
|
||||
let combinedGroup = ns.groups.find((g) => g.name === group.name);
|
||||
if (!combinedGroup) {
|
||||
combinedGroup = {
|
||||
name: group.name,
|
||||
rules: [],
|
||||
};
|
||||
ns.groups.push(combinedGroup);
|
||||
}
|
||||
|
||||
(group.rules ?? []).forEach((rule) => {
|
||||
const existingRule = getExistingRuleInGroup(rule, combinedGroup!, rulesSource);
|
||||
if (existingRule) {
|
||||
existingRule.promRule = rule;
|
||||
} else {
|
||||
combinedGroup!.rules.push({
|
||||
name: rule.name,
|
||||
query: rule.query,
|
||||
labels: rule.labels || {},
|
||||
annotations: isAlertingRule(rule) ? rule.annotations || {} : {},
|
||||
promRule: rule,
|
||||
});
|
||||
}
|
||||
// then correlate with prometheus rules
|
||||
promRules?.forEach(({ name: namespaceName, groups }) => {
|
||||
const ns = (namespaces[namespaceName] = namespaces[namespaceName] || {
|
||||
rulesSource,
|
||||
name: namespaceName,
|
||||
groups: [],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
const result = Object.values(namespaces);
|
||||
if (isGrafanaRulesSource(rulesSource)) {
|
||||
// merge all groups in case of grafana
|
||||
result.forEach((namespace) => {
|
||||
namespace.groups = [
|
||||
{
|
||||
name: 'default',
|
||||
rules: namespace.groups.flatMap((g) => g.rules).sort((a, b) => a.name.localeCompare(b.name)),
|
||||
},
|
||||
];
|
||||
addPromGroupsToCombinedNamespace(ns, groups);
|
||||
});
|
||||
}
|
||||
cache.current[rulesSourceName] = { promRules, rulerRules, result };
|
||||
return result;
|
||||
})
|
||||
.flat();
|
||||
return retv;
|
||||
}, [promRulesResponses, rulerRulesResponses]);
|
||||
|
||||
const result = Object.values(namespaces);
|
||||
if (isGrafanaRulesSource(rulesSource)) {
|
||||
// merge all groups in case of grafana managed, essentially treating namespaces (folders) as gorups
|
||||
result.forEach((namespace) => {
|
||||
namespace.groups = [
|
||||
{
|
||||
name: 'default',
|
||||
rules: namespace.groups.flatMap((g) => g.rules).sort((a, b) => a.name.localeCompare(b.name)),
|
||||
},
|
||||
];
|
||||
});
|
||||
}
|
||||
cache.current[rulesSourceName] = { promRules, rulerRules, result };
|
||||
return result;
|
||||
})
|
||||
.flat(),
|
||||
[promRulesResponses, rulerRulesResponses]
|
||||
);
|
||||
}
|
||||
|
||||
function addRulerGroupsToCombinedNamespace(namespace: CombinedRuleNamespace, groups: RulerRuleGroupDTO[]): void {
|
||||
namespace.groups = groups.map((group) => {
|
||||
const combinedGroup: CombinedRuleGroup = {
|
||||
name: group.name,
|
||||
rules: [],
|
||||
};
|
||||
combinedGroup.rules = group.rules.map((rule) => rulerRuleToCombinedRule(rule, namespace, combinedGroup));
|
||||
return combinedGroup;
|
||||
});
|
||||
}
|
||||
|
||||
function addPromGroupsToCombinedNamespace(namespace: CombinedRuleNamespace, groups: RuleGroup[]): void {
|
||||
groups.forEach((group) => {
|
||||
let combinedGroup = namespace.groups.find((g) => g.name === group.name);
|
||||
if (!combinedGroup) {
|
||||
combinedGroup = {
|
||||
name: group.name,
|
||||
rules: [],
|
||||
};
|
||||
namespace.groups.push(combinedGroup);
|
||||
}
|
||||
|
||||
(group.rules ?? []).forEach((rule) => {
|
||||
const existingRule = getExistingRuleInGroup(rule, combinedGroup!, namespace.rulesSource);
|
||||
if (existingRule) {
|
||||
existingRule.promRule = rule;
|
||||
} else {
|
||||
combinedGroup!.rules.push(promRuleToCombinedRule(rule, namespace, combinedGroup!));
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function promRuleToCombinedRule(rule: Rule, namespace: CombinedRuleNamespace, group: CombinedRuleGroup): CombinedRule {
|
||||
return {
|
||||
name: rule.name,
|
||||
query: rule.query,
|
||||
labels: rule.labels || {},
|
||||
annotations: isAlertingRule(rule) ? rule.annotations || {} : {},
|
||||
promRule: rule,
|
||||
namespace: namespace,
|
||||
group,
|
||||
};
|
||||
}
|
||||
|
||||
function rulerRuleToCombinedRule(
|
||||
rule: RulerRuleDTO,
|
||||
namespace: CombinedRuleNamespace,
|
||||
group: CombinedRuleGroup
|
||||
): CombinedRule {
|
||||
return isAlertingRulerRule(rule)
|
||||
? {
|
||||
name: rule.alert,
|
||||
query: rule.expr,
|
||||
labels: rule.labels || {},
|
||||
annotations: rule.annotations || {},
|
||||
rulerRule: rule,
|
||||
namespace,
|
||||
group,
|
||||
}
|
||||
: isRecordingRulerRule(rule)
|
||||
? {
|
||||
name: rule.record,
|
||||
query: rule.expr,
|
||||
labels: rule.labels || {},
|
||||
annotations: {},
|
||||
rulerRule: rule,
|
||||
namespace,
|
||||
group,
|
||||
}
|
||||
: {
|
||||
name: rule.grafana_alert.title,
|
||||
query: '',
|
||||
labels: rule.grafana_alert.labels || {},
|
||||
annotations: rule.grafana_alert.annotations || {},
|
||||
rulerRule: rule,
|
||||
namespace,
|
||||
group,
|
||||
};
|
||||
}
|
||||
|
||||
function getExistingRuleInGroup(
|
||||
|
@ -1,10 +1,16 @@
|
||||
import { RulesSource } from 'app/types/unified-alerting';
|
||||
import { useCallback } from 'react';
|
||||
import { GRAFANA_RULES_SOURCE_NAME } from '../utils/datasource';
|
||||
import { useUnifiedAlertingSelector } from './useUnifiedAlertingSelector';
|
||||
|
||||
// datasource has ruler if it's grafana managed or if we're able to load rules from it
|
||||
export function useHasRuler(rulesSource: string | RulesSource): boolean {
|
||||
export function useHasRuler(): (rulesSource: string | RulesSource) => boolean {
|
||||
const rulerRules = useUnifiedAlertingSelector((state) => state.rulerRules);
|
||||
const rulesSourceName = typeof rulesSource === 'string' ? rulesSource : rulesSource.name;
|
||||
return rulesSourceName === GRAFANA_RULES_SOURCE_NAME || !!rulerRules[rulesSourceName]?.result;
|
||||
return useCallback(
|
||||
(rulesSource: string | RulesSource) => {
|
||||
const rulesSourceName = typeof rulesSource === 'string' ? rulesSource : rulesSource.name;
|
||||
return rulesSourceName === GRAFANA_RULES_SOURCE_NAME || !!rulerRules[rulesSourceName]?.result;
|
||||
},
|
||||
[rulerRules]
|
||||
);
|
||||
}
|
||||
|
@ -79,6 +79,8 @@ export interface CombinedRule {
|
||||
annotations: Annotations;
|
||||
promRule?: Rule;
|
||||
rulerRule?: RulerRuleDTO;
|
||||
group: CombinedRuleGroup;
|
||||
namespace: CombinedRuleNamespace;
|
||||
}
|
||||
|
||||
export interface CombinedRuleGroup {
|
||||
|
Loading…
Reference in New Issue
Block a user