github.com/thanos-io/thanos@v0.32.5/pkg/ui/react-app/src/pages/alerts/AlertContents.tsx (about) 1 import React, { FC, useState, Fragment, ChangeEvent, useEffect } from 'react'; 2 import { Badge, Col, Row } from 'reactstrap'; 3 import CollapsibleAlertPanel from './CollapsibleAlertPanel'; 4 import Checkbox from '../../components/Checkbox'; 5 import { isPresent } from '../../utils'; 6 import { Rule } from '../../types/types'; 7 import { KVSearch } from '@nexucis/kvsearch'; 8 import CustomInfiniteScroll, { InfiniteScrollItemsProps } from '../../components/CustomInfiniteScroll'; 9 import SearchBar from '../../components/SearchBar'; 10 11 export type RuleState = keyof RuleStatus<any>; 12 13 export interface RuleStatus<T> { 14 firing: T; 15 pending: T; 16 inactive: T; 17 } 18 19 export interface AlertsProps { 20 groups?: RuleGroup[]; 21 statsCount: RuleStatus<number>; 22 } 23 24 export interface Alert { 25 labels: Record<string, string>; 26 state: RuleState; 27 value: string; 28 annotations: Record<string, string>; 29 activeAt: string; 30 } 31 32 interface RuleGroup { 33 name: string; 34 file: string; 35 rules: Rule[]; 36 interval: number; 37 } 38 39 const kvSearchRule = new KVSearch<Rule>({ 40 shouldSort: true, 41 indexedKeys: ['name', 'labels', ['labels', /.*/]], 42 }); 43 44 const stateColorTuples: Array<[RuleState, 'success' | 'warning' | 'danger']> = [ 45 ['inactive', 'success'], 46 ['pending', 'warning'], 47 ['firing', 'danger'], 48 ]; 49 50 function GroupContent(showAnnotations: boolean) { 51 const Content: FC<InfiniteScrollItemsProps<Rule>> = ({ items }) => { 52 return ( 53 <> 54 {items.map((rule, j) => ( 55 <CollapsibleAlertPanel key={rule.name + j} showAnnotations={showAnnotations} rule={rule} /> 56 ))} 57 </> 58 ); 59 }; 60 return Content; 61 } 62 63 const AlertsContent: FC<AlertsProps> = ({ groups = [], statsCount }) => { 64 const [groupList, setGroupList] = useState(groups); 65 const [filteredList, setFilteredList] = useState(groups); 66 const [filter, setFilter] = useState<RuleStatus<boolean>>({ 67 firing: true, 68 pending: true, 69 inactive: true, 70 }); 71 const [showAnnotations, setShowAnnotations] = useState(false); 72 73 const toggleFilter = (ruleState: RuleState) => () => { 74 setFilter({ 75 ...filter, 76 [ruleState]: !filter[ruleState], 77 }); 78 }; 79 80 const handleSearchChange = (e: ChangeEvent<HTMLTextAreaElement | HTMLInputElement>) => { 81 if (e.target.value !== '') { 82 const pattern = e.target.value.trim(); 83 const result: RuleGroup[] = []; 84 for (const group of groups) { 85 const ruleFilterList = kvSearchRule.filter(pattern, group.rules); 86 if (ruleFilterList.length > 0) { 87 result.push({ 88 file: group.file, 89 name: group.name, 90 interval: group.interval, 91 rules: ruleFilterList.map((value) => value.original), 92 }); 93 } 94 } 95 setGroupList(result); 96 } else { 97 setGroupList(groups); 98 } 99 }; 100 101 useEffect(() => { 102 const result: RuleGroup[] = []; 103 for (const group of groupList) { 104 const newGroup = { 105 file: group.file, 106 name: group.name, 107 interval: group.interval, 108 rules: group.rules.filter((value) => filter[value.state]), 109 }; 110 if (newGroup.rules.length > 0) { 111 result.push(newGroup); 112 } 113 } 114 setFilteredList(result); 115 }, [groupList, filter]); 116 117 return ( 118 <> 119 <Row className="align-items-center"> 120 <Col className="d-flex" lg="4" md="5"> 121 {stateColorTuples.map(([state, color]) => { 122 return ( 123 <Checkbox key={state} checked={filter[state]} id={`${state}-toggler`} onChange={toggleFilter(state)}> 124 <Badge color={color} className="text-capitalize"> 125 {state} ({statsCount[state]}) 126 </Badge> 127 </Checkbox> 128 ); 129 })} 130 </Col> 131 <Col lg="5" md="4"> 132 <SearchBar handleChange={handleSearchChange} placeholder="Filter by name or labels" /> 133 </Col> 134 <Col className="d-flex flex-row-reverse" md="3"> 135 <Checkbox 136 checked={showAnnotations} 137 id="show-annotations-toggler" 138 onChange={({ target }) => setShowAnnotations(target.checked)} 139 > 140 <span style={{ fontSize: '0.9rem', lineHeight: 1.9, display: 'inline-block', whiteSpace: 'nowrap' }}> 141 Show annotations 142 </span> 143 </Checkbox> 144 </Col> 145 </Row> 146 {filteredList.map((group, i) => ( 147 <Fragment key={i}> 148 <GroupInfo rules={group.rules}> 149 {group.file} > {group.name} 150 </GroupInfo> 151 <CustomInfiniteScroll allItems={group.rules} child={GroupContent(showAnnotations)} /> 152 </Fragment> 153 ))} 154 </> 155 ); 156 }; 157 158 interface GroupInfoProps { 159 rules: Rule[]; 160 } 161 162 export const GroupInfo: FC<GroupInfoProps> = ({ rules, children }) => { 163 const statesCounter = rules.reduce<any>( 164 (acc, r) => { 165 return { 166 ...acc, 167 [r.state]: acc[r.state] + r.alerts.length, 168 }; 169 }, 170 { 171 firing: 0, 172 pending: 0, 173 } 174 ); 175 176 return ( 177 <div className="group-info border rounded-sm" style={{ lineHeight: 1.1 }}> 178 {children} 179 <div className="badges-wrapper"> 180 {isPresent(statesCounter.inactive) && <Badge color="success">inactive</Badge>} 181 {statesCounter.pending > 0 && <Badge color="warning">pending ({statesCounter.pending})</Badge>} 182 {statesCounter.firing > 0 && <Badge color="danger">firing ({statesCounter.firing})</Badge>} 183 </div> 184 </div> 185 ); 186 }; 187 188 AlertsContent.displayName = 'Alerts'; 189 190 export default AlertsContent;