prometheus/web/ui/mantine-ui/src/pages/RulesPage.tsx
amanycodes 11baddb19b removed comments
Signed-off-by: amanycodes <amanycodes@gmail.com>
2025-02-19 15:28:27 +05:30

253 lines
9.5 KiB
TypeScript

import {
Accordion,
Alert,
Badge,
Card,
Group,
Pagination,
rem,
Stack,
Text,
Tooltip,
} from "@mantine/core";
// import { useQuery } from "react-query";
import {
humanizeDurationRelative,
humanizeDuration,
now,
} from "../lib/formatTime";
import {
IconAlertTriangle,
IconBell,
IconHourglass,
IconInfoCircle,
IconRefresh,
IconRepeat,
IconTimeline,
} from "@tabler/icons-react";
import { useSuspenseAPIQuery } from "../api/api";
import { RulesResult } from "../api/responseTypes/rules";
import badgeClasses from "../Badge.module.css";
import RuleDefinition from "../components/RuleDefinition";
import { badgeIconStyle } from "../styles";
import { NumberParam, useQueryParam, withDefault } from "use-query-params";
import { useSettings } from "../state/settingsSlice";
import { useEffect } from "react";
import CustomInfiniteScroll from "../components/CustomInfiniteScroll";
const healthBadgeClass = (state: string) => {
switch (state) {
case "ok":
return badgeClasses.healthOk;
case "err":
return badgeClasses.healthErr;
case "unknown":
return badgeClasses.healthUnknown;
default:
throw new Error("Unknown rule health state");
}
};
export default function RulesPage() {
const { data } = useSuspenseAPIQuery<RulesResult>({ path: `/rules` });
const { ruleGroupsPerPage } = useSettings();
const [activePage, setActivePage] = useQueryParam(
"page",
withDefault(NumberParam, 1)
);
// If we were e.g. on page 10 and the number of total pages decreases to 5 (due
// changing the max number of items per page), go to the largest possible page.
const totalPageCount = Math.ceil(data.data.groups.length / ruleGroupsPerPage);
const effectiveActivePage = Math.max(1, Math.min(activePage, totalPageCount));
useEffect(() => {
if (effectiveActivePage !== activePage) {
setActivePage(effectiveActivePage);
}
}, [effectiveActivePage, activePage, setActivePage]);
return (
<Stack mt="xs">
{data.data.groups.length === 0 && (
<Alert title="No rule groups" icon={<IconInfoCircle />}>
No rule groups configured.
</Alert>
)}
<Pagination
total={totalPageCount}
value={effectiveActivePage}
onChange={setActivePage}
hideWithOnePage
/>
{data.data.groups
.slice(
(effectiveActivePage - 1) * ruleGroupsPerPage,
effectiveActivePage * ruleGroupsPerPage
)
.map((g) => (
<Card
shadow="xs"
withBorder
p="md"
mb="md"
key={`${g.file}-${g.name}`}
>
<Group mb="md" mt="xs" ml="xs" justify="space-between">
<Group align="baseline">
<Text fz="xl" fw={600} c="var(--mantine-primary-color-filled)">
{g.name}
</Text>
<Text fz="sm" c="gray.6">
{g.file}
</Text>
</Group>
<Group>
<Tooltip label="Last group evaluation" withArrow>
<Badge
variant="light"
className={badgeClasses.statsBadge}
styles={{ label: { textTransform: "none" } }}
leftSection={<IconRefresh style={badgeIconStyle} />}
>
last run {humanizeDurationRelative(g.lastEvaluation, now())}
</Badge>
</Tooltip>
<Tooltip label="Duration of last group evaluation" withArrow>
<Badge
variant="light"
className={badgeClasses.statsBadge}
styles={{ label: { textTransform: "none" } }}
leftSection={<IconHourglass style={badgeIconStyle} />}
>
took {humanizeDuration(parseFloat(g.evaluationTime) * 1000)}
</Badge>
</Tooltip>
<Tooltip label="Group evaluation interval" withArrow>
<Badge
variant="transparent"
className={badgeClasses.statsBadge}
styles={{ label: { textTransform: "none" } }}
leftSection={<IconRepeat style={badgeIconStyle} />}
>
every {humanizeDuration(parseFloat(g.interval) * 1000)}{" "}
</Badge>
</Tooltip>
</Group>
</Group>
{g.rules.length === 0 && (
<Alert title="No rules" icon={<IconInfoCircle />}>
No rules in rule group.
</Alert>
)}
<CustomInfiniteScroll
allItems={g.rules}
child={({ items }) => (
<Accordion multiple variant="separated">
{items.map((r, j) => (
<Accordion.Item
styles={{
item: {
// TODO: This transparency hack is an OK workaround to make the collapsed items
// have a different background color than their surrounding group card in dark mode,
// but it would be better to use CSS to override the light/dark colors for
// collapsed/expanded accordion items.
backgroundColor: "#c0c0c015",
},
}}
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 justify="space-between" mr="lg">
<Group gap="xs" wrap="nowrap">
{r.type === "alerting" ? (
<Tooltip label="Alerting rule" withArrow>
<IconBell
style={{ width: rem(15), height: rem(15) }}
/>
</Tooltip>
) : (
<Tooltip label="Recording rule" withArrow>
<IconTimeline
style={{ width: rem(15), height: rem(15) }}
/>
</Tooltip>
)}
<Text>{r.name}</Text>
</Group>
<Group gap="xs">
<Group gap="xs" wrap="wrap">
<Tooltip label="Last rule evaluation" withArrow>
<Badge
variant="light"
className={badgeClasses.statsBadge}
styles={{ label: { textTransform: "none" } }}
leftSection={
<IconRefresh style={badgeIconStyle} />
}
>
{humanizeDurationRelative(
r.lastEvaluation,
now()
)}
</Badge>
</Tooltip>
<Tooltip
label="Duration of last rule evaluation"
withArrow
>
<Badge
variant="light"
className={badgeClasses.statsBadge}
styles={{ label: { textTransform: "none" } }}
leftSection={
<IconHourglass style={badgeIconStyle} />
}
>
{humanizeDuration(
parseFloat(r.evaluationTime) * 1000
)}
</Badge>
</Tooltip>
</Group>
<Badge className={healthBadgeClass(r.health)}>
{r.health}
</Badge>
</Group>
</Group>
</Accordion.Control>
<Accordion.Panel>
<RuleDefinition rule={r} />
{r.lastError && (
<Alert
color="red"
mt="sm"
title="Rule failed to evaluate"
icon={<IconAlertTriangle />}
>
<strong>Error:</strong> {r.lastError}
</Alert>
)}
</Accordion.Panel>
</Accordion.Item>
))}
</Accordion>
)}
/>
</Card>
))}
</Stack>
);
}