mirror of
https://github.com/prometheus/prometheus.git
synced 2024-12-26 06:04:05 -08:00
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:
commit
62bd893b80
|
@ -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>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
|
@ -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"
|
||||||
|
|
32
web/ui/mantine-ui/src/components/InfoPageCard.tsx
Normal file
32
web/ui/mantine-ui/src/components/InfoPageCard.tsx
Normal 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;
|
12
web/ui/mantine-ui/src/components/InfoPageStack.tsx
Normal file
12
web/ui/mantine-ui/src/components/InfoPageStack.tsx
Normal 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;
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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([])} />
|
||||||
|
|
4
web/ui/mantine-ui/src/mantine-overrides.css
Normal file
4
web/ui/mantine-ui/src/mantine-overrides.css
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
.mantine-Badge-label {
|
||||||
|
overflow: unset;
|
||||||
|
text-overflow: unset;
|
||||||
|
}
|
|
@ -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
|
||||||
|
@ -18,9 +18,10 @@ const AgentPage: FC = () => {
|
||||||
</Text>
|
</Text>
|
||||||
<Text p="md">
|
<Text p="md">
|
||||||
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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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,59 +126,56 @@ export default function FlagsPage() {
|
||||||
));
|
));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card shadow="xs" maw={1000} mx="auto" mt="xs" withBorder>
|
<InfoPageStack>
|
||||||
<TextInput
|
<InfoPageCard>
|
||||||
placeholder="Filter by flag name or value"
|
<TextInput
|
||||||
mb="md"
|
placeholder="Filter by flag name or value"
|
||||||
autoFocus
|
mb="md"
|
||||||
leftSection={
|
autoFocus
|
||||||
<IconSearch
|
leftSection={<IconSearch style={inputIconStyle} />}
|
||||||
style={{ width: rem(16), height: rem(16) }}
|
value={search}
|
||||||
stroke={1.5}
|
onChange={handleSearchChange}
|
||||||
/>
|
/>
|
||||||
}
|
<Table
|
||||||
value={search}
|
horizontalSpacing="md"
|
||||||
onChange={handleSearchChange}
|
verticalSpacing="xs"
|
||||||
/>
|
miw={700}
|
||||||
<Table
|
layout="fixed"
|
||||||
horizontalSpacing="md"
|
>
|
||||||
verticalSpacing="xs"
|
<Table.Tbody>
|
||||||
miw={700}
|
|
||||||
layout="fixed"
|
|
||||||
>
|
|
||||||
<Table.Tbody>
|
|
||||||
<Table.Tr>
|
|
||||||
<Th
|
|
||||||
sorted={sortBy === "flag"}
|
|
||||||
reversed={reverseSortDirection}
|
|
||||||
onSort={() => setSorting("flag")}
|
|
||||||
>
|
|
||||||
Flag
|
|
||||||
</Th>
|
|
||||||
|
|
||||||
<Th
|
|
||||||
sorted={sortBy === "value"}
|
|
||||||
reversed={reverseSortDirection}
|
|
||||||
onSort={() => setSorting("value")}
|
|
||||||
>
|
|
||||||
Value
|
|
||||||
</Th>
|
|
||||||
</Table.Tr>
|
|
||||||
</Table.Tbody>
|
|
||||||
<Table.Tbody>
|
|
||||||
{rows.length > 0 ? (
|
|
||||||
rows
|
|
||||||
) : (
|
|
||||||
<Table.Tr>
|
<Table.Tr>
|
||||||
<Table.Td colSpan={2}>
|
<Th
|
||||||
<Text fw={500} ta="center">
|
sorted={sortBy === "flag"}
|
||||||
Nothing found
|
reversed={reverseSortDirection}
|
||||||
</Text>
|
onSort={() => setSorting("flag")}
|
||||||
</Table.Td>
|
>
|
||||||
|
Flag
|
||||||
|
</Th>
|
||||||
|
|
||||||
|
<Th
|
||||||
|
sorted={sortBy === "value"}
|
||||||
|
reversed={reverseSortDirection}
|
||||||
|
onSort={() => setSorting("value")}
|
||||||
|
>
|
||||||
|
Value
|
||||||
|
</Th>
|
||||||
</Table.Tr>
|
</Table.Tr>
|
||||||
)}
|
</Table.Tbody>
|
||||||
</Table.Tbody>
|
<Table.Tbody>
|
||||||
</Table>
|
{rows.length > 0 ? (
|
||||||
</Card>
|
rows
|
||||||
|
) : (
|
||||||
|
<Table.Tr>
|
||||||
|
<Table.Td colSpan={2}>
|
||||||
|
<Text fw={500} ta="center">
|
||||||
|
Nothing found
|
||||||
|
</Text>
|
||||||
|
</Table.Td>
|
||||||
|
</Table.Tr>
|
||||||
|
)}
|
||||||
|
</Table.Tbody>
|
||||||
|
</Table>
|
||||||
|
</InfoPageCard>
|
||||||
|
</InfoPageStack>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,24 +81,22 @@ 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={{
|
wordBreak: "break-all",
|
||||||
wordBreak: "break-all",
|
}}
|
||||||
}}
|
>
|
||||||
>
|
{formatAsCode ? <code>{name}</code> : name}
|
||||||
{formatAsCode ? <code>{name}</code> : name}
|
</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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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}
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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))}
|
||||||
>
|
>
|
||||||
|
|
|
@ -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}
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
15
web/ui/mantine-ui/src/styles.ts
Normal file
15
web/ui/mantine-ui/src/styles.ts
Normal 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",
|
||||||
|
};
|
Loading…
Reference in a new issue