1
import React, { Component } from 'react';
3
import { Alert, Button, Col, Nav, NavItem, NavLink, Row, TabContent, TabPane } from 'reactstrap';
5
import moment from 'moment-timezone';
7
import ExpressionInput from './ExpressionInput';
8
import GraphControls from './GraphControls';
9
import { GraphTabContent } from './GraphTabContent';
10
import DataTable from './DataTable';
11
import TimeInput from './TimeInput';
12
import QueryStatsView, { QueryStats } from './QueryStatsView';
13
import { QueryParams, ExemplarData } from '../../types/types';
14
import { API_PATH } from '../../constants/constants';
15
import { debounce } from '../../utils';
16
import { isHeatmapData } from './GraphHeatmapHelpers';
19
options: PanelOptions;
20
onOptionsChanged: (opts: PanelOptions) => void;
21
useLocalTime: boolean;
22
pastQueries: string[];
23
metricNames: string[];
24
removePanel: () => void;
25
onExecuteQuery: (query: string) => void;
27
enableAutocomplete: boolean;
28
enableHighlighting: boolean;
29
enableLinter: boolean;
34
// eslint-disable-next-line @typescript-eslint/no-explicit-any
35
data: any; // TODO: Type data.
36
exemplars: ExemplarData;
37
lastQueryParams: QueryParams | null;
39
warnings: string[] | null;
41
stats: QueryStats | null;
42
exprInputValue: string;
43
isHeatmapData: boolean;
46
export interface PanelOptions {
49
range: number; // Range in milliseconds.
50
endTime: number | null; // Timestamp in milliseconds.
51
resolution: number | null; // Resolution in seconds.
52
displayMode: GraphDisplayMode;
53
showExemplars: boolean;
56
export enum PanelType {
61
export enum GraphDisplayMode {
67
export const PanelDefaultOptions: PanelOptions = {
68
type: PanelType.Table,
70
range: 60 * 60 * 1000,
73
displayMode: GraphDisplayMode.Lines,
77
class Panel extends Component<PanelProps, PanelState> {
78
private abortInFlightFetch: (() => void) | null = null;
79
private debounceExecuteQuery: () => void;
81
constructor(props: PanelProps) {
87
lastQueryParams: null,
92
exprInputValue: props.options.expr,
96
this.debounceExecuteQuery = debounce(this.executeQuery.bind(this), 250);
99
componentDidUpdate({ options: prevOpts }: PanelProps): void {
100
const { endTime, range, resolution, showExemplars, type } = this.props.options;
102
if (prevOpts.endTime !== endTime || prevOpts.range !== range) {
103
this.debounceExecuteQuery();
107
if (prevOpts.resolution !== resolution || prevOpts.type !== type || showExemplars !== prevOpts.showExemplars) {
112
componentDidMount(): void {
116
// eslint-disable-next-line @typescript-eslint/no-explicit-any
117
executeQuery = async (): Promise<any> => {
118
const { exprInputValue: expr } = this.state;
119
const queryStart = Date.now();
120
this.props.onExecuteQuery(expr);
121
if (this.props.options.expr !== expr) {
122
this.setOptions({ expr });
128
if (this.abortInFlightFetch) {
129
this.abortInFlightFetch();
130
this.abortInFlightFetch = null;
133
const abortController = new AbortController();
134
this.abortInFlightFetch = () => abortController.abort();
135
this.setState({ loading: true });
137
const endTime = this.getEndTime().valueOf() / 1000; // TODO: shouldn't valueof only work when it's a moment?
138
const startTime = endTime - this.props.options.range / 1000;
139
const resolution = this.props.options.resolution || Math.max(Math.floor(this.props.options.range / 250000), 1);
140
const params: URLSearchParams = new URLSearchParams({
145
switch (this.props.options.type) {
147
path = 'query_range';
148
params.append('start', startTime.toString());
149
params.append('end', endTime.toString());
150
params.append('step', resolution.toString());
154
params.append('time', endTime.toString());
157
throw new Error('Invalid panel type "' + this.props.options.type + '"');
163
query = await fetch(`${this.props.pathPrefix}/${API_PATH}/${path}?${params}`, {
165
credentials: 'same-origin',
166
signal: abortController.signal,
167
}).then((resp) => resp.json());
169
if (query.status !== 'success') {
170
throw new Error(query.error || 'invalid response JSON');
173
if (this.props.options.type === 'graph' && this.props.options.showExemplars) {
174
params.delete('step'); // Not needed for this request.
175
exemplars = await fetch(`${this.props.pathPrefix}/${API_PATH}/query_exemplars?${params}`, {
177
credentials: 'same-origin',
178
signal: abortController.signal,
179
}).then((resp) => resp.json());
181
if (exemplars.status !== 'success') {
182
throw new Error(exemplars.error || 'invalid response JSON');
186
let resultSeries = 0;
188
const { resultType, result } = query.data;
189
if (resultType === 'scalar') {
191
} else if (result && result.length > 0) {
192
resultSeries = result.length;
196
const isHeatmap = isHeatmapData(query.data);
197
const isHeatmapDisplayMode = this.props.options.displayMode === GraphDisplayMode.Heatmap;
198
if (!isHeatmap && isHeatmapDisplayMode) {
199
this.setOptions({ displayMode: GraphDisplayMode.Lines });
205
exemplars: exemplars?.data,
206
warnings: query.warnings,
213
loadTime: Date.now() - queryStart,
218
isHeatmapData: isHeatmap,
220
this.abortInFlightFetch = null;
221
} catch (err: unknown) {
222
const error = err as Error;
223
if (error.name === 'AbortError') {
224
// Aborts are expected, don't show an error for them.
228
error: 'Error executing query: ' + error.message,
234
setOptions(opts: Partial<PanelOptions>): void {
235
const newOpts = { ...this.props.options, ...opts };
236
this.props.onOptionsChanged(newOpts);
239
handleExpressionChange = (expr: string): void => {
240
this.setState({ exprInputValue: expr });
243
handleChangeRange = (range: number): void => {
244
this.setOptions({ range: range });
247
getEndTime = (): number | moment.Moment => {
248
if (this.props.options.endTime === null) {
251
return this.props.options.endTime;
254
handleChangeEndTime = (endTime: number | null): void => {
255
this.setOptions({ endTime: endTime });
258
handleChangeResolution = (resolution: number | null): void => {
259
this.setOptions({ resolution: resolution });
262
handleChangeType = (type: PanelType): void => {
263
if (this.props.options.type === type) {
267
this.setState({ data: null });
268
this.setOptions({ type: type });
271
handleChangeDisplayMode = (mode: GraphDisplayMode): void => {
272
this.setOptions({ displayMode: mode });
275
handleChangeShowExemplars = (show: boolean): void => {
276
this.setOptions({ showExemplars: show });
279
handleTimeRangeSelection = (startTime: number, endTime: number): void => {
280
this.setOptions({ range: endTime - startTime, endTime: endTime });
283
render(): JSX.Element {
284
const { pastQueries, metricNames, options } = this.props;
286
<div className="panel">
290
value={this.state.exprInputValue}
291
onExpressionChange={this.handleExpressionChange}
292
executeQuery={this.executeQuery}
293
loading={this.state.loading}
294
enableAutocomplete={this.props.enableAutocomplete}
295
enableHighlighting={this.props.enableHighlighting}
296
enableLinter={this.props.enableLinter}
297
queryHistory={pastQueries}
298
metricNames={metricNames}
303
<Col>{this.state.error && <Alert color="danger">{this.state.error}</Alert>}</Col>
305
{this.state.warnings?.map((warning, index) => (
307
<Col>{warning && <Alert color="warning">{warning}</Alert>}</Col>
315
className={options.type === 'table' ? 'active' : ''}
316
onClick={() => this.handleChangeType(PanelType.Table)}
323
className={options.type === 'graph' ? 'active' : ''}
324
onClick={() => this.handleChangeType(PanelType.Graph)}
329
{!this.state.loading && !this.state.error && this.state.stats && <QueryStatsView {...this.state.stats} />}
331
<TabContent activeTab={options.type}>
332
<TabPane tabId="table">
333
{options.type === 'table' && (
335
<div className="table-controls">
337
time={options.endTime}
338
useLocalTime={this.props.useLocalTime}
339
range={options.range}
340
placeholder="Evaluation time"
341
onChangeTime={this.handleChangeEndTime}
344
<DataTable data={this.state.data} useLocalTime={this.props.useLocalTime} />
348
<TabPane tabId="graph">
349
{this.props.options.type === 'graph' && (
352
range={options.range}
353
endTime={options.endTime}
354
useLocalTime={this.props.useLocalTime}
355
resolution={options.resolution}
356
displayMode={options.displayMode}
357
isHeatmapData={this.state.isHeatmapData}
358
showExemplars={options.showExemplars}
359
onChangeRange={this.handleChangeRange}
360
onChangeEndTime={this.handleChangeEndTime}
361
onChangeResolution={this.handleChangeResolution}
362
onChangeDisplayMode={this.handleChangeDisplayMode}
363
onChangeShowExemplars={this.handleChangeShowExemplars}
366
data={this.state.data}
367
exemplars={this.state.exemplars}
368
displayMode={options.displayMode}
369
useLocalTime={this.props.useLocalTime}
370
showExemplars={options.showExemplars}
371
lastQueryParams={this.state.lastQueryParams}
373
handleTimeRangeSelection={this.handleTimeRangeSelection}
383
<Button className="float-right" color="link" onClick={this.props.removePanel} size="sm">