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/notifications/styles.css";
import "@mantine/dates/styles.css";
import "./mantine-overrides.css";
import classes from "./App.module.css";
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 ServiceDiscoveryPage from "./pages/service-discovery/ServiceDiscoveryPage";
import AlertmanagerDiscoveryPage from "./pages/AlertmanagerDiscoveryPage";
import { actionIconStyle, navIconStyle } from "./styles";
const queryClient = new QueryClient();
const navIconStyle = { width: rem(16), height: rem(16) };
const mainNavPages = [
{
title: "Query",
@ -322,9 +322,9 @@ function App() {
color="gray"
title="Documentation"
aria-label="Documentation"
size={32}
size={rem(32)}
>
<IconBook size={20} />
<IconBook style={actionIconStyle} />
</ActionIcon>
</>
);

View file

@ -32,7 +32,7 @@ class ErrorBoundary extends Component<Props, State> {
<Alert
color="red"
title={this.props.title || "Error querying page data"}
icon={<IconAlertTriangle size={14} />}
icon={<IconAlertTriangle />}
maw={500}
mx="auto"
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,
Card,
Group,
rem,
Table,
Tooltip,
useComputedColorScheme,
@ -25,6 +24,7 @@ import {
import { PromQLExtension } from "@prometheus-io/codemirror-promql";
import { LabelBadges } from "./LabelBadges";
import { useSettings } from "../state/settingsSlice";
import { actionIconStyle, badgeIconStyle } from "../styles";
const promqlExtension = new PromQLExtension();
@ -64,7 +64,7 @@ const RuleDefinition: FC<{ rule: Rule }> = ({ rule }) => {
}}
className={codeboxClasses.queryButton}
>
<IconSearch style={{ width: rem(14) }} />
<IconSearch style={actionIconStyle} />
</ActionIcon>
</Tooltip>
</Card>
@ -74,7 +74,7 @@ const RuleDefinition: FC<{ rule: Rule }> = ({ rule }) => {
<Badge
variant="light"
styles={{ label: { textTransform: "none" } }}
leftSection={<IconClockPause size={12} />}
leftSection={<IconClockPause style={badgeIconStyle} />}
>
for: {formatPrometheusDuration(rule.duration * 1000)}
</Badge>
@ -83,7 +83,7 @@ const RuleDefinition: FC<{ rule: Rule }> = ({ rule }) => {
<Badge
variant="light"
styles={{ label: { textTransform: "none" } }}
leftSection={<IconClockPlay size={12} />}
leftSection={<IconClockPlay style={badgeIconStyle} />}
>
keep_firing_for: {formatPrometheusDuration(rule.duration * 1000)}
</Badge>

View file

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

View file

@ -10,6 +10,7 @@ import {
useCombobox,
} from "@mantine/core";
import { IconHeartRateMonitor } from "@tabler/icons-react";
import { inputIconStyle } from "../styles";
interface StatePillProps extends React.ComponentPropsWithoutRef<"div"> {
value: string;
@ -80,7 +81,7 @@ export const StateMultiSelect: FC<StateMultiSelectProps> = ({
pointer
onClick={() => combobox.toggleDropdown()}
miw={200}
leftSection={<IconHeartRateMonitor size={14} />}
leftSection={<IconHeartRateMonitor style={inputIconStyle} />}
rightSection={
values.length > 0 ? (
<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 { FC } from "react";
import InfoPageStack from "../components/InfoPageStack";
import InfoPageCard from "../components/InfoPageCard";
const AgentPage: FC = () => {
return (
<Card shadow="xs" withBorder p="md" mt="xs">
<Group wrap="nowrap" align="center" ml="xs" mb="sm" gap="xs">
<IconSpy size={22} />
<Text fz="xl" fw={600}>
Prometheus Agent
</Text>
</Group>
<InfoPageStack>
<InfoPageCard
title="Prometheus Agent"
icon={IconSpy}
>
<Text p="md">
This Prometheus instance is running in <strong>agent mode</strong>. In
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
alerting.
</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 { useSuspenseAPIQuery } from "../api/api";
import { AlertmanagersResult } from "../api/responseTypes/alertmanagers";
import EndpointLink from "../components/EndpointLink";
import InfoPageCard from "../components/InfoPageCard";
import InfoPageStack from "../components/InfoPageStack";
export const targetPoolDisplayLimit = 20;
@ -18,14 +20,8 @@ export default function AlertmanagerDiscoveryPage() {
});
return (
<Stack gap="lg" maw={1000} mx="auto" mt="xs">
<Card shadow="xs" withBorder p="md">
<Group wrap="nowrap" align="center" ml="xs" mb="sm" gap="xs">
<IconBell size={22} />
<Text fz="xl" fw={600}>
Active Alertmanagers
</Text>
</Group>
<InfoPageStack>
<InfoPageCard title="Active Alertmanagers" icon={IconBell}>
{activeAlertmanagers.length === 0 ? (
<Alert title="No active alertmanagers" icon={<IconInfoCircle />}>
No active alertmanagers found.
@ -46,14 +42,8 @@ export default function AlertmanagerDiscoveryPage() {
</Table.Tbody>
</Table>
)}
</Card>
<Card shadow="xs" withBorder p="md">
<Group wrap="nowrap" align="center" ml="xs" mb="sm" gap="xs">
<IconBellOff size={22} />
<Text fz="xl" fw={600}>
Dropped Alertmanagers
</Text>
</Group>
</InfoPageCard>
<InfoPageCard title="Dropped Alertmanagers" icon={IconBellOff}>
{droppedAlertmanagers.length === 0 ? (
<Alert title="No dropped alertmanagers" icon={<IconInfoCircle />}>
No dropped alertmanagers found.
@ -74,7 +64,7 @@ export default function AlertmanagerDiscoveryPage() {
</Table.Tbody>
</Table>
)}
</Card>
</Stack>
</InfoPageCard>
</InfoPageStack>
);
}

View file

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

View file

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

View file

@ -27,6 +27,7 @@ import { useSuspenseAPIQuery } from "../api/api";
import { RulesResult } from "../api/responseTypes/rules";
import badgeClasses from "../Badge.module.css";
import RuleDefinition from "../components/RuleDefinition";
import { badgeIconStyle } from "../styles";
const healthBadgeClass = (state: string) => {
switch (state) {
@ -47,7 +48,7 @@ export default function RulesPage() {
return (
<Stack mt="xs">
{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.
</Alert>
)}
@ -74,7 +75,7 @@ export default function RulesPage() {
variant="light"
className={badgeClasses.statsBadge}
styles={{ label: { textTransform: "none" } }}
leftSection={<IconRefresh size={12} />}
leftSection={<IconRefresh style={badgeIconStyle} />}
>
last run {humanizeDurationRelative(g.lastEvaluation, now())}
</Badge>
@ -84,7 +85,7 @@ export default function RulesPage() {
variant="light"
className={badgeClasses.statsBadge}
styles={{ label: { textTransform: "none" } }}
leftSection={<IconHourglass size={12} />}
leftSection={<IconHourglass style={badgeIconStyle} />}
>
took {humanizeDuration(parseFloat(g.evaluationTime) * 1000)}
</Badge>
@ -94,7 +95,7 @@ export default function RulesPage() {
variant="transparent"
className={badgeClasses.statsBadge}
styles={{ label: { textTransform: "none" } }}
leftSection={<IconRepeat size={12} />}
leftSection={<IconRepeat style={badgeIconStyle} />}
>
every {humanizeDuration(parseFloat(g.interval) * 1000)}{" "}
</Badge>
@ -102,7 +103,7 @@ export default function RulesPage() {
</Group>
</Group>
{g.rules.length === 0 && (
<Alert title="No rules" icon={<IconInfoCircle size={14} />}>
<Alert title="No rules" icon={<IconInfoCircle />}>
No rules in rule group.
</Alert>
)}
@ -150,7 +151,7 @@ export default function RulesPage() {
variant="light"
className={badgeClasses.statsBadge}
styles={{ label: { textTransform: "none" } }}
leftSection={<IconRefresh size={12} />}
leftSection={<IconRefresh style={badgeIconStyle} />}
>
{humanizeDurationRelative(r.lastEvaluation, now())}
</Badge>
@ -164,7 +165,9 @@ export default function RulesPage() {
variant="light"
className={badgeClasses.statsBadge}
styles={{ label: { textTransform: "none" } }}
leftSection={<IconHourglass size={12} />}
leftSection={
<IconHourglass style={badgeIconStyle} />
}
>
{humanizeDuration(
parseFloat(r.evaluationTime) * 1000
@ -185,7 +188,7 @@ export default function RulesPage() {
color="red"
mt="sm"
title="Rule failed to evaluate"
icon={<IconAlertTriangle size={14} />}
icon={<IconAlertTriangle />}
>
<strong>Error:</strong> {r.lastError}
</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 { IconRun, IconWall } from "@tabler/icons-react";
import { formatTimestamp } from "../lib/formatTime";
import { useSettings } from "../state/settingsSlice";
import InfoPageCard from "../components/InfoPageCard";
import InfoPageStack from "../components/InfoPageStack";
export default function StatusPage() {
const { data: buildinfo } = useSuspenseAPIQuery<Record<string, string>>({
@ -42,14 +44,8 @@ export default function StatusPage() {
};
return (
<Stack gap="lg" maw={1000} mx="auto" mt="xs">
<Card shadow="xs" withBorder p="md">
<Group wrap="nowrap" align="center" ml="xs" mb="sm" gap="xs">
<IconWall size={22} />
<Text fz="xl" fw={600}>
Build information
</Text>
</Group>
<InfoPageStack>
<InfoPageCard title="Build information" icon={IconWall}>
<Table layout="fixed">
<Table.Tbody>
{Object.entries(buildinfo.data).map(([k, v]) => (
@ -60,14 +56,8 @@ export default function StatusPage() {
))}
</Table.Tbody>
</Table>
</Card>
<Card shadow="xs" withBorder p="md">
<Group wrap="nowrap" align="center" ml="xs" mb="sm" gap="xs">
<IconRun size={22} />
<Text fz="xl" fw={600}>
Runtime information
</Text>
</Group>
</InfoPageCard>
<InfoPageCard title="Runtime information" icon={IconRun}>
<Table layout="fixed">
<Table.Tbody>
{Object.entries(runtimeinfo.data).map(([k, v]) => {
@ -84,7 +74,7 @@ export default function StatusPage() {
})}
</Table.Tbody>
</Table>
</Card>
</Stack>
</InfoPageCard>
</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 { TSDBStatusResult } from "../api/responseTypes/tsdbStatus";
import { formatTimestamp } from "../lib/formatTime";
import { useSettings } from "../state/settingsSlice";
import InfoPageStack from "../components/InfoPageStack";
import InfoPageCard from "../components/InfoPageCard";
export default function TSDBStatusPage() {
const {
@ -41,7 +43,7 @@ export default function TSDBStatusPage() {
];
return (
<Stack gap="lg" maw={1000} mx="auto" mt="xs">
<InfoPageStack>
{[
{
title: "TSDB Head Status",
@ -70,10 +72,7 @@ export default function TSDBStatusPage() {
formatAsCode: true,
},
].map(({ title, unit = "Count", stats, formatAsCode }) => (
<Card shadow="xs" withBorder p="md">
<Text fz="xl" fw={600} ml="xs" mb="sm">
{title}
</Text>
<InfoPageCard title={title}>
<Table layout="fixed">
<Table.Thead>
<Table.Tr>
@ -82,8 +81,7 @@ export default function TSDBStatusPage() {
</Table.Tr>
</Table.Thead>
<Table.Tbody>
{stats.map(({ name, value }) => {
return (
{stats.map(({ name, value }) => (
<Table.Tr key={name}>
<Table.Td
style={{
@ -94,12 +92,11 @@ export default function TSDBStatusPage() {
</Table.Td>
<Table.Td>{value}</Table.Td>
</Table.Tr>
);
})}
))}
</Table.Tbody>
</Table>
</Card>
</InfoPageCard>
))}
</Stack>
</InfoPageStack>
);
}

View file

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

View file

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

View file

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

View file

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

View file

@ -37,6 +37,7 @@ import {
} from "@tabler/icons-react";
import { formatNode } from "../../../promql/format";
import classes from "./LabelsExplorer.module.css";
import { buttonIconStyle } from "../../../styles";
type LabelsExplorerProps = {
metricName: string;
@ -150,7 +151,7 @@ const LabelsExplorer: FC<LabelsExplorerProps> = ({
<Alert
color="red"
title="Error querying series"
icon={<IconAlertTriangle size={14} />}
icon={<IconAlertTriangle />}
>
<strong>Error:</strong> {error.message}
</Alert>
@ -177,7 +178,7 @@ const LabelsExplorer: FC<LabelsExplorerProps> = ({
variant="light"
size="xs"
onClick={() => insertText(serializeNode(selector))}
leftSection={<IconCodePlus size={18} />}
leftSection={<IconCodePlus style={buttonIconStyle} />}
title="Insert selector at cursor and close explorer"
>
Insert
@ -188,7 +189,11 @@ const LabelsExplorer: FC<LabelsExplorerProps> = ({
variant="light"
size="xs"
leftSection={
copied ? <IconCheck size={18} /> : <IconCopy size={18} />
copied ? (
<IconCheck style={buttonIconStyle} />
) : (
<IconCopy style={buttonIconStyle} />
)
}
onClick={copy}
title="Copy selector to clipboard"
@ -228,7 +233,7 @@ const LabelsExplorer: FC<LabelsExplorerProps> = ({
variant="light"
size="xs"
onClick={hideLabelsExplorer}
leftSection={<IconArrowLeft size={18} />}
leftSection={<IconArrowLeft style={buttonIconStyle} />}
>
Back to all metrics
</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 {
IconAlertCircle,
IconAlertTriangle,
@ -17,6 +17,7 @@ import { useEffect, useState } from "react";
import { InstantQueryResult } from "../../api/responseTypes/query";
import { humanizeDuration } from "../../lib/formatTime";
import { decodePanelOptionsFromURLParams } from "./urlStateEncoding";
import { buttonIconStyle } from "../../styles";
export default function QueryPage() {
const panels = useAppSelector((state) => state.queryPage.panels);
@ -80,9 +81,7 @@ export default function QueryPage() {
{metricNamesError && (
<Alert
mb="sm"
icon={
<IconAlertTriangle style={{ width: rem(14), height: rem(14) }} />
}
icon={<IconAlertTriangle />}
color="red"
title="Error fetching metrics list"
withCloseButton
@ -93,9 +92,7 @@ export default function QueryPage() {
{timeError && (
<Alert
mb="sm"
icon={
<IconAlertTriangle style={{ width: rem(14), height: rem(14) }} />
}
icon={<IconAlertTriangle />}
color="red"
title="Error fetching server time"
withCloseButton
@ -108,7 +105,7 @@ export default function QueryPage() {
mb="sm"
title="Server time is out of sync"
color="red"
icon={<IconAlertCircle style={{ width: rem(14), height: rem(14) }} />}
icon={<IconAlertCircle />}
onClose={() => setTimeDelta(0)}
>
Detected a time difference of{" "}
@ -131,7 +128,7 @@ export default function QueryPage() {
<Button
variant="light"
mt="xl"
leftSection={<IconPlus size={18} />}
leftSection={<IconPlus style={buttonIconStyle} />}
onClick={() => dispatch(addPanel())}
>
Add query

View file

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

View file

@ -4,7 +4,6 @@ import {
useEffect,
useLayoutEffect,
useMemo,
useRef,
useState,
} from "react";
import ASTNode, { nodeType } from "../../promql/ast";
@ -17,6 +16,7 @@ import {
Group,
List,
Loader,
rem,
Text,
Tooltip,
} from "@mantine/core";
@ -37,6 +37,8 @@ const nodeIndent = 20;
const maxLabelNames = 10;
const maxLabelValues = 10;
const nodeIndicatorIconStyle = { width: rem(18), height: rem(18) };
type NodeState = "waiting" | "running" | "error" | "success";
const mergeChildStates = (states: NodeState[]): NodeState => {
@ -57,7 +59,7 @@ const TreeNode: FC<{
node: ASTNode;
selectedNode: { id: string; node: ASTNode } | null;
setSelectedNode: (Node: { id: string; node: ASTNode } | null) => void;
parentRef?: React.RefObject<HTMLDivElement>;
parentEl?: HTMLDivElement | null;
reportNodeState?: (childIdx: number, state: NodeState) => void;
reverse: boolean;
// The index of this node in its parent's children.
@ -66,13 +68,21 @@ const TreeNode: FC<{
node,
selectedNode,
setSelectedNode,
parentRef,
parentEl,
reportNodeState,
reverse,
childIdx,
}) => {
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>({
borderColor:
"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.
useEffect(() => {
if (parentRef === undefined) {
if (parentEl === undefined) {
setSelectedNode({ id: nodeID, node: node });
}
}, [parentRef, setSelectedNode, nodeID, node]);
}, [parentEl, setSelectedNode, nodeID, node]);
// Deselect node when node is unmounted.
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.
useLayoutEffect(() => {
if (parentRef === undefined) {
if (parentEl === undefined) {
// We're the root node.
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;
}
const parentRect = parentRef.current.getBoundingClientRect();
const nodeRect = nodeRef.current.getBoundingClientRect();
const parentRect = parentEl.getBoundingClientRect();
const nodeRect = nodeEl.getBoundingClientRect();
if (reverse) {
setConnectorStyle((prevStyle) => ({
...prevStyle,
@ -199,7 +211,7 @@ const TreeNode: FC<{
borderTopLeftRadius: undefined,
}));
}
}, [parentRef, reverse, nodeRef, setConnectorStyle]);
}, [parentEl, nodeEl, reverse, nodeRef, setConnectorStyle]);
// Update the node info state based on the query result.
useEffect(() => {
@ -261,7 +273,7 @@ const TreeNode: FC<{
pos="relative"
align="center"
>
{parentRef && (
{parentEl !== undefined && (
// Connector line between this node and its parent.
<Box pos="absolute" display="inline-block" style={connectorStyle} />
)}
@ -288,13 +300,14 @@ const TreeNode: FC<{
</Box>
{mergedChildState === "waiting" ? (
<Group c="gray">
<IconPointFilled size={18} />
<IconPointFilled style={nodeIndicatorIconStyle} />
</Group>
) : mergedChildState === "running" ? (
<Loader size={14} color="gray" type="dots" />
) : mergedChildState === "error" ? (
<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>
) : isFetching ? (
<Loader size={14} color="gray" />
@ -305,7 +318,7 @@ const TreeNode: FC<{
style={{ flexShrink: 0 }}
className={classes.errorText}
>
<IconPointFilled size={18} />
<IconPointFilled style={nodeIndicatorIconStyle} />
<Text fz="xs">
<strong>Error executing query:</strong> {error.message}
</Text>
@ -387,7 +400,7 @@ const TreeNode: FC<{
node={children[0]}
selectedNode={selectedNode}
setSelectedNode={setSelectedNode}
parentRef={nodeRef}
parentEl={nodeEl}
reverse={true}
childIdx={0}
reportNodeState={childReportNodeState}
@ -399,7 +412,7 @@ const TreeNode: FC<{
node={children[1]}
selectedNode={selectedNode}
setSelectedNode={setSelectedNode}
parentRef={nodeRef}
parentEl={nodeEl}
reverse={false}
childIdx={1}
reportNodeState={childReportNodeState}
@ -418,7 +431,7 @@ const TreeNode: FC<{
node={child}
selectedNode={selectedNode}
setSelectedNode={setSelectedNode}
parentRef={nodeRef}
parentEl={nodeEl}
reverse={false}
childIdx={idx}
reportNodeState={childReportNodeState}

View file

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

View file

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

View file

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

View file

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

View file

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