Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions console/src/config/flexibleDeploymentFlags.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@
*/
export const disabledFlexibleDeploymentFlags: Record<string, boolean> = {
"freshness-page-CNS164": false,
// Self-managed has no preview toggle, so a true flag would ship the
// redesign to everyone.
"cluster-details-page-cns-173": false,
};

export const flexibleDeploymentFlags = new Proxy(
Expand Down
4 changes: 4 additions & 0 deletions console/src/config/uiPreviews.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,10 @@ export const UI_PREVIEWS = {
ldFlag: "usage-metrics-in-cluster-list-CNS121",
label: "Cluster list usage metrics",
},
clusterDetailsRedesign: {
ldFlag: "cluster-details-page-cns-173",
label: "Cluster details redesign",
},
} as const satisfies Record<string, { ldFlag: string; label: string }>;

export type UiPreviewKey = keyof typeof UI_PREVIEWS;
27 changes: 20 additions & 7 deletions console/src/hooks/useUiPreview.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,26 +10,33 @@
import { screen } from "@testing-library/react";
import React from "react";

import { UI_PREVIEWS } from "~/config/uiPreviews";
import { flexibleDeploymentFlags } from "~/config/flexibleDeploymentFlags";
import { UI_PREVIEWS, UiPreviewKey } from "~/config/uiPreviews";
import { useUiPreview } from "~/hooks/useUiPreview";
import { renderComponent } from "~/test/utils";

const clusterListPreview = UI_PREVIEWS.clusterListUsageMetrics;

const flags: Record<string, boolean> = {};

const appConfig = { mode: "cloud" as "cloud" | "self-managed" };

// Mirrors `useFlags`: LaunchDarkly in cloud, the stubbed flags elsewhere.
vi.mock("~/hooks/useFlags", () => ({
useFlags: () => flags,
useFlags: () =>
appConfig.mode === "cloud" ? flags : flexibleDeploymentFlags,
}));

const appConfig = { mode: "cloud" as "cloud" | "self-managed" };

vi.mock("~/config/useAppConfig", () => ({
useAppConfig: () => appConfig,
}));

