diff --git a/pkg/ui/workspaces/cluster-ui/src/api/nodesApi.ts b/pkg/ui/workspaces/cluster-ui/src/api/nodesApi.ts index 3c694af6980c..dc60d113b0c1 100644 --- a/pkg/ui/workspaces/cluster-ui/src/api/nodesApi.ts +++ b/pkg/ui/workspaces/cluster-ui/src/api/nodesApi.ts @@ -9,12 +9,49 @@ // licenses/APL.txt. import { cockroach } from "@cockroachlabs/crdb-protobuf-client"; +import { useMemo } from "react"; +import useSWR from "swr"; import { fetchData } from "src/api"; +import { getRegionFromLocality } from "src/store/nodes"; -const NODES_PATH = "_status/nodes"; +import { NodeID, StoreID } from "../types/clusterTypes"; + +const NODES_PATH = "_status/nodes_ui"; export const getNodes = (): Promise => { return fetchData(cockroach.server.serverpb.NodesResponse, NODES_PATH); }; + +export const useNodeStatuses = () => { + const { data, isLoading, error } = useSWR(NODES_PATH, getNodes, { + revalidateOnFocus: false, + }); + + const { storeIDToNodeID, nodeIDToRegion } = useMemo(() => { + const nodeIDToRegion: Record = {}; + const storeIDToNodeID: Record = {}; + if (!data) { + return { nodeIDToRegion, storeIDToNodeID }; + } + data.nodes.forEach(ns => { + ns.store_statuses.forEach(store => { + storeIDToNodeID[store.desc.store_id as StoreID] = ns.desc + .node_id as NodeID; + }); + nodeIDToRegion[ns.desc.node_id as NodeID] = getRegionFromLocality( + ns.desc.locality, + ); + }); + return { nodeIDToRegion, storeIDToNodeID }; + }, [data]); + + return { + data, + isLoading, + error, + nodeIDToRegion, + storeIDToNodeID, + }; +}; diff --git a/pkg/ui/workspaces/cluster-ui/src/components/regionNodesLabel/regionNodesLabel.tsx b/pkg/ui/workspaces/cluster-ui/src/components/regionNodesLabel/regionNodesLabel.tsx index e9b2473577a7..722dc1c7b98a 100644 --- a/pkg/ui/workspaces/cluster-ui/src/components/regionNodesLabel/regionNodesLabel.tsx +++ b/pkg/ui/workspaces/cluster-ui/src/components/regionNodesLabel/regionNodesLabel.tsx @@ -34,7 +34,7 @@ export const RegionNodesLabel: React.FC = ({
"n" + nid).join(", ")}>
- {region.label} + {region.label || "Unknown Region"} {showCode && ({region.code})}
diff --git a/pkg/ui/workspaces/cluster-ui/src/databaseDetailsV2/tablesView.tsx b/pkg/ui/workspaces/cluster-ui/src/databaseDetailsV2/tablesView.tsx index 66c6af350b90..8fbd7c19c6df 100644 --- a/pkg/ui/workspaces/cluster-ui/src/databaseDetailsV2/tablesView.tsx +++ b/pkg/ui/workspaces/cluster-ui/src/databaseDetailsV2/tablesView.tsx @@ -21,6 +21,7 @@ import PageCount from "src/sharedFromCloud/pageCount"; import { Search } from "src/sharedFromCloud/search"; import { Table, TableColumnProps } from "src/sharedFromCloud/table"; import useTable from "src/sharedFromCloud/useTable"; +import { NodeID } from "src/types/clusterTypes"; import { ReactSelectOption } from "src/types/selectTypes"; import { Bytes, EncodeDatabaseTableUri } from "src/util"; @@ -44,8 +45,8 @@ const mockData: TableRow[] = new Array(20).fill(1).map((_, i) => ({ nodesByRegion: i % 2 === 0 ? { - [mockRegionOptions[0].value]: [1, 2], - [mockRegionOptions[1].value]: [3], + [mockRegionOptions[0].value]: [1, 2] as NodeID[], + [mockRegionOptions[1].value]: [3] as NodeID[], } : null, liveDataPercentage: 1, @@ -168,8 +169,12 @@ export const TablesPageV2 = () => { }); const data = mockData; - const [nodeRegions, setNodeRegions] = useState([]); - const onNodeRegionsChange = (selected: OptionsType) => { + const [nodeRegions, setNodeRegions] = useState[]>( + [], + ); + const onNodeRegionsChange = ( + selected: OptionsType>, + ) => { setNodeRegions((selected ?? []).map(v => v)); }; diff --git a/pkg/ui/workspaces/cluster-ui/src/databaseDetailsV2/types.ts b/pkg/ui/workspaces/cluster-ui/src/databaseDetailsV2/types.ts index fc483fd2bae9..3d3e7ef01450 100644 --- a/pkg/ui/workspaces/cluster-ui/src/databaseDetailsV2/types.ts +++ b/pkg/ui/workspaces/cluster-ui/src/databaseDetailsV2/types.ts @@ -10,6 +10,8 @@ import { Moment } from "moment-timezone"; +import { NodeID } from "src/types/clusterTypes"; + export type TableRow = { qualifiedNameWithSchema: string; name: string; @@ -18,7 +20,7 @@ export type TableRow = { replicationSizeBytes: number; rangeCount: number; columnCount: number; - nodesByRegion: Record; + nodesByRegion: Record; liveDataPercentage: number; liveDataBytes: number; totalDataBytes: number; diff --git a/pkg/ui/workspaces/cluster-ui/src/databasesV2/databaseTypes.ts b/pkg/ui/workspaces/cluster-ui/src/databasesV2/databaseTypes.ts index 6de83d04f354..3eef721ad041 100644 --- a/pkg/ui/workspaces/cluster-ui/src/databasesV2/databaseTypes.ts +++ b/pkg/ui/workspaces/cluster-ui/src/databasesV2/databaseTypes.ts @@ -8,13 +8,18 @@ // by the Apache License, Version 2.0, included in the file // licenses/APL.txt. +import { NodeID } from "src/types/clusterTypes"; + export type DatabaseRow = { name: string; id: number; approximateDiskSizeBytes: number; tableCount: number; rangeCount: number; - nodesByRegion: Record; + nodesByRegion: { + isLoading: boolean; + data: Record; + }; schemaInsightsCount: number; key: string; }; diff --git a/pkg/ui/workspaces/cluster-ui/src/databasesV2/index.tsx b/pkg/ui/workspaces/cluster-ui/src/databasesV2/index.tsx index ae459a6418ca..91a8f67f34b1 100644 --- a/pkg/ui/workspaces/cluster-ui/src/databasesV2/index.tsx +++ b/pkg/ui/workspaces/cluster-ui/src/databasesV2/index.tsx @@ -8,6 +8,7 @@ // by the Apache License, Version 2.0, included in the file // licenses/APL.txt. +import { Skeleton } from "antd"; import React, { useMemo, useState } from "react"; import { Link } from "react-router-dom"; import Select, { OptionsType } from "react-select"; @@ -34,6 +35,8 @@ import useTable, { TableParams } from "src/sharedFromCloud/useTable"; import { ReactSelectOption } from "src/types/selectTypes"; import { Bytes } from "src/util"; +import { useNodeStatuses } from "../api"; + import { DatabaseColName } from "./constants"; import { DatabaseRow } from "./databaseTypes"; import { rawDatabaseMetadataToDatabaseRows } from "./utils"; @@ -81,15 +84,17 @@ const COLUMNS: (TableColumnProps & { { title: DatabaseColName.NODE_REGIONS, render: (db: DatabaseRow) => ( -
- {Object.entries(db.nodesByRegion ?? {}).map(([region, nodes]) => ( - - ))} -
+ +
+ {Object.entries(db.nodesByRegion?.data).map(([region, nodes]) => ( + + ))} +
+
), }, { @@ -137,17 +142,26 @@ export const DatabasesPageV2 = () => { const { data, error, isLoading } = useDatabaseMetadata( createDatabaseMetadataRequestFromParams(params), ); - + const nodesResp = useNodeStatuses(); const paginationState = data?.pagination_info; - const [nodeRegions, setNodeRegions] = useState([]); - const onNodeRegionsChange = (selected: OptionsType) => { + const [nodeRegions, setNodeRegions] = useState[]>( + [], + ); + const onNodeRegionsChange = ( + selected: OptionsType>, + ) => { setNodeRegions((selected ?? []).map(v => v)); }; const tableData = useMemo( - () => rawDatabaseMetadataToDatabaseRows(data?.results ?? []), - [data], + () => + rawDatabaseMetadataToDatabaseRows(data?.results ?? [], { + nodeIDToRegion: nodesResp.nodeIDToRegion, + storeIDToNodeID: nodesResp.storeIDToNodeID, + isLoading: nodesResp.isLoading, + }), + [data, nodesResp], ); const onTableChange: TableChangeFn = (pagination, sorter) => { diff --git a/pkg/ui/workspaces/cluster-ui/src/databasesV2/utils.ts b/pkg/ui/workspaces/cluster-ui/src/databasesV2/utils.ts index 3e7df11c7915..39807e61d122 100644 --- a/pkg/ui/workspaces/cluster-ui/src/databasesV2/utils.ts +++ b/pkg/ui/workspaces/cluster-ui/src/databasesV2/utils.ts @@ -9,22 +9,42 @@ // licenses/APL.txt. import { DatabaseMetadata } from "src/api/databases/getDatabaseMetadataApi"; +import { NodeID, StoreID } from "src/types/clusterTypes"; import { DatabaseRow } from "./databaseTypes"; export const rawDatabaseMetadataToDatabaseRows = ( raw: DatabaseMetadata[], + nodesInfo: { + nodeIDToRegion: Record; + storeIDToNodeID: Record; + isLoading: boolean; + }, ): DatabaseRow[] => { - return raw.map( - (db: DatabaseMetadata): DatabaseRow => ({ + return raw.map((db: DatabaseMetadata): DatabaseRow => { + const nodesByRegion: Record = {}; + if (!nodesInfo.isLoading) { + db.store_ids?.forEach(storeID => { + const nodeID = nodesInfo.storeIDToNodeID[storeID as StoreID]; + const region = nodesInfo.nodeIDToRegion[nodeID]; + if (!nodesByRegion[region]) { + nodesByRegion[region] = []; + } + nodesByRegion[region].push(nodeID); + }); + } + return { name: db.db_name, id: db.db_id, tableCount: db.table_count, approximateDiskSizeBytes: db.size_bytes, rangeCount: db.table_count, - nodesByRegion: {}, schemaInsightsCount: 0, key: db.db_id.toString(), - }), - ); + nodesByRegion: { + isLoading: nodesInfo.isLoading, + data: nodesByRegion, + }, + }; + }); }; diff --git a/pkg/ui/workspaces/cluster-ui/src/types/clusterTypes.ts b/pkg/ui/workspaces/cluster-ui/src/types/clusterTypes.ts index 225669a397a5..d1cb0b398fea 100644 --- a/pkg/ui/workspaces/cluster-ui/src/types/clusterTypes.ts +++ b/pkg/ui/workspaces/cluster-ui/src/types/clusterTypes.ts @@ -10,8 +10,8 @@ // This explicit typing helps us differentiate between // node ids and store ids. -export type NodeID = number; -export type StoreID = number; +export type NodeID = number & { readonly __brand: unique symbol }; +export type StoreID = number & { readonly __brand: unique symbol }; export type Region = { code: string; // e.g. us-east-1 diff --git a/pkg/ui/workspaces/cluster-ui/src/types/selectTypes.ts b/pkg/ui/workspaces/cluster-ui/src/types/selectTypes.ts index 52b7751d70ef..d8f72058fa23 100644 --- a/pkg/ui/workspaces/cluster-ui/src/types/selectTypes.ts +++ b/pkg/ui/workspaces/cluster-ui/src/types/selectTypes.ts @@ -11,7 +11,7 @@ // This is temporary. We'll remove this when we can access the shared // component library from the console. In the meantime this just removes // some type pollution from new components using react-select. -export type ReactSelectOption = { +export type ReactSelectOption = { label: string; - value: string; + value: T; };