diff --git a/pkg/ui/workspaces/cluster-ui/src/databaseDetailsPage/databaseDetailsConnected.ts b/pkg/ui/workspaces/cluster-ui/src/databaseDetailsPage/databaseDetailsConnected.ts index 685ad0e29aee..39ccd0881f93 100644 --- a/pkg/ui/workspaces/cluster-ui/src/databaseDetailsPage/databaseDetailsConnected.ts +++ b/pkg/ui/workspaces/cluster-ui/src/databaseDetailsPage/databaseDetailsConnected.ts @@ -43,6 +43,7 @@ import { selectDropUnusedIndexDuration, selectIndexRecommendationsEnabled, } from "../store/clusterSettings/clusterSettings.selectors"; +import { actions as nodesActions } from "../store/nodes/nodes.reducer"; const mapStateToProps = ( state: AppState, @@ -54,6 +55,7 @@ const mapStateToProps = ( databaseDetails[database]?.data?.results.tablesResp.tables || []; const nodeRegions = nodeRegionsByIDSelector(state); const isTenant = selectIsTenant(state); + const nodeStatuses = state.adminUI?.nodes.data; return { loading: !!databaseDetails[database]?.inFlight, loaded: !!databaseDetails[database]?.valid, @@ -74,6 +76,7 @@ const mapStateToProps = ( tableDetails: state.adminUI?.tableDetails, nodeRegions, isTenant, + nodeStatuses, }), showIndexRecommendations: selectIndexRecommendationsEnabled(state), csIndexUnusedDuration: selectDropUnusedIndexDuration(state), @@ -174,6 +177,9 @@ const mapDispatchToProps = ( }), ); }, + refreshNodes: () => { + dispatch(nodesActions.refresh()); + }, }); export const ConnectedDatabaseDetailsPage = withRouter( diff --git a/pkg/ui/workspaces/cluster-ui/src/databaseDetailsPage/databaseDetailsPage.stories.tsx b/pkg/ui/workspaces/cluster-ui/src/databaseDetailsPage/databaseDetailsPage.stories.tsx index 57eafc5e5359..7797aa45eab0 100644 --- a/pkg/ui/workspaces/cluster-ui/src/databaseDetailsPage/databaseDetailsPage.stories.tsx +++ b/pkg/ui/workspaces/cluster-ui/src/databaseDetailsPage/databaseDetailsPage.stories.tsx @@ -53,6 +53,7 @@ const withLoadingIndicator: DatabaseDetailsPageProps = { onSortingGrantsChange: () => {}, refreshDatabaseDetails: () => {}, refreshTableDetails: () => {}, + refreshNodes: () => {}, search: null, filters: defaultFilters, nodeRegions: {}, @@ -88,6 +89,7 @@ const withoutData: DatabaseDetailsPageProps = { onSortingGrantsChange: () => {}, refreshDatabaseDetails: () => {}, refreshTableDetails: () => {}, + refreshNodes: () => {}, search: null, filters: defaultFilters, nodeRegions: {}, @@ -163,6 +165,7 @@ const withData: DatabaseDetailsPageProps = { onSortingGrantsChange: () => {}, refreshDatabaseDetails: () => {}, refreshTableDetails: () => {}, + refreshNodes: () => {}, search: null, filters: defaultFilters, nodeRegions: {}, diff --git a/pkg/ui/workspaces/cluster-ui/src/databaseDetailsPage/databaseDetailsPage.tsx b/pkg/ui/workspaces/cluster-ui/src/databaseDetailsPage/databaseDetailsPage.tsx index 30ee09759ec7..a9be6501975f 100644 --- a/pkg/ui/workspaces/cluster-ui/src/databaseDetailsPage/databaseDetailsPage.tsx +++ b/pkg/ui/workspaces/cluster-ui/src/databaseDetailsPage/databaseDetailsPage.tsx @@ -170,6 +170,7 @@ export interface DatabaseDetailsPageActions { onSortingTablesChange?: (columnTitle: string, ascending: boolean) => void; onSortingGrantsChange?: (columnTitle: string, ascending: boolean) => void; onViewModeChange?: (viewMode: ViewMode) => void; + refreshNodes: () => void; } export type DatabaseDetailsPageProps = DatabaseDetailsPageData & @@ -264,6 +265,7 @@ export class DatabaseDetailsPage extends React.Component< } componentDidMount(): void { + this.props.refreshNodes(); if (!this.props.loaded && !this.props.loading && !this.props.requestError) { this.props.refreshDatabaseDetails( this.props.name, diff --git a/pkg/ui/workspaces/cluster-ui/src/databaseTablePage/databaseTablePageConnected.ts b/pkg/ui/workspaces/cluster-ui/src/databaseTablePage/databaseTablePageConnected.ts index fd5fdfc1d644..ff42dda4668b 100644 --- a/pkg/ui/workspaces/cluster-ui/src/databaseTablePage/databaseTablePageConnected.ts +++ b/pkg/ui/workspaces/cluster-ui/src/databaseTablePage/databaseTablePageConnected.ts @@ -65,11 +65,17 @@ export const mapStateToProps = ( ); const nodeRegions = nodeRegionsByIDSelector(state); const isTenant = selectIsTenant(state); + const nodeStatuses = state.adminUI?.nodes.data; return { databaseName: database, name: table, schemaName: schema, - details: deriveTablePageDetailsMemoized({ details, nodeRegions, isTenant }), + details: deriveTablePageDetailsMemoized({ + details, + nodeRegions, + isTenant, + nodeStatuses, + }), showNodeRegionsSection: Object.keys(nodeRegions).length > 1 && !isTenant, automaticStatsCollectionEnabled: selectAutomaticStatsCollectionEnabled(state), diff --git a/pkg/ui/workspaces/cluster-ui/src/databases/combiners.ts b/pkg/ui/workspaces/cluster-ui/src/databases/combiners.ts index 401d4294ec46..f739875b2a9b 100644 --- a/pkg/ui/workspaces/cluster-ui/src/databases/combiners.ts +++ b/pkg/ui/workspaces/cluster-ui/src/databases/combiners.ts @@ -8,10 +8,13 @@ // by the Apache License, Version 2.0, included in the file // licenses/APL.txt. -import { DatabasesListResponse, SqlExecutionErrorMessage } from "../api"; +import { DatabasesListResponse } from "../api"; import { DatabasesPageDataDatabase } from "../databasesPage"; import { + Nodes, + Stores, buildIndexStatToRecommendationsMap, + getNodeIdsFromStoreIds, getNodesByRegionString, normalizePrivileges, normalizeRoles, @@ -38,6 +41,8 @@ interface DerivedDatabaseDetailsParams { spanStats: Record; nodeRegions: Record; isTenant: boolean; + /** A list of node statuses so that store ids can be mapped to nodes. */ + nodeStatuses: cockroach.server.status.statuspb.INodeStatus[]; } export const deriveDatabaseDetailsMemoized = createSelector( @@ -46,12 +51,14 @@ export const deriveDatabaseDetailsMemoized = createSelector( (params: DerivedDatabaseDetailsParams) => params.spanStats, (params: DerivedDatabaseDetailsParams) => params.nodeRegions, (params: DerivedDatabaseDetailsParams) => params.isTenant, + (params: DerivedDatabaseDetailsParams) => params.nodeStatuses, ( dbListResp, databaseDetails, spanStats, nodeRegions, isTenant, + nodeStatuses, ): DatabasesPageDataDatabase[] => { const databases = dbListResp?.databases ?? []; return databases.map(dbName => { @@ -61,9 +68,9 @@ export const deriveDatabaseDetailsMemoized = createSelector( dbName, dbDetails, spanStatsForDB, - dbListResp.error, nodeRegions, isTenant, + nodeStatuses, ); }); }, @@ -73,15 +80,22 @@ const deriveDatabaseDetails = ( database: string, dbDetails: DatabaseDetailsState, spanStats: DatabaseDetailsSpanStatsState, - dbListError: SqlExecutionErrorMessage, nodeRegionsByID: Record, isTenant: boolean, + nodeStatuses: cockroach.server.status.statuspb.INodeStatus[], ): DatabasesPageDataDatabase => { const dbStats = dbDetails?.data?.results.stats; - // TODO #118957 (xinhaoz) Use store id to regions mapping. - const stores = dbStats?.replicaData.storeIDs || []; + /** List of store IDs for the current cluster. All of the values in the + * `*replicas` columns correspond to store IDs. */ + const stores: Stores = { + kind: "store", + ids: dbStats?.replicaData.storeIDs || [], + }; + /** List of node IDs for the current cluster. */ + const nodes = getNodeIdsFromStoreIds(stores, nodeStatuses); + const nodesByRegionString = getNodesByRegionString( - stores, + nodes, nodeRegionsByID, isTenant, ); @@ -100,7 +114,7 @@ const deriveDatabaseDetails = ( name: database, spanStats: spanStats?.data?.results.spanStats, tables: dbDetails?.data?.results.tablesResp, - nodes: stores, + nodes: nodes.ids, nodesByRegionString, numIndexRecommendations, }; @@ -112,6 +126,8 @@ interface DerivedTableDetailsParams { tableDetails: Record; nodeRegions: Record; isTenant: boolean; + /** A list of node statuses so that store ids can be mapped to nodes. */ + nodeStatuses: cockroach.server.status.statuspb.INodeStatus[]; } export const deriveTableDetailsMemoized = createSelector( @@ -120,18 +136,26 @@ export const deriveTableDetailsMemoized = createSelector( (params: DerivedTableDetailsParams) => params.tableDetails, (params: DerivedTableDetailsParams) => params.nodeRegions, (params: DerivedTableDetailsParams) => params.isTenant, + (params: DerivedTableDetailsParams) => params.nodeStatuses, ( dbName, tables, tableDetails, nodeRegions, isTenant, + nodeStatuses, ): DatabaseDetailsPageDataTable[] => { tables = tables || []; return tables.map(table => { const tableID = generateTableID(dbName, table); const details = tableDetails[tableID]; - return deriveDatabaseTableDetails(table, details, nodeRegions, isTenant); + return deriveDatabaseTableDetails( + table, + details, + nodeRegions, + isTenant, + nodeStatuses, + ); }); }, ); @@ -141,6 +165,7 @@ const deriveDatabaseTableDetails = ( details: TableDetailsState, nodeRegions: Record, isTenant: boolean, + nodeStatuses: cockroach.server.status.statuspb.INodeStatus[], ): DatabaseDetailsPageDataTable => { const results = details?.data?.results; const grants = results?.grantsResp.grants ?? []; @@ -148,7 +173,11 @@ const deriveDatabaseTableDetails = ( const normalizedPrivileges = normalizePrivileges( [].concat(...grants.map(grant => grant.privileges)), ); - const storeIDs = results?.stats.replicaData.storeIDs || []; + const stores: Stores = { + kind: "store", + ids: results?.stats.replicaData.storeIDs || [], + }; + const nodes: Nodes = getNodeIdsFromStoreIds(stores, nodeStatuses); return { name: table, loading: !!details?.inFlight, @@ -165,13 +194,8 @@ const deriveDatabaseTableDetails = ( statsLastUpdated: results?.heuristicsDetails, indexStatRecs: results?.stats.indexStats, spanStats: results?.stats.spanStats, - // TODO #118957 (xinhaoz) Store IDs and node IDs cannot be used interchangeably. - nodes: storeIDs, - nodesByRegionString: getNodesByRegionString( - storeIDs, - nodeRegions, - isTenant, - ), + nodes: nodes.ids, + nodesByRegionString: getNodesByRegionString(nodes, nodeRegions, isTenant), }, }; }; @@ -180,13 +204,21 @@ interface DerivedTablePageDetailsParams { details: TableDetailsState; nodeRegions: Record; isTenant: boolean; + /** A list of node statuses so that store ids can be mapped to nodes. */ + nodeStatuses: cockroach.server.status.statuspb.INodeStatus[]; } export const deriveTablePageDetailsMemoized = createSelector( (params: DerivedTablePageDetailsParams) => params.details, (params: DerivedTablePageDetailsParams) => params.nodeRegions, (params: DerivedTablePageDetailsParams) => params.isTenant, - (details, nodeRegions, isTenant): DatabaseTablePageDataDetails => { + (params: DerivedTablePageDetailsParams) => params.nodeStatuses, + ( + details, + nodeRegions, + isTenant, + nodeStatuses, + ): DatabaseTablePageDataDetails => { const results = details?.data?.results; const grants = results?.grantsResp.grants || []; const normalizedGrants = @@ -194,7 +226,13 @@ export const deriveTablePageDetailsMemoized = createSelector( user: grant.user, privileges: normalizePrivileges(grant.privileges), })) || []; - const nodes = results?.stats.replicaData.storeIDs || []; + + const stores: Stores = { + kind: "store", + ids: results?.stats.replicaData.storeIDs || [], + }; + const nodes = getNodeIdsFromStoreIds(stores, nodeStatuses); + return { loading: !!details?.inFlight, loaded: !!details?.valid, diff --git a/pkg/ui/workspaces/cluster-ui/src/databases/util.spec.ts b/pkg/ui/workspaces/cluster-ui/src/databases/util.spec.ts index d3b068988976..e9c0c04e528d 100644 --- a/pkg/ui/workspaces/cluster-ui/src/databases/util.spec.ts +++ b/pkg/ui/workspaces/cluster-ui/src/databases/util.spec.ts @@ -8,8 +8,12 @@ // by the Apache License, Version 2.0, included in the file // licenses/APL.txt. +import { INodeStatus } from "../util"; import { + Nodes, + Stores, getNodesByRegionString, + getNodeIdsFromStoreIds, normalizePrivileges, normalizeRoles, } from "./util"; @@ -17,7 +21,7 @@ import { describe("Getting nodes by region string", () => { describe("is not tenant", () => { it("when all nodes different regions", () => { - const nodes = [1, 2, 3]; + const nodes: Nodes = { kind: "node", ids: [1, 2, 3] }; const regions = { "1": "region1", "2": "region2", @@ -28,7 +32,7 @@ describe("Getting nodes by region string", () => { }); it("when all nodes same region", () => { - const nodes = [1, 2, 3]; + const nodes: Nodes = { kind: "node", ids: [1, 2, 3] }; const regions = { "1": "region1", "2": "region1", @@ -39,7 +43,7 @@ describe("Getting nodes by region string", () => { }); it("when some nodes different regions", () => { - const nodes = [1, 2, 3]; + const nodes: Nodes = { kind: "node", ids: [1, 2, 3] }; const regions = { "1": "region1", "2": "region1", @@ -50,14 +54,14 @@ describe("Getting nodes by region string", () => { }); it("when region map is empty", () => { - const nodes = [1, 2, 3]; + const nodes: Nodes = { kind: "node", ids: [1, 2, 3] }; const regions = {}; const result = getNodesByRegionString(nodes, regions, false); expect(result).toEqual(""); }); it("when nodes are empty", () => { - const nodes: number[] = []; + const nodes: Nodes = { kind: "node", ids: [] }; const regions = { "1": "region1", "2": "region1", @@ -69,6 +73,79 @@ describe("Getting nodes by region string", () => { }); }); +describe("getNodeIdsFromStoreIds", () => { + it("returns the correct node ids when all nodes have multiple stores", () => { + const stores: Stores = { kind: "store", ids: [1, 3, 6, 2, 4, 5] }; + const nodeStatuses: INodeStatus[] = [ + { + desc: { + node_id: 1, + }, + store_statuses: [{ desc: { store_id: 1 } }, { desc: { store_id: 2 } }], + }, + { + desc: { + node_id: 2, + }, + store_statuses: [{ desc: { store_id: 3 } }, { desc: { store_id: 5 } }], + }, + { + desc: { + node_id: 3, + }, + store_statuses: [{ desc: { store_id: 4 } }, { desc: { store_id: 6 } }], + }, + ]; + const result = getNodeIdsFromStoreIds(stores, nodeStatuses); + expect(result).toEqual({ kind: "node", ids: [1, 2, 3] }); + }); + + it("returns an empty list when no stores ids are provided", () => { + const stores: Stores = { kind: "store", ids: [] }; + const result = getNodeIdsFromStoreIds(stores, []); + expect(result).toEqual({ kind: "node", ids: [] }); + }); + + it("returns the correct node ids when there is one store per node", () => { + const stores: Stores = { kind: "store", ids: [1, 3, 4] }; + const nodeStatuses: INodeStatus[] = [ + { + desc: { + node_id: 1, + }, + store_statuses: [{ desc: { store_id: 1 } }], + }, + { + desc: { + node_id: 2, + }, + store_statuses: [{ desc: { store_id: 3 } }], + }, + { + desc: { + node_id: 3, + }, + store_statuses: [{ desc: { store_id: 4 } }], + }, + ]; + const result = getNodeIdsFromStoreIds(stores, nodeStatuses); + expect(result).toEqual({ kind: "node", ids: [1, 2, 3] }); + }); + it("returns the correct node ids when there is only one node", () => { + const stores: Stores = { kind: "store", ids: [3] }; + const nodeStatuses: INodeStatus[] = [ + { + desc: { + node_id: 1, + }, + store_statuses: [{ desc: { store_id: 3 } }], + }, + ]; + const result = getNodeIdsFromStoreIds(stores, nodeStatuses); + expect(result).toEqual({ kind: "node", ids: [1] }); + }); +}); + describe("Normalize privileges", () => { it("sorts correctly when input is disordered", () => { const privs = ["CREATE", "DELETE", "UPDATE", "ALL", "GRANT"]; diff --git a/pkg/ui/workspaces/cluster-ui/src/databases/util.tsx b/pkg/ui/workspaces/cluster-ui/src/databases/util.tsx index 2a831dfef803..5a9340ce97f8 100644 --- a/pkg/ui/workspaces/cluster-ui/src/databases/util.tsx +++ b/pkg/ui/workspaces/cluster-ui/src/databases/util.tsx @@ -58,31 +58,71 @@ export class GetDatabaseInfoError extends Error { } } -// getNodesByRegionString converts a list of node ids and map of -// node ids to region to a string of node ids by region, ordered -// by region name, e.g. -// regionA(n1, n2), regionB(n2,n3), ... +/** Store ids and node ids are both of type `number[]`. To disambiguate, a + * `kind` field is included in the type. */ +export type Stores = { kind: "store"; ids: number[] }; + +/** Node ids and store IDs are both of type `number[]`. To disambiguate, a + * `kind` field is included in the type. */ +export type Nodes = { kind: "node"; ids: number[] }; + +/** getNodeIdsFromStoreIds converts a list of store IDs to a list of node IDs. + * + * @param stores - Store ids for the cluster. + * @param nodeStatuses - A list of nodes that includes store information. + * @returns A list of node ids for the cluster. + */ +export function getNodeIdsFromStoreIds( + stores: Stores, + nodeStatuses: cockroach.server.status.statuspb.INodeStatus[], +): Nodes { + /** Associates stores with their node. Nodes can have multiple stores: + * `{ store1: node1, store2: node1 }` */ + const nodeByStoreMap: Record = {}; + nodeStatuses?.map(node => + node.store_statuses?.map(store => { + nodeByStoreMap[store.desc.store_id] = node.desc.node_id; + }), + ); + + /** A unique list of node IDs derived from the nodeByStoreMap. */ + const nodeIds = Array.from( + new Set(stores.ids.map(id => nodeByStoreMap[id])), + ).filter(value => value !== undefined); + + return { kind: "node", ids: nodeIds }; +} + +/** getNodesByRegionString converts a list of node IDs to a user-facing string. + * + * @param nodes - Node ids for the cluster. + * @param nodeRegions - A map of node IDs to region IDs. + * @param isTenant - Whether the cluster is a tenant cluster. + * @returns A string of node IDs by region, ordered by region name, e.g. + * `regionA(n1, n2), regionB(n2,n3), ...` + */ export function getNodesByRegionString( - nodes: number[], + nodes: Nodes, nodeRegions: Record, isTenant: boolean, ): string { return nodesByRegionMapToString( - createNodesByRegionMap(nodes, nodeRegions), + createNodesByRegionMap(nodes.ids, nodeRegions), isTenant, ); } -// nodesByRegionMapToString converts a map of regions to node ids, -// ordered by region name, e.g. converts: -// { regionA: [1, 2], regionB: [2, 3] } -// to: -// regionA(n1, n2), regionB(n2,n3), ... -// If the cluster is a tenant cluster, then we redact node info -// and only display the region name, e.g. -// regionA(n1, n2), regionB(n2,n3), ... becomes: -// regionA, regionB, ... -export function nodesByRegionMapToString( +/** nodesByRegionMapToString converts a map of regions to node ids, + * ordered by region name, e.g. converts: + * `{ regionA: [1, 2], regionB: [2, 3] }` + * to: + * `regionA(n1, n2), regionB(n2,n3), ...` + * + * If the cluster is a tenant cluster, then we redact node info + * and only display the region name, e.g. + * `regionA(n1, n2), regionB(n2,n3), ...` becomes: + * `regionA, regionB, ...` */ +function nodesByRegionMapToString( nodesByRegion: Record, isTenant: boolean, ): string { @@ -102,7 +142,7 @@ export function nodesByRegionMapToString( .join(", "); } -export function createNodesByRegionMap( +function createNodesByRegionMap( nodes: number[], nodeRegions: Record, ): Record { diff --git a/pkg/ui/workspaces/cluster-ui/src/databasesPage/databasesPageConnected.ts b/pkg/ui/workspaces/cluster-ui/src/databasesPage/databasesPageConnected.ts index c28526efe2c0..6df5d93fd383 100644 --- a/pkg/ui/workspaces/cluster-ui/src/databasesPage/databasesPageConnected.ts +++ b/pkg/ui/workspaces/cluster-ui/src/databasesPage/databasesPageConnected.ts @@ -65,6 +65,7 @@ const mapStateToProps = (state: AppState): DatabasesPageData => { spanStats: state.adminUI?.databaseDetailsSpanStats, nodeRegions, isTenant, + nodeStatuses: state.adminUI.nodes.data, }), sortSetting: selectDatabasesSortSetting(state), search: selectDatabasesSearch(state), diff --git a/pkg/ui/workspaces/db-console/src/redux/timeScale.ts b/pkg/ui/workspaces/db-console/src/redux/timeScale.ts index 214286a5275e..9dccae023c70 100644 --- a/pkg/ui/workspaces/db-console/src/redux/timeScale.ts +++ b/pkg/ui/workspaces/db-console/src/redux/timeScale.ts @@ -81,10 +81,13 @@ export class TimeScaleState { fixedWindowEnd: val.fixedWindowEnd && moment(val.fixedWindowEnd), }; } catch (e) { - console.warn( - `Couldn't retrieve or parse TimeScale options from SessionStorage`, - e, - ); + // Don't log this in tests because it pollutes the output. + if (process.env.NODE_ENV !== "test") { + console.warn( + `Couldn't retrieve or parse TimeScale options from SessionStorage`, + e, + ); + } } this.scale = timeScale || { ...defaultTimeScaleOptions["Past Hour"], diff --git a/pkg/ui/workspaces/db-console/src/views/databases/databaseDetailsPage/redux.spec.ts b/pkg/ui/workspaces/db-console/src/views/databases/databaseDetailsPage/redux.spec.ts index 9b6845d3f0fe..c2a9cd757278 100644 --- a/pkg/ui/workspaces/db-console/src/views/databases/databaseDetailsPage/redux.spec.ts +++ b/pkg/ui/workspaces/db-console/src/views/databases/databaseDetailsPage/redux.spec.ts @@ -125,6 +125,10 @@ class TestDriver { ); } + async refreshNodes() { + return this.actions.refreshNodes(); + } + private findTable(name: string) { return _.find(this.properties().tables, { name }); } @@ -375,9 +379,29 @@ describe("Database Details Page", function () { ], ); + fakeApi.stubNodesUI({ + nodes: [...Array(5).keys()].map(node_id => { + return { + desc: { + node_id: node_id + 1, // 1-index offset. + locality: { + tiers: [ + { + key: "region", + value: "gcp-us-east1", + }, + ], + }, + }, + store_statuses: [{ desc: { store_id: node_id + 1 } }], + }; + }), + }); + await driver.refreshDatabaseDetails(); await driver.refreshTableDetails(`"public"."foo"`); await driver.refreshTableDetails(`"public"."bar"`); + await driver.refreshNodes(); driver.assertTableDetails(`"public"."foo"`, { name: `"public"."foo"`, @@ -407,7 +431,7 @@ describe("Database Details Page", function () { live_percentage: 0.5, }, nodes: [1, 2, 3], - nodesByRegionString: "", + nodesByRegionString: "gcp-us-east1(n1,n2,n3)", }, }); @@ -437,7 +461,7 @@ describe("Database Details Page", function () { approximate_disk_bytes: 10, }, nodes: [1, 2, 3, 4, 5], - nodesByRegionString: "", + nodesByRegionString: "gcp-us-east1(n1,n2,n3,n4,n5)", }, }); }); diff --git a/pkg/ui/workspaces/db-console/src/views/databases/databaseDetailsPage/redux.ts b/pkg/ui/workspaces/db-console/src/views/databases/databaseDetailsPage/redux.ts index 47cd3488ac09..48d8bec38a20 100644 --- a/pkg/ui/workspaces/db-console/src/views/databases/databaseDetailsPage/redux.ts +++ b/pkg/ui/workspaces/db-console/src/views/databases/databaseDetailsPage/redux.ts @@ -20,6 +20,7 @@ import { import { refreshDatabaseDetails, + refreshNodes, refreshTableDetails, } from "src/redux/apiReducers"; import { AdminUIState } from "src/redux/state"; @@ -77,6 +78,8 @@ export const mapStateToProps = ( const dbTables = databaseDetails[database]?.data?.results.tablesResp.tables || []; const nodeRegions = nodeRegionsByIDSelector(state); + const nodeStatuses = state?.cachedData.nodes.data; + return { loading: !!databaseDetails[database]?.inFlight, loaded: !!databaseDetails[database]?.valid, @@ -97,6 +100,7 @@ export const mapStateToProps = ( tableDetails, nodeRegions, isTenant, + nodeStatuses, }), showIndexRecommendations: selectIndexRecommendationsEnabled(state), csIndexUnusedDuration: selectDropUnusedIndexDuration(state), @@ -134,4 +138,5 @@ export const mapDispatchToProps = { }), onSearchComplete: (query: string) => searchLocalTablesSetting.set(query), onFilterChange: (filters: Filters) => filtersLocalTablesSetting.set(filters), + refreshNodes, }; 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 8ff1036b3c9e..442d475eaeb4 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 @@ -59,12 +59,18 @@ export const mapStateToProps = ( util.minDate, ); const nodeRegions = nodeRegionsByIDSelector(state); + const nodeStatuses = state?.cachedData.nodes.data; return { databaseName: database, name: table, schemaName: "", - details: deriveTablePageDetailsMemoized({ details, nodeRegions, isTenant }), + details: deriveTablePageDetailsMemoized({ + details, + nodeRegions, + isTenant, + nodeStatuses, + }), showNodeRegionsSection: selectIsMoreThanOneNode(state) && !isTenant, automaticStatsCollectionEnabled: selectAutomaticStatsCollectionEnabled(state) || false, diff --git a/pkg/ui/workspaces/db-console/src/views/databases/databasesPage/redux.spec.ts b/pkg/ui/workspaces/db-console/src/views/databases/databasesPage/redux.spec.ts index 9f2cef0519d4..c08b9519977e 100644 --- a/pkg/ui/workspaces/db-console/src/views/databases/databasesPage/redux.spec.ts +++ b/pkg/ui/workspaces/db-console/src/views/databases/databasesPage/redux.spec.ts @@ -18,6 +18,7 @@ import { defaultFilters, api as clusterUiApi, } from "@cockroachlabs/cluster-ui"; +import { cockroach } from "@cockroachlabs/crdb-protobuf-client"; import { AdminUIState, createAdminUIStore } from "src/redux/state"; import * as fakeApi from "src/util/fakeApi"; @@ -199,7 +200,9 @@ describe("Databases Page", function () { "gcp-europe-west1", ]; - const nodes = Array.from(Array(regions.length).keys()).map(node_id => { + const nodes: cockroach.server.serverpb.INodeResponse[] = Array.from( + Array(regions.length).keys(), + ).map(node_id => { return { desc: { node_id: node_id + 1, // 1-index offset. @@ -212,6 +215,7 @@ describe("Databases Page", function () { ], }, }, + store_statuses: [{ desc: { store_id: node_id + 1 } }], }; }); diff --git a/pkg/ui/workspaces/db-console/src/views/databases/databasesPage/redux.ts b/pkg/ui/workspaces/db-console/src/views/databases/databasesPage/redux.ts index 72e912572215..27804d8d308f 100644 --- a/pkg/ui/workspaces/db-console/src/views/databases/databasesPage/redux.ts +++ b/pkg/ui/workspaces/db-console/src/views/databases/databasesPage/redux.ts @@ -87,6 +87,7 @@ export const mapStateToProps = (state: AdminUIState): DatabasesPageData => { spanStats, nodeRegions, isTenant, + nodeStatuses: state?.cachedData.nodes.data, }), sortSetting: sortSettingLocalSetting.selector(state), filters: filtersLocalSetting.selector(state),