From 19c6a02f49a67412f027779fa0db67ba9f979350 Mon Sep 17 00:00:00 2001 From: Domas Date: Fri, 16 Apr 2021 12:08:08 +0300 Subject: [PATCH] Alerting: alert list state view (#33020) --- packages/grafana-ui/src/types/icon.ts | 1 + .../features/alerting/unified/RuleList.tsx | 57 +++-- .../components/rules/RuleListGroupView.tsx | 31 +++ .../components/rules/RuleListSateSection.tsx | 43 ++++ .../components/rules/RuleListStateView.tsx | 46 ++++ .../unified/components/rules/RulesGroup.tsx | 16 +- .../unified/components/rules/RulesTable.tsx | 90 ++++--- .../rules/SystemOrApplicationRules.tsx | 14 +- .../components/rules/ThresholdRules.tsx | 7 +- .../hooks/useCombinedRuleNamespaces.ts | 233 ++++++++++-------- .../alerting/unified/hooks/useHasRuler.ts | 12 +- public/app/types/unified-alerting.ts | 2 + 12 files changed, 365 insertions(+), 187 deletions(-) create mode 100644 public/app/features/alerting/unified/components/rules/RuleListGroupView.tsx create mode 100644 public/app/features/alerting/unified/components/rules/RuleListSateSection.tsx create mode 100644 public/app/features/alerting/unified/components/rules/RuleListStateView.tsx diff --git a/packages/grafana-ui/src/types/icon.ts b/packages/grafana-ui/src/types/icon.ts index ab4beeea00f..5c5c204772a 100644 --- a/packages/grafana-ui/src/types/icon.ts +++ b/packages/grafana-ui/src/types/icon.ts @@ -135,6 +135,7 @@ export type IconName = | 'user' | 'users-alt' | 'wrap-text' + | 'heart-rate' | 'x'; export const getAvailableIcons = (): IconName[] => [ diff --git a/public/app/features/alerting/unified/RuleList.tsx b/public/app/features/alerting/unified/RuleList.tsx index e7c9798f7e4..a94681f3a15 100644 --- a/public/app/features/alerting/unified/RuleList.tsx +++ b/public/app/features/alerting/unified/RuleList.tsx @@ -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 ( {(promReqeustErrors.length || rulerRequestErrors.length || grafanaPromError) && ( @@ -126,6 +122,18 @@ export const RuleList: FC = () => {
+ + + + Groups + + + + + State + + +
@@ -134,8 +142,7 @@ export const RuleList: FC = () => { )} {showNewAlertSplash && } - {haveResults && } - {haveResults && } + {haveResults && } ); }; diff --git a/public/app/features/alerting/unified/components/rules/RuleListGroupView.tsx b/public/app/features/alerting/unified/components/rules/RuleListGroupView.tsx new file mode 100644 index 00000000000..fcad09f78ae --- /dev/null +++ b/public/app/features/alerting/unified/components/rules/RuleListGroupView.tsx @@ -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 = ({ 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 ( + <> + + + + ); +}; diff --git a/public/app/features/alerting/unified/components/rules/RuleListSateSection.tsx b/public/app/features/alerting/unified/components/rules/RuleListSateSection.tsx new file mode 100644 index 00000000000..ea5799d6e87 --- /dev/null +++ b/public/app/features/alerting/unified/components/rules/RuleListSateSection.tsx @@ -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 = ({ rules, state, defaultCollapsed = false }) => { + const [collapsed, setCollapsed] = useState(defaultCollapsed); + const styles = useStyles(getStyles); + return ( + <> +

+ setCollapsed(!collapsed)} + /> + {capitalize(state)} ({rules.length}) +

+ {!collapsed && } + + ); +}; + +const getStyles = (theme: GrafanaTheme) => ({ + collapseToggle: css` + vertical-align: middle; + `, + header: css` + margin-top: ${theme.spacing.md}; + `, +}); diff --git a/public/app/features/alerting/unified/components/rules/RuleListStateView.tsx b/public/app/features/alerting/unified/components/rules/RuleListStateView.tsx new file mode 100644 index 00000000000..597c86cf377 --- /dev/null +++ b/public/app/features/alerting/unified/components/rules/RuleListStateView.tsx @@ -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; + +export const RuleListStateView: FC = ({ 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 ( + <> + + + + + ); +}; diff --git a/public/app/features/alerting/unified/components/rules/RulesGroup.tsx b/public/app/features/alerting/unified/components/rules/RulesGroup.tsx index ddd39f3b499..72242546214 100644 --- a/public/app/features/alerting/unified/components/rules/RulesGroup.tsx +++ b/public/app/features/alerting/unified/components/rules/RulesGroup.tsx @@ -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 = React.memo(({ group, namespace, rulesSource }) => { +export const RulesGroup: FC = 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 => @@ -60,14 +60,14 @@ export const RulesGroup: FC = React.memo(({ group, namespace, rulesSource } const actionIcons: React.ReactNode[] = []; - if (hasRuler) { + if (hasRuler(rulesSource)) { actionIcons.push(); } if (rulesSource === GRAFANA_RULES_SOURCE_NAME) { actionIcons.push(); } - const groupName = isCloudRulesSource(rulesSource) ? `${namespace} > ${group.name}` : namespace; + const groupName = isCloudRulesSource(rulesSource) ? `${namespace.name} > ${group.name}` : namespace.name; return (
@@ -105,7 +105,7 @@ export const RulesGroup: FC = React.memo(({ group, namespace, rulesSource )}
- {!isCollapsed && } + {!isCollapsed && }
); }); diff --git a/public/app/features/alerting/unified/components/rules/RulesTable.tsx b/public/app/features/alerting/unified/components/rules/RulesTable.tsx index d7f91e30935..925390ebfc0 100644 --- a/public/app/features/alerting/unified/components/rules/RulesTable.tsx +++ b/public/app/features/alerting/unified/components/rules/RulesTable.tsx @@ -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 = ({ group, rulesSource, namespace }) => { - const { rules } = group; +export const RulesTable: FC = ({ + 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([]); - const [ruleToDelete, setRuleToDelete] = useState(); + const [ruleToDelete, setRuleToDelete] = useState(); const toggleExpandedState = (ruleKey: string) => setExpandedKeys( @@ -42,20 +45,29 @@ export const RulesTable: FC = ({ 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
Folder is empty.
; + return
{emptyMessage}
; } return ( -
+
@@ -64,16 +76,17 @@ export const RulesTable: FC = ({ group, rulesSource, namespace }) => { + {showGroupColumn && } + {showGroupColumn && } - @@ -81,6 +94,8 @@ export const RulesTable: FC = ({ 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 = ({ 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 ( + {showGroupColumn && ( + + )} - {isExpanded && ( - @@ -179,9 +191,11 @@ export const RulesTable: FC = ({ 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}; diff --git a/public/app/features/alerting/unified/components/rules/SystemOrApplicationRules.tsx b/public/app/features/alerting/unified/components/rules/SystemOrApplicationRules.tsx index 0a04c1d1863..87daade1ade 100644 --- a/public/app/features/alerting/unified/components/rules/SystemOrApplicationRules.tsx +++ b/public/app/features/alerting/unified/components/rules/SystemOrApplicationRules.tsx @@ -36,17 +36,17 @@ export const SystemOrApplicationRules: FC = ({ namespaces }) => { )} - {namespaces?.map(({ rulesSource, name, groups }) => - groups.map((group) => ( + {namespaces.map((namespace) => { + const { groups, rulesSource } = namespace; + return groups.map((group) => ( - )) - )} - {namespaces?.length === 0 && !dataSourcesLoading.length && !!rulesDataSources.length &&

No rules found.

} + )); + })} + {namespaces?.length === 0 && !!rulesDataSources.length &&

No rules found.

} {!rulesDataSources.length &&

There are no Prometheus or Loki datas sources configured.

} ); diff --git a/public/app/features/alerting/unified/components/rules/ThresholdRules.tsx b/public/app/features/alerting/unified/components/rules/ThresholdRules.tsx index f4f4af8efb9..26224f05adf 100644 --- a/public/app/features/alerting/unified/components/rules/ThresholdRules.tsx +++ b/public/app/features/alerting/unified/components/rules/ThresholdRules.tsx @@ -27,12 +27,7 @@ export const ThresholdRules: FC = ({ namespaces }) => { {namespaces?.map((namespace) => namespace.groups.map((group) => ( - + )) )} {namespaces?.length === 0 &&

No rules found.

} diff --git a/public/app/features/alerting/unified/hooks/useCombinedRuleNamespaces.ts b/public/app/features/alerting/unified/hooks/useCombinedRuleNamespaces.ts index 9cb82fb8615..30c8e80289d 100644 --- a/public/app/features/alerting/unified/hooks/useCombinedRuleNamespaces.ts +++ b/public/app/features/alerting/unified/hooks/useCombinedRuleNamespaces.ts @@ -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>({}); - 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; - - const cached = cache.current[rulesSourceName]; - if (cached && cached.promRules === promRules && cached.rulerRules === rulerRules) { - return cached.result; - } - const namespaces: Record = {}; - - // 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: [], + 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 = {}; + + // 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: [], }); + + addPromGroupsToCombinedNamespace(ns, 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)), - }, - ]; - }); - } - 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( diff --git a/public/app/features/alerting/unified/hooks/useHasRuler.ts b/public/app/features/alerting/unified/hooks/useHasRuler.ts index 8923e7ef625..eb326280c64 100644 --- a/public/app/features/alerting/unified/hooks/useHasRuler.ts +++ b/public/app/features/alerting/unified/hooks/useHasRuler.ts @@ -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] + ); } diff --git a/public/app/types/unified-alerting.ts b/public/app/types/unified-alerting.ts index 6367d21de76..0033bcad14e 100644 --- a/public/app/types/unified-alerting.ts +++ b/public/app/types/unified-alerting.ts @@ -79,6 +79,8 @@ export interface CombinedRule { annotations: Annotations; promRule?: Rule; rulerRule?: RulerRuleDTO; + group: CombinedRuleGroup; + namespace: CombinedRuleNamespace; } export interface CombinedRuleGroup {
-
+ {showGuidelines &&
}
State NameGroupStatusEvaluation Actions
-
- {!(ruleIdx === rules.length - 1) && ( -
+ {showGuidelines && ( + <> +
+ {!(ruleIdx === rules.length - 1) && ( +
+ )} + )} = ({ group, rulesSource, namespace }) => {
{promRule && isAlertingRule(promRule) ? : 'n/a'} {rule.name}{isCloudRulesSource(rulesSource) ? `${namespace.name} > ${group.name}` : namespace.name}{statuses.join(', ') || 'n/a'} - {promRule?.lastEvaluation && promRule.evaluationTime ? ( - <> - , for{' '} - {rangeUtil.secondsToHms(promRule.evaluationTime)} - - ) : ( - 'n/a' - )} - {isCloudRulesSource(rulesSource) && ( = ({ 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 && ( - setRuleToDelete(rulerRule)} /> + setRuleToDelete(rule)} /> )}
- {!(ruleIdx === rules.length - 1) && ( + {!(ruleIdx === rules.length - 1) && showGuidelines && (
)}
+