diff --git a/pkg/ui/workspaces/cluster-ui/src/sql/sqlhighlight.module.scss b/pkg/ui/workspaces/cluster-ui/src/sql/sqlhighlight.module.scss index 820715c912d6..cffe2188c45b 100644 --- a/pkg/ui/workspaces/cluster-ui/src/sql/sqlhighlight.module.scss +++ b/pkg/ui/workspaces/cluster-ui/src/sql/sqlhighlight.module.scss @@ -27,7 +27,7 @@ font-family: SFMono-Semibold; font-size: 14px; line-height: 1.57; - white-space: pre-line; + white-space: pre; word-wrap: break-word; span { diff --git a/pkg/ui/workspaces/cluster-ui/src/statementDetails/planDetails/index.ts b/pkg/ui/workspaces/cluster-ui/src/statementDetails/planDetails/index.ts new file mode 100644 index 000000000000..36a26ec6363c --- /dev/null +++ b/pkg/ui/workspaces/cluster-ui/src/statementDetails/planDetails/index.ts @@ -0,0 +1,11 @@ +// Copyright 2022 The Cockroach Authors. +// +// Use of this software is governed by the Business Source License +// included in the file licenses/BSL.txt. +// +// As of the Change Date specified in that file, in accordance with +// the Business Source License, use of this software will be governed +// by the Apache License, Version 2.0, included in the file +// licenses/APL.txt. + +export * from "./planDetails"; diff --git a/pkg/ui/workspaces/cluster-ui/src/statementDetails/planDetails/planDetails.tsx b/pkg/ui/workspaces/cluster-ui/src/statementDetails/planDetails/planDetails.tsx new file mode 100644 index 000000000000..bc88de2f978d --- /dev/null +++ b/pkg/ui/workspaces/cluster-ui/src/statementDetails/planDetails/planDetails.tsx @@ -0,0 +1,76 @@ +// Copyright 2022 The Cockroach Authors. +// +// Use of this software is governed by the Business Source License +// included in the file licenses/BSL.txt. +// +// As of the Change Date specified in that file, in accordance with +// the Business Source License, use of this software will be governed +// by the Apache License, Version 2.0, included in the file +// licenses/APL.txt. + +import React, { useState } from "react"; +import { Helmet } from "react-helmet"; +import { ArrowLeft } from "@cockroachlabs/icons"; +import { + PlansSortedTable, + makeExplainPlanColumns, + PlanHashStats, +} from "./plansTable"; +import { Button } from "../../button"; +import { SqlBox } from "../../sql"; + +interface PlanDetailsProps { + plans: PlanHashStats[]; +} + +export function PlanDetails({ plans }: PlanDetailsProps): React.ReactElement { + const [plan, setPlan] = useState(null); + const handleDetails = (plan: PlanHashStats): void => { + setPlan(plan); + }; + const backToPlanTable = (): void => { + setPlan(null); + }; + + if (plan) { + return renderExplainPlan(plan, backToPlanTable); + } else { + return renderPlanTable(plans, handleDetails); + } +} + +function renderPlanTable( + plans: PlanHashStats[], + handleDetails: (plan: PlanHashStats) => void, +): React.ReactElement { + const columns = makeExplainPlanColumns(handleDetails); + return ( + + ); +} + +function renderExplainPlan( + plan: PlanHashStats, + backToPlanTable: () => void, +): React.ReactElement { + return ( +
+ + + +
+ ); +} diff --git a/pkg/ui/workspaces/cluster-ui/src/statementDetails/planDetails/plansTable.tsx b/pkg/ui/workspaces/cluster-ui/src/statementDetails/planDetails/plansTable.tsx new file mode 100644 index 000000000000..5885bd59581b --- /dev/null +++ b/pkg/ui/workspaces/cluster-ui/src/statementDetails/planDetails/plansTable.tsx @@ -0,0 +1,140 @@ +// Copyright 2022 The Cockroach Authors. +// +// Use of this software is governed by the Business Source License +// included in the file licenses/BSL.txt. +// +// As of the Change Date specified in that file, in accordance with +// the Business Source License, use of this software will be governed +// by the Apache License, Version 2.0, included in the file +// licenses/APL.txt. + +import React from "react"; +import { ColumnDescriptor, SortedTable } from "src/sortedtable"; +import { Tooltip } from "@cockroachlabs/ui-components"; +import { cockroach } from "@cockroachlabs/crdb-protobuf-client"; +import { + Duration, + formatNumberForDisplay, + longToInt, + TimestampToMoment, +} from "../../util"; + +export type PlanHashStats = cockroach.server.serverpb.StatementDetailsResponse.ICollectedStatementGroupedByPlanHash; +export class PlansSortedTable extends SortedTable {} + +const planDetailsColumnLabels = { + planID: "Plan ID", + last_exec_time: "Last Execution Time", + avg_exec_time: "Average Execution Time", + exec_count: "Execution Count", + avg_rows_read: "Average Rows Read", +}; +export type PlanDetailsTableColumnKeys = keyof typeof planDetailsColumnLabels; + +type PlanDetailsTableTitleType = { + [key in PlanDetailsTableColumnKeys]: () => JSX.Element; +}; + +export const planDetailsTableTitles: PlanDetailsTableTitleType = { + planID: () => { + return ( + + {planDetailsColumnLabels.planID} + + ); + }, + last_exec_time: () => { + return ( + + {planDetailsColumnLabels.last_exec_time} + + ); + }, + avg_exec_time: () => { + return ( + + {planDetailsColumnLabels.avg_exec_time} + + ); + }, + exec_count: () => { + return ( + + {planDetailsColumnLabels.exec_count} + + ); + }, + avg_rows_read: () => { + return ( + + {planDetailsColumnLabels.avg_rows_read} + + ); + }, +}; + +export function makeExplainPlanColumns( + handleDetails: (plan: PlanHashStats) => void, +): ColumnDescriptor[] { + const duration = (v: number) => Duration(v * 1e9); + return [ + { + name: "planID", + title: planDetailsTableTitles.planID(), + cell: (item: PlanHashStats) => ( + handleDetails(item)}>{longToInt(item.plan_hash)} + ), + sort: (item: PlanHashStats) => longToInt(item.plan_hash), + alwaysShow: true, + }, + { + name: "last_exec_time", + title: planDetailsTableTitles.last_exec_time(), + cell: (item: PlanHashStats) => + TimestampToMoment(item.stats.last_exec_timestamp).format( + "MMM DD, YYYY HH:MM", + ), + sort: (item: PlanHashStats) => + TimestampToMoment(item.stats.last_exec_timestamp).unix(), + }, + { + name: "avg_exec_time", + title: planDetailsTableTitles.avg_exec_time(), + cell: (item: PlanHashStats) => + formatNumberForDisplay(item.stats.run_lat.mean, duration), + sort: (item: PlanHashStats) => item.stats.run_lat.mean, + }, + { + name: "exec_count", + title: planDetailsTableTitles.exec_count(), + cell: (item: PlanHashStats) => longToInt(item.stats.count), + sort: (item: PlanHashStats) => longToInt(item.stats.count), + }, + { + name: "avg_rows_read", + title: planDetailsTableTitles.avg_rows_read(), + cell: (item: PlanHashStats) => longToInt(item.stats.rows_read.mean), + sort: (item: PlanHashStats) => longToInt(item.stats.rows_read.mean), + }, + ]; +} diff --git a/pkg/ui/workspaces/cluster-ui/src/statementDetails/statementDetails.fixture.ts b/pkg/ui/workspaces/cluster-ui/src/statementDetails/statementDetails.fixture.ts index 8254f7a9eced..4e70452dbdcf 100644 --- a/pkg/ui/workspaces/cluster-ui/src/statementDetails/statementDetails.fixture.ts +++ b/pkg/ui/workspaces/cluster-ui/src/statementDetails/statementDetails.fixture.ts @@ -536,8 +536,8 @@ export const getStatementDetailsPropsFixture = (): StatementDetailsProps => ({ statements_per_plan_hash: [ { stats: { - count: new Long(5), - first_attempt_count: new Long(5), + count: new Long(3), + first_attempt_count: new Long(3), max_retries: new Long(0), legacy_last_err: "", legacy_last_err_redacted: "", @@ -628,6 +628,100 @@ export const getStatementDetailsPropsFixture = (): StatementDetailsProps => ({ explain_plan: "• virtual table\n table: @primary", plan_hash: new Long(14192395335876201826), }, + { + stats: { + count: new Long(2), + first_attempt_count: new Long(2), + max_retries: new Long(0), + legacy_last_err: "", + legacy_last_err_redacted: "", + num_rows: { + mean: 6, + squared_diffs: 0, + }, + parse_lat: { + mean: 0.0000876, + squared_diffs: 2.35792e-8, + }, + plan_lat: { + mean: 0.008131, + squared_diffs: 0.00127640837, + }, + run_lat: { + mean: 0.0002796, + squared_diffs: 2.401919999999999e-8, + }, + service_lat: { + mean: 0.008522, + squared_diffs: 0.001298238058, + }, + overhead_lat: { + mean: 0.000023799999999999972, + squared_diffs: 5.492799999999973e-9, + }, + sensitive_info: { + last_err: "", + most_recent_plan_description: { + name: "virtual table", + attrs: [ + { + key: "Table", + value: "node_build_info@primary", + }, + ], + children: [], + }, + most_recent_plan_timestamp: { + seconds: new Long(1614851546), + nanos: 956814000, + }, + }, + bytes_read: { + mean: 0, + squared_diffs: 0, + }, + rows_read: { + mean: 0, + squared_diffs: 0, + }, + rows_written: { + mean: 0, + squared_diffs: 0, + }, + exec_stats: { + count: new Long(5), + network_bytes: { + mean: 0, + squared_diffs: 0, + }, + max_mem_usage: { + mean: 10240, + squared_diffs: 0, + }, + contention_time: { + mean: 0, + squared_diffs: 0, + }, + network_messages: { + mean: 0, + squared_diffs: 0, + }, + max_disk_usage: { + mean: 0, + squared_diffs: 0, + }, + }, + sql_type: "TypeDML", + last_exec_timestamp: { + seconds: Long.fromInt(1599670292), + nanos: 111613000, + }, + nodes: [new Long(1)], + plan_gists: ["Ah0GAg=="], + }, + explain_plan: "• virtual table\n table: @primary\nFULL SCAN", + plan_hash: new Long(14192395335876212345), + }, ], internal_app_name_prefix: "$ internal", toJSON: () => ({}), diff --git a/pkg/ui/workspaces/cluster-ui/src/statementDetails/statementDetails.tsx b/pkg/ui/workspaces/cluster-ui/src/statementDetails/statementDetails.tsx index 83967cec098c..25e77855dec3 100644 --- a/pkg/ui/workspaces/cluster-ui/src/statementDetails/statementDetails.tsx +++ b/pkg/ui/workspaces/cluster-ui/src/statementDetails/statementDetails.tsx @@ -41,7 +41,7 @@ import { Button } from "src/button"; import { SqlBox } from "src/sql"; import { SortSetting } from "src/sortedtable"; import { Tooltip } from "@cockroachlabs/ui-components"; -import { PlanView } from "./planView"; +import { PlanDetails } from "./planDetails"; import { SummaryCard } from "src/summaryCard"; import { latencyBreakdown, @@ -444,6 +444,7 @@ export class StatementDetails extends React.Component< hasViewActivityRedactedRole, } = this.props; const { currentTab } = this.state; + const { statements_per_plan_hash } = this.props.statementDetails; const { stats, app_names, @@ -500,9 +501,7 @@ export class StatementDetails extends React.Component< const regions = unique( (stats.nodes || []).map(node => nodeRegions[node.toString()]), ).sort(); - const explainPlan = - stats.sensitive_info && stats.sensitive_info.most_recent_plan_description; - const explainGlobalProps = { distribution: distSQL, vectorized: vec }; + const duration = (v: number) => Duration(v * 1e9); const hasDiagnosticReports = diagnosticsReports.length > 0; const lastExec = @@ -776,13 +775,13 @@ export class StatementDetails extends React.Component< )} - - - + + + + + +

+