@ -2,10 +2,10 @@ 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 { filterTargetsByHealth , groupTargets , ScrapePool , ScrapePools , Target } from './target' ;
import { withStatusIndicator } from '../../components/withStatusIndicator' ;
import { FC , useCallback , useEffect , useMemo , useState } from 'react' ;
import { Col , Collapse , Row } from 'reactstrap' ;
import { Badge , Col , Collapse , Dropdown , DropdownItem , DropdownMenu , DropdownToggle , Input , Row } from 'reactstrap' ;
import { ScrapePoolContent } from './ScrapePoolContent' ;
import Filter , { Expanded , FilterData } from './Filter' ;
import { useLocalStorage } from '../../hooks/useLocalStorage' ;
@ -13,8 +13,64 @@ import styles from './ScrapePoolPanel.module.css';
import { ToggleMoreLess } from '../../components/ToggleMoreLess' ;
import SearchBar from '../../components/SearchBar' ;
import { setQuerySearchFilter , getQuerySearchFilter } from '../../utils/index' ;
import Checkbox from '../../components/Checkbox' ;
export interface ScrapePoolNamesListProps {
scrapePools : string [ ] ;
}
interface ScrapePoolDropDownProps {
selectedPool : string | null ;
scrapePools : string [ ] ;
onScrapePoolChange : ( name : string ) = > void ;
}
const ScrapePoolDropDown : FC < ScrapePoolDropDownProps > = ( { selectedPool , scrapePools , onScrapePoolChange } ) = > {
const [ dropdownOpen , setDropdownOpen ] = useState ( false ) ;
const toggle = ( ) = > setDropdownOpen ( ( prevState ) = > ! prevState ) ;
const [ filter , setFilter ] = useState < string > ( '' ) ;
return (
< Dropdown isOpen = { dropdownOpen } toggle = { toggle } >
< DropdownToggle caret className = "mw-100 text-truncate" >
{ selectedPool === null || ! scrapePools . includes ( selectedPool ) ? 'All scrape pools' : selectedPool }
< / DropdownToggle >
< DropdownMenu style = { { maxHeight : 400 , overflowY : 'auto' } } >
{ selectedPool ? (
< >
< DropdownItem key = "__all__" value = { null } onClick = { ( ) = > onScrapePoolChange ( '' ) } >
Clear selection
< / DropdownItem >
< DropdownItem divider / >
< / >
) : null }
< DropdownItem key = "__header" header toggle = { false } >
< Input autoFocus placeholder = "Filter" value = { filter } onChange = { ( event ) = > setFilter ( event . target . value . trim ( ) ) } / >
< / DropdownItem >
{ scrapePools . length === 0 ? (
< DropdownItem disabled > No scrape pools configured < / DropdownItem >
) : (
scrapePools
. filter ( ( name ) = > filter === '' || name . includes ( filter ) )
. map ( ( name ) = > (
< DropdownItem key = { name } value = { name } onClick = { ( ) = > onScrapePoolChange ( name ) } active = { name === selectedPool } >
{ name }
< / DropdownItem >
) )
) }
< / DropdownMenu >
< / Dropdown >
) ;
} ;
interface ScrapePoolListProps {
scrapePools : string [ ] ;
selectedPool : string | null ;
onPoolSelect : ( name : string ) = > void ;
}
interface ScrapePoolListContentProps extends ScrapePoolListProps {
activeTargets : Target [ ] ;
}
@ -51,8 +107,21 @@ export const ScrapePoolPanel: FC<PanelProps> = (props: PanelProps) => {
) ;
} ;
type targetHealth = 'healthy' | 'unhealthy' | 'unknown' ;
const healthColorTuples : Array < [ targetHealth , string ] > = [
[ 'healthy' , 'success' ] ,
[ 'unhealthy' , 'danger' ] ,
[ 'unknown' , 'warning' ] ,
] ;
// ScrapePoolListContent is taking care of every possible filter
const ScrapePoolListContent : FC < ScrapePoolListProps > = ( { activeTargets } ) = > {
const ScrapePoolListContent : FC < ScrapePoolListContentProps > = ( {
activeTargets ,
scrapePools ,
selectedPool ,
onPoolSelect ,
} ) = > {
const initialPoolList = groupTargets ( activeTargets ) ;
const [ poolList , setPoolList ] = useState < ScrapePools > ( initialPoolList ) ;
const [ targetList , setTargetList ] = useState ( activeTargets ) ;
@ -63,6 +132,18 @@ const ScrapePoolListContent: FC<ScrapePoolListProps> = ({ activeTargets }) => {
} ;
const [ filter , setFilter ] = useLocalStorage ( 'targets-page-filter' , initialFilter ) ;
const [ healthFilters , setHealthFilters ] = useLocalStorage ( 'target-health-filter' , {
healthy : true ,
unhealthy : true ,
unknown : true ,
} ) ;
const toggleHealthFilter = ( val : targetHealth ) = > ( ) = > {
setHealthFilters ( {
. . . healthFilters ,
[ val ] : ! healthFilters [ val ] ,
} ) ;
} ;
const initialExpanded : Expanded = Object . keys ( initialPoolList ) . reduce (
( acc : { [ scrapePool : string ] : boolean } , scrapePool : string ) = > ( {
. . . acc ,
@ -95,17 +176,37 @@ const ScrapePoolListContent: FC<ScrapePoolListProps> = ({ activeTargets }) => {
return (
< >
< Row xs = "4" className = "align-items-center" >
< Col >
< Row className = "align-items-center" >
< Col className = "flex-grow-0 py-1" >
< ScrapePoolDropDown selectedPool = { selectedPool } scrapePools = { scrapePools } onScrapePoolChange = { onPoolSelect } / >
< / Col >
< Col className = "flex-grow-0 py-1" >
< Filter filter = { filter } setFilter = { setFilter } expanded = { expanded } setExpanded = { setExpanded } / >
< / Col >
< Col xs = "6" >
< Col className = "flex-grow-1 py-1 ">
< SearchBar
defaultValue = { defaultValue }
handleChange = { handleSearchChange }
placeholder = "Filter by endpoint or labels"
/ >
< / Col >
< Col className = "flex-grow-0 py-1" >
< div className = "d-flex flex-row-reverse" >
{ healthColorTuples . map ( ( [ val , color ] ) = > (
< Checkbox
wrapperStyles = { { marginBottom : 0 } }
key = { val }
checked = { healthFilters [ val ] }
id = { ` ${ val } -toggler ` }
onChange = { toggleHealthFilter ( val ) }
>
< Badge color = { color } className = "text-capitalize" >
{ val }
< / Badge >
< / Checkbox >
) ) }
< / div >
< / Col >
< / Row >
{ Object . keys ( poolList )
. filter ( ( scrapePool ) = > {
@ -117,7 +218,10 @@ const ScrapePoolListContent: FC<ScrapePoolListProps> = ({ activeTargets }) => {
< ScrapePoolPanel
key = { scrapePool }
scrapePool = { scrapePool }
targetGroup = { poolList [ scrapePool ] }
targetGroup = { {
upCount : poolList [ scrapePool ] . upCount ,
targets : poolList [ scrapePool ] . targets . filter ( ( target ) = > filterTargetsByHealth ( target . health , healthFilters ) ) ,
} }
expanded = { expanded [ scrapePool ] }
toggleExpanded = { ( ) : void = > setExpanded ( { . . . expanded , [ scrapePool ] : ! expanded [ scrapePool ] } ) }
/ >
@ -128,14 +232,26 @@ const ScrapePoolListContent: FC<ScrapePoolListProps> = ({ activeTargets }) => {
const ScrapePoolListWithStatusIndicator = withStatusIndicator ( ScrapePoolListContent ) ;
export const ScrapePoolList : FC = ( ) = > {
export const ScrapePoolList : FC < ScrapePoolListProps > = ( { selectedPool , scrapePools , . . . props } ) = > {
// If we have more than 20 scrape pools AND there's no pool selected then select first pool
// by default. This is to avoid loading a huge list of targets when we have many pools configured.
// If we have up to 20 scrape pools then pass whatever is the value of selectedPool, it can
// be a pool name or a null (if all pools should be shown).
const poolToShow = selectedPool === null && scrapePools . length > 20 ? scrapePools [ 0 ] : selectedPool ;
const pathPrefix = usePathPrefix ( ) ;
const { response , error , isLoading } = useFetch < ScrapePoolListProps > ( ` ${ pathPrefix } / ${ API_PATH } /targets?state=active ` ) ;
const { response , error , isLoading } = useFetch < ScrapePoolListContentProps > (
` ${ pathPrefix } / ${ API_PATH } /targets?state=active ${ poolToShow === null ? '' : ` &scrapePool= ${ poolToShow } ` } `
) ;
const { status : responseStatus } = response ;
const badResponse = responseStatus !== 'success' && responseStatus !== 'start fetching' ;
return (
< ScrapePoolListWithStatusIndicator
{ . . . props }
{ . . . response . data }
selectedPool = { poolToShow }
scrapePools = { scrapePools }
error = { badResponse ? new Error ( responseStatus ) : error }
isLoading = { isLoading }
componentTitle = "Targets information"