import { Card, Group, Table, Text, Accordion, Badge, Tooltip, Box, Stack, Alert, TextInput, Anchor, } from "@mantine/core"; import { useSuspenseAPIQuery } from "../api/api"; import { AlertingRule, AlertingRulesResult } from "../api/responseTypes/rules"; import badgeClasses from "../Badge.module.css"; import panelClasses from "../Panel.module.css"; import RuleDefinition from "../components/RuleDefinition"; import { humanizeDurationRelative, now } from "../lib/formatTime"; import { Fragment, useMemo } from "react"; import { StateMultiSelect } from "../components/StateMultiSelect"; import { IconInfoCircle, IconSearch } from "@tabler/icons-react"; import { LabelBadges } from "../components/LabelBadges"; import { useSettings } from "../state/settingsSlice"; import { ArrayParam, BooleanParam, StringParam, useQueryParam, withDefault, } from "use-query-params"; import { useDebouncedValue } from "@mantine/hooks"; import { KVSearch } from "@nexucis/kvsearch"; type AlertsPageData = { // How many rules are in each state across all groups. globalCounts: { inactive: number; pending: number; firing: number; }; groups: { name: string; file: string; // How many rules are in each state for this group. counts: { total: number; inactive: number; pending: number; firing: number; }; rules: { rule: AlertingRule; // How many alerts are in each state for this rule. counts: { firing: number; pending: number; }; }[]; }[]; }; const kvSearch = new KVSearch({ shouldSort: true, indexedKeys: ["name", "labels", ["labels", /.*/]], }); const buildAlertsPageData = ( data: AlertingRulesResult, search: string, stateFilter: (string | null)[] ) => { const pageData: AlertsPageData = { globalCounts: { inactive: 0, pending: 0, firing: 0, }, groups: [], }; for (const group of data.groups) { const groupCounts = { total: 0, inactive: 0, pending: 0, firing: 0, }; for (const r of group.rules) { groupCounts.total++; switch (r.state) { case "inactive": pageData.globalCounts.inactive++; groupCounts.inactive++; break; case "firing": pageData.globalCounts.firing++; groupCounts.firing++; break; case "pending": pageData.globalCounts.pending++; groupCounts.pending++; break; default: throw new Error(`Unknown rule state: ${r.state}`); } } const filteredRules: AlertingRule[] = ( search === "" ? group.rules : kvSearch.filter(search, group.rules).map((value) => value.original) ).filter((r) => stateFilter.length === 0 || stateFilter.includes(r.state)); pageData.groups.push({ name: group.name, file: group.file, counts: groupCounts, rules: filteredRules.map((r) => ({ rule: r, counts: { firing: r.alerts.filter((a) => a.state === "firing").length, pending: r.alerts.filter((a) => a.state === "pending").length, }, })), }); } return pageData; }; export default function AlertsPage() { // Fetch the alerting rules data. const { data } = useSuspenseAPIQuery({ path: `/rules`, params: { type: "alert", }, }); const { showAnnotations } = useSettings(); // Define URL query params. const [stateFilter, setStateFilter] = useQueryParam( "state", withDefault(ArrayParam, []) ); const [searchFilter, setSearchFilter] = useQueryParam( "search", withDefault(StringParam, "") ); const [debouncedSearch] = useDebouncedValue(searchFilter.trim(), 250); const [showEmptyGroups, setShowEmptyGroups] = useQueryParam( "showEmptyGroups", withDefault(BooleanParam, true) ); // Update the page data whenever the fetched data or filters change. const alertsPageData: AlertsPageData = useMemo( () => buildAlertsPageData(data.data, debouncedSearch, stateFilter), [data, stateFilter, debouncedSearch] ); const shownGroups = showEmptyGroups ? alertsPageData.groups : alertsPageData.groups.filter((g) => g.rules.length > 0); return ( o === "inactive" ? badgeClasses.healthOk : o === "pending" ? badgeClasses.healthWarn : badgeClasses.healthErr } optionCount={(o) => alertsPageData.globalCounts[ o as keyof typeof alertsPageData.globalCounts ] } placeholder="Filter by rule state" values={(stateFilter?.filter((v) => v !== null) as string[]) || []} onChange={(values) => setStateFilter(values)} /> } placeholder="Filter by rule name or labels" value={searchFilter || ""} onChange={(event) => setSearchFilter(event.currentTarget.value || null) } > {alertsPageData.groups.length === 0 ? ( }> No rules found. ) : ( !showEmptyGroups && alertsPageData.groups.length !== shownGroups.length && ( } > Hiding {alertsPageData.groups.length - shownGroups.length} empty groups due to filters or no rules. setShowEmptyGroups(true)}> Show empty groups ) )} {shownGroups.map((g, i) => { return ( {g.name} {g.file} {g.counts.firing > 0 && ( firing ({g.counts.firing}) )} {g.counts.pending > 0 && ( pending ({g.counts.pending}) )} {g.counts.inactive > 0 && ( inactive ({g.counts.inactive}) )} {g.counts.total === 0 ? ( }> No rules in this group. setShowEmptyGroups(false)} > Hide empty groups ) : g.rules.length === 0 ? ( }> No rules in this group match your filter criteria (omitted{" "} {g.counts.total} filtered rules). setShowEmptyGroups(false)} > Hide empty groups ) : ( {g.rules.map((r, j) => { return ( 0 ? panelClasses.panelHealthErr : r.counts.pending > 0 ? panelClasses.panelHealthWarn : panelClasses.panelHealthOk } > {r.rule.name} {r.counts.firing > 0 && ( firing ({r.counts.firing}) )} {r.counts.pending > 0 && ( pending ({r.counts.pending}) )} {r.rule.alerts.length > 0 && ( Alert labels State Active Since Value {r.rule.type === "alerting" && r.rule.alerts.map((a, k) => ( {a.state} {humanizeDurationRelative( a.activeAt, now(), "" )} {a.value} {showAnnotations && (
{Object.entries( a.annotations ).map(([k, v]) => ( {k} {v} ))}
)} ))} )}
); })}
)}
); })}
); }