From dd664d0892f8cd299c22cb5def5c4bc9499b9962 Mon Sep 17 00:00:00 2001 From: maryliag Date: Thu, 13 Apr 2023 16:24:10 +0000 Subject: [PATCH] ui: add timescale to diag details page Fixes #92417 This commit adds a time scale picker to the Diagnostics tab on Statement Details page. The time scale is aligned with the other ones, and now is possible to see bundles from only the selected period. This commit also makes some UX updates on the same tab, making it use the same SortedTable component as other pages, removing the white background and title to align with all other pages that no longer have those items. Release note (ui change): Add a time scale selector to the Diagnostics tab under the Statement Details page, make it possible to see bundles only from the selected period. --- .../src/searchCriteria/searchCriteria.tsx | 10 +- .../diagnostics/diagnosticsUtils.ts | 65 +----- .../diagnostics/diagnosticsView.module.scss | 18 +- .../diagnostics/diagnosticsView.spec.tsx | 23 +- .../diagnostics/diagnosticsView.tsx | 199 ++++++++++-------- .../src/statementDetails/statementDetails.tsx | 35 ++- 6 files changed, 195 insertions(+), 155 deletions(-) diff --git a/pkg/ui/workspaces/cluster-ui/src/searchCriteria/searchCriteria.tsx b/pkg/ui/workspaces/cluster-ui/src/searchCriteria/searchCriteria.tsx index 74df0a20eb5d..3675eaea231b 100644 --- a/pkg/ui/workspaces/cluster-ui/src/searchCriteria/searchCriteria.tsx +++ b/pkg/ui/workspaces/cluster-ui/src/searchCriteria/searchCriteria.tsx @@ -11,16 +11,14 @@ import React from "react"; import classNames from "classnames/bind"; import styles from "./searchCriteria.module.scss"; +import { PageConfig, PageConfigItem } from "src/pageConfig"; +import { Button } from "src/button"; +import { commonStyles, selectCustomStyles } from "src/common"; import { - Button, - commonStyles, - PageConfig, - PageConfigItem, - selectCustomStyles, TimeScale, timeScale1hMinOptions, TimeScaleDropdown, -} from "src"; +} from "src/timeScaleDropdown"; import { applyBtn } from "../queryFilter/filterClasses"; import Select from "react-select"; import { limitOptions } from "../util/sqlActivityConstants"; diff --git a/pkg/ui/workspaces/cluster-ui/src/statementDetails/diagnostics/diagnosticsUtils.ts b/pkg/ui/workspaces/cluster-ui/src/statementDetails/diagnostics/diagnosticsUtils.ts index 4b21c4539952..28bd54c05127 100644 --- a/pkg/ui/workspaces/cluster-ui/src/statementDetails/diagnostics/diagnosticsUtils.ts +++ b/pkg/ui/workspaces/cluster-ui/src/statementDetails/diagnostics/diagnosticsUtils.ts @@ -8,7 +8,7 @@ // by the Apache License, Version 2.0, included in the file // licenses/APL.txt. -import { isUndefined } from "lodash"; +import { TimeScale, toDateRange } from "src/timeScaleDropdown"; import { DiagnosticStatuses } from "src/statementsDiagnostics"; import { StatementDiagnosticsReport } from "../../api"; import moment from "moment-timezone"; @@ -19,60 +19,17 @@ export function getDiagnosticsStatus( if (diagnosticsRequest.completed) { return "READY"; } - return "WAITING"; } -export function sortByRequestedAtField( - a: StatementDiagnosticsReport, - b: StatementDiagnosticsReport, -): number { - const activatedOnA = moment(a.requested_at)?.unix(); - const activatedOnB = moment(b.requested_at)?.unix(); - if (isUndefined(activatedOnA) && isUndefined(activatedOnB)) { - return 0; - } - if (activatedOnA < activatedOnB) { - return -1; - } - if (activatedOnA > activatedOnB) { - return 1; - } - return 0; -} - -export function sortByCompletedField( - a: StatementDiagnosticsReport, - b: StatementDiagnosticsReport, -): number { - const completedA = a.completed ? 1 : -1; - const completedB = b.completed ? 1 : -1; - if (completedA < completedB) { - return -1; - } - if (completedA > completedB) { - return 1; - } - return 0; -} - -export function sortByStatementFingerprintField( - a: StatementDiagnosticsReport, - b: StatementDiagnosticsReport, -): number { - const statementFingerprintA = a.statement_fingerprint; - const statementFingerprintB = b.statement_fingerprint; - if ( - isUndefined(statementFingerprintA) && - isUndefined(statementFingerprintB) - ) { - return 0; - } - if (statementFingerprintA < statementFingerprintB) { - return -1; - } - if (statementFingerprintA > statementFingerprintB) { - return 1; - } - return 0; +export function filterByTimeScale( + diagnostics: StatementDiagnosticsReport[], + ts: TimeScale, +): StatementDiagnosticsReport[] { + const [start, end] = toDateRange(ts); + return diagnostics.filter( + diag => + start.isSameOrBefore(moment(diag.requested_at)) && + end.isSameOrAfter(moment(diag.requested_at)), + ); } diff --git a/pkg/ui/workspaces/cluster-ui/src/statementDetails/diagnostics/diagnosticsView.module.scss b/pkg/ui/workspaces/cluster-ui/src/statementDetails/diagnostics/diagnosticsView.module.scss index 43ec20d9a534..572c4993bd21 100644 --- a/pkg/ui/workspaces/cluster-ui/src/statementDetails/diagnostics/diagnosticsView.module.scss +++ b/pkg/ui/workspaces/cluster-ui/src/statementDetails/diagnostics/diagnosticsView.module.scss @@ -4,7 +4,7 @@ display: flex; flex-direction: column; - &__title { + &__header { display: flex; flex-direction: row; justify-content: space-between; @@ -79,3 +79,19 @@ margin-right: $spacing-x-small; } } + +.column-size-medium { + width: 230px; +} + +.column-size-small { + width: 140px; +} + +.sorted-table { + width: 100%; +} + +.margin-bottom { + margin-bottom: $spacing-smaller; +} diff --git a/pkg/ui/workspaces/cluster-ui/src/statementDetails/diagnostics/diagnosticsView.spec.tsx b/pkg/ui/workspaces/cluster-ui/src/statementDetails/diagnostics/diagnosticsView.spec.tsx index abc206afd9c6..8da776859fde 100644 --- a/pkg/ui/workspaces/cluster-ui/src/statementDetails/diagnostics/diagnosticsView.spec.tsx +++ b/pkg/ui/workspaces/cluster-ui/src/statementDetails/diagnostics/diagnosticsView.spec.tsx @@ -15,24 +15,31 @@ import { MemoryRouter } from "react-router-dom"; import { Button } from "@cockroachlabs/ui-components"; import { DiagnosticsView } from "./diagnosticsView"; -import { Table } from "src/table"; import { TestStoreProvider } from "src/test-utils"; import { StatementDiagnosticsReport } from "../../api"; import moment from "moment-timezone"; +import { SortedTable } from "src/sortedtable"; const activateDiagnosticsRef = { current: { showModalFor: jest.fn() } }; +const ts = { + windowSize: moment.duration(20, "day"), + sampleSize: moment.duration(5, "minutes"), + fixedWindowEnd: moment.utc("2023.01.5"), + key: "Custom", +}; +const mockSetTimeScale = jest.fn(); function generateDiagnosticsRequest( extendObject: Partial = {}, ): StatementDiagnosticsReport { - const requestedAt = moment.now(); + const requestedAt = moment("2023-01-01 00:00:00"); const report: StatementDiagnosticsReport = { id: "124354678574635", statement_fingerprint: "SELECT * FROM table", completed: true, requested_at: moment(requestedAt), min_execution_latency: moment.duration(10), - expires_at: moment(requestedAt + 10), + expires_at: moment("2023-01-01 00:00:10"), }; Object.assign(report, extendObject); return report; @@ -52,6 +59,8 @@ describe("DiagnosticsView", () => { hasData={false} diagnosticsReports={[]} dismissAlertMessage={() => {}} + currentScale={ts} + onChangeTimeScale={mockSetTimeScale} /> , ); @@ -81,13 +90,15 @@ describe("DiagnosticsView", () => { hasData={true} diagnosticsReports={diagnosticsRequests} dismissAlertMessage={() => {}} + currentScale={ts} + onChangeTimeScale={mockSetTimeScale} /> , ); }); it("renders Table component when diagnostics data is provided", () => { - assert.isTrue(wrapper.find(Table).exists()); + assert.isTrue(wrapper.find(SortedTable).exists()); }); it("opens the statement diagnostics modal when Activate button is clicked", () => { @@ -113,6 +124,8 @@ describe("DiagnosticsView", () => { hasData={true} diagnosticsReports={diagnosticsRequests} dismissAlertMessage={() => {}} + currentScale={ts} + onChangeTimeScale={mockSetTimeScale} /> , ); @@ -135,6 +148,8 @@ describe("DiagnosticsView", () => { hasData={true} diagnosticsReports={diagnosticsRequests} dismissAlertMessage={() => {}} + currentScale={ts} + onChangeTimeScale={mockSetTimeScale} /> , ); diff --git a/pkg/ui/workspaces/cluster-ui/src/statementDetails/diagnostics/diagnosticsView.tsx b/pkg/ui/workspaces/cluster-ui/src/statementDetails/diagnostics/diagnosticsView.tsx index 0ceea0acaa82..c4a24fc9ed3c 100644 --- a/pkg/ui/workspaces/cluster-ui/src/statementDetails/diagnostics/diagnosticsView.tsx +++ b/pkg/ui/workspaces/cluster-ui/src/statementDetails/diagnostics/diagnosticsView.tsx @@ -14,29 +14,30 @@ import moment from "moment-timezone"; import classnames from "classnames/bind"; import { Button, Icon } from "@cockroachlabs/ui-components"; import { Button as CancelButton } from "src/button"; -import { Text, TextTypes } from "src/text"; -import { Table, ColumnsConfig } from "src/table"; import { SummaryCard } from "src/summaryCard"; import { ActivateDiagnosticsModalRef, DiagnosticStatusBadge, } from "src/statementsDiagnostics"; import emptyListResultsImg from "src/assets/emptyState/empty-list-results.svg"; -import { - getDiagnosticsStatus, - sortByCompletedField, - sortByRequestedAtField, -} from "./diagnosticsUtils"; +import { filterByTimeScale, getDiagnosticsStatus } from "./diagnosticsUtils"; import { EmptyTable } from "src/empty"; import styles from "./diagnosticsView.module.scss"; import { getBasePath, StatementDiagnosticsReport } from "../../api"; import { DATE_FORMAT_24_UTC } from "../../util"; +import { + TimeScale, + timeScale1hMinOptions, + TimeScaleDropdown, +} from "src/timeScaleDropdown"; +import { ColumnDescriptor, SortedTable, SortSetting } from "src/sortedtable"; export interface DiagnosticsViewStateProps { hasData: boolean; diagnosticsReports: StatementDiagnosticsReport[]; showDiagnosticsViewLink?: boolean; activateDiagnosticsRef: React.RefObject; + currentScale: TimeScale; } export interface DiagnosticsViewDispatchProps { @@ -48,6 +49,7 @@ export interface DiagnosticsViewDispatchProps { columnTitle: string, ascending: boolean, ) => void; + onChangeTimeScale: (ts: TimeScale) => void; } export interface DiagnosticsViewOwnProps { @@ -59,9 +61,7 @@ export type DiagnosticsViewProps = DiagnosticsViewOwnProps & DiagnosticsViewDispatchProps; interface DiagnosticsViewState { - traces: { - [diagnosticsId: string]: string; - }; + sortSetting: SortSetting; } const cx = classnames.bind(styles); @@ -111,23 +111,33 @@ export class DiagnosticsView extends React.Component< DiagnosticsViewProps, DiagnosticsViewState > { - columns: ColumnsConfig = [ + constructor(props: DiagnosticsViewProps) { + super(props); + this.state = { + sortSetting: { + ascending: true, + columnTitle: "activatedOn", + }, + }; + } + + columns: ColumnDescriptor[] = [ { - key: "activatedOn", + name: "activatedOn", title: "Activated on", - sorter: sortByRequestedAtField, - defaultSortOrder: "descend", - render: (_text, record) => { - return moment.utc(record.requested_at).format(DATE_FORMAT_24_UTC); - }, + hideTitleUnderline: true, + cell: (diagnostic: StatementDiagnosticsReport) => + moment.utc(diagnostic.requested_at).format(DATE_FORMAT_24_UTC), + sort: (diagnostic: StatementDiagnosticsReport) => + moment(diagnostic.requested_at)?.unix(), }, { - key: "status", + name: "status", title: "Status", - sorter: sortByCompletedField, - width: "160px", - render: (_text, record) => { - const status = getDiagnosticsStatus(record); + hideTitleUnderline: true, + className: cx("column-size-small"), + cell: (diagnostic: StatementDiagnosticsReport) => { + const status = getDiagnosticsStatus(diagnostic); return ( ); }, + sort: (diagnostic: StatementDiagnosticsReport) => + String(diagnostic.completed), }, { - key: "actions", + name: "actions", title: "", - sorter: false, - width: "160px", - render: (() => { - const { - onDownloadDiagnosticBundleClick, - onDiagnosticCancelRequestClick, - } = this.props; - return (_text: string, record: StatementDiagnosticsReport) => { - if (record.completed) { - return ( -
- -
- ); - } + hideTitleUnderline: true, + className: cx("column-size-medium"), + cell: (diagnostic: StatementDiagnosticsReport) => { + if (diagnostic.completed) { return (
- - onDiagnosticCancelRequestClick && - onDiagnosticCancelRequestClick(record) + this.props.onDownloadDiagnosticBundleClick && + this.props.onDownloadDiagnosticBundleClick( + diagnostic.statement_fingerprint, + ) } + className={cx("download-bundle-button")} > - Cancel request - + + Bundle (.zip) +
); - }; - })(), + } + return ( +
+ + this.props.onDiagnosticCancelRequestClick && + this.props.onDiagnosticCancelRequestClick(diagnostic) + } + > + Cancel request + +
+ ); + }, + sort: (diagnostic: StatementDiagnosticsReport) => + String(diagnostic.completed), }, ]; @@ -200,10 +206,16 @@ export class DiagnosticsView extends React.Component< this.props.dismissAlertMessage(); } - onSortingChange = (columnName: string, ascending: boolean): void => { + onSortingChange = (ss: SortSetting): void => { if (this.props.onSortingChange) { - this.props.onSortingChange("Diagnostics", columnName, ascending); + this.props.onSortingChange("Diagnostics", ss.columnTitle, ss.ascending); } + this.setState({ + sortSetting: { + ascending: ss.ascending, + columnTitle: ss.columnTitle, + }, + }); }; render(): React.ReactElement { @@ -213,29 +225,47 @@ export class DiagnosticsView extends React.Component< showDiagnosticsViewLink, statementFingerprint, activateDiagnosticsRef, + currentScale, + onChangeTimeScale, } = this.props; const readyToRequestDiagnostics = diagnosticsReports.every( diagnostic => diagnostic.completed, ); - const dataSource = diagnosticsReports.map((diagnosticsReport, idx) => ({ - ...diagnosticsReport, - key: idx, - })); + const dataSource = filterByTimeScale( + diagnosticsReports.map((diagnosticsReport, idx) => ({ + ...diagnosticsReport, + key: idx, + })), + currentScale, + ); if (!hasData) { return ( - - - + <> + + + + + ); } return ( - -
- Statement diagnostics + <> +
+ {readyToRequestDiagnostics && ( )}
- {showDiagnosticsViewLink && (
@@ -265,7 +298,7 @@ export class DiagnosticsView extends React.Component<
)} - + ); } } diff --git a/pkg/ui/workspaces/cluster-ui/src/statementDetails/statementDetails.tsx b/pkg/ui/workspaces/cluster-ui/src/statementDetails/statementDetails.tsx index c29d8bdca885..45afb52e0359 100644 --- a/pkg/ui/workspaces/cluster-ui/src/statementDetails/statementDetails.tsx +++ b/pkg/ui/workspaces/cluster-ui/src/statementDetails/statementDetails.tsx @@ -89,6 +89,7 @@ import { makeInsightsColumns, } from "../insightsTable/insightsTable"; import { CockroachCloudContext } from "../contexts"; +import { filterByTimeScale } from "./diagnostics/diagnosticsUtils"; type StatementDetailsResponse = cockroach.server.serverpb.StatementDetailsResponse; @@ -463,7 +464,12 @@ export class StatementDetails extends React.Component< ( -
- -
+ <> + + + + + +
+ +
+ ); renderNoDataWithTimeScaleAndSqlBoxTabContent = ( @@ -870,15 +887,17 @@ export class StatementDetails extends React.Component< return this.renderNoDataTabContent(); } + const fingerprint = + this.props.statementDetails?.statement?.metadata?.query.length === 0 + ? this.state.formattedQuery + : this.props.statementDetails?.statement?.metadata?.query; return ( this.props.onDiagnosticCancelRequest(report) @@ -887,6 +906,8 @@ export class StatementDetails extends React.Component< this.props.uiConfig?.showStatementDiagnosticsLink } onSortingChange={this.props.onSortingChange} + currentScale={this.props.timeScale} + onChangeTimeScale={this.props.onTimeScaleChange} /> ); };