mirror of
https://github.com/prometheus/prometheus.git
synced 2025-01-30 07:03:06 -08:00
Add initial Service Discovery page
Signed-off-by: Julius Volz <julius.volz@gmail.com>
This commit is contained in:
parent
73a328f37b
commit
d22e721d39
|
@ -46,7 +46,6 @@ 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/targets/TargetsPage";
|
import TargetsPage from "./pages/targets/TargetsPage";
|
||||||
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";
|
||||||
import FlagsPage from "./pages/FlagsPage";
|
import FlagsPage from "./pages/FlagsPage";
|
||||||
|
@ -62,6 +61,7 @@ import SettingsMenu from "./components/SettingsMenu";
|
||||||
import ReadinessWrapper from "./components/ReadinessWrapper";
|
import ReadinessWrapper from "./components/ReadinessWrapper";
|
||||||
import { QueryParamProvider } from "use-query-params";
|
import { QueryParamProvider } from "use-query-params";
|
||||||
import { ReactRouter6Adapter } from "use-query-params/adapters/react-router-6";
|
import { ReactRouter6Adapter } from "use-query-params/adapters/react-router-6";
|
||||||
|
import ServiceDiscoveryPage from "./pages/service-discovery/ServiceDiscoveryPage";
|
||||||
|
|
||||||
const queryClient = new QueryClient();
|
const queryClient = new QueryClient();
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { Badge, BadgeVariant, Group, MantineColor } from "@mantine/core";
|
import { Badge, BadgeVariant, Group, MantineColor, Stack } from "@mantine/core";
|
||||||
import { FC } from "react";
|
import { FC } from "react";
|
||||||
import { escapeString } from "../lib/escapeString";
|
import { escapeString } from "../lib/escapeString";
|
||||||
import badgeClasses from "../Badge.module.css";
|
import badgeClasses from "../Badge.module.css";
|
||||||
|
@ -7,14 +7,16 @@ export interface LabelBadgesProps {
|
||||||
labels: Record<string, string>;
|
labels: Record<string, string>;
|
||||||
variant?: BadgeVariant;
|
variant?: BadgeVariant;
|
||||||
color?: MantineColor;
|
color?: MantineColor;
|
||||||
|
wrapper?: typeof Group | typeof Stack;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const LabelBadges: FC<LabelBadgesProps> = ({
|
export const LabelBadges: FC<LabelBadgesProps> = ({
|
||||||
labels,
|
labels,
|
||||||
variant,
|
variant,
|
||||||
color,
|
color,
|
||||||
|
wrapper: Wrapper = Group,
|
||||||
}) => (
|
}) => (
|
||||||
<Group gap="xs">
|
<Wrapper gap="xs">
|
||||||
{Object.entries(labels).map(([k, v]) => {
|
{Object.entries(labels).map(([k, v]) => {
|
||||||
return (
|
return (
|
||||||
<Badge
|
<Badge
|
||||||
|
@ -32,5 +34,5 @@ export const LabelBadges: FC<LabelBadgesProps> = ({
|
||||||
</Badge>
|
</Badge>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</Group>
|
</Wrapper>
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,3 +0,0 @@
|
||||||
export default function ServiceDiscoveryPage() {
|
|
||||||
return <>ServiceDiscovery page</>;
|
|
||||||
}
|
|
|
@ -0,0 +1,127 @@
|
||||||
|
import {
|
||||||
|
ActionIcon,
|
||||||
|
Box,
|
||||||
|
Group,
|
||||||
|
Select,
|
||||||
|
Skeleton,
|
||||||
|
TextInput,
|
||||||
|
} from "@mantine/core";
|
||||||
|
import {
|
||||||
|
IconLayoutNavbarCollapse,
|
||||||
|
IconLayoutNavbarExpand,
|
||||||
|
IconSearch,
|
||||||
|
} from "@tabler/icons-react";
|
||||||
|
import { Suspense } from "react";
|
||||||
|
import { useAppDispatch, useAppSelector } from "../../state/hooks";
|
||||||
|
|
||||||
|
import { StringParam, useQueryParam, withDefault } from "use-query-params";
|
||||||
|
import ErrorBoundary from "../../components/ErrorBoundary";
|
||||||
|
import ScrapePoolList from "./ServiceDiscoveryPoolsList";
|
||||||
|
import { useSuspenseAPIQuery } from "../../api/api";
|
||||||
|
import { ScrapePoolsResult } from "../../api/responseTypes/scrapePools";
|
||||||
|
import {
|
||||||
|
setCollapsedPools,
|
||||||
|
setShowLimitAlert,
|
||||||
|
} from "../../state/serviceDiscoveryPageSlice";
|
||||||
|
|
||||||
|
export const targetPoolDisplayLimit = 20;
|
||||||
|
|
||||||
|
export default function ServiceDiscoveryPage() {
|
||||||
|
// Load the list of all available scrape pools.
|
||||||
|
const {
|
||||||
|
data: {
|
||||||
|
data: { scrapePools },
|
||||||
|
},
|
||||||
|
} = useSuspenseAPIQuery<ScrapePoolsResult>({
|
||||||
|
path: `/scrape_pools`,
|
||||||
|
});
|
||||||
|
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
|
|
||||||
|
const [scrapePool, setScrapePool] = useQueryParam("scrapePool", StringParam);
|
||||||
|
const [searchFilter, setSearchFilter] = useQueryParam(
|
||||||
|
"searchFilter",
|
||||||
|
withDefault(StringParam, "")
|
||||||
|
);
|
||||||
|
|
||||||
|
const { collapsedPools, showLimitAlert } = useAppSelector(
|
||||||
|
(state) => state.serviceDiscoveryPage
|
||||||
|
);
|
||||||
|
|
||||||
|
// 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 (
|
||||||
|
<>
|
||||||
|
<Group mb="md" mt="xs">
|
||||||
|
<Select
|
||||||
|
placeholder="Select scrape pool"
|
||||||
|
data={[{ label: "All pools", value: "" }, ...scrapePools]}
|
||||||
|
value={(limited && scrapePools[0]) || scrapePool || null}
|
||||||
|
onChange={(value) => {
|
||||||
|
setScrapePool(value);
|
||||||
|
showLimitAlert && dispatch(setShowLimitAlert(false));
|
||||||
|
}}
|
||||||
|
searchable
|
||||||
|
/>
|
||||||
|
<TextInput
|
||||||
|
flex={1}
|
||||||
|
leftSection={<IconSearch size={14} />}
|
||||||
|
placeholder="Filter by labels"
|
||||||
|
value={searchFilter || ""}
|
||||||
|
onChange={(event) => setSearchFilter(event.currentTarget.value)}
|
||||||
|
></TextInput>
|
||||||
|
<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={(limited && scrapePools[0]) || scrapePool || null}
|
||||||
|
searchFilter={searchFilter}
|
||||||
|
/>
|
||||||
|
</Suspense>
|
||||||
|
</ErrorBoundary>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
|
@ -0,0 +1,334 @@
|
||||||
|
import {
|
||||||
|
Accordion,
|
||||||
|
Alert,
|
||||||
|
Anchor,
|
||||||
|
Group,
|
||||||
|
RingProgress,
|
||||||
|
Stack,
|
||||||
|
Table,
|
||||||
|
Text,
|
||||||
|
} from "@mantine/core";
|
||||||
|
import { KVSearch } from "@nexucis/kvsearch";
|
||||||
|
import { IconInfoCircle } from "@tabler/icons-react";
|
||||||
|
import { useSuspenseAPIQuery } from "../../api/api";
|
||||||
|
import {
|
||||||
|
DroppedTarget,
|
||||||
|
Labels,
|
||||||
|
Target,
|
||||||
|
TargetsResult,
|
||||||
|
} from "../../api/responseTypes/targets";
|
||||||
|
import { FC } from "react";
|
||||||
|
import { useAppDispatch, useAppSelector } from "../../state/hooks";
|
||||||
|
import {
|
||||||
|
setCollapsedPools,
|
||||||
|
setShowLimitAlert,
|
||||||
|
} from "../../state/serviceDiscoveryPageSlice";
|
||||||
|
import CustomInfiniteScroll from "../../components/CustomInfiniteScroll";
|
||||||
|
|
||||||
|
import TargetLabels from "./TargetLabels";
|
||||||
|
import { useDebouncedValue } from "@mantine/hooks";
|
||||||
|
import { targetPoolDisplayLimit } from "./ServiceDiscoveryPage";
|
||||||
|
import { BooleanParam, useQueryParam, withDefault } from "use-query-params";
|
||||||
|
import { LabelBadges } from "../../components/LabelBadges";
|
||||||
|
|
||||||
|
type TargetLabels = {
|
||||||
|
discoveredLabels: Labels;
|
||||||
|
labels: Labels;
|
||||||
|
isDropped: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
type ScrapePool = {
|
||||||
|
targets: TargetLabels[];
|
||||||
|
active: number;
|
||||||
|
total: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
type ScrapePools = {
|
||||||
|
[scrapePool: string]: ScrapePool;
|
||||||
|
};
|
||||||
|
|
||||||
|
const activeTargetKVSearch = new KVSearch<Target>({
|
||||||
|
shouldSort: true,
|
||||||
|
indexedKeys: [
|
||||||
|
"labels",
|
||||||
|
"discoveredLabels",
|
||||||
|
["discoveredLabels", /.*/],
|
||||||
|
["labels", /.*/],
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
const droppedTargetKVSearch = new KVSearch<DroppedTarget>({
|
||||||
|
shouldSort: true,
|
||||||
|
indexedKeys: ["discoveredLabels", ["discoveredLabels", /.*/]],
|
||||||
|
});
|
||||||
|
|
||||||
|
const groupTargets = (
|
||||||
|
poolNames: string[],
|
||||||
|
activeTargets: Target[],
|
||||||
|
droppedTargets: DroppedTarget[]
|
||||||
|
): ScrapePools => {
|
||||||
|
const pools: ScrapePools = {};
|
||||||
|
|
||||||
|
for (const pn of poolNames) {
|
||||||
|
pools[pn] = {
|
||||||
|
targets: [],
|
||||||
|
active: 0,
|
||||||
|
total: 0,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const target of activeTargets) {
|
||||||
|
const pool = pools[target.scrapePool];
|
||||||
|
if (!pool) {
|
||||||
|
// 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."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
pool.active++;
|
||||||
|
pool.total++;
|
||||||
|
|
||||||
|
pool.targets.push({
|
||||||
|
discoveredLabels: target.discoveredLabels,
|
||||||
|
labels: target.labels,
|
||||||
|
isDropped: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const target of droppedTargets) {
|
||||||
|
const { job: poolName } = target.discoveredLabels;
|
||||||
|
const pool = pools[poolName];
|
||||||
|
if (!pool) {
|
||||||
|
// 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."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
pool.total++;
|
||||||
|
|
||||||
|
pool.targets.push({
|
||||||
|
discoveredLabels: target.discoveredLabels,
|
||||||
|
isDropped: true,
|
||||||
|
labels: {},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// for (const target of shownTargets) {
|
||||||
|
// pools[target.scrapePool].targets.push(target);
|
||||||
|
// }
|
||||||
|
|
||||||
|
return pools;
|
||||||
|
};
|
||||||
|
|
||||||
|
type ScrapePoolListProp = {
|
||||||
|
poolNames: string[];
|
||||||
|
selectedPool: string | null;
|
||||||
|
searchFilter: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const ScrapePoolList: FC<ScrapePoolListProp> = ({
|
||||||
|
poolNames,
|
||||||
|
selectedPool,
|
||||||
|
searchFilter,
|
||||||
|
}) => {
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
|
const [showEmptyPools, setShowEmptyPools] = useQueryParam(
|
||||||
|
"showEmptyPools",
|
||||||
|
withDefault(BooleanParam, true)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Based on the selected pool (if any), load the list of targets.
|
||||||
|
const {
|
||||||
|
data: {
|
||||||
|
data: { activeTargets, droppedTargets },
|
||||||
|
},
|
||||||
|
} = useSuspenseAPIQuery<TargetsResult>({
|
||||||
|
path: `/targets`,
|
||||||
|
params: {
|
||||||
|
scrapePool: selectedPool === null ? "" : selectedPool,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const { collapsedPools, showLimitAlert } = useAppSelector(
|
||||||
|
(state) => state.serviceDiscoveryPage
|
||||||
|
);
|
||||||
|
|
||||||
|
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 ||
|
||||||
|
// healthFilter.includes(target.health.toLowerCase())
|
||||||
|
// );
|
||||||
|
// const filteredTargets =
|
||||||
|
// search === ""
|
||||||
|
// ? healthFilteredTargets
|
||||||
|
// : kvSearch
|
||||||
|
// .filter(search, healthFilteredTargets)
|
||||||
|
// .map((value) => value.original);
|
||||||
|
|
||||||
|
const allPools = groupTargets(
|
||||||
|
selectedPool ? [selectedPool] : poolNames,
|
||||||
|
activeTargets,
|
||||||
|
droppedTargets
|
||||||
|
);
|
||||||
|
const allPoolNames = Object.keys(allPools);
|
||||||
|
const shownPoolNames = showEmptyPools
|
||||||
|
? allPoolNames
|
||||||
|
: allPoolNames.filter((pn) => allPools[pn].targets.length !== 0);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Stack>
|
||||||
|
{allPoolNames.length === 0 ? (
|
||||||
|
<Alert
|
||||||
|
title="No scrape pools found"
|
||||||
|
icon={<IconInfoCircle size={14} />}
|
||||||
|
>
|
||||||
|
No scrape pools found.
|
||||||
|
</Alert>
|
||||||
|
) : (
|
||||||
|
!showEmptyPools &&
|
||||||
|
allPoolNames.length !== shownPoolNames.length && (
|
||||||
|
<Alert
|
||||||
|
title="Hiding pools with no matching targets"
|
||||||
|
icon={<IconInfoCircle size={14} />}
|
||||||
|
>
|
||||||
|
Hiding {allPoolNames.length - shownPoolNames.length} empty pools due
|
||||||
|
to filters or no targets.
|
||||||
|
<Anchor ml="md" fz="1em" onClick={() => setShowEmptyPools(true)}>
|
||||||
|
Show empty pools
|
||||||
|
</Anchor>
|
||||||
|
</Alert>
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
{showLimitAlert && (
|
||||||
|
<Alert
|
||||||
|
title="Found many pools, showing only one"
|
||||||
|
icon={<IconInfoCircle size={14} />}
|
||||||
|
withCloseButton
|
||||||
|
onClose={() => dispatch(setShowLimitAlert(false))}
|
||||||
|
>
|
||||||
|
There are more than {targetPoolDisplayLimit} 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)))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{shownPoolNames.map((poolName) => {
|
||||||
|
const pool = allPools[poolName];
|
||||||
|
return (
|
||||||
|
<Accordion.Item key={poolName} value={poolName}>
|
||||||
|
<Accordion.Control>
|
||||||
|
<Group wrap="nowrap" justify="space-between" mr="lg">
|
||||||
|
<Text>{poolName}</Text>
|
||||||
|
<Group gap="xs">
|
||||||
|
<Text c="gray.6">
|
||||||
|
{pool.active} / {pool.total}
|
||||||
|
</Text>
|
||||||
|
<RingProgress
|
||||||
|
size={25}
|
||||||
|
thickness={5}
|
||||||
|
sections={[
|
||||||
|
{
|
||||||
|
value: (pool.active / pool.total) * 100,
|
||||||
|
color: "green.4",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value:
|
||||||
|
((pool.total - pool.active) / pool.total) * 100,
|
||||||
|
color: "grape.4",
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</Group>
|
||||||
|
</Group>
|
||||||
|
</Accordion.Control>
|
||||||
|
<Accordion.Panel>
|
||||||
|
{pool.total === 0 ? (
|
||||||
|
<Alert title="No targets" icon={<IconInfoCircle />}>
|
||||||
|
No targets in this scrape pool.
|
||||||
|
<Anchor
|
||||||
|
ml="md"
|
||||||
|
fz="1em"
|
||||||
|
onClick={() => setShowEmptyPools(false)}
|
||||||
|
>
|
||||||
|
Hide empty pools
|
||||||
|
</Anchor>
|
||||||
|
</Alert>
|
||||||
|
) : pool.targets.length === 0 ? (
|
||||||
|
<Alert title="No matching targets" icon={<IconInfoCircle />}>
|
||||||
|
No targets in this pool match your filter criteria (omitted{" "}
|
||||||
|
{pool.total} filtered targets).
|
||||||
|
<Anchor
|
||||||
|
ml="md"
|
||||||
|
fz="1em"
|
||||||
|
onClick={() => setShowEmptyPools(false)}
|
||||||
|
>
|
||||||
|
Hide empty pools
|
||||||
|
</Anchor>
|
||||||
|
</Alert>
|
||||||
|
) : (
|
||||||
|
<CustomInfiniteScroll
|
||||||
|
allItems={pool.targets}
|
||||||
|
child={({ items }) => (
|
||||||
|
<Table>
|
||||||
|
<Table.Thead>
|
||||||
|
<Table.Tr>
|
||||||
|
<Table.Th w="50%">Discovered labels</Table.Th>
|
||||||
|
<Table.Th w="50%">Target labels</Table.Th>
|
||||||
|
</Table.Tr>
|
||||||
|
</Table.Thead>
|
||||||
|
<Table.Tbody>
|
||||||
|
{items.map((target, i) => (
|
||||||
|
// TODO: Find a stable and definitely unique key.
|
||||||
|
<Table.Tr key={i}>
|
||||||
|
<Table.Td py="lg" valign="top">
|
||||||
|
<LabelBadges
|
||||||
|
labels={target.discoveredLabels}
|
||||||
|
wrapper={Stack}
|
||||||
|
/>
|
||||||
|
</Table.Td>
|
||||||
|
<Table.Td
|
||||||
|
py="lg"
|
||||||
|
valign={target.isDropped ? "middle" : "top"}
|
||||||
|
>
|
||||||
|
{target.isDropped ? (
|
||||||
|
<Text c="blue.6" fw="bold">
|
||||||
|
dropped due to relabeling rules
|
||||||
|
</Text>
|
||||||
|
) : (
|
||||||
|
<LabelBadges
|
||||||
|
labels={target.labels}
|
||||||
|
wrapper={Stack}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Table.Td>
|
||||||
|
</Table.Tr>
|
||||||
|
))}
|
||||||
|
</Table.Tbody>
|
||||||
|
</Table>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Accordion.Panel>
|
||||||
|
</Accordion.Item>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</Accordion>
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ScrapePoolList;
|
|
@ -0,0 +1,49 @@
|
||||||
|
import { FC } from "react";
|
||||||
|
import { Labels } from "../../api/responseTypes/targets";
|
||||||
|
import { LabelBadges } from "../../components/LabelBadges";
|
||||||
|
import { ActionIcon, Collapse, Group, Stack, Text } from "@mantine/core";
|
||||||
|
import { useDisclosure } from "@mantine/hooks";
|
||||||
|
import { IconChevronDown, IconChevronUp } from "@tabler/icons-react";
|
||||||
|
|
||||||
|
type TargetLabelsProps = {
|
||||||
|
labels: Labels;
|
||||||
|
discoveredLabels: Labels;
|
||||||
|
};
|
||||||
|
|
||||||
|
const TargetLabels: FC<TargetLabelsProps> = ({ discoveredLabels, labels }) => {
|
||||||
|
const [showDiscovered, { toggle: toggleDiscovered }] = useDisclosure(false);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Stack my={showDiscovered ? "sm" : undefined}>
|
||||||
|
<Group wrap="nowrap" align="flex-start">
|
||||||
|
<LabelBadges labels={labels} />
|
||||||
|
|
||||||
|
<ActionIcon
|
||||||
|
size="xs"
|
||||||
|
color="gray"
|
||||||
|
variant="light"
|
||||||
|
onClick={toggleDiscovered}
|
||||||
|
title={`${showDiscovered ? "Hide" : "Show"} discovered (pre-relabeling) labels`}
|
||||||
|
>
|
||||||
|
{showDiscovered ? (
|
||||||
|
<IconChevronUp
|
||||||
|
style={{ width: "70%", height: "70%" }}
|
||||||
|
stroke={1.5}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<IconChevronDown style={{ width: "70%", height: "70%" }} />
|
||||||
|
)}
|
||||||
|
</ActionIcon>
|
||||||
|
</Group>
|
||||||
|
|
||||||
|
<Collapse in={showDiscovered}>
|
||||||
|
<Text fw={700} size="1em" my="lg" c="gray.7">
|
||||||
|
Discovered labels:
|
||||||
|
</Text>
|
||||||
|
<LabelBadges color="blue" labels={discoveredLabels} />
|
||||||
|
</Collapse>
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default TargetLabels;
|
|
@ -1,9 +1,13 @@
|
||||||
import { createListenerMiddleware } from "@reduxjs/toolkit";
|
import { createListenerMiddleware } from "@reduxjs/toolkit";
|
||||||
import { AppDispatch, RootState } from "./store";
|
import { AppDispatch, RootState } from "./store";
|
||||||
import {
|
import {
|
||||||
localStorageKeyCollapsedPools,
|
localStorageKeyCollapsedPools as localStorageKeyTargetsPageCollapsedPools,
|
||||||
setCollapsedPools,
|
setCollapsedPools as targetsPageSetCollapsedPools,
|
||||||
} from "./targetsPageSlice";
|
} from "./targetsPageSlice";
|
||||||
|
import {
|
||||||
|
localStorageKeyCollapsedPools as localStorageKeyServiceDiscoveryPageCollapsedPools,
|
||||||
|
setCollapsedPools as ServiceDiscoveryPageSetCollapsedPools,
|
||||||
|
} from "./serviceDiscoveryPageSlice";
|
||||||
import { updateSettings } from "./settingsSlice";
|
import { updateSettings } from "./settingsSlice";
|
||||||
|
|
||||||
const persistToLocalStorage = <T>(key: string, value: T) => {
|
const persistToLocalStorage = <T>(key: string, value: T) => {
|
||||||
|
@ -18,9 +22,19 @@ const startAppListening = localStorageMiddleware.startListening.withTypes<
|
||||||
>();
|
>();
|
||||||
|
|
||||||
startAppListening({
|
startAppListening({
|
||||||
actionCreator: setCollapsedPools,
|
actionCreator: targetsPageSetCollapsedPools,
|
||||||
effect: ({ payload }) => {
|
effect: ({ payload }) => {
|
||||||
persistToLocalStorage(localStorageKeyCollapsedPools, payload);
|
persistToLocalStorage(localStorageKeyTargetsPageCollapsedPools, payload);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
startAppListening({
|
||||||
|
actionCreator: ServiceDiscoveryPageSetCollapsedPools,
|
||||||
|
effect: ({ payload }) => {
|
||||||
|
persistToLocalStorage(
|
||||||
|
localStorageKeyServiceDiscoveryPageCollapsedPools,
|
||||||
|
payload
|
||||||
|
);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
37
web/ui/mantine-ui/src/state/serviceDiscoveryPageSlice.ts
Normal file
37
web/ui/mantine-ui/src/state/serviceDiscoveryPageSlice.ts
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
import { PayloadAction, createSlice } from "@reduxjs/toolkit";
|
||||||
|
import { initializeFromLocalStorage } from "./initializeFromLocalStorage";
|
||||||
|
|
||||||
|
export const localStorageKeyCollapsedPools = "serviceDiscovery.collapsedPools";
|
||||||
|
export const localStorageKeyTargetHealthFilter =
|
||||||
|
"serviceDiscovery.healthFilter";
|
||||||
|
|
||||||
|
interface ServiceDiscoveryPage {
|
||||||
|
collapsedPools: string[];
|
||||||
|
showLimitAlert: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const initialState: ServiceDiscoveryPage = {
|
||||||
|
collapsedPools: initializeFromLocalStorage<string[]>(
|
||||||
|
localStorageKeyCollapsedPools,
|
||||||
|
[]
|
||||||
|
),
|
||||||
|
showLimitAlert: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const serviceDiscoveryPageSlice = createSlice({
|
||||||
|
name: "serviceDiscoveryPage",
|
||||||
|
initialState,
|
||||||
|
reducers: {
|
||||||
|
setCollapsedPools: (state, { payload }: PayloadAction<string[]>) => {
|
||||||
|
state.collapsedPools = payload;
|
||||||
|
},
|
||||||
|
setShowLimitAlert: (state, { payload }: PayloadAction<boolean>) => {
|
||||||
|
state.showLimitAlert = payload;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const { setCollapsedPools, setShowLimitAlert } =
|
||||||
|
serviceDiscoveryPageSlice.actions;
|
||||||
|
|
||||||
|
export default serviceDiscoveryPageSlice.reducer;
|
|
@ -3,12 +3,14 @@ import queryPageSlice from "./queryPageSlice";
|
||||||
import settingsSlice from "./settingsSlice";
|
import settingsSlice from "./settingsSlice";
|
||||||
import targetsPageSlice from "./targetsPageSlice";
|
import targetsPageSlice from "./targetsPageSlice";
|
||||||
import { localStorageMiddleware } from "./localStorageMiddleware";
|
import { localStorageMiddleware } from "./localStorageMiddleware";
|
||||||
|
import serviceDiscoveryPageSlice from "./serviceDiscoveryPageSlice";
|
||||||
|
|
||||||
const store = configureStore({
|
const store = configureStore({
|
||||||
reducer: {
|
reducer: {
|
||||||
settings: settingsSlice,
|
settings: settingsSlice,
|
||||||
queryPage: queryPageSlice,
|
queryPage: queryPageSlice,
|
||||||
targetsPage: targetsPageSlice,
|
targetsPage: targetsPageSlice,
|
||||||
|
serviceDiscoveryPage: serviceDiscoveryPageSlice,
|
||||||
},
|
},
|
||||||
middleware: (getDefaultMiddleware) =>
|
middleware: (getDefaultMiddleware) =>
|
||||||
getDefaultMiddleware().prepend(localStorageMiddleware.middleware),
|
getDefaultMiddleware().prepend(localStorageMiddleware.middleware),
|
||||||
|
|
Loading…
Reference in a new issue