diff --git a/pkg/ui/workspaces/cluster-ui/src/api/statementsApi.ts b/pkg/ui/workspaces/cluster-ui/src/api/statementsApi.ts index db66a3885302..d103e32c1b77 100644 --- a/pkg/ui/workspaces/cluster-ui/src/api/statementsApi.ts +++ b/pkg/ui/workspaces/cluster-ui/src/api/statementsApi.ts @@ -9,8 +9,11 @@ // 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"; @@ -35,13 +38,48 @@ 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, @@ -59,6 +97,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 dc9bbc83425a..21a53658b6a1 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 "src/api/statementsApi"; type IStatementDiagnosticsReport = cockroach.server.serverpb.IStatementDiagnosticsReport; @@ -874,6 +875,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"), @@ -901,6 +904,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 ddbe6bebbae6..5d8c5196491c 100644 --- a/pkg/ui/workspaces/cluster-ui/src/statementsPage/statementsPage.tsx +++ b/pkg/ui/workspaces/cluster-ui/src/statementsPage/statementsPage.tsx @@ -63,8 +63,11 @@ 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 { @@ -80,7 +83,14 @@ import { commonStyles } from "../common"; import { isSelectedColumn } from "src/columnsSelector/utils"; import { StatementViewType } from "./statementPageTypes"; 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; @@ -120,14 +130,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[]; @@ -147,17 +160,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 +220,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 +282,26 @@ 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 => { + if (this.props.limit !== this.state.limit) { + this.props.onChangeLimit(this.state.limit); } + + if (this.props.reqSortSetting !== this.state.reqSortSetting) { + this.props.onChangeReqSort(this.state.reqSortSetting); + } + + if (this.props.timeScale !== this.state.timeScale) { + this.props.onTimeScaleChange(this.state.timeScale); + } + + this.refreshStatements(); }; resetPagination = (): void => { @@ -273,7 +316,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 +380,7 @@ export class StatementsPage extends React.Component< ); } - componentDidUpdate = (prevProps: StatementsPageProps): void => { + componentDidUpdate = (): void => { this.updateQueryParams(); if (!this.props.isTenant) { this.props.refreshNodes(); @@ -345,13 +388,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,8 +396,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 => { @@ -519,6 +558,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 { @@ -682,12 +729,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: mapDispatchToActiveStatementsPageProps(dispatch), }), diff --git a/pkg/ui/workspaces/cluster-ui/src/statementsTable/statementsTableContent.tsx b/pkg/ui/workspaces/cluster-ui/src/statementsTable/statementsTableContent.tsx index 13755197f141..0d81db849987 100644 --- a/pkg/ui/workspaces/cluster-ui/src/statementsTable/statementsTableContent.tsx +++ b/pkg/ui/workspaces/cluster-ui/src/statementsTable/statementsTableContent.tsx @@ -31,7 +31,7 @@ import { import styles from "./statementsTableContent.module.scss"; import { cockroach } from "@cockroachlabs/crdb-protobuf-client"; import { EllipsisVertical } from "@cockroachlabs/icons"; -import { getBasePath } from "../api"; +import { getBasePath } from "src/api/basePath"; export type NodeNames = { [nodeId: string]: string }; const cx = classNames.bind(styles); 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 68dd77866a33..fbfde1746a76 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,10 @@ import { DOMAIN_NAME } from "../utils"; import { defaultFilters, Filters } from "src/queryFilter/"; import { TimeScale, defaultTimeScaleSelected } from "../../timeScaleDropdown"; import { WorkloadInsightEventFilters } from "src/insights"; +import { + SqlStatsSortType, + DEFAULT_STATS_REQ_OPTIONS, +} from "src/api/statementsApi"; type SortSetting = { ascending: boolean; @@ -21,6 +25,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 +41,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; @@ -121,6 +133,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, @@ -128,6 +147,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": @@ -214,3 +239,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..61ba8d1ed005 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 81298eb6df5a..c73fabc72161 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 @@ -17,7 +17,7 @@ import { StatementDetailsResponseWithKey, } from "src/api/statementsApi"; import { actions as sqlDetailsStatsActions } from "./statementDetails.reducer"; -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 737894433ca1..e74072454513 100644 --- a/pkg/ui/workspaces/cluster-ui/src/store/utils/selectors.ts +++ b/pkg/ui/workspaces/cluster-ui/src/store/utils/selectors.ts @@ -21,3 +21,28 @@ export const localStorageSelector = createSelector( adminUISelector, adminUiState => adminUiState?.localStorage, ); + +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 565901965c20..79bf4b959899 100644 --- a/pkg/ui/workspaces/cluster-ui/src/transactionDetails/transactionDetails.tsx +++ b/pkg/ui/workspaces/cluster-ui/src/transactionDetails/transactionDetails.tsx @@ -60,7 +60,6 @@ import { } from "src/statementsTable/statementsTable"; import { Transaction } from "src/transactionsTable"; import Long from "long"; -import { StatementsRequest } from "../api"; import { getValidOption, TimeScale, @@ -72,6 +71,11 @@ import { import moment from "moment"; import timeScaleStyles from "../timeScaleDropdown/timeScale.module.scss"; +import { + SqlStatsSortType, + StatementsRequest, + createCombinedStmtsRequest, +} from "src/api/statementsApi"; const { containerClass } = tableClasses; const cx = classNames.bind(statementsStyles); const timeScaleStylesCx = classNames.bind(timeScaleStyles); @@ -84,6 +88,8 @@ const transactionDetailsStylesCx = classNames.bind(transactionDetailsStyles); export interface TransactionDetailsStateProps { timeScale: TimeScale; + limit: number; + reqSortSetting: SqlStatsSortType; error?: Error | null; isTenant: UIConfigState["isTenant"]; hasViewActivityRedactedRole?: UIConfigState["hasViewActivityRedactedRole"]; @@ -117,9 +123,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 1df8acf19d90..0dda0f2d850d 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"; @@ -92,6 +96,8 @@ const mapStateToProps = ( lastUpdated: lastUpdated, hasViewActivityRedactedRole: selectHasViewActivityRedactedRole(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 a8ddbaca1f50..cf95ced4f188 100644 --- a/pkg/ui/workspaces/cluster-ui/src/transactionsPage/transactionsPage.tsx +++ b/pkg/ui/workspaces/cluster-ui/src/transactionsPage/transactionsPage.tsx @@ -34,7 +34,6 @@ import { searchTransactionsData, filterTransactions, } from "./utils"; -import Long from "long"; import { merge } from "lodash"; import { unique, syncHistory } from "src/util"; import { EmptyTransactionsPlaceholder } from "./emptyTransactionsPlaceholder"; @@ -50,7 +49,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 { @@ -72,7 +75,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, + txnRequestSortOptions, +} from "src/util/sqlActivityConstants"; +import { Button } from "src/button"; type IStatementsResponse = protos.cockroach.server.serverpb.IStatementsResponse; @@ -81,6 +91,9 @@ const cx = classNames.bind(styles); interface TState { filters?: Filters; pagination: ISortedTablePagination; + timeScale: TimeScale; + limit: number; + reqSortSetting: SqlStatsSortType; } export interface TransactionsPageStateProps { @@ -90,6 +103,8 @@ export interface TransactionsPageStateProps { isReqInFlight: boolean; lastUpdated: moment.Moment | null; timeScale: TimeScale; + limit: number; + reqSortSetting: SqlStatsSortType; error?: Error | null; filters: Filters; isTenant?: UIConfigState["isTenant"]; @@ -106,6 +121,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; @@ -120,11 +137,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< @@ -133,7 +154,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, @@ -182,7 +207,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,31 @@ 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 => { + if (this.props.limit !== this.state.limit) { + this.props.onChangeLimit(this.state.limit); + } + + if (this.props.reqSortSetting !== this.state.reqSortSetting) { + this.props.onChangeReqSort(this.state.reqSortSetting); } + + if (this.props.timeScale !== this.state.timeScale) { + this.props.onTimeScaleChange(this.state.timeScale); + } + + this.refreshData(); }; render(): React.ReactElement { @@ -443,15 +483,33 @@ export class TransactionsPage extends React.Component< showNodes={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: mapDispatchToActiveTransactionsPageProps(dispatch), }), diff --git a/pkg/ui/workspaces/cluster-ui/src/util/constants.ts b/pkg/ui/workspaces/cluster-ui/src/util/constants.ts index c422ecba1b97..43d9e21b0706 100644 --- a/pkg/ui/workspaces/cluster-ui/src/util/constants.ts +++ b/pkg/ui/workspaces/cluster-ui/src/util/constants.ts @@ -46,4 +46,4 @@ export const serverToClientErrorMessageMap = new Map([ ], ]); -export const STATS_LONG_LOADING_DURATION = duration(2, "s"); +export const NO_SAMPLES_FOUND = "no samples"; 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..a5da57d40486 --- /dev/null +++ b/pkg/ui/workspaces/cluster-ui/src/util/sqlActivityConstants.ts @@ -0,0 +1,43 @@ +// 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.map(opt => opt); + +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 894ab2b6c6ed..3fa3335ee8d3 100644 --- a/pkg/ui/workspaces/db-console/src/redux/apiReducers.ts +++ b/pkg/ui/workspaces/db-console/src/redux/apiReducers.ts @@ -321,11 +321,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 5627a9663573..c68307bcf30e 100644 --- a/pkg/ui/workspaces/db-console/src/util/api.ts +++ b/pkg/ui/workspaces/db-console/src/util/api.ts @@ -192,8 +192,6 @@ export type MetricMetadataRequestMessage = export type MetricMetadataResponseMessage = protos.cockroach.server.serverpb.MetricMetadataResponse; -export type StatementDiagnosticsReportsRequestMessage = - protos.cockroach.server.serverpb.StatementDiagnosticsReportsRequest; export type StatementDiagnosticsReportsResponseMessage = protos.cockroach.server.serverpb.StatementDiagnosticsReportsResponse; @@ -212,8 +210,6 @@ export type StatementDiagnosticsRequestMessage = export type StatementDiagnosticsResponseMessage = protos.cockroach.server.serverpb.StatementDiagnosticsResponse; -export type StatementsRequestMessage = - protos.cockroach.server.serverpb.CombinedStatementsStatsRequest; export type StatementDetailsRequestMessage = protos.cockroach.server.serverpb.StatementDetailsRequest; @@ -816,23 +812,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 10e57058128b..888cfc9202da 100644 --- a/pkg/ui/workspaces/db-console/src/views/statements/statementsPage.tsx +++ b/pkg/ui/workspaces/db-console/src/views/statements/statementsPage.tsx @@ -46,6 +46,7 @@ import { ActiveStatementsViewDispatchProps, StatementsPageDispatchProps, StatementsPageRootProps, + api, } from "@cockroachlabs/cluster-ui"; import { cancelStatementDiagnosticsReportAction, @@ -274,7 +275,7 @@ export const statementColumnsLocalSetting = new LocalSetting( ); export const sortSettingLocalSetting = new LocalSetting( - "sortSetting/StatementsPage", + "tableSortSetting/StatementsPage", (state: AdminUIState) => state.localSettings, { ascending: false, columnTitle: "executionCount" }, ); @@ -291,6 +292,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, onTimeScaleChange: setGlobalTimeScaleAction, @@ -344,6 +357,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 = { @@ -383,6 +398,8 @@ export default withRouter( totalFingerprints: selectTotalFingerprints(state), hasViewActivityRedactedRole: selectHasViewActivityRedactedRole(state), hasAdminRole: selectHasAdminRole(state), + limit: limitSetting.selector(state), + reqSortSetting: reqSortSetting.selector(state), }, activePageProps: mapStateToActiveStatementViewProps(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 23930686cfff..3b42ce258ea1 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, @@ -87,6 +89,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 524f9f022680..7fcbbb3caed8 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: mapStateToActiveTransactionsPageProps(state), }),