Browse Source

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>
pull/10144/head
Augustin Husson 3 years ago committed by GitHub
parent
commit
dd9f96b893
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 69
      web/ui/package-lock.json
  2. 2
      web/ui/react-app/package.json
  3. 91
      web/ui/react-app/src/pages/targets/ScrapePoolContent.tsx
  4. 5
      web/ui/react-app/src/pages/targets/ScrapePoolList.test.tsx
  5. 104
      web/ui/react-app/src/pages/targets/ScrapePoolList.tsx
  6. 137
      web/ui/react-app/src/pages/targets/ScrapePoolPanel.test.tsx
  7. 95
      web/ui/react-app/src/pages/targets/ScrapePoolPanel.tsx
  8. 12
      web/ui/react-app/src/pages/targets/TargetLabels.tsx
  9. 2
      web/ui/react-app/src/pages/targets/__snapshots__/TargetLabels.test.tsx.snap
  10. 4
      web/ui/react-app/src/pages/targets/target.ts

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

2
web/ui/react-app/package.json

@ -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

@ -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>
);
};

5
web/ui/react-app/src/pages/targets/ScrapePoolList.test.tsx

@ -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);

104
web/ui/react-app/src/pages/targets/ScrapePoolList.tsx

@ -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 (
<>
<Row xs="4" className="align-items-center">
<Col>
<Filter filter={filter} setFilter={setFilter} expanded={expanded} setExpanded={setExpanded} />
{Object.keys(targetGroups)
</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;

137
web/ui/react-app/src/pages/targets/ScrapePoolPanel.test.tsx

@ -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);
}
});
}
);
});
});
});

95
web/ui/react-app/src/pages/targets/ScrapePoolPanel.tsx

@ -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;

12
web/ui/react-app/src/pages/targets/TargetLabels.tsx

@ -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>

2
web/ui/react-app/src/pages/targets/__snapshots__/TargetLabels.test.tsx.snap

@ -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 {

4
web/ui/react-app/src/pages/targets/target.ts

@ -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…
Cancel
Save