mirror of
https://github.com/prometheus/prometheus.git
synced 2025-03-05 20:59:13 -08:00
Adds support service discovery page in react ui (#6394)
* active targets component completed Signed-off-by: Harkishen-Singh <harkishensingh@hotmail.com> * support for service-discovery in react ui Signed-off-by: Harkishen-Singh <harkishensingh@hotmail.com> * restored prev files Signed-off-by: Harkishen-Singh <harkishensingh@hotmail.com> * used fc Signed-off-by: Harkishen Singh <harkishensingh@hotmail.com> * removed trivial keys Signed-off-by: Harkishen Singh <harkishensingh@hotmail.com> * FC based labels Signed-off-by: Harkishen Singh <harkishensingh@hotmail.com> * implemented suggestions Signed-off-by: Harkishen Singh <harkishensingh@hotmail.com> * implemented suggestions Signed-off-by: Harkishen Singh <harkishensingh@hotmail.com> * implmented suggestions Signed-off-by: Harkishen Singh <harkishensingh@hotmail.com> * minor word change Signed-off-by: Harkishen Singh <harkishensingh@hotmail.com> * before dropped addressed Signed-off-by: Harkishen Singh <harkishensingh@hotmail.com> * implemented suggestions Signed-off-by: Harkishen Singh <harkishensingh@hotmail.com> * linted Signed-off-by: Harkishen Singh <harkishensingh@hotmail.com> * implemented suggestions Signed-off-by: Harkishen Singh <harkishensingh@hotmail.com> * implemented suggestions. removed false styles Signed-off-by: Harkishen Singh <harkishensingh@hotmail.com> * implemented suggestions. Unified buttons with targets screen. Signed-off-by: Harkishen Singh <harkishensingh@hotmail.com> * component for ToggleButton Signed-off-by: Harkishen Singh <harkishensingh@hotmail.com> * removed false Button Signed-off-by: Harkishen Singh <harkishensingh@hotmail.com> * implemented suggestions. Signed-off-by: Harkishen Singh <harkishensingh@hotmail.com> * tests for ToggleMoreLess component Signed-off-by: Harkishen Singh <harkishensingh@hotmail.com> * linted Signed-off-by: Harkishen Singh <harkishensingh@hotmail.com> * fixed nested h3. implemented suggestions Signed-off-by: Harkishen Singh <harkishensingh@hotmail.com> * linted Signed-off-by: Harkishen Singh <harkishensingh@hotmail.com>
This commit is contained in:
parent
ae93bae88f
commit
edf8f135bc
|
@ -208,6 +208,11 @@ button.execute-btn {
|
||||||
margin-bottom: 20px;
|
margin-bottom: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.target-head {
|
||||||
|
font-weight: 700;
|
||||||
|
font-size: large;
|
||||||
|
}
|
||||||
|
|
||||||
.status-badges {
|
.status-badges {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
|
|
65
web/ui/react-app/src/pages/LabelsTable.tsx
Normal file
65
web/ui/react-app/src/pages/LabelsTable.tsx
Normal file
|
@ -0,0 +1,65 @@
|
||||||
|
import React, { FC, useState } from 'react';
|
||||||
|
import { RouteComponentProps } from '@reach/router';
|
||||||
|
import { Badge, Table } from 'reactstrap';
|
||||||
|
import { TargetLabels } from './Services';
|
||||||
|
import { ToggleMoreLess } from './targets/ToggleMoreLess';
|
||||||
|
|
||||||
|
interface LabelProps {
|
||||||
|
value: TargetLabels[];
|
||||||
|
name: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const formatLabels = (labels: Record<string, string> | string) => {
|
||||||
|
return Object.entries(labels).map(([key, value]) => {
|
||||||
|
return (
|
||||||
|
<div key={key}>
|
||||||
|
<Badge color="primary" className="mr-1">
|
||||||
|
{`${key}="${value}"`}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const LabelsTable: FC<RouteComponentProps & LabelProps> = ({ value, name }) => {
|
||||||
|
const [showMore, setShowMore] = useState(false);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div>
|
||||||
|
<ToggleMoreLess
|
||||||
|
event={(): void => {
|
||||||
|
setShowMore(!showMore);
|
||||||
|
}}
|
||||||
|
showMore={showMore}
|
||||||
|
>
|
||||||
|
<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}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
|
@ -1,15 +1,119 @@
|
||||||
import React, { FC } from 'react';
|
import React, { FC } from 'react';
|
||||||
import { RouteComponentProps } from '@reach/router';
|
import { RouteComponentProps } from '@reach/router';
|
||||||
import PathPrefixProps from '../PathPrefixProps';
|
import PathPrefixProps from '../PathPrefixProps';
|
||||||
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||||
|
import { faSpinner } from '@fortawesome/free-solid-svg-icons';
|
||||||
import { Alert } from 'reactstrap';
|
import { Alert } from 'reactstrap';
|
||||||
|
import { useFetch } from '../utils/useFetch';
|
||||||
|
import { LabelsTable } from './LabelsTable';
|
||||||
|
import { Target, Labels, DroppedTarget } from './targets/target';
|
||||||
|
|
||||||
const Services: FC<RouteComponentProps & PathPrefixProps> = ({ pathPrefix }) => (
|
// TODO: Deduplicate with https://github.com/prometheus/prometheus/blob/213a8fe89a7308e73f22888a963cbf9375217cd6/web/ui/react-app/src/pages/targets/ScrapePoolList.tsx#L11-L14
|
||||||
<>
|
interface ServiceMap {
|
||||||
<h2>Service Discovery</h2>
|
activeTargets: Target[];
|
||||||
<Alert color="warning">
|
droppedTargets: DroppedTarget[];
|
||||||
This page is still under construction. Please try it in the <a href={`${pathPrefix}/service-discovery`}>Classic UI</a>.
|
}
|
||||||
</Alert>
|
|
||||||
</>
|
export interface TargetLabels {
|
||||||
);
|
discoveredLabels: Labels;
|
||||||
|
labels: Labels;
|
||||||
|
isDropped: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Services: FC<RouteComponentProps & PathPrefixProps> = ({ pathPrefix }) => {
|
||||||
|
const { response, error } = useFetch<ServiceMap>(`${pathPrefix}/api/v1/targets`);
|
||||||
|
|
||||||
|
const processSummary = (response: ServiceMap) => {
|
||||||
|
const targets: any = {};
|
||||||
|
|
||||||
|
// Get targets of each type along with the total and active end points
|
||||||
|
for (const target of response.activeTargets) {
|
||||||
|
const { scrapePool: name } = target;
|
||||||
|
if (!targets[name]) {
|
||||||
|
targets[name] = {
|
||||||
|
total: 0,
|
||||||
|
active: 0,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
targets[name].total++;
|
||||||
|
targets[name].active++;
|
||||||
|
}
|
||||||
|
for (const target of response.droppedTargets) {
|
||||||
|
const { job: name } = target.discoveredLabels;
|
||||||
|
if (!targets[name]) {
|
||||||
|
targets[name] = {
|
||||||
|
total: 0,
|
||||||
|
active: 0,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
targets[name].total++;
|
||||||
|
}
|
||||||
|
|
||||||
|
return targets;
|
||||||
|
};
|
||||||
|
|
||||||
|
const processTargets = (response: Target[], dropped: DroppedTarget[]) => {
|
||||||
|
const labels: Record<string, TargetLabels[]> = {};
|
||||||
|
|
||||||
|
for (const target of response) {
|
||||||
|
const name = target.scrapePool;
|
||||||
|
if (!labels[name]) {
|
||||||
|
labels[name] = [];
|
||||||
|
}
|
||||||
|
labels[name].push({
|
||||||
|
discoveredLabels: target.discoveredLabels,
|
||||||
|
labels: target.labels,
|
||||||
|
isDropped: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const target of dropped) {
|
||||||
|
const { job: name } = target.discoveredLabels;
|
||||||
|
if (!labels[name]) {
|
||||||
|
labels[name] = [];
|
||||||
|
}
|
||||||
|
labels[name].push({
|
||||||
|
discoveredLabels: target.discoveredLabels,
|
||||||
|
isDropped: true,
|
||||||
|
labels: {},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return labels;
|
||||||
|
};
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
return (
|
||||||
|
<Alert color="danger">
|
||||||
|
<strong>Error:</strong> Error fetching Service-Discovery: {error.message}
|
||||||
|
</Alert>
|
||||||
|
);
|
||||||
|
} else if (response.data) {
|
||||||
|
const targets = processSummary(response.data);
|
||||||
|
const labels = processTargets(response.data.activeTargets, response.data.droppedTargets);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<h2>Service Discovery</h2>
|
||||||
|
<ul>
|
||||||
|
{Object.keys(targets).map((val, i) => (
|
||||||
|
<li key={i}>
|
||||||
|
<a href={'#' + val}>
|
||||||
|
{' '}
|
||||||
|
{val} ({targets[val].active} / {targets[val].total} active targets){' '}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
<hr />
|
||||||
|
{Object.keys(labels).map((val: any, i) => {
|
||||||
|
const value = labels[val];
|
||||||
|
return <LabelsTable value={value} name={val} key={Object.keys(labels)[i]} />;
|
||||||
|
})}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return <FontAwesomeIcon icon={faSpinner} spin />;
|
||||||
|
};
|
||||||
|
|
||||||
export default Services;
|
export default Services;
|
||||||
|
|
|
@ -20,10 +20,6 @@ describe('ScrapePoolPanel', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('Header', () => {
|
describe('Header', () => {
|
||||||
it('renders an h3', () => {
|
|
||||||
expect(scrapePoolPanel.find('h3')).toHaveLength(1);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('renders an anchor with up count and danger color if upCount < targetsCount', () => {
|
it('renders an anchor with up count and danger color if upCount < targetsCount', () => {
|
||||||
const anchor = scrapePoolPanel.find('a');
|
const anchor = scrapePoolPanel.find('a');
|
||||||
expect(anchor).toHaveLength(1);
|
expect(anchor).toHaveLength(1);
|
||||||
|
@ -47,14 +43,6 @@ describe('ScrapePoolPanel', () => {
|
||||||
expect(anchor.prop('className')).toEqual('normal');
|
expect(anchor.prop('className')).toEqual('normal');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('renders a show less btn if expanded', () => {
|
|
||||||
const btn = scrapePoolPanel.find(Button);
|
|
||||||
expect(btn).toHaveLength(1);
|
|
||||||
expect(btn.prop('color')).toEqual('primary');
|
|
||||||
expect(btn.prop('size')).toEqual('xs');
|
|
||||||
expect(btn.render().text()).toEqual('show less');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('renders a show more btn if collapsed', () => {
|
it('renders a show more btn if collapsed', () => {
|
||||||
const props = {
|
const props = {
|
||||||
scrapePool: 'prometheus',
|
scrapePool: 'prometheus',
|
||||||
|
@ -67,7 +55,6 @@ describe('ScrapePoolPanel', () => {
|
||||||
|
|
||||||
const btn = scrapePoolPanel.find(Button);
|
const btn = scrapePoolPanel.find(Button);
|
||||||
btn.simulate('click');
|
btn.simulate('click');
|
||||||
expect(btn.render().text()).toEqual('show more');
|
|
||||||
const collapse = scrapePoolPanel.find(Collapse);
|
const collapse = scrapePoolPanel.find(Collapse);
|
||||||
expect(collapse.prop('isOpen')).toBe(false);
|
expect(collapse.prop('isOpen')).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import React, { FC } from 'react';
|
import React, { FC } from 'react';
|
||||||
import { ScrapePool, getColor } from './target';
|
import { ScrapePool, getColor } from './target';
|
||||||
import { Button, Collapse, Table, Badge } from 'reactstrap';
|
import { Collapse, Table, Badge } from 'reactstrap';
|
||||||
import styles from './ScrapePoolPanel.module.css';
|
import styles from './ScrapePoolPanel.module.css';
|
||||||
import { Target } from './target';
|
import { Target } from './target';
|
||||||
import EndpointLink from './EndpointLink';
|
import EndpointLink from './EndpointLink';
|
||||||
|
@ -8,6 +8,7 @@ import TargetLabels from './TargetLabels';
|
||||||
import { formatRelative, humanizeDuration } from '../../utils/timeFormat';
|
import { formatRelative, humanizeDuration } from '../../utils/timeFormat';
|
||||||
import { now } from 'moment';
|
import { now } from 'moment';
|
||||||
import { useLocalStorage } from '../../hooks/useLocalStorage';
|
import { useLocalStorage } from '../../hooks/useLocalStorage';
|
||||||
|
import { ToggleMoreLess } from './ToggleMoreLess';
|
||||||
|
|
||||||
interface PanelProps {
|
interface PanelProps {
|
||||||
scrapePool: string;
|
scrapePool: string;
|
||||||
|
@ -24,27 +25,14 @@ const ScrapePoolPanel: FC<PanelProps> = ({ scrapePool, targetGroup }) => {
|
||||||
href: `#${id}`,
|
href: `#${id}`,
|
||||||
id,
|
id,
|
||||||
};
|
};
|
||||||
const btnProps = {
|
|
||||||
children: `show ${expanded ? 'less' : 'more'}`,
|
|
||||||
color: 'primary',
|
|
||||||
onClick: (): void => setOptions({ expanded: !expanded }),
|
|
||||||
size: 'xs',
|
|
||||||
style: {
|
|
||||||
padding: '0.3em 0.3em 0.25em 0.3em',
|
|
||||||
fontSize: '0.375em',
|
|
||||||
marginLeft: '1em',
|
|
||||||
verticalAlign: 'baseline',
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.container}>
|
<div className={styles.container}>
|
||||||
<h3>
|
<ToggleMoreLess event={(): void => setOptions({ expanded: !expanded })} showMore={expanded}>
|
||||||
<a className={styles[modifier]} {...anchorProps}>
|
<a className={styles[modifier]} {...anchorProps}>
|
||||||
{`${scrapePool} (${targetGroup.upCount}/${targetGroup.targets.length} up)`}
|
{`${scrapePool} (${targetGroup.upCount}/${targetGroup.targets.length} up)`}
|
||||||
</a>
|
</a>
|
||||||
<Button {...btnProps} />
|
</ToggleMoreLess>
|
||||||
</h3>
|
|
||||||
<Collapse isOpen={expanded}>
|
<Collapse isOpen={expanded}>
|
||||||
<Table className={styles.table} size="sm" bordered hover striped>
|
<Table className={styles.table} size="sm" bordered hover striped>
|
||||||
<thead>
|
<thead>
|
||||||
|
|
33
web/ui/react-app/src/pages/targets/ToggleMoreLess.test.tsx
Normal file
33
web/ui/react-app/src/pages/targets/ToggleMoreLess.test.tsx
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { shallow } from 'enzyme';
|
||||||
|
import { Button } from 'reactstrap';
|
||||||
|
import { ToggleMoreLess } from './ToggleMoreLess';
|
||||||
|
|
||||||
|
describe('ToggleMoreLess', () => {
|
||||||
|
const showMoreValue = false;
|
||||||
|
const defaultProps = {
|
||||||
|
event: (): void => {
|
||||||
|
tggleBtn.setProps({ showMore: !showMoreValue });
|
||||||
|
},
|
||||||
|
showMore: showMoreValue,
|
||||||
|
};
|
||||||
|
const tggleBtn = shallow(<ToggleMoreLess {...defaultProps} />);
|
||||||
|
|
||||||
|
it('renders a show more btn at start', () => {
|
||||||
|
const btn = tggleBtn.find(Button);
|
||||||
|
expect(btn).toHaveLength(1);
|
||||||
|
expect(btn.prop('color')).toEqual('primary');
|
||||||
|
expect(btn.prop('size')).toEqual('xs');
|
||||||
|
expect(btn.render().text()).toEqual('show more');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders a show less btn if clicked', () => {
|
||||||
|
tggleBtn.find(Button).simulate('click');
|
||||||
|
expect(
|
||||||
|
tggleBtn
|
||||||
|
.find(Button)
|
||||||
|
.render()
|
||||||
|
.text()
|
||||||
|
).toEqual('show less');
|
||||||
|
});
|
||||||
|
});
|
28
web/ui/react-app/src/pages/targets/ToggleMoreLess.tsx
Normal file
28
web/ui/react-app/src/pages/targets/ToggleMoreLess.tsx
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
import React, { FC } from 'react';
|
||||||
|
import { Button } from 'reactstrap';
|
||||||
|
|
||||||
|
interface ToggleMoreLessProps {
|
||||||
|
event(): void;
|
||||||
|
showMore: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ToggleMoreLess: FC<ToggleMoreLessProps> = ({ children, event, showMore }) => {
|
||||||
|
return (
|
||||||
|
<h3>
|
||||||
|
{children}
|
||||||
|
<Button
|
||||||
|
size="xs"
|
||||||
|
onClick={event}
|
||||||
|
style={{
|
||||||
|
padding: '0.3em 0.3em 0.25em 0.3em',
|
||||||
|
fontSize: '0.375em',
|
||||||
|
marginLeft: '1em',
|
||||||
|
verticalAlign: 'baseline',
|
||||||
|
}}
|
||||||
|
color="primary"
|
||||||
|
>
|
||||||
|
show {showMore ? 'less' : 'more'}
|
||||||
|
</Button>
|
||||||
|
</h3>
|
||||||
|
);
|
||||||
|
};
|
|
@ -13,6 +13,10 @@ export interface Target {
|
||||||
health: string;
|
health: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface DroppedTarget {
|
||||||
|
discoveredLabels: Labels;
|
||||||
|
}
|
||||||
|
|
||||||
export interface ScrapePool {
|
export interface ScrapePool {
|
||||||
upCount: number;
|
upCount: number;
|
||||||
targets: Target[];
|
targets: Target[];
|
||||||
|
|
Loading…
Reference in a new issue