mirror of
https://github.com/prometheus/prometheus.git
synced 2025-03-05 20:59:13 -08:00
Add state and label/alertname filter mockups to Alerts page
Signed-off-by: Julius Volz <julius.volz@gmail.com>
This commit is contained in:
parent
2f95bbe570
commit
627826783c
135
web/ui/mantine-ui/src/pages/AlertStateMultiSelect.tsx
Normal file
135
web/ui/mantine-ui/src/pages/AlertStateMultiSelect.tsx
Normal file
|
@ -0,0 +1,135 @@
|
|||
import { useState } from "react";
|
||||
import {
|
||||
Badge,
|
||||
CheckIcon,
|
||||
CloseButton,
|
||||
Combobox,
|
||||
ComboboxChevron,
|
||||
ComboboxClearButton,
|
||||
Group,
|
||||
Input,
|
||||
Pill,
|
||||
PillGroup,
|
||||
PillsInput,
|
||||
useCombobox,
|
||||
} from "@mantine/core";
|
||||
import badgeClasses from "../Badge.module.css";
|
||||
import { IconFilter } from "@tabler/icons-react";
|
||||
import { IconFilterSearch } from "@tabler/icons-react";
|
||||
|
||||
interface StatePillProps extends React.ComponentPropsWithoutRef<"div"> {
|
||||
value: string;
|
||||
onRemove?: () => void;
|
||||
}
|
||||
|
||||
export function StatePill({ value, onRemove, ...others }: StatePillProps) {
|
||||
return (
|
||||
<Pill
|
||||
fw={600}
|
||||
style={{ textTransform: "uppercase", fontWeight: 600 }}
|
||||
className={
|
||||
value === "inactive"
|
||||
? badgeClasses.healthOk
|
||||
: value === "pending"
|
||||
? badgeClasses.healthWarn
|
||||
: badgeClasses.healthErr
|
||||
}
|
||||
onRemove={onRemove}
|
||||
{...others}
|
||||
withRemoveButton={!!onRemove}
|
||||
>
|
||||
{value}
|
||||
</Pill>
|
||||
);
|
||||
}
|
||||
|
||||
export function AlertStateMultiSelect() {
|
||||
const combobox = useCombobox({
|
||||
onDropdownClose: () => combobox.resetSelectedOption(),
|
||||
onDropdownOpen: () => combobox.updateSelectedOptionIndex("active"),
|
||||
});
|
||||
|
||||
const [values, setValues] = useState<string[]>([]);
|
||||
|
||||
const handleValueSelect = (val: string) =>
|
||||
setValues((current) =>
|
||||
current.includes(val)
|
||||
? current.filter((v) => v !== val)
|
||||
: [...current, val]
|
||||
);
|
||||
|
||||
const handleValueRemove = (val: string) =>
|
||||
setValues((current) => current.filter((v) => v !== val));
|
||||
|
||||
const renderedValues = values.map((item) => (
|
||||
<StatePill
|
||||
value={item}
|
||||
onRemove={() => handleValueRemove(item)}
|
||||
key={item}
|
||||
/>
|
||||
));
|
||||
|
||||
const options = ["inactive", "pending", "firing"].map((value) => {
|
||||
return (
|
||||
<Combobox.Option
|
||||
value={value}
|
||||
key={value}
|
||||
active={values.includes(value)}
|
||||
>
|
||||
<Group gap="sm">
|
||||
{values.includes(value) ? <CheckIcon size={12} color="gray" /> : null}
|
||||
<StatePill value={value} />
|
||||
</Group>
|
||||
</Combobox.Option>
|
||||
);
|
||||
});
|
||||
|
||||
return (
|
||||
<Combobox
|
||||
store={combobox}
|
||||
onOptionSubmit={handleValueSelect}
|
||||
withinPortal={false}
|
||||
>
|
||||
<Combobox.DropdownTarget>
|
||||
<PillsInput
|
||||
pointer
|
||||
onClick={() => combobox.toggleDropdown()}
|
||||
miw={200}
|
||||
leftSection={<IconFilter size={14} />}
|
||||
rightSection={
|
||||
values.length > 0 ? (
|
||||
<ComboboxClearButton onClear={() => setValues([])} />
|
||||
) : (
|
||||
<ComboboxChevron />
|
||||
)
|
||||
}
|
||||
>
|
||||
<Pill.Group>
|
||||
{renderedValues.length > 0 ? (
|
||||
renderedValues
|
||||
) : (
|
||||
<Input.Placeholder>Filter by alert state</Input.Placeholder>
|
||||
)}
|
||||
|
||||
<Combobox.EventsTarget>
|
||||
<PillsInput.Field
|
||||
type="hidden"
|
||||
onBlur={() => combobox.closeDropdown()}
|
||||
onKeyDown={(event) => {
|
||||
if (event.key === "Backspace") {
|
||||
event.preventDefault();
|
||||
handleValueRemove(values[values.length - 1]);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</Combobox.EventsTarget>
|
||||
</Pill.Group>
|
||||
</PillsInput>
|
||||
</Combobox.DropdownTarget>
|
||||
|
||||
<Combobox.Dropdown>
|
||||
<Combobox.Options>{options}</Combobox.Options>
|
||||
</Combobox.Dropdown>
|
||||
</Combobox>
|
||||
);
|
||||
}
|
|
@ -7,14 +7,24 @@ import {
|
|||
Badge,
|
||||
Tooltip,
|
||||
Box,
|
||||
Switch,
|
||||
Divider,
|
||||
Button,
|
||||
Fieldset,
|
||||
Checkbox,
|
||||
MultiSelect,
|
||||
Pill,
|
||||
Stack,
|
||||
Input,
|
||||
} 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 { Fragment, useState } from "react";
|
||||
import { Fragment } from "react";
|
||||
import { AlertStateMultiSelect } from "./AlertStateMultiSelect";
|
||||
import { useAppSelector } from "../state/hooks";
|
||||
import { IconSearch } from "@tabler/icons-react";
|
||||
|
||||
export default function AlertsPage() {
|
||||
const { data } = useSuspenseAPIQuery<AlertingRulesMap>({
|
||||
|
@ -23,7 +33,9 @@ export default function AlertsPage() {
|
|||
type: "alert",
|
||||
},
|
||||
});
|
||||
const [showAnnotations, setShowAnnotations] = useState(false);
|
||||
const showAnnotations = useAppSelector(
|
||||
(state) => state.settings.showAnnotations
|
||||
);
|
||||
|
||||
const ruleStatsCount = {
|
||||
inactive: 0,
|
||||
|
@ -37,167 +49,173 @@ export default function AlertsPage() {
|
|||
|
||||
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 mb="md" mt="xs">
|
||||
<AlertStateMultiSelect />
|
||||
<Input
|
||||
flex={1}
|
||||
leftSection={<IconSearch size={14} />}
|
||||
placeholder="Filter by alert name or labels"
|
||||
></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>
|
||||
</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;
|
||||
<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>
|
||||
<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>
|
||||
)} */}
|
||||
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>
|
||||
)}
|
||||
{/* {numFiring === 0 && numPending === 0 && (
|
||||
<Badge className={badgeClasses.healthOk}>
|
||||
inactive
|
||||
</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.Tr key={k}>
|
||||
<Table.Th>{k}</Table.Th>
|
||||
<Table.Td>{v}</Table.Td>
|
||||
</Table.Tr>
|
||||
)
|
||||
)}
|
||||
</Table.Tbody>
|
||||
</Table>
|
||||
<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>
|
||||
)}
|
||||
</Fragment>
|
||||
))}
|
||||
</Table.Tbody>
|
||||
</Table>
|
||||
)}
|
||||
</Accordion.Panel>
|
||||
</Accordion.Item>
|
||||
);
|
||||
})}
|
||||
</Accordion>
|
||||
</Card>
|
||||
))}
|
||||
{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>
|
||||
))}
|
||||
</Stack>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue