prometheus

Форк
0
393 строки · 12.2 Кб
1
import React, { Component } from 'react';
2

3
import { Alert, Button, Col, Nav, NavItem, NavLink, Row, TabContent, TabPane } from 'reactstrap';
4

5
import moment from 'moment-timezone';
6

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';
17

18
interface PanelProps {
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;
26
  pathPrefix: string;
27
  enableAutocomplete: boolean;
28
  enableHighlighting: boolean;
29
  enableLinter: boolean;
30
  id: string;
31
}
32

33
interface PanelState {
34
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
35
  data: any; // TODO: Type data.
36
  exemplars: ExemplarData;
37
  lastQueryParams: QueryParams | null;
38
  loading: boolean;
39
  warnings: string[] | null;
40
  error: string | null;
41
  stats: QueryStats | null;
42
  exprInputValue: string;
43
  isHeatmapData: boolean;
44
}
45

46
export interface PanelOptions {
47
  expr: string;
48
  type: PanelType;
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;
54
}
55

56
export enum PanelType {
57
  Graph = 'graph',
58
  Table = 'table',
59
}
60

61
export enum GraphDisplayMode {
62
  Lines = 'lines',
63
  Stacked = 'stacked',
64
  Heatmap = 'heatmap',
65
}
66

67
export const PanelDefaultOptions: PanelOptions = {
68
  type: PanelType.Table,
69
  expr: '',
70
  range: 60 * 60 * 1000,
71
  endTime: null,
72
  resolution: null,
73
  displayMode: GraphDisplayMode.Lines,
74
  showExemplars: false,
75
};
76

77
class Panel extends Component<PanelProps, PanelState> {
78
  private abortInFlightFetch: (() => void) | null = null;
79
  private debounceExecuteQuery: () => void;
80

81
  constructor(props: PanelProps) {
82
    super(props);
83

84
    this.state = {
85
      data: null,
86
      exemplars: [],
87
      lastQueryParams: null,
88
      loading: false,
89
      warnings: null,
90
      error: null,
91
      stats: null,
92
      exprInputValue: props.options.expr,
93
      isHeatmapData: false,
94
    };
95

96
    this.debounceExecuteQuery = debounce(this.executeQuery.bind(this), 250);
97
  }
98

99
  componentDidUpdate({ options: prevOpts }: PanelProps): void {
100
    const { endTime, range, resolution, showExemplars, type } = this.props.options;
101

102
    if (prevOpts.endTime !== endTime || prevOpts.range !== range) {
103
      this.debounceExecuteQuery();
104
      return;
105
    }
106

107
    if (prevOpts.resolution !== resolution || prevOpts.type !== type || showExemplars !== prevOpts.showExemplars) {
108
      this.executeQuery();
109
    }
110
  }
111

112
  componentDidMount(): void {
113
    this.executeQuery();
114
  }
115

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 });
123
    }
124
    if (expr === '') {
125
      return;
126
    }
127

128
    if (this.abortInFlightFetch) {
129
      this.abortInFlightFetch();
130
      this.abortInFlightFetch = null;
131
    }
132

133
    const abortController = new AbortController();
134
    this.abortInFlightFetch = () => abortController.abort();
135
    this.setState({ loading: true });
136

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({
141
      query: expr,
142
    });
143

144
    let path: string;
145
    switch (this.props.options.type) {
146
      case 'graph':
147
        path = 'query_range';
148
        params.append('start', startTime.toString());
149
        params.append('end', endTime.toString());
150
        params.append('step', resolution.toString());
151
        break;
152
      case 'table':
153
        path = 'query';
154
        params.append('time', endTime.toString());
155
        break;
156
      default:
157
        throw new Error('Invalid panel type "' + this.props.options.type + '"');
158
    }
159

160
    let query;
161
    let exemplars;
