diff --git a/web/ui/mantine-ui/package.json b/web/ui/mantine-ui/package.json index 49944c79b..79228d89b 100644 --- a/web/ui/mantine-ui/package.json +++ b/web/ui/mantine-ui/package.json @@ -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", diff --git a/web/ui/mantine-ui/src/App.tsx b/web/ui/mantine-ui/src/App.tsx index c651b38ce..3ed950a9f 100644 --- a/web/ui/mantine-ui/src/App.tsx +++ b/web/ui/mantine-ui/src/App.tsx @@ -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 ( - - + + + - - - - - - - - - - Prometheus{agentMode && " Agent"} + + + + + + + + + + Prometheus{agentMode && " Agent"} + + + + {navLinks} - - - {navLinks} + + + + - - - - + - - - + - - {navLinks} - - - - - + + {navLinks} + + + + + - - - - {Array.from(Array(10), (_, i) => ( - + + + {Array.from(Array(10), (_, i) => ( + + ))} + + } + > + + + } + /> + {agentMode ? ( + + + + } + /> + ) : ( + <> + + + + } + /> + + + + } + /> + + )} + {allStatusPages.map((p) => ( + {p.element} + } /> ))} - - } - > - - - } - /> - {agentMode ? ( - - - - } - /> - ) : ( - <> - - - - } - /> - - - - } - /> - - )} - {allStatusPages.map((p) => ( - {p.element} - } - /> - ))} - - - - - - {/* */} - - + + + + + + {/* */} + + + ); } diff --git a/web/ui/mantine-ui/src/pages/targets/ScrapePoolsList.tsx b/web/ui/mantine-ui/src/pages/targets/ScrapePoolsList.tsx index 66e8f0e95..05521a596 100644 --- a/web/ui/mantine-ui/src/pages/targets/ScrapePoolsList.tsx +++ b/web/ui/mantine-ui/src/pages/targets/ScrapePoolsList.tsx @@ -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 = ({ 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 = ({ }, }); - const { healthFilter, searchFilter, collapsedPools } = useAppSelector( + const { collapsedPools, showLimitAlert } = useAppSelector( (state) => state.targetsPage ); - const search = searchFilter.trim(); + const [debouncedSearch] = useDebouncedValue(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 = ({ search === "" ? healthFilteredTargets : kvSearch - .filter(searchFilter, healthFilteredTargets) + .filter(search, healthFilteredTargets) .map((value) => value.original); const allPools = groupTargets( @@ -194,13 +213,15 @@ const ScrapePoolList: FC = ({ ) )} - {limited && ( + {showLimitAlert && ( } + 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. )} = ({ Endpoint - State Labels Last scrape - Scrape duration + {/* Scrape duration */} + State @@ -307,13 +328,7 @@ const ScrapePoolList: FC = ({ globalUrl={target.globalUrl} /> - - - {target.health} - - + = ({ /> - {humanizeDurationRelative( - target.lastScrape, - now() - )} + + + } + > + {humanizeDurationRelative( + target.lastScrape, + now() + )} + + + + + + } + > + {humanizeDuration( + target.lastScrapeDuration * 1000 + )} + + + - {humanizeDuration( - target.lastScrapeDuration * 1000 - )} + + {target.health} + {target.lastError && ( diff --git a/web/ui/mantine-ui/src/pages/targets/TargetLabels.tsx b/web/ui/mantine-ui/src/pages/targets/TargetLabels.tsx index 561d640eb..54e944f8c 100644 --- a/web/ui/mantine-ui/src/pages/targets/TargetLabels.tsx +++ b/web/ui/mantine-ui/src/pages/targets/TargetLabels.tsx @@ -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"; diff --git a/web/ui/mantine-ui/src/pages/targets/TargetsPage.tsx b/web/ui/mantine-ui/src/pages/targets/TargetsPage.tsx index 5e3be0a8e..2e04303c7 100644 --- a/web/ui/mantine-ui/src/pages/targets/TargetsPage.tsx +++ b/web/ui/mantine-ui/src/pages/targets/TargetsPage.tsx @@ -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() {