From 6d3a4e298dbb0a6c5693909f46131d330da27c8d Mon Sep 17 00:00:00 2001 From: Xin Hao Zhang Date: Wed, 18 Sep 2024 12:09:55 -0400 Subject: [PATCH] cluster-ui: db page - map store ids to nodes and region This commit populates the Node/Regions column of the new db page. It maps store ids to its node ids, and then to the node's region. A new SWR hook to fetch node statuses is added to the nodesApi. The hook also computes the storeID -> nodeID and nodeID -> region maps. Epic: CRDB-37558 Fixes: #130886 Release note: None --- .../workspaces/cluster-ui/src/api/nodesApi.ts | 37 ++++++++++++++++ .../regionNodesLabel/regionNodesLabel.tsx | 2 +- .../src/databaseDetailsV2/tablesView.tsx | 13 ++++-- .../cluster-ui/src/databaseDetailsV2/types.ts | 4 +- .../src/databasesV2/databaseTypes.ts | 7 +++- .../cluster-ui/src/databasesV2/index.tsx | 42 ++++++++++++------- .../cluster-ui/src/databasesV2/utils.ts | 30 ++++++++++--- .../cluster-ui/src/types/clusterTypes.ts | 4 +- .../cluster-ui/src/types/selectTypes.ts | 4 +- 9 files changed, 113 insertions(+), 30 deletions(-) diff --git a/pkg/ui/workspaces/cluster-ui/src/api/nodesApi.ts b/pkg/ui/workspaces/cluster-ui/src/api/nodesApi.ts index 35625d553777..dc60d113b0c1 100644 --- a/pkg/ui/workspaces/cluster-ui/src/api/nodesApi.ts +++ b/pkg/ui/workspaces/cluster-ui/src/api/nodesApi.ts @@ -9,8 +9,13 @@ // 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"; + +import { NodeID, StoreID } from "../types/clusterTypes"; const NODES_PATH = "_status/nodes_ui"; @@ -18,3 +23,35 @@ 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 396c2a038e09..9e427fce2646 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 { @@ -79,15 +82,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]) => ( + + ))} +
+
), }, { @@ -135,17 +140,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 73902e17b43b..6f8e61b745eb 100644 --- a/pkg/ui/workspaces/cluster-ui/src/databasesV2/utils.ts +++ b/pkg/ui/workspaces/cluster-ui/src/databasesV2/utils.ts @@ -12,6 +12,7 @@ import { DatabaseMetadata, DatabaseSortOptions, } from "src/api/databases/getDatabaseMetadataApi"; +import { NodeID, StoreID } from "src/types/clusterTypes"; import { DatabaseColName } from "./constants"; import { DatabaseRow } from "./databaseTypes"; @@ -52,17 +53,36 @@ export const getColTitleFromSortKey = ( 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; };