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 21, 2022
1 parent 4ee236c commit fa2213a
Show file tree
Hide file tree
Showing 17 changed files with 742 additions and 27 deletions.
52 changes: 52 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,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<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,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}`,
},
];
}
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),
);
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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: () => {},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -54,6 +54,7 @@ export interface IndexDetailsPageData {
tableName: string;
indexName: string;
details: IndexDetails;
breadcrumbItems: BreadcrumbItem[];
}

interface IndexDetails {
Expand Down Expand Up @@ -131,7 +132,11 @@ export class IndexDetailsPage extends React.Component<
indexRecommendations: IndexRecommendation[],
) {
if (indexRecommendations.length === 0) {
return "None";
return (
<tr>
<td>None</td>
</tr>
);
}
return indexRecommendations.map(recommendation => {
let recommendationType: string;
Expand All @@ -145,12 +150,11 @@ export class IndexDetailsPage extends React.Component<
return (
<tr
key={recommendationType}
className={cx("summary-card--row", "table__row")}
className={cx("index-recommendations-rows")}
>
<td
className={cx(
"table__cell",
"summary-card--label",
"index-recommendations-rows__header",
"icon__container",
)}
>
Expand All @@ -159,7 +163,7 @@ export class IndexDetailsPage extends React.Component<
</td>
<td
className={cx(
"summary-card--value",
"index-recommendations-rows__content",
"index-recommendations__tooltip-anchor",
)}
>
Expand All @@ -174,31 +178,44 @@ export class IndexDetailsPage extends React.Component<
});
}

private renderBreadcrumbs() {
if (this.props.breadcrumbItems) {
return (
<Breadcrumbs
items={this.props.breadcrumbItems}
divider={<CaretRight className={cx("icon--xxs", "icon--primary")} />}
/>
);
}
// If no props are passed, render db-console breadcrumb links by default.
return (
<Breadcrumbs
items={[
{ link: "/databases", name: "Databases" },
{
link: `/database/${this.props.databaseName}`,
name: "Tables",
},
{
link: `/database/${this.props.databaseName}/table/${this.props.tableName}`,
name: `Table: ${this.props.tableName}`,
},
{
link: `/database/${this.props.databaseName}/table/${this.props.tableName}/index/${this.props.indexName}`,
name: `Index: ${this.props.indexName}`,
},
]}
divider={<CaretRight className={cx("icon--xxs", "icon--primary")} />}
/>
);
}

render() {
return (
<div className={cx("page-container")}>
<div className="root table-area">
<section className={baseHeadingClasses.wrapper}>
<Breadcrumbs
items={[
{ link: "/databases", name: "Databases" },
{
link: `/database/${this.props.databaseName}`,
name: "Tables",
},
{
link: `/database/${this.props.databaseName}/table/${this.props.tableName}`,
name: `Table: ${this.props.tableName}`,
},
{
link: `/database/${this.props.databaseName}/table/${this.props.tableName}/index/${this.props.indexName}`,
name: `Index: ${this.props.indexName}`,
},
]}
divider={
<CaretRight className={cx("icon--xxs", "icon--primary")} />
}
/>
{this.renderBreadcrumbs()}
</section>
<div className={cx("header-container")}>
<h3
Expand Down Expand Up @@ -290,7 +307,7 @@ export class IndexDetailsPage extends React.Component<
<Col className="gutter-row" span={18}>
<SummaryCard className={cx("summary-card--row")}>
<Heading type="h5">Index recommendations</Heading>
<table className="table">
<table>
<tbody>
{this.renderIndexRecommendations(
this.props.details.indexRecommendations,
Expand Down
Loading

0 comments on commit fa2213a

Please sign in to comment.