mirror of
https://github.com/prometheus/prometheus.git
synced 2025-01-12 14:27:27 -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;
|
||||
}
|
||||
|
||||
.target-head {
|
||||
font-weight: 700;
|
||||
font-size: large;
|
||||
}
|
||||
|
||||
.status-badges {
|
||||
display: flex;
|
||||
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 { RouteComponentProps } from '@reach/router';
|
||||
import PathPrefixProps from '../PathPrefixProps';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import { faSpinner } from '@fortawesome/free-solid-svg-icons';
|
||||
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 }) => (
|
||||
<>
|
||||
<h2>Service Discovery</h2>
|
||||
<Alert color="warning">
|
||||
This page is still under construction. Please try it in the <a href={`${pathPrefix}/service-discovery`}>Classic UI</a>.
|
||||
</Alert>
|
||||
</>
|
||||
);
|
||||
// TODO: Deduplicate with https://github.com/prometheus/prometheus/blob/213a8fe89a7308e73f22888a963cbf9375217cd6/web/ui/react-app/src/pages/targets/ScrapePoolList.tsx#L11-L14
|
||||
interface ServiceMap {
|
||||
activeTargets: Target[];
|
||||
droppedTargets: DroppedTarget[];
|
||||
}
|
||||
|
||||
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;
|
||||
|
|
|
@ -20,10 +20,6 @@ describe('ScrapePoolPanel', () => {
|
|||
});
|
||||
|
||||
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', () => {
|
||||
const anchor = scrapePoolPanel.find('a');
|
||||
expect(anchor).toHaveLength(1);
|
||||
|
@ -47,14 +43,6 @@ describe('ScrapePoolPanel', () => {
|
|||
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', () => {
|
||||
const props = {
|
||||
scrapePool: 'prometheus',
|
||||
|
@ -67,7 +55,6 @@ describe('ScrapePoolPanel', () => {
|
|||
|
||||
const btn = scrapePoolPanel.find(Button);
|
||||
btn.simulate('click');
|
||||
expect(btn.render().text()).toEqual('show more');
|
||||
const collapse = scrapePoolPanel.find(Collapse);
|
||||
expect(collapse.prop('isOpen')).toBe(false);
|
||||
});
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import React, { FC } from 'react';
|
||||
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 { Target } from './target';
|
||||
import EndpointLink from './EndpointLink';
|
||||
|
@ -8,6 +8,7 @@ import TargetLabels from './TargetLabels';
|
|||
import { formatRelative, humanizeDuration } from '../../utils/timeFormat';
|
||||
import { now } from 'moment';
|
||||
import { useLocalStorage } from '../../hooks/useLocalStorage';
|
||||
import { ToggleMoreLess } from './ToggleMoreLess';
|
||||
|
||||
interface PanelProps {
|
||||
scrapePool: string;
|
||||
|
@ -24,27 +25,14 @@ const ScrapePoolPanel: FC<PanelProps> = ({ scrapePool, targetGroup }) => {
|
|||
href: `#${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 (
|
||||
<div className={styles.container}>
|
||||
<h3>
|
||||
<ToggleMoreLess event={(): void => setOptions({ expanded: !expanded })} showMore={expanded}>
|
||||
<a className={styles[modifier]} {...anchorProps}>
|
||||
{`${scrapePool} (${targetGroup.upCount}/${targetGroup.targets.length} up)`}
|
||||
</a>
|
||||
<Button {...btnProps} />
|
||||
</h3>
|
||||
</ToggleMoreLess>
|
||||
<Collapse isOpen={expanded}>
|
||||
<Table className={styles.table} size="sm" bordered hover striped>
|
||||
<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;
|
||||
}
|
||||
|
||||
export interface DroppedTarget {
|
||||
discoveredLabels: Labels;
|
||||
}
|
||||
|
||||
export interface ScrapePool {
|
||||
upCount: number;
|
||||
targets: Target[];
|
||||
|
|
Loading…
Reference in a new issue