From 6e973ebcd76c87d7ef3faafa14f137ec8b126bec Mon Sep 17 00:00:00 2001 From: Lindsey Jin Date: Thu, 18 Nov 2021 16:20:41 -0500 Subject: [PATCH] ui: add index stats to table details page Resolves #67647, #72842 Previously, there was no way to view and clear index usage stats from the frontend db console. This commit adds Index Stats tables for each table on the Table Detail pages, allowing users to view index names, total reads, and last used statistics. This commit also adds the functionality of clearing all index stats as a button on the Index Stats tables. Release note (ui change): Add index stats table and button to clear index usage stats on the Table Details page for each table. --- .../databaseTablePage.module.scss | 55 +++++++ .../databaseTablePage.stories.tsx | 43 ++++++ .../databaseTablePage/databaseTablePage.tsx | 143 +++++++++++++++++- .../db-console/src/redux/apiReducers.ts | 16 +- .../src/redux/indexUsageStats/index.ts | 12 ++ .../indexUsageStats/indexUsageStatsActions.ts | 50 ++++++ .../indexUsageStatsSagas.spec.ts | 50 ++++++ .../indexUsageStats/indexUsageStatsSagas.ts | 62 ++++++++ .../workspaces/db-console/src/redux/sagas.ts | 2 + pkg/ui/workspaces/db-console/src/util/api.ts | 31 ++++ .../databases/databaseTablePage/redux.spec.ts | 18 +++ .../databases/databaseTablePage/redux.ts | 69 ++++++++- 12 files changed, 544 insertions(+), 7 deletions(-) create mode 100644 pkg/ui/workspaces/db-console/src/redux/indexUsageStats/index.ts create mode 100644 pkg/ui/workspaces/db-console/src/redux/indexUsageStats/indexUsageStatsActions.ts create mode 100644 pkg/ui/workspaces/db-console/src/redux/indexUsageStats/indexUsageStatsSagas.spec.ts create mode 100644 pkg/ui/workspaces/db-console/src/redux/indexUsageStats/indexUsageStatsSagas.ts diff --git a/pkg/ui/workspaces/cluster-ui/src/databaseTablePage/databaseTablePage.module.scss b/pkg/ui/workspaces/cluster-ui/src/databaseTablePage/databaseTablePage.module.scss index e63861b7bfde..4e6b0299c330 100644 --- a/pkg/ui/workspaces/cluster-ui/src/databaseTablePage/databaseTablePage.module.scss +++ b/pkg/ui/workspaces/cluster-ui/src/databaseTablePage/databaseTablePage.module.scss @@ -54,3 +54,58 @@ fill: $colors--primary-text; } } + +.index-stats { + &__summary-card { + width: fit-content; + padding: 0; + } + + &__header { + align-items: baseline; + display: flex; + flex-direction: row; + justify-content: space-between; + padding: 24px 24px 0; + } + + &__clear-info { + display: flex; + flex-direction: row; + } + + &__last-cleared { + color: "#475872"; + margin-right: 6px; + } + + &__clear-btn { + border-bottom: none; + text-decoration: none; + } + + &-table { + &__col { + &-indexes { + width: 30em; + } + &-last-used { + width: 30em; + } + } + } +} + + +.icon { + &--s { + height: 16px; + width: 16px; + margin-right: 10px; + } + + &--primary { + fill: $colors--primary-text; + } +} + diff --git a/pkg/ui/workspaces/cluster-ui/src/databaseTablePage/databaseTablePage.stories.tsx b/pkg/ui/workspaces/cluster-ui/src/databaseTablePage/databaseTablePage.stories.tsx index a57643382927..59de75107ad1 100644 --- a/pkg/ui/workspaces/cluster-ui/src/databaseTablePage/databaseTablePage.stories.tsx +++ b/pkg/ui/workspaces/cluster-ui/src/databaseTablePage/databaseTablePage.stories.tsx @@ -19,6 +19,7 @@ import { randomTablePrivilege, } from "src/storybook/fixtures"; import { DatabaseTablePage, DatabaseTablePageProps } from "./databaseTablePage"; +import moment from "moment"; const withLoadingIndicator: DatabaseTablePageProps = { databaseName: randomName(), @@ -37,8 +38,23 @@ const withLoadingIndicator: DatabaseTablePageProps = { sizeInBytes: 0, rangeCount: 0, }, + indexStats: { + loading: true, + loaded: false, + stats: [ + { + totalReads: 0, + lastUsedTime: moment("2021-11-10T16:29:00Z"), + lastUsedString: "Nov 10, 2021 at 4:29 PM", + indexName: "primary", + }, + ], + lastReset: "Nov 12, 2021 at 8:14 PM (UTC)", + }, refreshTableDetails: () => {}, refreshTableStats: () => {}, + refreshIndexStats: () => {}, + resetIndexUsageStats: () => {}, }; const name = randomName(); @@ -80,8 +96,35 @@ const withData: DatabaseTablePageProps = { nodesByRegionString: "gcp-europe-west1(n8), gcp-us-east1(n1), gcp-us-west1(n6)", }, + indexStats: { + loading: false, + loaded: false, + stats: [ + { + totalReads: 0, + lastUsedTime: moment("2021-01-11T11:29:00Z"), + lastUsedString: "Jan 11, 2021 at 11:29 AM", + indexName: "primary", + }, + { + totalReads: 3, + lastUsedTime: moment("2021-11-10T16:29:00Z"), + lastUsedString: "Nov 10, 2021 at 4:29 PM", + indexName: "primary", + }, + { + totalReads: 2, + lastUsedTime: moment("2021-09-04T13:55:00Z"), + lastUsedString: "Sep 04, 2021 at 12:55 PM", + indexName: "secondary", + }, + ], + lastReset: "Oct 22, 2021 at 9:21 AM (UTC)", + }, refreshTableDetails: () => {}, refreshTableStats: () => {}, + refreshIndexStats: () => {}, + resetIndexUsageStats: () => {}, }; storiesOf("Database Table Page", module) diff --git a/pkg/ui/workspaces/cluster-ui/src/databaseTablePage/databaseTablePage.tsx b/pkg/ui/workspaces/cluster-ui/src/databaseTablePage/databaseTablePage.tsx index db0d80988f9f..047830d447e2 100644 --- a/pkg/ui/workspaces/cluster-ui/src/databaseTablePage/databaseTablePage.tsx +++ b/pkg/ui/workspaces/cluster-ui/src/databaseTablePage/databaseTablePage.tsx @@ -13,6 +13,7 @@ import { Col, Row, Tabs } from "antd"; import classNames from "classnames/bind"; import _ from "lodash"; import { Tooltip } from "antd"; +import { Heading } from "@cockroachlabs/ui-components"; import { Breadcrumbs } from "src/breadcrumbs"; import { CaretRight } from "src/icon/caretRight"; @@ -25,6 +26,8 @@ import * as format from "src/util/format"; import styles from "./databaseTablePage.module.scss"; import { commonStyles } from "src/common"; import { baseHeadingClasses } from "src/transactionsPage/transactionsPageClasses"; +import { Moment } from "moment"; +import { Search as IndexIcon } from "@cockroachlabs/icons"; const cx = classNames.bind(styles); const { TabPane } = Tabs; @@ -58,12 +61,24 @@ const { TabPane } = Tabs; // rangeCount: number; // nodesByRegionString: string; // }; +// indexUsageStats: { // DatabaseTablePageIndexStats +// loading: boolean; +// loaded: boolean; +// stats: { +// indexName: string; +// totalReads: number; +// lastUsedTime: Moment; +// lastUsedString: string; +// }[]; +// lastReset: string; +// }; // } export interface DatabaseTablePageData { databaseName: string; name: string; details: DatabaseTablePageDataDetails; stats: DatabaseTablePageDataStats; + indexStats: DatabaseTablePageIndexStats; showNodeRegionsSection?: boolean; } @@ -76,6 +91,20 @@ export interface DatabaseTablePageDataDetails { grants: Grant[]; } +export interface DatabaseTablePageIndexStats { + loading: boolean; + loaded: boolean; + stats: IndexStat[]; + lastReset: string; +} + +interface IndexStat { + indexName: string; + totalReads: number; + lastUsedTime: Moment; + lastUsedString: string; +} + interface Grant { user: string; privilege: string; @@ -91,7 +120,9 @@ export interface DatabaseTablePageDataStats { export interface DatabaseTablePageActions { refreshTableDetails: (database: string, table: string) => void; - refreshTableStats: (databse: string, table: string) => void; + refreshTableStats: (database: string, table: string) => void; + refreshIndexStats?: (database: string, table: string) => void; + resetIndexUsageStats?: (database: string, table: string) => void; refreshNodes?: () => void; } @@ -103,6 +134,7 @@ interface DatabaseTablePageState { } class DatabaseTableGrantsTable extends SortedTable {} +class IndexUsageStatsTable extends SortedTable {} export class DatabaseTablePage extends React.Component< DatabaseTablePageProps, @@ -143,13 +175,67 @@ export class DatabaseTablePage extends React.Component< this.props.name, ); } + + if (!this.props.indexStats.loaded && !this.props.indexStats.loading) { + return this.props.refreshIndexStats( + this.props.databaseName, + this.props.name, + ); + } } private changeSortSetting(sortSetting: SortSetting) { this.setState({ sortSetting }); } - private columns: ColumnDescriptor[] = [ + private indexStatsColumns: ColumnDescriptor[] = [ + { + name: "indexes", + title: ( + + Indexes + + ), + className: cx("index-stats-table__col-indexes"), + cell: indexStat => ( + <> + + {indexStat.indexName} + + ), + sort: indexStat => indexStat.indexName, + }, + { + name: "total reads", + title: ( + + Total Reads + + ), + cell: indexStat => indexStat.totalReads, + sort: indexStat => indexStat.totalReads, + }, + { + name: "last used", + title: ( + + Last Used (UTC) + + ), + className: cx("index-stats-table__col-last-used"), + cell: indexStat => indexStat.lastUsedString, + sort: indexStat => indexStat.lastUsedTime, + }, + // TODO(lindseyjin): add index recommendations column + ]; + + private grantsColumns: ColumnDescriptor[] = [ { name: "user", title: ( @@ -247,12 +333,61 @@ export class DatabaseTablePage extends React.Component< + + +
+ Index Stats + +
+ +
+
- generateTableID(req.database, req.table); const tableDetailsReducerObj = new KeyedCachedDataReducer( @@ -125,6 +128,15 @@ const tableStatsReducerObj = new KeyedCachedDataReducer( ); export const refreshTableStats = tableStatsReducerObj.refresh; +const indexStatsReducerObj = new KeyedCachedDataReducer( + api.getIndexStats, + "indexStats", + tableRequestToID, +); + +export const invalidateIndexStats = indexStatsReducerObj.cachedDataReducer.invalidateData; +export const refreshIndexStats = indexStatsReducerObj.refresh; + const nonTableStatsReducerObj = new CachedDataReducer( api.getNonTableStats, "nonTableStats", @@ -325,6 +337,7 @@ export interface APIReducersState { >; tableDetails: KeyedCachedDataReducerState; tableStats: KeyedCachedDataReducerState; + indexStats: KeyedCachedDataReducerState; nonTableStats: CachedDataReducerState; logs: CachedDataReducerState; liveness: CachedDataReducerState; @@ -362,6 +375,7 @@ export const apiReducersReducer = combineReducers({ databaseDetailsReducerObj.reducer, [tableDetailsReducerObj.actionNamespace]: tableDetailsReducerObj.reducer, [tableStatsReducerObj.actionNamespace]: tableStatsReducerObj.reducer, + [indexStatsReducerObj.actionNamespace]: indexStatsReducerObj.reducer, [nonTableStatsReducerObj.actionNamespace]: nonTableStatsReducerObj.reducer, [logsReducerObj.actionNamespace]: logsReducerObj.reducer, [livenessReducerObj.actionNamespace]: livenessReducerObj.reducer, diff --git a/pkg/ui/workspaces/db-console/src/redux/indexUsageStats/index.ts b/pkg/ui/workspaces/db-console/src/redux/indexUsageStats/index.ts new file mode 100644 index 000000000000..17406d764d45 --- /dev/null +++ b/pkg/ui/workspaces/db-console/src/redux/indexUsageStats/index.ts @@ -0,0 +1,12 @@ +// 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 "./indexUsageStatsActions"; +export * from "./indexUsageStatsSagas"; diff --git a/pkg/ui/workspaces/db-console/src/redux/indexUsageStats/indexUsageStatsActions.ts b/pkg/ui/workspaces/db-console/src/redux/indexUsageStats/indexUsageStatsActions.ts new file mode 100644 index 000000000000..76a99ef1016c --- /dev/null +++ b/pkg/ui/workspaces/db-console/src/redux/indexUsageStats/indexUsageStatsActions.ts @@ -0,0 +1,50 @@ +// 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 { Action } from "redux"; +import {PayloadAction} from "oss/src/interfaces/action"; + +export const RESET_INDEX_USAGE_STATS = + "cockroachui/indexUsageStats/RESET_INDEX_USAGE_STATS"; +export const RESET_INDEX_USAGE_STATS_COMPLETE = + "cockroachui/indexUsageStats/RESET_INDEX_USAGE_STATS_COMPLETE"; + "cockroachui/indexUsageStats/RESET_INDEX_USAGE_STATS_COMPLETE"; +export const RESET_INDEX_USAGE_STATS_FAILED = + "cockroachui/indexUsageStats/RESET_INDEX_USAGE_STATS_FAILED"; + +export type resetIndexUsageStatsPayload = { + database: string; + table: string; +}; + +export function resetIndexUsageStatsAction( + database: string, + table: string +): PayloadAction { + return { + type: RESET_INDEX_USAGE_STATS, + payload: { + database, + table, + }, + }; +} + +export function resetIndexUsageStatsCompleteAction(): Action { + return { + type: RESET_INDEX_USAGE_STATS_COMPLETE, + }; +} + +export function resetIndexUsageStatsFailedAction(): Action { + return { + type: RESET_INDEX_USAGE_STATS_FAILED, + }; +} diff --git a/pkg/ui/workspaces/db-console/src/redux/indexUsageStats/indexUsageStatsSagas.spec.ts b/pkg/ui/workspaces/db-console/src/redux/indexUsageStats/indexUsageStatsSagas.spec.ts new file mode 100644 index 000000000000..9d24d61e3038 --- /dev/null +++ b/pkg/ui/workspaces/db-console/src/redux/indexUsageStats/indexUsageStatsSagas.spec.ts @@ -0,0 +1,50 @@ +// 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 { call } from "redux-saga-test-plan/matchers"; + +import { + resetIndexUsageStatsFailedAction, + resetIndexUsageStatsCompleteAction, + resetIndexUsageStatsAction, +} from "./indexUsageStatsActions"; +import { resetIndexUsageStatsSaga } from "./indexUsageStatsSagas"; +import { resetIndexUsageStats } from "src/util/api"; +import { refreshIndexStats } from "src/redux/apiReducers"; +import { throwError } from "redux-saga-test-plan/providers"; + +import { cockroach } from "src/js/protos"; + + +describe("Index Usage Stats sagas", () => { + describe("resetIndexUsageStatsSaga", () => { + const resetIndexUsageStatsResponse = new cockroach.server.serverpb.ResetIndexUsageStatsResponse(); + const action = resetIndexUsageStatsAction("database", "table"); + + it("successfully resets index usage stats", () => { + expectSaga(resetIndexUsageStatsSaga, action) + .provide([ + [call.fn(resetIndexUsageStats), resetIndexUsageStatsResponse], + ]) + .put(resetIndexUsageStatsCompleteAction()) + .put(refreshIndexStats() as any) + .run(); + }); + + it("returns error on failed reset", () => { + const err = new Error("failed to reset"); + expectSaga(resetIndexUsageStatsSaga, action) + .provide([[call.fn(resetIndexUsageStats), throwError(err)]]) + .put(resetIndexUsageStatsFailedAction()) + .run(); + }); + }); +}); diff --git a/pkg/ui/workspaces/db-console/src/redux/indexUsageStats/indexUsageStatsSagas.ts b/pkg/ui/workspaces/db-console/src/redux/indexUsageStats/indexUsageStatsSagas.ts new file mode 100644 index 000000000000..ea3943d6f8a8 --- /dev/null +++ b/pkg/ui/workspaces/db-console/src/redux/indexUsageStats/indexUsageStatsSagas.ts @@ -0,0 +1,62 @@ +// 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 { cockroach } from "src/js/protos"; +import { all, call, put, takeEvery, select } from "redux-saga/effects"; +import { + RESET_INDEX_USAGE_STATS, + resetIndexUsageStatsCompleteAction, + resetIndexUsageStatsFailedAction, resetIndexUsageStatsPayload, +} from "./indexUsageStatsActions"; + +import ResetIndexUsageStatsRequest = cockroach.server.serverpb.ResetIndexUsageStatsRequest; +import {invalidateIndexStats, KeyedCachedDataReducerState, refreshIndexStats} from "oss/src/redux/apiReducers"; +import {IndexStatsResponseMessage, resetIndexUsageStats} from "oss/src/util/api"; +import {createSelector} from "reselect"; +import {AdminUIState} from "oss/src/redux/state"; +import TableIndexStatsRequest = cockroach.server.serverpb.TableIndexStatsRequest; +import {PayloadAction} from "oss/src/interfaces/action"; + +export const selectIndexStatsKeys = createSelector( + (state: AdminUIState) => state.cachedData.indexStats, + (indexUsageStats: KeyedCachedDataReducerState) => + Object.keys(indexUsageStats)) + +export const KeyToTableRequest = (key: string): TableIndexStatsRequest => { + const s = key.split("/"); + const database = s[0]; + const table = s[1]; + return new TableIndexStatsRequest({ database, table }); +} +export function* resetIndexUsageStatsSaga( + action: PayloadAction, +) { + const resetIndexUsageStatsRequest = new ResetIndexUsageStatsRequest(); + const { database, table } = action.payload; + try { + yield call(resetIndexUsageStats, resetIndexUsageStatsRequest); + yield put(resetIndexUsageStatsCompleteAction()); + + // invalidate all index stats in cache. + let keys: string[]; + keys = yield select(selectIndexStatsKeys); + yield keys.forEach((key) => put(invalidateIndexStats(KeyToTableRequest(key)))); + + // refresh index stats for table page that user is on. + yield put(refreshIndexStats(new TableIndexStatsRequest({ database, table })) as any); + } catch (e) { + console.log(e); + yield put(resetIndexUsageStatsFailedAction()); + } +} + +export function* indexUsageStatsSaga() { + yield all([takeEvery(RESET_INDEX_USAGE_STATS, resetIndexUsageStatsSaga)]); +} diff --git a/pkg/ui/workspaces/db-console/src/redux/sagas.ts b/pkg/ui/workspaces/db-console/src/redux/sagas.ts index 92d61cccbcd0..69c43fe9bf0f 100644 --- a/pkg/ui/workspaces/db-console/src/redux/sagas.ts +++ b/pkg/ui/workspaces/db-console/src/redux/sagas.ts @@ -17,6 +17,7 @@ import { statementsSaga } from "./statements"; import { analyticsSaga } from "./analyticsSagas"; import { sessionsSaga } from "./sessions"; import { sqlStatsSaga } from "./sqlStats"; +import { indexUsageStatsSaga } from "./indexUsageStats"; export default function* rootSaga() { yield all([ @@ -27,5 +28,6 @@ export default function* rootSaga() { fork(analyticsSaga), fork(sessionsSaga), fork(sqlStatsSaga), + fork(indexUsageStatsSaga), ]); } diff --git a/pkg/ui/workspaces/db-console/src/util/api.ts b/pkg/ui/workspaces/db-console/src/util/api.ts index 837f20aadd3e..e1e3dc03d3b5 100644 --- a/pkg/ui/workspaces/db-console/src/util/api.ts +++ b/pkg/ui/workspaces/db-console/src/util/api.ts @@ -59,6 +59,9 @@ export type ClusterResponseMessage = protos.cockroach.server.serverpb.ClusterRes export type TableStatsRequestMessage = protos.cockroach.server.serverpb.TableStatsRequest; export type TableStatsResponseMessage = protos.cockroach.server.serverpb.TableStatsResponse; +export type IndexStatsRequestMessage = protos.cockroach.server.serverpb.TableIndexStatsRequest; +export type IndexStatsResponseMessage = protos.cockroach.server.serverpb.TableIndexStatsResponse; + export type NonTableStatsRequestMessage = protos.cockroach.server.serverpb.NonTableStatsRequest; export type NonTableStatsResponseMessage = protos.cockroach.server.serverpb.NonTableStatsResponse; @@ -136,6 +139,9 @@ export type StatementsRequestMessage = protos.cockroach.server.serverpb.Statemen export type ResetSQLStatsRequestMessage = protos.cockroach.server.serverpb.ResetSQLStatsRequest; export type ResetSQLStatsResponseMessage = protos.cockroach.server.serverpb.ResetSQLStatsResponse; +export type ResetIndexUsageStatsRequestMessage = protos.cockroach.server.serverpb.ResetIndexUsageStatsRequest; +export type ResetIndexUsageStatsResponseMessage = protos.cockroach.server.serverpb.ResetIndexUsageStatsResponse; + // API constants export const API_PREFIX = "_admin/v1"; @@ -456,6 +462,19 @@ export function getTableStats( ); } +// getIndexStats gets detailed stats about the current table's index usage statistics +export function getIndexStats( + req: IndexStatsRequestMessage, + timeout?: moment.Duration, +): Promise { + return timeoutFetch( + serverpb.TableIndexStatsResponse, + `${STATUS_PREFIX}/databases/${req.database}/tables/${req.table}/indexstats`, + null, + timeout, + ); +} + // getNonTableStats gets detailed stats about non-table data ranges on the // cluster. export function getNonTableStats( @@ -759,3 +778,15 @@ export function resetSQLStats( timeout, ); } + +export function resetIndexUsageStats( + req: ResetIndexUsageStatsRequestMessage, + timeout?: moment.Duration, +): Promise { + return timeoutFetch( + serverpb.ResetIndexUsageStatsResponse, + `${STATUS_PREFIX}/resetindexusagestats`, + req as any, + timeout, + ); +} diff --git a/pkg/ui/workspaces/db-console/src/views/databases/databaseTablePage/redux.spec.ts b/pkg/ui/workspaces/db-console/src/views/databases/databaseTablePage/redux.spec.ts index 1eb923c05675..66e0ab05f652 100644 --- a/pkg/ui/workspaces/db-console/src/views/databases/databaseTablePage/redux.spec.ts +++ b/pkg/ui/workspaces/db-console/src/views/databases/databaseTablePage/redux.spec.ts @@ -25,6 +25,7 @@ import { AdminUIState, createAdminUIStore } from "src/redux/state"; import { databaseNameAttr, tableNameAttr } from "src/util/constants"; import * as fakeApi from "src/util/fakeApi"; import { mapStateToProps, mapDispatchToProps } from "./redux"; +import moment from "moment"; function fakeRouteComponentProps( k1: string, @@ -96,6 +97,10 @@ class TestDriver { async refreshTableStats() { return this.actions.refreshTableStats(this.database, this.table); } + + async resetIndexUsageStats() { + return this.actions.resetIndexUsageStats(); + } } describe("Database Table Page", function() { @@ -133,6 +138,19 @@ describe("Database Table Page", function() { rangeCount: 0, nodesByRegionString: "", }, + indexStats: { + loading: false, + loaded: false, + stats: [ + { + totalReads: 0, + lastUsedTime: moment("2021-11-10T16:29:00Z"), + lastUsedString: "Nov 10, 2021 at 4:29 PM", + indexName: "primary", + }, + ], + lastReset: "Nov 12, 2021 at 8:14 PM (UTC)", + }, }); }); diff --git a/pkg/ui/workspaces/db-console/src/views/databases/databaseTablePage/redux.ts b/pkg/ui/workspaces/db-console/src/views/databases/databaseTablePage/redux.ts index cc0ed1cbc913..48858229a7e1 100644 --- a/pkg/ui/workspaces/db-console/src/views/databases/databaseTablePage/redux.ts +++ b/pkg/ui/workspaces/db-console/src/views/databases/databaseTablePage/redux.ts @@ -19,18 +19,27 @@ import { refreshTableDetails, refreshTableStats, refreshNodes, + refreshIndexStats, } from "src/redux/apiReducers"; import { AdminUIState } from "src/redux/state"; import { databaseNameAttr, tableNameAttr } from "src/util/constants"; -import { FixLong } from "src/util/fixLong"; +import { FixLong, longToInt } from "src/util/fixLong"; import { getMatchParamByName } from "src/util/query"; import { nodeRegionsByIDSelector, selectIsMoreThanOneNode, } from "src/redux/nodes"; import { getNodesByRegionString } from "../utils"; +import { TimestampToMoment } from "src/util/convert"; +import { formatDate } from "antd/es/date-picker/utils"; +import { resetIndexUsageStatsAction } from "oss/src/redux/indexUsageStats"; +import moment from "moment"; -const { TableDetailsRequest, TableStatsRequest } = cockroach.server.serverpb; +const { + TableDetailsRequest, + TableStatsRequest, + TableIndexStatsRequest, +} = cockroach.server.serverpb; export const mapStateToProps = createSelector( (_state: AdminUIState, props: RouteComponentProps): string => @@ -40,6 +49,7 @@ export const mapStateToProps = createSelector( state => state.cachedData.tableDetails, state => state.cachedData.tableStats, + state => state.cachedData.indexStats, state => nodeRegionsByIDSelector(state), state => selectIsMoreThanOneNode(state), @@ -48,11 +58,54 @@ export const mapStateToProps = createSelector( table, tableDetails, tableStats, + indexUsageStats, nodeRegions, showNodeRegionsSection, ): DatabaseTablePageData => { const details = tableDetails[generateTableID(database, table)]; const stats = tableStats[generateTableID(database, table)]; + const indexStats = indexUsageStats[generateTableID(database, table)]; + + let lastResetString: string; + const minDate = moment.utc("0001-01-01"); // minimum value as per UTC + const lastReset = TimestampToMoment(indexStats?.data?.last_reset) + if (lastReset.isSame(minDate)) { + lastResetString = "never"; + } else { + lastResetString = formatDate(lastReset, "MMM DD, YYYY [at] h:mm A [(UTC)]"); + } + + const indexStatsData = _.flatMap( + indexStats?.data?.statistics, + indexStat => { + const lastRead = TimestampToMoment(indexStat.statistics?.stats?.last_read); + let lastUsed, lastUsedString; + if (lastRead.isAfter(lastReset)) { + lastUsed = lastRead; + lastUsedString = formatDate( + lastUsed, + "[Last read:] MMM DD, YYYY [at] h:mm A", + ); + } else { + // todo @lindseyjin: replace default with create time after it's added to table_indexes + lastUsed = lastReset; + if (lastReset.isSame(minDate)) { + lastUsedString = "Last reset: never" + } else { + lastUsedString = formatDate( + lastUsed, + "[Last reset:] MMM DD, YYYY [at] h:mm A", + ); + } + } + return { + indexName: indexStat.index_name, + totalReads: longToInt(indexStat.statistics?.stats?.total_read_count), + lastUsedTime: lastUsed, + lastUsedString: lastUsedString, + }; + }, + ); const grants = _.flatMap(details?.data?.grants, grant => _.map(grant.privileges, privilege => { return { user: grant.user, privilege }; @@ -81,6 +134,12 @@ export const mapStateToProps = createSelector( rangeCount: FixLong(stats?.data?.range_count || 0).toNumber(), nodesByRegionString: getNodesByRegionString(nodes, nodeRegions), }, + indexStats: { + loading: !!indexStats?.inFlight, + loaded: !!indexStats?.valid, + stats: indexStatsData, + lastReset: lastResetString, + }, }; }, ); @@ -94,5 +153,11 @@ export const mapDispatchToProps = { return refreshTableStats(new TableStatsRequest({ database, table })); }, + refreshIndexStats: (database: string, table: string) => { + return refreshIndexStats(new TableIndexStatsRequest({ database, table })); + }, + + resetIndexUsageStats: resetIndexUsageStatsAction, + refreshNodes, };