Merge pull request #14907 from prometheus/julius/new-ui-improvements

UI improvements: Factor out common styles, fix tree node line rendering, always show full badge contents (no ellipsis)
This commit is contained in:
Julius Volz 2024-09-16 09:00:38 +02:00 committed by GitHub
commit 62bd893b80
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
29 changed files with 284 additions and 248 deletions

View file

@ -2,6 +2,7 @@ import "@mantine/core/styles.css";
import "@mantine/code-highlight/styles.css"; import "@mantine/code-highlight/styles.css";
import "@mantine/notifications/styles.css"; import "@mantine/notifications/styles.css";
import "@mantine/dates/styles.css"; import "@mantine/dates/styles.css";
import "./mantine-overrides.css";
import classes from "./App.module.css"; import classes from "./App.module.css";
import PrometheusLogo from "./images/prometheus-logo.svg"; import PrometheusLogo from "./images/prometheus-logo.svg";
@ -67,11 +68,10 @@ import { QueryParamProvider } from "use-query-params";
import { ReactRouter6Adapter } from "use-query-params/adapters/react-router-6"; import { ReactRouter6Adapter } from "use-query-params/adapters/react-router-6";
import ServiceDiscoveryPage from "./pages/service-discovery/ServiceDiscoveryPage"; import ServiceDiscoveryPage from "./pages/service-discovery/ServiceDiscoveryPage";
import AlertmanagerDiscoveryPage from "./pages/AlertmanagerDiscoveryPage"; import AlertmanagerDiscoveryPage from "./pages/AlertmanagerDiscoveryPage";
import { actionIconStyle, navIconStyle } from "./styles";
const queryClient = new QueryClient(); const queryClient = new QueryClient();
const navIconStyle = { width: rem(16), height: rem(16) };
const mainNavPages = [ const mainNavPages = [
{ {
title: "Query", title: "Query",
@ -322,9 +322,9 @@ function App() {
color="gray" color="gray"
title="Documentation" title="Documentation"
aria-label="Documentation" aria-label="Documentation"
size={32} size={rem(32)}
> >
<IconBook size={20} /> <IconBook style={actionIconStyle} />
</ActionIcon> </ActionIcon>
</> </>
); );

View file

@ -32,7 +32,7 @@ class ErrorBoundary extends Component<Props, State> {
<Alert <Alert
color="red" color="red"
title={this.props.title || "Error querying page data"} title={this.props.title || "Error querying page data"}
icon={<IconAlertTriangle size={14} />} icon={<IconAlertTriangle />}
maw={500} maw={500}
mx="auto" mx="auto"
mt="lg" mt="lg"

View file

@ -0,0 +1,32 @@
import { Card, Group } from "@mantine/core";
import { TablerIconsProps } from "@tabler/icons-react";
import { FC, ReactNode } from "react";
import { infoPageCardTitleIconStyle } from "../styles";
const InfoPageCard: FC<{
children: ReactNode;
title?: string;
icon?: React.ComponentType<TablerIconsProps>;
}> = ({ children, title, icon: Icon }) => {
return (
<Card shadow="xs" withBorder p="md">
{title && (
<Group
wrap="nowrap"
align="center"
ml="xs"
mb="sm"
gap="xs"
fz="xl"
fw={600}
>
{Icon && <Icon style={infoPageCardTitleIconStyle} />}
{title}
</Group>
)}
{children}
</Card>
);
};
export default InfoPageCard;

View file

@ -0,0 +1,12 @@
import { Stack } from "@mantine/core";
import { FC, ReactNode } from "react";
const InfoPageStack: FC<{ children: ReactNode }> = ({ children }) => {
return (
<Stack gap="lg" maw={1000} mx="auto" mt="xs">
{children}
</Stack>
);
};
export default InfoPageStack;

View file

@ -4,7 +4,6 @@ import {
Box, Box,
Card, Card,
Group, Group,
rem,
Table, Table,
Tooltip, Tooltip,
useComputedColorScheme, useComputedColorScheme,
@ -25,6 +24,7 @@ import {
import { PromQLExtension } from "@prometheus-io/codemirror-promql"; import { PromQLExtension } from "@prometheus-io/codemirror-promql";
import { LabelBadges } from "./LabelBadges"; import { LabelBadges } from "./LabelBadges";
import { useSettings } from "../state/settingsSlice"; import { useSettings } from "../state/settingsSlice";
import { actionIconStyle, badgeIconStyle } from "../styles";
const promqlExtension = new PromQLExtension(); const promqlExtension = new PromQLExtension();
@ -64,7 +64,7 @@ const RuleDefinition: FC<{ rule: Rule }> = ({ rule }) => {
}} }}
className={codeboxClasses.queryButton} className={codeboxClasses.queryButton}
> >
<IconSearch style={{ width: rem(14) }} /> <IconSearch style={actionIconStyle} />
</ActionIcon> </ActionIcon>
</Tooltip> </Tooltip>
</Card> </Card>
@ -74,7 +74,7 @@ const RuleDefinition: FC<{ rule: Rule }> = ({ rule }) => {
<Badge <Badge
variant="light" variant="light"
styles={{ label: { textTransform: "none" } }} styles={{ label: { textTransform: "none" } }}
leftSection={<IconClockPause size={12} />} leftSection={<IconClockPause style={badgeIconStyle} />}
> >
for: {formatPrometheusDuration(rule.duration * 1000)} for: {formatPrometheusDuration(rule.duration * 1000)}
</Badge> </Badge>
@ -83,7 +83,7 @@ const RuleDefinition: FC<{ rule: Rule }> = ({ rule }) => {
<Badge <Badge
variant="light" variant="light"
styles={{ label: { textTransform: "none" } }} styles={{ label: { textTransform: "none" } }}
leftSection={<IconClockPlay size={12} />} leftSection={<IconClockPlay style={badgeIconStyle} />}
> >
keep_firing_for: {formatPrometheusDuration(rule.duration * 1000)} keep_firing_for: {formatPrometheusDuration(rule.duration * 1000)}
</Badge> </Badge>

View file

@ -3,6 +3,7 @@ import { IconSettings } from "@tabler/icons-react";
import { FC } from "react"; import { FC } from "react";
import { useAppDispatch } from "../state/hooks"; import { useAppDispatch } from "../state/hooks";
import { updateSettings, useSettings } from "../state/settingsSlice"; import { updateSettings, useSettings } from "../state/settingsSlice";
import { actionIconStyle } from "../styles";
const SettingsMenu: FC = () => { const SettingsMenu: FC = () => {
const { const {
@ -24,7 +25,7 @@ const SettingsMenu: FC = () => {
aria-label="Settings" aria-label="Settings"
size={32} size={32}
> >
<IconSettings size={20} /> <IconSettings style={actionIconStyle} />
</ActionIcon> </ActionIcon>
</Popover.Target> </Popover.Target>
<Popover.Dropdown> <Popover.Dropdown>

View file

@ -10,6 +10,7 @@ import {
useCombobox, useCombobox,
} from "@mantine/core"; } from "@mantine/core";
import { IconHeartRateMonitor } from "@tabler/icons-react"; import { IconHeartRateMonitor } from "@tabler/icons-react";
import { inputIconStyle } from "../styles";
interface StatePillProps extends React.ComponentPropsWithoutRef<"div"> { interface StatePillProps extends React.ComponentPropsWithoutRef<"div"> {
value: string; value: string;
@ -80,7 +81,7 @@ export const StateMultiSelect: FC<StateMultiSelectProps> = ({
pointer pointer
onClick={() => combobox.toggleDropdown()} onClick={() => combobox.toggleDropdown()}
miw={200} miw={200}
leftSection={<IconHeartRateMonitor size={14} />} leftSection={<IconHeartRateMonitor style={inputIconStyle} />}
rightSection={ rightSection={
values.length > 0 ? ( values.length > 0 ? (
<ComboboxClearButton onClear={() => onChange([])} /> <ComboboxClearButton onClear={() => onChange([])} />

View file

@ -0,0 +1,4 @@
.mantine-Badge-label {
overflow: unset;
text-overflow: unset;
}

View file

@ -1,16 +1,16 @@
import { Card, Group, Text } from "@mantine/core"; import { Text } from "@mantine/core";
import { IconSpy } from "@tabler/icons-react"; import { IconSpy } from "@tabler/icons-react";
import { FC } from "react"; import { FC } from "react";
import InfoPageStack from "../components/InfoPageStack";
import InfoPageCard from "../components/InfoPageCard";
const AgentPage: FC = () => { const AgentPage: FC = () => {
return ( return (
<Card shadow="xs" withBorder p="md" mt="xs"> <InfoPageStack>
<Group wrap="nowrap" align="center" ml="xs" mb="sm" gap="xs"> <InfoPageCard
<IconSpy size={22} /> title="Prometheus Agent"
<Text fz="xl" fw={600}> icon={IconSpy}
Prometheus Agent >
</Text>
</Group>
<Text p="md"> <Text p="md">
This Prometheus instance is running in <strong>agent mode</strong>. In This Prometheus instance is running in <strong>agent mode</strong>. In
this mode, Prometheus is only used to scrape discovered targets and this mode, Prometheus is only used to scrape discovered targets and
@ -20,7 +20,8 @@ const AgentPage: FC = () => {
Some features are not available in this mode, such as querying and Some features are not available in this mode, such as querying and
alerting. alerting.
</Text> </Text>
</Card> </InfoPageCard>
</InfoPageStack>
); );
}; };

View file

@ -1,9 +1,11 @@
import { Alert, Card, Group, Stack, Table, Text } from "@mantine/core"; import { Alert, Table } from "@mantine/core";
import { IconBell, IconBellOff, IconInfoCircle } from "@tabler/icons-react"; import { IconBell, IconBellOff, IconInfoCircle } from "@tabler/icons-react";
import { useSuspenseAPIQuery } from "../api/api"; import { useSuspenseAPIQuery } from "../api/api";
import { AlertmanagersResult } from "../api/responseTypes/alertmanagers"; import { AlertmanagersResult } from "../api/responseTypes/alertmanagers";
import EndpointLink from "../components/EndpointLink"; import EndpointLink from "../components/EndpointLink";
import InfoPageCard from "../components/InfoPageCard";
import InfoPageStack from "../components/InfoPageStack";
export const targetPoolDisplayLimit = 20; export const targetPoolDisplayLimit = 20;
@ -18,14 +20,8 @@ export default function AlertmanagerDiscoveryPage() {
}); });
return ( return (
<Stack gap="lg" maw={1000} mx="auto" mt="xs"> <InfoPageStack>
<Card shadow="xs" withBorder p="md"> <InfoPageCard title="Active Alertmanagers" icon={IconBell}>
<Group wrap="nowrap" align="center" ml="xs" mb="sm" gap="xs">
<IconBell size={22} />
<Text fz="xl" fw={600}>
Active Alertmanagers
</Text>
</Group>
{activeAlertmanagers.length === 0 ? ( {activeAlertmanagers.length === 0 ? (
<Alert title="No active alertmanagers" icon={<IconInfoCircle />}> <Alert title="No active alertmanagers" icon={<IconInfoCircle />}>
No active alertmanagers found. No active alertmanagers found.
@ -46,14 +42,8 @@ export default function AlertmanagerDiscoveryPage() {
</Table.Tbody> </Table.Tbody>
</Table> </Table>
)} )}
</Card> </InfoPageCard>
<Card shadow="xs" withBorder p="md"> <InfoPageCard title="Dropped Alertmanagers" icon={IconBellOff}>
<Group wrap="nowrap" align="center" ml="xs" mb="sm" gap="xs">
<IconBellOff size={22} />
<Text fz="xl" fw={600}>
Dropped Alertmanagers
</Text>
</Group>
{droppedAlertmanagers.length === 0 ? ( {droppedAlertmanagers.length === 0 ? (
<Alert title="No dropped alertmanagers" icon={<IconInfoCircle />}> <Alert title="No dropped alertmanagers" icon={<IconInfoCircle />}>
No dropped alertmanagers found. No dropped alertmanagers found.
@ -74,7 +64,7 @@ export default function AlertmanagerDiscoveryPage() {
</Table.Tbody> </Table.Tbody>
</Table> </Table>
)} )}
</Card> </InfoPageCard>
</Stack> </InfoPageStack>
); );
} }

View file

@ -32,6 +32,7 @@ import {
} from "use-query-params"; } from "use-query-params";
import { useDebouncedValue } from "@mantine/hooks"; import { useDebouncedValue } from "@mantine/hooks";
import { KVSearch } from "@nexucis/kvsearch"; import { KVSearch } from "@nexucis/kvsearch";
import { inputIconStyle } from "../styles";
type AlertsPageData = { type AlertsPageData = {
// How many rules are in each state across all groups. // How many rules are in each state across all groups.
@ -190,7 +191,7 @@ export default function AlertsPage() {
/> />
<TextInput <TextInput
flex={1} flex={1}
leftSection={<IconSearch size={14} />} leftSection={<IconSearch style={inputIconStyle} />}
placeholder="Filter by rule name or labels" placeholder="Filter by rule name or labels"
value={searchFilter || ""} value={searchFilter || ""}
onChange={(event) => onChange={(event) =>
@ -199,7 +200,7 @@ export default function AlertsPage() {
></TextInput> ></TextInput>
</Group> </Group>
{alertsPageData.groups.length === 0 ? ( {alertsPageData.groups.length === 0 ? (
<Alert title="No rules found" icon={<IconInfoCircle size={14} />}> <Alert title="No rules found" icon={<IconInfoCircle />}>
No rules found. No rules found.
</Alert> </Alert>
) : ( ) : (
@ -207,7 +208,7 @@ export default function AlertsPage() {
alertsPageData.groups.length !== shownGroups.length && ( alertsPageData.groups.length !== shownGroups.length && (
<Alert <Alert
title="Hiding groups with no matching rules" title="Hiding groups with no matching rules"
icon={<IconInfoCircle size={14} />} icon={<IconInfoCircle/>}
> >
Hiding {alertsPageData.groups.length - shownGroups.length} empty Hiding {alertsPageData.groups.length - shownGroups.length} empty
groups due to filters or no rules. groups due to filters or no rules.
@ -326,7 +327,7 @@ export default function AlertsPage() {
{r.rule.alerts.length > 0 && ( {r.rule.alerts.length > 0 && (
<Table mt="lg"> <Table mt="lg">
<Table.Thead> <Table.Thead>
<Table.Tr> <Table.Tr style={{whiteSpace: "nowrap"}}>
<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>

View file

@ -8,7 +8,6 @@ import {
TextInput, TextInput,
rem, rem,
keys, keys,
Card,
} from "@mantine/core"; } from "@mantine/core";
import { import {
IconSelector, IconSelector,
@ -18,6 +17,9 @@ import {
} from "@tabler/icons-react"; } from "@tabler/icons-react";
import classes from "./FlagsPage.module.css"; import classes from "./FlagsPage.module.css";
import { useSuspenseAPIQuery } from "../api/api"; import { useSuspenseAPIQuery } from "../api/api";
import InfoPageStack from "../components/InfoPageStack";
import InfoPageCard from "../components/InfoPageCard";
import { inputIconStyle } from "../styles";
interface RowData { interface RowData {
flag: string; flag: string;
@ -124,17 +126,13 @@ export default function FlagsPage() {
)); ));
return ( return (
<Card shadow="xs" maw={1000} mx="auto" mt="xs" withBorder> <InfoPageStack>
<InfoPageCard>
<TextInput <TextInput
placeholder="Filter by flag name or value" placeholder="Filter by flag name or value"
mb="md" mb="md"
autoFocus autoFocus
leftSection={ leftSection={<IconSearch style={inputIconStyle} />}
<IconSearch
style={{ width: rem(16), height: rem(16) }}
stroke={1.5}
/>
}
value={search} value={search}
onChange={handleSearchChange} onChange={handleSearchChange}
/> />
@ -177,6 +175,7 @@ export default function FlagsPage() {
)} )}
</Table.Tbody> </Table.Tbody>
</Table> </Table>
</Card> </InfoPageCard>
</InfoPageStack>
); );
} }

View file

@ -27,6 +27,7 @@ import { useSuspenseAPIQuery } from "../api/api";
import { RulesResult } from "../api/responseTypes/rules"; import { RulesResult } from "../api/responseTypes/rules";
import badgeClasses from "../Badge.module.css"; import badgeClasses from "../Badge.module.css";
import RuleDefinition from "../components/RuleDefinition"; import RuleDefinition from "../components/RuleDefinition";
import { badgeIconStyle } from "../styles";
const healthBadgeClass = (state: string) => { const healthBadgeClass = (state: string) => {
switch (state) { switch (state) {
@ -47,7 +48,7 @@ export default function RulesPage() {
return ( return (
<Stack mt="xs"> <Stack mt="xs">
{data.data.groups.length === 0 && ( {data.data.groups.length === 0 && (
<Alert title="No rule groups" icon={<IconInfoCircle size={14} />}> <Alert title="No rule groups" icon={<IconInfoCircle />}>
No rule groups configured. No rule groups configured.
</Alert> </Alert>
)} )}
@ -74,7 +75,7 @@ export default function RulesPage() {
variant="light" variant="light"
className={badgeClasses.statsBadge} className={badgeClasses.statsBadge}
styles={{ label: { textTransform: "none" } }} styles={{ label: { textTransform: "none" } }}
leftSection={<IconRefresh size={12} />} leftSection={<IconRefresh style={badgeIconStyle} />}
> >
last run {humanizeDurationRelative(g.lastEvaluation, now())} last run {humanizeDurationRelative(g.lastEvaluation, now())}
</Badge> </Badge>
@ -84,7 +85,7 @@ export default function RulesPage() {
variant="light" variant="light"
className={badgeClasses.statsBadge} className={badgeClasses.statsBadge}
styles={{ label: { textTransform: "none" } }} styles={{ label: { textTransform: "none" } }}
leftSection={<IconHourglass size={12} />} leftSection={<IconHourglass style={badgeIconStyle} />}
> >
took {humanizeDuration(parseFloat(g.evaluationTime) * 1000)} took {humanizeDuration(parseFloat(g.evaluationTime) * 1000)}
</Badge> </Badge>
@ -94,7 +95,7 @@ export default function RulesPage() {
variant="transparent" variant="transparent"
className={badgeClasses.statsBadge} className={badgeClasses.statsBadge}
styles={{ label: { textTransform: "none" } }} styles={{ label: { textTransform: "none" } }}
leftSection={<IconRepeat size={12} />} leftSection={<IconRepeat style={badgeIconStyle} />}
> >
every {humanizeDuration(parseFloat(g.interval) * 1000)}{" "} every {humanizeDuration(parseFloat(g.interval) * 1000)}{" "}
</Badge> </Badge>
@ -102,7 +103,7 @@ export default function RulesPage() {
</Group> </Group>
</Group> </Group>
{g.rules.length === 0 && ( {g.rules.length === 0 && (
<Alert title="No rules" icon={<IconInfoCircle size={14} />}> <Alert title="No rules" icon={<IconInfoCircle />}>
No rules in rule group. No rules in rule group.
</Alert> </Alert>
)} )}
@ -150,7 +151,7 @@ export default function RulesPage() {
variant="light" variant="light"
className={badgeClasses.statsBadge} className={badgeClasses.statsBadge}
styles={{ label: { textTransform: "none" } }} styles={{ label: { textTransform: "none" } }}
leftSection={<IconRefresh size={12} />} leftSection={<IconRefresh style={badgeIconStyle} />}
> >
{humanizeDurationRelative(r.lastEvaluation, now())} {humanizeDurationRelative(r.lastEvaluation, now())}
</Badge> </Badge>
@ -164,7 +165,9 @@ export default function RulesPage() {
variant="light" variant="light"
className={badgeClasses.statsBadge} className={badgeClasses.statsBadge}
styles={{ label: { textTransform: "none" } }} styles={{ label: { textTransform: "none" } }}
leftSection={<IconHourglass size={12} />} leftSection={
<IconHourglass style={badgeIconStyle} />
}
> >
{humanizeDuration( {humanizeDuration(
parseFloat(r.evaluationTime) * 1000 parseFloat(r.evaluationTime) * 1000
@ -185,7 +188,7 @@ export default function RulesPage() {
color="red" color="red"
mt="sm" mt="sm"
title="Rule failed to evaluate" title="Rule failed to evaluate"
icon={<IconAlertTriangle size={14} />} icon={<IconAlertTriangle />}
> >
<strong>Error:</strong> {r.lastError} <strong>Error:</strong> {r.lastError}
</Alert> </Alert>

View file

@ -1,8 +1,10 @@
import { Card, Group, Stack, Table, Text } from "@mantine/core"; import { Table } from "@mantine/core";
import { useSuspenseAPIQuery } from "../api/api"; import { useSuspenseAPIQuery } from "../api/api";
import { IconRun, IconWall } from "@tabler/icons-react"; import { IconRun, IconWall } from "@tabler/icons-react";
import { formatTimestamp } from "../lib/formatTime"; import { formatTimestamp } from "../lib/formatTime";
import { useSettings } from "../state/settingsSlice"; import { useSettings } from "../state/settingsSlice";
import InfoPageCard from "../components/InfoPageCard";
import InfoPageStack from "../components/InfoPageStack";
export default function StatusPage() { export default function StatusPage() {
const { data: buildinfo } = useSuspenseAPIQuery<Record<string, string>>({ const { data: buildinfo } = useSuspenseAPIQuery<Record<string, string>>({
@ -42,14 +44,8 @@ export default function StatusPage() {
}; };
return ( return (
<Stack gap="lg" maw={1000} mx="auto" mt="xs"> <InfoPageStack>
<Card shadow="xs" withBorder p="md"> <InfoPageCard title="Build information" icon={IconWall}>
<Group wrap="nowrap" align="center" ml="xs" mb="sm" gap="xs">
<IconWall size={22} />
<Text fz="xl" fw={600}>
Build information
</Text>
</Group>
<Table layout="fixed"> <Table layout="fixed">
<Table.Tbody> <Table.Tbody>
{Object.entries(buildinfo.data).map(([k, v]) => ( {Object.entries(buildinfo.data).map(([k, v]) => (
@ -60,14 +56,8 @@ export default function StatusPage() {
))} ))}
</Table.Tbody> </Table.Tbody>
</Table> </Table>
</Card> </InfoPageCard>
<Card shadow="xs" withBorder p="md"> <InfoPageCard title="Runtime information" icon={IconRun}>
<Group wrap="nowrap" align="center" ml="xs" mb="sm" gap="xs">
<IconRun size={22} />
<Text fz="xl" fw={600}>
Runtime information
</Text>
</Group>
<Table layout="fixed"> <Table layout="fixed">
<Table.Tbody> <Table.Tbody>
{Object.entries(runtimeinfo.data).map(([k, v]) => { {Object.entries(runtimeinfo.data).map(([k, v]) => {
@ -84,7 +74,7 @@ export default function StatusPage() {
})} })}
</Table.Tbody> </Table.Tbody>
</Table> </Table>
</Card> </InfoPageCard>
</Stack> </InfoPageStack>
); );
} }

View file

@ -1,8 +1,10 @@
import { Stack, Card, Table, Text } from "@mantine/core"; import { Table } from "@mantine/core";
import { useSuspenseAPIQuery } from "../api/api"; import { useSuspenseAPIQuery } from "../api/api";
import { TSDBStatusResult } from "../api/responseTypes/tsdbStatus"; import { TSDBStatusResult } from "../api/responseTypes/tsdbStatus";
import { formatTimestamp } from "../lib/formatTime"; import { formatTimestamp } from "../lib/formatTime";
import { useSettings } from "../state/settingsSlice"; import { useSettings } from "../state/settingsSlice";
import InfoPageStack from "../components/InfoPageStack";
import InfoPageCard from "../components/InfoPageCard";
export default function TSDBStatusPage() { export default function TSDBStatusPage() {
const { const {
@ -41,7 +43,7 @@ export default function TSDBStatusPage() {
]; ];
return ( return (
<Stack gap="lg" maw={1000} mx="auto" mt="xs"> <InfoPageStack>
{[ {[
{ {
title: "TSDB Head Status", title: "TSDB Head Status",
@ -70,10 +72,7 @@ export default function TSDBStatusPage() {
formatAsCode: true, formatAsCode: true,
}, },
].map(({ title, unit = "Count", stats, formatAsCode }) => ( ].map(({ title, unit = "Count", stats, formatAsCode }) => (
<Card shadow="xs" withBorder p="md"> <InfoPageCard title={title}>
<Text fz="xl" fw={600} ml="xs" mb="sm">
{title}
</Text>
<Table layout="fixed"> <Table layout="fixed">
<Table.Thead> <Table.Thead>
<Table.Tr> <Table.Tr>
@ -82,8 +81,7 @@ export default function TSDBStatusPage() {
</Table.Tr> </Table.Tr>
</Table.Thead> </Table.Thead>
<Table.Tbody> <Table.Tbody>
{stats.map(({ name, value }) => { {stats.map(({ name, value }) => (
return (
<Table.Tr key={name}> <Table.Tr key={name}>
<Table.Td <Table.Td
style={{ style={{
@ -94,12 +92,11 @@ export default function TSDBStatusPage() {
</Table.Td> </Table.Td>
<Table.Td>{value}</Table.Td> <Table.Td>{value}</Table.Td>
</Table.Tr> </Table.Tr>
); ))}
})}
</Table.Tbody> </Table.Tbody>
</Table> </Table>
</Card> </InfoPageCard>
))} ))}
</Stack> </InfoPageStack>
); );
} }

View file

@ -64,7 +64,7 @@ const DataTable: FC<DataTableProps> = ({
result.length > maxDisplayableSeries && ( result.length > maxDisplayableSeries && (
<Alert <Alert
color="orange" color="orange"
icon={<IconAlertTriangle size={14} />} icon={<IconAlertTriangle />}
title="Showing limited results" title="Showing limited results"
> >
Fetched {data.result.length} metrics, only displaying first{" "} Fetched {data.result.length} metrics, only displaying first{" "}
@ -76,10 +76,7 @@ const DataTable: FC<DataTableProps> = ({
)} )}
{!doFormat && ( {!doFormat && (
<Alert <Alert title="Formatting turned off" icon={<IconInfoCircle />}>
title="Formatting turned off"
icon={<IconInfoCircle size={14} />}
>
Showing more than {maxFormattableSeries} series, turning off label Showing more than {maxFormattableSeries} series, turning off label
formatting to improve rendering performance. formatting to improve rendering performance.
</Alert> </Alert>
@ -166,7 +163,7 @@ const DataTable: FC<DataTableProps> = ({
<Alert <Alert
color="red" color="red"
title="Invalid query response" title="Invalid query response"
icon={<IconAlertTriangle size={14} />} icon={<IconAlertTriangle />}
> >
Invalid result value type Invalid result value type
</Alert> </Alert>

View file

@ -381,11 +381,7 @@ const VectorVectorBinaryExprExplainView: FC<
</Group> </Group>
{numGroups > Object.keys(matchGroups).length && ( {numGroups > Object.keys(matchGroups).length && (
<Alert <Alert color="yellow" mb="md" icon={<IconAlertTriangle />}>
color="yellow"
mb="md"
icon={<IconAlertTriangle size={14} />}
>
Too many match groups to display, only showing{" "} Too many match groups to display, only showing{" "}
{Object.keys(matchGroups).length} out of {numGroups} groups. {Object.keys(matchGroups).length} out of {numGroups} groups.
<br /> <br />
@ -397,11 +393,7 @@ const VectorVectorBinaryExprExplainView: FC<
)} )}
{errCount > 0 && ( {errCount > 0 && (
<Alert <Alert color="yellow" mb="md" icon={<IconAlertTriangle />}>
color="yellow"
mb="md"
icon={<IconAlertTriangle size={14} />}
>
Found matching issues in {errCount} match group Found matching issues in {errCount} match group
{errCount > 1 ? "s" : ""}. See below for per-group error details. {errCount > 1 ? "s" : ""}. See below for per-group error details.
</Alert> </Alert>
@ -642,7 +634,7 @@ const VectorVectorBinaryExprExplainView: FC<
color="red" color="red"
mb="md" mb="md"
title="Error in match group below" title="Error in match group below"
icon={<IconAlertTriangle size={14} />} icon={<IconAlertTriangle />}
> >
{explainError(node, mg, error)} {explainError(node, mg, error)}
</Alert> </Alert>

View file

@ -7,7 +7,6 @@ import {
Loader, Loader,
Menu, Menu,
Modal, Modal,
rem,
Skeleton, Skeleton,
useComputedColorScheme, useComputedColorScheme,
} from "@mantine/core"; } from "@mantine/core";
@ -70,6 +69,7 @@ import { useSettings } from "../../state/settingsSlice";
import MetricsExplorer from "./MetricsExplorer/MetricsExplorer"; import MetricsExplorer from "./MetricsExplorer/MetricsExplorer";
import ErrorBoundary from "../../components/ErrorBoundary"; import ErrorBoundary from "../../components/ErrorBoundary";
import { useAppSelector } from "../../state/hooks"; import { useAppSelector } from "../../state/hooks";
import { inputIconStyle, menuIconStyle } from "../../styles";
const promqlExtension = new PromQLExtension(); const promqlExtension = new PromQLExtension();
@ -224,25 +224,19 @@ const ExpressionInput: FC<ExpressionInputProps> = ({
color="gray" color="gray"
aria-label="Show query options" aria-label="Show query options"
> >
<IconDotsVertical style={{ width: "1rem", height: "1rem" }} /> <IconDotsVertical style={inputIconStyle} />
</ActionIcon> </ActionIcon>
</Menu.Target> </Menu.Target>
<Menu.Dropdown> <Menu.Dropdown>
<Menu.Label>Query options</Menu.Label> <Menu.Label>Query options</Menu.Label>
<Menu.Item <Menu.Item
leftSection={ leftSection={<IconSearch style={menuIconStyle} />}
<IconSearch style={{ width: rem(14), height: rem(14) }} />
}
onClick={() => setShowMetricsExplorer(true)} onClick={() => setShowMetricsExplorer(true)}
> >
Explore metrics Explore metrics
</Menu.Item> </Menu.Item>
<Menu.Item <Menu.Item
leftSection={ leftSection={<IconAlignJustified style={menuIconStyle} />}
<IconAlignJustified
style={{ width: rem(14), height: rem(14) }}
/>
}
onClick={() => formatQuery()} onClick={() => formatQuery()}
disabled={ disabled={
isFormatting || expr === "" || expr === formatResult?.data isFormatting || expr === "" || expr === formatResult?.data
@ -251,18 +245,14 @@ const ExpressionInput: FC<ExpressionInputProps> = ({
Format expression Format expression
</Menu.Item> </Menu.Item>
<Menu.Item <Menu.Item
leftSection={ leftSection={<IconBinaryTree style={menuIconStyle} />}
<IconBinaryTree style={{ width: rem(14), height: rem(14) }} />
}
onClick={() => setShowTree(!treeShown)} onClick={() => setShowTree(!treeShown)}
> >
{treeShown ? "Hide" : "Show"} tree view {treeShown ? "Hide" : "Show"} tree view
</Menu.Item> </Menu.Item>
<Menu.Item <Menu.Item
color="red" color="red"
leftSection={ leftSection={<IconTrash style={menuIconStyle} />}
<IconTrash style={{ width: rem(14), height: rem(14) }} />
}
onClick={removePanel} onClick={removePanel}
> >
Remove query Remove query

View file

@ -131,7 +131,7 @@ const Graph: FC<GraphProps> = ({
<Alert <Alert
color="red" color="red"
title="Error executing query" title="Error executing query"
icon={<IconAlertTriangle size={14} />} icon={<IconAlertTriangle />}
> >
{error.message} {error.message}
</Alert> </Alert>
@ -146,7 +146,7 @@ const Graph: FC<GraphProps> = ({
if (result.length === 0) { if (result.length === 0) {
return ( return (
<Alert title="Empty query result" icon={<IconInfoCircle size={14} />}> <Alert title="Empty query result" icon={<IconInfoCircle />}>
This query returned no data. This query returned no data.
</Alert> </Alert>
); );
@ -158,7 +158,7 @@ const Graph: FC<GraphProps> = ({
<Alert <Alert
color="orange" color="orange"
title="Graphing modified expression" title="Graphing modified expression"
icon={<IconAlertTriangle size={14} />} icon={<IconAlertTriangle />}
> >
<strong>Note:</strong> Range vector selectors can't be graphed, so <strong>Note:</strong> Range vector selectors can't be graphed, so
graphing the equivalent instant vector selector instead. graphing the equivalent instant vector selector instead.

View file

@ -37,6 +37,7 @@ import {
} from "@tabler/icons-react"; } from "@tabler/icons-react";
import { formatNode } from "../../../promql/format"; import { formatNode } from "../../../promql/format";
import classes from "./LabelsExplorer.module.css"; import classes from "./LabelsExplorer.module.css";
import { buttonIconStyle } from "../../../styles";
type LabelsExplorerProps = { type LabelsExplorerProps = {
metricName: string; metricName: string;
@ -150,7 +151,7 @@ const LabelsExplorer: FC<LabelsExplorerProps> = ({
<Alert <Alert
color="red" color="red"
title="Error querying series" title="Error querying series"
icon={<IconAlertTriangle size={14} />} icon={<IconAlertTriangle />}
> >
<strong>Error:</strong> {error.message} <strong>Error:</strong> {error.message}
</Alert> </Alert>
@ -177,7 +178,7 @@ const LabelsExplorer: FC<LabelsExplorerProps> = ({
variant="light" variant="light"
size="xs" size="xs"
onClick={() => insertText(serializeNode(selector))} onClick={() => insertText(serializeNode(selector))}
leftSection={<IconCodePlus size={18} />} leftSection={<IconCodePlus style={buttonIconStyle} />}
title="Insert selector at cursor and close explorer" title="Insert selector at cursor and close explorer"
> >
Insert Insert
@ -188,7 +189,11 @@ const LabelsExplorer: FC<LabelsExplorerProps> = ({
variant="light" variant="light"
size="xs" size="xs"
leftSection={ leftSection={
copied ? <IconCheck size={18} /> : <IconCopy size={18} /> copied ? (
<IconCheck style={buttonIconStyle} />
) : (
<IconCopy style={buttonIconStyle} />
)
} }
onClick={copy} onClick={copy}
title="Copy selector to clipboard" title="Copy selector to clipboard"
@ -228,7 +233,7 @@ const LabelsExplorer: FC<LabelsExplorerProps> = ({
variant="light" variant="light"
size="xs" size="xs"
onClick={hideLabelsExplorer} onClick={hideLabelsExplorer}
leftSection={<IconArrowLeft size={18} />} leftSection={<IconArrowLeft style={buttonIconStyle} />}
> >
Back to all metrics Back to all metrics
</Button> </Button>

View file

@ -1,4 +1,4 @@
import { Alert, Box, Button, Stack, rem } from "@mantine/core"; import { Alert, Box, Button, Stack } from "@mantine/core";
import { import {
IconAlertCircle, IconAlertCircle,
IconAlertTriangle, IconAlertTriangle,
@ -17,6 +17,7 @@ import { useEffect, useState } from "react";
import { InstantQueryResult } from "../../api/responseTypes/query"; import { InstantQueryResult } from "../../api/responseTypes/query";
import { humanizeDuration } from "../../lib/formatTime"; import { humanizeDuration } from "../../lib/formatTime";
import { decodePanelOptionsFromURLParams } from "./urlStateEncoding"; import { decodePanelOptionsFromURLParams } from "./urlStateEncoding";
import { buttonIconStyle } from "../../styles";
export default function QueryPage() { export default function QueryPage() {
const panels = useAppSelector((state) => state.queryPage.panels); const panels = useAppSelector((state) => state.queryPage.panels);
@ -80,9 +81,7 @@ export default function QueryPage() {
{metricNamesError && ( {metricNamesError && (
<Alert <Alert
mb="sm" mb="sm"
icon={ icon={<IconAlertTriangle />}
<IconAlertTriangle style={{ width: rem(14), height: rem(14) }} />
}
color="red" color="red"
title="Error fetching metrics list" title="Error fetching metrics list"
withCloseButton withCloseButton
@ -93,9 +92,7 @@ export default function QueryPage() {
{timeError && ( {timeError && (
<Alert <Alert
mb="sm" mb="sm"
icon={ icon={<IconAlertTriangle />}
<IconAlertTriangle style={{ width: rem(14), height: rem(14) }} />
}
color="red" color="red"
title="Error fetching server time" title="Error fetching server time"
withCloseButton withCloseButton
@ -108,7 +105,7 @@ export default function QueryPage() {
mb="sm" mb="sm"
title="Server time is out of sync" title="Server time is out of sync"
color="red" color="red"
icon={<IconAlertCircle style={{ width: rem(14), height: rem(14) }} />} icon={<IconAlertCircle />}
onClose={() => setTimeDelta(0)} onClose={() => setTimeDelta(0)}
> >
Detected a time difference of{" "} Detected a time difference of{" "}
@ -131,7 +128,7 @@ export default function QueryPage() {
<Button <Button
variant="light" variant="light"
mt="xl" mt="xl"
leftSection={<IconPlus size={18} />} leftSection={<IconPlus style={buttonIconStyle} />}
onClick={() => dispatch(addPanel())} onClick={() => dispatch(addPanel())}
> >
Add query Add query

View file

@ -80,7 +80,7 @@ const TableTab: FC<TableTabProps> = ({ panelIdx, retriggerIdx, expr }) => {
<Alert <Alert
color="red" color="red"
title="Error executing query" title="Error executing query"
icon={<IconAlertTriangle size={14} />} icon={<IconAlertTriangle />}
> >
{error.message} {error.message}
</Alert> </Alert>
@ -89,10 +89,7 @@ const TableTab: FC<TableTabProps> = ({ panelIdx, retriggerIdx, expr }) => {
) : ( ) : (
<> <>
{data.data.result.length === 0 && ( {data.data.result.length === 0 && (
<Alert <Alert title="Empty query result" icon={<IconInfoCircle />}>
title="Empty query result"
icon={<IconInfoCircle size={14} />}
>
This query returned no data. This query returned no data.
</Alert> </Alert>
)} )}
@ -102,7 +99,7 @@ const TableTab: FC<TableTabProps> = ({ panelIdx, retriggerIdx, expr }) => {
key={idx} key={idx}
color="red" color="red"
title="Query warning" title="Query warning"
icon={<IconAlertTriangle size={14} />} icon={<IconAlertTriangle />}
> >
{w} {w}
</Alert> </Alert>
@ -113,7 +110,7 @@ const TableTab: FC<TableTabProps> = ({ panelIdx, retriggerIdx, expr }) => {
key={idx} key={idx}
color="yellow" color="yellow"
title="Query notice" title="Query notice"
icon={<IconInfoCircle size={14} />} icon={<IconInfoCircle />}
> >
{w} {w}
</Alert> </Alert>

View file

@ -4,7 +4,6 @@ import {
useEffect, useEffect,
useLayoutEffect, useLayoutEffect,
useMemo, useMemo,
useRef,
useState, useState,
} from "react"; } from "react";
import ASTNode, { nodeType } from "../../promql/ast"; import ASTNode, { nodeType } from "../../promql/ast";
@ -17,6 +16,7 @@ import {
Group, Group,
List, List,
Loader, Loader,
rem,
Text, Text,
Tooltip, Tooltip,
} from "@mantine/core"; } from "@mantine/core";
@ -37,6 +37,8 @@ const nodeIndent = 20;
const maxLabelNames = 10; const maxLabelNames = 10;
const maxLabelValues = 10; const maxLabelValues = 10;
const nodeIndicatorIconStyle = { width: rem(18), height: rem(18) };
type NodeState = "waiting" | "running" | "error" | "success"; type NodeState = "waiting" | "running" | "error" | "success";
const mergeChildStates = (states: NodeState[]): NodeState => { const mergeChildStates = (states: NodeState[]): NodeState => {
@ -57,7 +59,7 @@ const TreeNode: FC<{
node: ASTNode; node: ASTNode;
selectedNode: { id: string; node: ASTNode } | null; selectedNode: { id: string; node: ASTNode } | null;
setSelectedNode: (Node: { id: string; node: ASTNode } | null) => void; setSelectedNode: (Node: { id: string; node: ASTNode } | null) => void;
parentRef?: React.RefObject<HTMLDivElement>; parentEl?: HTMLDivElement | null;
reportNodeState?: (childIdx: number, state: NodeState) => void; reportNodeState?: (childIdx: number, state: NodeState) => void;
reverse: boolean; reverse: boolean;
// The index of this node in its parent's children. // The index of this node in its parent's children.
@ -66,13 +68,21 @@ const TreeNode: FC<{
node, node,
selectedNode, selectedNode,
setSelectedNode, setSelectedNode,
parentRef, parentEl,
reportNodeState, reportNodeState,
reverse, reverse,
childIdx, childIdx,
}) => { }) => {
const nodeID = useId(); const nodeID = useId();
const nodeRef = useRef<HTMLDivElement>(null);
// A normal ref won't work properly here because the ref's `current` property
// going from `null` to defined won't trigger a re-render of the child
// component, since it's not a React state update. So we manually have to
// create a state update using a callback ref. See also
// https://tkdodo.eu/blog/avoiding-use-effect-with-callback-refs
const [nodeEl, setNodeEl] = useState<HTMLDivElement | null>(null);
const nodeRef = useCallback((node: HTMLDivElement) => setNodeEl(node), []);
const [connectorStyle, setConnectorStyle] = useState<CSSProperties>({ const [connectorStyle, setConnectorStyle] = useState<CSSProperties>({
borderColor: borderColor:
"light-dark(var(--mantine-color-gray-4), var(--mantine-color-dark-3))", "light-dark(var(--mantine-color-gray-4), var(--mantine-color-dark-3))",
@ -94,10 +104,10 @@ const TreeNode: FC<{
// Select the node when it is mounted and it is the root of the tree. // Select the node when it is mounted and it is the root of the tree.
useEffect(() => { useEffect(() => {
if (parentRef === undefined) { if (parentEl === undefined) {
setSelectedNode({ id: nodeID, node: node }); setSelectedNode({ id: nodeID, node: node });
} }
}, [parentRef, setSelectedNode, nodeID, node]); }, [parentEl, setSelectedNode, nodeID, node]);
// Deselect node when node is unmounted. // Deselect node when node is unmounted.
useEffect(() => { useEffect(() => {
@ -170,16 +180,18 @@ const TreeNode: FC<{
// Update the size and position of tree connector lines based on the node's and its parent's position. // Update the size and position of tree connector lines based on the node's and its parent's position.
useLayoutEffect(() => { useLayoutEffect(() => {
if (parentRef === undefined) { if (parentEl === undefined) {
// We're the root node. // We're the root node.
return; return;
} }
if (parentRef.current === null || nodeRef.current === null) { if (parentEl === null || nodeEl === null) {
// Either of the two connected nodes hasn't been rendered yet.
return; return;
} }
const parentRect = parentRef.current.getBoundingClientRect();
const nodeRect = nodeRef.current.getBoundingClientRect(); const parentRect = parentEl.getBoundingClientRect();
const nodeRect = nodeEl.getBoundingClientRect();
if (reverse) { if (reverse) {
setConnectorStyle((prevStyle) => ({ setConnectorStyle((prevStyle) => ({
...prevStyle, ...prevStyle,
@ -199,7 +211,7 @@ const TreeNode: FC<{
borderTopLeftRadius: undefined, borderTopLeftRadius: undefined,
})); }));
} }
}, [parentRef, reverse, nodeRef, setConnectorStyle]); }, [parentEl, nodeEl, reverse, nodeRef, setConnectorStyle]);
// Update the node info state based on the query result. // Update the node info state based on the query result.
useEffect(() => { useEffect(() => {
@ -261,7 +273,7 @@ const TreeNode: FC<{
pos="relative" pos="relative"
align="center" align="center"
> >
{parentRef && ( {parentEl !== undefined && (
// Connector line between this node and its parent. // Connector line between this node and its parent.
<Box pos="absolute" display="inline-block" style={connectorStyle} /> <Box pos="absolute" display="inline-block" style={connectorStyle} />
)} )}
@ -288,13 +300,14 @@ const TreeNode: FC<{
</Box> </Box>
{mergedChildState === "waiting" ? ( {mergedChildState === "waiting" ? (
<Group c="gray"> <Group c="gray">
<IconPointFilled size={18} /> <IconPointFilled style={nodeIndicatorIconStyle} />
</Group> </Group>
) : mergedChildState === "running" ? ( ) : mergedChildState === "running" ? (
<Loader size={14} color="gray" type="dots" /> <Loader size={14} color="gray" type="dots" />
) : mergedChildState === "error" ? ( ) : mergedChildState === "error" ? (
<Group c="orange.7" gap={5} fz="xs" wrap="nowrap"> <Group c="orange.7" gap={5} fz="xs" wrap="nowrap">
<IconPointFilled size={18} /> Blocked on child query error <IconPointFilled style={nodeIndicatorIconStyle} /> Blocked on child
query error
</Group> </Group>
) : isFetching ? ( ) : isFetching ? (
<Loader size={14} color="gray" /> <Loader size={14} color="gray" />
@ -305,7 +318,7 @@ const TreeNode: FC<{
style={{ flexShrink: 0 }} style={{ flexShrink: 0 }}
className={classes.errorText} className={classes.errorText}
> >
<IconPointFilled size={18} /> <IconPointFilled style={nodeIndicatorIconStyle} />
<Text fz="xs"> <Text fz="xs">
<strong>Error executing query:</strong> {error.message} <strong>Error executing query:</strong> {error.message}
</Text> </Text>
@ -387,7 +400,7 @@ const TreeNode: FC<{
node={children[0]} node={children[0]}
selectedNode={selectedNode} selectedNode={selectedNode}
setSelectedNode={setSelectedNode} setSelectedNode={setSelectedNode}
parentRef={nodeRef} parentEl={nodeEl}
reverse={true} reverse={true}
childIdx={0} childIdx={0}
reportNodeState={childReportNodeState} reportNodeState={childReportNodeState}
@ -399,7 +412,7 @@ const TreeNode: FC<{
node={children[1]} node={children[1]}
selectedNode={selectedNode} selectedNode={selectedNode}
setSelectedNode={setSelectedNode} setSelectedNode={setSelectedNode}
parentRef={nodeRef} parentEl={nodeEl}
reverse={false} reverse={false}
childIdx={1} childIdx={1}
reportNodeState={childReportNodeState} reportNodeState={childReportNodeState}
@ -418,7 +431,7 @@ const TreeNode: FC<{
node={child} node={child}
selectedNode={selectedNode} selectedNode={selectedNode}
setSelectedNode={setSelectedNode} setSelectedNode={setSelectedNode}
parentRef={nodeRef} parentEl={nodeEl}
reverse={false} reverse={false}
childIdx={idx} childIdx={idx}
reportNodeState={childReportNodeState} reportNodeState={childReportNodeState}

View file

@ -30,6 +30,7 @@ import {
} from "../../state/serviceDiscoveryPageSlice"; } from "../../state/serviceDiscoveryPageSlice";
import { StateMultiSelect } from "../../components/StateMultiSelect"; import { StateMultiSelect } from "../../components/StateMultiSelect";
import badgeClasses from "../../Badge.module.css"; import badgeClasses from "../../Badge.module.css";
import { expandIconStyle, inputIconStyle } from "../../styles";
export const targetPoolDisplayLimit = 20; export const targetPoolDisplayLimit = 20;
@ -98,7 +99,7 @@ export default function ServiceDiscoveryPage() {
/> />
<TextInput <TextInput
flex={1} flex={1}
leftSection={<IconSearch size={14} />} leftSection={<IconSearch style={inputIconStyle} />}
placeholder="Filter by labels" placeholder="Filter by labels"
value={searchFilter || ""} value={searchFilter || ""}
onChange={(event) => setSearchFilter(event.currentTarget.value)} onChange={(event) => setSearchFilter(event.currentTarget.value)}
@ -118,9 +119,9 @@ export default function ServiceDiscoveryPage() {
} }
> >
{collapsedPools.length > 0 ? ( {collapsedPools.length > 0 ? (
<IconLayoutNavbarExpand size={16} /> <IconLayoutNavbarExpand style={expandIconStyle} />
) : ( ) : (
<IconLayoutNavbarCollapse size={16} /> <IconLayoutNavbarCollapse style={expandIconStyle} />
)} )}
</ActionIcon> </ActionIcon>
</Group> </Group>

View file

@ -204,10 +204,7 @@ const ScrapePoolList: FC<ScrapePoolListProp> = ({
return ( return (
<Stack> <Stack>
{allPoolNames.length === 0 ? ( {allPoolNames.length === 0 ? (
<Alert <Alert title="No scrape pools found" icon={<IconInfoCircle />}>
title="No scrape pools found"
icon={<IconInfoCircle size={14} />}
>
No scrape pools found. No scrape pools found.
</Alert> </Alert>
) : ( ) : (
@ -215,7 +212,7 @@ const ScrapePoolList: FC<ScrapePoolListProp> = ({
allPoolNames.length !== shownPoolNames.length && ( allPoolNames.length !== shownPoolNames.length && (
<Alert <Alert
title="Hiding pools with no matching targets" title="Hiding pools with no matching targets"
icon={<IconInfoCircle size={14} />} icon={<IconInfoCircle />}
> >
Hiding {allPoolNames.length - shownPoolNames.length} empty pools due Hiding {allPoolNames.length - shownPoolNames.length} empty pools due
to filters or no targets. to filters or no targets.
@ -228,7 +225,7 @@ const ScrapePoolList: FC<ScrapePoolListProp> = ({
{showLimitAlert && ( {showLimitAlert && (
<Alert <Alert
title="Found many pools, showing only one" title="Found many pools, showing only one"
icon={<IconInfoCircle size={14} />} icon={<IconInfoCircle />}
withCloseButton withCloseButton
onClose={() => dispatch(setShowLimitAlert(false))} onClose={() => dispatch(setShowLimitAlert(false))}
> >

View file

@ -39,6 +39,7 @@ import TargetLabels from "./TargetLabels";
import { useDebouncedValue } from "@mantine/hooks"; import { useDebouncedValue } from "@mantine/hooks";
import { targetPoolDisplayLimit } from "./TargetsPage"; import { targetPoolDisplayLimit } from "./TargetsPage";
import { BooleanParam, useQueryParam, withDefault } from "use-query-params"; import { BooleanParam, useQueryParam, withDefault } from "use-query-params";
import { badgeIconStyle } from "../../styles";
type ScrapePool = { type ScrapePool = {
targets: Target[]; targets: Target[];
@ -53,7 +54,7 @@ type ScrapePools = {
}; };
const poolPanelHealthClass = (pool: ScrapePool) => const poolPanelHealthClass = (pool: ScrapePool) =>
pool.count > 0 && pool.downCount === pool.count pool.count > 1 && pool.downCount === pool.count
? panelClasses.panelHealthErr ? panelClasses.panelHealthErr
: pool.downCount >= 1 : pool.downCount >= 1
? panelClasses.panelHealthWarn ? panelClasses.panelHealthWarn
@ -194,10 +195,7 @@ const ScrapePoolList: FC<ScrapePoolListProp> = ({
return ( return (
<Stack> <Stack>
{allPoolNames.length === 0 ? ( {allPoolNames.length === 0 ? (
<Alert <Alert title="No scrape pools found" icon={<IconInfoCircle />}>
title="No scrape pools found"
icon={<IconInfoCircle size={14} />}
>
No scrape pools found. No scrape pools found.
</Alert> </Alert>
) : ( ) : (
@ -205,7 +203,7 @@ const ScrapePoolList: FC<ScrapePoolListProp> = ({
allPoolNames.length !== shownPoolNames.length && ( allPoolNames.length !== shownPoolNames.length && (
<Alert <Alert
title="Hiding pools with no matching targets" title="Hiding pools with no matching targets"
icon={<IconInfoCircle size={14} />} icon={<IconInfoCircle />}
> >
Hiding {allPoolNames.length - shownPoolNames.length} empty pools due Hiding {allPoolNames.length - shownPoolNames.length} empty pools due
to filters or no targets. to filters or no targets.
@ -218,7 +216,7 @@ const ScrapePoolList: FC<ScrapePoolListProp> = ({
{showLimitAlert && ( {showLimitAlert && (
<Alert <Alert
title="Found many pools, showing only one" title="Found many pools, showing only one"
icon={<IconInfoCircle size={14} />} icon={<IconInfoCircle />}
withCloseButton withCloseButton
onClose={() => dispatch(setShowLimitAlert(false))} onClose={() => dispatch(setShowLimitAlert(false))}
> >
@ -355,7 +353,9 @@ const ScrapePoolList: FC<ScrapePoolListProp> = ({
styles={{ styles={{
label: { textTransform: "none" }, label: { textTransform: "none" },
}} }}
leftSection={<IconRefresh size={12} />} leftSection={
<IconRefresh style={badgeIconStyle} />
}
> >
{humanizeDurationRelative( {humanizeDurationRelative(
target.lastScrape, target.lastScrape,
@ -376,7 +376,9 @@ const ScrapePoolList: FC<ScrapePoolListProp> = ({
label: { textTransform: "none" }, label: { textTransform: "none" },
}} }}
leftSection={ leftSection={
<IconHourglass size={12} /> <IconHourglass
style={badgeIconStyle}
/>
} }
> >
{humanizeDuration( {humanizeDuration(
@ -401,7 +403,7 @@ const ScrapePoolList: FC<ScrapePoolListProp> = ({
<Alert <Alert
color="red" color="red"
mb="sm" mb="sm"
icon={<IconAlertTriangle size={14} />} icon={<IconAlertTriangle />}
> >
<strong>Error scraping target:</strong>{" "} <strong>Error scraping target:</strong>{" "}
{target.lastError} {target.lastError}

View file

@ -4,6 +4,7 @@ import { LabelBadges } from "../../components/LabelBadges";
import { ActionIcon, Collapse, Group, Stack, Text } from "@mantine/core"; import { ActionIcon, Collapse, Group, Stack, Text } from "@mantine/core";
import { useDisclosure } from "@mantine/hooks"; import { useDisclosure } from "@mantine/hooks";
import { IconChevronDown, IconChevronUp } from "@tabler/icons-react"; import { IconChevronDown, IconChevronUp } from "@tabler/icons-react";
import { actionIconStyle } from "../../styles";
type TargetLabelsProps = { type TargetLabelsProps = {
labels: Labels; labels: Labels;
@ -26,12 +27,9 @@ const TargetLabels: FC<TargetLabelsProps> = ({ discoveredLabels, labels }) => {
title={`${showDiscovered ? "Hide" : "Show"} discovered (pre-relabeling) labels`} title={`${showDiscovered ? "Hide" : "Show"} discovered (pre-relabeling) labels`}
> >
{showDiscovered ? ( {showDiscovered ? (
<IconChevronUp <IconChevronUp style={actionIconStyle} />
style={{ width: "70%", height: "70%" }}
stroke={1.5}
/>
) : ( ) : (
<IconChevronDown style={{ width: "70%", height: "70%" }} /> <IconChevronDown style={actionIconStyle} />
)} )}
</ActionIcon> </ActionIcon>
</Group> </Group>

View file

@ -29,6 +29,7 @@ import ErrorBoundary from "../../components/ErrorBoundary";
import ScrapePoolList from "./ScrapePoolsList"; import ScrapePoolList from "./ScrapePoolsList";
import { useSuspenseAPIQuery } from "../../api/api"; import { useSuspenseAPIQuery } from "../../api/api";
import { ScrapePoolsResult } from "../../api/responseTypes/scrapePools"; import { ScrapePoolsResult } from "../../api/responseTypes/scrapePools";
import { expandIconStyle, inputIconStyle } from "../../styles";
export const targetPoolDisplayLimit = 20; export const targetPoolDisplayLimit = 20;
@ -101,7 +102,7 @@ export default function TargetsPage() {
/> />
<TextInput <TextInput
flex={1} flex={1}
leftSection={<IconSearch size={14} />} leftSection={<IconSearch style={inputIconStyle} />}
placeholder="Filter by endpoint or labels" placeholder="Filter by endpoint or labels"
value={searchFilter || ""} value={searchFilter || ""}
onChange={(event) => onChange={(event) =>
@ -123,9 +124,9 @@ export default function TargetsPage() {
} }
> >
{collapsedPools.length > 0 ? ( {collapsedPools.length > 0 ? (
<IconLayoutNavbarExpand size={16} /> <IconLayoutNavbarExpand style={expandIconStyle} />
) : ( ) : (
<IconLayoutNavbarCollapse size={16} /> <IconLayoutNavbarCollapse style={expandIconStyle} />
)} )}
</ActionIcon> </ActionIcon>
</Group> </Group>

View file

@ -0,0 +1,15 @@
import { em, rem } from "@mantine/core";
export const navIconStyle = { width: rem(16), height: rem(16) };
export const menuIconStyle = { width: rem(14), height: rem(14) };
export const badgeIconStyle = { width: em(17), height: em(17) };
export const actionIconStyle = { width: "70%", height: "70%" };
export const inputIconStyle = { width: em(16), height: em(16) };
export const buttonIconStyle = { width: em(20), height: em(20) };
export const infoPageCardTitleIconStyle = { width: em(17.5), height: em(17.5) };
export const expandIconStyle = { width: em(16), height: em(16) };
export const themeSwitcherIconStyle = {
width: rem(20),
height: rem(20),
display: "block",
};