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 { TargetLabels } from './Services';
|
||||
import { ToggleMoreLess } from '../../components/ToggleMoreLess';
|
||||
import CustomInfiniteScroll, { InfiniteScrollItemsProps } from '../../components/CustomInfiniteScroll';
|
||||
|
||||
interface LabelProps {
|
||||
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 }) => {
|
||||
const [showMore, setShowMore] = useState(false);
|
||||
|
||||
@ -35,30 +63,7 @@ export const LabelsTable: FC<LabelProps> = ({ value, name }) => {
|
||||
<span className="target-head">{name}</span>
|
||||
</ToggleMoreLess>
|
||||
</div>
|
||||
{showMore ? (
|
||||
<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}
|
||||
{showMore ? <CustomInfiniteScroll allItems={value} child={LabelsTableContent} /> : null}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
@ -1,4 +1,4 @@
|
||||
import React, { FC } from 'react';
|
||||
import React, { ChangeEvent, FC, useEffect, useState } from 'react';
|
||||
import { useFetch } from '../../hooks/useFetch';
|
||||
import { LabelsTable } from './LabelsTable';
|
||||
import { DroppedTarget, Labels, Target } from '../targets/target';
|
||||
@ -7,6 +7,10 @@ import { withStatusIndicator } from '../../components/withStatusIndicator';
|
||||
import { mapObjEntries } from '../../utils';
|
||||
import { usePathPrefix } from '../../contexts/PathPrefixContext';
|
||||
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 {
|
||||
activeTargets: Target[];
|
||||
@ -19,6 +23,11 @@ export interface TargetLabels {
|
||||
isDropped: boolean;
|
||||
}
|
||||
|
||||
const kvSearch = new KVSearch({
|
||||
shouldSort: true,
|
||||
indexedKeys: ['labels', 'discoveredLabels', ['discoveredLabels', /.*/], ['labels', /.*/]],
|
||||
});
|
||||
|
||||
export const processSummary = (
|
||||
activeTargets: Target[],
|
||||
droppedTargets: DroppedTarget[]
|
||||
@ -82,14 +91,41 @@ export const processTargets = (activeTargets: Target[], droppedTargets: DroppedT
|
||||
};
|
||||
|
||||
export const ServiceDiscoveryContent: FC<ServiceMap> = ({ activeTargets, droppedTargets }) => {
|
||||
const targets = processSummary(activeTargets, droppedTargets);
|
||||
const labels = processTargets(activeTargets, droppedTargets);
|
||||
const [activeTargetList, setActiveTargetList] = useState(activeTargets);
|
||||
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 (
|
||||
<>
|
||||
<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>
|
||||
{mapObjEntries(targets, ([k, v]) => (
|
||||
{mapObjEntries(targetList, ([k, v]) => (
|
||||
<li key={k}>
|
||||
<a href={'#' + k}>
|
||||
{k} ({v.active} / {v.total} active targets)
|
||||
@ -98,7 +134,7 @@ export const ServiceDiscoveryContent: FC<ServiceMap> = ({ activeTargets, dropped
|
||||
))}
|
||||
</ul>
|
||||
<hr />
|
||||
{mapObjEntries(labels, ([k, v]) => {
|
||||
{mapObjEntries(labelList, ([k, v]) => {
|
||||
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 InfiniteScroll from 'react-infinite-scroll-component';
|
||||
import { Badge, Table } from 'reactstrap';
|
||||
import TargetLabels from './TargetLabels';
|
||||
import styles from './ScrapePoolPanel.module.css';
|
||||
@ -8,84 +7,61 @@ import { formatRelative } from '../../utils';
|
||||
import { now } from 'moment';
|
||||
import TargetScrapeDuration from './TargetScrapeDuration';
|
||||
import EndpointLink from './EndpointLink';
|
||||
import CustomInfiniteScroll, { InfiniteScrollItemsProps } from '../../components/CustomInfiniteScroll';
|
||||
|
||||
const columns = ['Endpoint', 'State', 'Labels', 'Last Scrape', 'Scrape Duration', 'Error'];
|
||||
const initialNumberOfTargetsDisplayed = 50;
|
||||
|
||||
interface ScrapePoolContentProps {
|
||||
targets: Target[];
|
||||
}
|
||||
|
||||
export const ScrapePoolContent: FC<ScrapePoolContentProps> = ({ targets }) => {
|
||||
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));
|
||||
}
|
||||
};
|
||||
|
||||
const ScrapePoolContentTable: FC<InfiniteScrollItemsProps<Target>> = ({ items }) => {
|
||||
return (
|
||||
<InfiniteScroll
|
||||
next={fetchMoreData}
|
||||
hasMore={hasMore}
|
||||
loader={<h4>loading...</h4>}
|
||||
dataLength={items.length}
|
||||
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>
|
||||
<Table className={styles.table} size="sm" bordered hover striped>
|
||||
<thead>
|
||||
<tr key="header">
|
||||
{columns.map((column) => (
|
||||
<th key={column}>{column}</th>
|
||||
))}
|
||||
</tbody>
|
||||
</Table>
|
||||
</InfiniteScroll>
|
||||
</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>
|
||||
</Table>
|
||||
);
|
||||
};
|
||||
|
||||
export const ScrapePoolContent: FC<ScrapePoolContentProps> = ({ targets }) => {
|
||||
return <CustomInfiniteScroll allItems={targets} child={ScrapePoolContentTable} />;
|
||||
};
|
||||
|
Loading…
x
Reference in New Issue
Block a user