Skip to content

Commit

Permalink
Merge #104180
Browse files Browse the repository at this point in the history
104180: cluster-ui: database table page connected component with reusable combiner logic r=THardy98 a=THardy98

Resolves: #97885 
Related to: #103979

Consumed by: https://github.com/cockroachlabs/managed-service/pull/13237

** DEMOS **
DB: https://www.loom.com/share/9138512826bf45729c6e698fb492c6eb

This PR adds a connected component for the database table page on
cluster-ui to be used on CC console. Similar to the PR for the connected
databases page, we move some common selector/combiner logic between the
two connected components (for db/cc console) into cluster-ui for reuse.

Release note: None

Co-authored-by: Thomas Hardy <[email protected]>
  • Loading branch information
craig[bot] and Thomas Hardy committed Jun 14, 2023
2 parents 0512f18 + 66c0e53 commit 0caad15
Show file tree
Hide file tree
Showing 17 changed files with 870 additions and 584 deletions.
420 changes: 159 additions & 261 deletions pkg/ui/workspaces/cluster-ui/src/databaseTablePage/databaseTablePage.tsx

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
// Copyright 2023 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 { RouteComponentProps } from "react-router";
import { AppState, uiConfigActions } from "src/store";
import {
databaseNameCCAttr,
generateTableID,
getMatchParamByName,
minDate,
schemaNameAttr,
tableNameCCAttr,
TimestampToMoment,
} from "src/util";
import {
actions as nodesActions,
nodeRegionsByIDSelector,
} from "src/store/nodes";
import {
selectAutomaticStatsCollectionEnabled,
selectIndexRecommendationsEnabled,
selectIndexUsageStatsEnabled,
} from "src/store/clusterSettings/clusterSettings.selectors";
import { selectHasAdminRole, selectIsTenant } from "src/store/uiConfig";
import {
DatabaseTablePageActions,
DatabaseTablePageData,
DatabaseTablePage,
} from "./databaseTablePage";
import { cockroach } from "@cockroachlabs/crdb-protobuf-client";
import { Dispatch } from "redux";
import { actions as tableDetailsActions } from "src/store/databaseTableDetails";
import { actions as indexStatsActions } from "src/store/indexStats";
import { actions as analyticsActions } from "src/store/analytics";
import { actions as clusterSettingsActions } from "src/store/clusterSettings";
import {
deriveIndexDetailsMemoized,
deriveTablePageDetailsMemoized,
} from "../databases";
import { withRouter } from "react-router-dom";
import { connect } from "react-redux";

export const mapStateToProps = (
state: AppState,
props: RouteComponentProps,
): DatabaseTablePageData => {
const database = getMatchParamByName(props.match, databaseNameCCAttr);
const table = getMatchParamByName(props.match, tableNameCCAttr);
const schema = getMatchParamByName(props.match, schemaNameAttr);
const tableDetails = state.adminUI?.tableDetails;
const indexUsageStats = state.adminUI?.indexStats;
const details = tableDetails[generateTableID(database, table)];
const indexStatsState = indexUsageStats[generateTableID(database, table)];
const lastReset = TimestampToMoment(
indexStatsState?.data?.last_reset,
minDate,
);
const nodeRegions = nodeRegionsByIDSelector(state);
const isTenant = selectIsTenant(state);

return {
databaseName: database,
name: table,
schemaName: schema,
details: deriveTablePageDetailsMemoized({ details, nodeRegions, isTenant }),
showNodeRegionsSection: Object.keys(nodeRegions).length > 1 && !isTenant,
automaticStatsCollectionEnabled:
selectAutomaticStatsCollectionEnabled(state),
indexUsageStatsEnabled: selectIndexUsageStatsEnabled(state),
showIndexRecommendations: selectIndexRecommendationsEnabled(state),
hasAdminRole: selectHasAdminRole(state),
indexStats: {
loading: !!indexStatsState?.inFlight,
loaded: !!indexStatsState?.valid,
lastError: indexStatsState?.lastError,
stats: deriveIndexDetailsMemoized({ database, table, indexUsageStats }),
lastReset,
},
};
};

export const mapDispatchToProps = (
dispatch: Dispatch,
): DatabaseTablePageActions => ({
refreshTableDetails: (database: string, table: string) => {
dispatch(tableDetailsActions.refresh({ database, table }));
},
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,
}),
);
dispatch(
analyticsActions.track({
name: "Reset Index Usage",
page: "Index Details",
}),
);
},
refreshNodes: () => {
dispatch(nodesActions.refresh());
},
refreshSettings: () => {
dispatch(clusterSettingsActions.refresh());
},
refreshUserSQLRoles: () => dispatch(uiConfigActions.refreshUserSQLRoles()),
});

export const ConnectedDatabaseTablePage = withRouter(
connect<DatabaseTablePageData, DatabaseTablePageActions, RouteComponentProps>(
mapStateToProps,
mapDispatchToProps,
)(DatabaseTablePage),
);
Original file line number Diff line number Diff line change
@@ -0,0 +1,222 @@
// Copyright 2023 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 {
DATE_FORMAT,
DATE_FORMAT_24_TZ,
EncodeDatabaseTableUri,
EncodeDatabaseUri,
EncodeUriName,
minDate,
performanceTuningRecipes,
} from "../util";
import { Timestamp } from "../timestamp";
import React, { useContext } from "react";
import { Moment } from "moment-timezone";
import { DatabaseTablePageDataDetails, IndexStat } from "./databaseTablePage";
import { CircleFilled } from "../icon";
import { Tooltip } from "antd";
import "antd/lib/tooltip/style";
import { Anchor } from "../anchor";
import classNames from "classnames/bind";
import styles from "./databaseTablePage.module.scss";
import { QuoteIdentifier } from "../api/safesql";
import IdxRecAction from "../insights/indexActionBtn";
import * as format from "../util/format";
import { Link } from "react-router-dom";
import { Search as IndexIcon } from "@cockroachlabs/icons";
import { Breadcrumbs } from "../breadcrumbs";
import { CaretRight } from "../icon/caretRight";
import { CockroachCloudContext } from "../contexts";
const cx = classNames.bind(styles);

