diff --git a/pkg/ui/workspaces/cluster-ui/src/statementDetails/statementDetails.selectors.ts b/pkg/ui/workspaces/cluster-ui/src/statementDetails/statementDetails.selectors.ts index e8547877d46d..d42804062369 100644 --- a/pkg/ui/workspaces/cluster-ui/src/statementDetails/statementDetails.selectors.ts +++ b/pkg/ui/workspaces/cluster-ui/src/statementDetails/statementDetails.selectors.ts @@ -142,16 +142,15 @@ function filterByRouterParamsPredicate( } export const selectStatement = createSelector( - (state: AppState) => state.adminUI.statements, + (state: AppState) => state.adminUI.sqlStats, (_state: AppState, props: RouteComponentProps) => props, - (statementsState, props) => { - const statements = statementsState.data?.statements; + (sqlStatsState, props) => { + const statements = sqlStatsState.data?.statements; if (!statements) { return null; } - const internalAppNamePrefix = - statementsState.data?.internal_app_name_prefix; + const internalAppNamePrefix = sqlStatsState.data?.internal_app_name_prefix; const flattened = flattenStatementStats(statements); const results = _.filter( flattened, diff --git a/pkg/ui/workspaces/cluster-ui/src/statementDetails/statementDetailsConnected.ts b/pkg/ui/workspaces/cluster-ui/src/statementDetails/statementDetailsConnected.ts index 1b94505ef5b8..a3b26cae0a32 100644 --- a/pkg/ui/workspaces/cluster-ui/src/statementDetails/statementDetailsConnected.ts +++ b/pkg/ui/workspaces/cluster-ui/src/statementDetails/statementDetailsConnected.ts @@ -26,7 +26,7 @@ import { nodeDisplayNameByIDSelector, nodeRegionsByIDSelector, } from "../store/nodes"; -import { actions as statementsActions } from "src/store/statements"; +import { actions as sqlStatsActions } from "src/store/sqlStats"; import { actions as statementDiagnosticsActions, selectDiagnosticsReportsByStatementFingerprint, @@ -44,7 +44,7 @@ const mapStateToProps = (state: AppState, props: StatementDetailsProps) => { const statementFingerprint = statement?.statement; return { statement, - statementsError: state.adminUI.statements.lastError, + statementsError: state.adminUI.sqlStats.lastError, dateRange: selectDateRange(state), nodeNames: selectIsTenant(state) ? {} : nodeDisplayNameByIDSelector(state), nodeRegions: selectIsTenant(state) ? {} : nodeRegionsByIDSelector(state), @@ -62,7 +62,7 @@ const mapStateToProps = (state: AppState, props: StatementDetailsProps) => { const mapDispatchToProps = ( dispatch: Dispatch, ): StatementDetailsDispatchProps => ({ - refreshStatements: () => dispatch(statementsActions.refresh()), + refreshStatements: () => dispatch(sqlStatsActions.refresh()), refreshStatementDiagnosticsRequests: () => dispatch(statementDiagnosticsActions.refresh()), refreshNodes: () => dispatch(nodesActions.refresh()), diff --git a/pkg/ui/workspaces/cluster-ui/src/statementsPage/statementsPage.selectors.ts b/pkg/ui/workspaces/cluster-ui/src/statementsPage/statementsPage.selectors.ts index 201cfbe01a1d..c0282cd5333a 100644 --- a/pkg/ui/workspaces/cluster-ui/src/statementsPage/statementsPage.selectors.ts +++ b/pkg/ui/workspaces/cluster-ui/src/statementsPage/statementsPage.selectors.ts @@ -26,9 +26,10 @@ import { cockroach } from "@cockroachlabs/crdb-protobuf-client"; import { RouteComponentProps } from "react-router-dom"; import { AppState } from "src/store"; -import { StatementsState } from "../store/statements"; import { selectDiagnosticsReportsPerStatement } from "../store/statementDiagnostics"; import { AggregateStatistics } from "../statementsTable"; +import { sqlStatsSelector } from "../store/sqlStats/sqlStats.selector"; +import { SQLStatsState } from "../store/sqlStats"; type ICollectedStatementStatistics = cockroach.server.serverpb.StatementsResponse.ICollectedStatementStatistics; export interface StatementsSummaryData { @@ -47,11 +48,6 @@ export const adminUISelector = createSelector( adminUiState => adminUiState, ); -export const statementsSelector = createSelector( - adminUISelector, - adminUiState => adminUiState.statements, -); - export const localStorageSelector = createSelector( adminUISelector, adminUiState => adminUiState.localStorage, @@ -59,53 +55,48 @@ export const localStorageSelector = createSelector( // selectApps returns the array of all apps with statement statistics present // in the data. -export const selectApps = createSelector( - statementsSelector, - statementsState => { - if (!statementsState.data) { - return []; - } +export const selectApps = createSelector(sqlStatsSelector, sqlStatsState => { + if (!sqlStatsState.data || !sqlStatsState.valid) { + return []; + } - let sawBlank = false; - let sawInternal = false; - const apps: { [app: string]: boolean } = {}; - statementsState.data.statements.forEach( - (statement: ICollectedStatementStatistics) => { - if ( - statementsState.data.internal_app_name_prefix && - statement.key.key_data.app.startsWith( - statementsState.data.internal_app_name_prefix, - ) - ) { - sawInternal = true; - } else if (statement.key.key_data.app) { - apps[statement.key.key_data.app] = true; - } else { - sawBlank = true; - } - }, - ); - return [] - .concat( - sawInternal ? [statementsState.data.internal_app_name_prefix] : [], - ) - .concat(sawBlank ? ["(unset)"] : []) - .concat(Object.keys(apps)); - }, -); + let sawBlank = false; + let sawInternal = false; + const apps: { [app: string]: boolean } = {}; + sqlStatsState.data.statements.forEach( + (statement: ICollectedStatementStatistics) => { + if ( + sqlStatsState.data.internal_app_name_prefix && + statement.key.key_data.app.startsWith( + sqlStatsState.data.internal_app_name_prefix, + ) + ) { + sawInternal = true; + } else if (statement.key.key_data.app) { + apps[statement.key.key_data.app] = true; + } else { + sawBlank = true; + } + }, + ); + return [] + .concat(sawInternal ? [sqlStatsState.data.internal_app_name_prefix] : []) + .concat(sawBlank ? ["(unset)"] : []) + .concat(Object.keys(apps)); +}); // selectDatabases returns the array of all databases with statement statistics present // in the data. export const selectDatabases = createSelector( - statementsSelector, - statementsState => { - if (!statementsState.data) { + sqlStatsSelector, + sqlStatsState => { + if (!sqlStatsState.data) { return []; } return Array.from( new Set( - statementsState.data.statements.map(s => + sqlStatsState.data.statements.map(s => s.key.key_data.database ? s.key.key_data.database : "(unset)", ), ), @@ -116,7 +107,7 @@ export const selectDatabases = createSelector( // selectTotalFingerprints returns the count of distinct statement fingerprints // present in the data. export const selectTotalFingerprints = createSelector( - statementsSelector, + sqlStatsSelector, state => { if (!state.data) { return 0; @@ -128,7 +119,7 @@ export const selectTotalFingerprints = createSelector( // selectLastReset returns a string displaying the last time the statement // statistics were reset. -export const selectLastReset = createSelector(statementsSelector, state => { +export const selectLastReset = createSelector(sqlStatsSelector, state => { if (!state.data) { return ""; } @@ -137,11 +128,11 @@ export const selectLastReset = createSelector(statementsSelector, state => { }); export const selectStatements = createSelector( - statementsSelector, + sqlStatsSelector, (_: AppState, props: RouteComponentProps) => props, selectDiagnosticsReportsPerStatement, ( - state: StatementsState, + state: SQLStatsState, props: RouteComponentProps, diagnosticsReportsPerStatement, ): AggregateStatistics[] => { @@ -214,7 +205,7 @@ export const selectStatements = createSelector( ); export const selectStatementsLastError = createSelector( - statementsSelector, + sqlStatsSelector, state => state.lastError, ); diff --git a/pkg/ui/workspaces/cluster-ui/src/statementsPage/statementsPageConnected.tsx b/pkg/ui/workspaces/cluster-ui/src/statementsPage/statementsPageConnected.tsx index 18d04031edb3..0a90db8b0716 100644 --- a/pkg/ui/workspaces/cluster-ui/src/statementsPage/statementsPageConnected.tsx +++ b/pkg/ui/workspaces/cluster-ui/src/statementsPage/statementsPageConnected.tsx @@ -14,11 +14,10 @@ import { Dispatch } from "redux"; import { Moment } from "moment"; import { AppState } from "src/store"; -import { actions as statementsActions } from "src/store/statements"; import { actions as statementDiagnosticsActions } from "src/store/statementDiagnostics"; import { actions as analyticsActions } from "src/store/analytics"; import { actions as localStorageActions } from "src/store/localStorage"; -import { actions as resetSQLStatsActions } from "src/store/sqlStats"; +import { actions as sqlStatsActions } from "src/store/sqlStats"; import { StatementsPage, StatementsPageDispatchProps, @@ -62,10 +61,10 @@ export const ConnectedStatementsPage = withRouter( }), (dispatch: Dispatch) => ({ refreshStatements: (req?: StatementsRequest) => - dispatch(statementsActions.refresh(req)), + dispatch(sqlStatsActions.refresh(req)), onDateRangeChange: (start: Moment, end: Moment) => { dispatch( - statementsActions.updateDateRange({ + sqlStatsActions.updateDateRange({ start: start.unix(), end: end.unix(), }), @@ -73,7 +72,7 @@ export const ConnectedStatementsPage = withRouter( }, refreshStatementDiagnosticsRequests: () => dispatch(statementDiagnosticsActions.refresh()), - resetSQLStats: () => dispatch(resetSQLStatsActions.request()), + resetSQLStats: () => dispatch(sqlStatsActions.reset()), dismissAlertMessage: () => dispatch( localStorageActions.update({ diff --git a/pkg/ui/workspaces/cluster-ui/src/store/reducers.spec.ts b/pkg/ui/workspaces/cluster-ui/src/store/reducers.spec.ts index 61726e90ad32..1f37059671eb 100644 --- a/pkg/ui/workspaces/cluster-ui/src/store/reducers.spec.ts +++ b/pkg/ui/workspaces/cluster-ui/src/store/reducers.spec.ts @@ -11,22 +11,22 @@ import { assert } from "chai"; import { createStore } from "redux"; import { rootActions, rootReducer } from "./reducers"; -import { actions as statementsActions } from "./statements"; +import { actions as sqlStatsActions } from "./sqlStats"; describe("rootReducer", () => { it("resets redux state on RESET_STATE action", () => { const store = createStore(rootReducer); const initState = store.getState(); const error = new Error("oops!"); - store.dispatch(statementsActions.failed(error)); + store.dispatch(sqlStatsActions.failed(error)); const changedState = store.getState(); store.dispatch(rootActions.resetState()); const resetState = store.getState(); assert.deepEqual(initState, resetState); assert.notDeepEqual( - resetState.statements.lastError, - changedState.statements.lastError, + resetState.sqlStats.lastError, + changedState.sqlStats.lastError, ); }); }); diff --git a/pkg/ui/workspaces/cluster-ui/src/store/reducers.ts b/pkg/ui/workspaces/cluster-ui/src/store/reducers.ts index 61c0fb72113f..ecd1bc22bfe7 100644 --- a/pkg/ui/workspaces/cluster-ui/src/store/reducers.ts +++ b/pkg/ui/workspaces/cluster-ui/src/store/reducers.ts @@ -10,7 +10,6 @@ import { combineReducers, createStore } from "redux"; import { createAction, createReducer } from "@reduxjs/toolkit"; -import { StatementsState, reducer as statements } from "./statements"; import { LocalStorageState, reducer as localStorage } from "./localStorage"; import { StatementDiagnosticsState, @@ -19,24 +18,23 @@ import { import { NodesState, reducer as nodes } from "./nodes"; import { LivenessState, reducer as liveness } from "./liveness"; import { SessionsState, reducer as sessions } from "./sessions"; -import { TransactionsState, reducer as transactions } from "./transactions"; import { TerminateQueryState, reducer as terminateQuery, } from "./terminateQuery"; import { UIConfigState, reducer as uiConfig } from "./uiConfig"; import { DOMAIN_NAME } from "./utils"; +import { SQLStatsState, reducer as sqlStats } from "./sqlStats"; export type AdminUiState = { - statements: StatementsState; statementDiagnostics: StatementDiagnosticsState; localStorage: LocalStorageState; nodes: NodesState; liveness: LivenessState; sessions: SessionsState; - transactions: TransactionsState; terminateQuery: TerminateQueryState; uiConfig: UIConfigState; + sqlStats: SQLStatsState; }; export type AppState = { @@ -46,13 +44,12 @@ export type AppState = { export const reducers = combineReducers({ localStorage, statementDiagnostics, - statements, nodes, liveness, sessions, - transactions, terminateQuery, uiConfig, + sqlStats, }); export const rootActions = { diff --git a/pkg/ui/workspaces/cluster-ui/src/store/sagas.ts b/pkg/ui/workspaces/cluster-ui/src/store/sagas.ts index c9f5a0c2a3ca..6ff243d76047 100644 --- a/pkg/ui/workspaces/cluster-ui/src/store/sagas.ts +++ b/pkg/ui/workspaces/cluster-ui/src/store/sagas.ts @@ -12,11 +12,9 @@ import { all, fork } from "redux-saga/effects"; import { localStorageSaga } from "./localStorage"; import { statementsDiagnosticsSagas } from "./statementDiagnostics"; -import { statementsSaga } from "./statements"; import { nodesSaga } from "./nodes"; import { livenessSaga } from "./liveness"; import { sessionsSaga } from "./sessions"; -import { transactionsSaga } from "./transactions"; import { terminateSaga } from "./terminateQuery"; import { notifificationsSaga } from "./notifications"; import { sqlStatsSaga } from "./sqlStats"; @@ -24,12 +22,10 @@ import { sqlStatsSaga } from "./sqlStats"; export function* sagas(cacheInvalidationPeriod?: number) { yield all([ fork(localStorageSaga), - fork(statementsSaga, cacheInvalidationPeriod), fork(statementsDiagnosticsSagas, cacheInvalidationPeriod), fork(nodesSaga, cacheInvalidationPeriod), fork(livenessSaga, cacheInvalidationPeriod), fork(sessionsSaga), - fork(transactionsSaga), fork(notifificationsSaga), fork(sqlStatsSaga), ]); 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 8f860541af92..4dbe699c025d 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 @@ -11,26 +11,32 @@ import { createSlice, PayloadAction } from "@reduxjs/toolkit"; import { cockroach } from "@cockroachlabs/crdb-protobuf-client"; import { DOMAIN_NAME, noopReducer } from "../utils"; +import { StatementsRequest } from "src/api/statementsApi"; -type ResetSQLStatsResponse = cockroach.server.serverpb.ResetSQLStatsResponse; +type StatementsResponse = cockroach.server.serverpb.StatementsResponse; -export type ResetSQLStatsState = { - data: ResetSQLStatsResponse; +export type SQLStatsState = { + data: StatementsResponse; lastError: Error; valid: boolean; }; -const initialState: ResetSQLStatsState = { +const initialState: SQLStatsState = { data: null, lastError: null, valid: true, }; -const resetSQLStatsSlice = createSlice({ - name: `${DOMAIN_NAME}/resetsqlstats`, +export type UpdateDateRangePayload = { + start: number; + end: number; +}; + +const sqlStatsSlice = createSlice({ + name: `${DOMAIN_NAME}/sqlstats`, initialState, reducers: { - received: (state, action: PayloadAction) => { + received: (state, action: PayloadAction) => { state.data = action.payload; state.valid = true; state.lastError = null; @@ -42,10 +48,11 @@ const resetSQLStatsSlice = createSlice({ invalidated: state => { state.valid = false; }, - // Define actions that don't change state - refresh: noopReducer, - request: noopReducer, + refresh: (_, action?: PayloadAction) => {}, + request: (_, action?: PayloadAction) => {}, + updateDateRange: (_, action: PayloadAction) => {}, + reset: _ => {}, }, }); -export const { reducer, actions } = resetSQLStatsSlice; +export const { reducer, actions } = sqlStatsSlice; diff --git a/pkg/ui/workspaces/cluster-ui/src/store/sqlStats/sqlStats.sagas.spec.ts b/pkg/ui/workspaces/cluster-ui/src/store/sqlStats/sqlStats.sagas.spec.ts index 10119be5eaf8..13cc90fcf987 100644 --- a/pkg/ui/workspaces/cluster-ui/src/store/sqlStats/sqlStats.sagas.spec.ts +++ b/pkg/ui/workspaces/cluster-ui/src/store/sqlStats/sqlStats.sagas.spec.ts @@ -9,34 +9,159 @@ // licenses/APL.txt. import { expectSaga } from "redux-saga-test-plan"; -import { throwError } from "redux-saga-test-plan/providers"; +import { + EffectProviders, + StaticProvider, + throwError, +} from "redux-saga-test-plan/providers"; import * as matchers from "redux-saga-test-plan/matchers"; import { cockroach } from "@cockroachlabs/crdb-protobuf-client"; -import { resetSQLStatsSaga } from "./sqlStats.sagas"; -import { resetSQLStats } from "../../api/sqlStatsApi"; +import { getStatements, getCombinedStatements } from "src/api/statementsApi"; +import { resetSQLStats } from "src/api/sqlStatsApi"; import { - actions as sqlStatsActions, - reducer as sqlStatsReducers, - ResetSQLStatsState, -} from "./sqlStats.reducer"; -import { actions as statementActions } from "src/store/statements/statements.reducer"; + receivedSQLStatsSaga, + refreshSQLStatsSaga, + requestSQLStatsSaga, + resetSQLStatsSaga, +} from "./sqlStats.sagas"; +import { actions, reducer, SQLStatsState } from "./sqlStats.reducer"; +import Long from "long"; + +describe("SQLStats sagas", () => { + const combinedSQLStatsResponse = new cockroach.server.serverpb.StatementsResponse( + { + statements: [ + { + id: new Long(1), + }, + { + id: new Long(2), + }, + ], + last_reset: null, + }, + ); + const nonCombinedSQLStatsResponse = new cockroach.server.serverpb.StatementsResponse( + { + statements: [ + { + id: new Long(1), + }, + ], + last_reset: null, + }, + ); + + const stmtStatsAPIProvider: (EffectProviders | StaticProvider)[] = [ + [matchers.call.fn(getCombinedStatements), combinedSQLStatsResponse], + [matchers.call.fn(getStatements), nonCombinedSQLStatsResponse], + ]; + + describe("refreshSQLStatsSaga", () => { + it("dispatches request SQLStats action", () => { + expectSaga(refreshSQLStatsSaga) + .put(actions.request()) + .run(); + }); + }); + + describe("requestSQLStatsSaga", () => { + it("successfully requests statements list", () => { + expectSaga(requestSQLStatsSaga) + .provide(stmtStatsAPIProvider) + .put(actions.received(nonCombinedSQLStatsResponse)) + .withReducer(reducer) + .hasFinalState({ + data: nonCombinedSQLStatsResponse, + lastError: null, + valid: true, + }) + .run(); + }); + + it("requests combined SQL Stats if combined=true in the request message", () => { + expectSaga(requestSQLStatsSaga, { + payload: new cockroach.server.serverpb.StatementsRequest({ + combined: true, + }), + }) + .provide(stmtStatsAPIProvider) + .put(actions.received(combinedSQLStatsResponse)) + .withReducer(reducer) + .hasFinalState({ + data: combinedSQLStatsResponse, + lastError: null, + valid: true, + }) + .run(); + }); + + it("requests combined SQL Stats if combined=false in the request message", () => { + expectSaga(requestSQLStatsSaga, { + payload: new cockroach.server.serverpb.StatementsRequest({ + combined: false, + }), + }) + .provide(stmtStatsAPIProvider) + .put(actions.received(nonCombinedSQLStatsResponse)) + .withReducer(reducer) + .hasFinalState({ + data: nonCombinedSQLStatsResponse, + lastError: null, + valid: true, + }) + .run(); + }); + + it("returns error on failed request", () => { + const error = new Error("Failed request"); + expectSaga(requestSQLStatsSaga) + .provide([[matchers.call.fn(getStatements), throwError(error)]]) + .put(actions.failed(error)) + .withReducer(reducer) + .hasFinalState({ + data: null, + lastError: error, + valid: false, + }) + .run(); + }); + }); + + describe("receivedSQLStatsSaga", () => { + it("sets valid status to false after specified period of time", () => { + const timeout = 500; + expectSaga(receivedSQLStatsSaga, timeout) + .delay(timeout) + .put(actions.invalidated()) + .withReducer(reducer, { + data: combinedSQLStatsResponse, + lastError: null, + valid: true, + }) + .hasFinalState({ + data: combinedSQLStatsResponse, + lastError: null, + valid: false, + }) + .run(1000); + }); + }); -describe("SQL Stats sagas", () => { describe("resetSQLStatsSaga", () => { const resetSQLStatsResponse = new cockroach.server.serverpb.ResetSQLStatsResponse(); it("successfully resets SQL stats", () => { expectSaga(resetSQLStatsSaga) .provide([[matchers.call.fn(resetSQLStats), resetSQLStatsResponse]]) - .put(sqlStatsActions.received(resetSQLStatsResponse)) - .put(statementActions.invalidated()) - .put(statementActions.refresh()) - .withReducer(sqlStatsReducers) - .hasFinalState({ - data: resetSQLStatsResponse, + .put(actions.invalidated()) + .put(actions.refresh()) + .withReducer(reducer) + .hasFinalState({ + data: null, lastError: null, - valid: true, + valid: false, }) .run(); }); @@ -45,9 +170,9 @@ describe("SQL Stats sagas", () => { const err = new Error("failed to reset"); expectSaga(resetSQLStatsSaga) .provide([[matchers.call.fn(resetSQLStats), throwError(err)]]) - .put(sqlStatsActions.failed(err)) - .withReducer(sqlStatsReducers) - .hasFinalState({ + .put(actions.failed(err)) + .withReducer(reducer) + .hasFinalState({ data: null, lastError: err, valid: false, diff --git a/pkg/ui/workspaces/cluster-ui/src/store/sqlStats/sqlStats.sagas.ts b/pkg/ui/workspaces/cluster-ui/src/store/sqlStats/sqlStats.sagas.ts index 0b36b93c849c..0ebec662d12e 100644 --- a/pkg/ui/workspaces/cluster-ui/src/store/sqlStats/sqlStats.sagas.ts +++ b/pkg/ui/workspaces/cluster-ui/src/store/sqlStats/sqlStats.sagas.ts @@ -8,22 +8,107 @@ // by the Apache License, Version 2.0, included in the file // licenses/APL.txt. -import { all, call, put, takeEvery } from "redux-saga/effects"; +import { PayloadAction } from "@reduxjs/toolkit"; +import { + all, + call, + put, + delay, + takeLatest, + takeEvery, +} from "redux-saga/effects"; +import Long from "long"; +import { cockroach } from "@cockroachlabs/crdb-protobuf-client"; +import { + getStatements, + getCombinedStatements, + StatementsRequest, +} from "src/api/statementsApi"; import { resetSQLStats } from "src/api/sqlStatsApi"; -import { actions as statementActions } from "src/store/statements"; -import { actions as sqlStatsActions } from "./sqlStats.reducer"; +import { actions as localStorageActions } from "src/store/localStorage"; +import { + actions as sqlStatsActions, + UpdateDateRangePayload, +} from "./sqlStats.reducer"; +import { rootActions } from "../reducers"; -export function* resetSQLStatsSaga(): any { +import { CACHE_INVALIDATION_PERIOD, throttleWithReset } from "src/store/utils"; + +export function* refreshSQLStatsSaga( + action?: PayloadAction, +) { + yield put(sqlStatsActions.request(action?.payload)); +} + +export function* requestSQLStatsSaga( + action?: PayloadAction, +): any { + try { + const result = yield action?.payload?.combined + ? call(getCombinedStatements, action.payload) + : call(getStatements); + yield put(sqlStatsActions.received(result)); + } catch (e) { + yield put(sqlStatsActions.failed(e)); + } +} + +export function* receivedSQLStatsSaga(delayMs: number) { + yield delay(delayMs); + yield put(sqlStatsActions.invalidated()); +} + +export function* updateSQLStatsDateRangeSaga( + action: PayloadAction, +) { + const { start, end } = action.payload; + yield put( + // TODO(azhng): do we want to rename this into dataRange/SQLActivity? + localStorageActions.update({ + key: "dateRange/StatementsPage", + value: { start, end }, + }), + ); + yield put(sqlStatsActions.invalidated()); + const req = new cockroach.server.serverpb.StatementsRequest({ + combined: true, + start: Long.fromNumber(start), + end: Long.fromNumber(end), + }); + yield put(sqlStatsActions.refresh(req)); +} + +export function* resetSQLStatsSaga() { try { - const response = yield call(resetSQLStats); - yield put(sqlStatsActions.received(response)); - yield put(statementActions.invalidated()); - yield put(statementActions.refresh()); + yield call(resetSQLStats); + yield put(sqlStatsActions.invalidated()); + yield put(sqlStatsActions.refresh()); } catch (e) { yield put(sqlStatsActions.failed(e)); } } -export function* sqlStatsSaga() { - yield all([takeEvery(sqlStatsActions.request, resetSQLStatsSaga)]); +export function* sqlStatsSaga( + cacheInvalidationPeriod: number = CACHE_INVALIDATION_PERIOD, +) { + yield all([ + throttleWithReset( + cacheInvalidationPeriod, + sqlStatsActions.refresh, + [ + sqlStatsActions.invalidated, + sqlStatsActions.failed, + rootActions.resetState, + ], + refreshSQLStatsSaga, + ), + takeLatest(sqlStatsActions.request, requestSQLStatsSaga), + takeLatest( + sqlStatsActions.received, + receivedSQLStatsSaga, + cacheInvalidationPeriod, + ), + takeLatest(sqlStatsActions.updateDateRange, updateSQLStatsDateRangeSaga), + takeEvery(sqlStatsActions.reset, resetSQLStatsSaga), + ]); } diff --git a/pkg/ui/workspaces/cluster-ui/src/store/statements/index.ts b/pkg/ui/workspaces/cluster-ui/src/store/sqlStats/sqlStats.selector.ts similarity index 54% rename from pkg/ui/workspaces/cluster-ui/src/store/statements/index.ts rename to pkg/ui/workspaces/cluster-ui/src/store/sqlStats/sqlStats.selector.ts index 68eee7e321b6..2ddec2be2e54 100644 --- a/pkg/ui/workspaces/cluster-ui/src/store/statements/index.ts +++ b/pkg/ui/workspaces/cluster-ui/src/store/sqlStats/sqlStats.selector.ts @@ -8,5 +8,15 @@ // by the Apache License, Version 2.0, included in the file // licenses/APL.txt. -export * from "./statements.reducer"; -export * from "./statements.sagas"; +import { createSelector } from "reselect"; +import { AppState } from "../reducers"; + +const adminUISelector = createSelector( + (state: AppState) => state.adminUI, + adminUiState => adminUiState, +); + +export const sqlStatsSelector = createSelector( + adminUISelector, + adminUiState => adminUiState.sqlStats, +); diff --git a/pkg/ui/workspaces/cluster-ui/src/store/statements/statements.reducer.ts b/pkg/ui/workspaces/cluster-ui/src/store/statements/statements.reducer.ts deleted file mode 100644 index 3836f999fa21..000000000000 --- a/pkg/ui/workspaces/cluster-ui/src/store/statements/statements.reducer.ts +++ /dev/null @@ -1,57 +0,0 @@ -// Copyright 2021 The Cockroach Authors. -// -// Use of this software is governed by the Business Source License -// included in the file licenses/BSL.txt. -// -// As of the Change Date specified in that file, in accordance with -// the Business Source License, use of this software will be governed -// by the Apache License, Version 2.0, included in the file -// licenses/APL.txt. - -import { createSlice, PayloadAction } from "@reduxjs/toolkit"; -import { cockroach } from "@cockroachlabs/crdb-protobuf-client"; -import { DOMAIN_NAME, noopReducer } from "../utils"; -import { StatementsRequest } from "src/api/statementsApi"; - -type StatementsResponse = cockroach.server.serverpb.StatementsResponse; - -export type StatementsState = { - data: StatementsResponse; - lastError: Error; - valid: boolean; -}; - -const initialState: StatementsState = { - data: null, - lastError: null, - valid: true, -}; - -export type UpdateDateRangePayload = { - start: number; - end: number; -}; - -const statementsSlice = createSlice({ - name: `${DOMAIN_NAME}/statements`, - initialState, - reducers: { - received: (state, action: PayloadAction) => { - state.data = action.payload; - state.valid = true; - state.lastError = null; - }, - failed: (state, action: PayloadAction) => { - state.valid = false; - state.lastError = action.payload; - }, - invalidated: state => { - state.valid = false; - }, - refresh: (_, action?: PayloadAction) => {}, - request: (_, action?: PayloadAction) => {}, - updateDateRange: (_, action: PayloadAction) => {}, - }, -}); - -export const { reducer, actions } = statementsSlice; diff --git a/pkg/ui/workspaces/cluster-ui/src/store/statements/statements.sagas.spec.ts b/pkg/ui/workspaces/cluster-ui/src/store/statements/statements.sagas.spec.ts deleted file mode 100644 index 7a757b6de8fc..000000000000 --- a/pkg/ui/workspaces/cluster-ui/src/store/statements/statements.sagas.spec.ts +++ /dev/null @@ -1,104 +0,0 @@ -// Copyright 2021 The Cockroach Authors. -// -// Use of this software is governed by the Business Source License -// included in the file licenses/BSL.txt. -// -// As of the Change Date specified in that file, in accordance with -// the Business Source License, use of this software will be governed -// by the Apache License, Version 2.0, included in the file -// licenses/APL.txt. - -import { expectSaga } from "redux-saga-test-plan"; -import { throwError } from "redux-saga-test-plan/providers"; -import * as matchers from "redux-saga-test-plan/matchers"; -import { cockroach } from "@cockroachlabs/crdb-protobuf-client"; - -import { getStatements, getCombinedStatements } from "src/api/statementsApi"; -import { - receivedStatementsSaga, - refreshStatementsSaga, - requestStatementsSaga, -} from "./statements.sagas"; -import { actions, reducer, StatementsState } from "./statements.reducer"; - -describe("StatementsPage sagas", () => { - const statements = new cockroach.server.serverpb.StatementsResponse({ - statements: [], - last_reset: null, - }); - - describe("refreshStatementsSaga", () => { - it("dispatches request statements action", () => { - expectSaga(refreshStatementsSaga) - .put(actions.request()) - .run(); - }); - }); - - describe("requestStatementsSaga", () => { - it("successfully requests statements list", () => { - expectSaga(requestStatementsSaga) - .provide([[matchers.call.fn(getStatements), statements]]) - .put(actions.received(statements)) - .withReducer(reducer) - .hasFinalState({ - data: statements, - lastError: null, - valid: true, - }) - .run(); - }); - - it("requests combined statements if combined=true in the request message", () => { - const req = { - payload: new cockroach.server.serverpb.StatementsRequest({ - combined: true, - }), - }; - expectSaga(requestStatementsSaga, req) - .provide([[matchers.call.fn(getCombinedStatements), statements]]) - .put(actions.received(statements)) - .withReducer(reducer) - .hasFinalState({ - data: statements, - lastError: null, - valid: true, - }) - .run(); - }); - - it("returns error on failed request", () => { - const error = new Error("Failed request"); - expectSaga(requestStatementsSaga) - .provide([[matchers.call.fn(getStatements), throwError(error)]]) - .put(actions.failed(error)) - .withReducer(reducer) - .hasFinalState({ - data: null, - lastError: error, - valid: false, - }) - .run(); - }); - }); - - describe("receivedStatementsSaga", () => { - it("sets valid status to false after specified period of time", () => { - const timeout = 500; - expectSaga(receivedStatementsSaga, timeout) - .delay(timeout) - .put(actions.invalidated()) - .withReducer(reducer, { - data: statements, - lastError: null, - valid: true, - }) - .hasFinalState({ - data: statements, - lastError: null, - valid: false, - }) - .run(1000); - }); - }); -}); diff --git a/pkg/ui/workspaces/cluster-ui/src/store/statements/statements.sagas.ts b/pkg/ui/workspaces/cluster-ui/src/store/statements/statements.sagas.ts deleted file mode 100644 index a1c00ca44563..000000000000 --- a/pkg/ui/workspaces/cluster-ui/src/store/statements/statements.sagas.ts +++ /dev/null @@ -1,87 +0,0 @@ -// Copyright 2021 The Cockroach Authors. -// -// Use of this software is governed by the Business Source License -// included in the file licenses/BSL.txt. -// -// As of the Change Date specified in that file, in accordance with -// the Business Source License, use of this software will be governed -// by the Apache License, Version 2.0, included in the file -// licenses/APL.txt. - -import { PayloadAction } from "@reduxjs/toolkit"; -import { all, call, put, delay, takeLatest } from "redux-saga/effects"; -import Long from "long"; -import { cockroach } from "@cockroachlabs/crdb-protobuf-client"; -import { - getStatements, - getCombinedStatements, - StatementsRequest, -} from "src/api/statementsApi"; -import { actions as localStorageActions } from "src/store/localStorage"; -import { actions, UpdateDateRangePayload } from "./statements.reducer"; -import { rootActions } from "../reducers"; - -import { CACHE_INVALIDATION_PERIOD, throttleWithReset } from "src/store/utils"; - -export function* refreshStatementsSaga( - action?: PayloadAction, -) { - yield put(actions.request(action?.payload)); -} - -export function* requestStatementsSaga( - action?: PayloadAction, -): any { - try { - const result = yield action?.payload?.combined - ? call(getCombinedStatements, action.payload) - : call(getStatements); - yield put(actions.received(result)); - } catch (e) { - yield put(actions.failed(e)); - } -} - -export function* receivedStatementsSaga(delayMs: number) { - yield delay(delayMs); - yield put(actions.invalidated()); -} - -export function* updateStatementesDateRangeSaga( - action: PayloadAction, -) { - const { start, end } = action.payload; - yield put( - localStorageActions.update({ - key: "dateRange/StatementsPage", - value: { start, end }, - }), - ); - yield put(actions.invalidated()); - const req = new cockroach.server.serverpb.StatementsRequest({ - combined: true, - start: Long.fromNumber(start), - end: Long.fromNumber(end), - }); - yield put(actions.refresh(req)); -} - -export function* statementsSaga( - cacheInvalidationPeriod: number = CACHE_INVALIDATION_PERIOD, -) { - yield all([ - throttleWithReset( - cacheInvalidationPeriod, - actions.refresh, - [actions.invalidated, actions.failed, rootActions.resetState], - refreshStatementsSaga, - ), - takeLatest(actions.request, requestStatementsSaga), - takeLatest( - actions.received, - receivedStatementsSaga, - cacheInvalidationPeriod, - ), - takeLatest(actions.updateDateRange, updateStatementesDateRangeSaga), - ]); -} diff --git a/pkg/ui/workspaces/cluster-ui/src/store/transactions/index.ts b/pkg/ui/workspaces/cluster-ui/src/store/transactions/index.ts deleted file mode 100644 index 14a252d8b2d5..000000000000 --- a/pkg/ui/workspaces/cluster-ui/src/store/transactions/index.ts +++ /dev/null @@ -1,12 +0,0 @@ -// Copyright 2021 The Cockroach Authors. -// -// Use of this software is governed by the Business Source License -// included in the file licenses/BSL.txt. -// -// As of the Change Date specified in that file, in accordance with -// the Business Source License, use of this software will be governed -// by the Apache License, Version 2.0, included in the file -// licenses/APL.txt. - -export * from "./transactions.reducer"; -export * from "./transactions.sagas"; diff --git a/pkg/ui/workspaces/cluster-ui/src/store/transactions/transactions.reducer.ts b/pkg/ui/workspaces/cluster-ui/src/store/transactions/transactions.reducer.ts deleted file mode 100644 index 4c8d9ddd4c6f..000000000000 --- a/pkg/ui/workspaces/cluster-ui/src/store/transactions/transactions.reducer.ts +++ /dev/null @@ -1,52 +0,0 @@ -// Copyright 2021 The Cockroach Authors. -// -// Use of this software is governed by the Business Source License -// included in the file licenses/BSL.txt. -// -// As of the Change Date specified in that file, in accordance with -// the Business Source License, use of this software will be governed -// by the Apache License, Version 2.0, included in the file -// licenses/APL.txt. - -import { createSlice, PayloadAction } from "@reduxjs/toolkit"; -import { cockroach } from "@cockroachlabs/crdb-protobuf-client"; -import { DOMAIN_NAME, noopReducer } from "../utils"; -import { StatementsRequest } from "src/api/statementsApi"; - -type StatementsResponse = cockroach.server.serverpb.StatementsResponse; - -export type TransactionsState = { - data: StatementsResponse; - lastError: Error; - valid: boolean; -}; - -const initialState: TransactionsState = { - data: null, - lastError: null, - valid: true, -}; - -const transactionsSlice = createSlice({ - name: `${DOMAIN_NAME}/transactions`, - initialState, - reducers: { - received: (state, action: PayloadAction) => { - state.data = action.payload; - state.valid = true; - state.lastError = null; - }, - failed: (state, action: PayloadAction) => { - state.valid = false; - state.lastError = action.payload; - }, - invalidated: state => { - state.valid = false; - }, - // Define actions that don't change state - refresh: (_, action?: PayloadAction) => {}, - request: (_, action?: PayloadAction) => {}, - }, -}); - -export const { reducer, actions } = transactionsSlice; diff --git a/pkg/ui/workspaces/cluster-ui/src/store/transactions/transactions.sagas.spec.ts b/pkg/ui/workspaces/cluster-ui/src/store/transactions/transactions.sagas.spec.ts deleted file mode 100644 index 12ad3245c633..000000000000 --- a/pkg/ui/workspaces/cluster-ui/src/store/transactions/transactions.sagas.spec.ts +++ /dev/null @@ -1,105 +0,0 @@ -// Copyright 2021 The Cockroach Authors. -// -// Use of this software is governed by the Business Source License -// included in the file licenses/BSL.txt. -// -// As of the Change Date specified in that file, in accordance with -// the Business Source License, use of this software will be governed -// by the Apache License, Version 2.0, included in the file -// licenses/APL.txt. - -import { expectSaga } from "redux-saga-test-plan"; -import { throwError } from "redux-saga-test-plan/providers"; -import * as matchers from "redux-saga-test-plan/matchers"; -import { cockroach } from "@cockroachlabs/crdb-protobuf-client"; - -import { getCombinedStatements, getStatements } from "src/api/statementsApi"; - -import { - receivedTransactionsSaga, - refreshTransactionsSaga, - requestTransactionsSaga, -} from "./transactions.sagas"; -import { actions, reducer, TransactionsState } from "./transactions.reducer"; - -describe("TransactionsPage sagas", () => { - const statements = new cockroach.server.serverpb.StatementsResponse({ - statements: [], - last_reset: null, - }); - - describe("refreshTransactionsSaga", () => { - it("dispatches request transactions action", () => { - expectSaga(refreshTransactionsSaga) - .put(actions.request()) - .run(); - }); - }); - - describe("requestStatementsSaga", () => { - it("successfully requests statements list", () => { - expectSaga(requestTransactionsSaga) - .provide([[matchers.call.fn(getStatements), statements]]) - .put(actions.received(statements)) - .withReducer(reducer) - .hasFinalState({ - data: statements, - lastError: null, - valid: true, - }) - .run(); - }); - - it("requests combined statements if combined=true in the request message", () => { - const req = { - payload: new cockroach.server.serverpb.StatementsRequest({ - combined: true, - }), - }; - expectSaga(requestTransactionsSaga, req) - .provide([[matchers.call.fn(getCombinedStatements), statements]]) - .put(actions.received(statements)) - .withReducer(reducer) - .hasFinalState({ - data: statements, - lastError: null, - valid: true, - }) - .run(); - }); - - it("returns error on failed request", () => { - const error = new Error("Failed request"); - expectSaga(requestTransactionsSaga) - .provide([[matchers.call.fn(getStatements), throwError(error)]]) - .put(actions.failed(error)) - .withReducer(reducer) - .hasFinalState({ - data: null, - lastError: error, - valid: false, - }) - .run(); - }); - }); - - describe("receivedStatementsSaga", () => { - it("sets valid status to false after specified period of time", () => { - const timeout = 500; - expectSaga(receivedTransactionsSaga, timeout) - .delay(timeout) - .put(actions.invalidated()) - .withReducer(reducer, { - data: statements, - lastError: null, - valid: true, - }) - .hasFinalState({ - data: statements, - lastError: null, - valid: false, - }) - .run(1000); - }); - }); -}); diff --git a/pkg/ui/workspaces/cluster-ui/src/store/transactions/transactions.sagas.ts b/pkg/ui/workspaces/cluster-ui/src/store/transactions/transactions.sagas.ts deleted file mode 100644 index 076b8e8f6714..000000000000 --- a/pkg/ui/workspaces/cluster-ui/src/store/transactions/transactions.sagas.ts +++ /dev/null @@ -1,64 +0,0 @@ -// Copyright 2021 The Cockroach Authors. -// -// Use of this software is governed by the Business Source License -// included in the file licenses/BSL.txt. -// -// As of the Change Date specified in that file, in accordance with -// the Business Source License, use of this software will be governed -// by the Apache License, Version 2.0, included in the file -// licenses/APL.txt. - -import { PayloadAction } from "@reduxjs/toolkit"; -import { all, call, put, delay, takeLatest } from "redux-saga/effects"; -import { - getStatements, - getCombinedStatements, - StatementsRequest, -} from "src/api/statementsApi"; -import { actions } from "./transactions.reducer"; -import { rootActions } from "../reducers"; - -import { CACHE_INVALIDATION_PERIOD, throttleWithReset } from "src/store/utils"; - -export function* refreshTransactionsSaga( - action?: PayloadAction, -) { - yield put(actions.request(action?.payload)); -} - -export function* requestTransactionsSaga( - action?: PayloadAction, -): any { - try { - const result = yield action?.payload?.combined - ? call(getCombinedStatements, action.payload) - : call(getStatements); - yield put(actions.received(result)); - } catch (e) { - yield put(actions.failed(e)); - } -} - -export function* receivedTransactionsSaga(delayMs: number) { - yield delay(delayMs); - yield put(actions.invalidated()); -} - -export function* transactionsSaga( - cacheInvalidationPeriod: number = CACHE_INVALIDATION_PERIOD, -) { - yield all([ - throttleWithReset( - cacheInvalidationPeriod, - actions.refresh, - [actions.invalidated, actions.failed, rootActions.resetState], - refreshTransactionsSaga, - ), - takeLatest(actions.request, requestTransactionsSaga), - takeLatest( - actions.received, - receivedTransactionsSaga, - cacheInvalidationPeriod, - ), - ]); -} diff --git a/pkg/ui/workspaces/cluster-ui/src/transactionsPage/transactionsPage.selectors.ts b/pkg/ui/workspaces/cluster-ui/src/transactionsPage/transactionsPage.selectors.ts index 2846fb33fe33..a80471ff76fb 100644 --- a/pkg/ui/workspaces/cluster-ui/src/transactionsPage/transactionsPage.selectors.ts +++ b/pkg/ui/workspaces/cluster-ui/src/transactionsPage/transactionsPage.selectors.ts @@ -10,25 +10,18 @@ import { createSelector } from "reselect"; -import { - adminUISelector, - localStorageSelector, -} from "../statementsPage/statementsPage.selectors"; - -export const selectTransactionsSlice = createSelector( - adminUISelector, - adminUiState => adminUiState.transactions, -); +import { localStorageSelector } from "../statementsPage/statementsPage.selectors"; +import { sqlStatsSelector } from "../store/sqlStats/sqlStats.selector"; export const selectTransactionsData = createSelector( - selectTransactionsSlice, + sqlStatsSelector, transactionsState => // The state is valid if we have successfully fetched data, and it has not yet been invalidated. transactionsState.valid ? transactionsState.data : null, ); export const selectTransactionsLastError = createSelector( - selectTransactionsSlice, + sqlStatsSelector, state => state.lastError, ); diff --git a/pkg/ui/workspaces/cluster-ui/src/transactionsPage/transactionsPageConnected.tsx b/pkg/ui/workspaces/cluster-ui/src/transactionsPage/transactionsPageConnected.tsx index 1f4c70bce0ae..17ad40d1ada7 100644 --- a/pkg/ui/workspaces/cluster-ui/src/transactionsPage/transactionsPageConnected.tsx +++ b/pkg/ui/workspaces/cluster-ui/src/transactionsPage/transactionsPageConnected.tsx @@ -14,9 +14,7 @@ import { Dispatch } from "redux"; import { Moment } from "moment"; import { AppState } from "src/store"; -import { actions as transactionsActions } from "src/store/transactions"; -import { actions as resetSQLStatsActions } from "src/store/sqlStats"; -import { actions as statementsActions } from "src/store/statements"; +import { actions as sqlStatsActions } from "src/store/sqlStats"; import { TransactionsPage } from "./transactionsPage"; import { TransactionsPageStateProps, @@ -52,11 +50,11 @@ export const TransactionsPageConnected = withRouter( }), (dispatch: Dispatch) => ({ refreshData: (req?: StatementsRequest) => - dispatch(transactionsActions.refresh(req)), - resetSQLStats: () => dispatch(resetSQLStatsActions.request()), + dispatch(sqlStatsActions.refresh(req)), + resetSQLStats: () => dispatch(sqlStatsActions.reset()), onDateRangeChange: (start: Moment, end: Moment) => { dispatch( - statementsActions.updateDateRange({ + sqlStatsActions.updateDateRange({ start: start.unix(), end: end.unix(), }), 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 e82bddd2a8f9..51548d01e0ab 100644 --- a/pkg/ui/workspaces/db-console/src/views/transactions/transactionsPage.tsx +++ b/pkg/ui/workspaces/db-console/src/views/transactions/transactionsPage.tsx @@ -32,7 +32,7 @@ import { LocalSetting } from "src/redux/localsettings"; export const selectData = createSelector( (state: AdminUIState) => state.cachedData.statements, (state: CachedDataReducerState) => { - if (!state.data || state.inFlight) return null; + if (!state.data || state.inFlight || !state.valid) return null; return state.data; }, );