mirror of
https://github.com/prometheus/prometheus.git
synced 2025-02-21 03:16:00 -08:00
Service Discovery Page rework (#10131)
* rework the target page Signed-off-by: Augustin Husson <husson.augustin@gmail.com> * put back the URL of the endpoint Signed-off-by: Augustin Husson <husson.augustin@gmail.com> * replace old code by the new one and change function style Signed-off-by: Augustin Husson <husson.augustin@gmail.com> * align filter and search bar on the same row Signed-off-by: Augustin Husson <husson.augustin@gmail.com> * remove unnecessary return Signed-off-by: Augustin Husson <husson.augustin@gmail.com> * upgrade kvsearch to v0.3.0 Signed-off-by: Augustin Husson <husson.augustin@gmail.com> * fix unit test Signed-off-by: Augustin Husson <husson.augustin@gmail.com> * add missing style on column Signed-off-by: Augustin Husson <husson.augustin@gmail.com> * add placeholder and autofocus Signed-off-by: Augustin Husson <husson.augustin@gmail.com> * put back the previous table design Signed-off-by: Augustin Husson <husson.augustin@gmail.com> * fix issue relative to the position of the tooltip Signed-off-by: Augustin Husson <husson.augustin@gmail.com> * fix health filter Signed-off-by: Augustin Husson <husson.augustin@gmail.com> * fix test on label tooltip Signed-off-by: Augustin Husson <husson.augustin@gmail.com> * simplify filter condition Signed-off-by: Augustin Husson <husson.augustin@gmail.com> * rework service discovery page Signed-off-by: Augustin Husson <husson.augustin@gmail.com> * introduced generic custom infinite scroll component Signed-off-by: Augustin Husson <husson.augustin@gmail.com> * adjust the placeholder in discovery page Signed-off-by: Augustin Husson <husson.augustin@gmail.com> * ignore returning type missing Signed-off-by: Augustin Husson <husson.augustin@gmail.com> * apply fix required by the review Signed-off-by: Augustin Husson <husson.augustin@gmail.com> * index discoveredLabels Signed-off-by: Augustin Husson <husson.augustin@gmail.com>
This commit is contained in:
parent
0f4a1e6eac
commit
bff9d06874
50
web/ui/react-app/src/components/CustomInfiniteScroll.tsx
Normal file
50
web/ui/react-app/src/components/CustomInfiniteScroll.tsx
Normal file
|
@ -0,0 +1,50 @@
|
||||||
|
import { ComponentType, useEffect, useState } from 'react';
|
||||||
|
import InfiniteScroll from 'react-infinite-scroll-component';
|
||||||
|
|
||||||
|
const initialNumberOfItemsDisplayed = 50;
|
||||||
|
|
||||||
|
export interface InfiniteScrollItemsProps<T> {
|
||||||
|
items: T[];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface CustomInfiniteScrollProps<T> {
|
||||||
|
allItems: T[];
|
||||||
|
child: ComponentType<InfiniteScrollItemsProps<T>>;
|
||||||
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
|
||||||
|
const CustomInfiniteScroll = <T extends unknown>({ allItems, child }: CustomInfiniteScrollProps<T>) => {
|
||||||
|
const [items, setItems] = useState<T[]>(allItems.slice(0, 50));
|
||||||
|
const [index, setIndex] = useState<number>(initialNumberOfItemsDisplayed);
|
||||||
|
const [hasMore, setHasMore] = useState<boolean>(allItems.length > initialNumberOfItemsDisplayed);
|
||||||
|
const Child = child;
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setItems(allItems.slice(0, initialNumberOfItemsDisplayed));
|
||||||
|
setHasMore(allItems.length > initialNumberOfItemsDisplayed);
|
||||||
|
}, [allItems]);
|
||||||
|
|
||||||
|
const fetchMoreData = () => {
|
||||||
|
if (items.length === allItems.length) {
|
||||||
|
setHasMore(false);
|
||||||
|
} else {
|
||||||
|
const newIndex = index + initialNumberOfItemsDisplayed;
|
||||||
|
setIndex(newIndex);
|
||||||
|
setItems(allItems.slice(0, newIndex));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<InfiniteScroll
|
||||||
|
next={fetchMoreData}
|
||||||
|
hasMore={hasMore}
|
||||||
|
loader={<h4>loading...</h4>}
|
||||||
|
dataLength={items.length}
|
||||||
|
height={items.length > 25 ? '75vh' : ''}
|
||||||
|
>
|
||||||
|
<Child items={items} />
|
||||||
|
</InfiniteScroll>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default CustomInfiniteScroll;
|
|
@ -2,6 +2,7 @@ import React, { FC, useState } from 'react';
|
||||||
import { Badge, Table } from 'reactstrap';
|
import { Badge, Table } from 'reactstrap';
|
||||||
import { TargetLabels } from './Services';
|
import { TargetLabels } from './Services';
|
||||||
import { ToggleMoreLess } from '../../components/ToggleMoreLess';
|
import { ToggleMoreLess } from '../../components/ToggleMoreLess';
|
||||||
|
import CustomInfiniteScroll, { InfiniteScrollItemsProps } from '../../components/CustomInfiniteScroll';
|
||||||
|
|
||||||
interface LabelProps {
|
interface LabelProps {
|
||||||
value: TargetLabels[];
|
value: TargetLabels[];
|
||||||
|
@ -20,6 +21,33 @@ const formatLabels = (labels: Record<string, string> | string) => {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const LabelsTableContent: FC<InfiniteScrollItemsProps<TargetLabels>> = ({ items }) => {
|
||||||
|
return (
|
||||||
|
<Table size="sm" bordered hover striped>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Discovered Labels</th>
|
||||||
|
<th>Target Labels</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{items.map((_, i) => {
|
||||||
|
return (
|
||||||
|
<tr key={i}>
|
||||||
|
<td>{formatLabels(items[i].discoveredLabels)}</td>
|
||||||
|
{items[i].isDropped ? (
|
||||||
|
<td style={{ fontWeight: 'bold' }}>Dropped</td>
|
||||||
|
) : (
|
||||||
|
<td>{formatLabels(items[i].labels)}</td>
|
||||||
|
)}
|
||||||
|
</tr>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</tbody>
|
||||||
|
</Table>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
export const LabelsTable: FC<LabelProps> = ({ value, name }) => {
|
export const LabelsTable: FC<LabelProps> = ({ value, name }) => {
|
||||||
const [showMore, setShowMore] = useState(false);
|
const [showMore, setShowMore] = useState(false);
|
||||||
|
|
||||||
|
@ -35,30 +63,7 @@ export const LabelsTable: FC<LabelProps> = ({ value, name }) => {
|
||||||
<span className="target-head">{name}</span>
|
<span className="target-head">{name}</span>
|
||||||
</ToggleMoreLess>
|
</ToggleMoreLess>
|
||||||
</div>
|
</div>
|
||||||
{showMore ? (
|
{showMore ? <CustomInfiniteScroll allItems={value} child={LabelsTableContent} /> : null}
|
||||||
<Table size="sm" bordered hover striped>
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>Discovered Labels</th>
|
|
||||||
<th>Target Labels</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{value.map((_, i) => {
|
|
||||||
return (
|
|
||||||
<tr key={i}>
|
|
||||||
<td>{formatLabels(value[i].discoveredLabels)}</td>
|
|
||||||
{value[i].isDropped ? (
|
|
||||||
<td style={{ fontWeight: 'bold' }}>Dropped</td>
|
|
||||||
) : (
|
|
||||||
<td>{formatLabels(value[i].labels)}</td>
|
|
||||||
)}
|
|
||||||
</tr>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</tbody>
|
|
||||||
</Table>
|
|
||||||
) : null}
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import React, { FC } from 'react';
|
import React, { ChangeEvent, FC, useEffect, useState } from 'react';
|
||||||
import { useFetch } from '../../hooks/useFetch';
|
import { useFetch } from '../../hooks/useFetch';
|
||||||
import { LabelsTable } from './LabelsTable';
|
import { LabelsTable } from './LabelsTable';
|
||||||
import { DroppedTarget, Labels, Target } from '../targets/target';
|
import { DroppedTarget, Labels, Target } from '../targets/target';
|
||||||
|
@ -7,6 +7,10 @@ import { withStatusIndicator } from '../../components/withStatusIndicator';
|
||||||
import { mapObjEntries } from '../../utils';
|
import { mapObjEntries } from '../../utils';
|
||||||
import { usePathPrefix } from '../../contexts/PathPrefixContext';
|
import { usePathPrefix } from '../../contexts/PathPrefixContext';
|
||||||
import { API_PATH } from '../../constants/constants';
|
import { API_PATH } from '../../constants/constants';
|
||||||
|
import { KVSearch } from '@nexucis/kvsearch';
|
||||||
|
import { Container, Input, InputGroup, InputGroupAddon, InputGroupText } from 'reactstrap';
|
||||||
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||||
|
import { faSearch } from '@fortawesome/free-solid-svg-icons';
|
||||||
|
|
||||||
interface ServiceMap {
|
interface ServiceMap {
|
||||||
activeTargets: Target[];
|
activeTargets: Target[];
|
||||||
|
@ -19,6 +23,11 @@ export interface TargetLabels {
|
||||||
isDropped: boolean;
|
isDropped: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const kvSearch = new KVSearch({
|
||||||
|
shouldSort: true,
|
||||||
|
indexedKeys: ['labels', 'discoveredLabels', ['discoveredLabels', /.*/], ['labels', /.*/]],
|
||||||
|
});
|
||||||
|
|
||||||
export const processSummary = (
|
export const processSummary = (
|
||||||
activeTargets: Target[],
|
activeTargets: Target[],
|
||||||
droppedTargets: DroppedTarget[]
|
droppedTargets: DroppedTarget[]
|
||||||
|
@ -82,14 +91,41 @@ export const processTargets = (activeTargets: Target[], droppedTargets: DroppedT
|
||||||
};
|
};
|
||||||
|
|
||||||
export const ServiceDiscoveryContent: FC<ServiceMap> = ({ activeTargets, droppedTargets }) => {
|
export const ServiceDiscoveryContent: FC<ServiceMap> = ({ activeTargets, droppedTargets }) => {
|
||||||
const targets = processSummary(activeTargets, droppedTargets);
|
const [activeTargetList, setActiveTargetList] = useState(activeTargets);
|
||||||
const labels = processTargets(activeTargets, droppedTargets);
|
const [targetList, setTargetList] = useState(processSummary(activeTargets, droppedTargets));
|
||||||
|
const [labelList, setLabelList] = useState(processTargets(activeTargets, droppedTargets));
|
||||||
|
|
||||||
|
const handleSearchChange = (e: ChangeEvent<HTMLTextAreaElement | HTMLInputElement>) => {
|
||||||
|
if (e.target.value !== '') {
|
||||||
|
const result = kvSearch.filter(e.target.value.trim(), activeTargets);
|
||||||
|
setActiveTargetList(
|
||||||
|
result.map((value) => {
|
||||||
|
return value.original as unknown as Target;
|
||||||
|
})
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
setActiveTargetList(activeTargets);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setTargetList(processSummary(activeTargetList, droppedTargets));
|
||||||
|
setLabelList(processTargets(activeTargetList, droppedTargets));
|
||||||
|
}, [activeTargetList, droppedTargets]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<h2>Service Discovery</h2>
|
<h2>Service Discovery</h2>
|
||||||
|
<Container>
|
||||||
|
<InputGroup>
|
||||||
|
<InputGroupAddon addonType="prepend">
|
||||||
|
<InputGroupText>{<FontAwesomeIcon icon={faSearch} />}</InputGroupText>
|
||||||
|
</InputGroupAddon>
|
||||||
|
<Input autoFocus onChange={handleSearchChange} placeholder="Filter by labels" />
|
||||||
|
</InputGroup>
|
||||||
|
</Container>
|
||||||
<ul>
|
<ul>
|
||||||
{mapObjEntries(targets, ([k, v]) => (
|
{mapObjEntries(targetList, ([k, v]) => (
|
||||||
<li key={k}>
|
<li key={k}>
|
||||||
<a href={'#' + k}>
|
<a href={'#' + k}>
|
||||||
{k} ({v.active} / {v.total} active targets)
|
{k} ({v.active} / {v.total} active targets)
|
||||||
|
@ -98,7 +134,7 @@ export const ServiceDiscoveryContent: FC<ServiceMap> = ({ activeTargets, dropped
|
||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
<hr />
|
<hr />
|
||||||
{mapObjEntries(labels, ([k, v]) => {
|
{mapObjEntries(labelList, ([k, v]) => {
|
||||||
return <LabelsTable value={v} name={k} key={k} />;
|
return <LabelsTable value={v} name={k} key={k} />;
|
||||||
})}
|
})}
|
||||||
</>
|
</>
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
import React, { FC, useEffect, useState } from 'react';
|
import React, { FC } from 'react';
|
||||||
import { getColor, Target } from './target';
|
import { getColor, Target } from './target';
|
||||||
import InfiniteScroll from 'react-infinite-scroll-component';
|
|
||||||
import { Badge, Table } from 'reactstrap';
|
import { Badge, Table } from 'reactstrap';
|
||||||
import TargetLabels from './TargetLabels';
|
import TargetLabels from './TargetLabels';
|
||||||
import styles from './ScrapePoolPanel.module.css';
|
import styles from './ScrapePoolPanel.module.css';
|
||||||
|
@ -8,84 +7,61 @@ import { formatRelative } from '../../utils';
|
||||||
import { now } from 'moment';
|
import { now } from 'moment';
|
||||||
import TargetScrapeDuration from './TargetScrapeDuration';
|
import TargetScrapeDuration from './TargetScrapeDuration';
|
||||||
import EndpointLink from './EndpointLink';
|
import EndpointLink from './EndpointLink';
|
||||||
|
import CustomInfiniteScroll, { InfiniteScrollItemsProps } from '../../components/CustomInfiniteScroll';
|
||||||
|
|
||||||
const columns = ['Endpoint', 'State', 'Labels', 'Last Scrape', 'Scrape Duration', 'Error'];
|
const columns = ['Endpoint', 'State', 'Labels', 'Last Scrape', 'Scrape Duration', 'Error'];
|
||||||
const initialNumberOfTargetsDisplayed = 50;
|
|
||||||
|
|
||||||
interface ScrapePoolContentProps {
|
interface ScrapePoolContentProps {
|
||||||
targets: Target[];
|
targets: Target[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ScrapePoolContent: FC<ScrapePoolContentProps> = ({ targets }) => {
|
const ScrapePoolContentTable: FC<InfiniteScrollItemsProps<Target>> = ({ items }) => {
|
||||||
const [items, setItems] = useState<Target[]>(targets.slice(0, 50));
|
|
||||||
const [index, setIndex] = useState<number>(initialNumberOfTargetsDisplayed);
|
|
||||||
const [hasMore, setHasMore] = useState<boolean>(targets.length > initialNumberOfTargetsDisplayed);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setItems(targets.slice(0, initialNumberOfTargetsDisplayed));
|
|
||||||
setHasMore(targets.length > initialNumberOfTargetsDisplayed);
|
|
||||||
}, [targets]);
|
|
||||||
|
|
||||||
const fetchMoreData = () => {
|
|
||||||
if (items.length === targets.length) {
|
|
||||||
setHasMore(false);
|
|
||||||
} else {
|
|
||||||
const newIndex = index + initialNumberOfTargetsDisplayed;
|
|
||||||
setIndex(newIndex);
|
|
||||||
setItems(targets.slice(0, newIndex));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<InfiniteScroll
|
<Table className={styles.table} size="sm" bordered hover striped>
|
||||||
next={fetchMoreData}
|
<thead>
|
||||||
hasMore={hasMore}
|
<tr key="header">
|
||||||
loader={<h4>loading...</h4>}
|
{columns.map((column) => (
|
||||||
dataLength={items.length}
|
<th key={column}>{column}</th>
|
||||||
height={items.length > 25 ? '75vh' : ''}
|
|
||||||
>
|
|
||||||
<Table className={styles.table} size="sm" bordered hover striped>
|
|
||||||
<thead>
|
|
||||||
<tr key="header">
|
|
||||||
{columns.map((column) => (
|
|
||||||
<th key={column}>{column}</th>
|
|
||||||
))}
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{items.map((target, index) => (
|
|
||||||
<tr key={index}>
|
|
||||||
<td className={styles.endpoint}>
|
|
||||||
<EndpointLink endpoint={target.scrapeUrl} globalUrl={target.globalUrl} />
|
|
||||||
</td>
|
|
||||||
<td className={styles.state}>
|
|
||||||
<Badge color={getColor(target.health)}>{target.health.toUpperCase()}</Badge>
|
|
||||||
</td>
|
|
||||||
<td className={styles.labels}>
|
|
||||||
<TargetLabels
|
|
||||||
discoveredLabels={target.discoveredLabels}
|
|
||||||
labels={target.labels}
|
|
||||||
scrapePool={target.scrapePool}
|
|
||||||
idx={index}
|
|
||||||
/>
|
|
||||||
</td>
|
|
||||||
<td className={styles['last-scrape']}>{formatRelative(target.lastScrape, now())}</td>
|
|
||||||
<td className={styles['scrape-duration']}>
|
|
||||||
<TargetScrapeDuration
|
|
||||||
duration={target.lastScrapeDuration}
|
|
||||||
scrapePool={target.scrapePool}
|
|
||||||
idx={index}
|
|
||||||
interval={target.scrapeInterval}
|
|
||||||
timeout={target.scrapeTimeout}
|
|
||||||
/>
|
|
||||||
</td>
|
|
||||||
<td className={styles.errors}>
|
|
||||||
{target.lastError ? <span className="text-danger">{target.lastError}</span> : null}
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
))}
|
))}
|
||||||
</tbody>
|
</tr>
|
||||||
</Table>
|
</thead>
|
||||||
</InfiniteScroll>
|
<tbody>
|
||||||
|
{items.map((target, index) => (
|
||||||
|
<tr key={index}>
|
||||||
|
<td className={styles.endpoint}>
|
||||||
|
<EndpointLink endpoint={target.scrapeUrl} globalUrl={target.globalUrl} />
|
||||||
|
</td>
|
||||||
|
<td className={styles.state}>
|
||||||
|
<Badge color={getColor(target.health)}>{target.health.toUpperCase()}</Badge>
|
||||||
|
</td>
|
||||||
|
<td className={styles.labels}>
|
||||||
|
<TargetLabels
|
||||||
|
discoveredLabels={target.discoveredLabels}
|
||||||
|
labels={target.labels}
|
||||||
|
scrapePool={target.scrapePool}
|
||||||
|
idx={index}
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
<td className={styles['last-scrape']}>{formatRelative(target.lastScrape, now())}</td>
|
||||||
|
<td className={styles['scrape-duration']}>
|
||||||
|
<TargetScrapeDuration
|
||||||
|
duration={target.lastScrapeDuration}
|
||||||
|
scrapePool={target.scrapePool}
|
||||||
|
idx={index}
|
||||||
|
interval={target.scrapeInterval}
|
||||||
|
timeout={target.scrapeTimeout}
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
<td className={styles.errors}>
|
||||||
|
{target.lastError ? <span className="text-danger">{target.lastError}</span> : null}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</Table>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const ScrapePoolContent: FC<ScrapePoolContentProps> = ({ targets }) => {
|
||||||
|
return <CustomInfiniteScroll allItems={targets} child={ScrapePoolContentTable} />;
|
||||||
|
};
|
||||||
|
|
Loading…
Reference in a new issue