github.com/cockroachdb/cockroach@v20.2.0-alpha.1+incompatible/pkg/ui/src/views/statements/statementsPage.tsx (about)

     1  // Copyright 2018 The Cockroach Authors.
     2  //
     3  // Use of this software is governed by the Business Source License
     4  // included in the file licenses/BSL.txt.
     5  //
     6  // As of the Change Date specified in that file, in accordance with
     7  // the Business Source License, use of this software will be governed
     8  // by the Apache License, Version 2.0, included in the file
     9  // licenses/APL.txt.
    10  
    11  import { Icon, Pagination } from "antd";
    12  import { isNil, merge, forIn } from "lodash";
    13  import moment from "moment";
    14  import { DATE_FORMAT } from "src/util/format";
    15  import React from "react";
    16  import Helmet from "react-helmet";
    17  import { connect } from "react-redux";
    18  import { createSelector } from "reselect";
    19  import { RouteComponentProps, withRouter } from "react-router-dom";
    20  import { paginationPageCount } from "src/components/pagination/pagination";
    21  import * as protos from "src/js/protos";
    22  import { refreshStatementDiagnosticsRequests, refreshStatements } from "src/redux/apiReducers";
    23  import { CachedDataReducerState } from "src/redux/cachedDataReducer";
    24  import { AdminUIState } from "src/redux/state";
    25  import { StatementsResponseMessage } from "src/util/api";
    26  import { aggregateStatementStats, combineStatementStats, ExecutionStatistics, flattenStatementStats, StatementStatistics } from "src/util/appStats";
    27  import { appAttr } from "src/util/constants";
    28  import { TimestampToMoment } from "src/util/convert";
    29  import { Pick } from "src/util/pick";
    30  import { PrintTime } from "src/views/reports/containers/range/print";
    31  import Dropdown, { DropdownOption } from "src/views/shared/components/dropdown";
    32  import Loading from "src/views/shared/components/loading";
    33  import { PageConfig, PageConfigItem } from "src/views/shared/components/pageconfig";
    34  import { SortSetting } from "src/views/shared/components/sortabletable";
    35  import { Search } from "../app/components/Search";
    36  import { AggregateStatistics, makeStatementsColumns, StatementsSortedTable } from "./statementsTable";
    37  import ActivateDiagnosticsModal, { ActivateDiagnosticsModalRef } from "src/views/statements/diagnostics/activateDiagnosticsModal";
    38  import "./statements.styl";
    39  import {
    40    selectLastDiagnosticsReportPerStatement,
    41  } from "src/redux/statements/statementsSelectors";
    42  import { createStatementDiagnosticsAlertLocalSetting } from "src/redux/alerts";
    43  import { getMatchParamByName } from "src/util/query";
    44  import { trackPaginate, trackSearch } from "src/util/analytics";
    45  
    46  import "./statements.styl";
    47  import { ISortedTablePagination } from "../shared/components/sortedtable";
    48  import { statementsTable } from "src/util/docs";
    49  
    50  type ICollectedStatementStatistics = protos.cockroach.server.serverpb.StatementsResponse.ICollectedStatementStatistics;
    51  
    52  interface OwnProps {
    53    statements: AggregateStatistics[];
    54    statementsError: Error | null;
    55    apps: string[];
    56    totalFingerprints: number;
    57    lastReset: string;
    58    refreshStatements: typeof refreshStatements;
    59    refreshStatementDiagnosticsRequests: typeof refreshStatementDiagnosticsRequests;
    60    dismissAlertMessage: () => void;
    61  }
    62  
    63  export interface StatementsPageState {
    64    sortSetting: SortSetting;
    65    search?: string;
    66    pagination: ISortedTablePagination;
    67  }
    68  
    69  export type StatementsPageProps = OwnProps & RouteComponentProps<any>;
    70  
    71  export class StatementsPage extends React.Component<StatementsPageProps, StatementsPageState> {
    72    activateDiagnosticsRef: React.RefObject<ActivateDiagnosticsModalRef>;
    73  
    74    constructor(props: StatementsPageProps) {
    75      super(props);
    76      const defaultState = {
    77        sortSetting: {
    78          sortKey: 3, // Sort by Execution Count column as default option
    79          ascending: false,
    80        },
    81        pagination: {
    82          pageSize: 20,
    83          current: 1,
    84        },
    85        search: "",
    86      };
    87  
    88      const stateFromHistory = this.getStateFromHistory();
    89      this.state = merge(defaultState, stateFromHistory);
    90      this.activateDiagnosticsRef = React.createRef();
    91    }
    92  
    93    getStateFromHistory = (): Partial<StatementsPageState> => {
    94      const { history } = this.props;
    95      const searchParams = new URLSearchParams(history.location.search);
    96      const sortKey = searchParams.get("sortKey") || undefined;
    97      const ascending = searchParams.get("ascending") || undefined;
    98      const searchQuery = searchParams.get("q") || undefined;
    99  
   100      return {
   101        sortSetting: {
   102          sortKey,
   103          ascending: Boolean(ascending),
   104        },
   105        search: searchQuery,
   106      };
   107    }
   108  
   109    syncHistory = (params: Record<string, string | undefined>) => {
   110      const { history } = this.props;
   111      const currentSearchParams = new URLSearchParams(history.location.search);
   112      // const nextSearchParams = new URLSearchParams(params);
   113  
   114      forIn(params, (value, key) => {
   115        if (!value) {
   116          currentSearchParams.delete(key);
   117        } else {
   118          currentSearchParams.set(key, value);
   119        }
   120      });
   121  
   122      history.location.search = currentSearchParams.toString();
   123      history.replace(history.location);
   124    }
   125  
   126    changeSortSetting = (ss: SortSetting) => {
   127      this.setState({
   128        sortSetting: ss,
   129      });
   130  
   131      this.syncHistory({
   132        "sortKey": ss.sortKey,
   133        "ascending": Boolean(ss.ascending).toString(),
   134      });
   135    }
   136  
   137    selectApp = (app: DropdownOption) => {
   138      const { history } = this.props;
   139      history.location.pathname = `/statements/${app.value}`;
   140      history.replace(history.location);
   141      this.resetPagination();
   142    }
   143  
   144    resetPagination = () => {
   145      this.setState((prevState) => {
   146        return {
   147          pagination: {
   148            current: 1,
   149            pageSize: prevState.pagination.pageSize,
   150          },
   151        };
   152      });
   153    }
   154  
   155    componentDidMount() {
   156      this.props.refreshStatements();
   157      this.props.refreshStatementDiagnosticsRequests();
   158    }
   159  
   160    componentDidUpdate = (__: StatementsPageProps, prevState: StatementsPageState) => {
   161      if (this.state.search && this.state.search !== prevState.search) {
   162        trackSearch(this.filteredStatementsData().length);
   163      }
   164      this.props.refreshStatements();
   165      this.props.refreshStatementDiagnosticsRequests();
   166    }
   167  
   168    componentWillUnmount() {
   169      this.props.dismissAlertMessage();
   170    }
   171  
   172    onChangePage = (current: number) => {
   173      const { pagination } = this.state;
   174      this.setState({ pagination: { ...pagination, current } });
   175      trackPaginate(current);
   176    }
   177    onSubmitSearchField = (search: string) => {
   178      this.setState({ search });
   179      this.resetPagination();
   180      this.syncHistory({
   181        "q": search,
   182      });
   183    }
   184  
   185    onClearSearchField = () => {
   186      this.setState({ search: "" });
   187      this.syncHistory({
   188        "q": undefined,
   189      });
   190    }
   191  
   192    filteredStatementsData = () => {
   193      const { search } = this.state;
   194      const { statements } = this.props;
   195      return statements.filter(statement => search.split(" ").every(val => statement.label.toLowerCase().includes(val.toLowerCase())));
   196    }
   197  
   198    renderPage = (_page: number, type: "page" | "prev" | "next" | "jump-prev" | "jump-next", originalElement: React.ReactNode) => {
   199      switch (type) {
   200        case "jump-prev":
   201          return (
   202            <div className="_pg-jump">
   203              <Icon type="left" />
   204              <span className="_jump-dots">•••</span>
   205            </div>
   206          );
   207        case "jump-next":
   208          return (
   209            <div className="_pg-jump">
   210              <Icon type="right" />
   211              <span className="_jump-dots">•••</span>
   212            </div>
   213          );
   214        default:
   215          return originalElement;
   216      }
   217    }
   218  
   219    renderLastCleared = () => {
   220      const { lastReset } = this.props;
   221      return `Last cleared ${moment.utc(lastReset).format(DATE_FORMAT)}`;
   222    }
   223  
   224    noStatementResult = () => (
   225      <>
   226        <h3 className="table__no-results--title">There are no SQL statements that match your search or filter since this page was last cleared.</h3>
   227        <p className="table__no-results--description">
   228          <span>Statements are cleared every hour by default, or according to your configuration.</span>
   229          <a href={statementsTable} target="_blank">Learn more</a>
   230        </p>
   231      </>
   232    )
   233  
   234    renderStatements = () => {
   235      const { pagination, search } = this.state;
   236      const { statements, match } = this.props;
   237      const appAttrValue = getMatchParamByName(match, appAttr);
   238      const selectedApp = appAttrValue || "";
   239      const appOptions = [{ value: "", label: "All" }];
   240      this.props.apps.forEach(app => appOptions.push({ value: app, label: app }));
   241      const data = this.filteredStatementsData();
   242      return (
   243        <div>
   244          <PageConfig>
   245            <PageConfigItem>
   246              <Search
   247                onSubmit={this.onSubmitSearchField as any}
   248                onClear={this.onClearSearchField}
   249                defaultValue={search}
   250              />
   251            </PageConfigItem>
   252            <PageConfigItem>
   253              <Dropdown
   254                title="App"
   255                options={appOptions}
   256                selected={selectedApp}
   257                onChange={this.selectApp}
   258              />
   259            </PageConfigItem>
   260          </PageConfig>
   261          <section className="cl-table-container">
   262            <div className="cl-table-statistic">
   263              <h4 className="cl-count-title">
   264                {paginationPageCount({ ...pagination, total: this.filteredStatementsData().length }, "statements", match, appAttr, search)}
   265              </h4>
   266              <h4 className="last-cleared-title">
   267                {this.renderLastCleared()}
   268              </h4>
   269            </div>
   270            <StatementsSortedTable
   271              className="statements-table"
   272              data={data}
   273              columns={
   274                makeStatementsColumns(
   275                  statements,
   276                  selectedApp,
   277                  search,
   278                  this.activateDiagnosticsRef,
   279                )
   280              }
   281              empty={data.length === 0 && search.length === 0}
   282              emptyProps={{
   283                title: "There are no statements since this page was last cleared.",
   284                description: "Statements help you identify frequently executed or high latency SQL statements. Statements are cleared every hour by default, or according to your configuration.",
   285                label: "Learn more",
   286                onClick: () => window.open(statementsTable),
   287              }}
   288              sortSetting={this.state.sortSetting}
   289              onChangeSortSetting={this.changeSortSetting}
   290              renderNoResult={this.noStatementResult()}
   291              pagination={pagination}
   292            />
   293          </section>
   294          <Pagination
   295            size="small"
   296            itemRender={this.renderPage as (page: number, type: "page" | "prev" | "next" | "jump-prev" | "jump-next") => React.ReactNode}
   297            pageSize={pagination.pageSize}
   298            current={pagination.current}
   299            total={data.length}
   300            onChange={this.onChangePage}
   301            hideOnSinglePage
   302          />
   303        </div>
   304      );
   305    }
   306  
   307    render() {
   308      const { match } = this.props;
   309      const app = getMatchParamByName(match, appAttr);
   310      return (
   311        <React.Fragment>
   312          <Helmet title={app ? `${app} App | Statements` : "Statements"} />
   313  
   314          <section className="section">
   315            <h1 className="base-heading">Statements</h1>
   316          </section>
   317  
   318          <Loading
   319            loading={isNil(this.props.statements)}
   320            error={this.props.statementsError}
   321            render={this.renderStatements}
   322          />
   323          <ActivateDiagnosticsModal ref={this.activateDiagnosticsRef} />
   324        </React.Fragment>
   325      );
   326    }
   327  }
   328  
   329  type StatementsState = Pick<AdminUIState, "cachedData", "statements">;
   330  
   331  interface StatementsSummaryData {
   332    statement: string;
   333    implicitTxn: boolean;
   334    stats: StatementStatistics[];
   335  }
   336  
   337  function keyByStatementAndImplicitTxn(stmt: ExecutionStatistics): string {
   338    return stmt.statement + stmt.implicit_txn;
   339  }
   340  
   341  // selectStatements returns the array of AggregateStatistics to show on the
   342  // StatementsPage, based on if the appAttr route parameter is set.
   343  export const selectStatements = createSelector(
   344    (state: StatementsState) => state.cachedData.statements,
   345    (_state: StatementsState, props: RouteComponentProps) => props,
   346    selectLastDiagnosticsReportPerStatement,
   347    (
   348      state: CachedDataReducerState<StatementsResponseMessage>,
   349      props: RouteComponentProps<any>,
   350      lastDiagnosticsReportPerStatement,
   351    ) => {
   352      if (!state.data) {
   353        return null;
   354      }
   355      let statements = flattenStatementStats(state.data.statements);
   356      const app = getMatchParamByName(props.match, appAttr);
   357      const isInternal = (statement: ExecutionStatistics) => statement.app.startsWith(state.data.internal_app_name_prefix);
   358  
   359      if (app) {
   360        let criteria = app;
   361        let showInternal = false;
   362        if (criteria === "(unset)") {
   363          criteria = "";
   364        } else if (criteria === "(internal)") {
   365          showInternal = true;
   366        }
   367  
   368        statements = statements.filter(
   369          (statement: ExecutionStatistics) => (showInternal && isInternal(statement)) || statement.app === criteria,
   370        );
   371      } else {
   372        statements = statements.filter((statement: ExecutionStatistics) => !isInternal(statement));
   373      }
   374  
   375      const statsByStatementAndImplicitTxn: { [statement: string]: StatementsSummaryData } = {};
   376      statements.forEach(stmt => {
   377        const key = keyByStatementAndImplicitTxn(stmt);
   378        if (!(key in statsByStatementAndImplicitTxn)) {
   379          statsByStatementAndImplicitTxn[key] = {
   380            statement: stmt.statement,
   381            implicitTxn: stmt.implicit_txn,
   382            stats: [],
   383          };
   384        }
   385        statsByStatementAndImplicitTxn[key].stats.push(stmt.stats);
   386      });
   387  
   388      return Object.keys(statsByStatementAndImplicitTxn).map(key => {
   389        const stmt = statsByStatementAndImplicitTxn[key];
   390        return {
   391          label: stmt.statement,
   392          implicitTxn: stmt.implicitTxn,
   393          stats: combineStatementStats(stmt.stats),
   394          diagnosticsReport: lastDiagnosticsReportPerStatement[stmt.statement],
   395        };
   396      });
   397    },
   398  );
   399  
   400  // selectApps returns the array of all apps with statement statistics present
   401  // in the data.
   402  export const selectApps = createSelector(
   403    (state: StatementsState) => state.cachedData.statements,
   404    (state: CachedDataReducerState<StatementsResponseMessage>) => {
   405      if (!state.data) {
   406        return [];
   407      }
   408  
   409      let sawBlank = false;
   410      let sawInternal = false;
   411      const apps: { [app: string]: boolean } = {};
   412      state.data.statements.forEach(
   413        (statement: ICollectedStatementStatistics) => {
   414          if (state.data.internal_app_name_prefix && statement.key.key_data.app.startsWith(state.data.internal_app_name_prefix)) {
   415            sawInternal = true;
   416          } else if (statement.key.key_data.app) {
   417            apps[statement.key.key_data.app] = true;
   418          } else {
   419            sawBlank = true;
   420          }
   421        },
   422      );
   423      return [].concat(sawInternal ? ["(internal)"] : []).concat(sawBlank ? ["(unset)"] : []).concat(Object.keys(apps));
   424    },
   425  );
   426  
   427  // selectTotalFingerprints returns the count of distinct statement fingerprints
   428  // present in the data.
   429  export const selectTotalFingerprints = createSelector(
   430    (state: StatementsState) => state.cachedData.statements,
   431    (state: CachedDataReducerState<StatementsResponseMessage>) => {
   432      if (!state.data) {
   433        return 0;
   434      }
   435      const aggregated = aggregateStatementStats(state.data.statements);
   436      return aggregated.length;
   437    },
   438  );
   439  
   440  // selectLastReset returns a string displaying the last time the statement
   441  // statistics were reset.
   442  export const selectLastReset = createSelector(
   443    (state: StatementsState) => state.cachedData.statements,
   444    (state: CachedDataReducerState<StatementsResponseMessage>) => {
   445      if (!state.data) {
   446        return "unknown";
   447      }
   448  
   449      return PrintTime(TimestampToMoment(state.data.last_reset));
   450    },
   451  );
   452  
   453  // tslint:disable-next-line:variable-name
   454  const StatementsPageConnected = withRouter(connect(
   455    (state: AdminUIState, props: RouteComponentProps) => ({
   456      statements: selectStatements(state, props),
   457      statementsError: state.cachedData.statements.lastError,
   458      apps: selectApps(state),
   459      totalFingerprints: selectTotalFingerprints(state),
   460      lastReset: selectLastReset(state),
   461    }),
   462    {
   463      refreshStatements,
   464      refreshStatementDiagnosticsRequests,
   465      dismissAlertMessage: () => createStatementDiagnosticsAlertLocalSetting.set({ show: false }),
   466    },
   467  )(StatementsPage));
   468  
   469  export default StatementsPageConnected;