Skip to content

CNS-130 Add Cluster Replica sub-rows to cluster table - #38150

Open
jdonelson wants to merge 8 commits into
mainfrom
jdonelson/CNS-130_add_replica_rows_to_cluster_table
Open

CNS-130 Add Cluster Replica sub-rows to cluster table#38150
jdonelson wants to merge 8 commits into
mainfrom
jdonelson/CNS-130_add_replica_rows_to_cluster_table

Conversation

@jdonelson

@jdonelson jdonelson commented Aug 10, 2026

Copy link
Copy Markdown
Contributor

Motivation

Implements CNS-130

To support https://linear.app/materializeinc/issue/CNS-121/add-memory-swap-and-cpu-metrics-in-the-clusters-list

Usage metrics are provided per replica. In order to provide meaningful data, the cluster table needs to display data at the replica level of granularity.

Description

Each row in the cluster table can be clicked to reveal child rows. Each child row displays the data for each replica that has been created of the selected cluster. While this change is not meaningful in its current state, this is a dependency for the goal state of providing usage metrics.

A feature flag usage-metrics-in-cluster-list-CNS121 has been put in place to hide this change until the full project (displaying the metrics) has been completed.

Verification

Added unit tests to confirm behavior of the replica rows. Confirmed the behavior locally while using Organization Impersonation. Also confirmed that behavior is hidden when feature flag is set.

Feature Flag = true
Screenshot 2026-08-10 at 4 54 38 PM

Feature Flag = false
Screenshot 2026-08-10 at 4 48 55 PM

@jdonelson
jdonelson requested a review from a team as a code owner August 10, 2026 21:06
@jdonelson
jdonelson requested a review from leedqin August 10, 2026 21:06
@linear-code

linear-code Bot commented Aug 10, 2026

Copy link
Copy Markdown

CNS-130

@leedqin leedqin added the A-CONSOLE Area: Console label Aug 10, 2026
@leedqin

leedqin commented Aug 10, 2026

Copy link
Copy Markdown
Contributor
image could we do a design change similar to this if this is not looking like too much trouble . This is sources list btw maybe when row expanded could have the cluster to follow the grey front quickstart and expanded row could be bolder. image

When we expand the row, the replica column with a "-" seems a bit redundant so perhaps we could not show it or figure out some other way to show that information.

@def- def- left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here's some extra tests, all failing currently on this PR:

diff --git a/console/src/platform/clusters/ClustersList.test.tsx b/console/src/platform/clusters/ClustersList.test.tsx
index 1a2ae37a88..5f5f1a954f 100644
--- a/console/src/platform/clusters/ClustersList.test.tsx
+++ b/console/src/platform/clusters/ClustersList.test.tsx
@@ -7,7 +7,7 @@
 // the Business Source License, use of this software will be governed
 // by the Apache License, Version 2.0.

-import { screen, within } from "@testing-library/react";
+import { screen, waitFor, within } from "@testing-library/react";
 import userEvent from "@testing-library/user-event";
 import React from "react";

@@ -15,7 +15,7 @@ import { Cluster, Replica } from "~/api/materialize/cluster/clusterList";
 import { getStore } from "~/jotai";
 import { allClusters } from "~/store/allClusters";
 import { mockSubscribeState } from "~/test/mockSubscribe";
