Dataflow Visualizer: After successful load, later failed refetch leaves previous graph on screen
The visualizer renders the graph whenever `failedToLoad` is false and `loading` is false. But `useSqlApiRequest` never clears the previous results when a new request starts or fails. On failure it only sets `error/databaseError`. Since `failedToLoad = Boolean(!results && error)`, the retained results keep it false. So after the error, loading goes false, `failedToLoad` stays false, and the component falls through to `<DotViz>` with the old structure. New test to prove: ```diff diff --git a/console/src/platform/clusters/DataflowVisualizer.test.tsx b/console/src/platform/clusters/DataflowVisualizer.test.tsx new file mode 100644 index 0000000000..6342adc692 --- /dev/null +++ b/console/src/platform/clusters/DataflowVisualizer.test.tsx @@ -0,0 +1,98 @@ +// 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 { screen } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; +import { http, HttpResponse } from "msw"; +import React from "react"; +import { Route, Routes } from "react-router-dom"; + +import { Cluster } from "~/api/materialize/cluster/clusterList"; +import { DatabaseObject } from "~/api/materialize/objects"; +import { ErrorCode } from "~/api/materialize/types"; +import server from "~/api/mocks/server"; +import { getStore } from "~/jotai"; +import { allClusters } from "~/store/allClusters"; +import { allObjects } from "~/store/allObjects"; +import { mockSubscribeState } from "~/test/mockSubscribe"; +import { renderComponent } from "~/test/utils"; + +import DataflowVisualizer from "./DataflowVisualizer"; + +// d3-graphviz uses WebAssembly that doesn't run in jsdom. Stub every method to a +// chainable no-op; we assert the error-vs-graph decision, not the rendered SVG. +vi.mock("d3-graphviz", () => { + const gv: any = new Proxy({}, { get: () => () => gv }); + return { graphviz: () => gv }; +}); + +const OBJECT_ID = "u42"; +const CLUSTER_ID = "u5"; +// Selecting this replica makes the refetch fail, standing in for a stale +// replica/cluster/object that no longer exists. +const FAILING_REPLICA = "r2"; + +const testObject = { id: OBJECT_ID, clusterId: CLUSTER_ID } as DatabaseObject; +const testCluster = { + id: CLUSTER_ID, + name: "test_cluster", + replicas: [{ name: "r1" }, { name: FAILING_REPLICA }], +} as Cluster; + +// Empty-but-successful structure for every replica except FAILING_REPLICA, which +// gets a non-permission database error. executeSql puts the replica on the +// request URL as the `cluster_replica` session option. +const dataflowHandler = 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 }, + }, + ], + }); + } + const empty = { desc: { columns: [] }, rows: [] }; + return HttpResponse.json({ results: [empty, empty, empty] }); +}); + +const renderVisualizer = () => + renderComponent( + <Routes> + <Route path=":id" element={<DataflowVisualizer />} /> + </Routes>, + { initialRouterEntries: [`/${OBJECT_ID}`] }, + ); + +describe("DataflowVisualizer", () => { + beforeEach(() => { + server.use(dataflowHandler); + const store = getStore(); + store.set(allObjects, mockSubscribeState({ data: [testObject] })); + store.set(allClusters, mockSubscribeState({ data: [testCluster] })); + }); + + it("shows an error instead of the stale graph when a refetch fails", async () => { + renderVisualizer(); + + // The replica selector only renders once the initial load has succeeded. + const replicaSelect = await screen.findByRole("combobox"); + + // Switch to the failing replica. The retained results keep `failedToLoad` + // false, so before the fix the stale loaded state stays rendered. + await userEvent.selectOptions(replicaSelect, FAILING_REPLICA); + + expect( + await screen.findByText("There was an error visualizing your dataflow"), + ).toBeVisible(); + }); +}); ``` Running `yarn test --run src/platform/clusters/DataflowVisualizer.test.tsx` fails: ``` FAIL src/platform/clusters/DataflowVisualizer.test.tsx > DataflowVisualizer > shows an error instead of the previous graph when a refetch fails TestingLibraryElementError: Unable to find an element with the text: There was an error visualizing your dataflow. This could be because the text is broken up by multiple elements. In this case, you can provide a function for your text matcher to make your matcher more flexible. ``` CC @moritz
No prototypes yet. Click "Generate Fix" to create one.