From 8a3f8df890f6f67e433c5b08d3a56bbac0d2b1dc Mon Sep 17 00:00:00 2001 From: Xin Hao Zhang Date: Tue, 10 Aug 2021 20:27:41 -0400 Subject: [PATCH] ui: Add date range selector in stmts and txns pages Resolves #68089, #69474 This commit adds date range state properties and a date range selector component to the DB console's statements and transactions pages. This change is necessary to support surfacing persisted statistics in the DB console. The date range query parameters used by the api to fetch data is stored in localSettings on db console and localStorage in cluster-ui. The default date range is set to 1 hour ago, and is used as the value when user's reset the date range. The user is able to select dates as far back as 1 year ago, this does not mean there is data available. In the future we should determine the date range of available persisted stats and limit the date range picker to show only available data. Release justification: category 4 low risk, high benefit changes to existing functionality Release note (ui change): New date range selector component to the DB console's statements and transactions pages with the ability to show historical data. The default date range is set to 1 hour ago, and is used as the value when user's reset the date range. --- .../cluster-ui/src/api/fetchData.spec.ts | 39 + .../cluster-ui/src/api/fetchData.ts | 13 + .../cluster-ui/src/api/statementsApi.ts | 18 +- .../src/dateRange/dateRange.fixtures.tsx | 16 + .../src/dateRange/dateRange.module.scss | 57 ++ .../src/dateRange/dateRange.stories.tsx | 32 + .../cluster-ui/src/dateRange/dateRange.tsx | 190 +++++ .../cluster-ui/src/dateRange/index.tsx | 11 + .../src/statementDetails/statementDetails.tsx | 27 +- .../statementDetailsConnected.ts | 6 +- .../statementsPage/statementsPage.fixture.ts | 744 +++++++++--------- .../statementsPage/statementsPage.module.scss | 11 + .../statementsPage.selectors.ts | 15 + .../src/statementsPage/statementsPage.tsx | 58 +- .../statementsPageConnected.tsx | 17 +- .../localStorage/localStorage.reducer.ts | 23 +- .../store/localStorage/localStorage.saga.ts | 6 +- .../workspaces/cluster-ui/src/store/sagas.ts | 1 - .../store/statements/statements.reducer.ts | 12 +- .../store/statements/statements.sagas.spec.ts | 20 +- .../src/store/statements/statements.sagas.ts | 46 +- .../transactions/transactions.reducer.ts | 5 +- .../transactions/transactions.sagas.spec.ts | 20 +- .../store/transactions/transactions.sagas.ts | 21 +- .../src/transactionsPage/transactionsPage.tsx | 59 +- .../transactionsPageConnected.tsx | 16 +- .../src/redux/statements/statementsActions.ts | 26 + .../src/redux/statements/statementsSagas.ts | 31 +- .../src/redux/statementsDateRange.ts | 34 + pkg/ui/workspaces/db-console/src/util/api.ts | 10 +- .../src/views/statements/statementDetails.tsx | 3 + .../statements/statementsPage.fixture.ts | 4 + .../src/views/statements/statementsPage.tsx | 14 +- .../views/transactions/transactionsPage.tsx | 12 + 34 files changed, 1217 insertions(+), 400 deletions(-) create mode 100644 pkg/ui/workspaces/cluster-ui/src/api/fetchData.spec.ts create mode 100644 pkg/ui/workspaces/cluster-ui/src/dateRange/dateRange.fixtures.tsx create mode 100644 pkg/ui/workspaces/cluster-ui/src/dateRange/dateRange.module.scss create mode 100644 pkg/ui/workspaces/cluster-ui/src/dateRange/dateRange.stories.tsx create mode 100644 pkg/ui/workspaces/cluster-ui/src/dateRange/dateRange.tsx create mode 100644 pkg/ui/workspaces/cluster-ui/src/dateRange/index.tsx create mode 100644 pkg/ui/workspaces/db-console/src/redux/statementsDateRange.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 new file mode 100644 index 000000000000..1bd6eab700e9 --- /dev/null +++ b/pkg/ui/workspaces/cluster-ui/src/api/fetchData.spec.ts @@ -0,0 +1,39 @@ +// 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 4669c4782f1d..b0ecc8c5c2ee 100644 --- a/pkg/ui/workspaces/cluster-ui/src/api/fetchData.ts +++ b/pkg/ui/workspaces/cluster-ui/src/api/fetchData.ts @@ -11,6 +11,7 @@ import { cockroach } from "@cockroachlabs/crdb-protobuf-client"; import { RequestError } from "../util"; import { getBasePath } from "./basePath"; +import { stringify } from "querystring"; interface ProtoBuilder< P extends ConstructorType, @@ -29,6 +30,18 @@ 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 3e3985befcd2..4eff1f9acf3d 100644 --- a/pkg/ui/workspaces/cluster-ui/src/api/statementsApi.ts +++ b/pkg/ui/workspaces/cluster-ui/src/api/statementsApi.ts @@ -9,7 +9,7 @@ // licenses/APL.txt. import { cockroach } from "@cockroachlabs/crdb-protobuf-client"; -import { fetchData } from "src/api"; +import { fetchData, propsToQueryString } from "src/api"; const STATEMENTS_PATH = "/_status/statements"; @@ -19,3 +19,19 @@ export const getStatements = (): Promise => { + const queryStr = propsToQueryString({ + start: req.start.toInt(), + end: req.end.toInt(), + combined: true, + }); + return fetchData( + cockroach.server.serverpb.StatementsResponse, + `${STATEMENTS_PATH}?${queryStr}`, + ); +}; diff --git a/pkg/ui/workspaces/cluster-ui/src/dateRange/dateRange.fixtures.tsx b/pkg/ui/workspaces/cluster-ui/src/dateRange/dateRange.fixtures.tsx new file mode 100644 index 000000000000..228b475b6dcc --- /dev/null +++ b/pkg/ui/workspaces/cluster-ui/src/dateRange/dateRange.fixtures.tsx @@ -0,0 +1,16 @@ +// 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 moment, { Moment } from "moment"; + +export const start = moment.utc("2021.08.08"); +export const end = moment.utc("2021.08.12"); + +export const allowedInterval: [Moment, Moment] = [start, end]; diff --git a/pkg/ui/workspaces/cluster-ui/src/dateRange/dateRange.module.scss b/pkg/ui/workspaces/cluster-ui/src/dateRange/dateRange.module.scss new file mode 100644 index 000000000000..6d6f53ae4c74 --- /dev/null +++ b/pkg/ui/workspaces/cluster-ui/src/dateRange/dateRange.module.scss @@ -0,0 +1,57 @@ +@import "../core/index.module"; + +.popup-container { + width: 506px; + height: 240px; + + .label { + display: block; + margin-bottom: 1rem; + } + + .popup-content { + :global(.ant-calendar-picker) { + width: 290px; + margin-right: 14px; + input { + padding-left: 34px; + } + } + + :global(.ant-time-picker-icon), + :global(.ant-calendar-picker-icon) { + left: 12px; + } + + :global(.ant-time-picker) { + width: 150px; + } + } + + .popup-footer { + margin-top: 24px; + text-align: right; + button { + margin-left: 8px; + } + } + + :global(.ant-popover-arrow) { + display: none; + } +} + +.date-range-form { + width: 300px; + height: 40px; + + :global(.ant-input-affix-wrapper) { + height: 40px; + &:hover { + :global(.ant-input:not(.ant-input-disabled)) { + border-color: $colors--primary-blue-3; + border-right-width: 2px !important; + } + } + } +} diff --git a/pkg/ui/workspaces/cluster-ui/src/dateRange/dateRange.stories.tsx b/pkg/ui/workspaces/cluster-ui/src/dateRange/dateRange.stories.tsx new file mode 100644 index 000000000000..228807bc909e --- /dev/null +++ b/pkg/ui/workspaces/cluster-ui/src/dateRange/dateRange.stories.tsx @@ -0,0 +1,32 @@ +// 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 React from "react"; +import { storiesOf } from "@storybook/react"; +import { start, end, allowedInterval } from "./dateRange.fixtures"; +import { DateRange } from "./index"; + +storiesOf("DateRange", module) + .add("default", () => ( + {}} + start={start} + /> + )) + .add("with invalid date range", () => ( + {}} + start={end} + /> + )); diff --git a/pkg/ui/workspaces/cluster-ui/src/dateRange/dateRange.tsx b/pkg/ui/workspaces/cluster-ui/src/dateRange/dateRange.tsx new file mode 100644 index 000000000000..22bca8175822 --- /dev/null +++ b/pkg/ui/workspaces/cluster-ui/src/dateRange/dateRange.tsx @@ -0,0 +1,190 @@ +// 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 React, { useState } from "react"; +import { Alert, DatePicker, Form, Input, Popover, TimePicker } from "antd"; +import { Moment } from "moment"; +import classNames from "classnames/bind"; +import { Time as TimeIcon, ErrorCircleFilled } from "@cockroachlabs/icons"; +import { Button } from "src/button"; +import { Text, TextTypes } from "src/text"; + +import styles from "./dateRange.module.scss"; + +const cx = classNames.bind(styles); + +function rangeToString(start: Moment, end: Moment): string { + const formatStr = "MMM D, H:mm"; + const formatStrSameDay = "H:mm"; + + const isSameDay = start.isSame(end, "day"); + return `${start.utc().format(formatStr)} - ${end + .utc() + .format(isSameDay ? formatStrSameDay : formatStr)} (UTC)`; +} + +type DateRangeMenuProps = { + start: Moment; + end: Moment; + allowedInterval?: [Moment, Moment]; + closeMenu: () => void; + onSubmit: (start: Moment, end: Moment) => void; +}; + +function DateRangeMenu({ + start, + end, + allowedInterval, + closeMenu, + onSubmit, +}: DateRangeMenuProps): React.ReactElement { + const dateFormat = "MMMM D, YYYY"; + const timeFormat = "H:mm [(UTC)]"; + const [startMoment, setStartMoment] = useState(start); + const [endMoment, setEndMoment] = useState(end); + + const onChangeStart = (m?: Moment) => { + m && setStartMoment(m); + }; + + const onChangeEnd = (m?: Moment) => { + m && setEndMoment(m); + }; + + const isDisabled = allowedInterval + ? (date: Moment): boolean => { + return ( + date.isBefore(allowedInterval[0]) || date.isAfter(allowedInterval[1]) + ); + } + : null; + + const isValid = startMoment.isBefore(endMoment); + + const onApply = (): void => { + onSubmit(startMoment, endMoment); + closeMenu(); + }; + + return ( +
+ + From + + } + value={startMoment} + /> + } + value={startMoment} + /> + + To + + } + value={endMoment} + /> + } + value={endMoment} + /> + {!isValid && ( + } + message="Date interval not valid" + type="error" + showIcon + /> + )} +
+ + +
+
+ ); +} + +type DateRangeProps = { + start: Moment; + end: Moment; + allowedInterval?: [Moment, Moment]; + onSubmit: (start: Moment, end: Moment) => void; +}; + +export function DateRange({ + allowedInterval, + start, + end, + onSubmit, +}: DateRangeProps): React.ReactElement { + const [menuVisible, setMenuVisible] = useState(false); + const displayStr = rangeToString(start, end); + + const onVisibleChange = (visible: boolean): void => { + setMenuVisible(visible); + }; + + const closeMenu = (): void => { + setMenuVisible(false); + }; + + const menu = ( + + ); + + return ( +
+ + + } /> + + +
+ ); +} diff --git a/pkg/ui/workspaces/cluster-ui/src/dateRange/index.tsx b/pkg/ui/workspaces/cluster-ui/src/dateRange/index.tsx new file mode 100644 index 000000000000..8d66fbc13843 --- /dev/null +++ b/pkg/ui/workspaces/cluster-ui/src/dateRange/index.tsx @@ -0,0 +1,11 @@ +// 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. + +export * from "./dateRange"; diff --git a/pkg/ui/workspaces/cluster-ui/src/statementDetails/statementDetails.tsx b/pkg/ui/workspaces/cluster-ui/src/statementDetails/statementDetails.tsx index f2c69763127a..2a9e1bd613fe 100644 --- a/pkg/ui/workspaces/cluster-ui/src/statementDetails/statementDetails.tsx +++ b/pkg/ui/workspaces/cluster-ui/src/statementDetails/statementDetails.tsx @@ -18,6 +18,7 @@ import classNames from "classnames/bind"; import { format as d3Format } from "d3-format"; import { ArrowLeft } from "@cockroachlabs/icons"; import { cockroach } from "@cockroachlabs/crdb-protobuf-client"; +import Long from "long"; import { intersperse, @@ -59,7 +60,8 @@ import summaryCardStyles from "src/summaryCard/summaryCard.module.scss"; import styles from "./statementDetails.module.scss"; import { NodeSummaryStats } from "../nodes"; import { UIConfigState } from "../store/uiConfig"; -import moment from "moment"; +import moment, { Moment } from "moment"; +import { StatementsRequest } from "src/api/statementsApi"; const { TabPane } = Tabs; @@ -125,7 +127,7 @@ export type NodesSummary = { }; export interface StatementDetailsDispatchProps { - refreshStatements: () => void; + refreshStatements: (req?: StatementsRequest) => void; refreshStatementDiagnosticsRequests: () => void; refreshNodes: () => void; refreshNodesLiveness: () => void; @@ -144,6 +146,7 @@ export interface StatementDetailsDispatchProps { export interface StatementDetailsStateProps { statement: SingleStatementStatistics; statementsError: Error | null; + dateRange?: [Moment, Moment]; nodeNames: { [nodeId: string]: string }; nodeRegions: { [nodeId: string]: string }; diagnosticsReports: cockroach.server.serverpb.IStatementDiagnosticsReport[]; @@ -158,6 +161,17 @@ const cx = classNames.bind(styles); const sortableTableCx = classNames.bind(sortedTableStyles); const summaryCardStylesCx = classNames.bind(summaryCardStyles); +function statementsRequestFromProps( + props: StatementDetailsProps, +): cockroach.server.serverpb.StatementsRequest | null { + if (props.isTenant || props.dateRange == null) return null; + return new cockroach.server.serverpb.StatementsRequest({ + combined: true, + start: Long.fromNumber(props.dateRange[0].unix()), + end: Long.fromNumber(props.dateRange[1].unix()), + }); +} + function AppLink(props: { app: string }) { if (!props.app) { return (unset); @@ -331,15 +345,20 @@ export class StatementDetails extends React.Component< } }; + refreshStatements = () => { + const req = statementsRequestFromProps(this.props); + this.props.refreshStatements(req); + }; + componentDidMount() { - this.props.refreshStatements(); + this.refreshStatements(); this.props.refreshStatementDiagnosticsRequests(); this.props.refreshNodes(); this.props.refreshNodesLiveness(); } componentDidUpdate() { - this.props.refreshStatements(); + this.refreshStatements(); this.props.refreshStatementDiagnosticsRequests(); this.props.refreshNodes(); this.props.refreshNodesLiveness(); diff --git a/pkg/ui/workspaces/cluster-ui/src/statementDetails/statementDetailsConnected.ts b/pkg/ui/workspaces/cluster-ui/src/statementDetails/statementDetailsConnected.ts index e83ebfa97ec9..21130d177e5f 100644 --- a/pkg/ui/workspaces/cluster-ui/src/statementDetails/statementDetailsConnected.ts +++ b/pkg/ui/workspaces/cluster-ui/src/statementDetails/statementDetailsConnected.ts @@ -26,7 +26,7 @@ import { nodeDisplayNameByIDSelector, nodeRegionsByIDSelector, } from "../store/nodes"; -import { actions as statementActions } from "src/store/statements"; +import { actions as statementsActions } from "src/store/statements"; import { actions as statementDiagnosticsActions, selectDiagnosticsReportsByStatementFingerprint, @@ -35,6 +35,7 @@ import { actions as analyticsActions } from "src/store/analytics"; import { actions as localStorageActions } from "src/store/localStorage"; import { actions as nodesActions } from "../store/nodes"; import { actions as nodeLivenessActions } from "../store/liveness"; +import { selectDateRange } from "../statementsPage/statementsPage.selectors"; const mapStateToProps = (state: AppState, props: StatementDetailsProps) => { const statement = selectStatement(state, props); @@ -42,6 +43,7 @@ const mapStateToProps = (state: AppState, props: StatementDetailsProps) => { return { statement, statementsError: state.adminUI.statements.lastError, + dateRange: selectIsTenant(state) ? null : selectDateRange(state), nodeNames: nodeDisplayNameByIDSelector(state), nodeRegions: nodeRegionsByIDSelector(state), diagnosticsReports: selectDiagnosticsReportsByStatementFingerprint( @@ -56,7 +58,7 @@ const mapStateToProps = (state: AppState, props: StatementDetailsProps) => { const mapDispatchToProps = ( dispatch: Dispatch, ): StatementDetailsDispatchProps => ({ - refreshStatements: () => dispatch(statementActions.refresh()), + refreshStatements: () => dispatch(statementsActions.refresh()), 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 9309380ea805..e71e8c9696a5 100644 --- a/pkg/ui/workspaces/cluster-ui/src/statementsPage/statementsPage.fixture.ts +++ b/pkg/ui/workspaces/cluster-ui/src/statementsPage/statementsPage.fixture.ts @@ -10,6 +10,7 @@ /* eslint-disable prettier/prettier */ import { StatementsPageProps } from "./statementsPage"; +import moment from "moment"; import { createMemoryHistory } from "history"; import Long from "long"; import { noop } from "lodash"; @@ -21,177 +22,179 @@ type IStatementDiagnosticsReport = cockroach.server.serverpb.IStatementDiagnosti type IStatementStatistics = protos.cockroach.sql.IStatementStatistics; type IExecStats = protos.cockroach.sql.IExecStats; -const history = createMemoryHistory({ initialEntries: ["/statements"]}); +const history = createMemoryHistory({ initialEntries: ["/statements"] }); const execStats: Required = { - "count": Long.fromNumber(180), - "network_bytes": { - "mean": 80, - "squared_diffs": 0.01, + count: Long.fromNumber(180), + network_bytes: { + mean: 80, + squared_diffs: 0.01, }, - "max_mem_usage": { - "mean": 80, - "squared_diffs": 0.01, + max_mem_usage: { + mean: 80, + squared_diffs: 0.01, }, - "contention_time": { - "mean": 80, - "squared_diffs": 0.01, + contention_time: { + mean: 80, + squared_diffs: 0.01, }, - "network_messages": { - "mean": 80, - "squared_diffs": 0.01, + network_messages: { + mean: 80, + squared_diffs: 0.01, }, - "max_disk_usage": { - "mean": 80, - "squared_diffs": 0.01, + max_disk_usage: { + mean: 80, + squared_diffs: 0.01, }, }; const statementStats: Required = { - "count": Long.fromNumber(180000), - "first_attempt_count": Long.fromNumber(50000), - "max_retries": Long.fromNumber(10), - "sql_type": "DDL", - "nodes": [Long.fromNumber(1), Long.fromNumber(2)], - "num_rows": { - "mean": 1, - "squared_diffs": 0, + count: Long.fromNumber(180000), + first_attempt_count: Long.fromNumber(50000), + max_retries: Long.fromNumber(10), + sql_type: "DDL", + nodes: [Long.fromNumber(1), Long.fromNumber(2)], + num_rows: { + mean: 1, + squared_diffs: 0, }, - "legacy_last_err": "", - "legacy_last_err_redacted": "", - "parse_lat": { - "mean": 0, - "squared_diffs": 0, + legacy_last_err: "", + legacy_last_err_redacted: "", + parse_lat: { + mean: 0, + squared_diffs: 0, }, - "plan_lat": { - "mean": 0.00018, - "squared_diffs": 5.4319999999999994e-9, + plan_lat: { + mean: 0.00018, + squared_diffs: 5.4319999999999994e-9, }, - "run_lat": { - "mean": 0.0022536666666666664, - "squared_diffs": 0.0000020303526666666667, + run_lat: { + mean: 0.0022536666666666664, + squared_diffs: 0.0000020303526666666667, }, - "service_lat": { - "mean": 0.002496, - "squared_diffs": 0.000002308794, + service_lat: { + mean: 0.002496, + squared_diffs: 0.000002308794, }, - "overhead_lat": { - "mean": 0.00006233333333333315, - "squared_diffs": 5.786666666666667e-10, + overhead_lat: { + mean: 0.00006233333333333315, + squared_diffs: 5.786666666666667e-10, }, - "bytes_read": { - "mean": 80, - "squared_diffs": 0.01, + bytes_read: { + mean: 80, + squared_diffs: 0.01, }, - "rows_read": { - "mean": 10, - "squared_diffs": 1, + rows_read: { + mean: 10, + squared_diffs: 1, }, exec_stats: execStats, - "last_exec_timestamp": { + last_exec_timestamp: { seconds: Long.fromInt(1599670292), nanos: 111613000, }, - "sensitive_info": { - "last_err": "", - "most_recent_plan_description": { - "name": "root", - "children": [ + sensitive_info: { + last_err: "", + most_recent_plan_description: { + name: "root", + children: [ { - "name": "render", - "attrs": [ + name: "render", + attrs: [ { - "key": "render", - "value": "COALESCE(a, b)", + key: "render", + value: "COALESCE(a, b)", }, ], - "children": [ + children: [ { - "name": "values", - "attrs": [ + name: "values", + attrs: [ { - "key": "size", - "value": "2 columns, 1 row", + key: "size", + value: "2 columns, 1 row", }, { - "key": "row 0, expr", - "value": "(SELECT code FROM promo_codes WHERE code > $1 ORDER BY code LIMIT _)", + key: "row 0, expr", + value: + "(SELECT code FROM promo_codes WHERE code > $1 ORDER BY code LIMIT _)", }, { - "key": "row 0, expr", - "value": "(SELECT code FROM promo_codes ORDER BY code LIMIT _)", + key: "row 0, expr", + value: "(SELECT code FROM promo_codes ORDER BY code LIMIT _)", }, ], }, ], }, { - "name": "subquery", - "attrs": [ + name: "subquery", + attrs: [ { - "key": "id", - "value": "@S1", + key: "id", + value: "@S1", }, { - "key": "original sql", - "value": "(SELECT code FROM promo_codes WHERE code > $1 ORDER BY code LIMIT _)", + key: "original sql", + value: + "(SELECT code FROM promo_codes WHERE code > $1 ORDER BY code LIMIT _)", }, { - "key": "exec mode", - "value": "one row", + key: "exec mode", + value: "one row", }, ], - "children": [ + children: [ { - "name": "scan", - "attrs": [ + name: "scan", + attrs: [ { - "key": "table", - "value": "promo_codes@primary", + key: "table", + value: "promo_codes@primary", }, { - "key": "spans", - "value": "1 span", + key: "spans", + value: "1 span", }, { - "key": "limit", - "value": "1", + key: "limit", + value: "1", }, ], }, ], }, { - "name": "subquery", - "attrs": [ + name: "subquery", + attrs: [ { - "key": "id", - "value": "@S2", + key: "id", + value: "@S2", }, { - "key": "original sql", - "value": "(SELECT code FROM promo_codes ORDER BY code LIMIT _)", + key: "original sql", + value: "(SELECT code FROM promo_codes ORDER BY code LIMIT _)", }, { - "key": "exec mode", - "value": "one row", + key: "exec mode", + value: "one row", }, ], - "children": [ + children: [ { - "name": "scan", - "attrs": [ + name: "scan", + attrs: [ { - "key": "table", - "value": "promo_codes@primary", + key: "table", + value: "promo_codes@primary", }, { - "key": "spans", - "value": "ALL", + key: "spans", + value: "ALL", }, { - "key": "limit", - "value": "1", + key: "limit", + value: "1", }, ], }, @@ -204,370 +207,389 @@ const statementStats: Required = { const diagnosticsReports: IStatementDiagnosticsReport[] = [ { - "id": Long.fromNumber(594413966918975489), - "completed": true, - "statement_fingerprint": "SHOW database", - "statement_diagnostics_id": Long.fromNumber(594413981435920385), - "requested_at": {"seconds": Long.fromNumber(1601471146), "nanos": 737251000} + id: Long.fromNumber(594413966918975489), + completed: true, + statement_fingerprint: "SHOW database", + statement_diagnostics_id: Long.fromNumber(594413981435920385), + requested_at: { seconds: Long.fromNumber(1601471146), nanos: 737251000 }, }, { - "id": Long.fromNumber(594413966918975429), - "completed": true, - "statement_fingerprint": "SHOW database", - "statement_diagnostics_id": Long.fromNumber(594413281435920385), - "requested_at": {"seconds": Long.fromNumber(1601491146), "nanos": 737251000} - } -] + id: Long.fromNumber(594413966918975429), + completed: true, + statement_fingerprint: "SHOW database", + statement_diagnostics_id: Long.fromNumber(594413281435920385), + requested_at: { seconds: Long.fromNumber(1601491146), nanos: 737251000 }, + }, +]; const diagnosticsReportsInProgress: IStatementDiagnosticsReport[] = [ { - "id": Long.fromNumber(594413966918975489), - "completed": false, - "statement_fingerprint": "SHOW database", - "statement_diagnostics_id": Long.fromNumber(594413981435920385), - "requested_at": {"seconds": Long.fromNumber(1601471146), "nanos": 737251000} + id: Long.fromNumber(594413966918975489), + completed: false, + statement_fingerprint: "SHOW database", + statement_diagnostics_id: Long.fromNumber(594413981435920385), + requested_at: { seconds: Long.fromNumber(1601471146), nanos: 737251000 }, }, { - "id": Long.fromNumber(594413966918975429), - "completed": true, - "statement_fingerprint": "SHOW database", - "statement_diagnostics_id": Long.fromNumber(594413281435920385), - "requested_at": {"seconds": Long.fromNumber(1601491146), "nanos": 737251000} - } -] + id: Long.fromNumber(594413966918975429), + completed: true, + statement_fingerprint: "SHOW database", + statement_diagnostics_id: Long.fromNumber(594413281435920385), + requested_at: { seconds: Long.fromNumber(1601491146), nanos: 737251000 }, + }, +]; const statementsPagePropsFixture: StatementsPageProps = { history, location: { - "pathname": "/statements", - "search": "", - "hash": "", - "state": null, + pathname: "/statements", + search: "", + hash: "", + state: null, }, - "match": { - "path": "/statements", - "url": "/statements", - "isExact": true, - "params": {}, + match: { + path: "/statements", + url: "/statements", + isExact: true, + params: {}, }, - "databases": ["defaultdb","foo","system"], - "nodeRegions": { + databases: ["defaultdb", "foo", "system"], + nodeRegions: { "1": "gcp-us-east1", "2": "gcp-us-east1", "3": "gcp-us-west1", "4": "gcp-europe-west1", }, - "statements": [ + statements: [ { - "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)", - "implicitTxn": true, - "database": "defaultdb", - "fullScan": false, - "stats": statementStats, + 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)", + implicitTxn: true, + database: "defaultdb", + fullScan: false, + stats: statementStats, }, { - "label": "INSERT INTO vehicles VALUES ($1, $2, __more6__)", - "implicitTxn": true, - "database": "defaultdb", - "fullScan": false, - "stats": statementStats, + label: "INSERT INTO vehicles VALUES ($1, $2, __more6__)", + implicitTxn: true, + database: "defaultdb", + fullScan: false, + stats: statementStats, }, { - "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)", - "implicitTxn": true, - "database": "defaultdb", - "fullScan": false, - "stats": statementStats, + 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)", + implicitTxn: true, + database: "defaultdb", + fullScan: false, + stats: statementStats, }, { - "label": "UPSERT INTO vehicle_location_histories VALUES ($1, $2, now(), $3, $4)", - "implicitTxn": true, - "database": "defaultdb", - "fullScan": false, - "stats": statementStats, + label: + "UPSERT INTO vehicle_location_histories VALUES ($1, $2, now(), $3, $4)", + implicitTxn: true, + database: "defaultdb", + fullScan: false, + stats: statementStats, }, { - "label": "INSERT INTO user_promo_codes VALUES ($1, $2, $3, now(), _)", - "implicitTxn": true, - "database": "defaultdb", - "fullScan": false, - "stats": statementStats, + label: "INSERT INTO user_promo_codes VALUES ($1, $2, $3, now(), _)", + implicitTxn: true, + database: "defaultdb", + fullScan: false, + stats: statementStats, }, { - "label": "SELECT city, id FROM vehicles WHERE city = $1", - "implicitTxn": true, - "database": "defaultdb", - "fullScan": true, - "stats": statementStats, + label: "SELECT city, id FROM vehicles WHERE city = $1", + implicitTxn: true, + database: "defaultdb", + fullScan: true, + stats: statementStats, }, { - "label": "INSERT INTO rides VALUES ($1, $2, $2, $3, $4, $5, _, now(), _, $6)", - "implicitTxn": true, - "database": "defaultdb", - "fullScan": false, - "stats": statementStats, + label: + "INSERT INTO rides VALUES ($1, $2, $2, $3, $4, $5, _, now(), _, $6)", + implicitTxn: true, + database: "defaultdb", + fullScan: false, + stats: statementStats, }, { - "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)", - "implicitTxn": true, - "database": "defaultdb", - "fullScan": false, - "stats": statementStats, + 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)", + implicitTxn: true, + database: "defaultdb", + fullScan: false, + stats: statementStats, }, { - "label": "UPDATE rides SET end_address = $3, end_time = now() WHERE (city = $1) AND (id = $2)", - "implicitTxn": true, - "database": "defaultdb", - "fullScan": false, - "stats": statementStats, + label: + "UPDATE rides SET end_address = $3, end_time = now() WHERE (city = $1) AND (id = $2)", + implicitTxn: true, + database: "defaultdb", + fullScan: false, + stats: statementStats, }, { - "label": "INSERT INTO users VALUES ($1, $2, __more3__)", - "implicitTxn": true, - "database": "defaultdb", - "fullScan": false, - "stats": statementStats, + label: "INSERT INTO users VALUES ($1, $2, __more3__)", + implicitTxn: true, + database: "defaultdb", + fullScan: false, + stats: statementStats, }, { - "label": "SELECT count(*) FROM user_promo_codes WHERE ((city = $1) AND (user_id = $2)) AND (code = $3)", - "implicitTxn": true, - "database": "defaultdb", - "fullScan": true, - "stats": statementStats, + label: + "SELECT count(*) FROM user_promo_codes WHERE ((city = $1) AND (user_id = $2)) AND (code = $3)", + implicitTxn: true, + database: "defaultdb", + fullScan: true, + stats: statementStats, }, { - "label": "INSERT INTO promo_codes VALUES ($1, $2, __more3__)", - "implicitTxn": true, - "database": "defaultdb", - "fullScan": false, - "stats": statementStats, + label: "INSERT INTO promo_codes VALUES ($1, $2, __more3__)", + implicitTxn: true, + database: "defaultdb", + fullScan: false, + stats: statementStats, }, { - "label": "ALTER TABLE users SCATTER FROM (_, _) TO (_, _)", - "implicitTxn": true, - "database": "defaultdb", - "fullScan": false, - "stats": statementStats, + label: "ALTER TABLE users SCATTER FROM (_, _) TO (_, _)", + implicitTxn: true, + database: "defaultdb", + fullScan: false, + stats: statementStats, }, { - "label": "ALTER TABLE rides ADD FOREIGN KEY (vehicle_city, vehicle_id) REFERENCES vehicles (city, id)", - "implicitTxn": true, - "database": "defaultdb", - "fullScan": false, - "stats": statementStats, + label: + "ALTER TABLE rides ADD FOREIGN KEY (vehicle_city, vehicle_id) REFERENCES vehicles (city, id)", + implicitTxn: true, + database: "defaultdb", + fullScan: false, + stats: statementStats, }, { - "label": "SHOW database", - "implicitTxn": true, - "database": "defaultdb", - "fullScan": false, - "stats": statementStats, + label: "SHOW database", + implicitTxn: true, + database: "defaultdb", + fullScan: false, + stats: statementStats, diagnosticsReports, }, { - "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))", - "implicitTxn": true, - "database": "defaultdb", - "fullScan": false, - "stats": statementStats, + 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))", + implicitTxn: true, + database: "defaultdb", + fullScan: false, + stats: statementStats, }, { - "label": "ALTER TABLE users SPLIT AT VALUES (_, _)", - "implicitTxn": true, - "database": "defaultdb", - "fullScan": false, - "stats": statementStats, + label: "ALTER TABLE users SPLIT AT VALUES (_, _)", + implicitTxn: true, + database: "defaultdb", + fullScan: false, + stats: statementStats, }, { - "label": "ALTER TABLE vehicles SCATTER FROM (_, _) TO (_, _)", - "implicitTxn": true, - "database": "defaultdb", - "fullScan": false, - "stats": statementStats, + label: "ALTER TABLE vehicles SCATTER FROM (_, _) TO (_, _)", + implicitTxn: true, + database: "defaultdb", + fullScan: false, + stats: statementStats, }, { - "label": "ALTER TABLE vehicle_location_histories ADD FOREIGN KEY (city, ride_id) REFERENCES rides (city, id)", - "implicitTxn": true, - "database": "defaultdb", - "fullScan": false, - "stats": statementStats, + label: + "ALTER TABLE vehicle_location_histories ADD FOREIGN KEY (city, ride_id) REFERENCES rides (city, id)", + implicitTxn: true, + database: "defaultdb", + fullScan: false, + stats: statementStats, }, { - "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))", - "implicitTxn": true, - "database": "defaultdb", - "fullScan": false, - "stats": statementStats, + 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))', + implicitTxn: true, + database: "defaultdb", + fullScan: false, + stats: statementStats, }, { - "label": "INSERT INTO users VALUES ($1, $2, __more3__), (__more40__)", - "implicitTxn": true, - "database": "defaultdb", - "fullScan": false, - "stats": statementStats, + label: "INSERT INTO users VALUES ($1, $2, __more3__), (__more40__)", + implicitTxn: true, + database: "defaultdb", + fullScan: false, + stats: statementStats, }, { - "label": "ALTER TABLE rides SCATTER FROM (_, _) TO (_, _)", - "implicitTxn": true, - "database": "defaultdb", - "fullScan": false, - "stats": statementStats, + label: "ALTER TABLE rides SCATTER FROM (_, _) TO (_, _)", + implicitTxn: true, + database: "defaultdb", + fullScan: false, + stats: statementStats, }, { - "label": "SET CLUSTER SETTING \"cluster.organization\" = $1", - "implicitTxn": true, - "database": "defaultdb", - "fullScan": false, - "stats": statementStats, + label: 'SET CLUSTER SETTING "cluster.organization" = $1', + implicitTxn: true, + database: "defaultdb", + fullScan: false, + stats: statementStats, }, { - "label": "ALTER TABLE vehicles ADD FOREIGN KEY (city, owner_id) REFERENCES users (city, id)", - "implicitTxn": true, - "database": "defaultdb", - "fullScan": false, - "stats": statementStats, + label: + "ALTER TABLE vehicles ADD FOREIGN KEY (city, owner_id) REFERENCES users (city, id)", + implicitTxn: true, + database: "defaultdb", + fullScan: false, + stats: statementStats, }, { - "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))", - "implicitTxn": true, - "database": "defaultdb", - "fullScan": false, - "stats": statementStats, + 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))", + implicitTxn: true, + database: "defaultdb", + fullScan: false, + stats: statementStats, }, { - "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))", - "implicitTxn": true, - "database": "defaultdb", - "fullScan": false, - "stats": statementStats, + 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))", + implicitTxn: true, + database: "defaultdb", + fullScan: false, + stats: statementStats, }, { - "label": "INSERT INTO rides VALUES ($1, $2, __more8__), (__more400__)", - "implicitTxn": true, - "database": "defaultdb", - "fullScan": false, - "stats": statementStats, + label: "INSERT INTO rides VALUES ($1, $2, __more8__), (__more400__)", + implicitTxn: true, + database: "defaultdb", + fullScan: false, + stats: statementStats, }, { - "label": "ALTER TABLE vehicles SPLIT AT VALUES (_, _)", - "implicitTxn": true, - "database": "defaultdb", - "fullScan": false, - "stats": statementStats, + label: "ALTER TABLE vehicles SPLIT AT VALUES (_, _)", + implicitTxn: true, + database: "defaultdb", + fullScan: false, + stats: statementStats, }, { - "label": "SET sql_safe_updates = _", - "implicitTxn": true, - "database": "defaultdb", - "fullScan": false, - "stats": statementStats, + label: "SET sql_safe_updates = _", + implicitTxn: true, + database: "defaultdb", + fullScan: false, + stats: statementStats, }, { - "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))", - "implicitTxn": true, - "database": "defaultdb", - "fullScan": false, - "stats": statementStats, + 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))", + implicitTxn: true, + database: "defaultdb", + fullScan: false, + stats: statementStats, }, { - "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))", - "implicitTxn": true, - "database": "defaultdb", - "fullScan": false, - "stats": statementStats, + 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))', + implicitTxn: true, + database: "defaultdb", + fullScan: false, + stats: statementStats, }, { - "label": "SELECT * FROM crdb_internal.node_build_info", - "implicitTxn": true, - "database": "defaultdb", - "fullScan": false, - "stats": statementStats, + label: "SELECT * FROM crdb_internal.node_build_info", + implicitTxn: true, + database: "defaultdb", + fullScan: false, + stats: statementStats, }, { - "label": "CREATE DATABASE movr", - "implicitTxn": true, - "database": "defaultdb", - "fullScan": false, - "stats": statementStats, - diagnosticsReports: diagnosticsReportsInProgress + label: "CREATE DATABASE movr", + implicitTxn: true, + database: "defaultdb", + fullScan: false, + stats: statementStats, + diagnosticsReports: diagnosticsReportsInProgress, }, { - "label": "SELECT count(*) > _ FROM [SHOW ALL CLUSTER SETTINGS] AS _ (v) WHERE v = _", - "implicitTxn": true, - "database": "defaultdb", - "fullScan": false, - "stats": statementStats, + label: + "SELECT count(*) > _ FROM [SHOW ALL CLUSTER SETTINGS] AS _ (v) WHERE v = _", + implicitTxn: true, + database: "defaultdb", + fullScan: false, + stats: statementStats, }, { - "label": "SET CLUSTER SETTING \"enterprise.license\" = $1", - "implicitTxn": true, - "database": "defaultdb", - "fullScan": false, - "stats": statementStats, + label: 'SET CLUSTER SETTING "enterprise.license" = $1', + implicitTxn: true, + database: "defaultdb", + fullScan: false, + stats: statementStats, }, { - "label": "ALTER TABLE rides ADD FOREIGN KEY (city, rider_id) REFERENCES users (city, id)", - "implicitTxn": true, - "database": "defaultdb", - "fullScan": false, - "stats": statementStats, + label: + "ALTER TABLE rides ADD FOREIGN KEY (city, rider_id) REFERENCES users (city, id)", + implicitTxn: true, + database: "defaultdb", + fullScan: false, + stats: statementStats, }, { - "label": "ALTER TABLE user_promo_codes ADD FOREIGN KEY (city, user_id) REFERENCES users (city, id)", - "implicitTxn": true, - "database": "defaultdb", - "fullScan": false, - "stats": statementStats, + label: + "ALTER TABLE user_promo_codes ADD FOREIGN KEY (city, user_id) REFERENCES users (city, id)", + implicitTxn: true, + database: "defaultdb", + fullScan: false, + stats: statementStats, }, { - "label": "INSERT INTO promo_codes VALUES ($1, $2, __more3__), (__more900__)", - "implicitTxn": true, - "database": "defaultdb", - "fullScan": false, - "stats": statementStats, + label: + "INSERT INTO promo_codes VALUES ($1, $2, __more3__), (__more900__)", + implicitTxn: true, + database: "defaultdb", + fullScan: false, + stats: statementStats, }, { - "label": "ALTER TABLE rides SPLIT AT VALUES (_, _)", - "implicitTxn": true, - "database": "defaultdb", - "fullScan": false, - "stats": statementStats, + label: "ALTER TABLE rides SPLIT AT VALUES (_, _)", + implicitTxn: true, + database: "defaultdb", + fullScan: false, + stats: statementStats, }, { - "label": "SELECT value FROM crdb_internal.node_build_info WHERE field = _", - "implicitTxn": true, - "database": "defaultdb", - "fullScan": false, - "stats": statementStats, + label: "SELECT value FROM crdb_internal.node_build_info WHERE field = _", + implicitTxn: true, + database: "defaultdb", + fullScan: false, + stats: statementStats, }, { - "label": "INSERT INTO vehicle_location_histories VALUES ($1, $2, __more3__), (__more900__)", - "implicitTxn": true, - "database": "defaultdb", - "fullScan": false, - "stats": statementStats, + label: + "INSERT INTO vehicle_location_histories VALUES ($1, $2, __more3__), (__more900__)", + implicitTxn: true, + database: "defaultdb", + fullScan: false, + stats: statementStats, }, { - "label": "INSERT INTO vehicles VALUES ($1, $2, __more6__), (__more10__)", - "implicitTxn": true, - "database": "defaultdb", - "fullScan": false, - "stats": statementStats, + label: "INSERT INTO vehicles VALUES ($1, $2, __more6__), (__more10__)", + implicitTxn: true, + database: "defaultdb", + fullScan: false, + stats: statementStats, }, ], - "statementsError": null, - "apps": [ - "(internal)", - "movr", - "$ cockroach demo", - ], - "totalFingerprints": 95, - "lastReset": "2020-04-13 07:22:23", - "columns": null, + statementsError: null, + dateRange: [moment.utc("2021.08.08"), moment.utc("2021.08.12")], + apps: ["(internal)", "movr", "$ cockroach demo"], + totalFingerprints: 95, + lastReset: "2020-04-13 07:22:23", + columns: null, dismissAlertMessage: noop, refreshStatementDiagnosticsRequests: noop, refreshStatements: noop, resetSQLStats: noop, + onDateRangeChange: noop, onActivateStatementDiagnostics: noop, onDiagnosticsModalOpen: noop, onSearchComplete: noop, @@ -577,7 +599,11 @@ const statementsPagePropsFixture: StatementsPageProps = { export const statementsPagePropsWithRequestError: StatementsPageProps = { ...statementsPagePropsFixture, - statementsError: new RequestError("request_error", 403, "this operation requires admin privilege") -} + statementsError: new RequestError( + "request_error", + 403, + "this operation requires admin privilege", + ), +}; export default statementsPagePropsFixture; diff --git a/pkg/ui/workspaces/cluster-ui/src/statementsPage/statementsPage.module.scss b/pkg/ui/workspaces/cluster-ui/src/statementsPage/statementsPage.module.scss index 887897ae1a07..f2634a07dfed 100644 --- a/pkg/ui/workspaces/cluster-ui/src/statementsPage/statementsPage.module.scss +++ b/pkg/ui/workspaces/cluster-ui/src/statementsPage/statementsPage.module.scss @@ -80,3 +80,14 @@ h2.base-heading { font-size: 14px; color: $colors--title; } + +.reset-btn { + background: transparent; + border: none; + color: $colors--primary-blue-3; + cursor: pointer; + + &:hover { + text-decoration: underline; + } +} 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 28d651124a88..c77ac67b517b 100644 --- a/pkg/ui/workspaces/cluster-ui/src/statementsPage/statementsPage.selectors.ts +++ b/pkg/ui/workspaces/cluster-ui/src/statementsPage/statementsPage.selectors.ts @@ -9,6 +9,7 @@ // licenses/APL.txt. import { createSelector } from "reselect"; +import moment, { Moment } from "moment"; import { aggregateStatementStats, appAttr, @@ -204,3 +205,17 @@ export const selectColumns = createSelector( ? localStorage["showColumns/StatementsPage"].split(",") : null, ); + +export const selectLocalStorageDateRange = createSelector( + localStorageSelector, + localStorage => localStorage["dateRange/StatementsPage"], +); + +export const selectDateRange = createSelector( + selectLocalStorageDateRange, + dateRange => + [moment.unix(dateRange.start).utc(), moment.unix(dateRange.end).utc()] as [ + Moment, + Moment, + ], +); diff --git a/pkg/ui/workspaces/cluster-ui/src/statementsPage/statementsPage.tsx b/pkg/ui/workspaces/cluster-ui/src/statementsPage/statementsPage.tsx index 7684dbabd3bd..5dc924c64ccf 100644 --- a/pkg/ui/workspaces/cluster-ui/src/statementsPage/statementsPage.tsx +++ b/pkg/ui/workspaces/cluster-ui/src/statementsPage/statementsPage.tsx @@ -12,12 +12,14 @@ import React from "react"; import { RouteComponentProps } from "react-router-dom"; import { isNil, merge, forIn } from "lodash"; import Helmet from "react-helmet"; +import moment, { Moment } from "moment"; import classNames from "classnames/bind"; import { Loading } from "src/loading"; import { PageConfig, PageConfigItem } from "src/pageConfig"; import { ColumnDescriptor, SortSetting } from "src/sortedtable"; import { Search } from "src/search"; import { Pagination } from "src/pagination"; +import { DateRange } from "src/dateRange"; import { TableStatistics } from "../tableStatistics"; import { Filter, @@ -59,6 +61,8 @@ import sortableTableStyles from "src/sortedtable/sortedtable.module.scss"; import ColumnsSelector from "../columnsSelector/columnsSelector"; import { SelectOption } from "../multiSelectCheckbox/multiSelectCheckbox"; import { UIConfigState } from "../store/uiConfig"; +import { StatementsRequest } from "src/api/statementsApi"; +import Long from "long"; const cx = classNames.bind(styles); const sortableTableCx = classNames.bind(sortableTableStyles); @@ -69,7 +73,7 @@ const sortableTableCx = classNames.bind(sortableTableStyles); // provide convenient definitions for `mapDispatchToProps`, `mapStateToProps` and props that // have to be provided by parent component. export interface StatementsPageDispatchProps { - refreshStatements: () => void; + refreshStatements: (req?: StatementsRequest) => void; refreshStatementDiagnosticsRequests: () => void; resetSQLStats: () => void; dismissAlertMessage: () => void; @@ -86,10 +90,12 @@ export interface StatementsPageDispatchProps { onFilterChange?: (value: string) => void; onStatementClick?: (statement: string) => void; onColumnsChange?: (selectedColumns: string[]) => void; + onDateRangeChange: (start: Moment, end: Moment) => void; } export interface StatementsPageStateProps { statements: AggregateStatistics[]; + dateRange?: [Moment, Moment]; statementsError: Error | null; apps: string[]; databases: string[]; @@ -112,6 +118,17 @@ export type StatementsPageProps = StatementsPageDispatchProps & StatementsPageStateProps & RouteComponentProps; +function statementsRequestFromProps( + props: StatementsPageProps, +): cockroach.server.serverpb.StatementsRequest | null { + if (props.isTenant || props.dateRange == null) return null; + return new cockroach.server.serverpb.StatementsRequest({ + combined: true, + start: Long.fromNumber(props.dateRange[0].unix()), + end: Long.fromNumber(props.dateRange[1].unix()), + }); +} + export class StatementsPage extends React.Component< StatementsPageProps, StatementsPageState @@ -192,6 +209,20 @@ export class StatementsPage extends React.Component< } }; + changeDateRange = (start: Moment, end: Moment): void => { + if (this.props.onDateRangeChange) { + this.props.onDateRangeChange(start, end); + } + }; + + resetTime = (): void => { + // Default range to reset to is one hour ago. + this.changeDateRange( + moment.utc().subtract(1, "hours"), + moment.utc().add(1, "minute"), + ); + }; + selectApp = (value: string) => { if (value == "All") value = ""; const { history, onFilterChange } = this.props; @@ -214,8 +245,13 @@ export class StatementsPage extends React.Component< }); }; + refreshStatements = () => { + const req = statementsRequestFromProps(this.props); + this.props.refreshStatements(req); + }; + componentDidMount() { - this.props.refreshStatements(); + this.refreshStatements(); this.props.refreshStatementDiagnosticsRequests(); } @@ -226,7 +262,7 @@ export class StatementsPage extends React.Component< if (this.state.search && this.state.search !== prevState.search) { this.props.onSearchComplete(this.filteredStatementsData()); } - this.props.refreshStatements(); + this.refreshStatements(); this.props.refreshStatementDiagnosticsRequests(); }; @@ -482,6 +518,22 @@ export class StatementsPage extends React.Component< showNodes={nodes.length > 1} /> + {this.props.dateRange && ( + <> + + + + + + + + )}
diff --git a/pkg/ui/workspaces/cluster-ui/src/statementsPage/statementsPageConnected.tsx b/pkg/ui/workspaces/cluster-ui/src/statementsPage/statementsPageConnected.tsx index 9df94f87580c..692713fd9e20 100644 --- a/pkg/ui/workspaces/cluster-ui/src/statementsPage/statementsPageConnected.tsx +++ b/pkg/ui/workspaces/cluster-ui/src/statementsPage/statementsPageConnected.tsx @@ -11,9 +11,10 @@ import { connect } from "react-redux"; import { RouteComponentProps, withRouter } from "react-router-dom"; import { Dispatch } from "redux"; +import moment, { Moment } from "moment"; import { AppState } from "src/store"; -import { actions as statementActions } from "src/store/statements"; +import { actions as statementsActions } from "src/store/statements"; import { actions as statementDiagnosticsActions } from "src/store/statementDiagnostics"; import { actions as analyticsActions } from "src/store/analytics"; import { actions as localStorageActions } from "src/store/localStorage"; @@ -32,10 +33,12 @@ import { selectStatementsLastError, selectTotalFingerprints, selectColumns, + selectDateRange, } from "./statementsPage.selectors"; import { selectIsTenant } from "../store/uiConfig"; import { AggregateStatistics } from "../statementsTable"; import { nodeRegionsByIDSelector } from "../store/nodes"; +import { StatementsRequest } from "src/api/statementsApi"; export const ConnectedStatementsPage = withRouter( connect< @@ -53,9 +56,19 @@ export const ConnectedStatementsPage = withRouter( columns: selectColumns(state), nodeRegions: nodeRegionsByIDSelector(state), isTenant: selectIsTenant(state), + dateRange: selectIsTenant(state) ? null : selectDateRange(state), }), (dispatch: Dispatch) => ({ - refreshStatements: () => dispatch(statementActions.refresh()), + refreshStatements: (req?: StatementsRequest) => + dispatch(statementsActions.refresh(req)), + onDateRangeChange: (start: Moment, end: Moment) => { + dispatch( + statementsActions.updateDateRange({ + start: start.unix(), + end: end.unix(), + }), + ); + }, refreshStatementDiagnosticsRequests: () => dispatch(statementDiagnosticsActions.refresh()), resetSQLStats: () => dispatch(resetSQLStatsActions.request()), diff --git a/pkg/ui/workspaces/cluster-ui/src/store/localStorage/localStorage.reducer.ts b/pkg/ui/workspaces/cluster-ui/src/store/localStorage/localStorage.reducer.ts index 758b03f9e70a..60d31a28f5ce 100644 --- a/pkg/ui/workspaces/cluster-ui/src/store/localStorage/localStorage.reducer.ts +++ b/pkg/ui/workspaces/cluster-ui/src/store/localStorage/localStorage.reducer.ts @@ -8,12 +8,19 @@ // by the Apache License, Version 2.0, included in the file // licenses/APL.txt. +import moment from "moment"; import { createSlice, PayloadAction } from "@reduxjs/toolkit"; import { DOMAIN_NAME } from "../utils"; +type StatementsDateRangeState = { + start: number; + end: number; +}; + export type LocalStorageState = { "adminUi/showDiagnosticsModal": boolean; "showColumns/StatementsPage": string; + "dateRange/StatementsPage": StatementsDateRangeState; }; type Payload = { @@ -21,12 +28,24 @@ type Payload = { value: any; }; +const defaultDateRange: StatementsDateRangeState = { + start: moment + .utc() + .subtract(1, "hours") + .unix(), + end: moment.utc().unix() + 60, // Add 1 minute to account for potential lag. +}; + // TODO (koorosh): initial state should be restored from preserved keys in LocalStorage const initialState: LocalStorageState = { "adminUi/showDiagnosticsModal": - Boolean(localStorage.getItem("adminUi/showDiagnosticsModal")) || false, + Boolean(JSON.parse(localStorage.getItem("adminUi/showDiagnosticsModal"))) || + false, "showColumns/StatementsPage": - localStorage.getItem("showColumns/StatementsPage") || "default", + JSON.parse(localStorage.getItem("showColumns/StatementsPage")) || "default", + "dateRange/StatementsPage": + JSON.parse(localStorage.getItem("dateRange/StatementsPage")) || + defaultDateRange, }; const localStorageSlice = createSlice({ diff --git a/pkg/ui/workspaces/cluster-ui/src/store/localStorage/localStorage.saga.ts b/pkg/ui/workspaces/cluster-ui/src/store/localStorage/localStorage.saga.ts index 265c46757b6a..c2d0ca08df60 100644 --- a/pkg/ui/workspaces/cluster-ui/src/store/localStorage/localStorage.saga.ts +++ b/pkg/ui/workspaces/cluster-ui/src/store/localStorage/localStorage.saga.ts @@ -14,7 +14,11 @@ import { actions } from "./localStorage.reducer"; export function* updateLocalStorageItemSaga(action: AnyAction) { const { key, value } = action.payload; - yield call({ context: localStorage, fn: localStorage.setItem }, key, value); + yield call( + { context: localStorage, fn: localStorage.setItem }, + key, + JSON.stringify(value), + ); } export function* localStorageSaga() { diff --git a/pkg/ui/workspaces/cluster-ui/src/store/sagas.ts b/pkg/ui/workspaces/cluster-ui/src/store/sagas.ts index 031aea9bcb23..c9f5a0c2a3ca 100644 --- a/pkg/ui/workspaces/cluster-ui/src/store/sagas.ts +++ b/pkg/ui/workspaces/cluster-ui/src/store/sagas.ts @@ -29,7 +29,6 @@ export function* sagas(cacheInvalidationPeriod?: number) { fork(nodesSaga, cacheInvalidationPeriod), fork(livenessSaga, cacheInvalidationPeriod), fork(sessionsSaga), - fork(terminateSaga), fork(transactionsSaga), fork(notifificationsSaga), fork(sqlStatsSaga), diff --git a/pkg/ui/workspaces/cluster-ui/src/store/statements/statements.reducer.ts b/pkg/ui/workspaces/cluster-ui/src/store/statements/statements.reducer.ts index b9dd91e2d788..3836f999fa21 100644 --- a/pkg/ui/workspaces/cluster-ui/src/store/statements/statements.reducer.ts +++ b/pkg/ui/workspaces/cluster-ui/src/store/statements/statements.reducer.ts @@ -11,6 +11,7 @@ import { createSlice, PayloadAction } from "@reduxjs/toolkit"; import { cockroach } from "@cockroachlabs/crdb-protobuf-client"; import { DOMAIN_NAME, noopReducer } from "../utils"; +import { StatementsRequest } from "src/api/statementsApi"; type StatementsResponse = cockroach.server.serverpb.StatementsResponse; @@ -26,6 +27,11 @@ const initialState: StatementsState = { valid: true, }; +export type UpdateDateRangePayload = { + start: number; + end: number; +}; + const statementsSlice = createSlice({ name: `${DOMAIN_NAME}/statements`, initialState, @@ -42,9 +48,9 @@ const statementsSlice = createSlice({ invalidated: state => { state.valid = false; }, - // Define actions that don't change state - refresh: noopReducer, - request: noopReducer, + refresh: (_, action?: PayloadAction) => {}, + request: (_, action?: PayloadAction) => {}, + updateDateRange: (_, action: PayloadAction) => {}, }, }); diff --git a/pkg/ui/workspaces/cluster-ui/src/store/statements/statements.sagas.spec.ts b/pkg/ui/workspaces/cluster-ui/src/store/statements/statements.sagas.spec.ts index 1d960bc17cdc..7a757b6de8fc 100644 --- a/pkg/ui/workspaces/cluster-ui/src/store/statements/statements.sagas.spec.ts +++ b/pkg/ui/workspaces/cluster-ui/src/store/statements/statements.sagas.spec.ts @@ -13,7 +13,7 @@ import { throwError } from "redux-saga-test-plan/providers"; import * as matchers from "redux-saga-test-plan/matchers"; import { cockroach } from "@cockroachlabs/crdb-protobuf-client"; -import { getStatements } from "src/api/statementsApi"; +import { getStatements, getCombinedStatements } from "src/api/statementsApi"; import { receivedStatementsSaga, refreshStatementsSaga, @@ -49,6 +49,24 @@ describe("StatementsPage sagas", () => { .run(); }); + it("requests combined statements if combined=true in the request message", () => { + const req = { + payload: new cockroach.server.serverpb.StatementsRequest({ + combined: true, + }), + }; + expectSaga(requestStatementsSaga, req) + .provide([[matchers.call.fn(getCombinedStatements), statements]]) + .put(actions.received(statements)) + .withReducer(reducer) + .hasFinalState({ + data: statements, + lastError: null, + valid: true, + }) + .run(); + }); + it("returns error on failed request", () => { const error = new Error("Failed request"); expectSaga(requestStatementsSaga) diff --git a/pkg/ui/workspaces/cluster-ui/src/store/statements/statements.sagas.ts b/pkg/ui/workspaces/cluster-ui/src/store/statements/statements.sagas.ts index 4b82419cbe9d..a1c00ca44563 100644 --- a/pkg/ui/workspaces/cluster-ui/src/store/statements/statements.sagas.ts +++ b/pkg/ui/workspaces/cluster-ui/src/store/statements/statements.sagas.ts @@ -8,20 +8,34 @@ // by the Apache License, Version 2.0, included in the file // licenses/APL.txt. +import { PayloadAction } from "@reduxjs/toolkit"; import { all, call, put, delay, takeLatest } from "redux-saga/effects"; -import { getStatements } from "src/api/statementsApi"; -import { actions } from "./statements.reducer"; +import Long from "long"; +import { cockroach } from "@cockroachlabs/crdb-protobuf-client"; +import { + getStatements, + getCombinedStatements, + StatementsRequest, +} from "src/api/statementsApi"; +import { actions as localStorageActions } from "src/store/localStorage"; +import { actions, UpdateDateRangePayload } from "./statements.reducer"; import { rootActions } from "../reducers"; import { CACHE_INVALIDATION_PERIOD, throttleWithReset } from "src/store/utils"; -export function* refreshStatementsSaga() { - yield put(actions.request()); +export function* refreshStatementsSaga( + action?: PayloadAction, +) { + yield put(actions.request(action?.payload)); } -export function* requestStatementsSaga(): any { +export function* requestStatementsSaga( + action?: PayloadAction, +): any { try { - const result = yield call(getStatements); + const result = yield action?.payload?.combined + ? call(getCombinedStatements, action.payload) + : call(getStatements); yield put(actions.received(result)); } catch (e) { yield put(actions.failed(e)); @@ -33,6 +47,25 @@ export function* receivedStatementsSaga(delayMs: number) { yield put(actions.invalidated()); } +export function* updateStatementesDateRangeSaga( + action: PayloadAction, +) { + const { start, end } = action.payload; + yield put( + localStorageActions.update({ + key: "dateRange/StatementsPage", + value: { start, end }, + }), + ); + yield put(actions.invalidated()); + const req = new cockroach.server.serverpb.StatementsRequest({ + combined: true, + start: Long.fromNumber(start), + end: Long.fromNumber(end), + }); + yield put(actions.refresh(req)); +} + export function* statementsSaga( cacheInvalidationPeriod: number = CACHE_INVALIDATION_PERIOD, ) { @@ -49,5 +82,6 @@ export function* statementsSaga( receivedStatementsSaga, cacheInvalidationPeriod, ), + takeLatest(actions.updateDateRange, updateStatementesDateRangeSaga), ]); } diff --git a/pkg/ui/workspaces/cluster-ui/src/store/transactions/transactions.reducer.ts b/pkg/ui/workspaces/cluster-ui/src/store/transactions/transactions.reducer.ts index 29e3a66c1c2b..4c8d9ddd4c6f 100644 --- a/pkg/ui/workspaces/cluster-ui/src/store/transactions/transactions.reducer.ts +++ b/pkg/ui/workspaces/cluster-ui/src/store/transactions/transactions.reducer.ts @@ -11,6 +11,7 @@ import { createSlice, PayloadAction } from "@reduxjs/toolkit"; import { cockroach } from "@cockroachlabs/crdb-protobuf-client"; import { DOMAIN_NAME, noopReducer } from "../utils"; +import { StatementsRequest } from "src/api/statementsApi"; type StatementsResponse = cockroach.server.serverpb.StatementsResponse; @@ -43,8 +44,8 @@ const transactionsSlice = createSlice({ state.valid = false; }, // Define actions that don't change state - refresh: noopReducer, - request: noopReducer, + refresh: (_, action?: PayloadAction) => {}, + request: (_, action?: PayloadAction) => {}, }, }); diff --git a/pkg/ui/workspaces/cluster-ui/src/store/transactions/transactions.sagas.spec.ts b/pkg/ui/workspaces/cluster-ui/src/store/transactions/transactions.sagas.spec.ts index 2ee891fb1bca..12ad3245c633 100644 --- a/pkg/ui/workspaces/cluster-ui/src/store/transactions/transactions.sagas.spec.ts +++ b/pkg/ui/workspaces/cluster-ui/src/store/transactions/transactions.sagas.spec.ts @@ -13,7 +13,7 @@ import { throwError } from "redux-saga-test-plan/providers"; import * as matchers from "redux-saga-test-plan/matchers"; import { cockroach } from "@cockroachlabs/crdb-protobuf-client"; -import { getStatements } from "src/api/statementsApi"; +import { getCombinedStatements, getStatements } from "src/api/statementsApi"; import { receivedTransactionsSaga, @@ -50,6 +50,24 @@ describe("TransactionsPage sagas", () => { .run(); }); + it("requests combined statements if combined=true in the request message", () => { + const req = { + payload: new cockroach.server.serverpb.StatementsRequest({ + combined: true, + }), + }; + expectSaga(requestTransactionsSaga, req) + .provide([[matchers.call.fn(getCombinedStatements), statements]]) + .put(actions.received(statements)) + .withReducer(reducer) + .hasFinalState({ + data: statements, + lastError: null, + valid: true, + }) + .run(); + }); + it("returns error on failed request", () => { const error = new Error("Failed request"); expectSaga(requestTransactionsSaga) diff --git a/pkg/ui/workspaces/cluster-ui/src/store/transactions/transactions.sagas.ts b/pkg/ui/workspaces/cluster-ui/src/store/transactions/transactions.sagas.ts index 1766800b0941..076b8e8f6714 100644 --- a/pkg/ui/workspaces/cluster-ui/src/store/transactions/transactions.sagas.ts +++ b/pkg/ui/workspaces/cluster-ui/src/store/transactions/transactions.sagas.ts @@ -8,20 +8,31 @@ // by the Apache License, Version 2.0, included in the file // licenses/APL.txt. +import { PayloadAction } from "@reduxjs/toolkit"; import { all, call, put, delay, takeLatest } from "redux-saga/effects"; -import { getStatements } from "src/api/statementsApi"; +import { + getStatements, + getCombinedStatements, + StatementsRequest, +} from "src/api/statementsApi"; import { actions } from "./transactions.reducer"; import { rootActions } from "../reducers"; import { CACHE_INVALIDATION_PERIOD, throttleWithReset } from "src/store/utils"; -export function* refreshTransactionsSaga() { - yield put(actions.request()); +export function* refreshTransactionsSaga( + action?: PayloadAction, +) { + yield put(actions.request(action?.payload)); } -export function* requestTransactionsSaga(): any { +export function* requestTransactionsSaga( + action?: PayloadAction, +): any { try { - const result = yield call(getStatements); + const result = yield action?.payload?.combined + ? call(getCombinedStatements, action.payload) + : call(getStatements); yield put(actions.received(result)); } catch (e) { yield put(actions.failed(e)); diff --git a/pkg/ui/workspaces/cluster-ui/src/transactionsPage/transactionsPage.tsx b/pkg/ui/workspaces/cluster-ui/src/transactionsPage/transactionsPage.tsx index e9708bdd134c..9bfb6c96ff5f 100644 --- a/pkg/ui/workspaces/cluster-ui/src/transactionsPage/transactionsPage.tsx +++ b/pkg/ui/workspaces/cluster-ui/src/transactionsPage/transactionsPage.tsx @@ -12,8 +12,10 @@ import React from "react"; import * as protos from "@cockroachlabs/crdb-protobuf-client"; import classNames from "classnames/bind"; import styles from "../statementsPage/statementsPage.module.scss"; +import moment, { Moment } from "moment"; import { RouteComponentProps } from "react-router-dom"; import { TransactionInfo, TransactionsTable } from "../transactionsTable"; +import { DateRange } from "src/dateRange"; import { TransactionDetails } from "../transactionDetails"; import { ISortedTablePagination, SortSetting } from "../sortedtable"; import { Pagination } from "../pagination"; @@ -47,6 +49,7 @@ import { getFiltersFromQueryString, } from "../queryFilter"; import { UIConfigState } from "../store/uiConfig"; +import { StatementsRequest } from "src/api/statementsApi"; type IStatementsResponse = protos.cockroach.server.serverpb.IStatementsResponse; type TransactionStats = protos.cockroach.sql.ITransactionStatistics; @@ -64,6 +67,7 @@ interface TState { export interface TransactionsPageStateProps { data: IStatementsResponse; + dateRange?: [Moment, Moment]; nodeRegions: { [nodeId: string]: string }; error?: Error | null; pageSize?: number; @@ -71,14 +75,25 @@ export interface TransactionsPageStateProps { } export interface TransactionsPageDispatchProps { - refreshData: () => void; + refreshData: (req?: StatementsRequest) => void; resetSQLStats: () => void; + onDateRangeChange?: (start: Moment, end: Moment) => void; } export type TransactionsPageProps = TransactionsPageStateProps & TransactionsPageDispatchProps & RouteComponentProps; +function statementsRequestFromProps( + props: TransactionsPageProps, +): protos.cockroach.server.serverpb.StatementsRequest | null { + if (props.isTenant || props.dateRange == null) return null; + return new protos.cockroach.server.serverpb.StatementsRequest({ + combined: true, + start: Long.fromNumber(props.dateRange[0].unix()), + end: Long.fromNumber(props.dateRange[1].unix()), + }); +} export class TransactionsPage extends React.Component< TransactionsPageProps, TState @@ -108,11 +123,16 @@ export class TransactionsPage extends React.Component< transactionStats: null, }; + refreshData = () => { + const req = statementsRequestFromProps(this.props); + this.props.refreshData(req); + }; + componentDidMount() { - this.props.refreshData(); + this.refreshData(); } componentDidUpdate() { - this.props.refreshData(); + this.refreshData(); } syncHistory = (params: Record) => { @@ -216,6 +236,20 @@ export class TransactionsPage extends React.Component< return new Date(Number(this.props.data?.last_reset.seconds) * 1000); }; + changeDateRange = (start: Moment, end: Moment): void => { + if (this.props.onDateRangeChange) { + this.props.onDateRangeChange(start, end); + } + }; + + resetTime = (): void => { + // Default range to reset to is one hour ago. + this.changeDateRange( + moment.utc().subtract(1, "hours"), + moment.utc().add(1, "minute"), + ); + }; + renderTransactionsList() { return (
@@ -295,6 +329,25 @@ export class TransactionsPage extends React.Component< showNodes={nodes.length > 1} /> + {this.props.dateRange && ( + <> + + + + + + + + )}
({ - refreshData: () => dispatch(transactionsActions.refresh()), + refreshData: (req?: StatementsRequest) => + dispatch(transactionsActions.refresh(req)), resetSQLStats: () => dispatch(resetSQLStatsActions.request()), + onDateRangeChange: (start: Moment, end: Moment) => { + dispatch( + statementsActions.updateDateRange({ + start: start.unix(), + end: end.unix(), + }), + ); + }, }), )(TransactionsPage), ); diff --git a/pkg/ui/workspaces/db-console/src/redux/statements/statementsActions.ts b/pkg/ui/workspaces/db-console/src/redux/statements/statementsActions.ts index 63611ec7ae77..b38df5dd905b 100644 --- a/pkg/ui/workspaces/db-console/src/redux/statements/statementsActions.ts +++ b/pkg/ui/workspaces/db-console/src/redux/statements/statementsActions.ts @@ -10,6 +10,7 @@ import { Action } from "redux"; import { PayloadAction } from "src/interfaces/action"; +import { Moment } from "moment"; export const CREATE_STATEMENT_DIAGNOSTICS_REPORT = "cockroachui/statements/CREATE_STATEMENT_DIAGNOSTICS_REPORT"; @@ -57,3 +58,28 @@ export function createOpenDiagnosticsModalAction( }, }; } + +/*************************************** + Combined Stats Actions +****************************************/ + +export const SET_COMBINED_STATEMENTS_RANGE = + "cockroachui/statements/SET_COMBINED_STATEMENTS_RANGE"; + +export type CombinedStatementsPayload = { + start: Moment; + end: Moment; +}; + +export function setCombinedStatementsDateRangeAction( + start: Moment, + end: Moment, +): PayloadAction { + return { + type: SET_COMBINED_STATEMENTS_RANGE, + payload: { + start, + end, + }, + }; +} diff --git a/pkg/ui/workspaces/db-console/src/redux/statements/statementsSagas.ts b/pkg/ui/workspaces/db-console/src/redux/statements/statementsSagas.ts index d72cdc35423f..00d24167abf2 100644 --- a/pkg/ui/workspaces/db-console/src/redux/statements/statementsSagas.ts +++ b/pkg/ui/workspaces/db-console/src/redux/statements/statementsSagas.ts @@ -8,7 +8,7 @@ // by the Apache License, Version 2.0, included in the file // licenses/APL.txt. -import { all, call, put, takeEvery } from "redux-saga/effects"; +import { all, call, put, takeEvery, takeLatest } from "redux-saga/effects"; import { PayloadAction } from "src/interfaces/action"; import { createStatementDiagnosticsReport } from "src/util/api"; @@ -17,14 +17,21 @@ import { DiagnosticsReportPayload, createStatementDiagnosticsReportCompleteAction, createStatementDiagnosticsReportFailedAction, + CombinedStatementsPayload, + SET_COMBINED_STATEMENTS_RANGE, } from "./statementsActions"; import { cockroach } from "src/js/protos"; import CreateStatementDiagnosticsReportRequest = cockroach.server.serverpb.CreateStatementDiagnosticsReportRequest; +import CombinedStatementsRequest = cockroach.server.serverpb.StatementsRequest; import { invalidateStatementDiagnosticsRequests, refreshStatementDiagnosticsRequests, + invalidateStatements, + refreshStatements, } from "src/redux/apiReducers"; import { createStatementDiagnosticsAlertLocalSetting } from "src/redux/alerts"; +import { statementsDateRangeLocalSetting } from "src/redux/statementsDateRange"; +import Long from "long"; export function* createDiagnosticsReportSaga( action: PayloadAction, @@ -56,8 +63,30 @@ export function* createDiagnosticsReportSaga( } } +export function* setCombinedStatementsDateRangeSaga( + action: PayloadAction, +) { + const { start, end } = action.payload; + yield put( + statementsDateRangeLocalSetting.set({ + start: start.unix(), + end: end.unix(), + }), + ); + const req = new CombinedStatementsRequest({ + start: Long.fromNumber(start.unix()), + end: Long.fromNumber(end.unix()), + }); + yield put(invalidateStatements()); + yield put(refreshStatements(req) as any); +} + export function* statementsSaga() { yield all([ takeEvery(CREATE_STATEMENT_DIAGNOSTICS_REPORT, createDiagnosticsReportSaga), + takeLatest( + SET_COMBINED_STATEMENTS_RANGE, + setCombinedStatementsDateRangeSaga, + ), ]); } diff --git a/pkg/ui/workspaces/db-console/src/redux/statementsDateRange.ts b/pkg/ui/workspaces/db-console/src/redux/statementsDateRange.ts new file mode 100644 index 000000000000..1626c1d97016 --- /dev/null +++ b/pkg/ui/workspaces/db-console/src/redux/statementsDateRange.ts @@ -0,0 +1,34 @@ +// 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 moment from "moment"; +import { LocalSetting } from "./localsettings"; +import { AdminUIState } from "./state"; + +export type CombinedStatementsDateRangePayload = { + start: number; + end: number; +}; + +const localSettingsSelector = (state: AdminUIState) => state.localSettings; + +// The default range for statements to display is one hour ago. +const oneHourAgo = { + start: moment + .utc() + .subtract(1, "hours") + .unix(), + end: moment.utc().unix() + 60, // Add 1 minute to account for potential lag +}; + +export const statementsDateRangeLocalSetting = new LocalSetting< + AdminUIState, + CombinedStatementsDateRangePayload +>("statements_date_range", localSettingsSelector, oneHourAgo); diff --git a/pkg/ui/workspaces/db-console/src/util/api.ts b/pkg/ui/workspaces/db-console/src/util/api.ts index af7b92809052..bbdab5cdef07 100644 --- a/pkg/ui/workspaces/db-console/src/util/api.ts +++ b/pkg/ui/workspaces/db-console/src/util/api.ts @@ -129,6 +129,8 @@ export type CreateStatementDiagnosticsReportResponseMessage = protos.cockroach.s export type StatementDiagnosticsRequestMessage = protos.cockroach.server.serverpb.StatementDiagnosticsRequest; export type StatementDiagnosticsResponseMessage = protos.cockroach.server.serverpb.StatementDiagnosticsResponse; +export type StatementsRequestMessage = protos.cockroach.server.serverpb.StatementsRequest; + export type ResetSQLStatsRequestMessage = protos.cockroach.server.serverpb.ResetSQLStatsRequest; export type ResetSQLStatsResponseMessage = protos.cockroach.server.serverpb.ResetSQLStatsResponse; @@ -670,11 +672,17 @@ export function getStores( // getStatements returns statements the cluster has recently executed, and some stats about them. export function getStatements( + req: StatementsRequestMessage, timeout?: moment.Duration, ): Promise { + const queryStr = propsToQueryString({ + combined: true, + start: req.start.toInt(), + end: req.end.toInt(), + }); return timeoutFetch( serverpb.StatementsResponse, - `${STATUS_PREFIX}/statements`, + `${STATUS_PREFIX}/statements?${queryStr}`, null, timeout, ); diff --git a/pkg/ui/workspaces/db-console/src/views/statements/statementDetails.tsx b/pkg/ui/workspaces/db-console/src/views/statements/statementDetails.tsx index fc9a49165227..56c90698dcaa 100644 --- a/pkg/ui/workspaces/db-console/src/views/statements/statementDetails.tsx +++ b/pkg/ui/workspaces/db-console/src/views/statements/statementDetails.tsx @@ -56,6 +56,7 @@ import { trackDownloadDiagnosticsBundleAction, trackStatementDetailsSubnavSelectionAction, } from "src/redux/analyticsActions"; +import { selectDateRange } from "src/views/statements/statementsPage"; interface Fraction { numerator: number; @@ -154,6 +155,7 @@ export const selectStatement = createSelector( (_state: AdminUIState, props: RouteComponentProps) => props, (statementsState, props) => { const statements = statementsState.data?.statements; + if (!statements) { return null; } @@ -192,6 +194,7 @@ const mapStateToProps = ( return { statement, statementsError: state.cachedData.statements.lastError, + dateRange: selectDateRange(state), nodeNames: nodeDisplayNameByIDSelector(state), nodeRegions: nodeRegionsByIDSelector(state), diagnosticsReports: selectDiagnosticsReportsByStatementFingerprint( diff --git a/pkg/ui/workspaces/db-console/src/views/statements/statementsPage.fixture.ts b/pkg/ui/workspaces/db-console/src/views/statements/statementsPage.fixture.ts index 3f4cbc0f0eed..42097c4169cf 100644 --- a/pkg/ui/workspaces/db-console/src/views/statements/statementsPage.fixture.ts +++ b/pkg/ui/workspaces/db-console/src/views/statements/statementsPage.fixture.ts @@ -10,12 +10,14 @@ import { StatementsPageProps } from "@cockroachlabs/cluster-ui"; import { createMemoryHistory } from "history"; +import moment from "moment"; import Long from "long"; import * as protos from "src/js/protos"; import { refreshStatementDiagnosticsRequests, refreshStatements, } from "src/redux/apiReducers"; + type IStatementStatistics = protos.cockroach.sql.IStatementStatistics; const history = createMemoryHistory({ initialEntries: ["/statements"] }); @@ -503,9 +505,11 @@ const statementsPagePropsFixture: StatementsPageProps = { apps: ["(internal)", "movr", "$ cockroach demo"], totalFingerprints: 95, lastReset: "2020-04-13 07:22:23", + dateRange: [moment.utc("2021.08.08"), moment.utc("2021.08.12")], dismissAlertMessage: () => {}, refreshStatementDiagnosticsRequests: (() => {}) as typeof refreshStatementDiagnosticsRequests, refreshStatements: (() => {}) as typeof refreshStatements, + onDateRangeChange: () => {}, onActivateStatementDiagnostics: _ => {}, onDiagnosticsModalOpen: _ => {}, onPageChanged: _ => {}, diff --git a/pkg/ui/workspaces/db-console/src/views/statements/statementsPage.tsx b/pkg/ui/workspaces/db-console/src/views/statements/statementsPage.tsx index 736ac7407948..972c9b36104a 100644 --- a/pkg/ui/workspaces/db-console/src/views/statements/statementsPage.tsx +++ b/pkg/ui/workspaces/db-console/src/views/statements/statementsPage.tsx @@ -11,6 +11,7 @@ import { connect } from "react-redux"; import { createSelector } from "reselect"; import { RouteComponentProps, withRouter } from "react-router-dom"; +import moment, { Moment } from "moment"; import * as protos from "src/js/protos"; import { refreshStatementDiagnosticsRequests, @@ -32,12 +33,14 @@ import { TimestampToMoment } from "src/util/convert"; import { PrintTime } from "src/views/reports/containers/range/print"; import { selectDiagnosticsReportsPerStatement } from "src/redux/statements/statementsSelectors"; import { createStatementDiagnosticsAlertLocalSetting } from "src/redux/alerts"; +import { statementsDateRangeLocalSetting } from "src/redux/statementsDateRange"; import { getMatchParamByName } from "src/util/query"; import { StatementsPage, AggregateStatistics } from "@cockroachlabs/cluster-ui"; import { createOpenDiagnosticsModalAction, createStatementDiagnosticsReportAction, + setCombinedStatementsDateRangeAction, } from "src/redux/statements"; import { trackDownloadDiagnosticsBundleAction, @@ -199,6 +202,13 @@ export const selectLastReset = createSelector( }, ); +export const selectDateRange = createSelector( + statementsDateRangeLocalSetting.selector, + (state: { start: number; end: number }): [Moment, Moment] => { + return [moment.unix(state.start), moment.unix(state.end)]; + }, +); + export const statementColumnsLocalSetting = new LocalSetting( "create_statement_columns", (state: AdminUIState) => state.localSettings, @@ -210,6 +220,7 @@ export default withRouter( (state: AdminUIState, props: RouteComponentProps) => ({ statements: selectStatements(state, props), statementsError: state.cachedData.statements.lastError, + dateRange: selectDateRange(state), apps: selectApps(state), databases: selectDatabases(state), totalFingerprints: selectTotalFingerprints(state), @@ -218,7 +229,8 @@ export default withRouter( nodeRegions: nodeRegionsByIDSelector(state), }), { - refreshStatements, + refreshStatements: refreshStatements, + onDateRangeChange: setCombinedStatementsDateRangeAction, refreshStatementDiagnosticsRequests, resetSQLStats: resetSQLStatsAction, dismissAlertMessage: () => diff --git a/pkg/ui/workspaces/db-console/src/views/transactions/transactionsPage.tsx b/pkg/ui/workspaces/db-console/src/views/transactions/transactionsPage.tsx index 0abb49e6c9de..890db9ae9850 100644 --- a/pkg/ui/workspaces/db-console/src/views/transactions/transactionsPage.tsx +++ b/pkg/ui/workspaces/db-console/src/views/transactions/transactionsPage.tsx @@ -11,6 +11,7 @@ import { connect } from "react-redux"; import { createSelector } from "reselect"; import { withRouter } from "react-router-dom"; +import moment from "moment"; import { refreshStatements } from "src/redux/apiReducers"; import { resetSQLStatsAction } from "src/redux/sqlStats"; import { CachedDataReducerState } from "src/redux/cachedDataReducer"; @@ -22,6 +23,8 @@ import { PrintTime } from "src/views/reports/containers/range/print"; import { TransactionsPage } from "@cockroachlabs/cluster-ui"; import { nodeRegionsByIDSelector } from "src/redux/nodes"; +import { statementsDateRangeLocalSetting } from "src/redux/statementsDateRange"; +import { setCombinedStatementsDateRangeAction } from "src/redux/statements"; // selectStatements returns the array of AggregateStatistics to show on the // TransactionsPage, based on if the appAttr route parameter is set. @@ -50,11 +53,19 @@ const selectLastError = createSelector( (state: CachedDataReducerState) => state.lastError, ); +export const selectDateRange = createSelector( + statementsDateRangeLocalSetting.selector, + (state: { start: number; end: number }): [moment.Moment, moment.Moment] => { + return [moment.unix(state.start), moment.unix(state.end)]; + }, +); + const TransactionsPageConnected = withRouter( connect( (state: AdminUIState) => ({ data: selectData(state), statementsError: state.cachedData.statements.lastError, + dateRange: selectDateRange(state), lastReset: selectLastReset(state), error: selectLastError(state), nodeRegions: nodeRegionsByIDSelector(state), @@ -62,6 +73,7 @@ const TransactionsPageConnected = withRouter( { refreshData: refreshStatements, resetSQLStats: resetSQLStatsAction, + onDateRangeChange: setCombinedStatementsDateRangeAction, }, )(TransactionsPage), );