diff --git a/web/ui/package-lock.json b/web/ui/package-lock.json index 7bce79f19..7267da902 100644 --- a/web/ui/package-lock.json +++ b/web/ui/package-lock.json @@ -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 diff --git a/web/ui/react-app/package.json b/web/ui/react-app/package.json index 3369b9332..5eed84df1 100644 --- a/web/ui/react-app/package.json +++ b/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", diff --git a/web/ui/react-app/src/pages/targets/ScrapePoolContent.tsx b/web/ui/react-app/src/pages/targets/ScrapePoolContent.tsx new file mode 100644 index 000000000..8a55a09b1 --- /dev/null +++ b/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 = ({ targets }) => { + const [items, setItems] = useState(targets.slice(0, 50)); + const [index, setIndex] = useState(initialNumberOfTargetsDisplayed); + const [hasMore, setHasMore] = useState(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 ( + loading...} + dataLength={items.length} + height={items.length > 25 ? '75vh' : ''} + > + + + + {columns.map((column) => ( + + ))} + + + + {items.map((target, index) => ( + + + + + + + + + ))} + +
{column}
+ + + {target.health.toUpperCase()} + + + {formatRelative(target.lastScrape, now())} + + + {target.lastError ? {target.lastError} : null} +
+
+ ); +}; diff --git a/web/ui/react-app/src/pages/targets/ScrapePoolList.test.tsx b/web/ui/react-app/src/pages/targets/ScrapePoolList.test.tsx index dc400de99..867d1d3be 100644 --- a/web/ui/react-app/src/pages/targets/ScrapePoolList.test.tsx +++ b/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); diff --git a/web/ui/react-app/src/pages/targets/ScrapePoolList.tsx b/web/ui/react-app/src/pages/targets/ScrapePoolList.tsx index a5f218363..f3f42da75 100644 --- a/web/ui/react-app/src/pages/targets/ScrapePoolList.tsx +++ b/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 = ({ 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 = (props: PanelProps) => { + const modifier = props.targetGroup.upCount < props.targetGroup.targets.length ? 'danger' : 'normal'; + const id = `pool-${props.scrapePool}`; + const anchorProps = { + href: `#${id}`, + id, + }; + return ( + + ); +}; + +// ScrapePoolListContent is taking care of every possible filter +const ScrapePoolListContent: FC = ({ activeTargets }) => { + const initialPoolList = groupTargets(activeTargets); + const [poolList, setPoolList] = useState(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 = ({ activeTargets }) => {} ); const [expanded, setExpanded] = useLocalStorage('targets-page-expansion-state', initialExpanded); - const { showHealthy, showUnhealthy } = filter; + + const handleSearchChange = (e: ChangeEvent) => { + 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 ( <> - - {Object.keys(targetGroups) + + + + + + + + {} + + + + + + {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 = ({ activeTargets }) => setExpanded({ ...expanded, [scrapePool]: !expanded[scrapePool] })} /> @@ -51,11 +124,10 @@ export const ScrapePoolContent: FC = ({ 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(`${pathPrefix}/${API_PATH}/targets?state=active`); const { status: responseStatus } = response; diff --git a/web/ui/react-app/src/pages/targets/ScrapePoolPanel.test.tsx b/web/ui/react-app/src/pages/targets/ScrapePoolPanel.test.tsx deleted file mode 100644 index 5facf7b76..000000000 --- a/web/ui/react-app/src/pages/targets/ScrapePoolPanel.test.tsx +++ /dev/null @@ -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(); - - 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(); - 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(); - - 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); - } - }); - } - ); - }); - }); -}); diff --git a/web/ui/react-app/src/pages/targets/ScrapePoolPanel.tsx b/web/ui/react-app/src/pages/targets/ScrapePoolPanel.tsx deleted file mode 100644 index 35ff99eb2..000000000 --- a/web/ui/react-app/src/pages/targets/ScrapePoolPanel.tsx +++ /dev/null @@ -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 = ({ scrapePool, targetGroup, expanded, toggleExpanded }) => { - const modifier = targetGroup.upCount < targetGroup.targets.length ? 'danger' : 'normal'; - const id = `pool-${scrapePool}`; - const anchorProps = { - href: `#${id}`, - id, - }; - - return ( -
- - - {`${scrapePool} (${targetGroup.upCount}/${targetGroup.targets.length} up)`} - - - - - - - {columns.map((column) => ( - - ))} - - - - {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 ( - - - - - - - - - ); - })} - -
{column}
- - - {health.toUpperCase()} - - - {formatRelative(lastScrape, now())} - - {lastError ? {lastError} : null}
-
-
- ); -}; - -export default ScrapePoolPanel; diff --git a/web/ui/react-app/src/pages/targets/TargetLabels.tsx b/web/ui/react-app/src/pages/targets/TargetLabels.tsx index 2664cda96..d85c58304 100644 --- a/web/ui/react-app/src/pages/targets/TargetLabels.tsx +++ b/web/ui/react-app/src/pages/targets/TargetLabels.tsx @@ -33,10 +33,16 @@ const TargetLabels: FC = ({ discoveredLabels, labels, idx, sc ); })} - + Before relabeling: - {formatLabels(discoveredLabels).map((s: string, idx: number) => ( - + {formatLabels(discoveredLabels).map((s: string, labelIndex: number) => ( +
{s}
diff --git a/web/ui/react-app/src/pages/targets/__snapshots__/TargetLabels.test.tsx.snap b/web/ui/react-app/src/pages/targets/__snapshots__/TargetLabels.test.tsx.snap index 76c139feb..3c5c856f0 100644 --- a/web/ui/react-app/src/pages/targets/__snapshots__/TargetLabels.test.tsx.snap +++ b/web/ui/react-app/src/pages/targets/__snapshots__/TargetLabels.test.tsx.snap @@ -37,7 +37,7 @@ exports[`targetLabels renders discovered labels 1`] = `