Add state filtering to alerts page

Signed-off-by: Julius Volz <julius.volz@gmail.com>
This commit is contained in:
Julius Volz 2024-04-03 14:46:17 +02:00
parent 2be782df77
commit 3c44f43815

View file

@ -15,16 +15,19 @@ import {
Pill,
Stack,
Input,
Alert,
} from "@mantine/core";
import { useSuspenseAPIQuery } from "../api/api";
import { AlertingRulesMap } from "../api/responseTypes/rules";
import badgeClasses from "../Badge.module.css";
import RuleDefinition from "../RuleDefinition";
import { formatRelative, now } from "../lib/formatTime";
import { humanizeDurationRelative, now } from "../lib/formatTime";
import { Fragment } from "react";
import { AlertStateMultiSelect } from "./AlertStateMultiSelect";
import { useAppSelector } from "../state/hooks";
import { IconSearch } from "@tabler/icons-react";
import { StateMultiSelect } from "../StateMultiSelect";
import { useAppDispatch, useAppSelector } from "../state/hooks";
import { IconInfoCircle, IconSearch } from "@tabler/icons-react";
import { LabelBadges } from "../LabelBadges";
import { updateAlertFilters } from "../state/alertsPageSlice";
export default function AlertsPage() {
const { data } = useSuspenseAPIQuery<AlertingRulesMap>({
@ -33,9 +36,12 @@ export default function AlertsPage() {
type: "alert",
},
});
const dispatch = useAppDispatch();
const showAnnotations = useAppSelector(
(state) => state.settings.showAnnotations
);
const filters = useAppSelector((state) => state.alertsPage.filters);
const ruleStatsCount = {
inactive: 0,
@ -50,7 +56,19 @@ export default function AlertsPage() {
return (
<>
<Group mb="md" mt="xs">
<AlertStateMultiSelect />
<StateMultiSelect
options={["inactive", "pending", "firing"]}
optionClass={(o) =>
o === "inactive"
? badgeClasses.healthOk
: o === "pending"
? badgeClasses.healthWarn
: badgeClasses.healthErr
}
placeholder="Filter by alert state"
values={filters.state}
onChange={(values) => dispatch(updateAlertFilters({ state: values }))}
/>
<Input
flex={1}
leftSection={<IconSearch size={14} />}
@ -58,163 +76,153 @@ export default function AlertsPage() {
></Input>
</Group>
<Stack>
{data.data.groups.map((g, i) => (
<Card
shadow="xs"
withBorder
p="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()}
style={{
borderLeft:
numFiring > 0
? "5px solid var(--mantine-color-red-4)"
: numPending > 0
? "5px solid var(--mantine-color-orange-5)"
: "5px solid var(--mantine-color-green-4)",
}}
{data.data.groups.map((g, i) => {
const filteredRules = g.rules.filter(
(r) => filters.state.length === 0 || filters.state.includes(r.state)
);
return (
<Card
shadow="xs"
withBorder
p="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)"
>
<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>
)} */}
{g.name}
</Text>
<Text fz="sm" c="gray.6">
{g.file}
</Text>
</Group>
</Group>
{filteredRules.length === 0 && (
<Alert
title="No matching rules"
icon={<IconInfoCircle size={14} />}
>
No rules found that match your filter criteria.
</Alert>
)}
<Accordion multiple variant="separated">
{filteredRules.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()}
style={{
borderLeft:
numFiring > 0
? "5px solid var(--mantine-color-red-4)"
: numPending > 0
? "5px solid var(--mantine-color-orange-5)"
: "5px solid var(--mantine-color-green-4)",
}}
>
<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>
)}
</Group>
</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 && (
</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 colSpan={4}>
<Table mt="md" mb="xl">
<Table.Tbody>
{Object.entries(a.annotations).map(
([k, v]) => (
<Table.Td>
<LabelBadges labels={a.labels} />
</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>
{humanizeDurationRelative(
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>
))}
))}
</Table.Tbody>
</Table>
</Table.Td>
</Table.Tr>
)}
</Fragment>
))}
</Table.Tbody>
</Table>
)}
</Accordion.Panel>
</Accordion.Item>
);
})}
</Accordion>
</Card>
);
})}
</Stack>
</>
);