From 512cfe396fe7fe545d90cf5295cefdfc72350338 Mon Sep 17 00:00:00 2001 From: Xin Hao Zhang Date: Tue, 14 Sep 2021 18:01:22 -0400 Subject: [PATCH] ui/cluster-ui: add 'Interval Start Time' column to stmts/txns tables This commit adds the `Interval Start Time (UTC)` column to stmt and txn tables. Statements and transactions are now both grouped by their `aggregated_ts` field in addition to the stmt / fingerprint id. To support viewing of statements grouped by aggregation interval start time, a new query parameter has been added to statement details pages. If `aggregated_ts` is set, it will display the statement details for statements aggregated at that interval, using data from combined statements API response. If unset, we will fetch and show statement data from in-memory stats. Release note (ui change): A new column, 'Interval Start Time (UTC)', has been added to both statement and transaction tables. The column represents the start time in UTC of the stats aggregation interval for a statement. By default, the aggregation interval is 1 hour. A new query parameter has been added to statement details pages. If the search param `aggregated_ts` is set, it will display the statement details for statements aggregated at that interval. If unset, we will display the statement details for the statement collected from the last hour. --- .../cluster-ui/src/api/fetchData.spec.ts | 39 ------- .../cluster-ui/src/api/fetchData.ts | 13 --- .../cluster-ui/src/api/statementsApi.ts | 5 +- .../src/sessions/sessionDetails.tsx | 2 +- .../statementDetails.fixture.ts | 7 ++ .../statementDetails.selectors.ts | 16 ++- .../src/statementDetails/statementDetails.tsx | 27 ++++- .../statementDetailsConnected.ts | 1 + .../statementsPage/statementsPage.fixture.ts | 44 ++++++++ .../statementsPage.selectors.ts | 6 +- .../src/statementsTable/statementsTable.tsx | 10 ++ .../statementsTableContent.tsx | 22 +++- .../src/statsTableUtil/statsTableUtil.tsx | 34 +++++- .../transactionDetails/transactionDetails.tsx | 9 +- .../transactionsPage/transactions.fixture.ts | 26 +++++ .../transactionsPage.selectors.ts | 4 +- .../src/transactionsPage/transactionsPage.tsx | 31 +++-- .../src/transactionsPage/utils.spec.ts | 16 ++- .../cluster-ui/src/transactionsPage/utils.ts | 63 ++++++++--- .../transactionsCells/transactionsCells.tsx | 57 +--------- .../transactionsTable/transactionsTable.tsx | 33 +++--- .../cluster-ui/src/util/appStats/appStats.ts | 18 +-- .../cluster-ui/src/util/constants.ts | 1 + .../workspaces/cluster-ui/src/util/convert.ts | 15 +++ .../cluster-ui/src/util/query/query.spec.ts | 30 ++++- .../cluster-ui/src/util/query/query.ts | 26 +++-- .../src/redux/statements/statementsSagas.ts | 1 + .../db-console/src/util/api.spec.ts | 106 ++---------------- pkg/ui/workspaces/db-console/src/util/api.ts | 16 +-- .../db-console/src/util/appStats.ts | 7 +- .../db-console/src/util/constants.ts | 1 + .../workspaces/db-console/src/util/convert.ts | 15 +++ .../db-console/src/util/query.spec.ts | 99 +++++++++++++++- .../workspaces/db-console/src/util/query.ts | 24 ++-- .../src/views/statements/statementDetails.tsx | 21 +++- .../statements/statementsPage.fixture.ts | 44 ++++++++ .../src/views/statements/statementsPage.tsx | 5 +- .../views/transactions/transactionsPage.tsx | 3 +- 38 files changed, 572 insertions(+), 325 deletions(-) delete mode 100644 pkg/ui/workspaces/cluster-ui/src/api/fetchData.spec.ts diff --git a/pkg/ui/workspaces/cluster-ui/src/api/fetchData.spec.ts b/pkg/ui/workspaces/cluster-ui/src/api/fetchData.spec.ts deleted file mode 100644 index 1bd6eab700e9..000000000000 --- a/pkg/ui/workspaces/cluster-ui/src/api/fetchData.spec.ts +++ /dev/null @@ -1,39 +0,0 @@ -// Copyright 2021 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 { propsToQueryString } from "./fetchData"; - -describe("fetchData functions", () => { - describe("propsToQueryString", () => { - it("creates query string from object", () => { - const obj = { - start: 100, - end: 200, - strParam: "hello", - bool: false, - }; - const expected = "start=100&end=200&strParam=hello&bool=false"; - const res = propsToQueryString(obj); - expect(res).toEqual(expected); - }); - - it("skips entries with nullish values", () => { - const obj = { - start: 100, - end: 200, - strParam: null as any, - hello: undefined as any, - }; - const expected = "start=100&end=200"; - const res = propsToQueryString(obj); - expect(res).toEqual(expected); - }); - }); -}); diff --git a/pkg/ui/workspaces/cluster-ui/src/api/fetchData.ts b/pkg/ui/workspaces/cluster-ui/src/api/fetchData.ts index b0ecc8c5c2ee..4669c4782f1d 100644 --- a/pkg/ui/workspaces/cluster-ui/src/api/fetchData.ts +++ b/pkg/ui/workspaces/cluster-ui/src/api/fetchData.ts @@ -11,7 +11,6 @@ import { cockroach } from "@cockroachlabs/crdb-protobuf-client"; import { RequestError } from "../util"; import { getBasePath } from "./basePath"; -import { stringify } from "querystring"; interface ProtoBuilder< P extends ConstructorType, @@ -30,18 +29,6 @@ export function toArrayBuffer(encodedRequest: Uint8Array): ArrayBuffer { ); } -// propsToQueryString is a helper function that converts a set of object -// properties to a query string -// - keys with null or undefined values will be skipped -// - non-string values will be toString'd -export function propsToQueryString(props: { [k: string]: any }) { - const params = new URLSearchParams(); - Object.entries(props).forEach( - ([k, v]: [string, any]) => v != null && params.set(k, v.toString()), - ); - return params.toString(); -} - /** * @param RespBuilder expects protobuf stub class to build decode response; * @param path relative URL path for requested resource; diff --git a/pkg/ui/workspaces/cluster-ui/src/api/statementsApi.ts b/pkg/ui/workspaces/cluster-ui/src/api/statementsApi.ts index 4eff1f9acf3d..1c500dd562ba 100644 --- a/pkg/ui/workspaces/cluster-ui/src/api/statementsApi.ts +++ b/pkg/ui/workspaces/cluster-ui/src/api/statementsApi.ts @@ -9,7 +9,8 @@ // licenses/APL.txt. import { cockroach } from "@cockroachlabs/crdb-protobuf-client"; -import { fetchData, propsToQueryString } from "src/api"; +import { fetchData } from "src/api"; +import { propsToQueryString } from "src/util"; const STATEMENTS_PATH = "/_status/statements"; @@ -28,7 +29,7 @@ export const getCombinedStatements = ( const queryStr = propsToQueryString({ start: req.start.toInt(), end: req.end.toInt(), - combined: true, + combined: req.combined, }); return fetchData( cockroach.server.serverpb.StatementsResponse, diff --git a/pkg/ui/workspaces/cluster-ui/src/sessions/sessionDetails.tsx b/pkg/ui/workspaces/cluster-ui/src/sessions/sessionDetails.tsx index 78ced333c249..3d7eb099a3ef 100644 --- a/pkg/ui/workspaces/cluster-ui/src/sessions/sessionDetails.tsx +++ b/pkg/ui/workspaces/cluster-ui/src/sessions/sessionDetails.tsx @@ -14,7 +14,7 @@ import { sessionAttr } from "src/util/constants"; import { Helmet } from "react-helmet"; import { Loading } from "../loading"; import _ from "lodash"; -import { Link, RouteComponentProps, withRouter } from "react-router-dom"; +import { Link, RouteComponentProps } from "react-router-dom"; import { SessionInfo } from "./sessionsTable"; 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 2245fc3e7470..ea4b584ea7e7 100644 --- a/pkg/ui/workspaces/cluster-ui/src/statementDetails/statementDetails.fixture.ts +++ b/pkg/ui/workspaces/cluster-ui/src/statementDetails/statementDetails.fixture.ts @@ -117,6 +117,8 @@ const statementStats: any = { exec_stats: execStats, }; +const aggregatedTs = Date.parse("Sep 15 2021 01:00:00 GMT") * 1e-3; + export const getStatementDetailsPropsFixture = (): StatementDetailsProps => ({ history, location: { @@ -144,6 +146,7 @@ export const getStatementDetailsPropsFixture = (): StatementDetailsProps => ({ byNode: [ { label: "4", + aggregatedTs, implicitTxn: true, database: "defaultdb", fullScan: true, @@ -151,6 +154,7 @@ export const getStatementDetailsPropsFixture = (): StatementDetailsProps => ({ }, { label: "3", + aggregatedTs, implicitTxn: true, database: "defaultdb", fullScan: true, @@ -158,6 +162,7 @@ export const getStatementDetailsPropsFixture = (): StatementDetailsProps => ({ }, { label: "2", + aggregatedTs, implicitTxn: true, database: "defaultdb", fullScan: true, @@ -165,6 +170,7 @@ export const getStatementDetailsPropsFixture = (): StatementDetailsProps => ({ }, { label: "1", + aggregatedTs, implicitTxn: true, database: "defaultdb", fullScan: true, @@ -208,6 +214,7 @@ export const getStatementDetailsPropsFixture = (): StatementDetailsProps => ({ "4": "gcp-europe-west1", }, refreshStatements: noop, + invalidateStatements: noop, refreshStatementDiagnosticsRequests: noop, refreshNodes: noop, refreshNodesLiveness: noop, diff --git a/pkg/ui/workspaces/cluster-ui/src/statementDetails/statementDetails.selectors.ts b/pkg/ui/workspaces/cluster-ui/src/statementDetails/statementDetails.selectors.ts index f57a1a6ea345..7691b7f09b62 100644 --- a/pkg/ui/workspaces/cluster-ui/src/statementDetails/statementDetails.selectors.ts +++ b/pkg/ui/workspaces/cluster-ui/src/statementDetails/statementDetails.selectors.ts @@ -10,6 +10,7 @@ import { createSelector } from "@reduxjs/toolkit"; import { RouteComponentProps, match as Match } from "react-router-dom"; +import { Location } from "history"; import _ from "lodash"; import { AppState } from "../store"; import { @@ -24,12 +25,15 @@ import { databaseAttr, StatementStatistics, statementKey, + aggregatedTsAttr, + queryByName, } from "../util"; import { AggregateStatistics } from "../statementsTable"; import { Fraction } from "./statementDetails"; interface StatementDetailsData { nodeId: number; + aggregatedTs: number; implicitTxn: boolean; fullScan: boolean; database: string; @@ -46,6 +50,7 @@ function coalesceNodeStats( if (!(key in statsKey)) { statsKey[key] = { nodeId: stmt.node_id, + aggregatedTs: stmt.aggregated_ts, implicitTxn: stmt.implicit_txn, fullScan: stmt.full_scan, database: stmt.database, @@ -59,6 +64,7 @@ function coalesceNodeStats( const stmt = statsKey[key]; return { label: stmt.nodeId.toString(), + aggregatedTs: stmt.aggregatedTs, implicitTxn: stmt.implicitTxn, fullScan: stmt.fullScan, database: stmt.database, @@ -87,15 +93,19 @@ function fractionMatching( function filterByRouterParamsPredicate( match: Match, + location: Location, internalAppNamePrefix: string, ): (stat: ExecutionStatistics) => boolean { const statement = getMatchParamByName(match, statementAttr); const implicitTxn = getMatchParamByName(match, implicitTxnAttr) === "true"; const database = getMatchParamByName(match, databaseAttr); + const aggregatedTs = queryByName(location, aggregatedTsAttr); let app = getMatchParamByName(match, appAttr); const filterByKeys = (stmt: ExecutionStatistics) => stmt.statement === statement && + aggregatedTs == null && + stmt.aggregated_ts.toString() === aggregatedTs && stmt.implicit_txn === implicitTxn && (stmt.database === database || database === null); @@ -129,7 +139,11 @@ export const selectStatement = createSelector( const flattened = flattenStatementStats(statements); const results = _.filter( flattened, - filterByRouterParamsPredicate(props.match, internalAppNamePrefix), + filterByRouterParamsPredicate( + props.match, + props.location, + internalAppNamePrefix, + ), ); const statement = getMatchParamByName(props.match, statementAttr); return { diff --git a/pkg/ui/workspaces/cluster-ui/src/statementDetails/statementDetails.tsx b/pkg/ui/workspaces/cluster-ui/src/statementDetails/statementDetails.tsx index 54b74642aa2d..3cf33754e689 100644 --- a/pkg/ui/workspaces/cluster-ui/src/statementDetails/statementDetails.tsx +++ b/pkg/ui/workspaces/cluster-ui/src/statementDetails/statementDetails.tsx @@ -34,6 +34,7 @@ import { formatNumberForDisplay, calculateTotalWorkload, unique, + queryByName, } from "src/util"; import { Loading } from "src/loading"; import { Button } from "src/button"; @@ -62,6 +63,7 @@ import { NodeSummaryStats } from "../nodes"; import { UIConfigState } from "../store/uiConfig"; import moment, { Moment } from "moment"; import { StatementsRequest } from "src/api/statementsApi"; +import { aggregatedTsAttr } from "../util/constants"; const { TabPane } = Tabs; @@ -128,6 +130,7 @@ export type NodesSummary = { export interface StatementDetailsDispatchProps { refreshStatements: (req?: StatementsRequest) => void; + invalidateStatements: () => void; refreshStatementDiagnosticsRequests: () => void; refreshNodes: () => void; refreshNodesLiveness: () => void; @@ -164,7 +167,15 @@ const summaryCardStylesCx = classNames.bind(summaryCardStyles); function statementsRequestFromProps( props: StatementDetailsProps, ): cockroach.server.serverpb.StatementsRequest | null { - if (props.isTenant || props.dateRange == null) return null; + // If there was no aggregated_ts requested in the search parameters, we show + // the latest statement from in-memory stats. This is mostly relevant for displaying + // statement details from active queries from the session details page. + const aggregatedTs = queryByName(props.location, aggregatedTsAttr); + if (aggregatedTs == null) + return new cockroach.server.serverpb.StatementsRequest({ + combined: false, + }); + return new cockroach.server.serverpb.StatementsRequest({ combined: true, start: Long.fromNumber(props.dateRange[0].unix()), @@ -351,6 +362,10 @@ export class StatementDetails extends React.Component< }; componentDidMount() { + const req = statementsRequestFromProps(this.props); + if (!req.combined) { + this.props.invalidateStatements(); + } this.refreshStatements(); if (!this.props.isTenant) { this.props.refreshStatementDiagnosticsRequests(); @@ -368,6 +383,16 @@ export class StatementDetails extends React.Component< } } + componentWillUnmount() { + // If we had to fetch in-memory statement statistics for this statement, + // we should invalidate the statements response in the cache, since the other + // pages will expect combined statements. + const req = statementsRequestFromProps(this.props); + if (!req.combined) { + this.props.invalidateStatements(); + } + } + onTabChange = (tabId: string) => { const { history } = this.props; const searchParams = new URLSearchParams(history.location.search); diff --git a/pkg/ui/workspaces/cluster-ui/src/statementDetails/statementDetailsConnected.ts b/pkg/ui/workspaces/cluster-ui/src/statementDetails/statementDetailsConnected.ts index 982dcabf7181..c7f8872fec15 100644 --- a/pkg/ui/workspaces/cluster-ui/src/statementDetails/statementDetailsConnected.ts +++ b/pkg/ui/workspaces/cluster-ui/src/statementDetails/statementDetailsConnected.ts @@ -63,6 +63,7 @@ const mapDispatchToProps = ( dispatch: Dispatch, ): StatementDetailsDispatchProps => ({ refreshStatements: () => dispatch(statementsActions.refresh()), + invalidateStatements: () => dispatch(statementsActions.invalidated()), refreshStatementDiagnosticsRequests: () => dispatch(statementDiagnosticsActions.refresh()), refreshNodes: () => dispatch(nodesActions.refresh()), diff --git a/pkg/ui/workspaces/cluster-ui/src/statementsPage/statementsPage.fixture.ts b/pkg/ui/workspaces/cluster-ui/src/statementsPage/statementsPage.fixture.ts index e71e8c9696a5..792adbd0c37c 100644 --- a/pkg/ui/workspaces/cluster-ui/src/statementsPage/statementsPage.fixture.ts +++ b/pkg/ui/workspaces/cluster-ui/src/statementsPage/statementsPage.fixture.ts @@ -239,6 +239,8 @@ const diagnosticsReportsInProgress: IStatementDiagnosticsReport[] = [ }, ]; +const aggregatedTs = Date.parse("Sep 15 2021 01:00:00 GMT") * 1e-3; + const statementsPagePropsFixture: StatementsPageProps = { history, location: { @@ -264,6 +266,7 @@ const statementsPagePropsFixture: StatementsPageProps = { { label: "SELECT IFNULL(a, b) FROM (SELECT (SELECT code FROM promo_codes WHERE code > $1 ORDER BY code LIMIT _) AS a, (SELECT code FROM promo_codes ORDER BY code LIMIT _) AS b)", + aggregatedTs, implicitTxn: true, database: "defaultdb", fullScan: false, @@ -271,6 +274,7 @@ const statementsPagePropsFixture: StatementsPageProps = { }, { label: "INSERT INTO vehicles VALUES ($1, $2, __more6__)", + aggregatedTs, implicitTxn: true, database: "defaultdb", fullScan: false, @@ -279,6 +283,7 @@ const statementsPagePropsFixture: StatementsPageProps = { { label: "SELECT IFNULL(a, b) FROM (SELECT (SELECT id FROM users WHERE (city = $1) AND (id > $2) ORDER BY id LIMIT _) AS a, (SELECT id FROM users WHERE city = $1 ORDER BY id LIMIT _) AS b)", + aggregatedTs, implicitTxn: true, database: "defaultdb", fullScan: false, @@ -287,6 +292,7 @@ const statementsPagePropsFixture: StatementsPageProps = { { label: "UPSERT INTO vehicle_location_histories VALUES ($1, $2, now(), $3, $4)", + aggregatedTs, implicitTxn: true, database: "defaultdb", fullScan: false, @@ -294,6 +300,7 @@ const statementsPagePropsFixture: StatementsPageProps = { }, { label: "INSERT INTO user_promo_codes VALUES ($1, $2, $3, now(), _)", + aggregatedTs, implicitTxn: true, database: "defaultdb", fullScan: false, @@ -301,6 +308,7 @@ const statementsPagePropsFixture: StatementsPageProps = { }, { label: "SELECT city, id FROM vehicles WHERE city = $1", + aggregatedTs, implicitTxn: true, database: "defaultdb", fullScan: true, @@ -309,6 +317,7 @@ const statementsPagePropsFixture: StatementsPageProps = { { label: "INSERT INTO rides VALUES ($1, $2, $2, $3, $4, $5, _, now(), _, $6)", + aggregatedTs, implicitTxn: true, database: "defaultdb", fullScan: false, @@ -317,6 +326,7 @@ const statementsPagePropsFixture: StatementsPageProps = { { label: "SELECT IFNULL(a, b) FROM (SELECT (SELECT id FROM vehicles WHERE (city = $1) AND (id > $2) ORDER BY id LIMIT _) AS a, (SELECT id FROM vehicles WHERE city = $1 ORDER BY id LIMIT _) AS b)", + aggregatedTs, implicitTxn: true, database: "defaultdb", fullScan: false, @@ -325,6 +335,7 @@ const statementsPagePropsFixture: StatementsPageProps = { { label: "UPDATE rides SET end_address = $3, end_time = now() WHERE (city = $1) AND (id = $2)", + aggregatedTs, implicitTxn: true, database: "defaultdb", fullScan: false, @@ -332,6 +343,7 @@ const statementsPagePropsFixture: StatementsPageProps = { }, { label: "INSERT INTO users VALUES ($1, $2, __more3__)", + aggregatedTs, implicitTxn: true, database: "defaultdb", fullScan: false, @@ -340,6 +352,7 @@ const statementsPagePropsFixture: StatementsPageProps = { { label: "SELECT count(*) FROM user_promo_codes WHERE ((city = $1) AND (user_id = $2)) AND (code = $3)", + aggregatedTs, implicitTxn: true, database: "defaultdb", fullScan: true, @@ -347,6 +360,7 @@ const statementsPagePropsFixture: StatementsPageProps = { }, { label: "INSERT INTO promo_codes VALUES ($1, $2, __more3__)", + aggregatedTs, implicitTxn: true, database: "defaultdb", fullScan: false, @@ -354,6 +368,7 @@ const statementsPagePropsFixture: StatementsPageProps = { }, { label: "ALTER TABLE users SCATTER FROM (_, _) TO (_, _)", + aggregatedTs, implicitTxn: true, database: "defaultdb", fullScan: false, @@ -362,6 +377,7 @@ const statementsPagePropsFixture: StatementsPageProps = { { label: "ALTER TABLE rides ADD FOREIGN KEY (vehicle_city, vehicle_id) REFERENCES vehicles (city, id)", + aggregatedTs, implicitTxn: true, database: "defaultdb", fullScan: false, @@ -369,6 +385,7 @@ const statementsPagePropsFixture: StatementsPageProps = { }, { label: "SHOW database", + aggregatedTs, implicitTxn: true, database: "defaultdb", fullScan: false, @@ -378,6 +395,7 @@ const statementsPagePropsFixture: StatementsPageProps = { { label: "CREATE TABLE IF NOT EXISTS promo_codes (code VARCHAR NOT NULL, description VARCHAR NULL, creation_time TIMESTAMP NULL, expiration_time TIMESTAMP NULL, rules JSONB NULL, PRIMARY KEY (code ASC))", + aggregatedTs, implicitTxn: true, database: "defaultdb", fullScan: false, @@ -385,6 +403,7 @@ const statementsPagePropsFixture: StatementsPageProps = { }, { label: "ALTER TABLE users SPLIT AT VALUES (_, _)", + aggregatedTs, implicitTxn: true, database: "defaultdb", fullScan: false, @@ -392,6 +411,7 @@ const statementsPagePropsFixture: StatementsPageProps = { }, { label: "ALTER TABLE vehicles SCATTER FROM (_, _) TO (_, _)", + aggregatedTs, implicitTxn: true, database: "defaultdb", fullScan: false, @@ -400,6 +420,7 @@ const statementsPagePropsFixture: StatementsPageProps = { { label: "ALTER TABLE vehicle_location_histories ADD FOREIGN KEY (city, ride_id) REFERENCES rides (city, id)", + aggregatedTs, implicitTxn: true, database: "defaultdb", fullScan: false, @@ -408,6 +429,7 @@ const statementsPagePropsFixture: StatementsPageProps = { { label: 'CREATE TABLE IF NOT EXISTS user_promo_codes (city VARCHAR NOT NULL, user_id UUID NOT NULL, code VARCHAR NOT NULL, "timestamp" TIMESTAMP NULL, usage_count INT8 NULL, PRIMARY KEY (city ASC, user_id ASC, code ASC))', + aggregatedTs, implicitTxn: true, database: "defaultdb", fullScan: false, @@ -415,6 +437,7 @@ const statementsPagePropsFixture: StatementsPageProps = { }, { label: "INSERT INTO users VALUES ($1, $2, __more3__), (__more40__)", + aggregatedTs, implicitTxn: true, database: "defaultdb", fullScan: false, @@ -422,6 +445,7 @@ const statementsPagePropsFixture: StatementsPageProps = { }, { label: "ALTER TABLE rides SCATTER FROM (_, _) TO (_, _)", + aggregatedTs, implicitTxn: true, database: "defaultdb", fullScan: false, @@ -429,6 +453,7 @@ const statementsPagePropsFixture: StatementsPageProps = { }, { label: 'SET CLUSTER SETTING "cluster.organization" = $1', + aggregatedTs, implicitTxn: true, database: "defaultdb", fullScan: false, @@ -437,6 +462,7 @@ const statementsPagePropsFixture: StatementsPageProps = { { label: "ALTER TABLE vehicles ADD FOREIGN KEY (city, owner_id) REFERENCES users (city, id)", + aggregatedTs, implicitTxn: true, database: "defaultdb", fullScan: false, @@ -445,6 +471,7 @@ const statementsPagePropsFixture: StatementsPageProps = { { label: "CREATE TABLE IF NOT EXISTS rides (id UUID NOT NULL, city VARCHAR NOT NULL, vehicle_city VARCHAR NULL, rider_id UUID NULL, vehicle_id UUID NULL, start_address VARCHAR NULL, end_address VARCHAR NULL, start_time TIMESTAMP NULL, end_time TIMESTAMP NULL, revenue DECIMAL(10,2) NULL, PRIMARY KEY (city ASC, id ASC), INDEX rides_auto_index_fk_city_ref_users (city ASC, rider_id ASC), INDEX rides_auto_index_fk_vehicle_city_ref_vehicles (vehicle_city ASC, vehicle_id ASC), CONSTRAINT check_vehicle_city_city CHECK (vehicle_city = city))", + aggregatedTs, implicitTxn: true, database: "defaultdb", fullScan: false, @@ -453,6 +480,7 @@ const statementsPagePropsFixture: StatementsPageProps = { { label: "CREATE TABLE IF NOT EXISTS vehicles (id UUID NOT NULL, city VARCHAR NOT NULL, type VARCHAR NULL, owner_id UUID NULL, creation_time TIMESTAMP NULL, status VARCHAR NULL, current_location VARCHAR NULL, ext JSONB NULL, PRIMARY KEY (city ASC, id ASC), INDEX vehicles_auto_index_fk_city_ref_users (city ASC, owner_id ASC))", + aggregatedTs, implicitTxn: true, database: "defaultdb", fullScan: false, @@ -460,6 +488,7 @@ const statementsPagePropsFixture: StatementsPageProps = { }, { label: "INSERT INTO rides VALUES ($1, $2, __more8__), (__more400__)", + aggregatedTs, implicitTxn: true, database: "defaultdb", fullScan: false, @@ -467,6 +496,7 @@ const statementsPagePropsFixture: StatementsPageProps = { }, { label: "ALTER TABLE vehicles SPLIT AT VALUES (_, _)", + aggregatedTs, implicitTxn: true, database: "defaultdb", fullScan: false, @@ -474,6 +504,7 @@ const statementsPagePropsFixture: StatementsPageProps = { }, { label: "SET sql_safe_updates = _", + aggregatedTs, implicitTxn: true, database: "defaultdb", fullScan: false, @@ -482,6 +513,7 @@ const statementsPagePropsFixture: StatementsPageProps = { { label: "CREATE TABLE IF NOT EXISTS users (id UUID NOT NULL, city VARCHAR NOT NULL, name VARCHAR NULL, address VARCHAR NULL, credit_card VARCHAR NULL, PRIMARY KEY (city ASC, id ASC))", + aggregatedTs, implicitTxn: true, database: "defaultdb", fullScan: false, @@ -490,6 +522,7 @@ const statementsPagePropsFixture: StatementsPageProps = { { label: 'CREATE TABLE IF NOT EXISTS vehicle_location_histories (city VARCHAR NOT NULL, ride_id UUID NOT NULL, "timestamp" TIMESTAMP NOT NULL, lat FLOAT8 NULL, long FLOAT8 NULL, PRIMARY KEY (city ASC, ride_id ASC, "timestamp" ASC))', + aggregatedTs, implicitTxn: true, database: "defaultdb", fullScan: false, @@ -497,6 +530,7 @@ const statementsPagePropsFixture: StatementsPageProps = { }, { label: "SELECT * FROM crdb_internal.node_build_info", + aggregatedTs, implicitTxn: true, database: "defaultdb", fullScan: false, @@ -505,6 +539,7 @@ const statementsPagePropsFixture: StatementsPageProps = { { label: "CREATE DATABASE movr", implicitTxn: true, + aggregatedTs, database: "defaultdb", fullScan: false, stats: statementStats, @@ -513,6 +548,7 @@ const statementsPagePropsFixture: StatementsPageProps = { { label: "SELECT count(*) > _ FROM [SHOW ALL CLUSTER SETTINGS] AS _ (v) WHERE v = _", + aggregatedTs, implicitTxn: true, database: "defaultdb", fullScan: false, @@ -520,6 +556,7 @@ const statementsPagePropsFixture: StatementsPageProps = { }, { label: 'SET CLUSTER SETTING "enterprise.license" = $1', + aggregatedTs, implicitTxn: true, database: "defaultdb", fullScan: false, @@ -528,6 +565,7 @@ const statementsPagePropsFixture: StatementsPageProps = { { label: "ALTER TABLE rides ADD FOREIGN KEY (city, rider_id) REFERENCES users (city, id)", + aggregatedTs, implicitTxn: true, database: "defaultdb", fullScan: false, @@ -536,6 +574,7 @@ const statementsPagePropsFixture: StatementsPageProps = { { label: "ALTER TABLE user_promo_codes ADD FOREIGN KEY (city, user_id) REFERENCES users (city, id)", + aggregatedTs, implicitTxn: true, database: "defaultdb", fullScan: false, @@ -544,6 +583,7 @@ const statementsPagePropsFixture: StatementsPageProps = { { label: "INSERT INTO promo_codes VALUES ($1, $2, __more3__), (__more900__)", + aggregatedTs, implicitTxn: true, database: "defaultdb", fullScan: false, @@ -551,6 +591,7 @@ const statementsPagePropsFixture: StatementsPageProps = { }, { label: "ALTER TABLE rides SPLIT AT VALUES (_, _)", + aggregatedTs, implicitTxn: true, database: "defaultdb", fullScan: false, @@ -558,6 +599,7 @@ const statementsPagePropsFixture: StatementsPageProps = { }, { label: "SELECT value FROM crdb_internal.node_build_info WHERE field = _", + aggregatedTs, implicitTxn: true, database: "defaultdb", fullScan: false, @@ -566,6 +608,7 @@ const statementsPagePropsFixture: StatementsPageProps = { { label: "INSERT INTO vehicle_location_histories VALUES ($1, $2, __more3__), (__more900__)", + aggregatedTs, implicitTxn: true, database: "defaultdb", fullScan: false, @@ -573,6 +616,7 @@ const statementsPagePropsFixture: StatementsPageProps = { }, { label: "INSERT INTO vehicles VALUES ($1, $2, __more6__), (__more10__)", + aggregatedTs, implicitTxn: true, database: "defaultdb", fullScan: false, diff --git a/pkg/ui/workspaces/cluster-ui/src/statementsPage/statementsPage.selectors.ts b/pkg/ui/workspaces/cluster-ui/src/statementsPage/statementsPage.selectors.ts index c77ac67b517b..4e39104e0c49 100644 --- a/pkg/ui/workspaces/cluster-ui/src/statementsPage/statementsPage.selectors.ts +++ b/pkg/ui/workspaces/cluster-ui/src/statementsPage/statementsPage.selectors.ts @@ -33,6 +33,7 @@ import { AggregateStatistics } from "../statementsTable"; type ICollectedStatementStatistics = cockroach.server.serverpb.StatementsResponse.ICollectedStatementStatistics; export interface StatementsSummaryData { statement: string; + aggregatedTs: number; implicitTxn: boolean; fullScan: boolean; database: string; @@ -138,7 +139,8 @@ export const selectStatements = createSelector( props: RouteComponentProps, diagnosticsReportsPerStatement, ): AggregateStatistics[] => { - if (!state.data) { + // State is valid if we successfully fetched data, and the data has not yet been invalidated. + if (!state.data || !state.valid) { return null; } let statements = flattenStatementStats(state.data.statements); @@ -169,6 +171,7 @@ export const selectStatements = createSelector( if (!(key in statsByStatementKey)) { statsByStatementKey[key] = { statement: stmt.statement, + aggregatedTs: stmt.aggregated_ts, implicitTxn: stmt.implicit_txn, fullScan: stmt.full_scan, database: stmt.database, @@ -182,6 +185,7 @@ export const selectStatements = createSelector( const stmt = statsByStatementKey[key]; return { label: stmt.statement, + aggregatedTs: stmt.aggregatedTs, implicitTxn: stmt.implicitTxn, fullScan: stmt.fullScan, database: stmt.database, diff --git a/pkg/ui/workspaces/cluster-ui/src/statementsTable/statementsTable.tsx b/pkg/ui/workspaces/cluster-ui/src/statementsTable/statementsTable.tsx index aa31450813f2..0218c99c38c9 100644 --- a/pkg/ui/workspaces/cluster-ui/src/statementsTable/statementsTable.tsx +++ b/pkg/ui/workspaces/cluster-ui/src/statementsTable/statementsTable.tsx @@ -42,6 +42,7 @@ import { statisticsTableTitles, NodeNames, StatisticType, + formatStartIntervalColumn, } from "../statsTableUtil/statsTableUtil"; type IStatementDiagnosticsReport = cockroach.server.serverpb.IStatementDiagnosticsReport; @@ -87,6 +88,14 @@ function makeCommonColumns( const retryBar = retryBarChart(statements, defaultBarChartOptions); return [ + { + name: "intervalStartTime", + title: statisticsTableTitles.intervalStartTime(statType), + className: cx("statements-table__interval_time"), + cell: (stmt: AggregateStatistics) => + formatStartIntervalColumn(stmt.aggregatedTs), + sort: (stmt: AggregateStatistics) => stmt.aggregatedTs, + }, { name: "executionCount", title: statisticsTableTitles.executionCount(statType), @@ -181,6 +190,7 @@ function makeCommonColumns( export interface AggregateStatistics { // label is either shortStatement (StatementsPage) or nodeId (StatementDetails). label: string; + aggregatedTs: number; implicitTxn: boolean; fullScan: boolean; database: string; diff --git a/pkg/ui/workspaces/cluster-ui/src/statementsTable/statementsTableContent.tsx b/pkg/ui/workspaces/cluster-ui/src/statementsTable/statementsTableContent.tsx index 175111ef2d32..17717942a654 100644 --- a/pkg/ui/workspaces/cluster-ui/src/statementsTable/statementsTableContent.tsx +++ b/pkg/ui/workspaces/cluster-ui/src/statementsTable/statementsTableContent.tsx @@ -22,7 +22,12 @@ import { Dropdown } from "src/dropdown"; import { Button } from "src/button"; import { Tooltip } from "@cockroachlabs/ui-components"; -import { summarize, TimestampToMoment } from "src/util"; +import { + aggregatedTsAttr, + propsToQueryString, + summarize, + TimestampToMoment, +} from "src/util"; import { shortStatement } from "./statementsTable"; import styles from "./statementsTableContent.module.scss"; import { cockroach } from "@cockroachlabs/crdb-protobuf-client"; @@ -41,6 +46,7 @@ export const StatementTableCell = { ) => (stmt: any) => ( { +export const StatementLinkTarget = (props: StatementLinkProps): string => { let base: string; if (props.app && props.app.length > 0) { base = `/statements/${props.app}`; @@ -150,10 +157,17 @@ export const StatementLinkTarget = (props: StatementLinkProps) => { if (props.statementNoConstants) { linkStatement = props.statementNoConstants; } - return `${base}/${encodeURIComponent(linkStatement)}`; + + const searchParams = propsToQueryString({ + [aggregatedTsAttr]: props.aggregatedTs, + }); + + return `${base}/${encodeURIComponent(linkStatement)}?${searchParams}`; }; -export const StatementLink = (props: StatementLinkProps) => { +export const StatementLink = ( + props: StatementLinkProps, +): React.ReactElement => { const summary = summarize(props.statement); const { onClick, statement } = props; const onStatementClick = React.useCallback(() => { diff --git a/pkg/ui/workspaces/cluster-ui/src/statsTableUtil/statsTableUtil.tsx b/pkg/ui/workspaces/cluster-ui/src/statsTableUtil/statsTableUtil.tsx index ffdc1800514d..73d0c211d133 100644 --- a/pkg/ui/workspaces/cluster-ui/src/statsTableUtil/statsTableUtil.tsx +++ b/pkg/ui/workspaces/cluster-ui/src/statsTableUtil/statsTableUtil.tsx @@ -10,6 +10,7 @@ import React from "react"; import { Anchor } from "src/anchor"; +import moment from "moment"; import { Tooltip } from "@cockroachlabs/ui-components"; import { @@ -22,6 +23,7 @@ import { contentionTime, readsAndWrites, } from "src/util"; +import { AggregateStatistics } from "src/statementsTable"; export type NodeNames = { [nodeId: string]: string }; @@ -32,6 +34,7 @@ export const statisticsColumnLabels = { database: "Database", diagnostics: "Diagnostics", executionCount: "Execution Count", + intervalStartTime: "Interval Start Time (UTC)", maxMemUsage: "Max Memory", networkBytes: "Network", regionNodes: "Regions/Nodes", @@ -91,7 +94,7 @@ export function getLabel( // of data the statistics are based on (e.g. statements, transactions, or transactionDetails). The // StatisticType is used to modify the content of the tooltip. export const statisticsTableTitles: StatisticTableTitleType = { - statements: (statType: StatisticType) => { + statements: () => { return ( ); }, + intervalStartTime: () => { + return ( + +

