mirror of
https://github.com/prometheus/prometheus.git
synced 2025-01-11 13:57:36 -08:00
rework the target page (#10103)
* 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>
This commit is contained in:
parent
0df3489275
commit
dd9f96b893
69
web/ui/package-lock.json
generated
69
web/ui/package-lock.json
generated
|
@ -1592,6 +1592,19 @@
|
|||
"@lezer/common": "^0.15.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@nexucis/fuzzy": {
|
||||
"version": "0.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@nexucis/fuzzy/-/fuzzy-0.3.0.tgz",
|
||||
"integrity": "sha512-Z1+ADKY0fxdBE28REraWhUCNy+Bp5UmpK3Tc/5wdCDpY+6fXh8l2csMtbPGaqEBsyGLxJz9wUYGCf+CW9unyvQ=="
|
||||
},
|
||||
"node_modules/@nexucis/kvsearch": {
|
||||
"version": "0.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@nexucis/kvsearch/-/kvsearch-0.3.0.tgz",
|
||||
"integrity": "sha512-tHIH6W/mRUZZ0ZQyRbgp2uhat+2O1c1jX1EC6NHv7/8OIeHx1HBZ5ZZb0KSUVWl4jkNzYw6AO39OoTELtrjaQw==",
|
||||
"dependencies": {
|
||||
"@nexucis/fuzzy": "^0.3.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@nodelib/fs.scandir": {
|
||||
"version": "2.1.5",
|
||||
"dev": true,
|
||||
|
@ -5952,6 +5965,17 @@
|
|||
"react": "17.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/react-infinite-scroll-component": {
|
||||
"version": "6.1.0",
|
||||
"resolved": "https://registry.npmjs.org/react-infinite-scroll-component/-/react-infinite-scroll-component-6.1.0.tgz",
|
||||
"integrity": "sha512-SQu5nCqy8DxQWpnUVLx7V7b7LcA37aM7tvoWjTLZp1dk6EJibM5/4EJKzOnl07/BsM1Y40sKLuqjCwwH/xV0TQ==",
|
||||
"dependencies": {
|
||||
"throttle-debounce": "^2.1.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=16.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-is": {
|
||||
"version": "17.0.2",
|
||||
"license": "MIT"
|
||||
|
@ -6603,6 +6627,14 @@
|
|||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/throttle-debounce": {
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/throttle-debounce/-/throttle-debounce-2.3.0.tgz",
|
||||
"integrity": "sha512-H7oLPV0P7+jgvrk+6mwwwBDmxTaxnu9HMXmloNLXwnNO0ZxZ31Orah2n8lU1eMPvsaowP2CX+USCgyovXfdOFQ==",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/to-fast-properties": {
|
||||
"version": "2.0.0",
|
||||
"dev": true,
|
||||
|
@ -7240,6 +7272,7 @@
|
|||
"@fortawesome/free-solid-svg-icons": "^5.7.2",
|
||||
"@fortawesome/react-fontawesome": "^0.1.16",
|
||||
"@nexucis/fuzzy": "^0.3.0",
|
||||
"@nexucis/kvsearch": "^0.3.0",
|
||||
"bootstrap": "^4.6.1",
|
||||
"codemirror-promql": "0.19.0",
|
||||
"css.escape": "^1.5.1",
|
||||
|
@ -7252,6 +7285,7 @@
|
|||
"react": "^17.0.2",
|
||||
"react-copy-to-clipboard": "^5.0.4",
|
||||
"react-dom": "^17.0.2",
|
||||
"react-infinite-scroll-component": "^6.1.0",
|
||||
"react-resize-detector": "^6.7.6",
|
||||
"react-router-dom": "^5.2.1",
|
||||
"react-test-renderer": "^17.0.2",
|
||||
|
@ -9920,10 +9954,6 @@
|
|||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"react-app/node_modules/@nexucis/fuzzy": {
|
||||
"version": "0.3.0",
|
||||
"license": "MIT"
|
||||
},
|
||||
"react-app/node_modules/@npmcli/fs": {
|
||||
"version": "1.0.0",
|
||||
"dev": true,
|
||||
|
@ -27660,6 +27690,19 @@
|
|||
"@lezer/common": "^0.15.0"
|
||||
}
|
||||
},
|
||||
"@nexucis/fuzzy": {
|
||||
"version": "0.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@nexucis/fuzzy/-/fuzzy-0.3.0.tgz",
|
||||
"integrity": "sha512-Z1+ADKY0fxdBE28REraWhUCNy+Bp5UmpK3Tc/5wdCDpY+6fXh8l2csMtbPGaqEBsyGLxJz9wUYGCf+CW9unyvQ=="
|
||||
},
|
||||
"@nexucis/kvsearch": {
|
||||
"version": "0.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@nexucis/kvsearch/-/kvsearch-0.3.0.tgz",
|
||||
"integrity": "sha512-tHIH6W/mRUZZ0ZQyRbgp2uhat+2O1c1jX1EC6NHv7/8OIeHx1HBZ5ZZb0KSUVWl4jkNzYw6AO39OoTELtrjaQw==",
|
||||
"requires": {
|
||||
"@nexucis/fuzzy": "^0.3.0"
|
||||
}
|
||||
},
|
||||
"@nodelib/fs.scandir": {
|
||||
"version": "2.1.5",
|
||||
"dev": true,
|
||||
|
@ -29682,6 +29725,7 @@
|
|||
"@fortawesome/free-solid-svg-icons": "^5.7.2",
|
||||
"@fortawesome/react-fontawesome": "^0.1.16",
|
||||
"@nexucis/fuzzy": "^0.3.0",
|
||||
"@nexucis/kvsearch": "^0.3.0",
|
||||
"@testing-library/react-hooks": "^7.0.1",
|
||||
"@types/enzyme": "^3.10.10",
|
||||
"@types/flot": "0.0.32",
|
||||
|
@ -29718,6 +29762,7 @@
|
|||
"react": "^17.0.2",
|
||||
"react-copy-to-clipboard": "^5.0.4",
|
||||
"react-dom": "^17.0.2",
|
||||
"react-infinite-scroll-component": "^6.1.0",
|
||||
"react-resize-detector": "^6.7.6",
|
||||
"react-router-dom": "^5.2.1",
|
||||
"react-scripts": "4.0.3",
|
||||
|
@ -31395,9 +31440,6 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"@nexucis/fuzzy": {
|
||||
"version": "0.3.0"
|
||||
},
|
||||
"@npmcli/fs": {
|
||||
"version": "1.0.0",
|
||||
"dev": true,
|
||||
|
@ -44490,6 +44532,14 @@
|
|||
"scheduler": "^0.20.2"
|
||||
}
|
||||
},
|
||||
"react-infinite-scroll-component": {
|
||||
"version": "6.1.0",
|
||||
"resolved": "https://registry.npmjs.org/react-infinite-scroll-component/-/react-infinite-scroll-component-6.1.0.tgz",
|
||||
"integrity": "sha512-SQu5nCqy8DxQWpnUVLx7V7b7LcA37aM7tvoWjTLZp1dk6EJibM5/4EJKzOnl07/BsM1Y40sKLuqjCwwH/xV0TQ==",
|
||||
"requires": {
|
||||
"throttle-debounce": "^2.1.0"
|
||||
}
|
||||
},
|
||||
"react-is": {
|
||||
"version": "17.0.2"
|
||||
},
|
||||
|
@ -44937,6 +44987,11 @@
|
|||
"version": "0.2.0",
|
||||
"dev": true
|
||||
},
|
||||
"throttle-debounce": {
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/throttle-debounce/-/throttle-debounce-2.3.0.tgz",
|
||||
"integrity": "sha512-H7oLPV0P7+jgvrk+6mwwwBDmxTaxnu9HMXmloNLXwnNO0ZxZ31Orah2n8lU1eMPvsaowP2CX+USCgyovXfdOFQ=="
|
||||
},
|
||||
"to-fast-properties": {
|
||||
"version": "2.0.0",
|
||||
"dev": true
|
||||
|
|
|
@ -20,6 +20,7 @@
|
|||
"@fortawesome/free-solid-svg-icons": "^5.7.2",
|
||||
"@fortawesome/react-fontawesome": "^0.1.16",
|
||||
"@nexucis/fuzzy": "^0.3.0",
|
||||
"@nexucis/kvsearch": "^0.3.0",
|
||||
"bootstrap": "^4.6.1",
|
||||
"codemirror-promql": "0.19.0",
|
||||
"css.escape": "^1.5.1",
|
||||
|
@ -32,6 +33,7 @@
|
|||
"react": "^17.0.2",
|
||||
"react-copy-to-clipboard": "^5.0.4",
|
||||
"react-dom": "^17.0.2",
|
||||
"react-infinite-scroll-component": "^6.1.0",
|
||||
"react-resize-detector": "^6.7.6",
|
||||
"react-router-dom": "^5.2.1",
|
||||
"react-test-renderer": "^17.0.2",
|
||||
|
|
91
web/ui/react-app/src/pages/targets/ScrapePoolContent.tsx
Normal file
91
web/ui/react-app/src/pages/targets/ScrapePoolContent.tsx
Normal file
|
@ -0,0 +1,91 @@
|
|||
import React, { FC, useEffect, useState } 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';
|
||||
import { formatRelative } from '../../utils';
|
||||
import { now } from 'moment';
|
||||
import TargetScrapeDuration from './TargetScrapeDuration';
|
||||
import EndpointLink from './EndpointLink';
|
||||
|
||||
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));
|
||||
}
|
||||
};
|
||||
|
||||
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>
|
||||
))}
|
||||
</tbody>
|
||||
</Table>
|
||||
</InfiniteScroll>
|
||||
);
|
||||
};
|
|
@ -3,8 +3,7 @@ import { mount, ReactWrapper } from 'enzyme';
|
|||
import { act } from 'react-dom/test-utils';
|
||||
import { Alert } from 'reactstrap';
|
||||
import { sampleApiResponse } from './__testdata__/testdata';
|
||||
import ScrapePoolList from './ScrapePoolList';
|
||||
import ScrapePoolPanel from './ScrapePoolPanel';
|
||||
import ScrapePoolList, { ScrapePoolPanel } from './ScrapePoolList';
|
||||
import { Target } from './target';
|
||||
import { FetchMock } from 'jest-fetch-mock/types';
|
||||
import { PathPrefixContext } from '../../contexts/PathPrefixContext';
|
||||
|
@ -48,7 +47,7 @@ describe('ScrapePoolList', () => {
|
|||
});
|
||||
const panels = scrapePoolList.find(ScrapePoolPanel);
|
||||
expect(panels).toHaveLength(3);
|
||||
const activeTargets: Target[] = sampleApiResponse.data.activeTargets as Target[];
|
||||
const activeTargets: Target[] = sampleApiResponse.data.activeTargets as unknown as Target[];
|
||||
activeTargets.forEach(({ scrapePool }: Target) => {
|
||||
const panel = scrapePoolList.find(ScrapePoolPanel).filterWhere((panel) => panel.prop('scrapePool') === scrapePool);
|
||||
expect(panel).toHaveLength(1);
|
||||
|
|
|
@ -1,26 +1,69 @@
|
|||
import React, { FC } from 'react';
|
||||
import Filter, { Expanded, FilterData } from './Filter';
|
||||
import { useFetch } from '../../hooks/useFetch';
|
||||
import { groupTargets, Target } from './target';
|
||||
import ScrapePoolPanel from './ScrapePoolPanel';
|
||||
import { withStatusIndicator } from '../../components/withStatusIndicator';
|
||||
import { KVSearch } from '@nexucis/kvsearch';
|
||||
import { usePathPrefix } from '../../contexts/PathPrefixContext';
|
||||
import { useFetch } from '../../hooks/useFetch';
|
||||
import { API_PATH } from '../../constants/constants';
|
||||
import { groupTargets, ScrapePool, ScrapePools, Target } from './target';
|
||||
import { withStatusIndicator } from '../../components/withStatusIndicator';
|
||||
import { ChangeEvent, FC, useEffect, useState } from 'react';
|
||||
import { Col, Collapse, Input, InputGroup, InputGroupAddon, InputGroupText, Row } from 'reactstrap';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import { faSearch } from '@fortawesome/free-solid-svg-icons';
|
||||
import { ScrapePoolContent } from './ScrapePoolContent';
|
||||
import Filter, { Expanded, FilterData } from './Filter';
|
||||
import { useLocalStorage } from '../../hooks/useLocalStorage';
|
||||
import styles from './ScrapePoolPanel.module.css';
|
||||
import { ToggleMoreLess } from '../../components/ToggleMoreLess';
|
||||
|
||||
interface ScrapePoolListProps {
|
||||
activeTargets: Target[];
|
||||
}
|
||||
|
||||
export const ScrapePoolContent: FC<ScrapePoolListProps> = ({ activeTargets }) => {
|
||||
const targetGroups = groupTargets(activeTargets);
|
||||
const kvSearch = new KVSearch({
|
||||
shouldSort: true,
|
||||
indexedKeys: ['labels', 'scrapePool', ['labels', /.*/]],
|
||||
});
|
||||
|
||||
interface PanelProps {
|
||||
scrapePool: string;
|
||||
targetGroup: ScrapePool;
|
||||
expanded: boolean;
|
||||
toggleExpanded: () => void;
|
||||
}
|
||||
|
||||
export const ScrapePoolPanel: FC<PanelProps> = (props: PanelProps) => {
|
||||
const modifier = props.targetGroup.upCount < props.targetGroup.targets.length ? 'danger' : 'normal';
|
||||
const id = `pool-${props.scrapePool}`;
|
||||
const anchorProps = {
|
||||
href: `#${id}`,
|
||||
id,
|
||||
};
|
||||
return (
|
||||
<div>
|
||||
<ToggleMoreLess event={props.toggleExpanded} showMore={props.expanded}>
|
||||
<a className={styles[modifier]} {...anchorProps}>
|
||||
{`${props.scrapePool} (${props.targetGroup.upCount}/${props.targetGroup.targets.length} up)`}
|
||||
</a>
|
||||
</ToggleMoreLess>
|
||||
<Collapse isOpen={props.expanded}>
|
||||
<ScrapePoolContent targets={props.targetGroup.targets} />
|
||||
</Collapse>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// ScrapePoolListContent is taking care of every possible filter
|
||||
const ScrapePoolListContent: FC<ScrapePoolListProps> = ({ activeTargets }) => {
|
||||
const initialPoolList = groupTargets(activeTargets);
|
||||
const [poolList, setPoolList] = useState<ScrapePools>(initialPoolList);
|
||||
const [targetList, setTargetList] = useState(activeTargets);
|
||||
|
||||
const initialFilter: FilterData = {
|
||||
showHealthy: true,
|
||||
showUnhealthy: true,
|
||||
};
|
||||
const [filter, setFilter] = useLocalStorage('targets-page-filter', initialFilter);
|
||||
|
||||
const initialExpanded: Expanded = Object.keys(targetGroups).reduce(
|
||||
const initialExpanded: Expanded = Object.keys(initialPoolList).reduce(
|
||||
(acc: { [scrapePool: string]: boolean }, scrapePool: string) => ({
|
||||
...acc,
|
||||
[scrapePool]: true,
|
||||
|
@ -28,14 +71,44 @@ export const ScrapePoolContent: FC<ScrapePoolListProps> = ({ activeTargets }) =>
|
|||
{}
|
||||
);
|
||||
const [expanded, setExpanded] = useLocalStorage('targets-page-expansion-state', initialExpanded);
|
||||
|
||||
const { showHealthy, showUnhealthy } = filter;
|
||||
|
||||
const handleSearchChange = (e: ChangeEvent<HTMLTextAreaElement | HTMLInputElement>) => {
|
||||
if (e.target.value !== '') {
|
||||
const result = kvSearch.filter(e.target.value.trim(), activeTargets);
|
||||
setTargetList(
|
||||
result.map((value) => {
|
||||
return value.original as unknown as Target;
|
||||
})
|
||||
);
|
||||
} else {
|
||||
setTargetList(activeTargets);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const list = targetList.filter((t) => showHealthy || t.health.toLowerCase() !== 'up');
|
||||
setPoolList(groupTargets(list));
|
||||
}, [showHealthy, targetList]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Filter filter={filter} setFilter={setFilter} expanded={expanded} setExpanded={setExpanded} />
|
||||
{Object.keys(targetGroups)
|
||||
<Row xs="4" className="align-items-center">
|
||||
<Col>
|
||||
<Filter filter={filter} setFilter={setFilter} expanded={expanded} setExpanded={setExpanded} />
|
||||
</Col>
|
||||
<Col xs="6">
|
||||
<InputGroup>
|
||||
<InputGroupAddon addonType="prepend">
|
||||
<InputGroupText>{<FontAwesomeIcon icon={faSearch} />}</InputGroupText>
|
||||
</InputGroupAddon>
|
||||
<Input autoFocus onChange={handleSearchChange} placeholder="Filter by endpoint or labels" />
|
||||
</InputGroup>
|
||||
</Col>
|
||||
</Row>
|
||||
{Object.keys(poolList)
|
||||
.filter((scrapePool) => {
|
||||
const targetGroup = targetGroups[scrapePool];
|
||||
const targetGroup = poolList[scrapePool];
|
||||
const isHealthy = targetGroup.upCount === targetGroup.targets.length;
|
||||
return (isHealthy && showHealthy) || (!isHealthy && showUnhealthy);
|
||||
})
|
||||
|
@ -43,7 +116,7 @@ export const ScrapePoolContent: FC<ScrapePoolListProps> = ({ activeTargets }) =>
|
|||
<ScrapePoolPanel
|
||||
key={scrapePool}
|
||||
scrapePool={scrapePool}
|
||||
targetGroup={targetGroups[scrapePool]}
|
||||
targetGroup={poolList[scrapePool]}
|
||||
expanded={expanded[scrapePool]}
|
||||
toggleExpanded={(): void => setExpanded({ ...expanded, [scrapePool]: !expanded[scrapePool] })}
|
||||
/>
|
||||
|
@ -51,11 +124,10 @@ export const ScrapePoolContent: FC<ScrapePoolListProps> = ({ activeTargets }) =>
|
|||
</>
|
||||
);
|
||||
};
|
||||
ScrapePoolContent.displayName = 'ScrapePoolContent';
|
||||
|
||||
const ScrapePoolListWithStatusIndicator = withStatusIndicator(ScrapePoolContent);
|
||||
const ScrapePoolListWithStatusIndicator = withStatusIndicator(ScrapePoolListContent);
|
||||
|
||||
const ScrapePoolList: FC = () => {
|
||||
export const ScrapePoolList: FC = () => {
|
||||
const pathPrefix = usePathPrefix();
|
||||
const { response, error, isLoading } = useFetch<ScrapePoolListProps>(`${pathPrefix}/${API_PATH}/targets?state=active`);
|
||||
const { status: responseStatus } = response;
|
||||
|
|
|
@ -1,137 +0,0 @@
|
|||
import React from 'react';
|
||||
import { mount, shallow } from 'enzyme';
|
||||
import { targetGroups } from './__testdata__/testdata';
|
||||
import ScrapePoolPanel, { columns } from './ScrapePoolPanel';
|
||||
import { Button, Collapse, Table, Badge } from 'reactstrap';
|
||||
import { Target, getColor } from './target';
|
||||
import EndpointLink from './EndpointLink';
|
||||
import TargetLabels from './TargetLabels';
|
||||
import sinon from 'sinon';
|
||||
|
||||
describe('ScrapePoolPanel', () => {
|
||||
const defaultProps = {
|
||||
scrapePool: 'blackbox',
|
||||
targetGroup: targetGroups.blackbox,
|
||||
expanded: true,
|
||||
toggleExpanded: sinon.spy(),
|
||||
};
|
||||
const scrapePoolPanel = shallow(<ScrapePoolPanel {...defaultProps} />);
|
||||
|
||||
it('renders a container', () => {
|
||||
const div = scrapePoolPanel.find('div').filterWhere((elem) => elem.hasClass('container'));
|
||||
expect(div).toHaveLength(1);
|
||||
});
|
||||
|
||||
describe('Header', () => {
|
||||
it('renders an anchor with up count and danger color if upCount < targetsCount', () => {
|
||||
const anchor = scrapePoolPanel.find('a');
|
||||
expect(anchor).toHaveLength(1);
|
||||
expect(anchor.prop('id')).toEqual('pool-blackbox');
|
||||
expect(anchor.prop('href')).toEqual('#pool-blackbox');
|
||||
expect(anchor.text()).toEqual('blackbox (2/3 up)');
|
||||
expect(anchor.prop('className')).toEqual('danger');
|
||||
});
|
||||
|
||||
it('renders an anchor with up count and normal color if upCount == targetsCount', () => {
|
||||
const props = {
|
||||
...defaultProps,
|
||||
scrapePool: 'prometheus',
|
||||
targetGroup: targetGroups.prometheus,
|
||||
};
|
||||
const scrapePoolPanel = shallow(<ScrapePoolPanel {...props} />);
|
||||
const anchor = scrapePoolPanel.find('a');
|
||||
expect(anchor).toHaveLength(1);
|
||||
expect(anchor.prop('id')).toEqual('pool-prometheus');
|
||||
expect(anchor.prop('href')).toEqual('#pool-prometheus');
|
||||
expect(anchor.text()).toEqual('prometheus (1/1 up)');
|
||||
expect(anchor.prop('className')).toEqual('normal');
|
||||
});
|
||||
|
||||
it('renders a show more btn if collapsed', () => {
|
||||
const props = {
|
||||
...defaultProps,
|
||||
scrapePool: 'prometheus',
|
||||
targetGroup: targetGroups.prometheus,
|
||||
toggleExpanded: sinon.spy(),
|
||||
};
|
||||
const div = document.createElement('div');
|
||||
div.id = `series-labels-prometheus-0`;
|
||||
document.body.appendChild(div);
|
||||
const div2 = document.createElement('div');
|
||||
div2.id = `scrape-duration-prometheus-0`;
|
||||
document.body.appendChild(div2);
|
||||
const scrapePoolPanel = mount(<ScrapePoolPanel {...props} />);
|
||||
|
||||
const btn = scrapePoolPanel.find(Button);
|
||||
btn.simulate('click');
|
||||
expect(props.toggleExpanded.calledOnce).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
it('renders a Collapse component', () => {
|
||||
const collapse = scrapePoolPanel.find(Collapse);
|
||||
expect(collapse.prop('isOpen')).toBe(true);
|
||||
});
|
||||
|
||||
describe('Table', () => {
|
||||
it('renders a table', () => {
|
||||
const table = scrapePoolPanel.find(Table);
|
||||
const headers = table.find('th');
|
||||
expect(table).toHaveLength(1);
|
||||
expect(headers).toHaveLength(6);
|
||||
columns.forEach((col) => {
|
||||
expect(headers.contains(col));
|
||||
});
|
||||
});
|
||||
|
||||
describe('for each target', () => {
|
||||
const table = scrapePoolPanel.find(Table);
|
||||
defaultProps.targetGroup.targets.forEach(
|
||||
({ discoveredLabels, labels, scrapeUrl, lastError, health }: Target, idx: number) => {
|
||||
const row = table.find('tr').at(idx + 1);
|
||||
|
||||
it('renders an EndpointLink with the scrapeUrl', () => {
|
||||
const link = row.find(EndpointLink);
|
||||
expect(link).toHaveLength(1);
|
||||
expect(link.prop('endpoint')).toEqual(scrapeUrl);
|
||||
});
|
||||
|
||||
it('renders a badge for health', () => {
|
||||
const td = row.find('td').filterWhere((elem) => Boolean(elem.hasClass('state')));
|
||||
const badge = td.find(Badge);
|
||||
expect(badge).toHaveLength(1);
|
||||
expect(badge.prop('color')).toEqual(getColor(health));
|
||||
expect(badge.children().text()).toEqual(health.toUpperCase());
|
||||
});
|
||||
|
||||
it('renders series labels', () => {
|
||||
const targetLabels = row.find(TargetLabels);
|
||||
expect(targetLabels).toHaveLength(1);
|
||||
expect(targetLabels.prop('discoveredLabels')).toEqual(discoveredLabels);
|
||||
expect(targetLabels.prop('labels')).toEqual(labels);
|
||||
});
|
||||
|
||||
it('renders last scrape time', () => {
|
||||
const lastScrapeCell = row.find('td').filterWhere((elem) => Boolean(elem.hasClass('last-scrape')));
|
||||
expect(lastScrapeCell).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('renders last scrape duration', () => {
|
||||
const lastScrapeCell = row.find('td').filterWhere((elem) => Boolean(elem.hasClass('scrape-duration')));
|
||||
expect(lastScrapeCell).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('renders a badge for Errors', () => {
|
||||
const td = row.find('td').filterWhere((elem) => Boolean(elem.hasClass('errors')));
|
||||
const badge = td.find(Badge);
|
||||
expect(badge).toHaveLength(lastError ? 1 : 0);
|
||||
if (lastError) {
|
||||
expect(badge.prop('color')).toEqual('danger');
|
||||
expect(badge.children().text()).toEqual(lastError);
|
||||
}
|
||||
});
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,95 +0,0 @@
|
|||
import React, { FC } from 'react';
|
||||
import { ScrapePool, getColor } from './target';
|
||||
import { Collapse, Table, Badge } from 'reactstrap';
|
||||
import styles from './ScrapePoolPanel.module.css';
|
||||
import { Target } from './target';
|
||||
import EndpointLink from './EndpointLink';
|
||||
import TargetLabels from './TargetLabels';
|
||||
import TargetScrapeDuration from './TargetScrapeDuration';
|
||||
import { now } from 'moment';
|
||||
import { ToggleMoreLess } from '../../components/ToggleMoreLess';
|
||||
import { formatRelative } from '../../utils';
|
||||
|
||||
interface PanelProps {
|
||||
scrapePool: string;
|
||||
targetGroup: ScrapePool;
|
||||
expanded: boolean;
|
||||
toggleExpanded: () => void;
|
||||
}
|
||||
|
||||
export const columns = ['Endpoint', 'State', 'Labels', 'Last Scrape', 'Scrape Duration', 'Error'];
|
||||
|
||||
const ScrapePoolPanel: FC<PanelProps> = ({ scrapePool, targetGroup, expanded, toggleExpanded }) => {
|
||||
const modifier = targetGroup.upCount < targetGroup.targets.length ? 'danger' : 'normal';
|
||||
const id = `pool-${scrapePool}`;
|
||||
const anchorProps = {
|
||||
href: `#${id}`,
|
||||
id,
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<ToggleMoreLess event={toggleExpanded} showMore={expanded}>
|
||||
<a className={styles[modifier]} {...anchorProps}>
|
||||
{`${scrapePool} (${targetGroup.upCount}/${targetGroup.targets.length} up)`}
|
||||
</a>
|
||||
</ToggleMoreLess>
|
||||
<Collapse isOpen={expanded}>
|
||||
<Table className={styles.table} size="sm" bordered hover striped>
|
||||
<thead>
|
||||
<tr key="header">
|
||||
{columns.map((column) => (
|
||||
<th key={column}>{column}</th>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{targetGroup.targets.map((target: Target, idx: number) => {
|
||||
const {
|
||||
discoveredLabels,
|
||||
labels,
|
||||
scrapePool,
|
||||
scrapeUrl,
|
||||
globalUrl,
|
||||
lastError,
|
||||
lastScrape,
|
||||
lastScrapeDuration,
|
||||
health,
|
||||
scrapeInterval,
|
||||
scrapeTimeout,
|
||||
} = target;
|
||||
const color = getColor(health);
|
||||
|
||||
return (
|
||||
<tr key={scrapeUrl}>
|
||||
<td className={styles.endpoint}>
|
||||
<EndpointLink endpoint={scrapeUrl} globalUrl={globalUrl} />
|
||||
</td>
|
||||
<td className={styles.state}>
|
||||
<Badge color={color}>{health.toUpperCase()}</Badge>
|
||||
</td>
|
||||
<td className={styles.labels}>
|
||||
<TargetLabels discoveredLabels={discoveredLabels} labels={labels} scrapePool={scrapePool} idx={idx} />
|
||||
</td>
|
||||
<td className={styles['last-scrape']}>{formatRelative(lastScrape, now())}</td>
|
||||
<td className={styles['scrape-duration']}>
|
||||
<TargetScrapeDuration
|
||||
duration={lastScrapeDuration}
|
||||
scrapePool={scrapePool}
|
||||
idx={idx}
|
||||
interval={scrapeInterval}
|
||||
timeout={scrapeTimeout}
|
||||
/>
|
||||
</td>
|
||||
<td className={styles.errors}>{lastError ? <span className="text-danger">{lastError}</span> : null}</td>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</Table>
|
||||
</Collapse>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ScrapePoolPanel;
|
|
@ -33,10 +33,16 @@ const TargetLabels: FC<TargetLabelsProps> = ({ discoveredLabels, labels, idx, sc
|
|||
);
|
||||
})}
|
||||
</div>
|
||||
<Tooltip isOpen={tooltipOpen} target={CSS.escape(id)} toggle={toggle} style={{ maxWidth: 'none', textAlign: 'left' }}>
|
||||
<Tooltip
|
||||
isOpen={tooltipOpen}
|
||||
target={CSS.escape(id)}
|
||||
toggle={toggle}
|
||||
placement={'right-end'}
|
||||
style={{ maxWidth: 'none', textAlign: 'left' }}
|
||||
>
|
||||
<b>Before relabeling:</b>
|
||||
{formatLabels(discoveredLabels).map((s: string, idx: number) => (
|
||||
<Fragment key={idx}>
|
||||
{formatLabels(discoveredLabels).map((s: string, labelIndex: number) => (
|
||||
<Fragment key={labelIndex}>
|
||||
<br />
|
||||
<span className={styles.discovered}>{s}</span>
|
||||
</Fragment>
|
||||
|
|
|
@ -37,7 +37,7 @@ exports[`targetLabels renders discovered labels 1`] = `
|
|||
<Tooltip
|
||||
autohide={true}
|
||||
isOpen={false}
|
||||
placement="top"
|
||||
placement="right-end"
|
||||
placementPrefix="bs-tooltip"
|
||||
style={
|
||||
Object {
|
||||
|
|
|
@ -2,7 +2,7 @@ export interface Labels {
|
|||
[key: string]: string;
|
||||
}
|
||||
|
||||
export interface Target {
|
||||
export type Target = {
|
||||
discoveredLabels: Labels;
|
||||
labels: Labels;
|
||||
scrapePool: string;
|
||||
|
@ -14,7 +14,7 @@ export interface Target {
|
|||
health: string;
|
||||
scrapeInterval: string;
|
||||
scrapeTimeout: string;
|
||||
}
|
||||
};
|
||||
|
||||
export interface DroppedTarget {
|
||||
discoveredLabels: Labels;
|
||||
|
|
Loading…
Reference in a new issue