1import React, { Component } from 'react'; 2 3import { Alert, Button, Col, Nav, NavItem, NavLink, Row, TabContent, TabPane } from 'reactstrap'; 4 5import moment from 'moment-timezone'; 6 7import ExpressionInput from './ExpressionInput'; 8import CMExpressionInput from './CMExpressionInput'; 9import GraphControls from './GraphControls'; 10import { GraphTabContent } from './GraphTabContent'; 11import DataTable from './DataTable'; 12import TimeInput from './TimeInput'; 13import QueryStatsView, { QueryStats } from './QueryStatsView'; 14import { QueryParams } from '../../types/types'; 15import { API_PATH } from '../../constants/constants'; 16 17interface PanelProps { 18 options: PanelOptions; 19 onOptionsChanged: (opts: PanelOptions) => void; 20 useLocalTime: boolean; 21 pastQueries: string[]; 22 metricNames: string[]; 23 removePanel: () => void; 24 onExecuteQuery: (query: string) => void; 25 pathPrefix: string; 26 useExperimentalEditor: boolean; 27 enableAutocomplete: boolean; 28 enableHighlighting: boolean; 29 enableLinter: boolean; 30} 31 32interface PanelState { 33 data: any; // TODO: Type data. 34 lastQueryParams: QueryParams | null; 35 loading: boolean; 36 warnings: string[] | null; 37 error: string | null; 38 stats: QueryStats | null; 39 exprInputValue: string; 40} 41 42export interface PanelOptions { 43 expr: string; 44 type: PanelType; 45 range: number; // Range in milliseconds. 46 endTime: number | null; // Timestamp in milliseconds. 47 resolution: number | null; // Resolution in seconds. 48 stacked: boolean; 49} 50 51export enum PanelType { 52 Graph = 'graph', 53 Table = 'table', 54} 55 56export const PanelDefaultOptions: PanelOptions = { 57 type: PanelType.Table, 58 expr: '', 59 range: 60 * 60 * 1000, 60 endTime: null, 61 resolution: null, 62 stacked: false, 63}; 64 65class Panel extends Component<PanelProps, PanelState> { 66 private abortInFlightFetch: (() => void) | null = null; 67 68 constructor(props: PanelProps) { 69 super(props); 70 71 this.state = { 72 data: null, 73 lastQueryParams: null, 74 loading: false, 75 warnings: null, 76 error: null, 77 stats: null, 78 exprInputValue: props.options.expr, 79 }; 80 } 81 82 componentDidUpdate({ options: prevOpts }: PanelProps) { 83 const { endTime, range, resolution, type } = this.props.options; 84 if ( 85 prevOpts.endTime !== endTime || 86 prevOpts.range !== range || 87 prevOpts.resolution !== resolution || 88 prevOpts.type !== type 89 ) { 90 this.executeQuery(); 91 } 92 } 93 94 componentDidMount() { 95 this.executeQuery(); 96 } 97 98 executeQuery = (): void => { 99 const { exprInputValue: expr } = this.state; 100 const queryStart = Date.now(); 101 this.props.onExecuteQuery(expr); 102 if (this.props.options.expr !== expr) { 103 this.setOptions({ expr }); 104 } 105 if (expr === '') { 106 return; 107 } 108 109 if (this.abortInFlightFetch) { 110 this.abortInFlightFetch(); 111 this.abortInFlightFetch = null; 112 } 113 114 const abortController = new AbortController(); 115 this.abortInFlightFetch = () => abortController.abort(); 116 this.setState({ loading: true }); 117 118 const endTime = this.getEndTime().valueOf() / 1000; // TODO: shouldn't valueof only work when it's a moment? 119 const startTime = endTime - this.props.options.range / 1000; 120 const resolution = this.props.options.resolution || Math.max(Math.floor(this.props.options.range / 250000), 1); 121 const params: URLSearchParams = new URLSearchParams({ 122 query: expr, 123 }); 124 125 let path: string; 126 switch (this.props.options.type) { 127 case 'graph': 128 path = 'query_range'; 129 params.append('start', startTime.toString()); 130 params.append('end', endTime.toString()); 131 params.append('step', resolution.toString()); 132 break; 133 case 'table': 134 path = 'query'; 135 params.append('time', endTime.toString()); 136 break; 137 default: 138 throw new Error('Invalid panel type "' + this.props.options.type + '"'); 139 } 140 141 fetch(`${this.props.pathPrefix}/${API_PATH}/${path}?${params}`, { 142 cache: 'no-store', 143 credentials: 'same-origin', 144 signal: abortController.signal, 145 }) 146 .then(resp => resp.json()) 147 .then(json => { 148 if (json.status !== 'success') { 149 throw new Error(json.error || 'invalid response JSON'); 150 } 151 152 let resultSeries = 0; 153 if (json.data) { 154 const { resultType, result } = json.data; 155 if (resultType === 'scalar') { 156 resultSeries = 1; 157 } else if (result && result.length > 0) { 158 resultSeries = result.length; 159 } 160 } 161 162 this.setState({ 163 error: null, 164 data: json.data, 165 warnings: json.warnings, 166 lastQueryParams: { 167 startTime, 168 endTime, 169 resolution, 170 }, 171 stats: { 172 loadTime: Date.now() - queryStart, 173 resolution, 174 resultSeries, 175 }, 176 loading: false, 177 }); 178 this.abortInFlightFetch = null; 179 }) 180 .catch(error => { 181 if (error.name === 'AbortError') { 182 // Aborts are expected, don't show an error for them. 183 return; 184 } 185 this.setState({ 186 error: 'Error executing query: ' + error.message, 187 loading: false, 188 }); 189 }); 190 }; 191 192 setOptions(opts: object): void { 193 const newOpts = { ...this.props.options, ...opts }; 194 this.props.onOptionsChanged(newOpts); 195 } 196 197 handleExpressionChange = (expr: string): void => { 198 this.setState({ exprInputValue: expr }); 199 }; 200 201 handleChangeRange = (range: number): void => { 202 this.setOptions({ range: range }); 203 }; 204 205 getEndTime = (): number | moment.Moment => { 206 if (this.props.options.endTime === null) { 207 return moment(); 208 } 209 return this.props.options.endTime; 210 }; 211 212 handleChangeEndTime = (endTime: number | null) => { 213 this.setOptions({ endTime: endTime }); 214 }; 215 216 handleChangeResolution = (resolution: number | null) => { 217 this.setOptions({ resolution: resolution }); 218 }; 219 220 handleChangeType = (type: PanelType) => { 221 if (this.props.options.type === type) { 222 return; 223 } 224 225 this.setState({ data: null }); 226 this.setOptions({ type: type }); 227 }; 228 229 handleChangeStacking = (stacked: boolean) => { 230 this.setOptions({ stacked: stacked }); 231 }; 232 233 render() { 234 const { pastQueries, metricNames, options } = this.props; 235 return ( 236 <div className="panel"> 237 <Row> 238 <Col> 239 {this.props.useExperimentalEditor ? ( 240 <CMExpressionInput 241 value={this.state.exprInputValue} 242 onExpressionChange={this.handleExpressionChange} 243 executeQuery={this.executeQuery} 244 loading={this.state.loading} 245 enableAutocomplete={this.props.enableAutocomplete} 246 enableHighlighting={this.props.enableHighlighting} 247 enableLinter={this.props.enableLinter} 248 queryHistory={pastQueries} 249 metricNames={metricNames} 250 /> 251 ) : ( 252 <ExpressionInput 253 value={this.state.exprInputValue} 254 onExpressionChange={this.handleExpressionChange} 255 executeQuery={this.executeQuery} 256 loading={this.state.loading} 257 enableAutocomplete={this.props.enableAutocomplete} 258 queryHistory={pastQueries} 259 metricNames={metricNames} 260 /> 261 )} 262 </Col> 263 </Row> 264 <Row> 265 <Col>{this.state.error && <Alert color="danger">{this.state.error}</Alert>}</Col> 266 </Row> 267 {this.state.warnings?.map((warning, index) => ( 268 <Row key={index}> 269 <Col>{warning && <Alert color="warning">{warning}</Alert>}</Col> 270 </Row> 271 ))} 272 <Row> 273 <Col> 274 <Nav tabs> 275 <NavItem> 276 <NavLink 277 className={options.type === 'table' ? 'active' : ''} 278 onClick={() => this.handleChangeType(PanelType.Table)} 279 > 280 Table 281 </NavLink> 282 </NavItem> 283 <NavItem> 284 <NavLink 285 className={options.type === 'graph' ? 'active' : ''} 286 onClick={() => this.handleChangeType(PanelType.Graph)} 287 > 288 Graph 289 </NavLink> 290 </NavItem> 291 {!this.state.loading && !this.state.error && this.state.stats && <QueryStatsView {...this.state.stats} />} 292 </Nav> 293 <TabContent activeTab={options.type}> 294 <TabPane tabId="table"> 295 {options.type === 'table' && ( 296 <> 297 <div className="table-controls"> 298 <TimeInput 299 time={options.endTime} 300 useLocalTime={this.props.useLocalTime} 301 range={options.range} 302 placeholder="Evaluation time" 303 onChangeTime={this.handleChangeEndTime} 304 /> 305 </div> 306 <DataTable data={this.state.data} /> 307 </> 308 )} 309 </TabPane> 310 <TabPane tabId="graph"> 311 {this.props.options.type === 'graph' && ( 312 <> 313 <GraphControls 314 range={options.range} 315 endTime={options.endTime} 316 useLocalTime={this.props.useLocalTime} 317 resolution={options.resolution} 318 stacked={options.stacked} 319 onChangeRange={this.handleChangeRange} 320 onChangeEndTime={this.handleChangeEndTime} 321 onChangeResolution={this.handleChangeResolution} 322 onChangeStacking={this.handleChangeStacking} 323 /> 324 <GraphTabContent 325 data={this.state.data} 326 stacked={options.stacked} 327 useLocalTime={this.props.useLocalTime} 328 lastQueryParams={this.state.lastQueryParams} 329 /> 330 </> 331 )} 332 </TabPane> 333 </TabContent> 334 </Col> 335 </Row> 336 <Row> 337 <Col> 338 <Button className="float-right" color="link" onClick={this.props.removePanel} size="sm"> 339 Remove Panel 340 </Button> 341 </Col> 342 </Row> 343 </div> 344 ); 345 } 346} 347 348export default Panel; 349