github.com/thanos-io/thanos@v0.32.5/pkg/ui/react-app/src/thanos/pages/blocks/Blocks.tsx (about) 1 import React, { ChangeEvent, FC, useMemo, useState } from 'react'; 2 import { RouteComponentProps } from '@reach/router'; 3 import { UncontrolledAlert } from 'reactstrap'; 4 import { useQueryParams, withDefault, NumberParam, StringParam, BooleanParam } from 'use-query-params'; 5 import { withStatusIndicator } from '../../../components/withStatusIndicator'; 6 import { useFetch } from '../../../hooks/useFetch'; 7 import PathPrefixProps from '../../../types/PathPrefixProps'; 8 import { Block } from './block'; 9 import { SourceView } from './SourceView'; 10 import { BlockDetails } from './BlockDetails'; 11 import { BlockSearchInput } from './BlockSearchInput'; 12 import { BlockFilterCompaction } from './BlockFilterCompaction'; 13 import { sortBlocks, getBlockByUlid, getFilteredBlockPools } from './helpers'; 14 import styles from './blocks.module.css'; 15 import TimeRange from './TimeRange'; 16 import Checkbox from '../../../components/Checkbox'; 17 18 export interface BlockListProps { 19 blocks: Block[]; 20 err: string | null; 21 label: string; 22 refreshedAt: string; 23 } 24 25 export const BlocksContent: FC<{ data: BlockListProps }> = ({ data }) => { 26 const [selectedBlock, selectBlock] = useState<Block>(); 27 const [searchState, setSearchState] = useState<string>(''); 28 29 const { blocks, label, err } = data; 30 31 const [gridMinTime, gridMaxTime] = useMemo(() => { 32 if (!err && blocks.length > 0) { 33 let gridMinTime = blocks[0].minTime; 34 let gridMaxTime = blocks[0].maxTime; 35 blocks.forEach((block) => { 36 if (block.minTime < gridMinTime) { 37 gridMinTime = block.minTime; 38 } 39 if (block.maxTime > gridMaxTime) { 40 gridMaxTime = block.maxTime; 41 } 42 }); 43 return [gridMinTime, gridMaxTime]; 44 } 45 return [0, 0]; 46 }, [blocks, err]); 47 48 const [ 49 { 50 'min-time': viewMinTime, 51 'max-time': viewMaxTime, 52 ulid: blockSearchParam, 53 'find-overlapping': findOverlappingParam, 54 'filter-compaction': filterCompactionParam, 55 'compaction-level': compactionLevelParam, 56 }, 57 setQuery, 58 ] = useQueryParams({ 59 'min-time': withDefault(NumberParam, gridMinTime), 60 'max-time': withDefault(NumberParam, gridMaxTime), 61 ulid: withDefault(StringParam, ''), 62 'find-overlapping': withDefault(BooleanParam, false), 63 'filter-compaction': withDefault(BooleanParam, false), 64 'compaction-level': withDefault(NumberParam, 0), 65 }); 66 67 const [filterCompaction, setFilterCompaction] = useState<boolean>(filterCompactionParam); 68 const [findOverlappingBlocks, setFindOverlappingBlocks] = useState<boolean>(findOverlappingParam); 69 const [compactionLevel, setCompactionLevel] = useState<number>(compactionLevelParam); 70 const [compactionLevelInput, setCompactionLevelInput] = useState<string>(compactionLevelParam.toString()); 71 const [blockSearch, setBlockSearch] = useState<string>(blockSearchParam); 72 73 const blockPools = useMemo(() => sortBlocks(blocks, label, findOverlappingBlocks), [blocks, label, findOverlappingBlocks]); 74 const filteredBlocks = useMemo(() => getBlockByUlid(blocks, blockSearch), [blocks, blockSearch]); 75 const filteredBlockPools = useMemo(() => getFilteredBlockPools(blockPools, filteredBlocks), [filteredBlocks, blockPools]); 76 77 const setViewTime = (times: number[]): void => { 78 setQuery({ 79 'min-time': times[0], 80 'max-time': times[1], 81 }); 82 }; 83 84 const setBlockSearchInput = (searchState: string): void => { 85 setQuery({ 86 ulid: searchState, 87 }); 88 setBlockSearch(searchState); 89 }; 90 91 const onChangeCompactionCheckbox = (target: EventTarget & HTMLInputElement) => { 92 setFilterCompaction(target.checked); 93 if (target.checked) { 94 const compactionLevel: number = parseInt(compactionLevelInput); 95 setQuery({ 96 'filter-compaction': target.checked, 97 'compaction-level': compactionLevel, 98 }); 99 setCompactionLevel(compactionLevel); 100 } else { 101 setQuery({ 102 'filter-compaction': target.checked, 103 'compaction-level': 0, 104 }); 105 setCompactionLevel(0); 106 } 107 }; 108 109 const onChangeCompactionInput = (target: HTMLInputElement) => { 110 if (filterCompaction) { 111 setQuery({ 112 'compaction-level': parseInt(target.value), 113 }); 114 setCompactionLevel(parseInt(target.value)); 115 } 116 setCompactionLevelInput(target.value); 117 }; 118 119 if (err) return <UncontrolledAlert color="danger">{err.toString()}</UncontrolledAlert>; 120 121 return ( 122 <> 123 {blocks.length > 0 ? ( 124 <> 125 <BlockSearchInput 126 onChange={({ target }: ChangeEvent<HTMLInputElement>): void => setSearchState(target.value)} 127 onClick={() => setBlockSearchInput(searchState)} 128 defaultValue={blockSearchParam} 129 /> 130 <div className={styles.blockFilter}> 131 <Checkbox 132 id="find-overlap-block-checkbox" 133 onChange={({ target }) => { 134 setQuery({ 135 'find-overlapping': target.checked, 136 }); 137 setFindOverlappingBlocks(target.checked); 138 }} 139 defaultChecked={findOverlappingBlocks} 140 > 141 Enable finding overlapping blocks 142 </Checkbox> 143 <BlockFilterCompaction 144 id="filter-compaction-checkbox" 145 defaultChecked={filterCompaction} 146 onChangeCheckbox={({ target }) => onChangeCompactionCheckbox(target)} 147 onChangeInput={({ target }: ChangeEvent<HTMLInputElement>): void => { 148 onChangeCompactionInput(target); 149 }} 150 defaultValue={compactionLevelInput} 151 /> 152 </div> 153 <div className={styles.container}> 154 <div className={styles.grid}> 155 <div className={styles.sources}> 156 {Object.keys(filteredBlockPools).length > 0 ? ( 157 Object.keys(filteredBlockPools).map((pk) => ( 158 <SourceView 159 key={pk} 160 data={filteredBlockPools[pk]} 161 title={pk} 162 selectBlock={selectBlock} 163 gridMinTime={viewMinTime} 164 gridMaxTime={viewMaxTime} 165 blockSearch={blockSearch} 166 compactionLevel={compactionLevel} 167 /> 168 )) 169 ) : ( 170 <div> 171 <h3>No Blocks Found!</h3> 172 </div> 173 )} 174 </div> 175 <TimeRange 176 gridMinTime={gridMinTime} 177 gridMaxTime={gridMaxTime} 178 viewMinTime={viewMinTime} 179 viewMaxTime={viewMaxTime} 180 onChange={setViewTime} 181 /> 182 </div> 183 <BlockDetails selectBlock={selectBlock} block={selectedBlock} /> 184 </div> 185 </> 186 ) : ( 187 <UncontrolledAlert color="warning">No blocks found.</UncontrolledAlert> 188 )} 189 </> 190 ); 191 }; 192 193 const BlocksWithStatusIndicator = withStatusIndicator(BlocksContent); 194 195 interface BlocksProps { 196 view?: string; 197 } 198 199 export const Blocks: FC<RouteComponentProps & PathPrefixProps & BlocksProps> = ({ pathPrefix = '', view = 'global' }) => { 200 const { response, error, isLoading } = useFetch<BlockListProps>( 201 `${pathPrefix}/api/v1/blocks${view ? '?view=' + view : ''}` 202 ); 203 const { status: responseStatus } = response; 204 const badResponse = responseStatus !== 'success' && responseStatus !== 'start fetching'; 205 206 return ( 207 <BlocksWithStatusIndicator 208 data={response.data} 209 error={badResponse ? new Error(responseStatus) : error} 210 isLoading={isLoading} 211 /> 212 ); 213 }; 214 215 export default Blocks;