From 987191a719762e15b9fdc6998f83cd32a2fe9a32 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Tue, 30 Dec 2025 15:17:49 +0000 Subject: [PATCH 1/3] Add FlowIconText component for UI v2 migration Create a new FlowIconText component that displays a flow's icon and name as a clickable link, fetching flow data by ID. This component follows the existing Suspense pattern used in similar link components like FlowLink and DeploymentLink. Files added: - flow-icon-text.tsx: Main component with Suspense wrapper - flow-icon-text.stories.tsx: Storybook stories - flow-icon-text.test.tsx: Unit tests - index.ts: Export file Co-Authored-By: alex.s@prefect.io --- .../flow-icon-text/flow-icon-text.stories.tsx | 40 ++++++++++ .../flow-icon-text/flow-icon-text.test.tsx | 77 +++++++++++++++++++ .../flows/flow-icon-text/flow-icon-text.tsx | 33 ++++++++ .../components/flows/flow-icon-text/index.ts | 1 + 4 files changed, 151 insertions(+) create mode 100644 ui-v2/src/components/flows/flow-icon-text/flow-icon-text.stories.tsx create mode 100644 ui-v2/src/components/flows/flow-icon-text/flow-icon-text.test.tsx create mode 100644 ui-v2/src/components/flows/flow-icon-text/flow-icon-text.tsx create mode 100644 ui-v2/src/components/flows/flow-icon-text/index.ts diff --git a/ui-v2/src/components/flows/flow-icon-text/flow-icon-text.stories.tsx b/ui-v2/src/components/flows/flow-icon-text/flow-icon-text.stories.tsx new file mode 100644 index 000000000000..d3d20c1c49fe --- /dev/null +++ b/ui-v2/src/components/flows/flow-icon-text/flow-icon-text.stories.tsx @@ -0,0 +1,40 @@ +import type { Meta, StoryObj } from "@storybook/react"; +import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; +import { FlowIconText } from "./flow-icon-text"; + +const queryClient = new QueryClient({ + defaultOptions: { + queries: { + retry: false, + }, + }, +}); + +const meta: Meta = { + title: "Components/Flows/FlowIconText", + component: FlowIconText, + decorators: [ + (Story) => ( + + + + ), + ], + parameters: { + docs: { + description: { + component: + "A link component that fetches and displays a flow name with a Workflow icon, linking to the flow detail page.", + }, + }, + }, +}; + +export default meta; +type Story = StoryObj; + +export const Default: Story = { + args: { + flowId: "flow-123", + }, +}; diff --git a/ui-v2/src/components/flows/flow-icon-text/flow-icon-text.test.tsx b/ui-v2/src/components/flows/flow-icon-text/flow-icon-text.test.tsx new file mode 100644 index 000000000000..3bbfe80938cb --- /dev/null +++ b/ui-v2/src/components/flows/flow-icon-text/flow-icon-text.test.tsx @@ -0,0 +1,77 @@ +import { QueryClient } from "@tanstack/react-query"; +import { + createMemoryHistory, + createRootRoute, + createRouter, + RouterProvider, +} from "@tanstack/react-router"; +import { render, screen, waitFor } from "@testing-library/react"; +import { buildApiUrl, createWrapper, server } from "@tests/utils"; +import { HttpResponse, http } from "msw"; +import { Suspense } from "react"; +import { describe, expect, it } from "vitest"; +import { createFakeFlow } from "@/mocks"; +import { FlowIconText } from "./flow-icon-text"; + +const mockFlow = createFakeFlow({ + id: "flow-123", + name: "my-flow", +}); + +type FlowIconTextRouterProps = { + flowId: string; +}; + +const FlowIconTextRouter = ({ flowId }: FlowIconTextRouterProps) => { + const rootRoute = createRootRoute({ + component: () => ( + Loading...}> + + + ), + }); + + const router = createRouter({ + routeTree: rootRoute, + history: createMemoryHistory({ + initialEntries: ["/"], + }), + context: { queryClient: new QueryClient() }, + }); + return ; +}; + +describe("FlowIconText", () => { + it("fetches and displays flow name", async () => { + server.use( + http.get(buildApiUrl("/flows/:id"), () => { + return HttpResponse.json(mockFlow); + }), + ); + + render(, { + wrapper: createWrapper(), + }); + + await waitFor(() => { + expect(screen.getByText("my-flow")).toBeInTheDocument(); + }); + }); + + it("renders a link to the flow detail page", async () => { + server.use( + http.get(buildApiUrl("/flows/:id"), () => { + return HttpResponse.json(mockFlow); + }), + ); + + render(, { + wrapper: createWrapper(), + }); + + await waitFor(() => { + const link = screen.getByRole("link"); + expect(link).toHaveAttribute("href", "/flows/flow/flow-123"); + }); + }); +}); diff --git a/ui-v2/src/components/flows/flow-icon-text/flow-icon-text.tsx b/ui-v2/src/components/flows/flow-icon-text/flow-icon-text.tsx new file mode 100644 index 000000000000..4889bbcb7d5a --- /dev/null +++ b/ui-v2/src/components/flows/flow-icon-text/flow-icon-text.tsx @@ -0,0 +1,33 @@ +import { useSuspenseQuery } from "@tanstack/react-query"; +import { Link } from "@tanstack/react-router"; +import { Suspense } from "react"; +import { buildFLowDetailsQuery } from "@/api/flows"; +import { Icon } from "@/components/ui/icons"; +import { Skeleton } from "@/components/ui/skeleton"; + +type FlowIconTextProps = { + flowId: string; +}; + +export const FlowIconText = ({ flowId }: FlowIconTextProps) => { + return ( + }> + + + ); +}; + +const FlowIconTextImplementation = ({ flowId }: FlowIconTextProps) => { + const { data: flow } = useSuspenseQuery(buildFLowDetailsQuery(flowId)); + + return ( + + + {flow.name} + + ); +}; diff --git a/ui-v2/src/components/flows/flow-icon-text/index.ts b/ui-v2/src/components/flows/flow-icon-text/index.ts new file mode 100644 index 000000000000..93fe09f346ab --- /dev/null +++ b/ui-v2/src/components/flows/flow-icon-text/index.ts @@ -0,0 +1 @@ +export { FlowIconText } from "./flow-icon-text"; From 5d6b31cf94c513328f221439568ec699e7e1d28f Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Tue, 30 Dec 2025 15:24:16 +0000 Subject: [PATCH 2/3] Fix FlowIconText story to include RouterProvider decorator Add RouterProvider decorator to the Storybook story so the component renders correctly in Storybook. The Link component from TanStack Router requires a router context to function properly. Co-Authored-By: alex.s@prefect.io --- .../flow-icon-text/flow-icon-text.stories.tsx | 36 ++++++++++++++++--- 1 file changed, 31 insertions(+), 5 deletions(-) diff --git a/ui-v2/src/components/flows/flow-icon-text/flow-icon-text.stories.tsx b/ui-v2/src/components/flows/flow-icon-text/flow-icon-text.stories.tsx index d3d20c1c49fe..29221b1e35c6 100644 --- a/ui-v2/src/components/flows/flow-icon-text/flow-icon-text.stories.tsx +++ b/ui-v2/src/components/flows/flow-icon-text/flow-icon-text.stories.tsx @@ -1,5 +1,12 @@ import type { Meta, StoryObj } from "@storybook/react"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; +import { + createMemoryHistory, + createRootRoute, + createRouter, + RouterProvider, +} from "@tanstack/react-router"; +import { Suspense } from "react"; import { FlowIconText } from "./flow-icon-text"; const queryClient = new QueryClient({ @@ -10,15 +17,34 @@ const queryClient = new QueryClient({ }, }); +const createTestRouter = (flowId: string) => { + const rootRoute = createRootRoute({ + component: () => ( + Loading...}> + + + ), + }); + + return createRouter({ + routeTree: rootRoute, + history: createMemoryHistory({ initialEntries: ["/"] }), + context: { queryClient }, + }); +}; + const meta: Meta = { title: "Components/Flows/FlowIconText", component: FlowIconText, decorators: [ - (Story) => ( - - - - ), + (_Story, context) => { + const router = createTestRouter(context.args.flowId ?? "flow-123"); + return ( + + + + ); + }, ], parameters: { docs: { From d7af125d6f9757dff85349d7dc04bfc0ee7cd506 Mon Sep 17 00:00:00 2001 From: tomerqodo Date: Sun, 25 Jan 2026 12:10:38 +0200 Subject: [PATCH 3/3] update pr --- .../flow-icon-text/flow-icon-text.stories.tsx | 2 +- .../flow-icon-text/flow-icon-text.test.tsx | 77 ------------------- .../flows/flow-icon-text/flow-icon-text.tsx | 2 +- 3 files changed, 2 insertions(+), 79 deletions(-) delete mode 100644 ui-v2/src/components/flows/flow-icon-text/flow-icon-text.test.tsx diff --git a/ui-v2/src/components/flows/flow-icon-text/flow-icon-text.stories.tsx b/ui-v2/src/components/flows/flow-icon-text/flow-icon-text.stories.tsx index 29221b1e35c6..5888f99f3207 100644 --- a/ui-v2/src/components/flows/flow-icon-text/flow-icon-text.stories.tsx +++ b/ui-v2/src/components/flows/flow-icon-text/flow-icon-text.stories.tsx @@ -29,7 +29,7 @@ const createTestRouter = (flowId: string) => { return createRouter({ routeTree: rootRoute, history: createMemoryHistory({ initialEntries: ["/"] }), - context: { queryClient }, + context: { queryClient: new QueryClient() }, }); }; diff --git a/ui-v2/src/components/flows/flow-icon-text/flow-icon-text.test.tsx b/ui-v2/src/components/flows/flow-icon-text/flow-icon-text.test.tsx deleted file mode 100644 index 3bbfe80938cb..000000000000 --- a/ui-v2/src/components/flows/flow-icon-text/flow-icon-text.test.tsx +++ /dev/null @@ -1,77 +0,0 @@ -import { QueryClient } from "@tanstack/react-query"; -import { - createMemoryHistory, - createRootRoute, - createRouter, - RouterProvider, -} from "@tanstack/react-router"; -import { render, screen, waitFor } from "@testing-library/react"; -import { buildApiUrl, createWrapper, server } from "@tests/utils"; -import { HttpResponse, http } from "msw"; -import { Suspense } from "react"; -import { describe, expect, it } from "vitest"; -import { createFakeFlow } from "@/mocks"; -import { FlowIconText } from "./flow-icon-text"; - -const mockFlow = createFakeFlow({ - id: "flow-123", - name: "my-flow", -}); - -type FlowIconTextRouterProps = { - flowId: string; -}; - -const FlowIconTextRouter = ({ flowId }: FlowIconTextRouterProps) => { - const rootRoute = createRootRoute({ - component: () => ( - Loading...}> - - - ), - }); - - const router = createRouter({ - routeTree: rootRoute, - history: createMemoryHistory({ - initialEntries: ["/"], - }), - context: { queryClient: new QueryClient() }, - }); - return ; -}; - -describe("FlowIconText", () => { - it("fetches and displays flow name", async () => { - server.use( - http.get(buildApiUrl("/flows/:id"), () => { - return HttpResponse.json(mockFlow); - }), - ); - - render(, { - wrapper: createWrapper(), - }); - - await waitFor(() => { - expect(screen.getByText("my-flow")).toBeInTheDocument(); - }); - }); - - it("renders a link to the flow detail page", async () => { - server.use( - http.get(buildApiUrl("/flows/:id"), () => { - return HttpResponse.json(mockFlow); - }), - ); - - render(, { - wrapper: createWrapper(), - }); - - await waitFor(() => { - const link = screen.getByRole("link"); - expect(link).toHaveAttribute("href", "/flows/flow/flow-123"); - }); - }); -}); diff --git a/ui-v2/src/components/flows/flow-icon-text/flow-icon-text.tsx b/ui-v2/src/components/flows/flow-icon-text/flow-icon-text.tsx index 4889bbcb7d5a..6284a101d2cb 100644 --- a/ui-v2/src/components/flows/flow-icon-text/flow-icon-text.tsx +++ b/ui-v2/src/components/flows/flow-icon-text/flow-icon-text.tsx @@ -11,7 +11,7 @@ type FlowIconTextProps = { export const FlowIconText = ({ flowId }: FlowIconTextProps) => { return ( - }> + );