github.com/thanos-io/thanos@v0.32.5/pkg/ui/react-app/src/pages/graph/PanelList.tsx (about) 1 import React, { FC, useState, useEffect } from 'react'; 2 import { RouteComponentProps } from '@reach/router'; 3 import { UncontrolledAlert, Button } from 'reactstrap'; 4 5 import Panel, { PanelOptions, PanelDefaultOptions } from './Panel'; 6 import Checkbox from '../../components/Checkbox'; 7 import PathPrefixProps from '../../types/PathPrefixProps'; 8 import { StoreListProps } from '../../thanos/pages/stores/Stores'; 9 import { Store } from '../../thanos/pages/stores/store'; 10 import { FlagMap } from '../flags/Flags'; 11 import { generateID, decodePanelOptionsFromQueryString, encodePanelOptionsToQueryString, callAll } from '../../utils'; 12 import { useFetch } from '../../hooks/useFetch'; 13 import { useLocalStorage } from '../../hooks/useLocalStorage'; 14 import { withStatusIndicator } from '../../components/withStatusIndicator'; 15 16 export type PanelMeta = { key: string; options: PanelOptions; id: string }; 17 18 export const updateURL = (nextPanels: PanelMeta[]) => { 19 const query = encodePanelOptionsToQueryString(nextPanels); 20 window.history.pushState({}, '', query); 21 }; 22 23 interface PanelListProps extends PathPrefixProps, RouteComponentProps { 24 panels: PanelMeta[]; 25 metrics: string[]; 26 useLocalTime: boolean; 27 queryHistoryEnabled: boolean; 28 stores: StoreListProps; 29 enableAutocomplete: boolean; 30 enableHighlighting: boolean; 31 enableLinter: boolean; 32 defaultStep: string; 33 defaultEngine: string; 34 } 35 36 export const PanelListContent: FC<PanelListProps> = ({ 37 metrics = [], 38 useLocalTime, 39 pathPrefix, 40 queryHistoryEnabled, 41 stores = {}, 42 enableAutocomplete, 43 enableHighlighting, 44 enableLinter, 45 defaultStep, 46 defaultEngine, 47 ...rest 48 }) => { 49 const [panels, setPanels] = useState(rest.panels); 50 const [historyItems, setLocalStorageHistoryItems] = useLocalStorage<string[]>('history', []); 51 const [storeData, setStoreData] = useState([] as Store[]); 52 53 useEffect(() => { 54 // Convert stores data to a unified stores array. 55 const storeList: Store[] = []; 56 for (const type in stores) { 57 storeList.push(...stores[type]); 58 } 59 setStoreData(storeList); 60 !panels.length && addPanel(); 61 window.onpopstate = () => { 62 const panels = decodePanelOptionsFromQueryString(window.location.search); 63 if (panels.length > 0) { 64 setPanels(panels); 65 } 66 }; 67 // We want useEffect to act only as componentDidMount, but react still complains about the empty dependencies list. 68 // eslint-disable-next-line react-hooks/exhaustive-deps 69 }, [stores]); 70 71 const handleExecuteQuery = (query: string) => { 72 const isSimpleMetric = metrics.indexOf(query) !== -1; 73 if (isSimpleMetric || !query.length) { 74 return; 75 } 76 const extendedItems = historyItems.reduce( 77 (acc, metric) => { 78 return metric === query ? acc : [...acc, metric]; // Prevent adding query twice. 79 }, 80 [query] 81 ); 82 setLocalStorageHistoryItems(extendedItems.slice(0, 50)); 83 }; 84 85 const addPanel = () => { 86 callAll( 87 setPanels, 88 updateURL 89 )([ 90 ...panels, 91 { 92 id: generateID(), 93 key: `${panels.length}`, 94 options: PanelDefaultOptions, 95 }, 96 ]); 97 }; 98 99 return ( 100 <> 101 {panels.map(({ id, options }) => ( 102 <Panel 103 onExecuteQuery={handleExecuteQuery} 104 key={id} 105 options={options} 106 id={id} 107 onOptionsChanged={(opts) => 108 callAll(setPanels, updateURL)(panels.map((p) => (id === p.id ? { ...p, options: opts } : p))) 109 } 110 removePanel={() => 111 callAll( 112 setPanels, 113 updateURL 114 )( 115 panels.reduce<PanelMeta[]>( 116 (acc, panel) => (panel.id !== id ? [...acc, { ...panel, key: `${acc.length}` }] : acc), 117 [] 118 ) 119 ) 120 } 121 useLocalTime={useLocalTime} 122 metricNames={metrics} 123 pastQueries={queryHistoryEnabled ? historyItems : []} 124 pathPrefix={pathPrefix} 125 stores={storeData} 126 enableAutocomplete={enableAutocomplete} 127 enableHighlighting={enableHighlighting} 128 defaultEngine={defaultEngine} 129 enableLinter={enableLinter} 130 defaultStep={defaultStep} 131 /> 132 ))} 133 <Button className="d-block mb-3" color="primary" onClick={addPanel}> 134 Add Panel 135 </Button> 136 </> 137 ); 138 }; 139 140 const PanelListContentWithIndicator = withStatusIndicator(PanelListContent); 141 142 const PanelList: FC<RouteComponentProps & PathPrefixProps> = ({ pathPrefix = '' }) => { 143 const [delta, setDelta] = useState(0); 144 const [useLocalTime, setUseLocalTime] = useLocalStorage('use-local-time', false); 145 const [enableQueryHistory, setEnableQueryHistory] = useLocalStorage('enable-query-history', false); 146 const [enableStoreFiltering, setEnableStoreFiltering] = useLocalStorage('enable-store-filtering', false); 147 const [enableAutocomplete, setEnableAutocomplete] = useLocalStorage('enable-autocomplete', true); 148 const [enableHighlighting, setEnableHighlighting] = useLocalStorage('enable-syntax-highlighting', true); 149 const [enableLinter, setEnableLinter] = useLocalStorage('enable-linter', true); 150 151 const { response: metricsRes, error: metricsErr } = useFetch<string[]>(`${pathPrefix}/api/v1/label/__name__/values`); 152 const { 153 response: storesRes, 154 error: storesErr, 155 isLoading: storesLoading, 156 } = useFetch<StoreListProps>(`${pathPrefix}/api/v1/stores`); 157 const { 158 response: flagsRes, 159 error: flagsErr, 160 isLoading: flagsLoading, 161 } = useFetch<FlagMap>(`${pathPrefix}/api/v1/status/flags`); 162 const defaultStep = flagsRes?.data?.['query.default-step'] || '1s'; 163 const defaultEngine = flagsRes?.data?.['query.promql-engine']; 164 165 const browserTime = new Date().getTime() / 1000; 166 const { response: timeRes, error: timeErr } = useFetch<{ result: number[] }>(`${pathPrefix}/api/v1/query?query=time()`); 167 168 useEffect(() => { 169 if (timeRes.data) { 170 const serverTime = timeRes.data.result[0]; 171 setDelta(Math.abs(browserTime - serverTime)); 172 } 173 /** 174 * React wants to include browserTime to useEffect dependencies list which will cause a delta change on every re-render 175 * Basically it's not recommended to disable this rule, but this is the only way to take control over the useEffect 176 * dependencies and to not include the browserTime variable. 177 **/ 178 // eslint-disable-next-line react-hooks/exhaustive-deps 179 }, [timeRes.data]); 180 181 return ( 182 <> 183 <div className="clearfix"> 184 <div className="float-left"> 185 <Checkbox 186 wrapperStyles={{ display: 'inline-block' }} 187 id="use-local-time-checkbox" 188 onChange={({ target }) => setUseLocalTime(target.checked)} 189 defaultChecked={useLocalTime} 190 > 191 Use local time 192 </Checkbox> 193 <Checkbox 194 wrapperStyles={{ marginLeft: 20, display: 'inline-block' }} 195 id="query-history-checkbox" 196 onChange={({ target }) => setEnableQueryHistory(target.checked)} 197 defaultChecked={enableQueryHistory} 198 > 199 Enable query history 200 </Checkbox> 201 <Checkbox 202 wrapperStyles={{ marginLeft: 20, display: 'inline-block' }} 203 id="store-filtering-checkbox" 204 defaultChecked={enableStoreFiltering} 205 onChange={({ target }) => setEnableStoreFiltering(target.checked)} 206 > 207 Enable Store Filtering 208 </Checkbox> 209 </div> 210 <div className="float-right"> 211 <Checkbox 212 wrapperStyles={{ marginLeft: 20, display: 'inline-block' }} 213 id="autocomplete-checkbox" 214 onChange={({ target }) => setEnableAutocomplete(target.checked)} 215 defaultChecked={enableAutocomplete} 216 > 217 Enable autocomplete 218 </Checkbox> 219 <Checkbox 220 wrapperStyles={{ marginLeft: 20, display: 'inline-block' }} 221 id="highlighting-checkbox" 222 onChange={({ target }) => setEnableHighlighting(target.checked)} 223 defaultChecked={enableHighlighting} 224 > 225 Enable highlighting 226 </Checkbox> 227 <Checkbox 228 wrapperStyles={{ marginLeft: 20, display: 'inline-block' }} 229 id="linter-checkbox" 230 onChange={({ target }) => setEnableLinter(target.checked)} 231 defaultChecked={enableLinter} 232 > 233 Enable linter 234 </Checkbox> 235 </div> 236 </div> 237 {(delta > 30 || timeErr) && ( 238 <UncontrolledAlert color="danger"> 239 <strong>Warning: </strong> 240 {timeErr && `Unexpected response status when fetching server time: ${timeErr.message}`} 241 {delta >= 30 && 242 `Error fetching server time: Detected ${delta} seconds time difference between your browser and the server. Thanos relies on accurate time and time drift might cause unexpected query results.`} 243 </UncontrolledAlert> 244 )} 245 {metricsErr && ( 246 <UncontrolledAlert color="danger"> 247 <strong>Warning: </strong> 248 Error fetching metrics list: Unexpected response status when fetching metric names: {metricsErr.message} 249 </UncontrolledAlert> 250 )} 251 {storesErr && ( 252 <UncontrolledAlert color="danger"> 253 <strong>Warning: </strong> 254 Error fetching stores list: Unexpected response status when fetching stores: {storesErr.message} 255 </UncontrolledAlert> 256 )} 257 {flagsErr && ( 258 <UncontrolledAlert color="danger"> 259 <strong>Warning: </strong> 260 Error fetching flags list: Unexpected response status when fetching flags: {flagsErr.message} 261 </UncontrolledAlert> 262 )} 263 <PanelListContentWithIndicator 264 panels={decodePanelOptionsFromQueryString(window.location.search)} 265 pathPrefix={pathPrefix} 266 useLocalTime={useLocalTime} 267 metrics={metricsRes.data} 268 stores={enableStoreFiltering ? storesRes.data : {}} 269 enableAutocomplete={enableAutocomplete} 270 enableHighlighting={enableHighlighting} 271 enableLinter={enableLinter} 272 defaultStep={defaultStep} 273 defaultEngine={defaultEngine} 274 queryHistoryEnabled={enableQueryHistory} 275 isLoading={storesLoading || flagsLoading} 276 /> 277 </> 278 ); 279 }; 280 281 export default PanelList;