diff --git a/pkg/ui/workspaces/cluster-ui/src/api/indexDetailsApi.ts b/pkg/ui/workspaces/cluster-ui/src/api/indexDetailsApi.ts new file mode 100644 index 000000000000..22b1b59d3518 --- /dev/null +++ b/pkg/ui/workspaces/cluster-ui/src/api/indexDetailsApi.ts @@ -0,0 +1,52 @@ +// Copyright 2022 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 { cockroach } from "@cockroachlabs/crdb-protobuf-client"; +import { fetchData } from "src/api"; + +export type TableIndexStatsRequest = + cockroach.server.serverpb.TableIndexStatsRequest; +export type TableIndexStatsResponse = + cockroach.server.serverpb.TableIndexStatsResponse; +export type TableIndexStatsResponseWithKey = { + indexStatsResponse: TableIndexStatsResponse; + key: string; +}; + +type ResetIndexUsageStatsRequest = + cockroach.server.serverpb.ResetIndexUsageStatsRequest; +type ResetIndexUsageStatsResponse = + cockroach.server.serverpb.ResetIndexUsageStatsResponse; + +// getIndexStats gets detailed stats about the current table's index usage statistics. +export const getIndexStats = ( + req: TableIndexStatsRequest, +): Promise => { + return fetchData( + cockroach.server.serverpb.TableIndexStatsResponse, + `/_status/databases/${req.database}/tables/${req.table}/indexstats`, + null, + null, + "30M", + ); +}; + +// resetIndexStats refreshes all index usage stats for all tables. +export const resetIndexStats = ( + req: ResetIndexUsageStatsRequest, +): Promise => { + return fetchData( + cockroach.server.serverpb.ResetIndexUsageStatsResponse, + "/_status/resetindexusagestats", + null, + req, + "30M", + ); +}; diff --git a/pkg/ui/workspaces/cluster-ui/src/indexDetailsPage/index.ts b/pkg/ui/workspaces/cluster-ui/src/indexDetailsPage/index.ts index dd80cc90ca3e..7de789a111c3 100644 --- a/pkg/ui/workspaces/cluster-ui/src/indexDetailsPage/index.ts +++ b/pkg/ui/workspaces/cluster-ui/src/indexDetailsPage/index.ts @@ -9,3 +9,4 @@ // licenses/APL.txt. export * from "./indexDetailsPage"; +export * from "./indexDetailsConnected"; diff --git a/pkg/ui/workspaces/cluster-ui/src/indexDetailsPage/indexDetails.selectors.ts b/pkg/ui/workspaces/cluster-ui/src/indexDetailsPage/indexDetails.selectors.ts new file mode 100644 index 000000000000..46f82e5b316c --- /dev/null +++ b/pkg/ui/workspaces/cluster-ui/src/indexDetailsPage/indexDetails.selectors.ts @@ -0,0 +1,104 @@ +// Copyright 2022 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 { createSelector } from "reselect"; +import { AppState } from "../store"; +import { RouteComponentProps } from "react-router"; +import { + databaseNameAttr, + generateTableID, + getMatchParamByName, + indexNameAttr, + longToInt, + schemaNameAttr, + tableNameAttr, + TimestampToMoment, +} from "../util"; +import { cockroach } from "@cockroachlabs/crdb-protobuf-client"; +import { IndexDetailsPageData } from "./indexDetailsPage"; +import { selectIsTenant } from "../store/uiConfig"; +import { BreadcrumbItem } from "../breadcrumbs"; +const { RecommendationType } = cockroach.sql.IndexRecommendation; + +export const selectIndexDetails = createSelector( + (_state: AppState, props: RouteComponentProps): string => + getMatchParamByName(props.match, databaseNameAttr), + (_state: AppState, props: RouteComponentProps): string => + getMatchParamByName(props.match, schemaNameAttr), + (_state: AppState, props: RouteComponentProps): string => + getMatchParamByName(props.match, tableNameAttr), + (_state: AppState, props: RouteComponentProps): string => + getMatchParamByName(props.match, indexNameAttr), + (state: AppState) => state.adminUI.indexStats.cachedData, + (state: AppState) => selectIsTenant(state), + (database, schema, table, index, indexStats): IndexDetailsPageData => { + const stats = indexStats[generateTableID(database, table)]; + const details = stats?.data?.statistics.filter( + stat => stat.index_name === index, // index names must be unique for a table + )[0]; + const filteredIndexRecommendations = + stats?.data?.index_recommendations.filter( + indexRec => indexRec.index_id === details.statistics.key.index_id, + ) || []; + const indexRecommendations = filteredIndexRecommendations.map(indexRec => { + return { + type: RecommendationType[indexRec.type].toString(), + reason: indexRec.reason, + }; + }); + + return { + databaseName: database, + tableName: table, + indexName: index, + breadcrumbItems: createManagedServiceBreadcrumbs( + database, + schema, + table, + index, + ), + details: { + loading: !!stats?.inFlight, + loaded: !!stats?.valid, + createStatement: details?.create_statement || "", + totalReads: + longToInt(details?.statistics?.stats?.total_read_count) || 0, + lastRead: TimestampToMoment(details?.statistics?.stats?.last_read), + lastReset: TimestampToMoment(stats?.data?.last_reset), + indexRecommendations, + }, + }; + }, +); + +// Note: if the managed-service routes to the index detail or the previous +// database pages change, the breadcrumbs displayed here need to be updated. +function createManagedServiceBreadcrumbs( + database: string, + schema: string, + table: string, + index: string, +): BreadcrumbItem[] { + return [ + { link: "/databases", name: "Databases" }, + { + link: `/databases/${database}`, + name: "Tables", + }, + { + link: `/databases/${database}/${schema}/${table}`, + name: `Table: ${table}`, + }, + { + link: `/databases/${database}/${schema}/${table}/${index}`, + name: `Index: ${index}`, + }, + ]; +} diff --git a/pkg/ui/workspaces/cluster-ui/src/indexDetailsPage/indexDetailsConnected.ts b/pkg/ui/workspaces/cluster-ui/src/indexDetailsPage/indexDetailsConnected.ts new file mode 100644 index 000000000000..da511800e103 --- /dev/null +++ b/pkg/ui/workspaces/cluster-ui/src/indexDetailsPage/indexDetailsConnected.ts @@ -0,0 +1,49 @@ +// Copyright 2022 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 { AppState } from "../store"; +import { RouteComponentProps, withRouter } from "react-router-dom"; +import { selectIndexDetails } from "./indexDetails.selectors"; +import { Dispatch } from "redux"; +import { IndexDetailPageActions, IndexDetailsPage } from "./indexDetailsPage"; +import { connect } from "react-redux"; +import { actions as indexStatsActions } from "src/store/indexStats/indexStats.reducer"; +import { cockroach } from "@cockroachlabs/crdb-protobuf-client"; +import { actions as nodesActions } from "../store/nodes"; + +const mapStateToProps = (state: AppState, props: RouteComponentProps) => { + return selectIndexDetails(state, props); +}; + +const mapDispatchToProps = (dispatch: Dispatch): IndexDetailPageActions => ({ + refreshIndexStats: (database: string, table: string) => { + dispatch( + indexStatsActions.refresh( + new cockroach.server.serverpb.TableIndexStatsRequest({ + database, + table, + }), + ), + ); + }, + resetIndexUsageStats: (database: string, table: string) => { + dispatch( + indexStatsActions.reset({ + database, + table, + }), + ); + }, + refreshNodes: () => dispatch(nodesActions.refresh()), +}); + +export const ConnectedIndexDetailsPage = withRouter( + connect(mapStateToProps, mapDispatchToProps)(IndexDetailsPage), +); diff --git a/pkg/ui/workspaces/cluster-ui/src/indexDetailsPage/indexDetailsPage.module.scss b/pkg/ui/workspaces/cluster-ui/src/indexDetailsPage/indexDetailsPage.module.scss index 552c1c75e741..6cc3ee36ca0c 100644 --- a/pkg/ui/workspaces/cluster-ui/src/indexDetailsPage/indexDetailsPage.module.scss +++ b/pkg/ui/workspaces/cluster-ui/src/indexDetailsPage/indexDetailsPage.module.scss @@ -98,6 +98,24 @@ } .index-recommendations { + &-rows { + display: flex; + flex-direction: row; + align-items: center; + padding: 16px; + + &__header { + font-family: $font-family--semi-bold; + flex-basis: 20%; + flex-shrink: 0; + } + + &__content { + font-family: $font-family--base; + width: 65%; + flex-grow: 0; + } + } &__tooltip-anchor { a { &:hover { diff --git a/pkg/ui/workspaces/cluster-ui/src/indexDetailsPage/indexDetailsPage.stories.tsx b/pkg/ui/workspaces/cluster-ui/src/indexDetailsPage/indexDetailsPage.stories.tsx index e86394653f35..b3af43360003 100644 --- a/pkg/ui/workspaces/cluster-ui/src/indexDetailsPage/indexDetailsPage.stories.tsx +++ b/pkg/ui/workspaces/cluster-ui/src/indexDetailsPage/indexDetailsPage.stories.tsx @@ -37,6 +37,21 @@ const withData: IndexDetailsPageProps = { }, ], }, + breadcrumbItems: [ + { link: "/databases", name: "Databases" }, + { + link: `/databases/story_db`, + name: "Tables", + }, + { + link: `/database/story_db/$public/story_table`, + name: `Table: story_table`, + }, + { + link: `/database/story_db/public/story_table/story_index`, + name: `Index: story_index`, + }, + ], refreshIndexStats: () => {}, resetIndexUsageStats: () => {}, refreshNodes: () => {}, diff --git a/pkg/ui/workspaces/cluster-ui/src/indexDetailsPage/indexDetailsPage.tsx b/pkg/ui/workspaces/cluster-ui/src/indexDetailsPage/indexDetailsPage.tsx index ba6d5b02d734..d6746ae37163 100644 --- a/pkg/ui/workspaces/cluster-ui/src/indexDetailsPage/indexDetailsPage.tsx +++ b/pkg/ui/workspaces/cluster-ui/src/indexDetailsPage/indexDetailsPage.tsx @@ -15,7 +15,7 @@ import { SortSetting } from "src/sortedtable"; import styles from "./indexDetailsPage.module.scss"; import { baseHeadingClasses } from "src/transactionsPage/transactionsPageClasses"; import { CaretRight } from "../icon/caretRight"; -import { Breadcrumbs } from "../breadcrumbs"; +import { BreadcrumbItem, Breadcrumbs } from "../breadcrumbs"; import { Caution, Search as IndexIcon } from "@cockroachlabs/icons"; import { SqlBox } from "src/sql"; import { Col, Row, Tooltip } from "antd"; @@ -54,6 +54,7 @@ export interface IndexDetailsPageData { tableName: string; indexName: string; details: IndexDetails; + breadcrumbItems: BreadcrumbItem[]; } interface IndexDetails { @@ -131,7 +132,11 @@ export class IndexDetailsPage extends React.Component< indexRecommendations: IndexRecommendation[], ) { if (indexRecommendations.length === 0) { - return "None"; + return ( + + None + + ); } return indexRecommendations.map(recommendation => { let recommendationType: string; @@ -145,12 +150,11 @@ export class IndexDetailsPage extends React.Component< return ( @@ -159,7 +163,7 @@ export class IndexDetailsPage extends React.Component< @@ -174,31 +178,44 @@ export class IndexDetailsPage extends React.Component< }); } + private renderBreadcrumbs() { + if (this.props.breadcrumbItems) { + return ( + } + /> + ); + } + // If no props are passed, render db-console breadcrumb links by default. + return ( + } + /> + ); + } + render() { return (
- - } - /> + {this.renderBreadcrumbs()}

