From e798bc25d230a31bc306e1aad5774dfe3ebcf926 Mon Sep 17 00:00:00 2001 From: Xin Hao Zhang Date: Sun, 5 Mar 2023 23:48:32 -0500 Subject: [PATCH] ui: add limit and sort to fingerprints pages This commit adds new knobs to the sql stats fingerprint pages. Users can now specify a limit and sort priority with their sql stats fingerprints requests. Closes: #97876 Part of: #97875 Release note: None --- docs/generated/http/full.md | 2 + .../cluster-ui/src/api/statementsApi.ts | 44 ++++++- .../statementsPage/statementsPage.fixture.ts | 5 + .../src/statementsPage/statementsPage.tsx | 107 ++++++++++++++---- .../statementsPageConnected.tsx | 18 ++- .../localStorage/localStorage.reducer.ts | 57 ++++++++++ .../src/store/sqlStats/sqlStats.reducer.ts | 2 + .../statementDetails.sagas.ts | 2 +- .../cluster-ui/src/store/utils/selectors.ts | 20 ++++ .../transactionDetails/transactionDetails.tsx | 14 ++- .../transactionDetailsConnected.tsx | 8 +- .../src/transactionsPage/transactionsPage.tsx | 93 +++++++++++---- .../transactionsPageConnected.tsx | 20 +++- .../cluster-ui/src/util/constants.ts | 2 - .../src/util/sqlActivityConstants.ts | 45 ++++++++ .../db-console/src/redux/apiReducers.ts | 8 +- pkg/ui/workspaces/db-console/src/util/api.ts | 17 --- .../src/views/statements/statementsPage.tsx | 19 +++- .../views/transactions/transactionDetails.tsx | 4 + .../views/transactions/transactionsPage.tsx | 17 +++ 20 files changed, 419 insertions(+), 85 deletions(-) create mode 100644 pkg/ui/workspaces/cluster-ui/src/util/sqlActivityConstants.ts diff --git a/docs/generated/http/full.md b/docs/generated/http/full.md index 863d2b8fece5..88a19ed22503 100644 --- a/docs/generated/http/full.md +++ b/docs/generated/http/full.md @@ -4005,6 +4005,7 @@ Support status: [reserved](#support-status) | start | [int64](#cockroach.server.serverpb.CombinedStatementsStatsRequest-int64) | | Unix time range for aggregated statements. | [reserved](#support-status) | | end | [int64](#cockroach.server.serverpb.CombinedStatementsStatsRequest-int64) | | | [reserved](#support-status) | | fetch_mode | [CombinedStatementsStatsRequest.FetchMode](#cockroach.server.serverpb.CombinedStatementsStatsRequest-cockroach.server.serverpb.CombinedStatementsStatsRequest.FetchMode) | | Note that if fetch_mode is set to transactions only, we will also include the statement statistics for the stmts in the transactions response. This is more of a hack-y method to get the complete stats for txns, because in the client we need to fill in some txn stats info from its stmt stats, such as the query string.