+ The time that the statement execution interval started. By + default, statements are configured to aggregate over an hour + interval. +
+ For example, if a statement is executed at 1:23PM it will fall in + the 1:00PM - 2:00PM time interval. +

+ + } + > + {getLabel("intervalStartTime")} +
+ ); + }, executionCount: (statType: StatisticType) => { let contentModifier = ""; let fingerprintModifier = ""; @@ -603,3 +628,10 @@ export const statisticsTableTitles: StatisticTableTitleType = { ); }, }; + +export function formatStartIntervalColumn(aggregatedTs: number) { + return moment + .unix(aggregatedTs) + .utc() + .format("MMM D, h:mm A"); +} diff --git a/pkg/ui/workspaces/cluster-ui/src/transactionDetails/transactionDetails.tsx b/pkg/ui/workspaces/cluster-ui/src/transactionDetails/transactionDetails.tsx index 1ccbd18ea7fa..a7bfe9d95e8c 100644 --- a/pkg/ui/workspaces/cluster-ui/src/transactionDetails/transactionDetails.tsx +++ b/pkg/ui/workspaces/cluster-ui/src/transactionDetails/transactionDetails.tsx @@ -26,7 +26,6 @@ import { Button } from "../button"; import { tableClasses } from "../transactionsTable/transactionsTableClasses"; import { SqlBox } from "../sql"; import { aggregateStatements } from "../transactionsPage/utils"; -import Long from "long"; import { Loading } from "../loading"; import { SummaryCard } from "../summaryCard"; import { Bytes, Duration, formatNumberForDisplay } from "src/util"; @@ -42,6 +41,7 @@ import { populateRegionNodeForStatements, makeStatementFingerprintColumn, } from "src/statementsTable/statementsTable"; +import { TransactionInfo } from "src/transactionsTable"; const { containerClass } = tableClasses; const cx = classNames.bind(statementsStyles); @@ -58,10 +58,7 @@ interface TransactionDetailsProps { nodeRegions: { [nodeId: string]: string }; transactionStats?: TransactionStats; lastReset?: string | Date; - handleDetails: ( - statementFingerprintIds: Long[] | null, - transactionStats: TransactionStats | null, - ) => void; + handleDetails: (txn?: TransactionInfo) => void; error?: Error | null; resetSQLStats: () => void; isTenant: UIConfigState["isTenant"]; @@ -117,7 +114,7 @@ export class TransactionDetails extends React.Component<