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

     1  // Copyright 2020 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 React from "react";
    12  import { connect } from "react-redux";
    13  import { Link } from "react-router-dom";
    14  import moment from "moment";
    15  import Long from "long";
    16  import emptyTracingBackground from "assets/statementsPage/emptyTracingBackground.svg";
    17  
    18  import {
    19    Button,
    20    Text,
    21    TextTypes,
    22    Table,
    23    ColumnsConfig,
    24    DownloadFile,
    25    DownloadFileRef,
    26  } from "src/components";
    27  import { AdminUIState } from "src/redux/state";
    28  import { getStatementDiagnostics } from "src/util/api";
    29  import { SummaryCard } from "src/views/shared/components/summaryCard";
    30  import {
    31    selectDiagnosticsReportsByStatementFingerprint,
    32    selectDiagnosticsReportsCountByStatementFingerprint,
    33  } from "src/redux/statements/statementsSelectors";
    34  import { createStatementDiagnosticsReportAction } from "src/redux/statements";
    35  import { trustIcon } from "src/util/trust";
    36  
    37  import { DiagnosticStatusBadge } from "./diagnosticStatusBadge";
    38  import DownloadIcon from "!!raw-loader!assets/download.svg";
    39  import "./diagnosticsView.styl";
    40  import { cockroach } from "src/js/protos";
    41  import IStatementDiagnosticsReport = cockroach.server.serverpb.IStatementDiagnosticsReport;
    42  import StatementDiagnosticsRequest = cockroach.server.serverpb.StatementDiagnosticsRequest;
    43  import { getDiagnosticsStatus, sortByCompletedField, sortByRequestedAtField } from "./diagnosticsUtils";
    44  import { statementDiagnostics } from "src/util/docs";
    45  import { createStatementDiagnosticsAlertLocalSetting } from "src/redux/alerts";
    46  import { trackActivateDiagnostics, trackDownloadDiagnosticsBundle } from "src/util/analytics";
    47  import { Empty } from "src/components/empty";
    48  
    49  interface DiagnosticsViewOwnProps {
    50    statementFingerprint?: string;
    51  }
    52  
    53  type DiagnosticsViewProps = DiagnosticsViewOwnProps & MapStateToProps & MapDispatchToProps;
    54  
    55  interface DiagnosticsViewState {
    56    traces: {
    57      [diagnosticsId: string]: string;
    58    };
    59  }
    60  
    61  export class DiagnosticsView extends React.Component<DiagnosticsViewProps, DiagnosticsViewState> {
    62    columns: ColumnsConfig<IStatementDiagnosticsReport> = [
    63      {
    64        key: "activatedOn",
    65        title: "Activated on",
    66        sorter: sortByRequestedAtField,
    67        defaultSortOrder: "descend",
    68        render: (_text, record) => {
    69          const timestamp = record.requested_at.seconds.toNumber() * 1000;
    70          return moment(timestamp).format("LL[ at ]h:mm a");
    71        },
    72      },
    73      {
    74        key: "status",
    75        title: "status",
    76        sorter: sortByCompletedField,
    77        width: "160px",
    78        render: (_text, record) => {
    79          const status = getDiagnosticsStatus(record);
    80          return (
    81            <DiagnosticStatusBadge
    82              status={status}
    83              enableTooltip={status !== "READY"}
    84            />
    85          );
    86        },
    87      },
    88      {
    89        key: "actions",
    90        title: "",
    91        sorter: false,
    92        width: "160px",
    93        render: (_text, record) => {
    94          if (record.completed) {
    95            return (
    96              <div className="crl-statements-diagnostics-view__actions-column">
    97                <a href={`_admin/v1/stmtbundle/${record.statement_diagnostics_id}`}
    98                   onClick={() => trackDownloadDiagnosticsBundle(record.statement_fingerprint)}>
    99                  <Button
   100                    size="small"
   101                    type="flat"
   102                    iconPosition="left"
   103                    icon={() => (
   104                      <span
   105                        className="crl-statements-diagnostics-view__icon"
   106                        dangerouslySetInnerHTML={ trustIcon(DownloadIcon) }
   107                      />
   108                    )}
   109                  >
   110                    Bundle (.zip)
   111                  </Button>
   112                </a>
   113              </div>
   114            );
   115          }
   116          return null;
   117        },
   118      },
   119    ];
   120  
   121    downloadRef = React.createRef<DownloadFileRef>();
   122  
   123    getStatementDiagnostics = async (diagnosticsId: Long) => {
   124      const request = new StatementDiagnosticsRequest({ statement_diagnostics_id: diagnosticsId });
   125      const response = await getStatementDiagnostics(request);
   126      const trace = response.diagnostics?.trace;
   127      this.downloadRef.current?.download("statement-diagnostics.json", "application/json", trace);
   128    }
   129  
   130    onActivateButtonClick = () => {
   131      const { activate, statementFingerprint } = this.props;
   132      activate(statementFingerprint);
   133      trackActivateDiagnostics(statementFingerprint);
   134    }
   135  
   136    componentWillUnmount() {
   137      this.props.dismissAlertMessage();
   138    }
   139  
   140    render() {
   141      const { hasData, diagnosticsReports } = this.props;
   142  
   143      const canRequestDiagnostics = diagnosticsReports.every(diagnostic => diagnostic.completed);
   144  
   145      const dataSource = diagnosticsReports.map((diagnosticsReport, idx) => ({
   146        ...diagnosticsReport,
   147        key: idx,
   148      }));
   149  
   150      if (!hasData) {
   151        return (
   152          <SummaryCard className="summary--card__empty-state">
   153            <EmptyDiagnosticsView {...this.props} />
   154          </SummaryCard>
   155        );
   156      }
   157      return (
   158        <SummaryCard>
   159          <div
   160            className="crl-statements-diagnostics-view__title"
   161          >
   162            <Text
   163              textType={TextTypes.Heading3}
   164            >
   165              Statement diagnostics
   166            </Text>
   167            {
   168              canRequestDiagnostics && (
   169                <Button
   170                  onClick={this.onActivateButtonClick}
   171                  disabled={!canRequestDiagnostics}
   172                  type="secondary"
   173                  className="crl-statements-diagnostics-view__activate-button"
   174                >
   175                  Activate diagnostics
   176                </Button>
   177              )
   178            }
   179          </div>
   180          <Table
   181            dataSource={dataSource}
   182            columns={this.columns}
   183          />
   184          <div className="crl-statements-diagnostics-view__footer">
   185            <Link to="/reports/statements/diagnosticshistory">All statement diagnostics</Link>
   186          </div>
   187          <DownloadFile ref={this.downloadRef}/>
   188        </SummaryCard>
   189      );
   190    }
   191  }
   192  
   193  export const EmptyDiagnosticsView = ({ activate, statementFingerprint }: DiagnosticsViewProps) => {
   194    const onActivateButtonClick = () => {
   195      activate(statementFingerprint);
   196      trackActivateDiagnostics(statementFingerprint);
   197    };
   198    return (
   199      <Empty
   200        title="Activate statement diagnostics"
   201        description="When you activate statement diagnostics, CockroachDB will wait for the next query that matches
   202        this statement fingerprint. A download button will appear on the statement list and detail pages
   203        when the query is ready. The statement diagnostic will include EXPLAIN plans,
   204        table statistics, and traces."
   205        anchor="Learn More"
   206        link={statementDiagnostics}
   207        label="Activate"
   208        onClick={onActivateButtonClick}
   209        backgroundImage={emptyTracingBackground}
   210      />
   211    );
   212  };
   213  
   214  interface MapStateToProps {
   215    hasData: boolean;
   216    diagnosticsReports: IStatementDiagnosticsReport[];
   217  }
   218  
   219  interface MapDispatchToProps {
   220    activate: (statementFingerprint: string) => void;
   221    dismissAlertMessage: () => void;
   222  }
   223  
   224  const mapStateToProps = (state: AdminUIState, props: DiagnosticsViewProps): MapStateToProps => {
   225    const { statementFingerprint } = props;
   226    const hasData = selectDiagnosticsReportsCountByStatementFingerprint(state, statementFingerprint) > 0;
   227    const diagnosticsReports = selectDiagnosticsReportsByStatementFingerprint(state, statementFingerprint);
   228    return {
   229      hasData,
   230      diagnosticsReports,
   231    };
   232  };
   233  
   234  const mapDispatchToProps: MapDispatchToProps = {
   235    activate: createStatementDiagnosticsReportAction,
   236    dismissAlertMessage: () => createStatementDiagnosticsAlertLocalSetting.set({ show: false }),
   237  };
   238  
   239  export default connect<
   240    MapStateToProps,
   241    MapDispatchToProps,
   242    DiagnosticsViewOwnProps
   243    >(mapStateToProps, mapDispatchToProps)(DiagnosticsView);