mirror of
https://github.com/prometheus/prometheus.git
synced 2024-09-20 07:47:31 -07:00
Better target table formatting, store filters in URL
Signed-off-by: Julius Volz <julius.volz@gmail.com>
This commit is contained in:
parent
b2a8657d58
commit
648751568d
|
@ -39,7 +39,8 @@
|
||||||
"react-redux": "^9.1.0",
|
"react-redux": "^9.1.0",
|
||||||
"react-router-dom": "^6.22.1",
|
"react-router-dom": "^6.22.1",
|
||||||
"uplot": "^1.6.30",
|
"uplot": "^1.6.30",
|
||||||
"uplot-react": "^1.2.2"
|
"uplot-react": "^1.2.2",
|
||||||
|
"use-query-params": "^2.2.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/react": "^18.2.55",
|
"@types/react": "^18.2.55",
|
||||||
|
|
|
@ -60,6 +60,8 @@ import { useAppDispatch } from "./state/hooks";
|
||||||
import { updateSettings, useSettings } from "./state/settingsSlice";
|
import { updateSettings, useSettings } from "./state/settingsSlice";
|
||||||
import SettingsMenu from "./components/SettingsMenu";
|
import SettingsMenu from "./components/SettingsMenu";
|
||||||
import ReadinessWrapper from "./components/ReadinessWrapper";
|
import ReadinessWrapper from "./components/ReadinessWrapper";
|
||||||
|
import { QueryParamProvider } from "use-query-params";
|
||||||
|
import { ReactRouter6Adapter } from "use-query-params/adapters/react-router-6";
|
||||||
|
|
||||||
const queryClient = new QueryClient();
|
const queryClient = new QueryClient();
|
||||||
|
|
||||||
|
@ -295,134 +297,136 @@ function App() {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<BrowserRouter basename={pathPrefix}>
|
<BrowserRouter basename={pathPrefix}>
|
||||||
<MantineProvider defaultColorScheme="auto" theme={theme}>
|
<QueryParamProvider adapter={ReactRouter6Adapter}>
|
||||||
<Notifications position="top-right" />
|
<MantineProvider defaultColorScheme="auto" theme={theme}>
|
||||||
|
<Notifications position="top-right" />
|
||||||
|
|
||||||
<QueryClientProvider client={queryClient}>
|
<QueryClientProvider client={queryClient}>
|
||||||
<AppShell
|
<AppShell
|
||||||
header={{ height: 56 }}
|
header={{ height: 56 }}
|
||||||
navbar={{
|
navbar={{
|
||||||
width: 300,
|
width: 300,
|
||||||
// TODO: On pages with a long title like "/status", the navbar
|
// TODO: On pages with a long title like "/status", the navbar
|
||||||
// breaks in an ugly way for narrow windows. Fix this.
|
// breaks in an ugly way for narrow windows. Fix this.
|
||||||
breakpoint: "sm",
|
breakpoint: "sm",
|
||||||
collapsed: { desktop: true, mobile: !opened },
|
collapsed: { desktop: true, mobile: !opened },
|
||||||
}}
|
}}
|
||||||
padding="md"
|
padding="md"
|
||||||
>
|
>
|
||||||
<AppShell.Header bg="rgb(65, 73, 81)" c="#fff">
|
<AppShell.Header bg="rgb(65, 73, 81)" c="#fff">
|
||||||
<Group h="100%" px="md">
|
<Group h="100%" px="md">
|
||||||
<Group style={{ flex: 1 }} justify="space-between">
|
<Group style={{ flex: 1 }} justify="space-between">
|
||||||
<Group gap={65}>
|
<Group gap={65}>
|
||||||
<Link
|
<Link
|
||||||
to="/"
|
to="/"
|
||||||
style={{ textDecoration: "none", color: "white" }}
|
style={{ textDecoration: "none", color: "white" }}
|
||||||
>
|
>
|
||||||
<Group gap={10}>
|
<Group gap={10}>
|
||||||
<img src={PrometheusLogo} height={30} />
|
<img src={PrometheusLogo} height={30} />
|
||||||
<Text fz={20}>Prometheus{agentMode && " Agent"}</Text>
|
<Text fz={20}>Prometheus{agentMode && " Agent"}</Text>
|
||||||
|
</Group>
|
||||||
|
</Link>
|
||||||
|
<Group gap={12} visibleFrom="sm">
|
||||||
|
{navLinks}
|
||||||
</Group>
|
</Group>
|
||||||
</Link>
|
</Group>
|
||||||
<Group gap={12} visibleFrom="sm">
|
<Group visibleFrom="xs">
|
||||||
{navLinks}
|
<ThemeSelector />
|
||||||
|
<SettingsMenu />
|
||||||
</Group>
|
</Group>
|
||||||
</Group>
|
</Group>
|
||||||
<Group visibleFrom="xs">
|
<Burger
|
||||||
<ThemeSelector />
|
opened={opened}
|
||||||
<SettingsMenu />
|
onClick={toggle}
|
||||||
</Group>
|
hiddenFrom="sm"
|
||||||
|
size="sm"
|
||||||
|
color="gray.2"
|
||||||
|
/>
|
||||||
</Group>
|
</Group>
|
||||||
<Burger
|
</AppShell.Header>
|
||||||
opened={opened}
|
|
||||||
onClick={toggle}
|
|
||||||
hiddenFrom="sm"
|
|
||||||
size="sm"
|
|
||||||
color="gray.2"
|
|
||||||
/>
|
|
||||||
</Group>
|
|
||||||
</AppShell.Header>
|
|
||||||
|
|
||||||
<AppShell.Navbar py="md" px={4} bg="rgb(65, 73, 81)" c="#fff">
|
<AppShell.Navbar py="md" px={4} bg="rgb(65, 73, 81)" c="#fff">
|
||||||
{navLinks}
|
{navLinks}
|
||||||
<Group mt="md" hiddenFrom="xs" justify="center">
|
<Group mt="md" hiddenFrom="xs" justify="center">
|
||||||
<ThemeSelector />
|
<ThemeSelector />
|
||||||
<SettingsMenu />
|
<SettingsMenu />
|
||||||
</Group>
|
</Group>
|
||||||
</AppShell.Navbar>
|
</AppShell.Navbar>
|
||||||
|
|
||||||
<AppShell.Main>
|
<AppShell.Main>
|
||||||
<ErrorBoundary key={location.pathname}>
|
<ErrorBoundary key={location.pathname}>
|
||||||
<Suspense
|
<Suspense
|
||||||
fallback={
|
fallback={
|
||||||
<Box mt="lg">
|
<Box mt="lg">
|
||||||
{Array.from(Array(10), (_, i) => (
|
{Array.from(Array(10), (_, i) => (
|
||||||
<Skeleton
|
<Skeleton
|
||||||
key={i}
|
key={i}
|
||||||
height={40}
|
height={40}
|
||||||
mb={15}
|
mb={15}
|
||||||
width={1000}
|
width={1000}
|
||||||
mx="auto"
|
mx="auto"
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</Box>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Routes>
|
||||||
|
<Route
|
||||||
|
path="/"
|
||||||
|
element={
|
||||||
|
<Navigate
|
||||||
|
to={agentMode ? "/agent" : "/query"}
|
||||||
|
replace
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
{agentMode ? (
|
||||||
|
<Route
|
||||||
|
path="/agent"
|
||||||
|
element={
|
||||||
|
<ReadinessWrapper>
|
||||||
|
<AgentPage />
|
||||||
|
</ReadinessWrapper>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<Route
|
||||||
|
path="/query"
|
||||||
|
element={
|
||||||
|
<ReadinessWrapper>
|
||||||
|
<QueryPage />
|
||||||
|
</ReadinessWrapper>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<Route
|
||||||
|
path="/alerts"
|
||||||
|
element={
|
||||||
|
<ReadinessWrapper>
|
||||||
|
<AlertsPage />
|
||||||
|
</ReadinessWrapper>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{allStatusPages.map((p) => (
|
||||||
|
<Route
|
||||||
|
key={p.path}
|
||||||
|
path={p.path}
|
||||||
|
element={
|
||||||
|
<ReadinessWrapper>{p.element}</ReadinessWrapper>
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</Box>
|
</Routes>
|
||||||
}
|
</Suspense>
|
||||||
>
|
</ErrorBoundary>
|
||||||
<Routes>
|
</AppShell.Main>
|
||||||
<Route
|
</AppShell>
|
||||||
path="/"
|
{/* <ReactQueryDevtools initialIsOpen={false} /> */}
|
||||||
element={
|
</QueryClientProvider>
|
||||||
<Navigate
|
</MantineProvider>
|
||||||
to={agentMode ? "/agent" : "/query"}
|
</QueryParamProvider>
|
||||||
replace
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
{agentMode ? (
|
|
||||||
<Route
|
|
||||||
path="/agent"
|
|
||||||
element={
|
|
||||||
<ReadinessWrapper>
|
|
||||||
<AgentPage />
|
|
||||||
</ReadinessWrapper>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<Route
|
|
||||||
path="/query"
|
|
||||||
element={
|
|
||||||
<ReadinessWrapper>
|
|
||||||
<QueryPage />
|
|
||||||
</ReadinessWrapper>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<Route
|
|
||||||
path="/alerts"
|
|
||||||
element={
|
|
||||||
<ReadinessWrapper>
|
|
||||||
<AlertsPage />
|
|
||||||
</ReadinessWrapper>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
{allStatusPages.map((p) => (
|
|
||||||
<Route
|
|
||||||
key={p.path}
|
|
||||||
path={p.path}
|
|
||||||
element={
|
|
||||||
<ReadinessWrapper>{p.element}</ReadinessWrapper>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</Routes>
|
|
||||||
</Suspense>
|
|
||||||
</ErrorBoundary>
|
|
||||||
</AppShell.Main>
|
|
||||||
</AppShell>
|
|
||||||
{/* <ReactQueryDevtools initialIsOpen={false} /> */}
|
|
||||||
</QueryClientProvider>
|
|
||||||
</MantineProvider>
|
|
||||||
</BrowserRouter>
|
</BrowserRouter>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,26 +8,37 @@ import {
|
||||||
Stack,
|
Stack,
|
||||||
Table,
|
Table,
|
||||||
Text,
|
Text,
|
||||||
|
Tooltip,
|
||||||
} from "@mantine/core";
|
} from "@mantine/core";
|
||||||
import { KVSearch } from "@nexucis/kvsearch";
|
import { KVSearch } from "@nexucis/kvsearch";
|
||||||
import { IconAlertTriangle, IconInfoCircle } from "@tabler/icons-react";
|
import {
|
||||||
|
IconAlertTriangle,
|
||||||
|
IconHourglass,
|
||||||
|
IconInfoCircle,
|
||||||
|
IconRefresh,
|
||||||
|
} from "@tabler/icons-react";
|
||||||
import { useSuspenseAPIQuery } from "../../api/api";
|
import { useSuspenseAPIQuery } from "../../api/api";
|
||||||
import { Target, TargetsResult } from "../../api/responseTypes/targets";
|
import { Target, TargetsResult } from "../../api/responseTypes/targets";
|
||||||
import React, { FC, useState } from "react";
|
import React, { FC } from "react";
|
||||||
import {
|
import {
|
||||||
humanizeDurationRelative,
|
humanizeDurationRelative,
|
||||||
humanizeDuration,
|
humanizeDuration,
|
||||||
now,
|
now,
|
||||||
} from "../../lib/formatTime";
|
} from "../../lib/formatTime";
|
||||||
import { LabelBadges } from "../../components/LabelBadges";
|
|
||||||
import { useAppDispatch, useAppSelector } from "../../state/hooks";
|
import { useAppDispatch, useAppSelector } from "../../state/hooks";
|
||||||
import { setCollapsedPools } from "../../state/targetsPageSlice";
|
import {
|
||||||
|
setCollapsedPools,
|
||||||
|
setShowLimitAlert,
|
||||||
|
} from "../../state/targetsPageSlice";
|
||||||
import EndpointLink from "../../components/EndpointLink";
|
import EndpointLink from "../../components/EndpointLink";
|
||||||
import CustomInfiniteScroll from "../../components/CustomInfiniteScroll";
|
import CustomInfiniteScroll from "../../components/CustomInfiniteScroll";
|
||||||
|
|
||||||
import badgeClasses from "../../Badge.module.css";
|
import badgeClasses from "../../Badge.module.css";
|
||||||
import panelClasses from "../../Panel.module.css";
|
import panelClasses from "../../Panel.module.css";
|
||||||
import TargetLabels from "./TargetLabels";
|
import TargetLabels from "./TargetLabels";
|
||||||
|
import { useDebouncedValue } from "@mantine/hooks";
|
||||||
|
import { targetPoolDisplayLimit } from "./TargetsPage";
|
||||||
|
import { BooleanParam, useQueryParam, withDefault } from "use-query-params";
|
||||||
|
|
||||||
type ScrapePool = {
|
type ScrapePool = {
|
||||||
targets: Target[];
|
targets: Target[];
|
||||||
|
@ -119,16 +130,21 @@ const groupTargets = (
|
||||||
type ScrapePoolListProp = {
|
type ScrapePoolListProp = {
|
||||||
poolNames: string[];
|
poolNames: string[];
|
||||||
selectedPool: string | null;
|
selectedPool: string | null;
|
||||||
limited: boolean;
|
healthFilter: string[];
|
||||||
|
searchFilter: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
const ScrapePoolList: FC<ScrapePoolListProp> = ({
|
const ScrapePoolList: FC<ScrapePoolListProp> = ({
|
||||||
poolNames,
|
poolNames,
|
||||||
selectedPool,
|
selectedPool,
|
||||||
limited,
|
healthFilter,
|
||||||
|
searchFilter,
|
||||||
}) => {
|
}) => {
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const [showEmptyPools, setShowEmptyPools] = useState(true);
|
const [showEmptyPools, setShowEmptyPools] = useQueryParam(
|
||||||
|
"showEmptyPools",
|
||||||
|
withDefault(BooleanParam, true)
|
||||||
|
);
|
||||||
|
|
||||||
// Based on the selected pool (if any), load the list of targets.
|
// Based on the selected pool (if any), load the list of targets.
|
||||||
const {
|
const {
|
||||||
|
@ -143,11 +159,14 @@ const ScrapePoolList: FC<ScrapePoolListProp> = ({
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const { healthFilter, searchFilter, collapsedPools } = useAppSelector(
|
const { collapsedPools, showLimitAlert } = useAppSelector(
|
||||||
(state) => state.targetsPage
|
(state) => state.targetsPage
|
||||||
);
|
);
|
||||||
|
|
||||||
const search = searchFilter.trim();
|
const [debouncedSearch] = useDebouncedValue<string>(searchFilter, 250);
|
||||||
|
|
||||||
|
// TODO: Memoize all this computation, especially groupTargets().
|
||||||
|
const search = debouncedSearch.trim();
|
||||||
const healthFilteredTargets = activeTargets.filter(
|
const healthFilteredTargets = activeTargets.filter(
|
||||||
(target) =>
|
(target) =>
|
||||||
healthFilter.length === 0 ||
|
healthFilter.length === 0 ||
|
||||||
|
@ -157,7 +176,7 @@ const ScrapePoolList: FC<ScrapePoolListProp> = ({
|
||||||
search === ""
|
search === ""
|
||||||
? healthFilteredTargets
|
? healthFilteredTargets
|
||||||
: kvSearch
|
: kvSearch
|
||||||
.filter(searchFilter, healthFilteredTargets)
|
.filter(search, healthFilteredTargets)
|
||||||
.map((value) => value.original);
|
.map((value) => value.original);
|
||||||
|
|
||||||
const allPools = groupTargets(
|
const allPools = groupTargets(
|
||||||
|
@ -194,13 +213,15 @@ const ScrapePoolList: FC<ScrapePoolListProp> = ({
|
||||||
</Alert>
|
</Alert>
|
||||||
)
|
)
|
||||||
)}
|
)}
|
||||||
{limited && (
|
{showLimitAlert && (
|
||||||
<Alert
|
<Alert
|
||||||
title="Found many pools, showing only one"
|
title="Found many pools, showing only one"
|
||||||
icon={<IconInfoCircle size={14} />}
|
icon={<IconInfoCircle size={14} />}
|
||||||
|
withCloseButton
|
||||||
|
onClose={() => dispatch(setShowLimitAlert(false))}
|
||||||
>
|
>
|
||||||
There are more than 20 scrape pools. Showing only the first one. Use
|
There are more than {targetPoolDisplayLimit} scrape pools. Showing
|
||||||
the dropdown to select a different pool.
|
only the first one. Use the dropdown to select a different pool.
|
||||||
</Alert>
|
</Alert>
|
||||||
)}
|
)}
|
||||||
<Accordion
|
<Accordion
|
||||||
|
@ -283,10 +304,10 @@ const ScrapePoolList: FC<ScrapePoolListProp> = ({
|
||||||
<Table.Thead>
|
<Table.Thead>
|
||||||
<Table.Tr>
|
<Table.Tr>
|
||||||
<Table.Th w="30%">Endpoint</Table.Th>
|
<Table.Th w="30%">Endpoint</Table.Th>
|
||||||
<Table.Th w={100}>State</Table.Th>
|
|
||||||
<Table.Th>Labels</Table.Th>
|
<Table.Th>Labels</Table.Th>
|
||||||
<Table.Th w="10%">Last scrape</Table.Th>
|
<Table.Th w="10%">Last scrape</Table.Th>
|
||||||
<Table.Th w="10%">Scrape duration</Table.Th>
|
{/* <Table.Th w="10%">Scrape duration</Table.Th> */}
|
||||||
|
<Table.Th w={100}>State</Table.Th>
|
||||||
</Table.Tr>
|
</Table.Tr>
|
||||||
</Table.Thead>
|
</Table.Thead>
|
||||||
<Table.Tbody>
|
<Table.Tbody>
|
||||||
|
@ -307,13 +328,7 @@ const ScrapePoolList: FC<ScrapePoolListProp> = ({
|
||||||
globalUrl={target.globalUrl}
|
globalUrl={target.globalUrl}
|
||||||
/>
|
/>
|
||||||
</Table.Td>
|
</Table.Td>
|
||||||
<Table.Td>
|
|
||||||
<Badge
|
|
||||||
className={healthBadgeClass(target.health)}
|
|
||||||
>
|
|
||||||
{target.health}
|
|
||||||
</Badge>
|
|
||||||
</Table.Td>
|
|
||||||
<Table.Td>
|
<Table.Td>
|
||||||
<TargetLabels
|
<TargetLabels
|
||||||
labels={target.labels}
|
labels={target.labels}
|
||||||
|
@ -321,15 +336,53 @@ const ScrapePoolList: FC<ScrapePoolListProp> = ({
|
||||||
/>
|
/>
|
||||||
</Table.Td>
|
</Table.Td>
|
||||||
<Table.Td>
|
<Table.Td>
|
||||||
{humanizeDurationRelative(
|
<Group gap="xs" wrap="wrap">
|
||||||
target.lastScrape,
|
<Tooltip
|
||||||
now()
|
label="Last target scrape"
|
||||||
)}
|
withArrow
|
||||||
|
>
|
||||||
|
<Badge
|
||||||
|
variant="light"
|
||||||
|
className={badgeClasses.statsBadge}
|
||||||
|
styles={{
|
||||||
|
label: { textTransform: "none" },
|
||||||
|
}}
|
||||||
|
leftSection={<IconRefresh size={12} />}
|
||||||
|
>
|
||||||
|
{humanizeDurationRelative(
|
||||||
|
target.lastScrape,
|
||||||
|
now()
|
||||||
|
)}
|
||||||
|
</Badge>
|
||||||
|
</Tooltip>
|
||||||
|
|
||||||
|
<Tooltip
|
||||||
|
label="Duration of last target scrape"
|
||||||
|
withArrow
|
||||||
|
>
|
||||||
|
<Badge
|
||||||
|
variant="light"
|
||||||
|
className={badgeClasses.statsBadge}
|
||||||
|
styles={{
|
||||||
|
label: { textTransform: "none" },
|
||||||
|
}}
|
||||||
|
leftSection={
|
||||||
|
<IconHourglass size={12} />
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{humanizeDuration(
|
||||||
|
target.lastScrapeDuration * 1000
|
||||||
|
)}
|
||||||
|
</Badge>
|
||||||
|
</Tooltip>
|
||||||
|
</Group>
|
||||||
</Table.Td>
|
</Table.Td>
|
||||||
<Table.Td>
|
<Table.Td>
|
||||||
{humanizeDuration(
|
<Badge
|
||||||
target.lastScrapeDuration * 1000
|
className={healthBadgeClass(target.health)}
|
||||||
)}
|
>
|
||||||
|
{target.health}
|
||||||
|
</Badge>
|
||||||
</Table.Td>
|
</Table.Td>
|
||||||
</Table.Tr>
|
</Table.Tr>
|
||||||
{target.lastError && (
|
{target.lastError && (
|
||||||
|
|
|
@ -1,14 +1,7 @@
|
||||||
import { FC } from "react";
|
import { FC } from "react";
|
||||||
import { Labels } from "../../api/responseTypes/targets";
|
import { Labels } from "../../api/responseTypes/targets";
|
||||||
import { LabelBadges } from "../../components/LabelBadges";
|
import { LabelBadges } from "../../components/LabelBadges";
|
||||||
import {
|
import { ActionIcon, Collapse, Group, Stack, Text } from "@mantine/core";
|
||||||
ActionIcon,
|
|
||||||
Collapse,
|
|
||||||
Divider,
|
|
||||||
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";
|
||||||
|
|
||||||
|
|
|
@ -12,21 +12,25 @@ import {
|
||||||
IconSearch,
|
IconSearch,
|
||||||
} from "@tabler/icons-react";
|
} from "@tabler/icons-react";
|
||||||
import { StateMultiSelect } from "../../components/StateMultiSelect";
|
import { StateMultiSelect } from "../../components/StateMultiSelect";
|
||||||
import { useSuspenseAPIQuery } from "../../api/api";
|
import { Suspense } from "react";
|
||||||
import { ScrapePoolsResult } from "../../api/responseTypes/scrapePools";
|
|
||||||
import { Suspense, useEffect } from "react";
|
|
||||||
import badgeClasses from "../../Badge.module.css";
|
import badgeClasses from "../../Badge.module.css";
|
||||||
import { useAppDispatch, useAppSelector } from "../../state/hooks";
|
import { useAppDispatch, useAppSelector } from "../../state/hooks";
|
||||||
import {
|
import {
|
||||||
setCollapsedPools,
|
setCollapsedPools,
|
||||||
setHealthFilter,
|
setShowLimitAlert,
|
||||||
setSearchFilter,
|
|
||||||
setSelectedPool,
|
|
||||||
} from "../../state/targetsPageSlice";
|
} from "../../state/targetsPageSlice";
|
||||||
import ScrapePoolList from "./ScrapePoolsList";
|
import {
|
||||||
|
ArrayParam,
|
||||||
|
StringParam,
|
||||||
|
useQueryParam,
|
||||||
|
withDefault,
|
||||||
|
} from "use-query-params";
|
||||||
import ErrorBoundary from "../../components/ErrorBoundary";
|
import ErrorBoundary from "../../components/ErrorBoundary";
|
||||||
|
import ScrapePoolList from "./ScrapePoolsList";
|
||||||
|
import { useSuspenseAPIQuery } from "../../api/api";
|
||||||
|
import { ScrapePoolsResult } from "../../api/responseTypes/scrapePools";
|
||||||
|
|
||||||
const scrapePoolQueryParam = "scrapePool";
|
export const targetPoolDisplayLimit = 3;
|
||||||
|
|
||||||
export default function TargetsPage() {
|
export default function TargetsPage() {
|
||||||
// Load the list of all available scrape pools.
|
// Load the list of all available scrape pools.
|
||||||
|
@ -40,25 +44,33 @@ export default function TargetsPage() {
|
||||||
|
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
|
|
||||||
// If there is a selected pool in the URL, extract it on initial load.
|
const [scrapePool, setScrapePool] = useQueryParam("scrapePool", StringParam);
|
||||||
useEffect(() => {
|
const [healthFilter, setHealthFilter] = useQueryParam(
|
||||||
const uriPool = new URLSearchParams(window.location.search).get(
|
"healthFilter",
|
||||||
scrapePoolQueryParam
|
withDefault(ArrayParam, [])
|
||||||
);
|
);
|
||||||
if (uriPool !== null) {
|
const [searchFilter, setSearchFilter] = useQueryParam(
|
||||||
dispatch(setSelectedPool(uriPool));
|
"searchFilter",
|
||||||
}
|
withDefault(StringParam, "")
|
||||||
}, [dispatch]);
|
);
|
||||||
|
|
||||||
const { selectedPool, healthFilter, searchFilter, collapsedPools } =
|
const { collapsedPools, showLimitAlert } = useAppSelector(
|
||||||
useAppSelector((state) => state.targetsPage);
|
(state) => state.targetsPage
|
||||||
|
);
|
||||||
|
|
||||||
let poolToShow = selectedPool;
|
// When we have more than X targets, we want to limit the display by selecting the first
|
||||||
let limitedDueToManyPools = false;
|
// scrape pool and reflecting that in the URL as well. We also want to show an alert
|
||||||
|
// about the fact that we are limiting the display, but the tricky bit is that this
|
||||||
if (poolToShow === null && scrapePools.length > 20) {
|
// alert should only be shown once, upon the first "redirect" that causes the limiting,
|
||||||
poolToShow = scrapePools[0];
|
// not again when the page is reloaded with the same URL parameters. That's why we remember
|
||||||
limitedDueToManyPools = true;
|
// `showLimitAlert` in Redux (just useState() doesn't work properly, because the component
|
||||||
|
// for some Suspense-related reasons seems to be mounted/unmounted multiple times, so the
|
||||||
|
// state cell would get initialized multiple times as well).
|
||||||
|
const limited =
|
||||||
|
scrapePools.length > targetPoolDisplayLimit && scrapePool === undefined;
|
||||||
|
if (limited) {
|
||||||
|
setScrapePool(scrapePools[0]);
|
||||||
|
dispatch(setShowLimitAlert(true));
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -67,8 +79,11 @@ export default function TargetsPage() {
|
||||||
<Select
|
<Select
|
||||||
placeholder="Select scrape pool"
|
placeholder="Select scrape pool"
|
||||||
data={[{ label: "All pools", value: "" }, ...scrapePools]}
|
data={[{ label: "All pools", value: "" }, ...scrapePools]}
|
||||||
value={selectedPool}
|
value={(limited && scrapePools[0]) || scrapePool || null}
|
||||||
onChange={(value) => dispatch(setSelectedPool(value || null))}
|
onChange={(value) => {
|
||||||
|
setScrapePool(value);
|
||||||
|
showLimitAlert && dispatch(setShowLimitAlert(false));
|
||||||
|
}}
|
||||||
searchable
|
searchable
|
||||||
/>
|
/>
|
||||||
<StateMultiSelect
|
<StateMultiSelect
|
||||||
|
@ -81,17 +96,15 @@ export default function TargetsPage() {
|
||||||
: badgeClasses.healthErr
|
: badgeClasses.healthErr
|
||||||
}
|
}
|
||||||
placeholder="Filter by target state"
|
placeholder="Filter by target state"
|
||||||
values={healthFilter}
|
values={(healthFilter?.filter((v) => v !== null) as string[]) || []}
|
||||||
onChange={(values) => dispatch(setHealthFilter(values))}
|
onChange={(values) => setHealthFilter(values)}
|
||||||
/>
|
/>
|
||||||
<TextInput
|
<TextInput
|
||||||
flex={1}
|
flex={1}
|
||||||
leftSection={<IconSearch size={14} />}
|
leftSection={<IconSearch size={14} />}
|
||||||
placeholder="Filter by endpoint or labels"
|
placeholder="Filter by endpoint or labels"
|
||||||
value={searchFilter}
|
value={searchFilter || ""}
|
||||||
onChange={(event) =>
|
onChange={(event) => setSearchFilter(event.currentTarget.value)}
|
||||||
dispatch(setSearchFilter(event.currentTarget.value))
|
|
||||||
}
|
|
||||||
></TextInput>
|
></TextInput>
|
||||||
<ActionIcon
|
<ActionIcon
|
||||||
size="input-sm"
|
size="input-sm"
|
||||||
|
@ -127,8 +140,9 @@ export default function TargetsPage() {
|
||||||
>
|
>
|
||||||
<ScrapePoolList
|
<ScrapePoolList
|
||||||
poolNames={scrapePools}
|
poolNames={scrapePools}
|
||||||
selectedPool={poolToShow}
|
selectedPool={(limited && scrapePools[0]) || scrapePool || null}
|
||||||
limited={limitedDueToManyPools}
|
healthFilter={healthFilter as string[]}
|
||||||
|
searchFilter={searchFilter}
|
||||||
/>
|
/>
|
||||||
</Suspense>
|
</Suspense>
|
||||||
</ErrorBoundary>
|
</ErrorBoundary>
|
||||||
|
|
|
@ -2,9 +2,7 @@ import { createListenerMiddleware } from "@reduxjs/toolkit";
|
||||||
import { AppDispatch, RootState } from "./store";
|
import { AppDispatch, RootState } from "./store";
|
||||||
import {
|
import {
|
||||||
localStorageKeyCollapsedPools,
|
localStorageKeyCollapsedPools,
|
||||||
localStorageKeyTargetHealthFilter,
|
|
||||||
setCollapsedPools,
|
setCollapsedPools,
|
||||||
setHealthFilter,
|
|
||||||
} from "./targetsPageSlice";
|
} from "./targetsPageSlice";
|
||||||
import { updateSettings } from "./settingsSlice";
|
import { updateSettings } from "./settingsSlice";
|
||||||
|
|
||||||
|
@ -26,13 +24,6 @@ startAppListening({
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
startAppListening({
|
|
||||||
actionCreator: setHealthFilter,
|
|
||||||
effect: ({ payload }) => {
|
|
||||||
persistToLocalStorage(localStorageKeyTargetHealthFilter, payload);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
startAppListening({
|
startAppListening({
|
||||||
actionCreator: updateSettings,
|
actionCreator: updateSettings,
|
||||||
effect: ({ payload }) => {
|
effect: ({ payload }) => {
|
||||||
|
|
|
@ -5,49 +5,32 @@ export const localStorageKeyCollapsedPools = "targetsPage.collapsedPools";
|
||||||
export const localStorageKeyTargetHealthFilter = "targetsPage.healthFilter";
|
export const localStorageKeyTargetHealthFilter = "targetsPage.healthFilter";
|
||||||
|
|
||||||
interface TargetsPage {
|
interface TargetsPage {
|
||||||
selectedPool: string | null;
|
|
||||||
healthFilter: string[];
|
|
||||||
searchFilter: string;
|
|
||||||
collapsedPools: string[];
|
collapsedPools: string[];
|
||||||
|
showLimitAlert: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const initialState: TargetsPage = {
|
const initialState: TargetsPage = {
|
||||||
selectedPool: null,
|
|
||||||
healthFilter: initializeFromLocalStorage<string[]>(
|
|
||||||
localStorageKeyTargetHealthFilter,
|
|
||||||
[]
|
|
||||||
),
|
|
||||||
searchFilter: "",
|
|
||||||
collapsedPools: initializeFromLocalStorage<string[]>(
|
collapsedPools: initializeFromLocalStorage<string[]>(
|
||||||
localStorageKeyCollapsedPools,
|
localStorageKeyCollapsedPools,
|
||||||
[]
|
[]
|
||||||
),
|
),
|
||||||
|
showLimitAlert: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const targetsPageSlice = createSlice({
|
export const targetsPageSlice = createSlice({
|
||||||
name: "targetsPage",
|
name: "targetsPage",
|
||||||
initialState,
|
initialState,
|
||||||
reducers: {
|
reducers: {
|
||||||
setSelectedPool: (state, { payload }: PayloadAction<string | null>) => {
|
|
||||||
state.selectedPool = payload;
|
|
||||||
},
|
|
||||||
setHealthFilter: (state, { payload }: PayloadAction<string[]>) => {
|
|
||||||
state.healthFilter = payload;
|
|
||||||
},
|
|
||||||
setSearchFilter: (state, { payload }: PayloadAction<string>) => {
|
|
||||||
state.searchFilter = payload;
|
|
||||||
},
|
|
||||||
setCollapsedPools: (state, { payload }: PayloadAction<string[]>) => {
|
setCollapsedPools: (state, { payload }: PayloadAction<string[]>) => {
|
||||||
state.collapsedPools = payload;
|
state.collapsedPools = payload;
|
||||||
},
|
},
|
||||||
|
setShowLimitAlert: (state, { payload }: PayloadAction<boolean>) => {
|
||||||
|
state.showLimitAlert = payload;
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export const {
|
export const { setCollapsedPools, setShowLimitAlert } =
|
||||||
setSelectedPool,
|
targetsPageSlice.actions;
|
||||||
setHealthFilter,
|
|
||||||
setSearchFilter,
|
|
||||||
setCollapsedPools,
|
|
||||||
} = targetsPageSlice.actions;
|
|
||||||
|
|
||||||
export default targetsPageSlice.reducer;
|
export default targetsPageSlice.reducer;
|
||||||
|
|
32
web/ui/package-lock.json
generated
32
web/ui/package-lock.json
generated
|
@ -136,7 +136,8 @@
|
||||||
"react-redux": "^9.1.0",
|
"react-redux": "^9.1.0",
|
||||||
"react-router-dom": "^6.22.1",
|
"react-router-dom": "^6.22.1",
|
||||||
"uplot": "^1.6.30",
|
"uplot": "^1.6.30",
|
||||||
"uplot-react": "^1.2.2"
|
"uplot-react": "^1.2.2",
|
||||||
|
"use-query-params": "^2.2.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/react": "^18.2.55",
|
"@types/react": "^18.2.55",
|
||||||
|
@ -313,6 +314,29 @@
|
||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"mantine-ui/node_modules/use-query-params": {
|
||||||
|
"version": "2.2.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/use-query-params/-/use-query-params-2.2.1.tgz",
|
||||||
|
"integrity": "sha512-i6alcyLB8w9i3ZK3caNftdb+UnbfBRNPDnc89CNQWkGRmDrm/gfydHvMBfVsQJRq3NoHOM2dt/ceBWG2397v1Q==",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"serialize-query-params": "^2.0.2"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@reach/router": "^1.2.1",
|
||||||
|
"react": ">=16.8.0",
|
||||||
|
"react-dom": ">=16.8.0",
|
||||||
|
"react-router-dom": ">=5"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@reach/router": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"react-router-dom": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"module/codemirror-promql": {
|
"module/codemirror-promql": {
|
||||||
"name": "@prometheus-io/codemirror-promql",
|
"name": "@prometheus-io/codemirror-promql",
|
||||||
"version": "0.53.1",
|
"version": "0.53.1",
|
||||||
|
@ -6879,6 +6903,12 @@
|
||||||
"node": ">=10"
|
"node": ">=10"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/serialize-query-params": {
|
||||||
|
"version": "2.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/serialize-query-params/-/serialize-query-params-2.0.2.tgz",
|
||||||
|
"integrity": "sha512-1chMo1dST4pFA9RDXAtF0Rbjaut4is7bzFbI1Z26IuMub68pNCILku85aYmeFhvnY//BXUPUhoRMjYcsT93J/Q==",
|
||||||
|
"license": "ISC"
|
||||||
|
},
|
||||||
"node_modules/shebang-command": {
|
"node_modules/shebang-command": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
|
||||||
|
|
Loading…
Reference in a new issue