Skip to content

Commit

Permalink
cluster-ui: created a "connected" component for index details page
Browse files Browse the repository at this point in the history
This PR introduces an "indexDetailsConnected" component on cluster-ui.
This allows us to reuse the cluster-ui redux store without replicating
the same logic on CC console.

Release note: None
  • Loading branch information
Thomas Hardy committed Jun 7, 2022
1 parent 1254e16 commit 7ceb336
Show file tree
Hide file tree
Showing 12 changed files with 616 additions and 0 deletions.
48 changes: 48 additions & 0 deletions pkg/ui/workspaces/cluster-ui/src/api/indexDetailsApi.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
// 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<TableIndexStatsResponse> => {
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<ResetIndexUsageStatsResponse> => {
return fetchData(
cockroach.server.serverpb.ResetIndexUsageStatsResponse,
"/_status/resetindexusagestats",
null,
req,
"30M",
);
};
1 change: 1 addition & 0 deletions pkg/ui/workspaces/cluster-ui/src/indexDetailsPage/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,4 @@
// licenses/APL.txt.

export * from "./indexDetailsPage";
export * from "./indexDetailsConnected";
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
// 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,
tableNameAttr,
TimestampToMoment,
} from "../util";
import { cockroach } from "@cockroachlabs/crdb-protobuf-client";
import { IndexDetailsPageData } from "./indexDetailsPage";
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, tableNameAttr),
(_state: AppState, props: RouteComponentProps): string =>
getMatchParamByName(props.match, indexNameAttr),
(state: AppState) => state.adminUI.indexStats.cachedData,
(database, 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,
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,
},
};
},
);
Original file line number Diff line number Diff line change
@@ -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<any, any>(
connect(mapStateToProps, mapDispatchToProps)(IndexDetailsPage),
);
12 changes: 12 additions & 0 deletions pkg/ui/workspaces/cluster-ui/src/store/indexStats/index.ts
Original file line number Diff line number Diff line change
@@ -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";
Original file line number Diff line number Diff line change
@@ -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<TableIndexStatsResponseWithKey>,
) => {
state.cachedData[action.payload.key] = {
data: action.payload.indexStatsResponse,
valid: true,
lastError: null,
inFlight: false,
};
},
failed: (state, action: PayloadAction<ErrorWithKey>) => {
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<TableIndexStatsRequest>) => {
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<TableIndexStatsRequest>) => {
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<ResetIndexUsageStatsPayload>) => {
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;
Loading

0 comments on commit 7ceb336

Please sign in to comment.