Index recommendations - +
{this.renderIndexRecommendations( this.props.details.indexRecommendations, diff --git a/pkg/ui/workspaces/cluster-ui/src/store/indexStats/index.ts b/pkg/ui/workspaces/cluster-ui/src/store/indexStats/index.ts new file mode 100644 index 000000000000..d356e8a56a12 --- /dev/null +++ b/pkg/ui/workspaces/cluster-ui/src/store/indexStats/index.ts @@ -0,0 +1,12 @@ +// Copyright 2022 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 "./indexStats.reducer"; +export * from "./indexStats.sagas"; diff --git a/pkg/ui/workspaces/cluster-ui/src/store/indexStats/indexStats.reducer.ts b/pkg/ui/workspaces/cluster-ui/src/store/indexStats/indexStats.reducer.ts new file mode 100644 index 000000000000..a91a5305b52a --- /dev/null +++ b/pkg/ui/workspaces/cluster-ui/src/store/indexStats/indexStats.reducer.ts @@ -0,0 +1,111 @@ +// Copyright 2022 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 { DOMAIN_NAME } from "../utils"; +import { ErrorWithKey } from "../../api"; +import { generateTableID } from "../../util"; +import { + TableIndexStatsRequest, + TableIndexStatsResponse, + TableIndexStatsResponseWithKey, +} from "../../api/indexDetailsApi"; + +export type IndexStatsState = { + data: TableIndexStatsResponse; + lastError: Error; + valid: boolean; + inFlight: boolean; +}; + +export type IndexStatsReducerState = { + cachedData: { + [id: string]: IndexStatsState; + }; +}; + +export type ResetIndexUsageStatsPayload = { + database: string; + table: string; +}; + +const initialState: IndexStatsReducerState = { + cachedData: {}, +}; + +const indexStatsSlice = createSlice({ + name: `${DOMAIN_NAME}/indexstats`, + initialState, + reducers: { + received: ( + state, + action: PayloadAction, + ) => { + state.cachedData[action.payload.key] = { + data: action.payload.indexStatsResponse, + valid: true, + lastError: null, + inFlight: false, + }; + }, + failed: (state, action: PayloadAction) => { + state.cachedData[action.payload.key] = { + data: null, + valid: false, + lastError: action.payload.err, + inFlight: false, + }; + }, + invalidated: (state, action: PayloadAction<{ key: string }>) => { + delete state.cachedData[action.payload.key]; + }, + invalidateAll: state => { + const keys = Object.keys(state); + for (const key in keys) { + delete state.cachedData[key]; + } + }, + refresh: (state, action: PayloadAction) => { + const key = action?.payload + ? generateTableID(action.payload.database, action.payload.table) + : ""; + state.cachedData[key] = { + data: null, + valid: false, + lastError: null, + inFlight: true, + }; + }, + request: (state, action: PayloadAction) => { + const key = action?.payload + ? generateTableID(action.payload.database, action.payload.table) + : ""; + state.cachedData[key] = { + data: null, + valid: false, + lastError: null, + inFlight: true, + }; + }, + reset: (state, action: PayloadAction) => { + const key = action?.payload + ? generateTableID(action.payload.database, action.payload.table) + : ""; + state.cachedData[key] = { + data: null, + valid: false, + lastError: null, + inFlight: true, + }; + }, + }, +}); + +export const { reducer, actions } = indexStatsSlice; diff --git a/pkg/ui/workspaces/cluster-ui/src/store/indexStats/indexStats.sagas.spec.ts b/pkg/ui/workspaces/cluster-ui/src/store/indexStats/indexStats.sagas.spec.ts new file mode 100644 index 000000000000..5f24b071074c --- /dev/null +++ b/pkg/ui/workspaces/cluster-ui/src/store/indexStats/indexStats.sagas.spec.ts @@ -0,0 +1,204 @@ +// Copyright 2022 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 { cockroach } from "@cockroachlabs/crdb-protobuf-client"; +import { PayloadAction } from "@reduxjs/toolkit"; +import { generateTableID } from "../../util"; +import Long from "long"; +import RecommendationType = cockroach.sql.IndexRecommendation.RecommendationType; +import { + EffectProviders, + StaticProvider, + throwError, +} from "redux-saga-test-plan/providers"; +import * as matchers from "redux-saga-test-plan/matchers"; +import { + getIndexStats, + resetIndexStats, + TableIndexStatsRequest, +} from "../../api/indexDetailsApi"; +import { expectSaga } from "redux-saga-test-plan"; +import { + refreshIndexStatsSaga, + requestIndexStatsSaga, + resetIndexStatsSaga, +} from "./indexStats.sagas"; +import { + actions, + IndexStatsReducerState, + reducer, + ResetIndexUsageStatsPayload, +} from "./indexStats.reducer"; + +describe("IndexStats sagas", () => { + const database = "test_db"; + const table = "test_table"; + const requestAction: PayloadAction = { + payload: cockroach.server.serverpb.TableIndexStatsRequest.create({ + database: database, + table: table, + }), + type: "request", + }; + const resetAction: PayloadAction = { + payload: { + database: database, + table: table, + }, + type: "reset", + }; + const key = generateTableID(database, table); + const tableIndexStatsResponse = + new cockroach.server.serverpb.TableIndexStatsResponse({ + statistics: [ + { + statistics: { + key: { + table_id: 1, + index_id: 2, + }, + stats: { + total_read_count: Long.fromInt(0, true), + last_read: null, + total_rows_read: Long.fromInt(0, true), + total_write_count: Long.fromInt(1, true), + last_write: null, + total_rows_written: Long.fromInt(5, true), + }, + }, + index_name: "test_index", + index_type: "secondary", + create_statement: "mock create statement", + created_at: null, + }, + ], + last_reset: null, + index_recommendations: [ + { + table_id: 1, + index_id: 2, + type: RecommendationType.DROP_UNUSED, + reason: "mock reason", + }, + ], + }); + const resetIndexStatsResponse = + new cockroach.server.serverpb.ResetIndexUsageStatsResponse(); + const indexStatsAPIProvider: (EffectProviders | StaticProvider)[] = [ + [matchers.call.fn(getIndexStats), tableIndexStatsResponse], + [matchers.call.fn(resetIndexStats), resetIndexStatsResponse], + ]; + + describe("refreshIndexStatsSaga", () => { + it("dispatches request IndexStats action", () => { + return expectSaga(refreshIndexStatsSaga).put(actions.request()).run(); + }); + }); + + describe("requestIndexStatsSaga", () => { + it("successfully requests index stats", () => { + return expectSaga(requestIndexStatsSaga, requestAction) + .provide(indexStatsAPIProvider) + .put( + actions.received({ + indexStatsResponse: tableIndexStatsResponse, + key, + }), + ) + .withReducer(reducer) + .hasFinalState({ + cachedData: { + "test_db/test_table": { + data: tableIndexStatsResponse, + lastError: null, + valid: true, + inFlight: false, + }, + }, + }) + .run(); + }); + + it("returns error on failed request", () => { + const error = new Error("Failed request"); + return expectSaga(requestIndexStatsSaga, requestAction) + .provide([[matchers.call.fn(getIndexStats), throwError(error)]]) + .put( + actions.failed({ + err: error, + key, + }), + ) + .withReducer(reducer) + .hasFinalState({ + cachedData: { + "test_db/test_table": { + data: null, + lastError: error, + valid: false, + inFlight: false, + }, + }, + }) + .run(); + }); + }); + + describe("resetIndexStatsSaga", () => { + it("successfully resets index stats", () => { + return expectSaga(resetIndexStatsSaga, resetAction) + .provide(indexStatsAPIProvider) + .put(actions.invalidateAll()) + .put( + actions.refresh( + new cockroach.server.serverpb.TableIndexStatsRequest({ + ...resetAction.payload, + }), + ), + ) + .withReducer(reducer) + .hasFinalState({ + cachedData: { + "test_db/test_table": { + data: null, + valid: false, + lastError: null, + inFlight: true, + }, + }, + }) + .run(); + }); + + it("returns error on failed reset", () => { + const err = new Error("failed to reset"); + return expectSaga(resetIndexStatsSaga, resetAction) + .provide([[matchers.call.fn(resetIndexStats), throwError(err)]]) + .put( + actions.failed({ + err: err, + key, + }), + ) + .withReducer(reducer) + .hasFinalState({ + cachedData: { + "test_db/test_table": { + data: null, + lastError: err, + valid: false, + inFlight: false, + }, + }, + }) + .run(); + }); + }); +}); diff --git a/pkg/ui/workspaces/cluster-ui/src/store/indexStats/indexStats.sagas.ts b/pkg/ui/workspaces/cluster-ui/src/store/indexStats/indexStats.sagas.ts new file mode 100644 index 000000000000..e606e8956d1a --- /dev/null +++ b/pkg/ui/workspaces/cluster-ui/src/store/indexStats/indexStats.sagas.ts @@ -0,0 +1,115 @@ +// Copyright 2022 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, + takeEvery, +} from "redux-saga/effects"; +import { ErrorWithKey } from "src/api/statementsApi"; +import { + actions as indexStatsActions, + ResetIndexUsageStatsPayload, +} from "./indexStats.reducer"; +import { CACHE_INVALIDATION_PERIOD } from "src/store/utils"; +import { generateTableID } from "../../util"; +import { + getIndexStats, + resetIndexStats, + TableIndexStatsRequest, + TableIndexStatsResponseWithKey, +} from "../../api/indexDetailsApi"; +import { cockroach } from "@cockroachlabs/crdb-protobuf-client"; + +export function* refreshIndexStatsSaga( + action: PayloadAction, +) { + yield put(indexStatsActions.request(action?.payload)); +} + +export function* requestIndexStatsSaga( + action: PayloadAction, +): any { + const key = action?.payload + ? generateTableID(action.payload.database, action.payload.table) + : ""; + try { + const result = yield call(getIndexStats, action?.payload); + const resultWithKey: TableIndexStatsResponseWithKey = { + indexStatsResponse: result, + key, + }; + yield put(indexStatsActions.received(resultWithKey)); + } catch (e) { + const err: ErrorWithKey = { + err: e, + key, + }; + yield put(indexStatsActions.failed(err)); + } +} + +export function receivedIndexStatsSagaFactory(delayMs: number) { + return function* receivedIndexStatsSaga( + action: PayloadAction, + ) { + yield delay(delayMs); + yield put( + indexStatsActions.invalidated({ + key: action?.payload.key, + }), + ); + }; +} + +export function* resetIndexStatsSaga( + action: PayloadAction, +) { + const key = action?.payload + ? generateTableID(action.payload.database, action.payload.table) + : ""; + const resetIndexUsageStatsRequest = + new cockroach.server.serverpb.ResetIndexUsageStatsRequest(); + try { + yield call(resetIndexStats, resetIndexUsageStatsRequest); + yield put(indexStatsActions.invalidateAll()); + yield put( + indexStatsActions.refresh( + new cockroach.server.serverpb.TableIndexStatsRequest({ + ...action.payload, + }), + ), + ); + } catch (e) { + const err: ErrorWithKey = { + err: e, + key, + }; + yield put(indexStatsActions.failed(err)); + } +} + +export function* indexStatsSaga( + cacheInvalidationPeriod: number = CACHE_INVALIDATION_PERIOD, +) { + yield all([ + takeLatest(indexStatsActions.refresh, refreshIndexStatsSaga), + takeLatest(indexStatsActions.request, requestIndexStatsSaga), + takeLatest( + indexStatsActions.received, + receivedIndexStatsSagaFactory(cacheInvalidationPeriod), + ), + takeEvery(indexStatsActions.reset, resetIndexStatsSaga), + ]); +} diff --git a/pkg/ui/workspaces/cluster-ui/src/store/reducers.ts b/pkg/ui/workspaces/cluster-ui/src/store/reducers.ts index ed7750f5626c..82b97e648a83 100644 --- a/pkg/ui/workspaces/cluster-ui/src/store/reducers.ts +++ b/pkg/ui/workspaces/cluster-ui/src/store/reducers.ts @@ -29,6 +29,10 @@ import { SQLDetailsStatsReducerState, reducer as sqlDetailsStats, } from "./statementDetails"; +import { + IndexStatsReducerState, + reducer as indexStats, +} from "./indexStats/indexStats.reducer"; export type AdminUiState = { statementDiagnostics: StatementDiagnosticsState; @@ -40,6 +44,7 @@ export type AdminUiState = { uiConfig: UIConfigState; sqlStats: SQLStatsState; sqlDetailsStats: SQLDetailsStatsReducerState; + indexStats: IndexStatsReducerState; }; export type AppState = { @@ -56,6 +61,7 @@ export const reducers = combineReducers({ uiConfig, sqlStats, sqlDetailsStats, + indexStats, }); 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 623969030b0b..7c786cd3c52c 100644 --- a/pkg/ui/workspaces/cluster-ui/src/store/sagas.ts +++ b/pkg/ui/workspaces/cluster-ui/src/store/sagas.ts @@ -20,6 +20,7 @@ import { terminateSaga } from "./terminateQuery"; import { notifificationsSaga } from "./notifications"; import { sqlStatsSaga } from "./sqlStats"; import { sqlDetailsStatsSaga } from "./statementDetails"; +import { indexStatsSaga } from "./indexStats/indexStats.sagas"; export function* sagas(cacheInvalidationPeriod?: number): SagaIterator { yield all([ @@ -32,5 +33,6 @@ export function* sagas(cacheInvalidationPeriod?: number): SagaIterator { fork(notifificationsSaga), fork(sqlStatsSaga), fork(sqlDetailsStatsSaga), + fork(indexStatsSaga), ]); } diff --git a/pkg/ui/workspaces/cluster-ui/src/util/appStats/appStats.ts b/pkg/ui/workspaces/cluster-ui/src/util/appStats/appStats.ts index ef2313c384c5..e047e04f4474 100644 --- a/pkg/ui/workspaces/cluster-ui/src/util/appStats/appStats.ts +++ b/pkg/ui/workspaces/cluster-ui/src/util/appStats/appStats.ts @@ -316,3 +316,7 @@ export const generateStmtDetailsToID = ( } return generatedID; }; + +export const generateTableID = (db: string, table: string): string => { + return `${encodeURIComponent(db)}/${encodeURIComponent(table)}`; +}; diff --git a/pkg/ui/workspaces/cluster-ui/src/util/constants.ts b/pkg/ui/workspaces/cluster-ui/src/util/constants.ts index 2d3d99897316..f9a891317244 100644 --- a/pkg/ui/workspaces/cluster-ui/src/util/constants.ts +++ b/pkg/ui/workspaces/cluster-ui/src/util/constants.ts @@ -25,7 +25,9 @@ export const rangeIDAttr = "range_id"; export const statementAttr = "statement"; export const sessionAttr = "session"; export const tabAttr = "tab"; +export const schemaNameAttr = "schemaName"; export const tableNameAttr = "table_name"; +export const indexNameAttr = "index_name"; export const txnFingerprintIdAttr = "txn_fingerprint_id"; export const viewAttr = "view"; diff --git a/pkg/ui/workspaces/db-console/src/views/databases/indexDetailsPage/redux.spec.ts b/pkg/ui/workspaces/db-console/src/views/databases/indexDetailsPage/redux.spec.ts index 95fae26e525f..0cd743b3a5ce 100644 --- a/pkg/ui/workspaces/db-console/src/views/databases/indexDetailsPage/redux.spec.ts +++ b/pkg/ui/workspaces/db-console/src/views/databases/indexDetailsPage/redux.spec.ts @@ -144,6 +144,7 @@ describe("Index Details Page", function () { lastReset: moment(), indexRecommendations: [], }, + breadcrumbItems: null, }, false, ); @@ -196,6 +197,7 @@ describe("Index Details Page", function () { ), indexRecommendations: [], }, + breadcrumbItems: null, }); }); }); diff --git a/pkg/ui/workspaces/db-console/src/views/databases/indexDetailsPage/redux.ts b/pkg/ui/workspaces/db-console/src/views/databases/indexDetailsPage/redux.ts index 1fb21c5331d8..ed5f93e5c11c 100644 --- a/pkg/ui/workspaces/db-console/src/views/databases/indexDetailsPage/redux.ts +++ b/pkg/ui/workspaces/db-console/src/views/databases/indexDetailsPage/redux.ts @@ -67,6 +67,7 @@ export const mapStateToProps = createSelector( lastReset: util.TimestampToMoment(stats?.data?.last_reset), indexRecommendations, }, + breadcrumbItems: null, }; }, );