We prefer this hackier method right now to reduce surface area for backporting these changes, but in the future we will introduce more endpoints to properly organize these differing requests. TODO (xinhaoz) - Split this API into stmts and txns properly instead of using this param. | [reserved](#support-status) | +| limit | [int64](#cockroach.server.serverpb.CombinedStatementsStatsRequest-int64) | | | [reserved](#support-status) | @@ -4019,6 +4020,7 @@ Support status: [reserved](#support-status) | Field | Type | Label | Description | Support status | | ----- | ---- | ----- | ----------- | -------------- | | stats_type | [CombinedStatementsStatsRequest.StatsType](#cockroach.server.serverpb.CombinedStatementsStatsRequest-cockroach.server.serverpb.CombinedStatementsStatsRequest.StatsType) | | | [reserved](#support-status) | +| sort | [StatsSortOptions](#cockroach.server.serverpb.CombinedStatementsStatsRequest-cockroach.server.serverpb.StatsSortOptions) | | | [reserved](#support-status) | diff --git a/pkg/ui/workspaces/cluster-ui/src/api/statementsApi.ts b/pkg/ui/workspaces/cluster-ui/src/api/statementsApi.ts index e2e136b9cea8..a364b3aab376 100644 --- a/pkg/ui/workspaces/cluster-ui/src/api/statementsApi.ts +++ b/pkg/ui/workspaces/cluster-ui/src/api/statementsApi.ts @@ -9,8 +9,10 @@ // licenses/APL.txt. import { cockroach } from "@cockroachlabs/crdb-protobuf-client"; -import { fetchData } from "src/api"; +import { fetchData } from "src/api/fetchData"; import { propsToQueryString } from "src/util"; +import Long from "long"; +import moment from "moment"; const STATEMENTS_PATH = "/_status/combinedstmts"; const STATEMENT_DETAILS_PATH = "/_status/stmtdetails"; @@ -34,13 +36,49 @@ export type ErrorWithKey = { key: string; }; +export const SqlStatsSortOptions = cockroach.server.serverpb.StatsSortOptions; +export type SqlStatsSortType = cockroach.server.serverpb.StatsSortOptions; + +export const DEFAULT_STATS_REQ_OPTIONS = { + limit: 100, + sort: SqlStatsSortOptions.SERVICE_LAT, +}; + +// The required fields to create a stmts request. +type StmtReqFields = { + limit: number; + sort: SqlStatsSortType; + start: moment.Moment; + end: moment.Moment; +}; + +export function createCombinedStmtsRequest({ + limit, + sort, + start, + end, +}: StmtReqFields): StatementsRequest { + return new cockroach.server.serverpb.CombinedStatementsStatsRequest({ + start: Long.fromNumber(start.unix()), + end: Long.fromNumber(end.unix()), + limit: Long.fromNumber(limit ?? DEFAULT_STATS_REQ_OPTIONS.limit), + fetch_mode: new cockroach.server.serverpb.CombinedStatementsStatsRequest.FetchMode( + { + sort: sort, + }, + ), + }); +} + export const getCombinedStatements = ( req: StatementsRequest, -): Promise => { +): Promise => { const queryStr = propsToQueryString({ start: req.start.toInt(), end: req.end.toInt(), "fetch_mode.stats_type": FetchStatsMode.StmtStatsOnly, + "fetch_mode.sort": req.fetch_mode.sort, + limit: req.limit.toInt(), }); return fetchData( cockroach.server.serverpb.StatementsResponse, @@ -58,6 +96,8 @@ export const getFlushedTxnStatsApi = ( start: req.start.toInt(), end: req.end.toInt(), "fetch_mode.stats_type": FetchStatsMode.TxnStatsOnly, + "fetch_mode.sort": req.fetch_mode?.sort, + limit: req.limit.toInt() ?? DEFAULT_STATS_REQ_OPTIONS.limit, }); return fetchData( cockroach.server.serverpb.StatementsResponse, 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 1b5c425f667c..a198e76e4836 100644 --- a/pkg/ui/workspaces/cluster-ui/src/statementsPage/statementsPage.fixture.ts +++ b/pkg/ui/workspaces/cluster-ui/src/statementsPage/statementsPage.fixture.ts @@ -17,6 +17,7 @@ import { noop } from "lodash"; import * as protos from "@cockroachlabs/crdb-protobuf-client"; import { cockroach } from "@cockroachlabs/crdb-protobuf-client"; import { RequestError } from "src/util"; +import { DEFAULT_STATS_REQ_OPTIONS } from "../api/statementsApi"; type IStatementDiagnosticsReport = cockroach.server.serverpb.IStatementDiagnosticsReport; type IStatementStatistics = protos.cockroach.sql.IStatementStatistics; @@ -913,6 +914,8 @@ const statementsPagePropsFixture: StatementsPageProps = { }, ], statementsError: null, + limit: DEFAULT_STATS_REQ_OPTIONS.limit, + reqSortSetting: DEFAULT_STATS_REQ_OPTIONS.sort, timeScale: { windowSize: moment.duration(5, "day"), sampleSize: moment.duration(5, "minutes"), @@ -940,6 +943,8 @@ const statementsPagePropsFixture: StatementsPageProps = { onColumnsChange: noop, onSortingChange: noop, onFilterChange: noop, + onChangeLimit: noop, + onChangeReqSort: noop, }; export const statementsPagePropsWithRequestError: StatementsPageProps = { diff --git a/pkg/ui/workspaces/cluster-ui/src/statementsPage/statementsPage.tsx b/pkg/ui/workspaces/cluster-ui/src/statementsPage/statementsPage.tsx index 23f4f0d7e839..59f39a4f47d9 100644 --- a/pkg/ui/workspaces/cluster-ui/src/statementsPage/statementsPage.tsx +++ b/pkg/ui/workspaces/cluster-ui/src/statementsPage/statementsPage.tsx @@ -60,15 +60,15 @@ import styles from "./statementsPage.module.scss"; import { EmptyStatementsPlaceholder } from "./emptyStatementsPlaceholder"; import { cockroach, google } from "@cockroachlabs/crdb-protobuf-client"; import { InlineAlert } from "@cockroachlabs/ui-components"; - -type IStatementDiagnosticsReport = cockroach.server.serverpb.IStatementDiagnosticsReport; -type IDuration = google.protobuf.IDuration; import sortableTableStyles from "src/sortedtable/sortedtable.module.scss"; import ColumnsSelector from "../columnsSelector/columnsSelector"; import { SelectOption } from "../multiSelectCheckbox/multiSelectCheckbox"; import { UIConfigState } from "../store"; -import { StatementsRequest } from "src/api/statementsApi"; -import Long from "long"; +import { + SqlStatsSortType, + StatementsRequest, + createCombinedStmtsRequest, +} from "src/api/statementsApi"; import ClearStats from "../sqlActivity/clearStats"; import LoadingError from "../sqlActivity/errorComponent"; import { @@ -83,7 +83,17 @@ import { import { commonStyles } from "../common"; import moment from "moment"; -import { STATS_LONG_LOADING_DURATION } from "src/util/constants"; +import { Dropdown } from "src/dropdown"; +import { + STATS_LONG_LOADING_DURATION, + limitOptions, + stmtRequestSortOptions, + getSortLabel, +} from "src/util/sqlActivityConstants"; +import { Button } from "src/button"; + +type IStatementDiagnosticsReport = cockroach.server.serverpb.IStatementDiagnosticsReport; +type IDuration = google.protobuf.IDuration; const cx = classNames.bind(styles); const sortableTableCx = classNames.bind(sortableTableStyles); @@ -120,6 +130,8 @@ export interface StatementsPageDispatchProps { onStatementClick?: (statement: string) => void; onColumnsChange?: (selectedColumns: string[]) => void; onTimeScaleChange: (ts: TimeScale) => void; + onChangeLimit: (limit: number) => void; + onChangeReqSort: (sort: SqlStatsSortType) => void; } export interface StatementsPageStateProps { @@ -128,6 +140,8 @@ export interface StatementsPageStateProps { isReqInFlight: boolean; lastUpdated: moment.Moment | null; timeScale: TimeScale; + limit: number; + reqSortSetting: SqlStatsSortType; statementsError: Error | null; apps: string[]; databases: string[]; @@ -147,17 +161,27 @@ export interface StatementsPageState { pagination: ISortedTablePagination; filters?: Filters; activeFilters?: number; + timeScale: TimeScale; + limit: number; + reqSortSetting: SqlStatsSortType; } export type StatementsPageProps = StatementsPageDispatchProps & StatementsPageStateProps & RouteComponentProps; -function stmtsRequestFromTimeScale(ts: TimeScale): StatementsRequest { - const [start, end] = toRoundedDateRange(ts); - return new cockroach.server.serverpb.CombinedStatementsStatsRequest({ - start: Long.fromNumber(start.unix()), - end: Long.fromNumber(end.unix()), +type RequestParams = Pick< + StatementsPageState, + "limit" | "reqSortSetting" | "timeScale" +>; + +function stmtsRequestFromParams(params: RequestParams): StatementsRequest { + const [start, end] = toRoundedDateRange(params.timeScale); + return createCombinedStmtsRequest({ + start, + end, + limit: params.limit, + sort: params.reqSortSetting, }); } @@ -197,6 +221,9 @@ export class StatementsPage extends React.Component< pageSize: 20, current: 1, }, + limit: this.props.limit, + timeScale: this.props.timeScale, + reqSortSetting: this.props.reqSortSetting, }; const stateFromHistory = this.getStateFromHistory(); this.state = merge(defaultState, stateFromHistory); @@ -256,9 +283,17 @@ export class StatementsPage extends React.Component< }; changeTimeScale = (ts: TimeScale): void => { - if (this.props.onTimeScaleChange) { - this.props.onTimeScaleChange(ts); - } + this.setState(prevState => ({ + ...prevState, + timeScale: ts, + })); + }; + + updateRequestParams = (): void => { + this.props.onChangeLimit(this.state.limit); + this.props.onChangeReqSort(this.state.reqSortSetting); + this.props.onTimeScaleChange(this.state.timeScale); + this.refreshStatements(); }; resetPagination = (): void => { @@ -273,7 +308,7 @@ export class StatementsPage extends React.Component< }; refreshStatements = (): void => { - const req = stmtsRequestFromTimeScale(this.props.timeScale); + const req = stmtsRequestFromParams(this.state); this.props.refreshStatements(req); }; @@ -337,7 +372,7 @@ export class StatementsPage extends React.Component< ); } - componentDidUpdate = (prevProps: StatementsPageProps): void => { + componentDidUpdate = (): void => { this.updateQueryParams(); if (!this.props.isTenant) { this.props.refreshNodes(); @@ -345,13 +380,6 @@ export class StatementsPage extends React.Component< this.props.refreshStatementDiagnosticsRequests(); } } - - if ( - prevProps.timeScale !== this.props.timeScale || - (prevProps.isDataValid && !this.props.isDataValid) - ) { - this.refreshStatements(); - } }; componentWillUnmount(): void { @@ -360,7 +388,10 @@ export class StatementsPage extends React.Component< onChangePage = (current: number): void => { const { pagination } = this.state; - this.setState({ pagination: { ...pagination, current } }); + this.setState(prevState => ({ + ...prevState, + pagination: { ...pagination, current }, + })); this.props.onPageChanged != null && this.props.onPageChanged(current); }; @@ -522,6 +553,14 @@ export class StatementsPage extends React.Component< ); }; + onChangeLimit = (newLimit: number): void => { + this.setState(prevState => ({ ...prevState, limit: newLimit })); + }; + + onChangeReqSort = (newSort: SqlStatsSortType): void => { + this.setState(prevState => ({ ...prevState, reqSortSetting: newSort })); + }; + renderStatements = (regions: string[]): React.ReactElement => { const { pagination, filters, activeFilters } = this.state; const { @@ -692,12 +731,30 @@ export class StatementsPage extends React.Component< /> + + Limit: {this.state.limit ?? "N/A"} + + + + + Sort By: {getSortLabel(this.state.reqSortSetting)} + + + + + + {hasAdminRole && ( ({ refreshStatements: (req: StatementsRequest) => @@ -226,6 +236,10 @@ export const ConnectedStatementsPage = withRouter( selectedColumns.length === 0 ? " " : selectedColumns.join(","), }), ), + onChangeLimit: (limit: number) => + dispatch(updateStmtsPageLimitAction(limit)), + onChangeReqSort: (sort: SqlStatsSortType) => + dispatch(updateStmsPageReqSortAction(sort)), }), )(StatementsPage), ); 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 044ea5581bce..003867ad6bb2 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 @@ -12,6 +12,10 @@ import { createSlice, PayloadAction } from "@reduxjs/toolkit"; import { DOMAIN_NAME } from "../utils"; import { defaultFilters, Filters } from "../../queryFilter"; import { TimeScale, defaultTimeScaleSelected } from "../../timeScaleDropdown"; +import { + SqlStatsSortType, + DEFAULT_STATS_REQ_OPTIONS, +} from "src/api/statementsApi"; type SortSetting = { ascending: boolean; @@ -20,6 +24,10 @@ type SortSetting = { export enum LocalStorageKeys { GLOBAL_TIME_SCALE = "timeScale/SQLActivity", + STMT_FINGERPRINTS_LIMIT = "limit/StatementsPage", + STMT_FINGERPRINTS_SORT = "sort/StatementsPage", + TXN_FINGERPRINTS_LIMIT = "limit/TransactionsPage", + TXN_FINGERPRINTS_SORT = "sort/TransactionsPage", } export type LocalStorageState = { @@ -28,6 +36,10 @@ export type LocalStorageState = { "showColumns/TransactionPage": string; "showColumns/SessionsPage": string; [LocalStorageKeys.GLOBAL_TIME_SCALE]: TimeScale; + [LocalStorageKeys.STMT_FINGERPRINTS_LIMIT]: number; + [LocalStorageKeys.STMT_FINGERPRINTS_SORT]: SqlStatsSortType; + [LocalStorageKeys.TXN_FINGERPRINTS_LIMIT]: number; + [LocalStorageKeys.TXN_FINGERPRINTS_SORT]: SqlStatsSortType; "sortSetting/StatementsPage": SortSetting; "sortSetting/TransactionsPage": SortSetting; "sortSetting/SessionsPage": SortSetting; @@ -62,10 +74,23 @@ const initialState: LocalStorageState = { "adminUi/showDiagnosticsModal": Boolean(JSON.parse(localStorage.getItem("adminUi/showDiagnosticsModal"))) || false, + [LocalStorageKeys.STMT_FINGERPRINTS_LIMIT]: + JSON.parse( + localStorage.getItem(LocalStorageKeys.STMT_FINGERPRINTS_LIMIT), + ) || DEFAULT_STATS_REQ_OPTIONS.limit, + [LocalStorageKeys.STMT_FINGERPRINTS_SORT]: + JSON.parse(localStorage.getItem(LocalStorageKeys.STMT_FINGERPRINTS_SORT)) || + DEFAULT_STATS_REQ_OPTIONS.sort, "showColumns/StatementsPage": JSON.parse(localStorage.getItem("showColumns/StatementsPage")) || null, "showColumns/TransactionPage": JSON.parse(localStorage.getItem("showColumns/TransactionPage")) || null, + [LocalStorageKeys.TXN_FINGERPRINTS_LIMIT]: + JSON.parse(localStorage.getItem(LocalStorageKeys.TXN_FINGERPRINTS_LIMIT)) || + DEFAULT_STATS_REQ_OPTIONS.limit, + [LocalStorageKeys.TXN_FINGERPRINTS_SORT]: + JSON.parse(localStorage.getItem(LocalStorageKeys.TXN_FINGERPRINTS_SORT)) || + DEFAULT_STATS_REQ_OPTIONS.sort, "showColumns/SessionsPage": JSON.parse(localStorage.getItem("showColumns/SessionsPage")) || null, [LocalStorageKeys.GLOBAL_TIME_SCALE]: @@ -111,3 +136,35 @@ const localStorageSlice = createSlice({ }); export const { actions, reducer } = localStorageSlice; + +export const updateStmtsPageLimitAction = ( + limit: number, +): PayloadAction => + localStorageSlice.actions.update({ + key: LocalStorageKeys.STMT_FINGERPRINTS_LIMIT, + value: limit, + }); + +export const updateStmsPageReqSortAction = ( + sort: SqlStatsSortType, +): PayloadAction => + localStorageSlice.actions.update({ + key: LocalStorageKeys.STMT_FINGERPRINTS_SORT, + value: sort, + }); + +export const updateTxnsPageLimitAction = ( + limit: number, +): PayloadAction => + localStorageSlice.actions.update({ + key: LocalStorageKeys.TXN_FINGERPRINTS_LIMIT, + value: limit, + }); + +export const updateTxnsPageReqSortAction = ( + sort: SqlStatsSortType, +): PayloadAction => + localStorageSlice.actions.update({ + key: LocalStorageKeys.TXN_FINGERPRINTS_SORT, + value: sort, + }); diff --git a/pkg/ui/workspaces/cluster-ui/src/store/sqlStats/sqlStats.reducer.ts b/pkg/ui/workspaces/cluster-ui/src/store/sqlStats/sqlStats.reducer.ts index 620e4124a2f4..9c5a6115fa7c 100644 --- a/pkg/ui/workspaces/cluster-ui/src/store/sqlStats/sqlStats.reducer.ts +++ b/pkg/ui/workspaces/cluster-ui/src/store/sqlStats/sqlStats.reducer.ts @@ -44,6 +44,7 @@ const sqlStatsSlice = createSlice({ initialState, reducers: { received: (state, action: PayloadAction) => { + state.inFlight = false; state.data = action.payload; state.valid = true; state.lastError = null; @@ -57,6 +58,7 @@ const sqlStatsSlice = createSlice({ state.inFlight = false; }, invalidated: state => { + state.inFlight = false; state.valid = false; }, refresh: (state, _action: PayloadAction) => { diff --git a/pkg/ui/workspaces/cluster-ui/src/store/statementDetails/statementDetails.sagas.ts b/pkg/ui/workspaces/cluster-ui/src/store/statementDetails/statementDetails.sagas.ts index e5d8bb6612f6..37303e123867 100644 --- a/pkg/ui/workspaces/cluster-ui/src/store/statementDetails/statementDetails.sagas.ts +++ b/pkg/ui/workspaces/cluster-ui/src/store/statementDetails/statementDetails.sagas.ts @@ -18,7 +18,7 @@ import { } from "src/api/statementsApi"; import { actions as sqlDetailsStatsActions } from "./statementDetails.reducer"; import { CACHE_INVALIDATION_PERIOD } from "src/store/utils"; -import { generateStmtDetailsToID } from "../../util"; +import { generateStmtDetailsToID } from "src/util/appStats"; export function* refreshSQLDetailsStatsSaga( action: PayloadAction, diff --git a/pkg/ui/workspaces/cluster-ui/src/store/utils/selectors.ts b/pkg/ui/workspaces/cluster-ui/src/store/utils/selectors.ts index 9b986ae61c7d..e74072454513 100644 --- a/pkg/ui/workspaces/cluster-ui/src/store/utils/selectors.ts +++ b/pkg/ui/workspaces/cluster-ui/src/store/utils/selectors.ts @@ -26,3 +26,23 @@ export const selectTimeScale = createSelector( localStorageSelector, localStorage => localStorage[LocalStorageKeys.GLOBAL_TIME_SCALE], ); + +export const selectStmtsPageLimit = createSelector( + localStorageSelector, + localStorage => localStorage[LocalStorageKeys.STMT_FINGERPRINTS_LIMIT], +); + +export const selectStmtsPageReqSort = createSelector( + localStorageSelector, + localStorage => localStorage[LocalStorageKeys.STMT_FINGERPRINTS_SORT], +); + +export const selectTxnsPageLimit = createSelector( + localStorageSelector, + localStorage => localStorage[LocalStorageKeys.TXN_FINGERPRINTS_LIMIT], +); + +export const selectTxnsPageReqSort = createSelector( + localStorageSelector, + localStorage => localStorage[LocalStorageKeys.TXN_FINGERPRINTS_SORT], +); diff --git a/pkg/ui/workspaces/cluster-ui/src/transactionDetails/transactionDetails.tsx b/pkg/ui/workspaces/cluster-ui/src/transactionDetails/transactionDetails.tsx index 6d8660c6f5dd..14a095bb380f 100644 --- a/pkg/ui/workspaces/cluster-ui/src/transactionDetails/transactionDetails.tsx +++ b/pkg/ui/workspaces/cluster-ui/src/transactionDetails/transactionDetails.tsx @@ -58,7 +58,7 @@ import { } from "src/statementsTable/statementsTable"; import { Transaction } from "src/transactionsTable"; import Long from "long"; -import { StatementsRequest } from "../api"; +import { createCombinedStmtsRequest, StatementsRequest } from "../api"; import { getValidOption, TimeScale, @@ -68,7 +68,7 @@ import { toRoundedDateRange, } from "../timeScaleDropdown"; import timeScaleStyles from "../timeScaleDropdown/timeScale.module.scss"; - +import { SqlStatsSortType } from "src/api/statementsApi"; const { containerClass } = tableClasses; const cx = classNames.bind(statementsStyles); const timeScaleStylesCx = classNames.bind(timeScaleStyles); @@ -80,6 +80,8 @@ const transactionDetailsStylesCx = classNames.bind(transactionDetailsStyles); export interface TransactionDetailsStateProps { timeScale: TimeScale; + limit: number; + reqSortSetting: SqlStatsSortType; error?: Error | null; isTenant: UIConfigState["isTenant"]; hasViewActivityRedactedRole?: UIConfigState["hasViewActivityRedactedRole"]; @@ -113,9 +115,11 @@ function statementsRequestFromProps( props: TransactionDetailsProps, ): protos.cockroach.server.serverpb.CombinedStatementsStatsRequest { const [start, end] = toRoundedDateRange(props.timeScale); - return new protos.cockroach.server.serverpb.CombinedStatementsStatsRequest({ - start: Long.fromNumber(start.unix()), - end: Long.fromNumber(end.unix()), + return createCombinedStmtsRequest({ + start, + end, + limit: props.limit, + sort: props.reqSortSetting, }); } diff --git a/pkg/ui/workspaces/cluster-ui/src/transactionDetails/transactionDetailsConnected.tsx b/pkg/ui/workspaces/cluster-ui/src/transactionDetails/transactionDetailsConnected.tsx index c928df90cf86..40e60b8ca752 100644 --- a/pkg/ui/workspaces/cluster-ui/src/transactionDetails/transactionDetailsConnected.tsx +++ b/pkg/ui/workspaces/cluster-ui/src/transactionDetails/transactionDetailsConnected.tsx @@ -32,7 +32,11 @@ import { selectHasViewActivityRedactedRole, } from "../store/uiConfig"; import { nodeRegionsByIDSelector } from "../store/nodes"; -import { selectTimeScale } from "src/statementsPage/statementsPage.selectors"; +import { + selectTimeScale, + selectTxnsPageLimit, + selectTxnsPageReqSort, +} from "../store/utils/selectors"; import { StatementsRequest } from "src/api/statementsApi"; import { txnFingerprintIdAttr, getMatchParamByName } from "../util"; import { TimeScale } from "../timeScaleDropdown"; @@ -93,6 +97,8 @@ const mapStateToProps = ( hasViewActivityRedactedRole: selectHasViewActivityRedactedRole(state), isDataValid: isValid, lastUpdated, + limit: selectTxnsPageLimit(state), + reqSortSetting: selectTxnsPageReqSort(state), }; }; diff --git a/pkg/ui/workspaces/cluster-ui/src/transactionsPage/transactionsPage.tsx b/pkg/ui/workspaces/cluster-ui/src/transactionsPage/transactionsPage.tsx index f9324cb8e19b..270941861e37 100644 --- a/pkg/ui/workspaces/cluster-ui/src/transactionsPage/transactionsPage.tsx +++ b/pkg/ui/workspaces/cluster-ui/src/transactionsPage/transactionsPage.tsx @@ -35,7 +35,6 @@ import { searchTransactionsData, filterTransactions, } from "./utils"; -import Long from "long"; import { merge } from "lodash"; import { unique, syncHistory } from "src/util"; import { EmptyTransactionsPlaceholder } from "./emptyTransactionsPlaceholder"; @@ -51,7 +50,11 @@ import { updateFiltersQueryParamsOnTab, } from "../queryFilter"; import { UIConfigState } from "../store"; -import { StatementsRequest } from "src/api/statementsApi"; +import { + createCombinedStmtsRequest, + SqlStatsSortType, + StatementsRequest, +} from "src/api/statementsApi"; import ColumnsSelector from "../columnsSelector/columnsSelector"; import { SelectOption } from "../multiSelectCheckbox/multiSelectCheckbox"; import { @@ -71,7 +74,14 @@ import { } from "../timeScaleDropdown"; import { InlineAlert } from "@cockroachlabs/ui-components"; import moment from "moment"; -import { STATS_LONG_LOADING_DURATION } from "../util/constants"; +import { Dropdown } from "src/dropdown"; +import { + STATS_LONG_LOADING_DURATION, + getSortLabel, + limitOptions, + txnRequestSortOptions, +} from "src/util/sqlActivityConstants"; +import { Button } from "src/button"; type IStatementsResponse = protos.cockroach.server.serverpb.IStatementsResponse; @@ -80,6 +90,9 @@ const cx = classNames.bind(styles); interface TState { filters?: Filters; pagination: ISortedTablePagination; + timeScale: TimeScale; + limit: number; + reqSortSetting: SqlStatsSortType; } export interface TransactionsPageStateProps { @@ -89,6 +102,8 @@ export interface TransactionsPageStateProps { isReqInFlight: boolean; lastUpdated: moment.Moment | null; timeScale: TimeScale; + limit: number; + reqSortSetting: SqlStatsSortType; error?: Error | null; filters: Filters; isTenant?: UIConfigState["isTenant"]; @@ -105,6 +120,8 @@ export interface TransactionsPageDispatchProps { refreshUserSQLRoles: () => void; resetSQLStats: () => void; onTimeScaleChange?: (ts: TimeScale) => void; + onChangeLimit: (limit: number) => void; + onChangeReqSort: (sort: SqlStatsSortType) => void; onColumnsChange?: (selectedColumns: string[]) => void; onFilterChange?: (value: Filters) => void; onSearchComplete?: (query: string) => void; @@ -119,11 +136,15 @@ export type TransactionsPageProps = TransactionsPageStateProps & TransactionsPageDispatchProps & RouteComponentProps; -function stmtsRequestFromTimeScale(ts: TimeScale): StatementsRequest { - const [start, end] = toRoundedDateRange(ts); - return new protos.cockroach.server.serverpb.CombinedStatementsStatsRequest({ - start: Long.fromNumber(start.unix()), - end: Long.fromNumber(end.unix()), +type RequestParams = Pick; + +function stmtsRequestFromParams(params: RequestParams): StatementsRequest { + const [start, end] = toRoundedDateRange(params.timeScale); + return createCombinedStmtsRequest({ + start, + end, + limit: params.limit, + sort: params.reqSortSetting, }); } export class TransactionsPage extends React.Component< @@ -132,7 +153,11 @@ export class TransactionsPage extends React.Component< > { constructor(props: TransactionsPageProps) { super(props); + this.state = { + limit: this.props.limit, + timeScale: this.props.timeScale, + reqSortSetting: this.props.reqSortSetting, pagination: { pageSize: this.props.pageSize || 20, current: 1, @@ -181,7 +206,7 @@ export class TransactionsPage extends React.Component< }; refreshData = (): void => { - const req = stmtsRequestFromTimeScale(this.props.timeScale); + const req = stmtsRequestFromParams(this.state); this.props.refreshData(req); }; @@ -243,18 +268,11 @@ export class TransactionsPage extends React.Component< ); } - componentDidUpdate(prevProps: TransactionsPageProps): void { + componentDidUpdate(): void { this.updateQueryParams(); if (!this.props.isTenant) { this.props.refreshNodes(); } - - if ( - prevProps.timeScale !== this.props.timeScale || - (prevProps.isDataValid && !this.props.isDataValid) - ) { - this.refreshData(); - } } onChangeSortSetting = (ss: SortSetting): void => { @@ -360,9 +378,22 @@ export class TransactionsPage extends React.Component< }; changeTimeScale = (ts: TimeScale): void => { - if (this.props.onTimeScaleChange) { - this.props.onTimeScaleChange(ts); - } + this.setState(prevState => ({ ...prevState, timeScale: ts })); + }; + + onChangeLimit = (newLimit: number): void => { + this.setState(prevState => ({ ...prevState, limit: newLimit })); + }; + + onChangeReqSort = (newSort: SqlStatsSortType): void => { + this.setState(prevState => ({ ...prevState, reqSortSetting: newSort })); + }; + + updateRequestParams = (): void => { + this.props.onChangeLimit(this.state.limit); + this.props.onChangeReqSort(this.state.reqSortSetting); + this.props.onTimeScaleChange(this.state.timeScale); + this.refreshData(); }; render(): React.ReactElement { @@ -445,15 +476,33 @@ export class TransactionsPage extends React.Component< showNodes={nodes.length > 1} /> + + + Limit: {this.state.limit ?? "N/A"} + + + + + Sort By: {getSortLabel(this.state.reqSortSetting)} + + + + + {hasAdminRole && ( - + ({ refreshData: (req: StatementsRequest) => @@ -130,6 +140,10 @@ export const TransactionsPageConnected = withRouter( }), ); }, + onChangeLimit: (limit: number) => + dispatch(updateTxnsPageLimitAction(limit)), + onChangeReqSort: (sort: SqlStatsSortType) => + dispatch(updateTxnsPageReqSortAction(sort)), }), )(TransactionsPage), ); diff --git a/pkg/ui/workspaces/cluster-ui/src/util/constants.ts b/pkg/ui/workspaces/cluster-ui/src/util/constants.ts index 43df28f9a999..66e2ca61d8be 100644 --- a/pkg/ui/workspaces/cluster-ui/src/util/constants.ts +++ b/pkg/ui/workspaces/cluster-ui/src/util/constants.ts @@ -41,5 +41,3 @@ export const serverToClientErrorMessageMap = new Map([ REMOTE_DEBUGGING_ERROR_TEXT, ], ]); - -export const STATS_LONG_LOADING_DURATION = duration(2, "s"); diff --git a/pkg/ui/workspaces/cluster-ui/src/util/sqlActivityConstants.ts b/pkg/ui/workspaces/cluster-ui/src/util/sqlActivityConstants.ts new file mode 100644 index 000000000000..cd563995c327 --- /dev/null +++ b/pkg/ui/workspaces/cluster-ui/src/util/sqlActivityConstants.ts @@ -0,0 +1,45 @@ +// Copyright 2023 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 { duration } from "moment"; +import { SqlStatsSortOptions, SqlStatsSortType } from "src/api/statementsApi"; + +export const limitOptions = [ + { value: 25, name: "25" }, + { value: 50, name: "50" }, + { value: 100, name: "100" }, + { value: 500, name: "500" }, +]; + +export function getSortLabel(sort: SqlStatsSortType): string { + switch (sort) { + case SqlStatsSortOptions.SERVICE_LAT: + return "Service Latency"; + case SqlStatsSortOptions.EXECUTION_COUNT: + return "Execution Count"; + case SqlStatsSortOptions.CONTENTION_TIME: + return "Contention Time"; + default: + return ""; + } +} + +export const stmtRequestSortOptions = Object.values(SqlStatsSortOptions).map( + sortVal => ({ + value: sortVal, + name: getSortLabel(sortVal as SqlStatsSortType), + }), +); + +export const txnRequestSortOptions = stmtRequestSortOptions.filter( + option => true, +); + +export const STATS_LONG_LOADING_DURATION = duration(2, "s"); diff --git a/pkg/ui/workspaces/db-console/src/redux/apiReducers.ts b/pkg/ui/workspaces/db-console/src/redux/apiReducers.ts index aadea9478217..6107a4816e0c 100644 --- a/pkg/ui/workspaces/db-console/src/redux/apiReducers.ts +++ b/pkg/ui/workspaces/db-console/src/redux/apiReducers.ts @@ -318,11 +318,11 @@ const storesReducerObj = new KeyedCachedDataReducer( export const refreshStores = storesReducerObj.refresh; const queriesReducerObj = new CachedDataReducer( - api.getCombinedStatements, + clusterUiApi.getCombinedStatements, "statements", - moment.duration(5, "m"), - moment.duration(30, "m"), - true, // Allow new requests to replace in flight ones. + null, + moment.duration(10, "m"), + true, ); export const invalidateStatements = queriesReducerObj.invalidateData; export const refreshStatements = queriesReducerObj.refresh; diff --git a/pkg/ui/workspaces/db-console/src/util/api.ts b/pkg/ui/workspaces/db-console/src/util/api.ts index 806690dcdd57..fe994cd53c44 100644 --- a/pkg/ui/workspaces/db-console/src/util/api.ts +++ b/pkg/ui/workspaces/db-console/src/util/api.ts @@ -724,23 +724,6 @@ export function getStores( ); } -// getCombinedStatements returns statements the cluster has recently executed, and some stats about them. -export function getCombinedStatements( - req: StatementsRequestMessage, - timeout?: moment.Duration, -): Promise { - const queryStr = propsToQueryString({ - start: req.start.toInt(), - end: req.end.toInt(), - }); - return timeoutFetch( - serverpb.StatementsResponse, - `${STATUS_PREFIX}/combinedstmts?${queryStr}`, - null, - timeout, - ); -} - // getStatementDetails returns the statistics about the selected statement. export function getStatementDetails( req: StatementDetailsRequestMessage, 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 d42278fc87ca..a6772588e4b6 100644 --- a/pkg/ui/workspaces/db-console/src/views/statements/statementsPage.tsx +++ b/pkg/ui/workspaces/db-console/src/views/statements/statementsPage.tsx @@ -40,6 +40,7 @@ import { Filters, defaultFilters, util, + api, } from "@cockroachlabs/cluster-ui"; import { cancelStatementDiagnosticsReportAction, @@ -260,7 +261,7 @@ export const statementColumnsLocalSetting = new LocalSetting( ); export const sortSettingLocalSetting = new LocalSetting( - "sortSetting/StatementsPage", + "tableSortSetting/StatementsPage", (state: AdminUIState) => state.localSettings, { ascending: false, columnTitle: "executionCount" }, ); @@ -277,6 +278,18 @@ export const searchLocalSetting = new LocalSetting( null, ); +export const reqSortSetting = new LocalSetting( + "reqSortSetting/StatementsPage", + (state: AdminUIState) => state.localSettings, + api.DEFAULT_STATS_REQ_OPTIONS.sort, +); + +export const limitSetting = new LocalSetting( + "reqLimitSetting/StatementsPage", + (state: AdminUIState) => state.localSettings, + api.DEFAULT_STATS_REQ_OPTIONS.limit, +); + export default withRouter( connect( (state: AdminUIState, props: RouteComponentProps) => ({ @@ -297,6 +310,8 @@ export default withRouter( totalFingerprints: selectTotalFingerprints(state), hasViewActivityRedactedRole: selectHasViewActivityRedactedRole(state), hasAdminRole: selectHasAdminRole(state), + limit: limitSetting.selector(state), + reqSortSetting: reqSortSetting.selector(state), }), { refreshStatements: refreshStatements, @@ -353,6 +368,8 @@ export default withRouter( statementColumnsLocalSetting.set( value.length === 0 ? " " : value.join(","), ), + onChangeLimit: (newLimit: number) => limitSetting.set(newLimit), + onChangeReqSort: (sort: api.SqlStatsSortType) => reqSortSetting.set(sort), }, )(StatementsPage), ); diff --git a/pkg/ui/workspaces/db-console/src/views/transactions/transactionDetails.tsx b/pkg/ui/workspaces/db-console/src/views/transactions/transactionDetails.tsx index 3e4b842b3f9c..f47d83ace752 100644 --- a/pkg/ui/workspaces/db-console/src/views/transactions/transactionDetails.tsx +++ b/pkg/ui/workspaces/db-console/src/views/transactions/transactionDetails.tsx @@ -21,8 +21,10 @@ import { txnFingerprintIdAttr } from "src/util/constants"; import { getMatchParamByName } from "src/util/query"; import { nodeRegionsByIDSelector } from "src/redux/nodes"; import { + reqSortSetting, selectData, selectLastError, + limitSetting, } from "src/views/transactions/transactionsPage"; import { TransactionDetailsStateProps, @@ -92,6 +94,8 @@ export default withRouter( isLoading: isLoading, lastUpdated: lastUpdated, isDataValid: isValid, + limit: limitSetting.selector(state), + reqSortSetting: reqSortSetting.selector(state), }; }, { 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 2b8f599d2cad..62d68ed64ce2 100644 --- a/pkg/ui/workspaces/db-console/src/views/transactions/transactionsPage.tsx +++ b/pkg/ui/workspaces/db-console/src/views/transactions/transactionsPage.tsx @@ -29,6 +29,7 @@ import { Filters, defaultFilters, util, + api, } from "@cockroachlabs/cluster-ui"; import { nodeRegionsByIDSelector } from "src/redux/nodes"; import { setGlobalTimeScaleAction } from "src/redux/statements"; @@ -96,6 +97,18 @@ export const transactionColumnsLocalSetting = new LocalSetting( null, ); +export const reqSortSetting = new LocalSetting( + "reqSortSetting/TransactionsPage", + (state: AdminUIState) => state.localSettings, + api.DEFAULT_STATS_REQ_OPTIONS.sort, +); + +export const limitSetting = new LocalSetting( + "reqLimitSetting/TransactionsPage", + (state: AdminUIState) => state.localSettings, + api.DEFAULT_STATS_REQ_OPTIONS.limit, +); + const TransactionsPageConnected = withRouter( connect( (state: AdminUIState) => ({ @@ -113,6 +126,8 @@ const TransactionsPageConnected = withRouter( sortSetting: sortSettingLocalSetting.selector(state), statementsError: state.cachedData.transactions.lastError, hasAdminRole: selectHasAdminRole(state), + limit: limitSetting.selector(state), + reqSortSetting: reqSortSetting.selector(state), }), { refreshData: refreshTxns, @@ -139,6 +154,8 @@ const TransactionsPageConnected = withRouter( }), onFilterChange: (filters: Filters) => filtersLocalSetting.set(filters), onSearchComplete: (query: string) => searchLocalSetting.set(query), + onChangeLimit: (newLimit: number) => limitSetting.set(newLimit), + onChangeReqSort: (sort: api.SqlStatsSortType) => reqSortSetting.set(sort), }, )(TransactionsPage), );