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-router-dom": "^6.22.1",
|
||||
"uplot": "^1.6.30",
|
||||
"uplot-react": "^1.2.2"
|
||||
"uplot-react": "^1.2.2",
|
||||
"use-query-params": "^2.2.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/react": "^18.2.55",
|
||||
|
|
|
@ -60,6 +60,8 @@ import { useAppDispatch } from "./state/hooks";
|
|||
import { updateSettings, useSettings } from "./state/settingsSlice";
|
||||
import SettingsMenu from "./components/SettingsMenu";
|
||||
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();
|
||||
|
||||
|
@ -295,134 +297,136 @@ function App() {
|
|||
|
||||
return (
|
||||
<BrowserRouter basename={pathPrefix}>
|
||||
<MantineProvider defaultColorScheme="auto" theme={theme}>
|
||||
<Notifications position="top-right" />
|
||||
<QueryParamProvider adapter={ReactRouter6Adapter}>
|
||||
<MantineProvider defaultColorScheme="auto" theme={theme}>
|
||||
<Notifications position="top-right" />
|
||||
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<AppShell
|
||||
header={{ height: 56 }}
|
||||
navbar={{
|
||||
width: 300,
|
||||
// TODO: On pages with a long title like "/status", the navbar
|
||||
// breaks in an ugly way for narrow windows. Fix this.
|
||||
breakpoint: "sm",
|
||||
collapsed: { desktop: true, mobile: !opened },
|
||||
}}
|
||||
padding="md"
|
||||
>
|
||||
<AppShell.Header bg="rgb(65, 73, 81)" c="#fff">
|
||||
<Group h="100%" px="md">
|
||||
<Group style={{ flex: 1 }} justify="space-between">
|
||||
<Group gap={65}>
|
||||
<Link
|
||||
to="/"
|
||||
style={{ textDecoration: "none", color: "white" }}
|
||||
>
|
||||
<Group gap={10}>
|
||||
<img src={PrometheusLogo} height={30} />
|
||||
<Text fz={20}>Prometheus{agentMode && " Agent"}</Text>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<AppShell
|
||||
header={{ height: 56 }}
|
||||
navbar={{
|
||||
width: 300,
|
||||
// TODO: On pages with a long title like "/status", the navbar
|
||||
// breaks in an ugly way for narrow windows. Fix this.
|
||||
breakpoint: "sm",
|
||||
collapsed: { desktop: true, mobile: !opened },
|
||||
}}
|
||||
padding="md"
|
||||
>
|
||||
<AppShell.Header bg="rgb(65, 73, 81)" c="#fff">
|
||||
<Group h="100%" px="md">
|
||||
<Group style={{ flex: 1 }} justify="space-between">
|
||||
<Group gap={65}>
|
||||
<Link
|
||||
to="/"
|
||||
style={{ textDecoration: "none", color: "white" }}
|
||||
>
|
||||
<Group gap={10}>
|
||||
<img src={PrometheusLogo} height={30} />
|
||||
<Text fz={20}>Prometheus{agentMode && " Agent"}</Text>
|
||||
</Group>
|
||||
</Link>
|
||||
<Group gap={12} visibleFrom="sm">
|
||||
{navLinks}
|
||||
</Group>
|
||||
</Link>
|
||||
<Group gap={12} visibleFrom="sm">
|
||||
{navLinks}
|
||||
</Group>
|
||||
<Group visibleFrom="xs">
|
||||
<ThemeSelector />
|
||||
<SettingsMenu />
|
||||
</Group>
|
||||
</Group>
|
||||
<Group visibleFrom="xs">
|
||||
<ThemeSelector />
|
||||
<SettingsMenu />
|
||||
</Group>
|
||||
<Burger
|
||||
opened={opened}
|
||||
onClick={toggle}
|
||||
hiddenFrom="sm"
|
||||
size="sm"
|
||||
color="gray.2"
|
||||
/>
|
||||
</Group>
|
||||
<Burger
|
||||
opened={opened}
|
||||
onClick={toggle}
|
||||
hiddenFrom="sm"
|
||||
size="sm"
|
||||
color="gray.2"
|
||||
/>
|
||||
</Group>
|
||||
</AppShell.Header>
|
||||
</AppShell.Header>
|
||||
|
||||
<AppShell.Navbar py="md" px={4} bg="rgb(65, 73, 81)" c="#fff">
|
||||
{navLinks}
|
||||
<Group mt="md" hiddenFrom="xs" justify="center">
|
||||
<ThemeSelector />
|
||||
<SettingsMenu />
|
||||
</Group>
|
||||
</AppShell.Navbar>
|
||||
<AppShell.Navbar py="md" px={4} bg="rgb(65, 73, 81)" c="#fff">
|
||||
{navLinks}
|
||||
<Group mt="md" hiddenFrom="xs" justify="center">
|
||||
<ThemeSelector />
|
||||
<SettingsMenu />
|
||||
</Group>
|
||||
</AppShell.Navbar>
|
||||
|
||||
<AppShell.Main>
|
||||
<ErrorBoundary key={location.pathname}>
|
||||
<Suspense
|
||||
fallback={
|
||||
<Box mt="lg">
|
||||
{Array.from(Array(10), (_, i) => (
|
||||
<Skeleton
|
||||
key={i}
|
||||
height={40}
|
||||
mb={15}
|
||||
width={1000}
|
||||
mx="auto"
|
||||
<AppShell.Main>
|
||||
<ErrorBoundary key={location.pathname}>
|
||||
<Suspense
|
||||
fallback={
|
||||
<Box mt="lg">
|
||||
{Array.from(Array(10), (_, i) => (
|
||||
<Skeleton
|
||||
key={i}
|
||||
height={40}
|
||||
mb={15}
|
||||
width={1000}
|
||||
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>
|
||||
<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>
|
||||
}
|
||||
/>
|
||||
))}
|
||||
</Routes>
|
||||
</Suspense>
|
||||
</ErrorBoundary>
|
||||
</AppShell.Main>
|
||||
</AppShell>
|
||||
{/* <ReactQueryDevtools initialIsOpen={false} /> */}
|
||||
</QueryClientProvider>
|
||||
</MantineProvider>
|
||||
</Routes>
|
||||
</Suspense>
|
||||
</ErrorBoundary>
|
||||
</AppShell.Main>
|
||||
</AppShell>
|
||||
{/* <ReactQueryDevtools initialIsOpen={false} /> */}
|
||||
</QueryClientProvider>
|
||||
</MantineProvider>
|
||||
</QueryParamProvider>
|
||||
</BrowserRouter>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -8,26 +8,37 @@ import {
|
|||
Stack,
|
||||
Table,
|
||||
Text,
|
||||
Tooltip,
|
||||
} from "@mantine/core";
|
||||
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 { Target, TargetsResult } from "../../api/responseTypes/targets";
|
||||
import React, { FC, useState } from "react";
|
||||
import React, { FC } from "react";
|
||||
import {
|
||||
humanizeDurationRelative,
|
||||
humanizeDuration,
|
||||
now,
|
||||
} from "../../lib/formatTime";
|
||||
import { LabelBadges } from "../../components/LabelBadges";
|
||||
import { useAppDispatch, useAppSelector } from "../../state/hooks";
|
||||
import { setCollapsedPools } from "../../state/targetsPageSlice";
|
||||
import {
|
||||
setCollapsedPools,
|
||||
setShowLimitAlert,
|
||||
} from "../../state/targetsPageSlice";
|
||||
import EndpointLink from "../../components/EndpointLink";
|
||||
import CustomInfiniteScroll from "../../components/CustomInfiniteScroll";
|
||||
|
||||
import badgeClasses from "../../Badge.module.css";
|
||||
import panelClasses from "../../Panel.module.css";
|
||||
import TargetLabels from "./TargetLabels";
|
||||
import { useDebouncedValue } from "@mantine/hooks";
|
||||
import { targetPoolDisplayLimit } from "./TargetsPage";
|
||||
import { BooleanParam, useQueryParam, withDefault } from "use-query-params";
|
||||
|
||||
type ScrapePool = {
|
||||
targets: Target[];
|
||||
|
@ -119,16 +130,21 @@ const groupTargets = (
|
|||
type ScrapePoolListProp = {
|
||||
poolNames: string[];
|
||||
selectedPool: string | null;
|
||||
limited: boolean;
|
||||
healthFilter: string[];
|
||||
searchFilter: string;
|
||||
};
|
||||
|
||||
const ScrapePoolList: FC<ScrapePoolListProp> = ({
|
||||
poolNames,
|
||||
selectedPool,
|
||||
limited,
|
||||
healthFilter,
|
||||
searchFilter,
|
||||
}) => {
|
||||
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.
|
||||
const {
|
||||
|
@ -143,11 +159,14 @@ const ScrapePoolList: FC<ScrapePoolListProp> = ({
|
|||
},
|
||||
});
|
||||
|
||||
const { healthFilter, searchFilter, collapsedPools } = useAppSelector(
|
||||
const { collapsedPools, showLimitAlert } = useAppSelector(
|
||||
(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(
|
||||
(target) =>
|
||||
healthFilter.length === 0 ||
|
||||
|
@ -157,7 +176,7 @@ const ScrapePoolList: FC<ScrapePoolListProp> = ({
|
|||
search === ""
|
||||
? healthFilteredTargets
|
||||
: kvSearch
|
||||
.filter(searchFilter, healthFilteredTargets)
|
||||
.filter(search, healthFilteredTargets)
|
||||
.map((value) => value.original);
|
||||
|
||||
const allPools = groupTargets(
|
||||
|
@ -194,13 +213,15 @@ const ScrapePoolList: FC<ScrapePoolListProp> = ({
|
|||
</Alert>
|
||||
)
|
||||
)}
|
||||
{limited && (
|
||||
{showLimitAlert && (
|
||||
<Alert
|
||||
title="Found many pools, showing only one"
|
||||
icon={<IconInfoCircle size={14} />}
|
||||
withCloseButton
|
||||
onClose={() => dispatch(setShowLimitAlert(false))}
|
||||
>
|
||||
There are more than 20 scrape pools. Showing only the first one. Use
|
||||
the dropdown to select a different pool.
|
||||
There are more than {targetPoolDisplayLimit} scrape pools. Showing
|
||||
only the first one. Use the dropdown to select a different pool.
|
||||
</Alert>
|
||||
)}
|
||||
<Accordion
|
||||
|
@ -283,10 +304,10 @@ const ScrapePoolList: FC<ScrapePoolListProp> = ({
|
|||
<Table.Thead>
|
||||
<Table.Tr>
|
||||
<Table.Th w="30%">Endpoint</Table.Th>
|
||||
<Table.Th w={100}>State</Table.Th>
|
||||
<Table.Th>Labels</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.Thead>
|
||||
<Table.Tbody>
|
||||
|
@ -307,13 +328,7 @@ const ScrapePoolList: FC<ScrapePoolListProp> = ({
|
|||
globalUrl={target.globalUrl}
|
||||
/>
|
||||
</Table.Td>
|
||||
<Table.Td>
|
||||
<Badge
|
||||
className={healthBadgeClass(target.health)}
|
||||
>
|
||||
{target.health}
|
||||
</Badge>
|
||||
</Table.Td>
|
||||
|
||||
<Table.Td>
|
||||
<TargetLabels
|
||||
labels={target.labels}
|
||||
|
@ -321,15 +336,53 @@ const ScrapePoolList: FC<ScrapePoolListProp> = ({
|
|||
/>
|
||||
</Table.Td>
|
||||
<Table.Td>
|
||||
{humanizeDurationRelative(
|
||||
target.lastScrape,
|
||||
now()
|
||||
)}
|
||||
<Group gap="xs" wrap="wrap">
|
||||
<Tooltip
|
||||
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>
|
||||
{humanizeDuration(
|
||||
target.lastScrapeDuration * 1000
|
||||
)}
|
||||
<Badge
|
||||
className={healthBadgeClass(target.health)}
|
||||
>
|
||||
{target.health}
|
||||
</Badge>
|
||||
</Table.Td>
|
||||
</Table.Tr>
|
||||
{target.lastError && (
|
||||
|
|
|
@ -1,14 +1,7 @@
|
|||
import { FC } from "react";
|
||||
import { Labels } from "../../api/responseTypes/targets";
|
||||
import { LabelBadges } from "../../components/LabelBadges";
|
||||
import {
|
||||
ActionIcon,
|
||||
Collapse,
|
||||
Divider,
|
||||
Group,
|
||||
Stack,
|
||||
Text,
|
||||
} from "@mantine/core";
|
||||
import { ActionIcon, Collapse, Group, Stack, Text } from "@mantine/core";
|
||||
import { useDisclosure } from "@mantine/hooks";
|
||||
import { IconChevronDown, IconChevronUp } from "@tabler/icons-react";
|
||||
|
||||
|
|
|
@ -12,21 +12,25 @@ import {
|
|||
IconSearch,
|
||||
} from "@tabler/icons-react";
|
||||
import { StateMultiSelect } from "../../components/StateMultiSelect";
|
||||
import { useSuspenseAPIQuery } from "../../api/api";
|
||||
import { ScrapePoolsResult } from "../../api/responseTypes/scrapePools";
|
||||
import { Suspense, useEffect } from "react";
|
||||
import { Suspense } from "react";
|
||||
import badgeClasses from "../../Badge.module.css";
|
||||
import { useAppDispatch, useAppSelector } from "../../state/hooks";
|
||||
import {
|
||||
setCollapsedPools,
|
||||
setHealthFilter,
|
||||
setSearchFilter,
|
||||
setSelectedPool,
|
||||
setShowLimitAlert,
|
||||
} from "../../state/targetsPageSlice";
|
||||
import ScrapePoolList from "./ScrapePoolsList";
|
||||
import {
|
||||
ArrayParam,
|
||||
StringParam,
|
||||
useQueryParam,
|
||||
withDefault,
|
||||
} from "use-query-params";
|
||||
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() {
|
||||
// Load the list of all available scrape pools.
|
||||
|
@ -40,25 +44,33 @@ export default function TargetsPage() {
|
|||
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
// If there is a selected pool in the URL, extract it on initial load.
|
||||
useEffect(() => {
|
||||
const uriPool = new URLSearchParams(window.location.search).get(
|
||||
scrapePoolQueryParam
|
||||
);
|
||||
if (uriPool !== null) {
|
||||
dispatch(setSelectedPool(uriPool));
|
||||
}
|
||||
}, [dispatch]);
|
||||
const [scrapePool, setScrapePool] = useQueryParam("scrapePool", StringParam);
|
||||
const [healthFilter, setHealthFilter] = useQueryParam(
|
||||
"healthFilter",
|
||||
withDefault(ArrayParam, [])
|
||||
);
|
||||
const [searchFilter, setSearchFilter] = useQueryParam(
|
||||
"searchFilter",
|
||||
withDefault(StringParam, "")
|
||||
);
|
||||
|
||||
const { selectedPool, healthFilter, searchFilter, collapsedPools } =
|
||||
useAppSelector((state) => state.targetsPage);
|
||||
const { collapsedPools, showLimitAlert } = useAppSelector(
|
||||
(state) => state.targetsPage
|
||||
);
|
||||
|
||||
let poolToShow = selectedPool;
|
||||
let limitedDueToManyPools = false;
|
||||
|
||||
if (poolToShow === null && scrapePools.length > 20) {
|
||||
poolToShow = scrapePools[0];
|
||||
limitedDueToManyPools = true;
|
||||
// When we have more than X targets, we want to limit the display by selecting the first
|
||||
// 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
|
||||
// alert should only be shown once, upon the first "redirect" that causes the limiting,
|
||||
// not again when the page is reloaded with the same URL parameters. That's why we remember
|
||||
// `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 (
|
||||
|
@ -67,8 +79,11 @@ export default function TargetsPage() {
|
|||
<Select
|
||||
placeholder="Select scrape pool"
|
||||
data={[{ label: "All pools", value: "" }, ...scrapePools]}
|
||||
value={selectedPool}
|
||||
onChange={(value) => dispatch(setSelectedPool(value || null))}
|
||||
value={(limited && scrapePools[0]) || scrapePool || null}
|
||||
onChange={(value) => {
|
||||
setScrapePool(value);
|
||||
showLimitAlert && dispatch(setShowLimitAlert(false));
|
||||
}}
|
||||
searchable
|
||||
/>
|
||||
<StateMultiSelect
|
||||
|
@ -81,17 +96,15 @@ export default function TargetsPage() {
|
|||
: badgeClasses.healthErr
|
||||
}
|
||||
placeholder="Filter by target state"
|
||||
values={healthFilter}
|
||||
onChange={(values) => dispatch(setHealthFilter(values))}
|
||||
values={(healthFilter?.filter((v) => v !== null) as string[]) || []}
|
||||
onChange={(values) => setHealthFilter(values)}
|
||||
/>
|
||||
<TextInput
|
||||
flex={1}
|
||||
leftSection={<IconSearch size={14} />}
|
||||
placeholder="Filter by endpoint or labels"
|
||||
value={searchFilter}
|
||||
onChange={(event) =>
|
||||
dispatch(setSearchFilter(event.currentTarget.value))
|
||||
}
|
||||
value={searchFilter || ""}
|
||||
onChange={(event) => setSearchFilter(event.currentTarget.value)}
|
||||
></TextInput>
|
||||
<ActionIcon
|
||||
size="input-sm"
|
||||
|
@ -127,8 +140,9 @@ export default function TargetsPage() {
|
|||
>
|
||||
<ScrapePoolList
|
||||
poolNames={scrapePools}
|
||||
selectedPool={poolToShow}
|
||||
limited={limitedDueToManyPools}
|
||||
selectedPool={(limited && scrapePools[0]) || scrapePool || null}
|
||||
healthFilter={healthFilter as string[]}
|
||||
searchFilter={searchFilter}
|
||||
/>
|
||||
</Suspense>
|
||||
</ErrorBoundary>
|
||||
|
|
|
@ -2,9 +2,7 @@ import { createListenerMiddleware } from "@reduxjs/toolkit";
|
|||
import { AppDispatch, RootState } from "./store";
|
||||
import {
|
||||
localStorageKeyCollapsedPools,
|
||||
localStorageKeyTargetHealthFilter,
|
||||
setCollapsedPools,
|
||||
setHealthFilter,
|
||||
} from "./targetsPageSlice";
|
||||
import { updateSettings } from "./settingsSlice";
|
||||
|
||||
|
@ -26,13 +24,6 @@ startAppListening({
|
|||
},
|
||||
});
|
||||
|
||||
startAppListening({
|
||||
actionCreator: setHealthFilter,
|
||||
effect: ({ payload }) => {
|
||||
persistToLocalStorage(localStorageKeyTargetHealthFilter, payload);
|
||||
},
|
||||
});
|
||||
|
||||
startAppListening({
|
||||
actionCreator: updateSettings,
|
||||
effect: ({ payload }) => {
|
||||
|
|
|
@ -5,49 +5,32 @@ export const localStorageKeyCollapsedPools = "targetsPage.collapsedPools";
|
|||
export const localStorageKeyTargetHealthFilter = "targetsPage.healthFilter";
|
||||
|
||||
interface TargetsPage {
|
||||
selectedPool: string | null;
|
||||
healthFilter: string[];
|
||||
searchFilter: string;
|
||||
collapsedPools: string[];
|
||||
showLimitAlert: boolean;
|
||||
}
|
||||
|
||||
const initialState: TargetsPage = {
|
||||
selectedPool: null,
|
||||
healthFilter: initializeFromLocalStorage<string[]>(
|
||||
localStorageKeyTargetHealthFilter,
|
||||
[]
|
||||
),
|
||||
searchFilter: "",
|
||||
collapsedPools: initializeFromLocalStorage<string[]>(
|
||||
localStorageKeyCollapsedPools,
|
||||
[]
|
||||
),
|
||||
showLimitAlert: false,
|
||||
};
|
||||
|
||||
export const targetsPageSlice = createSlice({
|
||||
name: "targetsPage",
|
||||
initialState,
|
||||
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[]>) => {
|
||||
state.collapsedPools = payload;
|
||||
},
|
||||
setShowLimitAlert: (state, { payload }: PayloadAction<boolean>) => {
|
||||
state.showLimitAlert = payload;
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const {
|
||||
setSelectedPool,
|
||||
setHealthFilter,
|
||||
setSearchFilter,
|
||||
setCollapsedPools,
|
||||
} = targetsPageSlice.actions;
|
||||
export const { setCollapsedPools, setShowLimitAlert } =
|
||||
targetsPageSlice.actions;
|
||||
|
||||
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-router-dom": "^6.22.1",
|
||||
"uplot": "^1.6.30",
|
||||
"uplot-react": "^1.2.2"
|
||||
"uplot-react": "^1.2.2",
|
||||
"use-query-params": "^2.2.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/react": "^18.2.55",
|
||||
|
@ -313,6 +314,29 @@
|
|||
"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": {
|
||||
"name": "@prometheus-io/codemirror-promql",
|
||||
"version": "0.53.1",
|
||||
|
@ -6879,6 +6903,12 @@
|
|||
"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": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
|
||||
|
|
Loading…
Reference in a new issue