162
    try {
163
      query = await fetch(`${this.props.pathPrefix}/${API_PATH}/${path}?${params}`, {
164
        cache: 'no-store',
165
        credentials: 'same-origin',
166
        signal: abortController.signal,
167
      }).then((resp) => resp.json());
168

169
      if (query.status !== 'success') {
170
        throw new Error(query.error || 'invalid response JSON');
171
      }
172

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}`, {
176
          cache: 'no-store',
177
          credentials: 'same-origin',
178
          signal: abortController.signal,
179
        }).then((resp) => resp.json());
180

181
        if (exemplars.status !== 'success') {
182
          throw new Error(exemplars.error || 'invalid response JSON');
183
        }
184
      }
185

186
      let resultSeries = 0;
187
      if (query.data) {
188
        const { resultType, result } = query.data;
189
        if (resultType === 'scalar') {
190
          resultSeries = 1;
191
        } else if (result && result.length > 0) {
192
          resultSeries = result.length;
193
        }
194
      }
195

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 });
200
      }
201

202
      this.setState({
203
        error: null,
204
        data: query.data,
205
        exemplars: exemplars?.data,
206
        warnings: query.warnings,
207
        lastQueryParams: {
208
          startTime,
209
          endTime,
210
          resolution,
211
        },
212
        stats: {
213
          loadTime: Date.now() - queryStart,
214
          resolution,
215
          resultSeries,
216
        },
217
        loading: false,
218
        isHeatmapData: isHeatmap,
219
      });
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.
225
        return;
226
      }
227
      this.setState({
228
        error: 'Error executing query: ' + error.message,
229
        loading: false,
230
      });
231
    }
232
  };
233

234
  setOptions(opts: Partial<PanelOptions>): void {
235
    const newOpts = { ...this.props.options, ...opts };
236
    this.props.onOptionsChanged(newOpts);
237
  }
238

239
  handleExpressionChange = (expr: string): void => {
240
    this.setState({ exprInputValue: expr });
241
  };
242

243
  handleChangeRange = (range: number): void => {
244
    this.setOptions({ range: range });
245
  };
246

247
  getEndTime = (): number | moment.Moment => {
248
    if (this.props.options.endTime === null) {
249
      return moment();
250
    }
251
    return this.props.options.endTime;
252
  };
253

254
  handleChangeEndTime = (endTime: number | null): void => {
255
    this.setOptions({ endTime: endTime });
256
  };
257

258
  handleChangeResolution = (resolution: number | null): void => {
259
    this.setOptions({ resolution: resolution });
260
  };
261

262
  handleChangeType = (type: PanelType): void => {
263
    if (this.props.options.type === type) {
264
      return;
265
    }
266

267
    this.setState({ data: null });
268
    this.setOptions({ type: type });
269
  };
270

271
  handleChangeDisplayMode = (mode: GraphDisplayMode): void => {
272
    this.setOptions({ displayMode: mode });
273
  };
274

275
  handleChangeShowExemplars = (show: boolean): void => {
276
    this.setOptions({ showExemplars: show });
277
  };
278

279
  handleTimeRangeSelection = (startTime: number, endTime: number): void => {
280
    this.setOptions({ range: endTime - startTime, endTime: endTime });
281
  };
282

283
  render(): JSX.Element {
284
    const { pastQueries, metricNames, options } = this.props;
285
    return (
286
      <div className="panel">
287
        <Row>
288
          <Col>
289
            <ExpressionInput
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}
299
            />
300
          </Col>
301
        </Row>
302
        <Row>
303
          <Col>{this.state.error && <Alert color="danger">{this.state.error}</Alert>}</Col>
304
        </Row>
305
        {this.state.warnings?.map((warning, index) => (
306
          <Row key={index}>
307
            <Col>{warning && <Alert color="warning">{warning}</Alert>}</Col>
308
          </Row>
309
        ))}
310
        <Row>
311
          <Col>
312
            <Nav tabs>
313
              <NavItem>
314
                <NavLink
315
                  className={options.type === 'table' ? 'active' : ''}
316
                  onClick={() => this.handleChangeType(PanelType.Table)}
317
                >
318
                  Table
319
                </NavLink>
320
              </NavItem>
321
              <NavItem>
322
                <NavLink
323
                  className={options.type === 'graph' ? 'active' : ''}
324
                  onClick={() => this.handleChangeType(PanelType.Graph)}
325
                >
326
                  Graph
327
                </NavLink>
328
              </NavItem>
329
              {!this.state.loading && !this.state.error && this.state.stats && <QueryStatsView {...this.state.stats} />}
330
            </Nav>
331
            <TabContent activeTab={options.type}>
332
              <TabPane tabId="table">
333
                {options.type === 'table' && (
334
                  <>
335
                    <div className="table-controls">
336
                      <TimeInput
337
                        time={options.endTime}
338
                        useLocalTime={this.props.useLocalTime}
339
                        range={options.range}
340
                        placeholder="Evaluation time"
341
                        onChangeTime={this.handleChangeEndTime}
342
                      />
343
                    </div>
344
                    <DataTable data={this.state.data} useLocalTime={this.props.useLocalTime} />
345
                  </>
346
                )}
347
              </TabPane>
348
              <TabPane tabId="graph">
349
                {this.props.options.type === 'graph' && (
350
                  <>
351
                    <GraphControls
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}
364
                    />
365
                    <GraphTabContent
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}
372
                      id={this.props.id}
373
                      handleTimeRangeSelection={this.handleTimeRangeSelection}
374
                    />
375
                  </>
376
                )}
377
              </TabPane>
378
            </TabContent>
379
          </Col>
380
        </Row>
381
        <Row>
382
          <Col>
383
            <Button className="float-right" color="link" onClick={this.props.removePanel} size="sm">
384
              Remove Panel
385
            </Button>
386
          </Col>
387
        </Row>
388
      </div>
389
    );
390
  }
391
}
392

393
export default Panel;
394

Использование cookies

Мы используем файлы cookie в соответствии с Политикой конфиденциальности и Политикой использования cookies.

Нажимая кнопку «Принимаю», Вы даете АО «СберТех» согласие на обработку Ваших персональных данных в целях совершенствования нашего веб-сайта и Сервиса GitVerse, а также повышения удобства их использования.

Запретить использование cookies Вы можете самостоятельно в настройках Вашего браузера.