-import { renderComponent } from "~/test/utils";
+import { renderComponent, RenderWithPathname } from "~/test/utils";
 import {
   formatDate,
   FRIENDLY_DATETIME_FORMAT_NO_SECONDS,
@@ -30,6 +30,16 @@ vi.mock("~/hooks/useFlags", () => ({
   useFlags: () => ({ "usage-metrics-in-cluster-list-CNS121": true }),
 }));

+vi.mock("./queries", async () => {
+  const actual = await vi.importActual<typeof import("./queries")>("./queries");
+  return {
+    ...actual,
+    useAvailableClusterSizes: () => ({ data: undefined }),
+    useMaxReplicasPerCluster: () => ({ data: undefined }),
+    useOwners: () => ({ isOwner: () => true }),
+  };
+});
+
 // The list opens a websocket subscribe to surface out-of-memory warnings.
 // Stubbing it keeps these tests off the socket and, because it reports no
 // error, leaves the `lastStatusChange` column visible.
@@ -92,7 +102,11 @@ const buildCluster = (overrides: Partial<Cluster> = {}): Cluster => ({

 const renderClustersList = async (clusters: Cluster[]) => {
   getStore().set(allClusters, mockSubscribeState({ data: clusters }));
-  return renderComponent(<ClustersListPage />);
+  return renderComponent(
+    <RenderWithPathname>
+      <ClustersListPage />
+    </RenderWithPathname>,
+  );
 };

 /**
@@ -109,6 +123,16 @@ const expandCluster = async (
   await user.keyboard("{Enter}");
 };

+const tabTo = async (
+  user: ReturnType<typeof userEvent.setup>,
+  target: HTMLElement,
+) => {
+  for (let i = 0; i < 10 && document.activeElement !== target; i += 1) {
+    await user.tab();
+  }
+  expect(target).toHaveFocus();
+};
+
 /** Text of every visible cell in the row containing `rowLabel`. */
 const cellsForRow = (rowLabel: string) => {
   const row = screen.getByText(rowLabel).closest("tr");
@@ -153,6 +177,36 @@ describe("ClustersList replica rows", () => {
     expect(cellsForRow("r2")[2]).toBe("100cc");
   });

+  it("navigates when the cluster link is activated with the keyboard", async () => {
+    const user = userEvent.setup();
+    await renderClustersList([buildCluster()]);
+
+    await tabTo(user, screen.getByRole("link", { name: "compute" }));
+
+    await user.keyboard("{Enter}");
+
+    expect(screen.getByTestId("pathname")).toHaveTextContent("/u1/compute");
+  });
+
+  it("keeps replicas expandable when searching by cluster name", async () => {
+    const user = userEvent.setup();
+    await renderClustersList([
+      buildCluster(),
+      buildCluster({ id: "u2", name: "serving", replicas: [] }),
+    ]);
+
+    await user.type(
+      screen.getByPlaceholderText("Search clusters..."),
+      "compute",
+    );
+    await waitFor(() => {
+      expect(screen.queryByText("serving")).not.toBeInTheDocument();
+    });
+    await expandCluster(user, "compute");
+
+    expect(screen.getByText("r1")).toBeInTheDocument();
+  });
+
   it("renders the most recent status when a replica has several processes", async () => {
     const user = userEvent.setup();
     const newest = "2024-03-07T09:00:00.000Z";
@@ -250,4 +304,21 @@ describe("ClustersList replica rows", () => {
     expect(cells[2]).toBe("50cc, 100cc");
     expect(cells[3]).toBe(formatted(STATUS_UPDATED_AT));
   });
+
+  it("opens the action menu when its button is activated with the keyboard", async () => {
+    const user = userEvent.setup();
+    await renderClustersList([buildCluster()]);
+
+    const actionButton = await screen.findByRole("button", {
+      name: "More actions",
+    });
+    await tabTo(user, actionButton);
+
+    await user.keyboard("[Space]");
+
+    expect(actionButton).toHaveAttribute("aria-expanded", "true");
+    expect(
+      screen.getByRole("menuitem", { name: "Alter cluster" }),
+    ).toBeVisible();
+  });
 });

const row = screen.getByText(clusterName).closest("tr");
if (!row) throw new Error(`no row found for cluster "${clusterName}"`);
row.focus();
await user.keyboard("{Enter}");

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Doesn't this break the existing enter (also space) handling on this page?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Aaaahhh keyboard navigation! 🤦 Thanks for calling it out.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

A-CONSOLE Area: Console

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants