Show rules page as collapsed accordion instead of rules table

Signed-off-by: Julius Volz <julius.volz@gmail.com>
This commit is contained in:
Julius Volz 2024-04-03 14:48:59 +02:00
parent 8165a6e337
commit 719b31f1b5
3 changed files with 66 additions and 50 deletions

View file

@ -1,7 +1,7 @@
import { Badge, Card, Group, useComputedColorScheme } from "@mantine/core"; import { Badge, Box, Card, Group, useComputedColorScheme } from "@mantine/core";
import { IconClockPause, IconClockPlay } from "@tabler/icons-react"; import { IconClockPause, IconClockPlay } from "@tabler/icons-react";
import { FC } from "react"; import { FC } from "react";
import { formatDuration } from "./lib/formatTime"; import { formatPrometheusDuration } from "./lib/formatTime";
import codeboxClasses from "./codebox.module.css"; import codeboxClasses from "./codebox.module.css";
import badgeClasses from "./Badge.module.css"; import badgeClasses from "./Badge.module.css";
import { Rule } from "./api/responseTypes/rules"; import { Rule } from "./api/responseTypes/rules";
@ -14,6 +14,7 @@ import {
promqlHighlighter, promqlHighlighter,
} from "./codemirror/theme"; } from "./codemirror/theme";
import { PromQLExtension } from "@prometheus-io/codemirror-promql"; import { PromQLExtension } from "@prometheus-io/codemirror-promql";
import { LabelBadges } from "./LabelBadges";
const promqlExtension = new PromQLExtension(); const promqlExtension = new PromQLExtension();
@ -46,7 +47,7 @@ const RuleDefinition: FC<{ rule: Rule }> = ({ rule }) => {
styles={{ label: { textTransform: "none" } }} styles={{ label: { textTransform: "none" } }}
leftSection={<IconClockPause size={12} />} leftSection={<IconClockPause size={12} />}
> >
for: {formatDuration(rule.duration * 1000)} for: {formatPrometheusDuration(rule.duration * 1000)}
</Badge> </Badge>
)} )}
{rule.keepFiringFor && ( {rule.keepFiringFor && (
@ -55,24 +56,15 @@ const RuleDefinition: FC<{ rule: Rule }> = ({ rule }) => {
styles={{ label: { textTransform: "none" } }} styles={{ label: { textTransform: "none" } }}
leftSection={<IconClockPlay size={12} />} leftSection={<IconClockPlay size={12} />}
> >
keep_firing_for: {formatDuration(rule.duration * 1000)} keep_firing_for: {formatPrometheusDuration(rule.duration * 1000)}
</Badge> </Badge>
)} )}
</Group> </Group>
)} )}
{rule.labels && Object.keys(rule.labels).length > 0 && ( {rule.labels && Object.keys(rule.labels).length > 0 && (
<Group mt="md" gap="xs"> <Box mt="md">
{Object.entries(rule.labels).map(([k, v]) => ( <LabelBadges labels={rule.labels} />
<Badge </Box>
variant="light"
className={badgeClasses.labelBadge}
styles={{ label: { textTransform: "none" } }}
key={k}
>
{k}: {v}
</Badge>
))}
</Group>
)} )}
{/* {Object.keys(r.annotations).length > 0 && ( {/* {Object.keys(r.annotations).length > 0 && (
<Group mt="md" gap="xs"> <Group mt="md" gap="xs">

View file

@ -13,7 +13,7 @@ type CommonRuleFields = {
name: string; name: string;
query: string; query: string;
evaluationTime: string; evaluationTime: string;
health: string; health: "ok" | "unknown" | "err";
lastError?: string; lastError?: string;
lastEvaluation: string; lastEvaluation: string;
}; };

View file

@ -1,4 +1,5 @@
import { import {
Accordion,
Alert, Alert,
Badge, Badge,
Card, Card,
@ -9,12 +10,17 @@ import {
Tooltip, Tooltip,
} from "@mantine/core"; } from "@mantine/core";
// import { useQuery } from "react-query"; // import { useQuery } from "react-query";
import { formatRelative, humanizeDuration, now } from "../lib/formatTime"; import {
humanizeDurationRelative,
humanizeDuration,
now,
} from "../lib/formatTime";
import { import {
IconAlertTriangle, IconAlertTriangle,
IconBell, IconBell,
IconDatabaseImport, IconDatabaseImport,
IconHourglass, IconHourglass,
IconInfoCircle,
IconRefresh, IconRefresh,
IconRepeat, IconRepeat,
} from "@tabler/icons-react"; } from "@tabler/icons-react";
@ -32,7 +38,7 @@ const healthBadgeClass = (state: string) => {
case "unknown": case "unknown":
return badgeClasses.healthUnknown; return badgeClasses.healthUnknown;
default: default:
return "orange"; throw new Error("Unknown rule health state");
} }
}; };
@ -41,6 +47,11 @@ export default function RulesPage() {
return ( return (
<Stack mt="xs"> <Stack mt="xs">
{data.data.groups.length === 0 && (
<Alert title="No rule groups" icon={<IconInfoCircle size={14} />}>
No rule groups configured.
</Alert>
)}
{data.data.groups.map((g, i) => ( {data.data.groups.map((g, i) => (
<Card <Card
shadow="xs" shadow="xs"
@ -66,7 +77,7 @@ export default function RulesPage() {
styles={{ label: { textTransform: "none" } }} styles={{ label: { textTransform: "none" } }}
leftSection={<IconRefresh size={12} />} leftSection={<IconRefresh size={12} />}
> >
last run {formatRelative(g.lastEvaluation, now())} last run {humanizeDurationRelative(g.lastEvaluation, now())}
</Badge> </Badge>
</Tooltip> </Tooltip>
<Tooltip label="Duration of last group evaluation" withArrow> <Tooltip label="Duration of last group evaluation" withArrow>
@ -91,21 +102,34 @@ export default function RulesPage() {
</Tooltip> </Tooltip>
</Group> </Group>
</Group> </Group>
<Table> {g.rules.length === 0 && (
<Table.Tbody> <Alert title="No rules" icon={<IconInfoCircle size={14} />}>
{g.rules.map((r) => ( No rules in rule group.
// TODO: Find a stable and definitely unique key. </Alert>
<Table.Tr key={r.name}> )}
<Table.Td p="md" py="xl" valign="top"> <Accordion multiple variant="separated">
{g.rules.map((r, j) => (
<Accordion.Item
key={j}
value={j.toString()}
style={{
borderLeft:
r.health === "err"
? "5px solid var(--mantine-color-red-4)"
: r.health === "unknown"
? "5px solid var(--mantine-color-gray-5)"
: "5px solid var(--mantine-color-green-4)",
}}
>
<Accordion.Control>
<Group wrap="nowrap" justify="space-between" mr="lg">
<Group gap="xs" wrap="nowrap"> <Group gap="xs" wrap="nowrap">
{r.type === "alerting" ? ( {r.type === "alerting" ? (
<IconBell size={14} /> <IconBell size={15} />
) : ( ) : (
<IconDatabaseImport size={14} /> <IconDatabaseImport size={15} />
)} )}
<Text fz="sm" fw={600}> <Text>{r.name}</Text>
{r.name}
</Text>
</Group> </Group>
<Group mt="md" gap="xs"> <Group mt="md" gap="xs">
<Badge className={healthBadgeClass(r.health)}> <Badge className={healthBadgeClass(r.health)}>
@ -120,7 +144,7 @@ export default function RulesPage() {
styles={{ label: { textTransform: "none" } }} styles={{ label: { textTransform: "none" } }}
leftSection={<IconRefresh size={12} />} leftSection={<IconRefresh size={12} />}
> >
{formatRelative(r.lastEvaluation, now())} {humanizeDurationRelative(r.lastEvaluation, now())}
</Badge> </Badge>
</Tooltip> </Tooltip>
@ -141,24 +165,24 @@ export default function RulesPage() {
</Tooltip> </Tooltip>
</Group> </Group>
</Group> </Group>
</Table.Td> </Group>
<Table.Td p="md" py="xl"> </Accordion.Control>
<RuleDefinition rule={r} /> <Accordion.Panel>
{r.lastError && ( <RuleDefinition rule={r} />
<Alert {r.lastError && (
color="red" <Alert
mt="sm" color="red"
title="Rule failed to evaluate" mt="sm"
icon={<IconAlertTriangle size={14} />} title="Rule failed to evaluate"
> icon={<IconAlertTriangle size={14} />}
<strong>Error:</strong> {r.lastError} >
</Alert> <strong>Error:</strong> {r.lastError}
)} </Alert>
</Table.Td> )}
</Table.Tr> </Accordion.Panel>
))} </Accordion.Item>
</Table.Tbody> ))}
</Table> </Accordion>
</Card> </Card>
))} ))}
</Stack> </Stack>