const Probe = () => {
const { isAvailable, isEnabled } = useUiPreview("clusterListUsageMetrics");
const Probe = ({
previewKey = "clusterListUsageMetrics",
}: {
previewKey?: UiPreviewKey;
}) => {
const { isAvailable, isEnabled } = useUiPreview(previewKey);
return (
<>
<div>available: {String(isAvailable)}</div>
Expand All @@ -54,9 +61,15 @@ describe("useUiPreview", () => {

it("ships the flagged UI outright in self-managed mode, offering no choice", async () => {
appConfig.mode = "self-managed";
flags[clusterListPreview.ldFlag] = true;
await renderComponent(<Probe />);
expect(screen.getByText("available: false")).toBeInTheDocument();
expect(screen.getByText("enabled: true")).toBeInTheDocument();
});

it("keeps the cluster details redesign off in self-managed mode", async () => {
appConfig.mode = "self-managed";
await renderComponent(<Probe previewKey="clusterDetailsRedesign" />);
expect(screen.getByText("available: false")).toBeInTheDocument();
expect(screen.getByText("enabled: false")).toBeInTheDocument();
});
});
261 changes: 261 additions & 0 deletions console/src/platform/clusters/ClusterDetail.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ import userEvent from "@testing-library/user-event";
import React from "react";
import { Route, Routes } from "react-router-dom";

import { UI_PREVIEWS } from "~/config/uiPreviews";
import { uiPreviewOptInStorageKey } from "~/hooks/useUiPreview";
import { getStore } from "~/jotai";
import { allClusters } from "~/store/allClusters";
import { mockSubscribeState } from "~/test/mockSubscribe";
Expand All @@ -20,11 +22,62 @@ import { renderComponent, RenderWithPathname } from "~/test/utils";
import ClusterDetail from "./ClusterDetail";
import { buildClusterServerResponse } from "./clustersTestUtils";

const flags: Record<string, boolean> = {};

vi.mock("~/hooks/useFlags", () => ({
useFlags: () => flags,
}));

vi.mock("~/platform/clusters/ClusterOverview", () => ({
default: function () {
return <div>ClusterOverview component</div>;
},
}));
vi.mock("~/platform/clusters/ClusterReplicas", () => ({
default: () => <div>ClusterReplicas component</div>,
}));
vi.mock("~/platform/clusters/MaterializedViewsList", () => ({
default: () => <div>MaterializedViewsList component</div>,
}));
vi.mock("~/platform/clusters/IndexList", () => ({
default: () => <div>IndexList component</div>,
}));
vi.mock("~/platform/clusters/Sources", () => ({
default: () => <div>Sources component</div>,
}));
vi.mock("~/platform/clusters/Sinks", () => ({
default: () => <div>Sinks component</div>,
}));

const redesignFlag = UI_PREVIEWS.clusterDetailsRedesign.ldFlag;

const seedClusters = () =>
getStore().set(
allClusters,
mockSubscribeState({
data: [
buildClusterServerResponse({ id: "u1", name: "default" }),
buildClusterServerResponse({ id: "u2", name: "quickstart" }),
],
}),
);

const renderClusterDetail = (initialPath: string) =>
renderComponent(
<RenderWithPathname>
<Routes>
<Route path=":clusterId/:clusterName">
<Route index path="*" element={<ClusterDetail />} />
</Route>
</Routes>
</RenderWithPathname>,
{ initialRouterEntries: [initialPath] },
);

const expectPathname = async (pathname: string) =>
waitFor(() =>
expect(screen.getByTestId("pathname").textContent).toBe(pathname),
);

describe("ClusterRoutes", () => {
it("breadcrumb context menu allows switching clusters", async () => {
Expand Down Expand Up @@ -70,3 +123,211 @@ describe("ClusterRoutes", () => {
expect(screen.getByText("ClusterOverview component")).toBeVisible();
});
});

describe("ClusterDetail redesign preview", () => {
beforeEach(() => {
localStorage.clear();
delete flags[redesignFlag];
seedClusters();
});

describe("when opted in", () => {
beforeEach(() => {
flags[redesignFlag] = true;
localStorage.setItem(
uiPreviewOptInStorageKey("clusterDetailsRedesign"),
"true",
);
});

it("shows the cluster title and the redesigned tabs", async () => {
await renderClusterDetail("/u1/default");

expect(
await screen.findByRole("heading", { level: 1, name: "default" }),
).toBeVisible();
expect(screen.getByText("small")).toBeVisible();
expect(screen.getByText("1 replica")).toBeVisible();
expect(screen.getByRole("link", { name: "Metrics" })).toBeVisible();
expect(screen.getByRole("link", { name: "Objects" })).toBeVisible();
expect(screen.getByRole("link", { name: "Replicas" })).toBeVisible();
expect(
screen.queryByRole("link", { name: "Materialized Views" }),
).not.toBeInTheDocument();
expect(screen.getByText("ClusterOverview component")).toBeVisible();
});

it("tags an unmanaged cluster without replicas", async () => {
getStore().set(
allClusters,
mockSubscribeState({
data: [
buildClusterServerResponse(
{ id: "u3", name: "adhoc", managed: false },
[],
),
],
}),
);
await renderClusterDetail("/u3/adhoc");

expect(
await screen.findByRole("heading", { level: 1, name: "adhoc" }),
).toBeVisible();
expect(screen.getByText("Unmanaged")).toBeVisible();
expect(screen.getByText("No replicas")).toBeVisible();
expect(screen.queryByText("small")).not.toBeInTheDocument();
});

it("opens the Objects tab on its first object type", async () => {
const user = userEvent.setup();
await renderClusterDetail("/u1/default");

await user.click(await screen.findByRole("link", { name: "Objects" }));

await expectPathname("/u1/default/objects/materialized-views");
expect(
await screen.findByText("MaterializedViewsList component"),
).toBeVisible();
});

it("switches object types from the type tabs", async () => {
const user = userEvent.setup();
await renderClusterDetail("/u1/default/objects/materialized-views");

await user.click(await screen.findByRole("tab", { name: "Indexes" }));

await expectPathname("/u1/default/objects/indexes");
expect(await screen.findByText("IndexList component")).toBeVisible();
expect(
screen.queryByText("MaterializedViewsList component"),
).not.toBeInTheDocument();
});

it("returns to Metrics from a nested Objects URL", async () => {
const user = userEvent.setup();
await renderClusterDetail("/u1/default/objects/indexes");

await user.click(await screen.findByRole("link", { name: "Metrics" }));

await expectPathname("/u1/default");
expect(
await screen.findByText("ClusterOverview component"),
).toBeVisible();
});

it("redirects classic tab URLs into the Objects tab", async () => {
await renderClusterDetail("/u1/default/sinks");

await expectPathname("/u1/default/objects/sinks");
expect(await screen.findByText("Sinks component")).toBeVisible();
});

it("redirects an unknown object type to the first type", async () => {
await renderClusterDetail("/u1/default/objects/tables");

await expectPathname("/u1/default/objects/materialized-views");
});

it("keeps the current tab when switching clusters", async () => {
const user = userEvent.setup();
await renderClusterDetail("/u1/default/objects/indexes");

await user.click(
await screen.findByRole("button", { name: "Navigation actions" }),
);
await user.click(
await screen.findByRole("menuitem", { name: "quickstart" }),
);

await expectPathname("/u2/quickstart/objects/indexes");
expect(
await screen.findByRole("heading", { level: 1, name: "quickstart" }),
).toBeVisible();
});

it("switches back to the classic page", async () => {
const user = userEvent.setup();
await renderClusterDetail("/u1/default");

await user.click(
await screen.findByRole("button", {
name: /Show classic cluster details page/,
}),
);

expect(
await screen.findByRole("link", { name: "Materialized Views" }),
).toBeVisible();
expect(
screen.queryByRole("heading", { level: 1, name: "default" }),
).not.toBeInTheDocument();
});

it("keeps keyboard focus on the preview pill when it switches pages", async () => {
const user = userEvent.setup();
await renderClusterDetail("/u1/default");

(
await screen.findByRole("button", {
name: /Show classic cluster details page/,
})
).focus();
await user.keyboard("{Enter}");

expect(
await screen.findByRole("button", {
name: /Show the new cluster details page/,
}),
).toHaveFocus();
});
});

describe("when not opted in", () => {
it("offers the preview on the classic page when the flag is on", async () => {
flags[redesignFlag] = true;
await renderClusterDetail("/u1/default");

expect(
await screen.findByRole("button", {
name: /Show the new cluster details page/,
}),
).toBeVisible();
expect(screen.getByRole("link", { name: "Overview" })).toBeVisible();
});

it("opens the matching redesigned tab when opting in from a classic tab URL", async () => {
flags[redesignFlag] = true;
const user = userEvent.setup();
await renderClusterDetail("/u1/default/indexes");

await user.click(
await screen.findByRole("button", {
name: /Show the new cluster details page/,
}),
);

await expectPathname("/u1/default/objects/indexes");
expect(await screen.findByText("IndexList component")).toBeVisible();
});

it("sends an unknown object type to the classic overview", async () => {
await renderClusterDetail("/u1/default/objects/tables");

await expectPathname("/u1/default");
});

it("opens the first classic object tab from a redesigned Objects tab link", async () => {
await renderClusterDetail("/u1/default/objects");

await expectPathname("/u1/default/materialized-views");
});

it("redirects redesigned Objects URLs to the classic tabs", async () => {
await renderClusterDetail("/u1/default/objects/sinks");

await expectPathname("/u1/default/sinks");
expect(await screen.findByText("Sinks component")).toBeVisible();
});
});
});
Loading
Loading