mirror of
https://github.com/prometheus/prometheus.git
synced 2025-03-05 20:59:13 -08:00
Add filtering, limiting, and more to targets page
Signed-off-by: Julius Volz <julius.volz@gmail.com>
This commit is contained in:
parent
6321267996
commit
b06bb78543
|
@ -48,7 +48,7 @@ import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||||
import QueryPage from "./pages/query/QueryPage";
|
import QueryPage from "./pages/query/QueryPage";
|
||||||
import AlertsPage from "./pages/AlertsPage";
|
import AlertsPage from "./pages/AlertsPage";
|
||||||
import RulesPage from "./pages/RulesPage";
|
import RulesPage from "./pages/RulesPage";
|
||||||
import TargetsPage from "./pages/TargetsPage";
|
import TargetsPage from "./pages/targets/TargetsPage";
|
||||||
import ServiceDiscoveryPage from "./pages/ServiceDiscoveryPage";
|
import ServiceDiscoveryPage from "./pages/ServiceDiscoveryPage";
|
||||||
import StatusPage from "./pages/StatusPage";
|
import StatusPage from "./pages/StatusPage";
|
||||||
import TSDBStatusPage from "./pages/TSDBStatusPage";
|
import TSDBStatusPage from "./pages/TSDBStatusPage";
|
||||||
|
|
|
@ -5,6 +5,7 @@ import { useLocation } from "react-router-dom";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
children?: ReactNode;
|
children?: ReactNode;
|
||||||
|
title?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface State {
|
interface State {
|
||||||
|
@ -30,7 +31,7 @@ class ErrorBoundary extends Component<Props, State> {
|
||||||
return (
|
return (
|
||||||
<Alert
|
<Alert
|
||||||
color="red"
|
color="red"
|
||||||
title="Error querying page data"
|
title={this.props.title || "Error querying page data"}
|
||||||
icon={<IconAlertTriangle size={14} />}
|
icon={<IconAlertTriangle size={14} />}
|
||||||
maw={500}
|
maw={500}
|
||||||
mx="auto"
|
mx="auto"
|
||||||
|
|
302
web/ui/mantine-ui/src/pages/targets/ScrapePoolsList.tsx
Normal file
302
web/ui/mantine-ui/src/pages/targets/ScrapePoolsList.tsx
Normal file
|
@ -0,0 +1,302 @@
|
||||||
|
import {
|
||||||
|
Accordion,
|
||||||
|
Alert,
|
||||||
|
Badge,
|
||||||
|
Group,
|
||||||
|
RingProgress,
|
||||||
|
Stack,
|
||||||
|
Table,
|
||||||
|
Text,
|
||||||
|
} from "@mantine/core";
|
||||||
|
import { IconAlertTriangle, IconInfoCircle } from "@tabler/icons-react";
|
||||||
|
import { useSuspenseAPIQuery } from "../../api/api";
|
||||||
|
import { Target, TargetsResult } from "../../api/responseTypes/targets";
|
||||||
|
import React, { FC } from "react";
|
||||||
|
import badgeClasses from "../../Badge.module.css";
|
||||||
|
import {
|
||||||
|
humanizeDurationRelative,
|
||||||
|
humanizeDuration,
|
||||||
|
now,
|
||||||
|
} from "../../lib/formatTime";
|
||||||
|
import { LabelBadges } from "../../components/LabelBadges";
|
||||||
|
import { useAppDispatch, useAppSelector } from "../../state/hooks";
|
||||||
|
import { setCollapsedPools } from "../../state/targetsPageSlice";
|
||||||
|
import EndpointLink from "../../components/EndpointLink";
|
||||||
|
import CustomInfiniteScroll from "../../components/CustomInfiniteScroll";
|
||||||
|
|
||||||
|
type ScrapePool = {
|
||||||
|
targets: Target[];
|
||||||
|
count: number;
|
||||||
|
upCount: number;
|
||||||
|
downCount: number;
|
||||||
|
unknownCount: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
type ScrapePools = {
|
||||||
|
[scrapePool: string]: ScrapePool;
|
||||||
|
};
|
||||||
|
|
||||||
|
const healthBadgeClass = (state: string) => {
|
||||||
|
switch (state.toLowerCase()) {
|
||||||
|
case "up":
|
||||||
|
return badgeClasses.healthOk;
|
||||||
|
case "down":
|
||||||
|
return badgeClasses.healthErr;
|
||||||
|
case "unknown":
|
||||||
|
return badgeClasses.healthUnknown;
|
||||||
|
default:
|
||||||
|
return badgeClasses.warn;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const groupTargets = (
|
||||||
|
poolNames: string[],
|
||||||
|
targets: Target[],
|
||||||
|
healthFilter: string[]
|
||||||
|
): ScrapePools => {
|
||||||
|
const pools: ScrapePools = {};
|
||||||
|
|
||||||
|
for (const pn of poolNames) {
|
||||||
|
pools[pn] = {
|
||||||
|
targets: [],
|
||||||
|
count: 0,
|
||||||
|
upCount: 0,
|
||||||
|
downCount: 0,
|
||||||
|
unknownCount: 0,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const target of targets) {
|
||||||
|
if (!pools[target.scrapePool]) {
|
||||||
|
// TODO: Should we do better here?
|
||||||
|
throw new Error(
|
||||||
|
"Received target information for an unknown scrape pool, likely the list of scrape pools has changed. Please reload the page."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
healthFilter.length === 0 ||
|
||||||
|
healthFilter.includes(target.health.toLowerCase())
|
||||||
|
) {
|
||||||
|
pools[target.scrapePool].targets.push(target);
|
||||||
|
}
|
||||||
|
|
||||||
|
pools[target.scrapePool].count++;
|
||||||
|
|
||||||
|
switch (target.health.toLowerCase()) {
|
||||||
|
case "up":
|
||||||
|
pools[target.scrapePool].upCount++;
|
||||||
|
break;
|
||||||
|
case "down":
|
||||||
|
pools[target.scrapePool].downCount++;
|
||||||
|
break;
|
||||||
|
case "unknown":
|
||||||
|
pools[target.scrapePool].unknownCount++;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return pools;
|
||||||
|
};
|
||||||
|
|
||||||
|
type ScrapePoolListProp = {
|
||||||
|
poolNames: string[];
|
||||||
|
selectedPool: string | null;
|
||||||
|
limited: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
const ScrapePoolList: FC<ScrapePoolListProp> = ({
|
||||||
|
poolNames,
|
||||||
|
selectedPool,
|
||||||
|
limited,
|
||||||
|
}) => {
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
|
|
||||||
|
// Based on the selected pool (if any), load the list of targets.
|
||||||
|
const {
|
||||||
|
data: {
|
||||||
|
data: { activeTargets },
|
||||||
|
},
|
||||||
|
} = useSuspenseAPIQuery<TargetsResult>({
|
||||||
|
path: `/targets`,
|
||||||
|
params: {
|
||||||
|
state: "active",
|
||||||
|
scrapePool: selectedPool === null ? "" : selectedPool,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const { healthFilter, collapsedPools } = useAppSelector(
|
||||||
|
(state) => state.targetsPage
|
||||||
|
);
|
||||||
|
|
||||||
|
const allPools = groupTargets(
|
||||||
|
selectedPool ? [selectedPool] : poolNames,
|
||||||
|
activeTargets,
|
||||||
|
healthFilter
|
||||||
|
);
|
||||||
|
const allPoolNames = Object.keys(allPools);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Stack>
|
||||||
|
{allPoolNames.length === 0 && (
|
||||||
|
<Alert title="No matching targets" icon={<IconInfoCircle size={14} />}>
|
||||||
|
No targets found that match your filter criteria.
|
||||||
|
</Alert>
|
||||||
|
)}
|
||||||
|
{limited && (
|
||||||
|
<Alert
|
||||||
|
title="Found many pools, showing only one"
|
||||||
|
icon={<IconInfoCircle size={14} />}
|
||||||
|
>
|
||||||
|
There are more than 20 scrape pools. Showing only the first one. Use
|
||||||
|
the dropdown to select a different pool.
|
||||||
|
</Alert>
|
||||||
|
)}
|
||||||
|
<Accordion
|
||||||
|
multiple
|
||||||
|
variant="separated"
|
||||||
|
value={allPoolNames.filter((p) => !collapsedPools.includes(p))}
|
||||||
|
onChange={(value) =>
|
||||||
|
dispatch(
|
||||||
|
setCollapsedPools(allPoolNames.filter((p) => !value.includes(p)))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{allPoolNames.map((poolName) => {
|
||||||
|
const pool = allPools[poolName];
|
||||||
|
return (
|
||||||
|
<Accordion.Item
|
||||||
|
key={poolName}
|
||||||
|
value={poolName}
|
||||||
|
style={{
|
||||||
|
borderLeft:
|
||||||
|
pool.upCount === 0
|
||||||
|
? "5px solid var(--mantine-color-red-4)"
|
||||||
|
: pool.upCount !== pool.count
|
||||||
|
? "5px solid var(--mantine-color-orange-5)"
|
||||||
|
: "5px solid var(--mantine-color-green-4)",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Accordion.Control>
|
||||||
|
<Group wrap="nowrap" justify="space-between" mr="lg">
|
||||||
|
<Text>{poolName}</Text>
|
||||||
|
<Group gap="xs">
|
||||||
|
<Text c="gray.6">
|
||||||
|
{pool.upCount} / {pool.count} up
|
||||||
|
</Text>
|
||||||
|
<RingProgress
|
||||||
|
size={25}
|
||||||
|
thickness={5}
|
||||||
|
sections={[
|
||||||
|
{
|
||||||
|
value: (pool.upCount / pool.count) * 100,
|
||||||
|
color: "green.4",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: (pool.downCount / pool.count) * 100,
|
||||||
|
color: "red.5",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: (pool.unknownCount / pool.count) * 100,
|
||||||
|
color: "gray.4",
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</Group>
|
||||||
|
</Group>
|
||||||
|
</Accordion.Control>
|
||||||
|
<Accordion.Panel>
|
||||||
|
{pool.count === 0 ? (
|
||||||
|
<Alert title="No targets" icon={<IconInfoCircle />}>
|
||||||
|
No targets in this scrape pool.
|
||||||
|
</Alert>
|
||||||
|
) : pool.targets.length === 0 ? (
|
||||||
|
<Alert title="No matching targets" icon={<IconInfoCircle />}>
|
||||||
|
No targets in this pool match your filter criteria (omitted{" "}
|
||||||
|
{pool.count} filtered targets).
|
||||||
|
</Alert>
|
||||||
|
) : (
|
||||||
|
<CustomInfiniteScroll
|
||||||
|
allItems={pool.targets}
|
||||||
|
child={({ items }) => (
|
||||||
|
<Table>
|
||||||
|
<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.Tr>
|
||||||
|
</Table.Thead>
|
||||||
|
<Table.Tbody>
|
||||||
|
{items.map((target, i) => (
|
||||||
|
// TODO: Find a stable and definitely unique key.
|
||||||
|
<React.Fragment key={i}>
|
||||||
|
<Table.Tr
|
||||||
|
style={{
|
||||||
|
borderBottom: target.lastError
|
||||||
|
? "none"
|
||||||
|
: undefined,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Table.Td>
|
||||||
|
{/* TODO: Process target URL like in old UI */}
|
||||||
|
<EndpointLink
|
||||||
|
endpoint={target.scrapeUrl}
|
||||||
|
globalUrl={target.globalUrl}
|
||||||
|
/>
|
||||||
|
</Table.Td>
|
||||||
|
<Table.Td>
|
||||||
|
<Badge
|
||||||
|
className={healthBadgeClass(target.health)}
|
||||||
|
>
|
||||||
|
{target.health}
|
||||||
|
</Badge>
|
||||||
|
</Table.Td>
|
||||||
|
<Table.Td>
|
||||||
|
<LabelBadges labels={target.labels} />
|
||||||
|
</Table.Td>
|
||||||
|
<Table.Td>
|
||||||
|
{humanizeDurationRelative(
|
||||||
|
target.lastScrape,
|
||||||
|
now()
|
||||||
|
)}
|
||||||
|
</Table.Td>
|
||||||
|
<Table.Td>
|
||||||
|
{humanizeDuration(
|
||||||
|
target.lastScrapeDuration * 1000
|
||||||
|
)}
|
||||||
|
</Table.Td>
|
||||||
|
</Table.Tr>
|
||||||
|
{target.lastError && (
|
||||||
|
<Table.Tr>
|
||||||
|
<Table.Td colSpan={5}>
|
||||||
|
<Alert
|
||||||
|
color="red"
|
||||||
|
mb="sm"
|
||||||
|
icon={<IconAlertTriangle size={14} />}
|
||||||
|
>
|
||||||
|
<strong>Error scraping target:</strong>{" "}
|
||||||
|
{target.lastError}
|
||||||
|
</Alert>
|
||||||
|
</Table.Td>
|
||||||
|
</Table.Tr>
|
||||||
|
)}
|
||||||
|
</React.Fragment>
|
||||||
|
))}
|
||||||
|
</Table.Tbody>
|
||||||
|
</Table>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Accordion.Panel>
|
||||||
|
</Accordion.Item>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</Accordion>
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ScrapePoolList;
|
134
web/ui/mantine-ui/src/pages/targets/TargetsPage.tsx
Normal file
134
web/ui/mantine-ui/src/pages/targets/TargetsPage.tsx
Normal file
|
@ -0,0 +1,134 @@
|
||||||
|
import {
|
||||||
|
ActionIcon,
|
||||||
|
Box,
|
||||||
|
Checkbox,
|
||||||
|
Group,
|
||||||
|
Input,
|
||||||
|
Select,
|
||||||
|
Skeleton,
|
||||||
|
} from "@mantine/core";
|
||||||
|
import {
|
||||||
|
IconLayoutNavbarCollapse,
|
||||||
|
IconLayoutNavbarExpand,
|
||||||
|
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 badgeClasses from "../../Badge.module.css";
|
||||||
|
import { useAppDispatch, useAppSelector } from "../../state/hooks";
|
||||||
|
import {
|
||||||
|
setCollapsedPools,
|
||||||
|
setHealthFilter,
|
||||||
|
setSelectedPool,
|
||||||
|
} from "../../state/targetsPageSlice";
|
||||||
|
import ScrapePoolList from "./ScrapePoolsList";
|
||||||
|
import ErrorBoundary from "../../components/ErrorBoundary";
|
||||||
|
|
||||||
|
const scrapePoolQueryParam = "scrapePool";
|
||||||
|
|
||||||
|
export default function TargetsPage() {
|
||||||
|
// Load the list of all available scrape pools.
|
||||||
|
const {
|
||||||
|
data: {
|
||||||
|
data: { scrapePools },
|
||||||
|
},
|
||||||
|
} = useSuspenseAPIQuery<ScrapePoolsResult>({
|
||||||
|
path: `/scrape_pools`,
|
||||||
|
});
|
||||||
|
|
||||||
|
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 { selectedPool, healthFilter, collapsedPools } = useAppSelector(
|
||||||
|
(state) => state.targetsPage
|
||||||
|
);
|
||||||
|
|
||||||
|
let poolToShow = selectedPool;
|
||||||
|
let limitedDueToManyPools = false;
|
||||||
|
|
||||||
|
if (poolToShow === null && scrapePools.length > 20) {
|
||||||
|
poolToShow = scrapePools[0];
|
||||||
|
limitedDueToManyPools = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Group mb="md" mt="xs">
|
||||||
|
<Select
|
||||||
|
placeholder="Select scrape pool"
|
||||||
|
data={[{ label: "All pools", value: "" }, ...scrapePools]}
|
||||||
|
value={selectedPool}
|
||||||
|
onChange={(value) => dispatch(setSelectedPool(value || null))}
|
||||||
|
searchable
|
||||||
|
/>
|
||||||
|
<StateMultiSelect
|
||||||
|
options={["unknown", "up", "down"]}
|
||||||
|
optionClass={(o) =>
|
||||||
|
o === "unknown"
|
||||||
|
? badgeClasses.healthUnknown
|
||||||
|
: o === "up"
|
||||||
|
? badgeClasses.healthOk
|
||||||
|
: badgeClasses.healthErr
|
||||||
|
}
|
||||||
|
placeholder="Filter by target state"
|
||||||
|
values={healthFilter}
|
||||||
|
onChange={(values) => dispatch(setHealthFilter(values))}
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
flex={1}
|
||||||
|
leftSection={<IconSearch size={14} />}
|
||||||
|
placeholder="Filter by endpoint or labels"
|
||||||
|
></Input>
|
||||||
|
<ActionIcon
|
||||||
|
size="input-sm"
|
||||||
|
title={
|
||||||
|
collapsedPools.length > 0
|
||||||
|
? "Expand all pools"
|
||||||
|
: "Collapse all pools"
|
||||||
|
}
|
||||||
|
variant="light"
|
||||||
|
onClick={() =>
|
||||||
|
dispatch(
|
||||||
|
setCollapsedPools(collapsedPools.length > 0 ? [] : scrapePools)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{collapsedPools.length > 0 ? (
|
||||||
|
<IconLayoutNavbarExpand size={16} />
|
||||||
|
) : (
|
||||||
|
<IconLayoutNavbarCollapse size={16} />
|
||||||
|
)}
|
||||||
|
</ActionIcon>
|
||||||
|
</Group>
|
||||||
|
|
||||||
|
<ErrorBoundary key={location.pathname} title="Error showing target pools">
|
||||||
|
<Suspense
|
||||||
|
fallback={
|
||||||
|
<Box mt="lg">
|
||||||
|
{Array.from(Array(10), (_, i) => (
|
||||||
|
<Skeleton key={i} height={40} mb={15} width={1000} mx="auto" />
|
||||||
|
))}
|
||||||
|
</Box>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<ScrapePoolList
|
||||||
|
poolNames={scrapePools}
|
||||||
|
selectedPool={poolToShow}
|
||||||
|
limited={limitedDueToManyPools}
|
||||||
|
/>
|
||||||
|
</Suspense>
|
||||||
|
</ErrorBoundary>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
|
@ -18,25 +18,26 @@ import {
|
||||||
IconLayoutNavbarExpand,
|
IconLayoutNavbarExpand,
|
||||||
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 { useSuspenseAPIQuery } from "../../api/api";
|
||||||
import { ScrapePoolsResult } from "../api/responseTypes/scrapePools";
|
import { ScrapePoolsResult } from "../../api/responseTypes/scrapePools";
|
||||||
import { Target, TargetsResult } from "../api/responseTypes/targets";
|
import { Target, TargetsResult } from "../../api/responseTypes/targets";
|
||||||
import React from "react";
|
import React, { useEffect } from "react";
|
||||||
import badgeClasses from "../Badge.module.css";
|
import badgeClasses from "../../Badge.module.css";
|
||||||
import {
|
import {
|
||||||
humanizeDurationRelative,
|
humanizeDurationRelative,
|
||||||
humanizeDuration,
|
humanizeDuration,
|
||||||
now,
|
now,
|
||||||
} from "../lib/formatTime";
|
} from "../../lib/formatTime";
|
||||||
import { LabelBadges } from "../components/LabelBadges";
|
import { LabelBadges } from "../../components/LabelBadges";
|
||||||
import { useAppDispatch, useAppSelector } from "../state/hooks";
|
import { useAppDispatch, useAppSelector } from "../../state/hooks";
|
||||||
import {
|
import {
|
||||||
setCollapsedPools,
|
setCollapsedPools,
|
||||||
updateTargetFilters,
|
updateTargetFilters,
|
||||||
} from "../state/targetsPageSlice";
|
} from "../../state/targetsPageSlice";
|
||||||
import EndpointLink from "../components/EndpointLink";
|
import EndpointLink from "../../components/EndpointLink";
|
||||||
import CustomInfiniteScroll from "../components/CustomInfiniteScroll";
|
import CustomInfiniteScroll from "../../components/CustomInfiniteScroll";
|
||||||
|
import { filter } from "lodash";
|
||||||
|
|
||||||
type ScrapePool = {
|
type ScrapePool = {
|
||||||
targets: Target[];
|
targets: Target[];
|
||||||
|
@ -89,7 +90,10 @@ const groupTargets = (targets: Target[]): ScrapePools => {
|
||||||
return pools;
|
return pools;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const scrapePoolQueryParam = "scrapePool";
|
||||||
|
|
||||||
export default function TargetsPage() {
|
export default function TargetsPage() {
|
||||||
|
// Load the list of all available scrape pools.
|
||||||
const {
|
const {
|
||||||
data: {
|
data: {
|
||||||
data: { scrapePools },
|
data: { scrapePools },
|
||||||
|
@ -98,6 +102,29 @@ export default function TargetsPage() {
|
||||||
path: `/scrape_pools`,
|
path: `/scrape_pools`,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
|
|
||||||
|
// If there is a selected pool in the URL, extract it on initial load.
|
||||||
|
useEffect(() => {
|
||||||
|
const selectedPool = new URLSearchParams(window.location.search).get(
|
||||||
|
scrapePoolQueryParam
|
||||||
|
);
|
||||||
|
if (selectedPool !== null) {
|
||||||
|
dispatch(updateTargetFilters({ scrapePool: selectedPool }));
|
||||||
|
}
|
||||||
|
}, [dispatch]);
|
||||||
|
|
||||||
|
const filters = useAppSelector((state) => state.targetsPage.filters);
|
||||||
|
|
||||||
|
let poolToShow = filters.scrapePool;
|
||||||
|
let limitedDueToManyPools = false;
|
||||||
|
|
||||||
|
if (poolToShow === null && scrapePools.length > 20) {
|
||||||
|
poolToShow = scrapePools[0];
|
||||||
|
limitedDueToManyPools = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Based on the selected pool (if any), load the list of targets.
|
||||||
const {
|
const {
|
||||||
data: {
|
data: {
|
||||||
data: { activeTargets },
|
data: { activeTargets },
|
||||||
|
@ -106,11 +133,10 @@ export default function TargetsPage() {
|
||||||
path: `/targets`,
|
path: `/targets`,
|
||||||
params: {
|
params: {
|
||||||
state: "active",
|
state: "active",
|
||||||
|
scrapePool: poolToShow === null ? "" : poolToShow,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const dispatch = useAppDispatch();
|
|
||||||
const filters = useAppSelector((state) => state.targetsPage.filters);
|
|
||||||
const collapsedPools = useAppSelector(
|
const collapsedPools = useAppSelector(
|
||||||
(state) => state.targetsPage.collapsedPools
|
(state) => state.targetsPage.collapsedPools
|
||||||
);
|
);
|
||||||
|
@ -123,7 +149,12 @@ export default function TargetsPage() {
|
||||||
<Group mb="md" mt="xs">
|
<Group mb="md" mt="xs">
|
||||||
<Select
|
<Select
|
||||||
placeholder="Select scrape pool"
|
placeholder="Select scrape pool"
|
||||||
data={["All pools", ...scrapePools]}
|
data={[{ label: "All pools", value: "" }, ...scrapePools]}
|
||||||
|
value={filters.scrapePool}
|
||||||
|
onChange={(value) =>
|
||||||
|
dispatch(updateTargetFilters({ scrapePool: value || null }))
|
||||||
|
}
|
||||||
|
searchable
|
||||||
/>
|
/>
|
||||||
<StateMultiSelect
|
<StateMultiSelect
|
||||||
options={["unknown", "up", "down"]}
|
options={["unknown", "up", "down"]}
|
||||||
|
@ -131,8 +162,8 @@ export default function TargetsPage() {
|
||||||
o === "unknown"
|
o === "unknown"
|
||||||
? badgeClasses.healthUnknown
|
? badgeClasses.healthUnknown
|
||||||
: o === "up"
|
: o === "up"
|
||||||
? badgeClasses.healthOk
|
? badgeClasses.healthOk
|
||||||
: badgeClasses.healthErr
|
: badgeClasses.healthErr
|
||||||
}
|
}
|
||||||
placeholder="Filter by target state"
|
placeholder="Filter by target state"
|
||||||
values={filters.health}
|
values={filters.health}
|
||||||
|
@ -171,6 +202,15 @@ export default function TargetsPage() {
|
||||||
No targets found that match your filter criteria.
|
No targets found that match your filter criteria.
|
||||||
</Alert>
|
</Alert>
|
||||||
)}
|
)}
|
||||||
|
{limitedDueToManyPools && (
|
||||||
|
<Alert
|
||||||
|
title="Found many pools, showing only one"
|
||||||
|
icon={<IconInfoCircle size={14} />}
|
||||||
|
>
|
||||||
|
There are many scrape pools configured. Showing only the first one.
|
||||||
|
Use the dropdown to select a different pool.
|
||||||
|
</Alert>
|
||||||
|
)}
|
||||||
<Accordion
|
<Accordion
|
||||||
multiple
|
multiple
|
||||||
variant="separated"
|
variant="separated"
|
||||||
|
@ -192,8 +232,8 @@ export default function TargetsPage() {
|
||||||
pool.upCount === 0
|
pool.upCount === 0
|
||||||
? "5px solid var(--mantine-color-red-4)"
|
? "5px solid var(--mantine-color-red-4)"
|
||||||
: pool.upCount !== pool.targets.length
|
: pool.upCount !== pool.targets.length
|
||||||
? "5px solid var(--mantine-color-orange-5)"
|
? "5px solid var(--mantine-color-orange-5)"
|
||||||
: "5px solid var(--mantine-color-green-4)",
|
: "5px solid var(--mantine-color-green-4)",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Accordion.Control>
|
<Accordion.Control>
|
|
@ -2,9 +2,9 @@ import { createListenerMiddleware } from "@reduxjs/toolkit";
|
||||||
import { AppDispatch, RootState } from "./store";
|
import { AppDispatch, RootState } from "./store";
|
||||||
import {
|
import {
|
||||||
localStorageKeyCollapsedPools,
|
localStorageKeyCollapsedPools,
|
||||||
localStorageKeyTargetFilters,
|
localStorageKeyTargetHealthFilter,
|
||||||
setCollapsedPools,
|
setCollapsedPools,
|
||||||
updateTargetFilters,
|
setHealthFilter,
|
||||||
} from "./targetsPageSlice";
|
} from "./targetsPageSlice";
|
||||||
import { updateSettings } from "./settingsSlice";
|
import { updateSettings } from "./settingsSlice";
|
||||||
|
|
||||||
|
@ -27,9 +27,9 @@ startAppListening({
|
||||||
});
|
});
|
||||||
|
|
||||||
startAppListening({
|
startAppListening({
|
||||||
actionCreator: updateTargetFilters,
|
actionCreator: setHealthFilter,
|
||||||
effect: ({ payload }) => {
|
effect: ({ payload }) => {
|
||||||
persistToLocalStorage(localStorageKeyTargetFilters, payload);
|
persistToLocalStorage(localStorageKeyTargetHealthFilter, payload);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -2,25 +2,19 @@ import { PayloadAction, createSlice } from "@reduxjs/toolkit";
|
||||||
import { initializeFromLocalStorage } from "./initializeFromLocalStorage";
|
import { initializeFromLocalStorage } from "./initializeFromLocalStorage";
|
||||||
|
|
||||||
export const localStorageKeyCollapsedPools = "targetsPage.collapsedPools";
|
export const localStorageKeyCollapsedPools = "targetsPage.collapsedPools";
|
||||||
export const localStorageKeyTargetFilters = "targetsPage.filters";
|
export const localStorageKeyTargetHealthFilter = "targetsPage.healthFilter";
|
||||||
|
|
||||||
interface TargetFilters {
|
|
||||||
scrapePool: string | null;
|
|
||||||
health: string[];
|
|
||||||
}
|
|
||||||
|
|
||||||
interface TargetsPage {
|
interface TargetsPage {
|
||||||
filters: TargetFilters;
|
selectedPool: string | null;
|
||||||
|
healthFilter: string[];
|
||||||
collapsedPools: string[];
|
collapsedPools: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
const initialState: TargetsPage = {
|
const initialState: TargetsPage = {
|
||||||
filters: initializeFromLocalStorage<TargetFilters>(
|
selectedPool: null,
|
||||||
localStorageKeyTargetFilters,
|
healthFilter: initializeFromLocalStorage<string[]>(
|
||||||
{
|
localStorageKeyTargetHealthFilter,
|
||||||
scrapePool: null,
|
[]
|
||||||
health: [],
|
|
||||||
}
|
|
||||||
),
|
),
|
||||||
collapsedPools: initializeFromLocalStorage<string[]>(
|
collapsedPools: initializeFromLocalStorage<string[]>(
|
||||||
localStorageKeyCollapsedPools,
|
localStorageKeyCollapsedPools,
|
||||||
|
@ -32,11 +26,11 @@ export const targetsPageSlice = createSlice({
|
||||||
name: "targetsPage",
|
name: "targetsPage",
|
||||||
initialState,
|
initialState,
|
||||||
reducers: {
|
reducers: {
|
||||||
updateTargetFilters: (
|
setSelectedPool: (state, { payload }: PayloadAction<string | null>) => {
|
||||||
state,
|
state.selectedPool = payload;
|
||||||
{ payload }: PayloadAction<Partial<TargetFilters>>
|
},
|
||||||
) => {
|
setHealthFilter: (state, { payload }: PayloadAction<string[]>) => {
|
||||||
Object.assign(state.filters, payload);
|
state.healthFilter = payload;
|
||||||
},
|
},
|
||||||
setCollapsedPools: (state, { payload }: PayloadAction<string[]>) => {
|
setCollapsedPools: (state, { payload }: PayloadAction<string[]>) => {
|
||||||
state.collapsedPools = payload;
|
state.collapsedPools = payload;
|
||||||
|
@ -44,7 +38,7 @@ export const targetsPageSlice = createSlice({
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export const { updateTargetFilters, setCollapsedPools } =
|
export const { setSelectedPool, setHealthFilter, setCollapsedPools } =
|
||||||
targetsPageSlice.actions;
|
targetsPageSlice.actions;
|
||||||
|
|
||||||
export default targetsPageSlice.reducer;
|
export default targetsPageSlice.reducer;
|
||||||
|
|
Loading…
Reference in a new issue