mirror of
https://github.com/prometheus/prometheus.git
synced 2024-12-25 05:34:05 -08:00
Start working on /alerts page, factor out rule definition display
Signed-off-by: Julius Volz <julius.volz@gmail.com>
This commit is contained in:
parent
65cc7b058e
commit
128b6461e9
|
@ -46,6 +46,14 @@ interface RuleGroup {
|
|||
lastEvaluation: string;
|
||||
}
|
||||
|
||||
type AlertingRuleGroup = Omit<RuleGroup, "rules"> & {
|
||||
rules: AlertingRule[];
|
||||
};
|
||||
|
||||
export interface RulesMap {
|
||||
groups: RuleGroup[];
|
||||
}
|
||||
|
||||
export interface AlertingRulesMap {
|
||||
groups: AlertingRuleGroup[];
|
||||
}
|
||||
|
|
49
web/ui/mantine-ui/src/badge.module.css
Normal file
49
web/ui/mantine-ui/src/badge.module.css
Normal file
|
@ -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)
|
||||
);
|
||||
}
|
|
@ -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)
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
|
|
|
@ -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<AlertingRulesMap>(`/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 (
|
||||
<>
|
||||
<Switch
|
||||
checked={showAnnotations}
|
||||
label="Show annotations"
|
||||
onChange={(event) => setShowAnnotations(event.currentTarget.checked)}
|
||||
mb="md"
|
||||
/>
|
||||
{data.data.groups.map((g, i) => (
|
||||
<Card
|
||||
shadow="xs"
|
||||
withBorder
|
||||
radius="md"
|
||||
p="md"
|
||||
mb="md"
|
||||
key={i} // TODO: Find a stable and definitely unique key.
|
||||
>
|
||||
<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>
|
||||
<Accordion multiple variant="separated">
|
||||
{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 (
|
||||
<Accordion.Item key={j} value={j.toString()}>
|
||||
<Accordion.Control>
|
||||
<Group wrap="nowrap" justify="space-between" mr="lg">
|
||||
<Text>{r.name}</Text>
|
||||
<Group gap="xs">
|
||||
{numFiring > 0 && (
|
||||
<Badge className={badgeClasses.healthErr}>
|
||||
firing ({numFiring})
|
||||
</Badge>
|
||||
)}
|
||||
{numPending > 0 && (
|
||||
<Badge className={badgeClasses.healthWarn}>
|
||||
pending ({numPending})
|
||||
</Badge>
|
||||
)}
|
||||
{/* {numFiring === 0 && numPending === 0 && (
|
||||
<Badge className={badgeClasses.healthOk}>
|
||||
inactive
|
||||
</Badge>
|
||||
)} */}
|
||||
</Group>
|
||||
</Group>
|
||||
</Accordion.Control>
|
||||
<Accordion.Panel>
|
||||
<RuleDefinition rule={r} />
|
||||
{r.alerts.length > 0 && (
|
||||
<Table mt="lg">
|
||||
<Table.Thead>
|
||||
<Table.Tr>
|
||||
<Table.Th>Alert labels</Table.Th>
|
||||
<Table.Th>State</Table.Th>
|
||||
<Table.Th>Active Since</Table.Th>
|
||||
<Table.Th>Value</Table.Th>
|
||||
</Table.Tr>
|
||||
</Table.Thead>
|
||||
<Table.Tbody>
|
||||
{r.type === "alerting" &&
|
||||
r.alerts.map((a, k) => (
|
||||
<Fragment key={k}>
|
||||
<Table.Tr>
|
||||
<Table.Td>
|
||||
<Group gap="xs">
|
||||
{Object.entries(a.labels).map(
|
||||
([k, v]) => {
|
||||
return (
|
||||
<Badge
|
||||
variant="light"
|
||||
className={
|
||||
badgeClasses.labelBadge
|
||||
}
|
||||
styles={{
|
||||
label: {
|
||||
textTransform: "none",
|
||||
},
|
||||
}}
|
||||
key={k}
|
||||
>
|
||||
{/* TODO: Proper quote escaping */}
|
||||
{k}="{v}"
|
||||
</Badge>
|
||||
);
|
||||
}
|
||||
)}
|
||||
</Group>
|
||||
</Table.Td>
|
||||
<Table.Td>
|
||||
<Badge
|
||||
className={
|
||||
a.state === "firing"
|
||||
? badgeClasses.healthErr
|
||||
: badgeClasses.healthWarn
|
||||
}
|
||||
>
|
||||
{a.state}
|
||||
</Badge>
|
||||
</Table.Td>
|
||||
<Table.Td>
|
||||
<Tooltip label={a.activeAt}>
|
||||
<Box>
|
||||
{formatRelative(a.activeAt, now(), "")}
|
||||
</Box>
|
||||
</Tooltip>
|
||||
</Table.Td>
|
||||
<Table.Td>{a.value}</Table.Td>
|
||||
</Table.Tr>
|
||||
{showAnnotations && (
|
||||
<Table.Tr>
|
||||
<Table.Td colSpan={4}>
|
||||
<Table mt="md" mb="xl">
|
||||
<Table.Tbody>
|
||||
{Object.entries(a.annotations).map(
|
||||
([k, v]) => (
|
||||
<Table.Tr key={k}>
|
||||
<Table.Th>{k}</Table.Th>
|
||||
<Table.Td>{v}</Table.Td>
|
||||
</Table.Tr>
|
||||
)
|
||||
)}
|
||||
</Table.Tbody>
|
||||
</Table>
|
||||
</Table.Td>
|
||||
</Table.Tr>
|
||||
)}
|
||||
</Fragment>
|
||||
))}
|
||||
</Table.Tbody>
|
||||
</Table>
|
||||
)}
|
||||
</Accordion.Panel>
|
||||
</Accordion.Item>
|
||||
);
|
||||
})}
|
||||
</Accordion>
|
||||
</Card>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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<RulesMap>(`/rules`);
|
||||
const theme = useComputedColorScheme();
|
||||
|
||||
return (
|
||||
<>
|
||||
{data.data.groups.map((g) => (
|
||||
{data.data.groups.map((g, i) => (
|
||||
<Card
|
||||
shadow="xs"
|
||||
withBorder
|
||||
radius="md"
|
||||
p="md"
|
||||
mb="md"
|
||||
key={g.name + ";" + g.file}
|
||||
key={i} // TODO: Find a stable and definitely unique key.
|
||||
>
|
||||
<Group mb="md" mt="xs" ml="xs" justify="space-between">
|
||||
<Group align="baseline">
|
||||
|
@ -81,7 +54,7 @@ export default function Rules() {
|
|||
<Tooltip label="Last group evaluation" withArrow>
|
||||
<Badge
|
||||
variant="light"
|
||||
className={classes.statsBadge}
|
||||
className={badgeClasses.statsBadge}
|
||||
styles={{ label: { textTransform: "none" } }}
|
||||
leftSection={<IconRefresh size={12} />}
|
||||
>
|
||||
|
@ -91,7 +64,7 @@ export default function Rules() {
|
|||
<Tooltip label="Duration of last group evaluation" withArrow>
|
||||
<Badge
|
||||
variant="light"
|
||||
className={classes.statsBadge}
|
||||
className={badgeClasses.statsBadge}
|
||||
styles={{ label: { textTransform: "none" } }}
|
||||
leftSection={<IconHourglass size={12} />}
|
||||
>
|
||||
|
@ -101,7 +74,7 @@ export default function Rules() {
|
|||
<Tooltip label="Group evaluation interval" withArrow>
|
||||
<Badge
|
||||
variant="light"
|
||||
className={classes.statsBadge}
|
||||
className={badgeClasses.statsBadge}
|
||||
styles={{ label: { textTransform: "none" } }}
|
||||
leftSection={<IconRepeat size={12} />}
|
||||
>
|
||||
|
@ -113,8 +86,9 @@ export default function Rules() {
|
|||
<Table>
|
||||
<Table.Tbody>
|
||||
{g.rules.map((r) => (
|
||||
// TODO: Find a stable and definitely unique key.
|
||||
<Table.Tr key={r.name}>
|
||||
<Table.Td p="md" valign="top">
|
||||
<Table.Td p="md" py="xl" valign="top">
|
||||
<Group gap="xs" wrap="nowrap">
|
||||
{r.type === "alerting" ? (
|
||||
<IconBell size={14} />
|
||||
|
@ -134,7 +108,7 @@ export default function Rules() {
|
|||
<Tooltip label="Last rule evaluation" withArrow>
|
||||
<Badge
|
||||
variant="light"
|
||||
className={classes.statsBadge}
|
||||
className={badgeClasses.statsBadge}
|
||||
styles={{ label: { textTransform: "none" } }}
|
||||
leftSection={<IconRefresh size={12} />}
|
||||
>
|
||||
|
@ -148,7 +122,7 @@ export default function Rules() {
|
|||
>
|
||||
<Badge
|
||||
variant="light"
|
||||
className={classes.statsBadge}
|
||||
className={badgeClasses.statsBadge}
|
||||
styles={{ label: { textTransform: "none" } }}
|
||||
leftSection={<IconHourglass size={12} />}
|
||||
>
|
||||
|
@ -160,31 +134,8 @@ export default function Rules() {
|
|||
</Group>
|
||||
</Group>
|
||||
</Table.Td>
|
||||
<Table.Td p="md">
|
||||
<Card
|
||||
p="xs"
|
||||
className={classes.codebox}
|
||||
radius="sm"
|
||||
shadow="none"
|
||||
>
|
||||
<CodeMirror
|
||||
basicSetup={false}
|
||||
value={r.query}
|
||||
editable={false}
|
||||
extensions={[
|
||||
baseTheme,
|
||||
lightTheme,
|
||||
syntaxHighlighting(
|
||||
theme === "light"
|
||||
? promqlHighlighter
|
||||
: darkPromqlHighlighter
|
||||
),
|
||||
promqlExtension.asExtension(),
|
||||
EditorView.lineWrapping,
|
||||
]}
|
||||
/>
|
||||
</Card>
|
||||
|
||||
<Table.Td p="md" py="xl">
|
||||
<RuleDefinition rule={r} />
|
||||
{r.lastError && (
|
||||
<Alert
|
||||
color="red"
|
||||
|
@ -195,56 +146,6 @@ export default function Rules() {
|
|||
<strong>Error:</strong> {r.lastError}
|
||||
</Alert>
|
||||
)}
|
||||
{r.type === "alerting" && (
|
||||
<Group mt="md" gap="xs">
|
||||
{r.duration && (
|
||||
<Badge
|
||||
variant="light"
|
||||
styles={{ label: { textTransform: "none" } }}
|
||||
leftSection={<IconClockPause size={12} />}
|
||||
>
|
||||
for: {formatDuration(r.duration * 1000)}
|
||||
</Badge>
|
||||
)}
|
||||
{r.keepFiringFor && (
|
||||
<Badge
|
||||
variant="light"
|
||||
styles={{ label: { textTransform: "none" } }}
|
||||
leftSection={<IconClockPlay size={12} />}
|
||||
>
|
||||
keep_firing_for: {formatDuration(r.duration * 1000)}
|
||||
</Badge>
|
||||
)}
|
||||
</Group>
|
||||
)}
|
||||
{r.labels && Object.keys(r.labels).length > 0 && (
|
||||
<Group mt="md" gap="xs">
|
||||
{Object.entries(r.labels).map(([k, v]) => (
|
||||
<Badge
|
||||
variant="light"
|
||||
className={classes.labelBadge}
|
||||
styles={{ label: { textTransform: "none" } }}
|
||||
key={k}
|
||||
>
|
||||
{k}: {v}
|
||||
</Badge>
|
||||
))}
|
||||
</Group>
|
||||
)}
|
||||
{/* {Object.keys(r.annotations).length > 0 && (
|
||||
<Group mt="md" gap="xs">
|
||||
{Object.entries(r.annotations).map(([k, v]) => (
|
||||
<Badge
|
||||
variant="light"
|
||||
color="orange.9"
|
||||
styles={{ label: { textTransform: "none" } }}
|
||||
key={k}
|
||||
>
|
||||
{k}: {v}
|
||||
</Badge>
|
||||
))}
|
||||
</Group>
|
||||
)} */}
|
||||
</Table.Td>
|
||||
</Table.Tr>
|
||||
))}
|
||||
|
|
105
web/ui/mantine-ui/src/rule-definition.tsx
Normal file
105
web/ui/mantine-ui/src/rule-definition.tsx
Normal file
|
@ -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 (
|
||||
<>
|
||||
<Card p="xs" className={codeboxClasses.codebox} radius="sm" shadow="none">
|
||||
<CodeMirror
|
||||
basicSetup={false}
|
||||
value={rule.query}
|
||||
editable={false}
|
||||
extensions={[
|
||||
baseTheme,
|
||||
lightTheme,
|
||||
syntaxHighlighting(
|
||||
theme === "light" ? promqlHighlighter : darkPromqlHighlighter
|
||||
),
|
||||
promqlExtension.asExtension(),
|
||||
EditorView.lineWrapping,
|
||||
]}
|
||||
/>
|
||||
</Card>
|
||||
{rule.type === "alerting" && (
|
||||
<Group mt="md" gap="xs">
|
||||
{rule.duration && (
|
||||
<Badge
|
||||
variant="light"
|
||||
styles={{ label: { textTransform: "none" } }}
|
||||
leftSection={<IconClockPause size={12} />}
|
||||
>
|
||||
for: {formatDuration(rule.duration * 1000)}
|
||||
</Badge>
|
||||
)}
|
||||
{rule.keepFiringFor && (
|
||||
<Badge
|
||||
variant="light"
|
||||
styles={{ label: { textTransform: "none" } }}
|
||||
leftSection={<IconClockPlay size={12} />}
|
||||
>
|
||||
keep_firing_for: {formatDuration(rule.duration * 1000)}
|
||||
</Badge>
|
||||
)}
|
||||
</Group>
|
||||
)}
|
||||
{rule.labels && Object.keys(rule.labels).length > 0 && (
|
||||
<Group mt="md" gap="xs">
|
||||
{Object.entries(rule.labels).map(([k, v]) => (
|
||||
<Badge
|
||||
variant="light"
|
||||
className={badgeClasses.labelBadge}
|
||||
styles={{ label: { textTransform: "none" } }}
|
||||
key={k}
|
||||
>
|
||||
{k}: {v}
|
||||
</Badge>
|
||||
))}
|
||||
</Group>
|
||||
)}
|
||||
{/* {Object.keys(r.annotations).length > 0 && (
|
||||
<Group mt="md" gap="xs">
|
||||
{Object.entries(r.annotations).map(([k, v]) => (
|
||||
<Badge
|
||||
variant="light"
|
||||
color="orange.9"
|
||||
styles={{ label: { textTransform: "none" } }}
|
||||
key={k}
|
||||
>
|
||||
{k}: {v}
|
||||
</Badge>
|
||||
))}
|
||||
</Group>
|
||||
)} */}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default RuleDefinition;
|
Loading…
Reference in a new issue