diff --git a/web/ui/react-app/src/App.css b/web/ui/react-app/src/App.css index b7e0bcf4a7..1043755ab2 100644 --- a/web/ui/react-app/src/App.css +++ b/web/ui/react-app/src/App.css @@ -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; diff --git a/web/ui/react-app/src/pages/LabelsTable.tsx b/web/ui/react-app/src/pages/LabelsTable.tsx new file mode 100644 index 0000000000..7bf2fb0e96 --- /dev/null +++ b/web/ui/react-app/src/pages/LabelsTable.tsx @@ -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) => { + return Object.entries(labels).map(([key, value]) => { + return ( +
+ + {`${key}="${value}"`} + +
+ ); + }); +}; + +export const LabelsTable: FC = ({ value, name }) => { + const [showMore, setShowMore] = useState(false); + + return ( + <> +
+ { + setShowMore(!showMore); + }} + showMore={showMore} + > + {name} + +
+ {showMore ? ( + + + + + + + + + {value.map((_, i) => { + return ( + + + {value[i].isDropped ? ( + + ) : ( + + )} + + ); + })} + +
Discovered LabelsTarget Labels
{formatLabels(value[i].discoveredLabels)}Dropped{formatLabels(value[i].labels)}
+ ) : null} + + ); +}; diff --git a/web/ui/react-app/src/pages/Services.tsx b/web/ui/react-app/src/pages/Services.tsx index e28e20b2b6..1b4a71d83a 100644 --- a/web/ui/react-app/src/pages/Services.tsx +++ b/web/ui/react-app/src/pages/Services.tsx @@ -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 = ({ pathPrefix }) => ( - <> -

Service Discovery

- - This page is still under construction. Please try it in the Classic UI. - - -); +// 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 = ({ pathPrefix }) => { + const { response, error } = useFetch(`${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 = {}; + + 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 ( + + Error: Error fetching Service-Discovery: {error.message} + + ); + } else if (response.data) { + const targets = processSummary(response.data); + const labels = processTargets(response.data.activeTargets, response.data.droppedTargets); + + return ( + <> +

Service Discovery

+ +
+ {Object.keys(labels).map((val: any, i) => { + const value = labels[val]; + return ; + })} + + ); + } + return ; +}; export default Services; diff --git a/web/ui/react-app/src/pages/targets/ScrapePoolPanel.test.tsx b/web/ui/react-app/src/pages/targets/ScrapePoolPanel.test.tsx index 2d98279895..c14a531a79 100644 --- a/web/ui/react-app/src/pages/targets/ScrapePoolPanel.test.tsx +++ b/web/ui/react-app/src/pages/targets/ScrapePoolPanel.test.tsx @@ -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); }); diff --git a/web/ui/react-app/src/pages/targets/ScrapePoolPanel.tsx b/web/ui/react-app/src/pages/targets/ScrapePoolPanel.tsx index b2c8497f28..fa2b21d3d4 100644 --- a/web/ui/react-app/src/pages/targets/ScrapePoolPanel.tsx +++ b/web/ui/react-app/src/pages/targets/ScrapePoolPanel.tsx @@ -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 = ({ 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 (
-

+ setOptions({ expanded: !expanded })} showMore={expanded}> {`${scrapePool} (${targetGroup.upCount}/${targetGroup.targets.length} up)`} -

+ diff --git a/web/ui/react-app/src/pages/targets/ToggleMoreLess.test.tsx b/web/ui/react-app/src/pages/targets/ToggleMoreLess.test.tsx new file mode 100644 index 0000000000..dc3c5a0f94 --- /dev/null +++ b/web/ui/react-app/src/pages/targets/ToggleMoreLess.test.tsx @@ -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(); + + 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'); + }); +}); diff --git a/web/ui/react-app/src/pages/targets/ToggleMoreLess.tsx b/web/ui/react-app/src/pages/targets/ToggleMoreLess.tsx new file mode 100644 index 0000000000..80abbcd1e9 --- /dev/null +++ b/web/ui/react-app/src/pages/targets/ToggleMoreLess.tsx @@ -0,0 +1,28 @@ +import React, { FC } from 'react'; +import { Button } from 'reactstrap'; + +interface ToggleMoreLessProps { + event(): void; + showMore: boolean; +} + +export const ToggleMoreLess: FC = ({ children, event, showMore }) => { + return ( +

+ {children} + +

+ ); +}; diff --git a/web/ui/react-app/src/pages/targets/target.ts b/web/ui/react-app/src/pages/targets/target.ts index cca5ca138f..509bb98037 100644 --- a/web/ui/react-app/src/pages/targets/target.ts +++ b/web/ui/react-app/src/pages/targets/target.ts @@ -13,6 +13,10 @@ export interface Target { health: string; } +export interface DroppedTarget { + discoveredLabels: Labels; +} + export interface ScrapePool { upCount: number; targets: Target[];