diff --git a/web/ui/mantine-ui/src/api/response-types/rules.ts b/web/ui/mantine-ui/src/api/response-types/rules.ts index eba15a6169..63d63d3d4b 100644 --- a/web/ui/mantine-ui/src/api/response-types/rules.ts +++ b/web/ui/mantine-ui/src/api/response-types/rules.ts @@ -46,6 +46,14 @@ interface RuleGroup { lastEvaluation: string; } +type AlertingRuleGroup = Omit & { + rules: AlertingRule[]; +}; + export interface RulesMap { groups: RuleGroup[]; } + +export interface AlertingRulesMap { + groups: AlertingRuleGroup[]; +} diff --git a/web/ui/mantine-ui/src/badge.module.css b/web/ui/mantine-ui/src/badge.module.css new file mode 100644 index 0000000000..80eb3565d2 --- /dev/null +++ b/web/ui/mantine-ui/src/badge.module.css @@ -0,0 +1,49 @@ +.statsBadge { + background-color: light-dark( + var(--mantine-color-gray-1), + var(--mantine-color-gray-9) + ); + color: light-dark(var(--mantine-color-gray-7), var(--mantine-color-gray-5)); +} + +.labelBadge { + background-color: light-dark( + var(--mantine-color-gray-1), + var(--mantine-color-gray-9) + ); + color: light-dark(var(--mantine-color-gray-7), var(--mantine-color-gray-5)); +} + +.healthOk { + background-color: light-dark( + var(--mantine-color-green-1), + var(--mantine-color-green-9) + ); + color: light-dark(var(--mantine-color-green-9), var(--mantine-color-green-1)); +} + +.healthErr { + background-color: light-dark( + var(--mantine-color-red-1), + darken(var(--mantine-color-red-9), 0.25) + ); + color: light-dark(var(--mantine-color-red-9), var(--mantine-color-red-1)); +} + +.healthWarn { + background-color: light-dark( + var(--mantine-color-yellow-1), + var(--mantine-color-yellow-9) + ); + color: light-dark( + var(--mantine-color-yellow-9), + var(--mantine-color-yellow-1) + ); +} + +.healthUnknown { + background-color: light-dark( + var(--mantine-color-gray-1), + var(--mantine-color-gray-9) + ); +} diff --git a/web/ui/mantine-ui/src/codebox.module.css b/web/ui/mantine-ui/src/codebox.module.css index 23387c7764..d54a7d1b4f 100644 --- a/web/ui/mantine-ui/src/codebox.module.css +++ b/web/ui/mantine-ui/src/codebox.module.css @@ -4,42 +4,3 @@ var(--mantine-color-gray-9) ); } - -.statsBadge { - background-color: light-dark( - var(--mantine-color-gray-1), - var(--mantine-color-gray-9) - ); - color: light-dark(var(--mantine-color-gray-7), var(--mantine-color-gray-5)); -} - -.labelBadge { - background-color: light-dark( - var(--mantine-color-gray-1), - var(--mantine-color-gray-9) - ); - color: light-dark(var(--mantine-color-gray-7), var(--mantine-color-gray-5)); -} - -.healthOk { - background-color: light-dark( - var(--mantine-color-green-1), - var(--mantine-color-green-9) - ); - color: light-dark(var(--mantine-color-green-9), var(--mantine-color-green-1)); -} - -.healthErr { - background-color: light-dark( - var(--mantine-color-red-1), - var(--mantine-color-red-9) - ); - color: light-dark(var(--mantine-color-red-9), var(--mantine-color-red-1)); -} - -.healthUnknown { - background-color: light-dark( - var(--mantine-color-gray-1), - var(--mantine-color-gray-9) - ); -} diff --git a/web/ui/mantine-ui/src/lib/time-format.ts b/web/ui/mantine-ui/src/lib/time-format.ts index f31e574682..cfa5536319 100644 --- a/web/ui/mantine-ui/src/lib/time-format.ts +++ b/web/ui/mantine-ui/src/lib/time-format.ts @@ -109,10 +109,14 @@ export const humanizeDuration = (milliseconds: number): string => { return "0s"; }; -export const formatRelative = (startStr: string, end: number): string => { +export const formatRelative = ( + startStr: string, + end: number, + suffix: string = " ago" +): string => { const start = parseTime(startStr); if (start < 0) { - return "Never"; + return "never"; } - return humanizeDuration(end - start) + " ago"; + return humanizeDuration(end - start) + suffix; }; diff --git a/web/ui/mantine-ui/src/pages/alerts.tsx b/web/ui/mantine-ui/src/pages/alerts.tsx index e5514d254b..587d3cfe68 100644 --- a/web/ui/mantine-ui/src/pages/alerts.tsx +++ b/web/ui/mantine-ui/src/pages/alerts.tsx @@ -1,3 +1,187 @@ +import { + Card, + Group, + Table, + Text, + Accordion, + Badge, + Tooltip, + Box, + Switch, +} from "@mantine/core"; +import { useSuspenseAPIQuery } from "../api/api"; +import { AlertingRulesMap } from "../api/response-types/rules"; +import badgeClasses from "../badge.module.css"; +import RuleDefinition from "../rule-definition"; +import { formatRelative, now } from "../lib/time-format"; +import { Fragment, useState } from "react"; + export default function Alerts() { - return <>Alerts page; + const { data } = useSuspenseAPIQuery(`/rules?type=alert`); + const [showAnnotations, setShowAnnotations] = useState(false); + + const ruleStatsCount = { + inactive: 0, + pending: 0, + firing: 0, + }; + + data.data.groups.forEach((el) => + el.rules.forEach((r) => ruleStatsCount[r.state]++) + ); + + return ( + <> + setShowAnnotations(event.currentTarget.checked)} + mb="md" + /> + {data.data.groups.map((g, i) => ( + + + + + {g.name} + + + {g.file} + + + + + {g.rules.map((r, j) => { + const numFiring = r.alerts.filter( + (a) => a.state === "firing" + ).length; + const numPending = r.alerts.filter( + (a) => a.state === "pending" + ).length; + + return ( + + + + {r.name} + + {numFiring > 0 && ( + + firing ({numFiring}) + + )} + {numPending > 0 && ( + + pending ({numPending}) + + )} + {/* {numFiring === 0 && numPending === 0 && ( + + inactive + + )} */} + + + + + + {r.alerts.length > 0 && ( + + + + Alert labels + State + Active Since + Value + + + + {r.type === "alerting" && + r.alerts.map((a, k) => ( + + + + + {Object.entries(a.labels).map( + ([k, v]) => { + return ( + + {/* TODO: Proper quote escaping */} + {k}="{v}" + + ); + } + )} + + + + + {a.state} + + + + + + {formatRelative(a.activeAt, now(), "")} + + + + {a.value} + + {showAnnotations && ( + + +
+ + {Object.entries(a.annotations).map( + ([k, v]) => ( + + {k} + {v} + + ) + )} + +
+ + + )} + + ))} + + + )} +
+
+ ); + })} +
+
+ ))} + + ); } diff --git a/web/ui/mantine-ui/src/pages/rules.tsx b/web/ui/mantine-ui/src/pages/rules.tsx index ab1e8a5075..0893dd0a2c 100644 --- a/web/ui/mantine-ui/src/pages/rules.tsx +++ b/web/ui/mantine-ui/src/pages/rules.tsx @@ -1,72 +1,45 @@ -import { - Alert, - Badge, - Card, - Group, - Table, - Text, - Tooltip, - useComputedColorScheme, -} from "@mantine/core"; +import { Alert, Badge, Card, Group, Table, Text, Tooltip } from "@mantine/core"; // import { useQuery } from "react-query"; -import { - formatDuration, - formatRelative, - humanizeDuration, - now, -} from "../lib/time-format"; +import { formatRelative, humanizeDuration, now } from "../lib/time-format"; import { IconAlertTriangle, IconBell, - IconClockPause, - IconClockPlay, IconDatabaseImport, IconHourglass, IconRefresh, IconRepeat, } from "@tabler/icons-react"; -import CodeMirror, { EditorView } from "@uiw/react-codemirror"; import { useSuspenseAPIQuery } from "../api/api"; import { RulesMap } from "../api/response-types/rules"; -import { syntaxHighlighting } from "@codemirror/language"; -import { - baseTheme, - darkPromqlHighlighter, - lightTheme, - promqlHighlighter, -} from "../codemirror/theme"; -import { PromQLExtension } from "@prometheus-io/codemirror-promql"; -import classes from "../codebox.module.css"; +import badgeClasses from "../badge.module.css"; +import RuleDefinition from "../rule-definition"; const healthBadgeClass = (state: string) => { switch (state) { case "ok": - return classes.healthOk; + return badgeClasses.healthOk; case "err": - return classes.healthErr; + return badgeClasses.healthErr; case "unknown": - return classes.healthUnknown; + return badgeClasses.healthUnknown; default: return "orange"; } }; -const promqlExtension = new PromQLExtension(); - export default function Rules() { const { data } = useSuspenseAPIQuery(`/rules`); - const theme = useComputedColorScheme(); return ( <> - {data.data.groups.map((g) => ( + {data.data.groups.map((g, i) => ( @@ -81,7 +54,7 @@ export default function Rules() { } > @@ -91,7 +64,7 @@ export default function Rules() { } > @@ -101,7 +74,7 @@ export default function Rules() { } > @@ -113,8 +86,9 @@ export default function Rules() { {g.rules.map((r) => ( + // TODO: Find a stable and definitely unique key. - + {r.type === "alerting" ? ( @@ -134,7 +108,7 @@ export default function Rules() { } > @@ -148,7 +122,7 @@ export default function Rules() { > } > @@ -160,31 +134,8 @@ export default function Rules() { - - - - - + + {r.lastError && ( Error: {r.lastError} )} - {r.type === "alerting" && ( - - {r.duration && ( - } - > - for: {formatDuration(r.duration * 1000)} - - )} - {r.keepFiringFor && ( - } - > - keep_firing_for: {formatDuration(r.duration * 1000)} - - )} - - )} - {r.labels && Object.keys(r.labels).length > 0 && ( - - {Object.entries(r.labels).map(([k, v]) => ( - - {k}: {v} - - ))} - - )} - {/* {Object.keys(r.annotations).length > 0 && ( - - {Object.entries(r.annotations).map(([k, v]) => ( - - {k}: {v} - - ))} - - )} */} ))} diff --git a/web/ui/mantine-ui/src/rule-definition.tsx b/web/ui/mantine-ui/src/rule-definition.tsx new file mode 100644 index 0000000000..8f426fd82d --- /dev/null +++ b/web/ui/mantine-ui/src/rule-definition.tsx @@ -0,0 +1,105 @@ +import { + Alert, + Badge, + Card, + Group, + useComputedColorScheme, +} from "@mantine/core"; +import { + IconAlertTriangle, + IconClockPause, + IconClockPlay, +} from "@tabler/icons-react"; +import { FC } from "react"; +import { formatDuration } from "./lib/time-format"; +import codeboxClasses from "./codebox.module.css"; +import badgeClasses from "./badge.module.css"; +import { Rule } from "./api/response-types/rules"; +import CodeMirror, { EditorView } from "@uiw/react-codemirror"; +import { syntaxHighlighting } from "@codemirror/language"; +import { + baseTheme, + darkPromqlHighlighter, + lightTheme, + promqlHighlighter, +} from "./codemirror/theme"; +import { PromQLExtension } from "@prometheus-io/codemirror-promql"; + +const promqlExtension = new PromQLExtension(); + +const RuleDefinition: FC<{ rule: Rule }> = ({ rule }) => { + const theme = useComputedColorScheme(); + + return ( + <> + + + + {rule.type === "alerting" && ( + + {rule.duration && ( + } + > + for: {formatDuration(rule.duration * 1000)} + + )} + {rule.keepFiringFor && ( + } + > + keep_firing_for: {formatDuration(rule.duration * 1000)} + + )} + + )} + {rule.labels && Object.keys(rule.labels).length > 0 && ( + + {Object.entries(rule.labels).map(([k, v]) => ( + + {k}: {v} + + ))} + + )} + {/* {Object.keys(r.annotations).length > 0 && ( + + {Object.entries(r.annotations).map(([k, v]) => ( + + {k}: {v} + + ))} + + )} */} + + ); +}; + +export default RuleDefinition;