Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
45 commits
Select commit Hold shift + click to select a range
f95032d
console: design doc for dataflow visualizer rebuild
antiguru Jul 8, 2026
065ea7e
console: address spec review for dataflow visualizer rebuild
antiguru Jul 8, 2026
50f5913
console: implementation plan for dataflow visualizer rebuild
antiguru Jul 8, 2026
de1a83e
Add @xyflow/react and elkjs, and the dataflow graph model
antiguru Jul 8, 2026
67f6b71
Derive the visible dataflow graph from collapse state, add the elk la…
antiguru Jul 8, 2026
0981d7d
console: add dataflow graph data hook keyed by dataflow id
antiguru Jul 8, 2026
b498ac5
console: React Flow dataflow graph view with collapsible regions
antiguru Jul 8, 2026
b8d6cc0
Add the dataflow detail page behind a flag, with phase-1 gate fixes
antiguru Jul 8, 2026
f5b0c6e
Add the cluster dataflows list page, deep-link, and manual refresh
antiguru Jul 8, 2026
7e96109
console: node detail panel for dataflow visualizer
antiguru Jul 8, 2026
4d9c4f6
console: dataflow visualizer filters and toolbar
antiguru Jul 8, 2026
be28c7a
Add the LIR operator panel, error states, and CNS-109 regression test
antiguru Jul 8, 2026
b9eb340
console: delete graphviz dataflow visualizer and d3-graphviz dep
antiguru Jul 8, 2026
e22e343
Drop dangling channels and route external-to-region channels through …
antiguru Jul 8, 2026
5d996c0
console: reroute connectivity around hidden idle runs
antiguru Jul 8, 2026
a774b10
Add click-to-inspect for edges/ports and a dataflow switcher
antiguru Jul 8, 2026
5e8cd45
Render the LIR panel as a navigable tree, link entries to their opera…
antiguru Jul 8, 2026
918a5da
console: fit all LIR members into view, sticky multi-pin, per-LIR sum…
antiguru Jul 8, 2026
04aa303
console: peer-port jump, skew heatmap, layout and drill-down fixes
antiguru Jul 8, 2026
559b8f6
console: drop dataflow visualizer implementation plan doc
antiguru Jul 8, 2026
090d050
console: update dataflow visualizer design doc for drill-down + skew
antiguru Jul 8, 2026
9338185
Fix review findings: empty-dataflow crash, stale search index, dead V…
antiguru Jul 8, 2026
90a433f
Put drill-down scope in the URL, recenter on empty canvas, minimap st…
antiguru Jul 8, 2026
d268cb7
console: design doc for LIR grouping boxes
antiguru Jul 8, 2026
2389f99
console: implementation plan for LIR grouping boxes
antiguru Jul 8, 2026
dc36de5
Add LIR grouping: groupByLir, elk compound nesting, LirGroupNode comp…
antiguru Jul 8, 2026
25553fe
Wire LIR grouping into the view, add toolbar toggle and summary-click…
antiguru Jul 8, 2026
a00bef2
Fix skew heatmap: log2 domain over real worker count, MAX rollup, has…
antiguru Jul 8, 2026
0dfdbcb
Add edge-to-endpoint jump navigation, fix port-keyed landings at deep…
antiguru Jul 8, 2026
206bb5a
Add collapsible LIR/detail panels, link node LIR ids to their group, …
antiguru Jul 8, 2026
5dee52f
Put selection in the URL alongside scope, and fit the viewport on his…
antiguru Jul 8, 2026
10e8e98
Add schedule count/skew metric and per-region overhead
antiguru Jul 8, 2026
da45de9
Add schedules heatmap, unfiltered own skew, and a region's own stats …
antiguru Jul 8, 2026
5f4fc14
Remove superseded LIR-grouping-boxes design doc and plan
antiguru Jul 8, 2026
f4f9fe1
dataflowGraph: replace non-null assertions with mustGet, tidy doc com…
antiguru Jul 8, 2026
a885f91
clusters: narrow props.cluster once instead of asserting it at each use
antiguru Jul 8, 2026
94c263a
dataflows: read colors through the theme instead of hardcoded values
antiguru Jul 8, 2026
4e2df5c
dataflows: reuse TextLink and the shared Alert component
antiguru Jul 8, 2026
984f5a4
NodeDetailPanel: reuse TextLink/icons, extract named prop interfaces
antiguru Jul 8, 2026
2c204f7
Extract useLastGoodByKey, dedup the last-good-result pattern
antiguru Jul 8, 2026
4492aa3
Disable skew heatmap options on single-worker replicas
antiguru Jul 8, 2026
16651b8
Add isInsufficientPrivilegeError, use it for the dataflows permission…
antiguru Jul 8, 2026
6002926
DataflowDetailPage: wire up mustGet, isInsufficientPrivilegeError, wo…
antiguru Jul 8, 2026
10aa5c2
Align dataflow node/panel styling with the workflow graph's visual la…
antiguru Jul 8, 2026
b26299d
Fix unreadable node text on dark hashed palette colors
antiguru Jul 8, 2026
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
262 changes: 262 additions & 0 deletions console/doc/design/20260706_dataflow_visualizer_rebuild.md

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions console/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,6 @@
"@tanstack/react-query-devtools": "^5.95.0",
"@tanstack/react-table": "^8.21.3",
"@types/d3": "^7.4.3",
"@types/d3-graphviz": "^2.6.10",
"@types/debug": "^4.1.12",
"@types/react-table": "^7.7.20",
"@visx/axis": "^3.12.0",
Expand All @@ -72,16 +71,17 @@
"@visx/shape": "^3.12.0",
"@visx/tooltip": "^3.12.0",
"@xstate/fsm": "^2.1.0",
"@xyflow/react": "^12.11.1",
"base32-encoding": "^1.0.0",
"buffer": "^6.0.3",
"codemirror": "^6.0.1",
"copy-to-clipboard": "^3.3.3",
"d3": "^7.9.0",
"d3-graphviz": "^5.6.0",
"d3-scale-chromatic": "^3.1.0",
"date-fns": "^4.1.0",
"date-fns-tz": "^3.2.0",
"downshift": "^9.0.8",
"elkjs": "^0.11.1",
"fast-deep-equal": "^3.1.3",
"framer-motion": "^12.38.0",
"jotai": "^2.9.3",
Expand Down
176 changes: 176 additions & 0 deletions console/src/api/materialize/dataflow/useDataflowGraphData.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
// Copyright Materialize, Inc. and contributors. All rights reserved.
//
// Use of this software is governed by the Business Source License
// included in the LICENSE file.
//
// As of the Change Date specified in that file, in accordance with
// the Business Source License, use of this software will be governed
// by the Apache License, Version 2.0.

import { renderHook, waitFor } from "@testing-library/react";
import { http, HttpResponse } from "msw";

import { ErrorCode } from "~/api/materialize/types";
import server from "~/api/mocks/server";
import { createProviderWrapper } from "~/test/utils";

import { useDataflowGraphData } from "./useDataflowGraphData";

const FAILING_REPLICA = "r2";
const okResult = {
desc: { columns: [] },
rows: [],
};
// operators result with one root row so buildDataflowStructure succeeds
const operatorsResult = {
desc: {
columns: [
{ name: "id" },
{ name: "address" },
{ name: "name" },
{ name: "arrangementRecords" },
{ name: "arrangementSize" },
{ name: "elapsedNs" },
],
},
rows: [["10", ["7"], "Dataflow", "0", "0", "0"]],
};

beforeEach(() => {
server.use(
http.post("*/api/sql", async ({ request }) => {
const options = JSON.parse(
new URL(request.url).searchParams.get("options") ?? "{}",
);
if (options.cluster_replica === FAILING_REPLICA) {
return HttpResponse.json({
results: [
{
error: {
message: "no such replica",
code: ErrorCode.INTERNAL_ERROR,
},
},
],
});
}
return HttpResponse.json({
results: [operatorsResult, okResult, okResult, okResult, okResult],
});
}),
);
});

describe("useDataflowGraphData", () => {
it("clears data when a refetch for new params fails", async () => {
// The hook reads the current environment off a jotai atom that resolves
// asynchronously, so it needs the same Suspense + JotaiProvider wrapper
// the app renders under. Without it the atom never resolves against the
// fake environment set up in vitest.setup.ts and the query never fires.
const Wrapper = await createProviderWrapper();
const { result, rerender } = renderHook(
(params) => useDataflowGraphData(params),
{
wrapper: Wrapper,
initialProps: {
clusterName: "c",
replicaName: "r1",
dataflowId: "7",
} as
| { clusterName: string; replicaName: string; dataflowId: string }
| undefined,
},
);
await waitFor(() => expect(result.current.data).not.toBeNull());

rerender({
clusterName: "c",
replicaName: FAILING_REPLICA,
dataflowId: "7",
});
await waitFor(() => expect(result.current.error).toBeTruthy());
// The retained previous result must not surface for the new params.
expect(result.current.data).toBeNull();
});

it("hides the previous graph while a new selection is loading", async () => {
const Wrapper = await createProviderWrapper();
const { result, rerender } = renderHook(
(params) => useDataflowGraphData(params),
{
wrapper: Wrapper,
initialProps: {
clusterName: "c",
replicaName: "r1",
dataflowId: "7",
} as
| { clusterName: string; replicaName: string; dataflowId: string }
| undefined,
},
);
await waitFor(() => expect(result.current.data).not.toBeNull());
const firstStructure = result.current.data?.structure;

// Switch to a different valid dataflow whose fetch also succeeds. The
// previous fetch's results are still resident, so a naive guard would tag
// them with the new key and render the old graph. React flushes the
// render-phase reset synchronously, so data must already be null here.
rerender({ clusterName: "c", replicaName: "r1", dataflowId: "8" });
expect(result.current.data).toBeNull();

await waitFor(() => expect(result.current.data).not.toBeNull());
expect(result.current.data?.structure).not.toBe(firstStructure);
});

it("threads the replica's worker count through from workers * processes", async () => {
server.use(
http.post("*/api/sql", async () => {
return HttpResponse.json({
results: [
operatorsResult,
okResult,
okResult,
okResult,
{
desc: { columns: [{ name: "workerCount" }] },
rows: [["8"]],
},
],
});
}),
);
const Wrapper = await createProviderWrapper();
const { result } = renderHook(
() =>
useDataflowGraphData({
clusterName: "c",
replicaName: "r1",
dataflowId: "7",
}),
{ wrapper: Wrapper },
);
await waitFor(() => expect(result.current.data).not.toBeNull());
// 8, not the 1-worker fallback: proves the query result actually flows
// through, not just that the field exists with its default value.
expect(result.current.data?.workerCount).toEqual(8);
});

it("rejects a non-numeric dataflow id without ever compiling a query for it", async () => {
const Wrapper = await createProviderWrapper();
// A malformed id is only reachable via a hand-edited URL, so this must
// report an error the page can render, not throw and crash to the error
// boundary over a URL typo.
const { result } = renderHook(
() =>
useDataflowGraphData({
clusterName: "c",
replicaName: "r1",
dataflowId: "7; DROP",
}),
{ wrapper: Wrapper },
);
expect(result.current.error).toEqual("invalid dataflow id: 7; DROP");
expect(result.current.loading).toEqual(false);
expect(result.current.data).toBeNull();
});
});
Loading
Loading