github.com/thanos-io/thanos@v0.32.5/pkg/ui/react-app/src/pages/graph/Panel.tsx (about)

     1  import React, { Component } from 'react';
     2  
     3  import {
     4    UncontrolledAlert,
     5    Alert,
     6    Button,
     7    Col,
     8    Nav,
     9    NavItem,
    10    NavLink,
    11    Row,
    12    TabContent,
    13    TabPane,
    14    Input,
    15    Label,
    16  } from 'reactstrap';
    17  import Select from 'react-select';
    18  
    19  import moment from 'moment-timezone';
    20  
    21  import Checkbox from '../../components/Checkbox';
    22  import ListTree, { QueryTree } from '../../components/ListTree';
    23  import ExpressionInput from './ExpressionInput';
    24  import GraphControls from './GraphControls';
    25  import { GraphTabContent } from './GraphTabContent';
    26  import DataTable from './DataTable';
    27  import TimeInput from './TimeInput';
    28  import QueryStatsView, { QueryStats } from './QueryStatsView';
    29  import { Store } from '../../thanos/pages/stores/store';
    30  import PathPrefixProps from '../../types/PathPrefixProps';
    31  import { QueryParams } from '../../types/types';
    32  import { parseDuration } from '../../utils';
    33  
    34  export interface PanelProps {
    35    id: string;
    36    options: PanelOptions;
    37    onOptionsChanged: (opts: PanelOptions) => void;
    38    useLocalTime: boolean;
    39    pastQueries: string[];
    40    metricNames: string[];
    41    removePanel: () => void;
    42    onExecuteQuery: (query: string) => void;
    43    stores: Store[];
    44    enableAutocomplete: boolean;
    45    enableHighlighting: boolean;
    46    enableLinter: boolean;
    47    defaultStep: string;
    48    defaultEngine: string;
    49  }
    50  
    51  interface PanelState {
    52    data: any; // TODO: Type data.
    53    lastQueryParams: QueryParams | null;
    54    loading: boolean;
    55    error: string | null;
    56    warnings: string[] | null;
    57    stats: QueryStats | null;
    58    exprInputValue: string;
    59    explanation: QueryTree | null;
    60  }
    61  
    62  export interface PanelOptions {
    63    expr: string;
    64    type: PanelType;
    65    range: number; // Range in milliseconds.
    66    endTime: number | null; // Timestamp in milliseconds.
    67    resolution: number | null; // Resolution in seconds.
    68    stacked: boolean;
    69    maxSourceResolution: string;
    70    useDeduplication: boolean;
    71    usePartialResponse: boolean;
    72    storeMatches: Store[];
    73    engine: string;
    74    explain: boolean;
    75    disableExplainCheckbox: boolean;
    76  }
    77  
    78  export enum PanelType {
    79    Graph = 'graph',
    80    Table = 'table',
    81  }
    82  
    83  export const PanelDefaultOptions: PanelOptions = {
    84    type: PanelType.Table,
    85    expr: '',
    86    range: 60 * 60 * 1000,
    87    endTime: null,
    88    resolution: null,
    89    stacked: false,
    90    maxSourceResolution: '0s',
    91    useDeduplication: true,
    92    usePartialResponse: false,
    93    storeMatches: [],
    94    engine: '',
    95    explain: false,
    96    disableExplainCheckbox: false,
    97  };
    98  
    99  class Panel extends Component<PanelProps & PathPrefixProps, PanelState> {
   100    private abortInFlightFetch: (() => void) | null = null;
   101  
   102    constructor(props: PanelProps) {
   103      super(props);
   104  
   105      this.state = {
   106        data: null,
   107        lastQueryParams: null,
   108        loading: false,
   109        warnings: null,
   110        error: null,
   111        stats: null,
   112        exprInputValue: props.options.expr,
   113        explanation: null,
   114      };
   115  
   116      if (this.props.options.engine === '') {
   117        this.props.options.engine = this.props.defaultEngine;
   118      }
   119      this.handleEngine(this.props.options.engine);
   120  
   121      this.handleChangeDeduplication = this.handleChangeDeduplication.bind(this);
   122      this.handleChangePartialResponse = this.handleChangePartialResponse.bind(this);
   123      this.handleStoreMatchChange = this.handleStoreMatchChange.bind(this);
   124      this.handleChangeEngine = this.handleChangeEngine.bind(this);
   125      this.handleChangeExplain = this.handleChangeExplain.bind(this);
   126    }
   127  
   128    componentDidUpdate({ options: prevOpts }: PanelProps): void {
   129      const {
   130        endTime,
   131        range,
   132        resolution,
   133        type,
   134        maxSourceResolution,
   135        useDeduplication,
   136        usePartialResponse,
   137        engine,
   138        explain,
   139        // TODO: Add support for Store Matches
   140      } = this.props.options;
   141      if (
   142        prevOpts.endTime !== endTime ||
   143        prevOpts.range !== range ||
   144        prevOpts.resolution !== resolution ||
   145        prevOpts.type !== type ||
   146        prevOpts.maxSourceResolution !== maxSourceResolution ||
   147        prevOpts.useDeduplication !== useDeduplication ||
   148        prevOpts.usePartialResponse !== usePartialResponse ||
   149        prevOpts.engine !== engine ||
   150        prevOpts.explain !== explain
   151        // Check store matches
   152      ) {
   153        this.executeQuery();
   154      }
   155    }
   156  
   157    componentDidMount(): void {
   158      this.executeQuery();
   159    }
   160  
   161    executeQuery = (): void => {
   162      const { exprInputValue: expr } = this.state;
   163      const queryStart = Date.now();
   164      this.props.onExecuteQuery(expr);
   165      if (this.props.options.expr !== expr) {
   166        this.setOptions({ expr });
   167      }
   168      if (expr === '') {
   169        return;
   170      }
   171  
   172      if (this.abortInFlightFetch) {
   173        this.abortInFlightFetch();
   174        this.abortInFlightFetch = null;
   175      }
   176  
   177      const abortController = new AbortController();
   178      this.abortInFlightFetch = () => abortController.abort();
   179      this.setState({ loading: true });
   180  
   181      const endTime = this.getEndTime().valueOf() / 1000; // TODO: shouldn't valueof only work when it's a moment?
   182      const startTime = endTime - this.props.options.range / 1000;
   183      const resolution =
   184        this.props.options.resolution ||
   185        Math.max(Math.floor(this.props.options.range / 250000), (parseDuration(this.props.defaultStep) || 0) / 1000);
   186      const params: URLSearchParams = new URLSearchParams({
   187        query: expr,
   188        dedup: this.props.options.useDeduplication.toString(),
   189        partial_response: this.props.options.usePartialResponse.toString(),
   190      });
   191  
   192      // Add storeMatches to query params.
   193      // eslint-disable-next-line @typescript-eslint/no-unused-expressions
   194      this.props.options.storeMatches?.forEach((store: Store) =>
   195        params.append('storeMatch[]', `{__address__="${store.name}"}`)
   196      );
   197  
   198      let path: string;
   199      switch (this.props.options.type) {
   200        case 'graph':
   201          path = '/api/v1/query_range';
   202          params.append('start', startTime.toString());
   203          params.append('end', endTime.toString());
   204          params.append('step', resolution.toString());
   205          params.append('max_source_resolution', this.props.options.maxSourceResolution);
   206          params.append('engine', this.props.options.engine);
   207          params.append('explain', this.props.options.explain.toString());
   208          // TODO path prefix here and elsewhere.
   209          break;
   210        case 'table':
   211          path = '/api/v1/query';
   212          params.append('time', endTime.toString());
   213          params.append('engine', this.props.options.engine);
   214          params.append('explain', this.props.options.explain.toString());
   215          break;
   216        default:
   217          throw new Error('Invalid panel type "' + this.props.options.type + '"');
   218      }
   219  
   220      fetch(`${this.props.pathPrefix}${path}?${params}`, {
   221        cache: 'no-store',
   222        credentials: 'same-origin',
   223        signal: abortController.signal,
   224      })
   225        .then((resp) => resp.json())
   226        .then((json) => {
   227          if (json.status !== 'success') {
   228            throw new Error(json.error || 'invalid response JSON');
   229          }
   230  
   231          let resultSeries = 0;
   232          let explanation = null;
   233          if (json.data) {
   234            const { resultType, result } = json.data;
   235            if (resultType === 'scalar') {
   236              resultSeries = 1;
   237            } else if (result && result.length > 0) {
   238              resultSeries = result.length;
   239            }
   240            explanation = json.data.explanation;
   241          }
   242  
   243          this.setState({
   244            error: null,
   245            data: json.data,
   246            lastQueryParams: {
   247              startTime,
   248              endTime,
   249              resolution,
   250            },
   251            warnings: json.warnings,
   252            stats: {
   253              loadTime: Date.now() - queryStart,
   254              resolution,
   255              resultSeries,
   256            },
   257            loading: false,
   258            explanation: explanation,
   259          });
   260          this.abortInFlightFetch = null;
   261        })
   262        .catch((error) => {
   263          if (error.name === 'AbortError') {
   264            // Aborts are expected, don't show an error for them.
   265            return;
   266          }
   267          this.setState({
   268            error: 'Error executing query: ' + error.message,
   269            loading: false,
   270          });
   271        });
   272    };
   273  
   274    setOptions(opts: any): void {
   275      const newOpts = { ...this.props.options, ...opts };
   276      this.props.onOptionsChanged(newOpts);
   277    }
   278  
   279    handleExpressionChange = (expr: string): void => {
   280      this.setState({ exprInputValue: expr });
   281    };
   282  
   283    handleChangeRange = (range: number): void => {
   284      this.setOptions({ range: range });
   285    };
   286  
   287    getEndTime = (): number | moment.Moment => {
   288      if (this.props.options.endTime === null) {
   289        return moment();
   290      }
   291      return this.props.options.endTime;
   292    };
   293  
   294    handleChangeEndTime = (endTime: number | null): void => {
   295      this.setOptions({ endTime: endTime });
   296    };
   297  
   298    handleChangeResolution = (resolution: number | null): void => {
   299      this.setOptions({ resolution: resolution });
   300    };
   301  
   302    handleChangeMaxSourceResolution = (maxSourceResolution: string): void => {
   303      this.setOptions({ maxSourceResolution });
   304    };
   305  
   306    handleChangeType = (type: PanelType): void => {
   307      this.setState({ data: null });
   308      this.setOptions({ type: type });
   309    };
   310  
   311    handleChangeStacking = (stacked: boolean): void => {
   312      this.setOptions({ stacked: stacked });
   313    };
   314  
   315    handleChangeDeduplication = (event: React.ChangeEvent<HTMLInputElement>): void => {
   316      this.setOptions({ useDeduplication: event.target.checked });
   317    };
   318  
   319    handleChangePartialResponse = (event: React.ChangeEvent<HTMLInputElement>): void => {
   320      this.setOptions({ usePartialResponse: event.target.checked });
   321    };
   322  
   323    handleStoreMatchChange = (selectedStores: any): void => {
   324      this.setOptions({ storeMatches: selectedStores || [] });
   325    };
   326  
   327    handleToggleAlert = (): void => {
   328      this.setState({ error: null });
   329    };
   330  
   331    handleToggleWarn = (): void => {
   332      this.setState({ warnings: null });
   333    };
   334  
   335    handleChangeEngine = (event: React.ChangeEvent<HTMLInputElement>): void => {
   336      this.handleEngine(event.target.value);
   337    };
   338  
   339    handleChangeExplain = (event: React.ChangeEvent<HTMLInputElement>): void => {
   340      this.setOptions({ explain: event.target.checked });
   341    };
   342  
   343    handleEngine = (engine: string): void => {
   344      if (engine === 'prometheus') {
   345        this.setOptions({ engine: engine, explain: false, disableExplainCheckbox: true });
   346      } else {
   347        this.setOptions({ engine: engine, disableExplainCheckbox: false });
   348      }
   349    };
   350  
   351    render(): JSX.Element {
   352      const { pastQueries, metricNames, options, id, stores } = this.props;
   353      return (
   354        <div className="panel">
   355          <Row>
   356            <Col>
   357              <ExpressionInput
   358                pathPrefix={this.props.pathPrefix}
   359                value={this.state.exprInputValue}
   360                onExpressionChange={this.handleExpressionChange}
   361                executeQuery={this.executeQuery}
   362                loading={this.state.loading}
   363                enableAutocomplete={this.props.enableAutocomplete}
   364                enableHighlighting={this.props.enableHighlighting}
   365                enableLinter={this.props.enableLinter}
   366                queryHistory={pastQueries}
   367                metricNames={metricNames}
   368              />
   369            </Col>
   370          </Row>
   371          <Row>
   372            <Col>
   373              <UncontrolledAlert isOpen={this.state.error || false} toggle={this.handleToggleAlert} color="danger">
   374                {this.state.error}
   375              </UncontrolledAlert>
   376            </Col>
   377          </Row>
   378          <Row>
   379            <Col>
   380              <UncontrolledAlert
   381                isOpen={this.state.warnings || false}
   382                toggle={this.handleToggleWarn}
   383                color="info"
   384                style={{ whiteSpace: 'break-spaces' }}
   385              >
   386                {this.state.warnings}
   387              </UncontrolledAlert>
   388            </Col>
   389          </Row>
   390          <Row>
   391            <Col>
   392              <div className="float-left">
   393                <Checkbox
   394                  wrapperStyles={{ marginLeft: 20, display: 'inline-block' }}
   395                  id={`use-deduplication-checkbox-${id}`}
   396                  onChange={this.handleChangeDeduplication}
   397                  defaultChecked={options.useDeduplication}
   398                >
   399                  Use Deduplication
   400                </Checkbox>
   401                <Checkbox
   402                  wrapperStyles={{ marginLeft: 20, display: 'inline-block' }}
   403                  id={`use-partial-resp-checkbox-${id}`}
   404                  onChange={this.handleChangePartialResponse}
   405                  defaultChecked={options.usePartialResponse}
   406                >
   407                  Use Partial Response
   408                </Checkbox>
   409                <Label
   410                  style={{ marginLeft: '10px', display: 'inline-block' }}
   411                  for={`select-engine=${id}`}
   412                  className="control-label"
   413                >
   414                  Engine
   415                </Label>
   416                <Input
   417                  style={{
   418                    width: 'auto',
   419                    marginLeft: '10px',
   420                    display: 'inline-block',
   421                  }}
   422                  id={`select-engine=${id}`}
   423                  type="select"
   424                  value={options.engine}
   425                  onChange={this.handleChangeEngine}
   426                  bsSize="sm"
   427                >
   428                  <option value="prometheus">Prometheus</option>
   429                  <option value="thanos">Thanos</option>
   430                </Input>
   431              </div>
   432              <div className="float-right">
   433                <Checkbox
   434                  wrapperStyles={{ marginRight: 20, display: 'inline-block' }}
   435                  id={`explain-${id}`}
   436                  onChange={this.handleChangeExplain}
   437                  checked={options.explain}
   438                  disabled={options.disableExplainCheckbox}
   439                >
   440                  Explain
   441                </Checkbox>
   442              </div>
   443            </Col>
   444          </Row>
   445          <Row hidden={!(options.explain && this.state.explanation)}>
   446            <Col>
   447              <Alert color="info" style={{ overflowX: 'auto', whiteSpace: 'nowrap', width: '100%' }}>
   448                <ListTree id={`explain-tree-${id}`} node={this.state.explanation} />
   449              </Alert>
   450            </Col>
   451          </Row>
   452          {stores?.length > 0 && (
   453            <Row>
   454              <Col>
   455                <div className="store-filter-wrapper">
   456                  <label className="store-filter-label">Store Filter:</label>
   457                  <Select
   458                    defaultValue={options.storeMatches}
   459                    options={stores}
   460                    isMulti
   461                    getOptionLabel={(option: Store) => option.name}
   462                    getOptionValue={(option: Store) => option.name}
   463                    closeMenuOnSelect={false}
   464                    styles={{
   465                      container: (provided, state) => ({
   466                        ...provided,
   467                        marginBottom: 20,
   468                        zIndex: 3,
   469                        width: '100%',
   470                        color: '#000',
   471                      }),
   472                    }}
   473                    onChange={this.handleStoreMatchChange}
   474                  />
   475                </div>
   476              </Col>
   477            </Row>
   478          )}
   479          <Row>
   480            <Col>
   481              <Nav tabs>
   482                <NavItem>
   483                  <NavLink
   484                    className={options.type === 'table' ? 'active' : ''}
   485                    onClick={() => this.handleChangeType(PanelType.Table)}
   486                  >
   487                    Table
   488                  </NavLink>
   489                </NavItem>
   490                <NavItem>
   491                  <NavLink
   492                    className={options.type === 'graph' ? 'active' : ''}
   493                    onClick={() => this.handleChangeType(PanelType.Graph)}
   494                  >
   495                    Graph
   496                  </NavLink>
   497                </NavItem>
   498                {!this.state.loading && !this.state.error && this.state.stats && <QueryStatsView {...this.state.stats} />}
   499              </Nav>
   500              <TabContent activeTab={options.type}>
   501                <TabPane tabId="table">
   502                  {options.type === 'table' && (
   503                    <>
   504                      <div className="table-controls">
   505                        <TimeInput
   506                          time={options.endTime}
   507                          useLocalTime={this.props.useLocalTime}
   508                          range={options.range}
   509                          placeholder="Evaluation time"
   510                          onChangeTime={this.handleChangeEndTime}
   511                        />
   512                      </div>
   513                      <DataTable data={this.state.data} />
   514                    </>
   515                  )}
   516                </TabPane>
   517                <TabPane tabId="graph">
   518                  {this.props.options.type === 'graph' && (
   519                    <>
   520                      <GraphControls
   521                        range={options.range}
   522                        endTime={options.endTime}
   523                        useLocalTime={this.props.useLocalTime}
   524                        resolution={options.resolution}
   525                        stacked={options.stacked}
   526                        maxSourceResolution={options.maxSourceResolution}
   527                        onChangeRange={this.handleChangeRange}
   528                        onChangeEndTime={this.handleChangeEndTime}
   529                        onChangeResolution={this.handleChangeResolution}
   530                        onChangeStacking={this.handleChangeStacking}
   531                        onChangeMaxSourceResolution={this.handleChangeMaxSourceResolution}
   532                      />
   533                      <GraphTabContent
   534                        data={this.state.data}
   535                        stacked={options.stacked}
   536                        useLocalTime={this.props.useLocalTime}
   537                        lastQueryParams={this.state.lastQueryParams}
   538                      />
   539                    </>
   540                  )}
   541                </TabPane>
   542              </TabContent>
   543            </Col>
   544          </Row>
   545          <Row>
   546            <Col>
   547              <Button className="float-right" color="link" onClick={this.props.removePanel} size="sm">
   548                Remove Panel
   549              </Button>
   550            </Col>
   551          </Row>
   552        </div>
   553      );
   554    }
   555  }
   556  
   557  export default Panel;