1
import { KVSearch } from '@nexucis/kvsearch';
2
import { usePathPrefix } from '../../contexts/PathPrefixContext';
3
import { useFetch } from '../../hooks/useFetch';
4
import { API_PATH } from '../../constants/constants';
5
import { filterTargetsByHealth, groupTargets, ScrapePool, ScrapePools, Target } from './target';
6
import { withStatusIndicator } from '../../components/withStatusIndicator';
7
import { FC, useCallback, useEffect, useMemo, useState } from 'react';
8
import { Badge, Col, Collapse, Dropdown, DropdownItem, DropdownMenu, DropdownToggle, Input, Row } from 'reactstrap';
9
import { ScrapePoolContent } from './ScrapePoolContent';
10
import Filter, { Expanded, FilterData } from './Filter';
11
import { useLocalStorage } from '../../hooks/useLocalStorage';
12
import styles from './ScrapePoolPanel.module.css';
13
import { ToggleMoreLess } from '../../components/ToggleMoreLess';
14
import SearchBar from '../../components/SearchBar';
15
import { setQuerySearchFilter, getQuerySearchFilter } from '../../utils/index';
16
import Checkbox from '../../components/Checkbox';
18
export interface ScrapePoolNamesListProps {
19
scrapePools: string[];
22
interface ScrapePoolDropDownProps {
23
selectedPool: string | null;
24
scrapePools: string[];
25
onScrapePoolChange: (name: string) => void;
28
const ScrapePoolDropDown: FC<ScrapePoolDropDownProps> = ({ selectedPool, scrapePools, onScrapePoolChange }) => {
29
const [dropdownOpen, setDropdownOpen] = useState(false);
30
const toggle = () => setDropdownOpen((prevState) => !prevState);
32
const [filter, setFilter] = useState<string>('');
34
const filteredPools = scrapePools.filter((pool) => pool.toLowerCase().includes(filter.toLowerCase()));
37
<Dropdown isOpen={dropdownOpen} toggle={toggle}>
38
<DropdownToggle caret className="mw-100 text-truncate">
39
{selectedPool === null || !scrapePools.includes(selectedPool) ? 'All scrape pools' : selectedPool}
41
<DropdownMenu style={{ maxHeight: 400, overflowY: 'auto' }}>
44
<DropdownItem key="__all__" value={null} onClick={() => onScrapePoolChange('')}>
47
<DropdownItem divider />
50
<DropdownItem key="__header" header toggle={false}>
51
<Input autoFocus placeholder="Filter" value={filter} onChange={(event) => setFilter(event.target.value.trim())} />
53
{scrapePools.length === 0 ? (
54
<DropdownItem disabled>No scrape pools configured</DropdownItem>
56
filteredPools.map((name) => (
57
<DropdownItem key={name} value={name} onClick={() => onScrapePoolChange(name)} active={name === selectedPool}>
67
interface ScrapePoolListProps {
68
scrapePools: string[];
69
selectedPool: string | null;
70
onPoolSelect: (name: string) => void;
73
interface ScrapePoolListContentProps extends ScrapePoolListProps {
74
activeTargets: Target[];
77
const kvSearch = new KVSearch<Target>({
79
indexedKeys: ['labels', 'scrapePool', ['labels', /.*/]],
84
targetGroup: ScrapePool;
86
toggleExpanded: () => void;
89
export const ScrapePoolPanel: FC<PanelProps> = (props: PanelProps) => {
90
const modifier = props.targetGroup.upCount < props.targetGroup.targets.length ? 'danger' : 'normal';
91
const id = `pool-${props.scrapePool}`;
98
<ToggleMoreLess event={props.toggleExpanded} showMore={props.expanded}>
99
<a className={styles[modifier]} {...anchorProps}>
100
{`${props.scrapePool} (${props.targetGroup.upCount}/${props.targetGroup.targets.length} up)`}
103
<Collapse isOpen={props.expanded}>
104
<ScrapePoolContent targets={props.targetGroup.targets} />
110
type targetHealth = 'healthy' | 'unhealthy' | 'unknown';
112
const healthColorTuples: Array<[targetHealth, string]> = [
113
['healthy', 'success'],
114
['unhealthy', 'danger'],
115
['unknown', 'warning'],
118
// ScrapePoolListContent is taking care of every possible filter
119
const ScrapePoolListContent: FC<ScrapePoolListContentProps> = ({
125
const initialPoolList = groupTargets(activeTargets);
126
const [poolList, setPoolList] = useState<ScrapePools>(initialPoolList);
127
const [targetList, setTargetList] = useState(activeTargets);
129
const initialFilter: FilterData = {
133
const [filter, setFilter] = useLocalStorage('targets-page-filter', initialFilter);
135
const [healthFilters, setHealthFilters] = useLocalStorage('target-health-filter', {
140
const toggleHealthFilter = (val: targetHealth) => () => {
143
[val]: !healthFilters[val],
147
const initialExpanded: Expanded = Object.keys(initialPoolList).reduce(
148
(acc: { [scrapePool: string]: boolean }, scrapePool: string) => ({
154
const [expanded, setExpanded] = useLocalStorage('targets-page-expansion-state', initialExpanded);
155
const { showHealthy, showUnhealthy } = filter;
157
const handleSearchChange = useCallback(
159
setQuerySearchFilter(value);
161
const result = kvSearch.filter(value.trim(), activeTargets);
162
setTargetList(result.map((value) => value.original));
164
setTargetList(activeTargets);
170
const defaultValue = useMemo(getQuerySearchFilter, []);
173
const list = targetList.filter((t) => showHealthy || t.health.toLowerCase() !== 'up');
174
setPoolList(groupTargets(list));
175
}, [showHealthy, targetList]);
179
<Row className="align-items-center">
180
<Col className="flex-grow-0 py-1">
181
<ScrapePoolDropDown selectedPool={selectedPool} scrapePools={scrapePools} onScrapePoolChange={onPoolSelect} />
183
<Col className="flex-grow-0 py-1">
184
<Filter filter={filter} setFilter={setFilter} expanded={expanded} setExpanded={setExpanded} />
186
<Col className="flex-grow-1 py-1">
188
defaultValue={defaultValue}
189
handleChange={handleSearchChange}
190
placeholder="Filter by endpoint or labels"
193
<Col className="flex-grow-0 py-1">
194
<div className="d-flex flex-row-reverse">
195
{healthColorTuples.map(([val, color]) => (
197
wrapperStyles={{ marginBottom: 0 }}
199
checked={healthFilters[val]}
200
id={`${val}-toggler`}
201
onChange={toggleHealthFilter(val)}
203
<Badge color={color} className="text-capitalize">
211
{Object.keys(poolList)
212
.filter((scrapePool) => {
213
const targetGroup = poolList[scrapePool];
214
const isHealthy = targetGroup.upCount === targetGroup.targets.length;
215
return (isHealthy && showHealthy) || (!isHealthy && showUnhealthy);
217
.map<JSX.Element>((scrapePool) => (
220
scrapePool={scrapePool}
222
upCount: poolList[scrapePool].upCount,
223
targets: poolList[scrapePool].targets.filter((target) => filterTargetsByHealth(target.health, healthFilters)),
225
expanded={expanded[scrapePool]}
226
toggleExpanded={(): void => setExpanded({ ...expanded, [scrapePool]: !expanded[scrapePool] })}
233
const ScrapePoolListWithStatusIndicator = withStatusIndicator(ScrapePoolListContent);
235
export const ScrapePoolList: FC<ScrapePoolListProps> = ({ selectedPool, scrapePools, ...props }) => {
236
// If we have more than 20 scrape pools AND there's no pool selected then select first pool
237
// by default. This is to avoid loading a huge list of targets when we have many pools configured.
238
// If we have up to 20 scrape pools then pass whatever is the value of selectedPool, it can
239
// be a pool name or a null (if all pools should be shown).
240
const poolToShow = selectedPool === null && scrapePools.length > 20 ? scrapePools[0] : selectedPool;
242
const pathPrefix = usePathPrefix();
243
const { response, error, isLoading } = useFetch<ScrapePoolListContentProps>(
244
`${pathPrefix}/${API_PATH}/targets?state=active${poolToShow === null ? '' : `&scrapePool=${poolToShow}`}`
246
const { status: responseStatus } = response;
247
const badResponse = responseStatus !== 'success' && responseStatus !== 'start fetching';
250
<ScrapePoolListWithStatusIndicator
253
selectedPool={poolToShow}
254
scrapePools={scrapePools}
255
error={badResponse ? new Error(responseStatus) : error}
256
isLoading={isLoading}
257
componentTitle="Targets information"
262
export default ScrapePoolList;