export const NameCell = ({
indexStat,
showIndexRecommendations,
tableName,
}: {
indexStat: IndexStat;
showIndexRecommendations: boolean;
tableName: string;
}): JSX.Element => {
const isCockroachCloud = useContext(CockroachCloudContext);
if (showIndexRecommendations) {
const linkURL = isCockroachCloud
? `${location.pathname}/${indexStat.indexName}`
: `${tableName}/index/${EncodeUriName(indexStat.indexName)}`;
return (
<Link to={linkURL} className={cx("icon__container")}>
<IndexIcon className={cx("icon--s", "icon--primary")} />
{indexStat.indexName}
</Link>
);
}
return (
<>
<IndexIcon className={cx("icon--s", "icon--primary")} />
{indexStat.indexName}
</>
);
};

export const DbTablesBreadcrumbs = ({
tableName,
schemaName,
databaseName,
}: {
tableName: string;
schemaName: string;
databaseName: string;
}): JSX.Element => {
const isCockroachCloud = useContext(CockroachCloudContext);
return (
<Breadcrumbs
items={[
{ link: "/databases", name: "Databases" },
{
link: isCockroachCloud
? `/databases/${EncodeUriName(databaseName)}`
: EncodeDatabaseUri(databaseName),
name: "Tables",
},
{
link: isCockroachCloud
? `/databases/${EncodeUriName(databaseName)}/${EncodeUriName(
schemaName,
)}/${EncodeUriName(tableName)}`
: EncodeDatabaseTableUri(databaseName, tableName),
name: `Table: ${tableName}`,
},
]}
divider={<CaretRight className={cx("icon--xxs", "icon--primary")} />}
/>
);
};

export const LastReset = ({
lastReset,
}: {
lastReset: Moment;
}): JSX.Element => {
return (
<span>
Last reset:{" "}
{lastReset.isSame(minDate) ? (
"Never"
) : (
<Timestamp time={lastReset} format={DATE_FORMAT_24_TZ} />
)}
</span>
);
};

interface IndexStatProps {
indexStat: IndexStat;
}

export const LastUsed = ({ indexStat }: IndexStatProps): JSX.Element => {
// This case only occurs when we have no reads, resets, or creation time on
// the index.
if (indexStat.lastUsed.isSame(minDate)) {
return <>Never</>;
}
return (
<>
Last {indexStat.lastUsedType}:{" "}
<Timestamp time={indexStat.lastUsed} format={DATE_FORMAT} />
</>
);
};

export const IndexRecCell = ({ indexStat }: IndexStatProps): JSX.Element => {
const classname =
indexStat.indexRecommendations.length > 0
? "index-recommendations-icon__exist"
: "index-recommendations-icon__none";

if (indexStat.indexRecommendations.length === 0) {
return (
<div>
<CircleFilled className={cx(classname)} />
<span>None</span>
</div>
);
}
// Render only the first recommendation for an index.
const recommendation = indexStat.indexRecommendations[0];
let text: string;
switch (recommendation.type) {
case "DROP_UNUSED":
text = "Drop unused index";
}
return (
<Tooltip
placement="bottom"
title={
<div className={cx("index-recommendations-text__tooltip-anchor")}>
{recommendation.reason}{" "}
<Anchor href={performanceTuningRecipes} target="_blank">
Learn more
</Anchor>
</div>
}
>
<CircleFilled className={cx(classname)} />
<span className={cx("index-recommendations-text__border")}>{text}</span>
</Tooltip>
);
};

export const ActionCell = ({
indexStat,
tableName,
databaseName,
}: {
indexStat: IndexStat;
tableName: string;
databaseName: string;
}): JSX.Element => {
const query = indexStat.indexRecommendations.map(recommendation => {
switch (recommendation.type) {
case "DROP_UNUSED":
return `DROP INDEX ${QuoteIdentifier(tableName)}@${QuoteIdentifier(
indexStat.indexName,
)};`;
}
});
if (query.length === 0) {
return <></>;
}

return (
<IdxRecAction
actionQuery={query.join(" ")}
actionType={"DropIndex"}
database={databaseName}
/>
);
};

export const FormatMVCCInfo = ({
details,
}: {
details: DatabaseTablePageDataDetails;
}): JSX.Element => {
return (
<>
{format.Percentage(details.livePercentage, 1, 1)}
{" ("}
<span className={cx("bold")}>{format.Bytes(details.liveBytes)}</span> live
data /{" "}
<span className={cx("bold")}>{format.Bytes(details.totalBytes)}</span>
{" total)"}
</>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,4 @@
// licenses/APL.txt.

export * from "./databaseTablePage";
export * from "./databaseTablePageConnected";
Loading

0 comments on commit 0caad15

Please sign in to comment.