From 7b96e87dff33eb3885ad35fbd8fd2282f3f20d2f Mon Sep 17 00:00:00 2001 From: Salomon Popp Date: Wed, 24 Jun 2026 17:11:55 +0200 Subject: [PATCH 01/10] refactor: migrate restful-react to orval + tanstack-query --- frontend/__mocks__/fileMock.js | 1 + frontend/__mocks__/styleMock.js | 1 + frontend/__tests__/App.test.tsx | 32 +- frontend/__tests__/Details.test.tsx | 59 +- frontend/__tests__/DetailsCard.test.tsx | 17 +- frontend/__tests__/Search.test.tsx | 13 +- frontend/components/App.tsx | 96 +- frontend/components/Details.tsx | 32 +- frontend/components/Schema.tsx | 38 +- frontend/components/Search.tsx | 2 +- frontend/components/api/fetchers.tsx | 271 - frontend/components/graph/Visualization.tsx | 2 +- frontend/jest.setup.js | 4 + frontend/lib/api/fetchers.ts | 866 ++ frontend/lib/api/model/edge.ts | 11 + ...getLinkingApiNodeLinkingNodeIdGetParams.ts | 10 + ...eSchemaApiNodeNodeIdSchemaVersionGet200.ts | 8 + .../getPositionedGraphApiGraphGetParams.ts | 10 + frontend/lib/api/model/graph.ts | 16 + frontend/lib/api/model/hTTPValidationError.ts | 11 + frontend/lib/api/model/index.ts | 22 + frontend/lib/api/model/metric.ts | 18 + frontend/lib/api/model/node.ts | 14 + frontend/lib/api/model/nodeInfoListItem.ts | 14 + .../lib/api/model/nodeInfoListItemValue.ts | 8 + frontend/lib/api/model/nodeInfoType.ts | 18 + frontend/lib/api/model/nodeInformation.ts | 14 + frontend/lib/api/model/nodeTypesEnum.ts | 20 + frontend/lib/api/model/pipelines.ts | 10 + frontend/lib/api/model/validationError.ts | 12 + frontend/orval.config.ts | 14 + frontend/package-lock.json | 11066 ++++++---------- frontend/package.json | 6 +- frontend/pages/_app.tsx | 9 +- frontend/tsconfig.json | 2 +- 35 files changed, 5148 insertions(+), 7599 deletions(-) create mode 100644 frontend/__mocks__/fileMock.js create mode 100644 frontend/__mocks__/styleMock.js delete mode 100644 frontend/components/api/fetchers.tsx create mode 100644 frontend/lib/api/fetchers.ts create mode 100644 frontend/lib/api/model/edge.ts create mode 100644 frontend/lib/api/model/getLinkingApiNodeLinkingNodeIdGetParams.ts create mode 100644 frontend/lib/api/model/getNodeSchemaApiNodeNodeIdSchemaVersionGet200.ts create mode 100644 frontend/lib/api/model/getPositionedGraphApiGraphGetParams.ts create mode 100644 frontend/lib/api/model/graph.ts create mode 100644 frontend/lib/api/model/hTTPValidationError.ts create mode 100644 frontend/lib/api/model/index.ts create mode 100644 frontend/lib/api/model/metric.ts create mode 100644 frontend/lib/api/model/node.ts create mode 100644 frontend/lib/api/model/nodeInfoListItem.ts create mode 100644 frontend/lib/api/model/nodeInfoListItemValue.ts create mode 100644 frontend/lib/api/model/nodeInfoType.ts create mode 100644 frontend/lib/api/model/nodeInformation.ts create mode 100644 frontend/lib/api/model/nodeTypesEnum.ts create mode 100644 frontend/lib/api/model/pipelines.ts create mode 100644 frontend/lib/api/model/validationError.ts create mode 100644 frontend/orval.config.ts diff --git a/frontend/__mocks__/fileMock.js b/frontend/__mocks__/fileMock.js new file mode 100644 index 00000000..0a445d06 --- /dev/null +++ b/frontend/__mocks__/fileMock.js @@ -0,0 +1 @@ +module.exports = "test-file-stub"; diff --git a/frontend/__mocks__/styleMock.js b/frontend/__mocks__/styleMock.js new file mode 100644 index 00000000..f053ebf7 --- /dev/null +++ b/frontend/__mocks__/styleMock.js @@ -0,0 +1 @@ +module.exports = {}; diff --git a/frontend/__tests__/App.test.tsx b/frontend/__tests__/App.test.tsx index e43f88a2..1d296c1d 100644 --- a/frontend/__tests__/App.test.tsx +++ b/frontend/__tests__/App.test.tsx @@ -1,3 +1,4 @@ +import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import { act, fireEvent, @@ -11,6 +12,16 @@ import nock from "nock"; import React from "react"; import App from "../components/App"; +const renderWithClient = (ui: React.ReactElement) => { + const queryClient = new QueryClient({ + defaultOptions: { queries: { retry: false } }, + }); + const Wrapper = ({ children }: { children: React.ReactNode }) => ( + {children} + ); + return render(ui, { wrapper: Wrapper }); +}; + jest.mock("next/router", () => require("next-router-mock")); // -- Mock GraphVisualization @@ -82,7 +93,7 @@ describe("Streams Explorer", () => { describe("renders", () => { mockBackendGraph(true); it("without crashing", async () => { - const { findByTestId } = render(); + const { findByTestId } = renderWithClient(); await findByTestId("graph"); }); }); @@ -128,7 +139,7 @@ describe("Streams Explorer", () => { it("should set pipeline from url parameter", async () => { mockRouter.setCurrentUrl("/?pipeline=test-pipeline"); - const { getByTestId, findByTestId, asFragment } = render(); + const { getByTestId, findByTestId, asFragment } = renderWithClient(); expect(singletonRouter).toMatchObject({ asPath: "/?pipeline=test-pipeline", @@ -161,7 +172,7 @@ describe("Streams Explorer", () => { info: [], }); - const { findByTestId, getByTestId } = render(); + const { findByTestId, getByTestId } = renderWithClient(); expect(singletonRouter).toMatchObject({ asPath: "/?focus-node=test-app", @@ -185,7 +196,7 @@ describe("Streams Explorer", () => { it("should render without url parameters", async () => { mockRouter.setCurrentUrl("/"); - const { getByTestId, findByTestId } = render(); + const { getByTestId, findByTestId } = renderWithClient(); expect(singletonRouter).toMatchObject({ asPath: "/", @@ -209,9 +220,8 @@ describe("Streams Explorer", () => { mockRouter.setCurrentUrl("/?pipeline=test-pipeline"); // render App - const { getByTestId, getByText, findByTestId, findAllByTestId } = render( - - ); + const { getByTestId, getByText, findByTestId, findAllByTestId } = + renderWithClient(); await findByTestId("graph"); const nodeSelect = getByTestId("node-select"); @@ -311,7 +321,7 @@ describe("Streams Explorer", () => { mockRouter.setCurrentUrl("/?pipeline=doesnt-exist"); - const { findByTestId } = render(); + const { findByTestId } = renderWithClient(); expect(singletonRouter).toMatchObject({ asPath: "/?pipeline=doesnt-exist", @@ -350,7 +360,7 @@ describe("Streams Explorer", () => { mockRouter.setCurrentUrl("/?pipeline=avail-after-scrape"); - const { getByTestId, findByTestId } = render(); + const { getByTestId, findByTestId } = renderWithClient(); expect(singletonRouter.asPath).toBe("/?pipeline=avail-after-scrape"); @@ -378,7 +388,7 @@ describe("Streams Explorer", () => { it("should persist metrics refresh interval across page reloads", async () => { mockBackendGraph(true); - const { findByText, findByTestId, rerender } = render(); + const { findByText, findByTestId, rerender } = renderWithClient(); await findByTestId("graph"); @@ -414,7 +424,7 @@ describe("Streams Explorer", () => { // set metrics refresh interval to 'off' window.localStorage.setItem("metrics-interval", "0"); - const { findByTestId, getByText } = render(); + const { findByTestId, getByText } = renderWithClient(); await findByTestId("graph"); diff --git a/frontend/__tests__/Details.test.tsx b/frontend/__tests__/Details.test.tsx index 9120461a..a4a090d6 100644 --- a/frontend/__tests__/Details.test.tsx +++ b/frontend/__tests__/Details.test.tsx @@ -1,6 +1,6 @@ +import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import nock from "nock"; import React from "react"; -import { RestfulProvider } from "restful-react"; import { fireEvent, @@ -10,6 +10,15 @@ import { } from "@testing-library/react"; import Details from "../components/Details"; +const renderWithClient = (ui: React.ReactElement) => { + const queryClient = new QueryClient({ + defaultOptions: { queries: { retry: false } }, + }); + return render( + {ui} + ); +}; + describe("display node information", () => { beforeAll(() => { Object.defineProperty(window, "matchMedia", { @@ -32,10 +41,8 @@ describe("display node information", () => { detail: 'Could not find information for node with name "fake-node"', }); - const { findByTestId, asFragment } = render( - -
- + const { findByTestId, asFragment } = renderWithClient( +
); await findByTestId("no-node-info"); @@ -49,10 +56,8 @@ describe("display node information", () => { info: [], }); - const { findByText, asFragment } = render( - -
- + const { findByText, asFragment } = renderWithClient( +
); await findByText("connector"); @@ -66,10 +71,8 @@ describe("display node information", () => { info: [], }); - const { findByText, asFragment } = render( - -
- + const { findByText, asFragment } = renderWithClient( +
); await findByText("connector"); @@ -105,10 +108,8 @@ describe("display node information", () => { 200, "http://localhost:5601/app/kibana#/discover?_a=(columns:!(_source),query:(language:lucene,query:'kubernetes.labels.app:%20%22atm-fraud-transactionavroproducer%22'))" ); - const { findByText, asFragment, queryByText } = render( - -
- + const { findByText, asFragment, queryByText } = renderWithClient( +
); await findByText("streaming-app"); @@ -199,10 +200,8 @@ describe("display node information", () => { "http://localhost:3000/d/path/to/dashboard?var-topics=atm-fraud-incoming-transactions-topic" ); - const { getByText, findByText, getByTestId } = render( - -
- + const { getByText, findByText, getByTestId } = renderWithClient( +
); await findByText("v2"); // get dropdown menu for schema version @@ -251,10 +250,8 @@ describe("display node information", () => { .get("/api/node/atm-fraud-incoming-transactions-topic/schema") .reply(404); - const { findByTestId } = render( - -
- + const { findByTestId } = renderWithClient( +
); await findByTestId("no-schema-versions"); @@ -279,10 +276,8 @@ describe("display node information", () => { .get("/api/node/atm-fraud-incoming-transactions-topic/schema") .reply(200, []); - const { findByTestId } = render( - -
- + const { findByTestId } = renderWithClient( +
); await findByTestId("no-schema-versions"); @@ -311,10 +306,8 @@ describe("display node information", () => { .get("/api/node/atm-fraud-incoming-transactions-topic/schema/1") .reply(404); - const { findByTestId } = render( - -
- + const { findByTestId } = renderWithClient( +
); await findByTestId("no-schema"); diff --git a/frontend/__tests__/DetailsCard.test.tsx b/frontend/__tests__/DetailsCard.test.tsx index 065e5c77..a07b8009 100644 --- a/frontend/__tests__/DetailsCard.test.tsx +++ b/frontend/__tests__/DetailsCard.test.tsx @@ -1,10 +1,19 @@ +import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import { render } from "@testing-library/react"; import nock from "nock"; import React from "react"; -import { RestfulProvider } from "restful-react"; import DetailsCard from "../components/DetailsCard"; import Node from "../components/graph/Node"; +const renderWithClient = (ui: React.ReactElement) => { + const queryClient = new QueryClient({ + defaultOptions: { queries: { retry: false } }, + }); + return render( + {ui} + ); +}; + describe("display card for node details", () => { beforeAll(() => { Object.defineProperty(window, "matchMedia", { @@ -37,11 +46,7 @@ describe("display card for node details", () => { info: [], }); - const { queryByText } = render( - - - - ); + const { queryByText } = renderWithClient(); expect(queryByText("test-app-name - Details")).toBeInTheDocument(); }); diff --git a/frontend/__tests__/Search.test.tsx b/frontend/__tests__/Search.test.tsx index 809fdd7a..33303ad8 100644 --- a/frontend/__tests__/Search.test.tsx +++ b/frontend/__tests__/Search.test.tsx @@ -1,8 +1,19 @@ +import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import { fireEvent, render, within } from "@testing-library/react"; import nock from "nock"; import React from "react"; import App from "../components/App"; +const renderWithClient = (ui: React.ReactElement) => { + const queryClient = new QueryClient({ + defaultOptions: { queries: { retry: false } }, + }); + const Wrapper = ({ children }: { children: React.ReactNode }) => ( + {children} + ); + return render(ui, { wrapper: Wrapper }); +}; + jest.mock("next/router", () => require("next-router-mock")); // -- Mock GraphVisualization @@ -81,7 +92,7 @@ describe("Search", () => { it("node icons", async () => { // render App - const { getByTestId, findByTestId, findAllByTestId } = render(); + const { getByTestId, findByTestId, findAllByTestId } = renderWithClient(); await findByTestId("graph"); const nodeSelect = getByTestId("node-select"); diff --git a/frontend/components/App.tsx b/frontend/components/App.tsx index eeec143a..6676fe52 100644 --- a/frontend/components/App.tsx +++ b/frontend/components/App.tsx @@ -12,13 +12,14 @@ import { import { useRouter } from "next/router"; import React, { useCallback, useEffect, useRef, useState } from "react"; import { useResizeDetector } from "react-resize-detector"; -import { useMutate } from "restful-react"; import { - HTTPValidationError, + getPositionedGraphApiGraphGetResponse200, useGetMetricsApiMetricsGet, useGetPipelinesApiPipelinesGet, useGetPositionedGraphApiGraphGet, -} from "./api/fetchers"; + useUpdateApiUpdatePost, +} from "../lib/api/fetchers"; +import { HTTPValidationError } from "../lib/api/model"; import DetailsCard from "./DetailsCard"; import Node from "./graph/Node"; import GraphVisualization from "./graph/Visualization"; @@ -68,43 +69,56 @@ const App: React.FC = () => { localStorage.setItem(REFRESH_INTERVAL, refreshInterval.toString()); }, [refreshInterval]); - const { mutate: update, loading: isUpdating } = useMutate({ - verb: "POST", - path: "/api/update", - }); + const { mutate: updateMutate, isLoading: isUpdating } = + useUpdateApiUpdatePost(); + + const update = () => updateMutate(); const { - data: graph, - loading: isLoadingGraph, + data: graphResponse, + isLoading: isLoadingGraph, error: graphError, refetch: graphRefetch, - } = useGetPositionedGraphApiGraphGet({ - queryParams: currentPipeline !== ALL_PIPELINES + } = useGetPositionedGraphApiGraphGet( + currentPipeline !== ALL_PIPELINES ? { pipeline_name: currentPipeline } - : undefined, - }); + : undefined + ); + const graph = + graphResponse?.status === 200 + ? (graphResponse as getPositionedGraphApiGraphGetResponse200).data + : undefined; const { refetch: retryPipelineGraph, error: retryPipelineGraphError, - data: retryPipelineGraphData, - } = useGetPositionedGraphApiGraphGet({ - queryParams: { pipeline_name: currentPipeline }, - lazy: true, - }); + data: retryPipelineGraphResponse, + } = useGetPositionedGraphApiGraphGet( + { pipeline_name: currentPipeline }, + { query: { enabled: false } } + ); + const retryPipelineGraphData = + retryPipelineGraphResponse?.status === 200 + ? (retryPipelineGraphResponse as getPositionedGraphApiGraphGetResponse200) + .data + : undefined; const { - data: pipelines, - loading: isLoadingPipelines, + data: pipelinesResponse, + isLoading: isLoadingPipelines, error: pipelineError, - } = useGetPipelinesApiPipelinesGet({}); + } = useGetPipelinesApiPipelinesGet(); + const pipelines = + pipelinesResponse?.status === 200 ? pipelinesResponse.data : undefined; const { - data: metrics, - loading: isLoadingMetrics, + data: metricsResponse, + isLoading: isLoadingMetrics, refetch: refetchMetrics, error: metricsError, - } = useGetMetricsApiMetricsGet({ lazy: true }); + } = useGetMetricsApiMetricsGet({ query: { enabled: false } }); + const metrics = + metricsResponse?.status === 200 ? metricsResponse.data : undefined; useEffect(() => { if (refreshInterval && refreshInterval > 0) { @@ -134,28 +148,29 @@ const App: React.FC = () => { useEffect(() => { if (graphError) { let errorMessage: string | undefined; - if ("data" in graphError) { - // specific pipeline was not found - const data = graphError["data"] as HTTPValidationError; + const err = graphError as any; + if (err?.data) { + const data = err.data as HTTPValidationError; if (data.detail) { errorMessage = data.detail.toString(); } } message.error(errorMessage || "Failed loading graph", 5); - if (graphError.status === 404 && currentPipeline !== ALL_PIPELINES) { + if (err?.status === 404 && currentPipeline !== ALL_PIPELINES) { // check if a re-scrape solves it const hideMessage = message.warning("Refreshing pipelines", 0); - update({}) - .then(() => { + updateMutate(undefined, { + onSuccess: () => { retryPipelineGraph(); - }) - .catch(() => { + }, + onError: () => { redirectAllPipelines(); - }) - .finally(() => { + }, + onSettled: () => { hideMessage(); - }); + }, + }); } } }, [graphError]); // eslint-disable-line react-hooks/exhaustive-deps @@ -163,7 +178,7 @@ const App: React.FC = () => { useEffect(() => { if ( retryPipelineGraphError - && retryPipelineGraphError.status === 404 + && (retryPipelineGraphError as any)?.status === 404 && currentPipeline !== ALL_PIPELINES ) { // pipeline still not found @@ -261,9 +276,10 @@ const App: React.FC = () => { key="3" style={{ float: "right", marginLeft: "auto" }} onClick={() => { - update({}) - .then(() => router.reload()) - .catch(() => message.error("Failed to update!")); + updateMutate(undefined, { + onSuccess: () => router.reload(), + onError: () => message.error("Failed to update!"), + }); }} >