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, Pill,
Stack, Stack,
Input, Input,
Alert,
} from "@mantine/core"; } from "@mantine/core";
import { useSuspenseAPIQuery } from "../api/api"; import { useSuspenseAPIQuery } from "../api/api";
import { AlertingRulesMap } from "../api/responseTypes/rules"; import { AlertingRulesMap } from "../api/responseTypes/rules";
import badgeClasses from "../Badge.module.css"; import badgeClasses from "../Badge.module.css";
import RuleDefinition from "../RuleDefinition"; import RuleDefinition from "../RuleDefinition";
import { formatRelative, now } from "../lib/formatTime"; import { humanizeDurationRelative, now } from "../lib/formatTime";
import { Fragment } from "react"; import { Fragment } from "react";
import { AlertStateMultiSelect } from "./AlertStateMultiSelect"; import { StateMultiSelect } from "../StateMultiSelect";
import { useAppSelector } from "../state/hooks"; import { useAppDispatch, useAppSelector } from "../state/hooks";
import { IconSearch } from "@tabler/icons-react"; import { IconInfoCircle, IconSearch } from "@tabler/icons-react";
import { LabelBadges } from "../LabelBadges";
import { updateAlertFilters } from "../state/alertsPageSlice";
export default function AlertsPage() { export default function AlertsPage() {
const { data } = useSuspenseAPIQuery<AlertingRulesMap>({ const { data } = useSuspenseAPIQuery<AlertingRulesMap>({
@ -33,9 +36,12 @@ export default function AlertsPage() {
type: "alert", type: "alert",
}, },
}); });
const dispatch = useAppDispatch();
const showAnnotations = useAppSelector( const showAnnotations = useAppSelector(
(state) => state.settings.showAnnotations (state) => state.settings.showAnnotations
); );
const filters = useAppSelector((state) => state.alertsPage.filters);
const ruleStatsCount = { const ruleStatsCount = {
inactive: 0, inactive: 0,
@ -50,7 +56,19 @@ export default function AlertsPage() {
return ( return (
<> <>
<Group mb="md" mt="xs"> <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 <Input
flex={1} flex={1}
leftSection={<IconSearch size={14} />} leftSection={<IconSearch size={14} />}
@ -58,163 +76,153 @@ export default function AlertsPage() {
></Input> ></Input>
</Group> </Group>
<Stack> <Stack>
{data.data.groups.map((g, i) => ( {data.data.groups.map((g, i) => {
<Card const filteredRules = g.rules.filter(
shadow="xs" (r) => filters.state.length === 0 || filters.state.includes(r.state)
withBorder );
p="md" return (
key={i} // TODO: Find a stable and definitely unique key. <Card
> shadow="xs"
<Group mb="md" mt="xs" ml="xs" justify="space-between"> withBorder
<Group align="baseline"> p="md"
<Text fz="xl" fw={600} c="var(--mantine-primary-color-filled)"> key={i} // TODO: Find a stable and definitely unique key.
{g.name} >
</Text> <Group mb="md" mt="xs" ml="xs" justify="space-between">
<Text fz="sm" c="gray.6"> <Group align="baseline">
{g.file} <Text
</Text> fz="xl"
</Group> fw={600}
</Group> c="var(--mantine-primary-color-filled)"
<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)",
}}
> >
<Accordion.Control> {g.name}
<Group wrap="nowrap" justify="space-between" mr="lg"> </Text>
<Text>{r.name}</Text> <Text fz="sm" c="gray.6">
<Group gap="xs"> {g.file}
{numFiring > 0 && ( </Text>
<Badge className={badgeClasses.healthErr}> </Group>
firing ({numFiring}) </Group>
</Badge> {filteredRules.length === 0 && (
)} <Alert
{numPending > 0 && ( title="No matching rules"
<Badge className={badgeClasses.healthWarn}> icon={<IconInfoCircle size={14} />}
pending ({numPending}) >
</Badge> No rules found that match your filter criteria.
)} </Alert>
{/* {numFiring === 0 && numPending === 0 && ( )}
<Badge className={badgeClasses.healthOk}> <Accordion multiple variant="separated">
inactive {filteredRules.map((r, j) => {
</Badge> 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>
</Group> </Accordion.Control>
</Accordion.Control> <Accordion.Panel>
<Accordion.Panel> <RuleDefinition rule={r} />
<RuleDefinition rule={r} /> {r.alerts.length > 0 && (
{r.alerts.length > 0 && ( <Table mt="lg">
<Table mt="lg"> <Table.Thead>
<Table.Thead> <Table.Tr>
<Table.Tr> <Table.Th>Alert labels</Table.Th>
<Table.Th>Alert labels</Table.Th> <Table.Th>State</Table.Th>
<Table.Th>State</Table.Th> <Table.Th>Active Since</Table.Th>
<Table.Th>Active Since</Table.Th> <Table.Th>Value</Table.Th>
<Table.Th>Value</Table.Th> </Table.Tr>
</Table.Tr> </Table.Thead>
</Table.Thead> <Table.Tbody>
<Table.Tbody> {r.type === "alerting" &&
{r.type === "alerting" && r.alerts.map((a, k) => (
r.alerts.map((a, k) => ( <Fragment key={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.Tr>
<Table.Td colSpan={4}> <Table.Td>
<Table mt="md" mb="xl"> <LabelBadges labels={a.labels} />
<Table.Tbody> </Table.Td>
{Object.entries(a.annotations).map( <Table.Td>
([k, v]) => ( <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.Tr key={k}>
<Table.Th>{k}</Table.Th> <Table.Th>{k}</Table.Th>
<Table.Td>{v}</Table.Td> <Table.Td>{v}</Table.Td>
</Table.Tr> </Table.Tr>
) ))}
)} </Table.Tbody>
</Table.Tbody> </Table>
</Table> </Table.Td>
</Table.Td> </Table.Tr>
</Table.Tr> )}
)} </Fragment>
</Fragment> ))}
))} </Table.Tbody>
</Table.Tbody> </Table>
</Table> )}
)} </Accordion.Panel>
</Accordion.Panel> </Accordion.Item>
</Accordion.Item> );
); })}
})} </Accordion>
</Accordion> </Card>
</Card> );
))} })}
</Stack> </Stack>
</> </>
); );