Skip to content

Commit

Permalink
cluster-ui: db page - map store ids to nodes and region
Browse files Browse the repository at this point in the history
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
  • Loading branch information
xinhaoz committed Sep 20, 2024
1 parent 47557af commit 2d5066f
Show file tree
Hide file tree
Showing 9 changed files with 113 additions and 30 deletions.
37 changes: 37 additions & 0 deletions pkg/ui/workspaces/cluster-ui/src/api/nodesApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";

import { NodeID, StoreID } from "../types/clusterTypes";

const NODES_PATH = "_status/nodes_ui";

export const getNodes =
(): Promise<cockroach.server.serverpb.NodesResponse> => {
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<NodeID, string> = {};
const storeIDToNodeID: Record<StoreID, NodeID> = {};
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,
};
};
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ export const RegionNodesLabel: React.FC<RegionNodesLabelProps> = ({
<div className={styles.container}>
<Tooltip placement="top" title={nodes.map(nid => "n" + nid).join(", ")}>
<div className={styles["label-body"]}>
<Text strong>{region.label}</Text>
<Text strong>{region.label || "Unknown Region"}</Text>
{showCode && <Text>({region.code})</Text>}
<div>
<Badge count={nodes.length} className={styles.badge} />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand All @@ -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,
Expand Down Expand Up @@ -168,8 +169,12 @@ export const TablesPageV2 = () => {
});
const data = mockData;

const [nodeRegions, setNodeRegions] = useState<ReactSelectOption[]>([]);
const onNodeRegionsChange = (selected: OptionsType<ReactSelectOption>) => {
const [nodeRegions, setNodeRegions] = useState<ReactSelectOption<string>[]>(
[],
);
const onNodeRegionsChange = (
selected: OptionsType<ReactSelectOption<string>>,
) => {
setNodeRegions((selected ?? []).map(v => v));
};

Expand Down
4 changes: 3 additions & 1 deletion pkg/ui/workspaces/cluster-ui/src/databaseDetailsV2/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@

import { Moment } from "moment-timezone";

import { NodeID } from "src/types/clusterTypes";

export type TableRow = {
qualifiedNameWithSchema: string;
name: string;
Expand All @@ -18,7 +20,7 @@ export type TableRow = {
replicationSizeBytes: number;
rangeCount: number;
columnCount: number;
nodesByRegion: Record<string, number[]>;
nodesByRegion: Record<string, NodeID[]>;
liveDataPercentage: number;
liveDataBytes: number;
totalDataBytes: number;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, number[]>;
nodesByRegion: {
isLoading: boolean;
data: Record<string, NodeID[]>;
};
schemaInsightsCount: number;
key: string;
};
42 changes: 28 additions & 14 deletions pkg/ui/workspaces/cluster-ui/src/databasesV2/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -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";
Expand Down Expand Up @@ -81,15 +84,17 @@ const COLUMNS: (TableColumnProps<DatabaseRow> & {
{
title: DatabaseColName.NODE_REGIONS,
render: (db: DatabaseRow) => (
<div>
{Object.entries(db.nodesByRegion ?? {}).map(([region, nodes]) => (
<RegionNodesLabel
key={region}
nodes={nodes}
region={{ label: region, code: region }}
/>
))}
</div>
<Skeleton loading={db.nodesByRegion.isLoading}>
<div>
{Object.entries(db.nodesByRegion?.data).map(([region, nodes]) => (
<RegionNodesLabel
key={region}
nodes={nodes}
region={{ label: region, code: region }}
/>
))}
</div>
</Skeleton>
),
},
{
Expand Down Expand Up @@ -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<ReactSelectOption[]>([]);
const onNodeRegionsChange = (selected: OptionsType<ReactSelectOption>) => {
const [nodeRegions, setNodeRegions] = useState<ReactSelectOption<string>[]>(
[],
);
const onNodeRegionsChange = (
selected: OptionsType<ReactSelectOption<string>>,
) => {
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<DatabaseRow> = (pagination, sorter) => {
Expand Down
30 changes: 25 additions & 5 deletions pkg/ui/workspaces/cluster-ui/src/databasesV2/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<NodeID, string>;
storeIDToNodeID: Record<StoreID, NodeID>;
isLoading: boolean;
},
): DatabaseRow[] => {
return raw.map(
(db: DatabaseMetadata): DatabaseRow => ({
return raw.map((db: DatabaseMetadata): DatabaseRow => {
const nodesByRegion: Record<string, NodeID[]> = {};
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,
},
};
});
};
4 changes: 2 additions & 2 deletions pkg/ui/workspaces/cluster-ui/src/types/clusterTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions pkg/ui/workspaces/cluster-ui/src/types/selectTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<T> = {
label: string;
value: string;
value: T;
};

0 comments on commit 2d5066f

Please sign in to comment.