-
Notifications
You must be signed in to change notification settings - Fork 3.9k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
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
Showing
17 changed files
with
870 additions
and
584 deletions.
There are no files selected for viewing
420 changes: 159 additions & 261 deletions
420
pkg/ui/workspaces/cluster-ui/src/databaseTablePage/databaseTablePage.tsx
Large diffs are not rendered by default.
Oops, something went wrong.
133 changes: 133 additions & 0 deletions
133
pkg/ui/workspaces/cluster-ui/src/databaseTablePage/databaseTablePageConnected.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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), | ||
); |
222 changes: 222 additions & 0 deletions
222
pkg/ui/workspaces/cluster-ui/src/databaseTablePage/helperComponents.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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)"} | ||
</> | ||
); | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -9,3 +9,4 @@ | |
// licenses/APL.txt. | ||
|
||
export * from "./databaseTablePage"; | ||
export * from "./databaseTablePageConnected"; |
Oops, something went wrong.