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() {