From 48d68567b8cdf54abb14cf123eb2788e67be1fce 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 --- .../cluster-ui/src/api/statementsApi.ts | 37 +++++- .../statementsPage/statementsPage.fixture.ts | 5 + .../src/statementsPage/statementsPage.tsx | 105 +++++++++++++----- .../statementsPageConnected.tsx | 20 +++- .../localStorage/localStorage.reducer.ts | 55 +++++++++ .../src/store/sqlStats/sqlStats.reducer.ts | 3 + .../cluster-ui/src/store/utils/selectors.ts | 20 ++++ .../transactionDetails/transactionDetails.tsx | 12 +- .../transactionDetailsConnected.tsx | 8 +- .../src/transactionsPage/transactionsPage.tsx | 93 ++++++++++++---- .../transactionsPageConnected.tsx | 20 +++- .../cluster-ui/src/util/constants.ts | 2 - .../src/util/sqlActivityConstants.ts | 47 ++++++++ .../db-console/src/redux/apiReducers.ts | 6 +- pkg/ui/workspaces/db-console/src/util/api.ts | 19 ---- .../src/views/statements/statementsPage.tsx | 19 +++- .../views/transactions/transactionDetails.tsx | 4 + .../views/transactions/transactionsPage.tsx | 17 +++ 18 files changed, 409 insertions(+), 83 deletions(-) create mode 100644 pkg/ui/workspaces/cluster-ui/src/util/sqlActivityConstants.ts diff --git a/pkg/ui/workspaces/cluster-ui/src/api/statementsApi.ts b/pkg/ui/workspaces/cluster-ui/src/api/statementsApi.ts index 8c69028c4fad..e16fd6a6f9b5 100644 --- a/pkg/ui/workspaces/cluster-ui/src/api/statementsApi.ts +++ b/pkg/ui/workspaces/cluster-ui/src/api/statementsApi.ts @@ -40,13 +40,46 @@ 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, +}; + +// THhe 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.StatementsRequest({ + combined: true, + start: Long.fromNumber(start.unix()), + end: Long.fromNumber(end.unix()), + limit: Long.fromNumber(limit ?? DEFAULT_STATS_REQ_OPTIONS.limit), + sort, + }); +} + export const getCombinedStatements = ( req: StatementsRequest, -): Promise => { +): Promise => { const queryStr = propsToQueryString({ start: req.start.toInt(), end: req.end.toInt(), fetch_mode: cockroach.server.serverpb.StatsFetchMode.StmtStatsOnly, + limit: req.limit.toInt(), + sort: req.sort, }); return fetchData( cockroach.server.serverpb.StatementsResponse, @@ -65,6 +98,8 @@ export const getFlushedTxnStatsApi = ( end: req.end.toInt(), combined: true, fetch_mode: cockroach.server.serverpb.StatsFetchMode.TxnStatsOnly, + limit: req.limit.toInt(), + sort: req.sort, }); 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 c13d351ae5b2..376fd6099317 100644 --- a/pkg/ui/workspaces/cluster-ui/src/statementsPage/statementsPage.fixture.ts +++ b/pkg/ui/workspaces/cluster-ui/src/statementsPage/statementsPage.fixture.ts @@ -20,6 +20,7 @@ import { StatementDiagnosticsReport } from "../api"; import { cockroach } from "@cockroachlabs/crdb-protobuf-client"; import ILatencyInfo = cockroach.sql.ILatencyInfo; import { AggregateStatistics } from "src/statementsTable"; +import { DEFAULT_STATS_REQ_OPTIONS } from "../api/statementsApi"; type IStatementStatistics = protos.cockroach.sql.IStatementStatistics; type IExecStats = protos.cockroach.sql.IExecStats; @@ -587,6 +588,8 @@ const statementsPagePropsFixture: StatementsPageProps = { // Aggregate key values in these statements will need to change if implementation // of 'statementKey' in appStats.ts changes. 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"), @@ -615,6 +618,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 eeb7484fd62b..0036c00f5f8d 100644 --- a/pkg/ui/workspaces/cluster-ui/src/statementsPage/statementsPage.tsx +++ b/pkg/ui/workspaces/cluster-ui/src/statementsPage/statementsPage.tsx @@ -50,14 +50,16 @@ import { import { ISortedTablePagination } from "../sortedtable"; import styles from "./statementsPage.module.scss"; import { EmptyStatementsPlaceholder } from "./emptyStatementsPlaceholder"; -import { cockroach } from "@cockroachlabs/crdb-protobuf-client"; import { InlineAlert } from "@cockroachlabs/ui-components"; 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 { @@ -78,7 +80,14 @@ import { StatementDiagnosticsReport, } from "../api"; import { filteredStatementsData } from "../sqlActivity/util"; -import { STATS_LONG_LOADING_DURATION } from "../util/constants"; +import { Dropdown } from "src/dropdown"; +import { + STATS_LONG_LOADING_DURATION, + limitOptions, + requestSortOptions, + getSortLabel, +} from "src/util/sqlActivityConstants"; +import { Button } from "src/button"; const cx = classNames.bind(styles); const sortableTableCx = classNames.bind(sortableTableStyles); @@ -114,14 +123,17 @@ 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 { statements: AggregateStatistics[]; isDataValid: boolean; isReqInFlight: boolean; lastUpdated: moment.Moment | null; timeScale: TimeScale; + limit: number; + reqSortSetting: SqlStatsSortType; statementsError: Error | null; apps: string[]; databases: string[]; @@ -141,17 +153,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, }); } @@ -191,6 +213,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); @@ -250,9 +275,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 => { @@ -267,7 +300,7 @@ export class StatementsPage extends React.Component< }; refreshStatements = (): void => { - const req = stmtsRequestFromTimeScale(this.props.timeScale); + const req = stmtsRequestFromParams(this.state); this.props.refreshStatements(req); }; @@ -336,7 +369,7 @@ export class StatementsPage extends React.Component< ); } - componentDidUpdate = (prevProps: StatementsPageProps): void => { + componentDidUpdate = (): void => { this.updateQueryParams(); if (!this.props.isTenant) { this.props.refreshNodes(); @@ -344,13 +377,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 { @@ -359,8 +385,11 @@ export class StatementsPage extends React.Component< onChangePage = (current: number): void => { const { pagination } = this.state; - this.setState({ pagination: { ...pagination, current } }); - this.props.onPageChanged != null && this.props.onPageChanged(current); + this.setState(prevState => ({ + ...prevState, + pagination: { ...pagination, current }, + })); + this.props.onPageChanged(current); }; onSubmitSearchField = (search: string): void => { @@ -440,6 +469,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 { @@ -612,12 +649,30 @@ export class StatementsPage extends React.Component< /> + + Limit: {this.state.limit ?? "N/A"} + + + + + Sort By: {getSortLabel(this.state.reqSortSetting)} + + + + + + {hasAdminRole && ( + dispatch(updateStmtsPageLimitAction(limit)), + onChangeReqSort: (sort: SqlStatsSortType) => + dispatch(updateStmsPageReqSortAction(sort)), }, activePageProps: mapDispatchToRecentStatementsPageProps(dispatch), }), 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 75aa472c490d..d2753d162118 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 @@ -13,6 +13,8 @@ import { DOMAIN_NAME } from "../utils"; import { defaultFilters, Filters } from "src/queryFilter/"; import { TimeScale, defaultTimeScaleSelected } from "../../timeScaleDropdown"; import { WorkloadInsightEventFilters } from "src/insights"; +import { DEFAULT_STATS_REQ_OPTIONS } from "../../api/statementsApi"; +import { SqlStatsSortType } from "src/api/statementsApi"; type SortSetting = { ascending: boolean; @@ -21,6 +23,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 = { @@ -33,6 +39,10 @@ export type LocalStorageState = { "showColumns/StatementInsightsPage": string; "showColumns/JobsPage": 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/ActiveStatementsPage": SortSetting; "sortSetting/ActiveTransactionsPage": SortSetting; "sortSetting/StatementsPage": SortSetting; @@ -125,6 +135,13 @@ const initialState: LocalStorageState = { "showColumns/ActiveStatementsPage": JSON.parse(localStorage.getItem("showColumns/ActiveStatementsPage")) ?? null, + [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/ActiveTransactionsPage": JSON.parse(localStorage.getItem("showColumns/ActiveTransactionsPage")) ?? null, @@ -132,6 +149,12 @@ const initialState: LocalStorageState = { 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, "showColumns/StatementInsightsPage": @@ -223,3 +246,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 7596374a2b9a..5d2dc93fb4ff 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; @@ -51,12 +52,14 @@ const sqlStatsSlice = createSlice({ state.inFlight = false; }, failed: (state, action: PayloadAction) => { + state.inFlight = false; state.valid = false; state.lastError = action.payload; state.lastUpdated = moment.utc(); 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/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 e27df1940dd7..a5d5d23b0564 100644 --- a/pkg/ui/workspaces/cluster-ui/src/transactionDetails/transactionDetails.tsx +++ b/pkg/ui/workspaces/cluster-ui/src/transactionDetails/transactionDetails.tsx @@ -61,6 +61,7 @@ import { import { Transaction } from "src/transactionsTable"; import Long from "long"; import { + createCombinedStmtsRequest, InsightRecommendation, StatementsRequest, TxnInsightsRequest, @@ -88,6 +89,7 @@ import { makeInsightsColumns, } from "../insightsTable/insightsTable"; import { CockroachCloudContext } from "../contexts"; +import { SqlStatsSortType } from "src/api/statementsApi"; const { containerClass } = tableClasses; const cx = classNames.bind(statementsStyles); const timeScaleStylesCx = classNames.bind(timeScaleStyles); @@ -101,6 +103,8 @@ const transactionDetailsStylesCx = classNames.bind(transactionDetailsStyles); export interface TransactionDetailsStateProps { timeScale: TimeScale; + limit: number; + reqSortSetting: SqlStatsSortType; error?: Error | null; isTenant: UIConfigState["isTenant"]; hasViewActivityRedactedRole?: UIConfigState["hasViewActivityRedactedRole"]; @@ -137,9 +141,11 @@ function statementsRequestFromProps( props: TransactionDetailsProps, ): StatementsRequest { 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 ea81a1e0f03c..40de5b6ec8a1 100644 --- a/pkg/ui/workspaces/cluster-ui/src/transactionDetails/transactionDetailsConnected.tsx +++ b/pkg/ui/workspaces/cluster-ui/src/transactionDetails/transactionDetailsConnected.tsx @@ -38,7 +38,11 @@ import { selectHasAdminRole, } from "../store/uiConfig"; import { nodeRegionsByIDSelector } from "../store/nodes"; -import { selectTimeScale } from "../store/utils/selectors"; +import { + selectTimeScale, + selectTxnsPageLimit, + selectTxnsPageReqSort, +} from "../store/utils/selectors"; import { StatementsRequest } from "src/api/statementsApi"; import { txnFingerprintIdAttr, getMatchParamByName } from "../util"; import { TimeScale } from "../timeScaleDropdown"; @@ -100,6 +104,8 @@ const mapStateToProps = ( transactionInsights: selectTxnInsightsByFingerprint(state, props), hasAdminRole: selectHasAdminRole(state), isDataValid: isValid, + 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 b7a4ed885754..649202d06ca4 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 { flatMap, 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 { @@ -73,7 +76,14 @@ import { InlineAlert } from "@cockroachlabs/ui-components"; import { TransactionViewType } from "./transactionsPageTypes"; import { isSelectedColumn } from "../columnsSelector/utils"; import moment from "moment"; -import { STATS_LONG_LOADING_DURATION } from "../util/constants"; +import { Dropdown } from "src/dropdown"; +import { + STATS_LONG_LOADING_DURATION, + getSortLabel, + limitOptions, + requestSortOptions, +} from "src/util/sqlActivityConstants"; +import { Button } from "src/button"; type IStatementsResponse = protos.cockroach.server.serverpb.IStatementsResponse; @@ -82,6 +92,9 @@ const cx = classNames.bind(styles); interface TState { filters?: Filters; pagination: ISortedTablePagination; + timeScale: TimeScale; + limit: number; + reqSortSetting: SqlStatsSortType; } export interface TransactionsPageStateProps { @@ -91,6 +104,8 @@ export interface TransactionsPageStateProps { isReqInFlight: boolean; lastUpdated: moment.Moment | null; timeScale: TimeScale; + limit: number; + reqSortSetting: SqlStatsSortType; error?: Error | null; filters: Filters; isTenant?: UIConfigState["isTenant"]; @@ -107,6 +122,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; @@ -121,11 +138,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< @@ -134,7 +155,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, @@ -183,7 +208,7 @@ export class TransactionsPage extends React.Component< }; refreshData = (): void => { - const req = stmtsRequestFromTimeScale(this.props.timeScale); + const req = stmtsRequestFromParams(this.state); this.props.refreshData(req); }; @@ -245,18 +270,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 => { @@ -362,9 +380,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={!isTenant && nodes.length > 1} /> + + + Limit: {this.state.limit ?? "N/A"} + + + + + Sort By: {getSortLabel(this.state.reqSortSetting)} + + + + + {hasAdminRole && ( - + + dispatch(updateTxnsPageLimitAction(limit)), + onChangeReqSort: (sort: SqlStatsSortType) => + dispatch(updateTxnsPageReqSortAction(sort)), }, activePageProps: mapDispatchToRecentTransactionsPageProps(dispatch), }), diff --git a/pkg/ui/workspaces/cluster-ui/src/util/constants.ts b/pkg/ui/workspaces/cluster-ui/src/util/constants.ts index 049aefbcc4cf..43d9e21b0706 100644 --- a/pkg/ui/workspaces/cluster-ui/src/util/constants.ts +++ b/pkg/ui/workspaces/cluster-ui/src/util/constants.ts @@ -47,5 +47,3 @@ export const serverToClientErrorMessageMap = new Map([ ]); export const NO_SAMPLES_FOUND = "no samples"; - -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..8e5130e65774 --- /dev/null +++ b/pkg/ui/workspaces/cluster-ui/src/util/sqlActivityConstants.ts @@ -0,0 +1,47 @@ +// 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 { TimeScale } from "../timeScaleDropdown"; +import { SqlStatsSortOptions, SqlStatsSortType } from "../api/statementsApi"; + +export const limitOptions = [ + { value: 100, name: "100" }, + { value: 200, name: "200" }, + { value: 500, name: "500" }, + { value: 1000, name: "1000" }, +]; + +export function getSortLabel(sort: SqlStatsSortType): string { + switch (sort) { + case SqlStatsSortOptions.SERVICE_LAT: + return "Service Latency"; + case SqlStatsSortOptions.EXECUTION_COUNT: + return "Execution Count"; + case SqlStatsSortOptions.CPU_TIME: + return "CPU Time"; + default: + return ""; + } +} + +export const requestSortOptions = Object.values(SqlStatsSortOptions).map( + sortVal => ({ + value: sortVal, + name: getSortLabel(sortVal as SqlStatsSortType), + }), +); + +export type StatsRequestParams = { + timeScale: TimeScale; + limit: number; +}; + +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 db8917ef19a8..0a220ad72174 100644 --- a/pkg/ui/workspaces/db-console/src/redux/apiReducers.ts +++ b/pkg/ui/workspaces/db-console/src/redux/apiReducers.ts @@ -325,11 +325,11 @@ const storesReducerObj = new KeyedCachedDataReducer( export const refreshStores = storesReducerObj.refresh; const queriesReducerObj = new CachedDataReducer( - api.getCombinedStatements, + clusterUiApi.getCombinedStatements, "statements", null, - moment.duration(30, "m"), - true, // Allow new requests to replace in flight ones. + 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 8d7580423635..711b290234c2 100644 --- a/pkg/ui/workspaces/db-console/src/util/api.ts +++ b/pkg/ui/workspaces/db-console/src/util/api.ts @@ -175,8 +175,6 @@ export type MetricMetadataRequestMessage = export type MetricMetadataResponseMessage = protos.cockroach.server.serverpb.MetricMetadataResponse; -export type StatementsRequestMessage = - protos.cockroach.server.serverpb.CombinedStatementsStatsRequest; export type StatementDetailsRequestMessage = protos.cockroach.server.serverpb.StatementDetailsRequest; @@ -744,23 +742,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 d7cd2bafa47b..79cc567c6167 100644 --- a/pkg/ui/workspaces/db-console/src/views/statements/statementsPage.tsx +++ b/pkg/ui/workspaces/db-console/src/views/statements/statementsPage.tsx @@ -47,6 +47,7 @@ import { RecentStatementsViewDispatchProps, StatementsPageDispatchProps, StatementsPageRootProps, + api, } from "@cockroachlabs/cluster-ui"; import { cancelStatementDiagnosticsReportAction, @@ -268,7 +269,7 @@ export const statementColumnsLocalSetting = new LocalSetting( ); export const sortSettingLocalSetting = new LocalSetting( - "sortSetting/StatementsPage", + "tableSortSetting/StatementsPage", (state: AdminUIState) => state.localSettings, { ascending: false, columnTitle: "executionCount" }, ); @@ -285,6 +286,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, +); + const fingerprintsPageActions = { refreshStatements: refreshStatements, refreshDatabases: refreshDatabases, @@ -348,6 +361,8 @@ const fingerprintsPageActions = { statementColumnsLocalSetting.set( value.length === 0 ? " " : value.join(","), ), + onChangeLimit: (newLimit: number) => limitSetting.set(newLimit), + onChangeReqSort: (sort: api.SqlStatsSortType) => reqSortSetting.set(sort), }; type StateProps = { @@ -387,6 +402,8 @@ export default withRouter( totalFingerprints: selectTotalFingerprints(state), hasViewActivityRedactedRole: selectHasViewActivityRedactedRole(state), hasAdminRole: selectHasAdminRole(state), + limit: limitSetting.selector(state), + reqSortSetting: reqSortSetting.selector(state), }, activePageProps: mapStateToRecentStatementViewProps(state), }), 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 5d95393c418a..91140e50dea1 100644 --- a/pkg/ui/workspaces/db-console/src/views/transactions/transactionDetails.tsx +++ b/pkg/ui/workspaces/db-console/src/views/transactions/transactionDetails.tsx @@ -22,6 +22,7 @@ import { txnFingerprintIdAttr } from "src/util/constants"; import { getMatchParamByName } from "src/util/query"; import { nodeRegionsByIDSelector } from "src/redux/nodes"; import { + reqSortSetting, selectData, selectLastError, } from "src/views/transactions/transactionsPage"; @@ -35,6 +36,7 @@ import { setGlobalTimeScaleAction } from "src/redux/statements"; import { selectTimeScale } from "src/redux/timeScale"; import { selectTxnInsightsByFingerprint } from "src/views/insights/insightsSelectors"; import { selectHasAdminRole } from "src/redux/user"; +import { limitSetting } from "./transactionsPage"; export const selectTransaction = createSelector( (state: AdminUIState) => state.cachedData.transactions, @@ -92,6 +94,8 @@ export default withRouter( transactionInsights: selectTxnInsightsByFingerprint(state, props), hasAdminRole: selectHasAdminRole(state), 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 d1ac5d75871e..77cb34167f82 100644 --- a/pkg/ui/workspaces/db-console/src/views/transactions/transactionsPage.tsx +++ b/pkg/ui/workspaces/db-console/src/views/transactions/transactionsPage.tsx @@ -34,6 +34,7 @@ import { TransactionsPageDispatchProps, TransactionsPageRoot, TransactionsPageRootProps, + api, } from "@cockroachlabs/cluster-ui"; import { nodeRegionsByIDSelector } from "src/redux/nodes"; import { setGlobalTimeScaleAction } from "src/redux/statements"; @@ -102,6 +103,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 fingerprintsPageActions = { refreshData: refreshTxns, refreshNodes, @@ -127,6 +140,8 @@ const fingerprintsPageActions = { }), 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), }; type StateProps = { @@ -163,6 +178,8 @@ const TransactionsPageConnected = withRouter( sortSetting: sortSettingLocalSetting.selector(state), statementsError: state.cachedData.transactions.lastError, hasAdminRole: selectHasAdminRole(state), + limit: limitSetting.selector(state), + reqSortSetting: reqSortSetting.selector(state), }, activePageProps: mapStateToRecentTransactionsPageProps(state), }),