diff --git a/src/components/WorkflowDetail.tsx b/src/components/WorkflowDetail.tsx index c546b7cd..210b185e 100644 --- a/src/components/WorkflowDetail.tsx +++ b/src/components/WorkflowDetail.tsx @@ -126,10 +126,7 @@ export default function WorkflowDetail({ if (!features.workflowQueries) { return (
- +
); } diff --git a/src/components/WorkflowList.stories.tsx b/src/components/WorkflowList.stories.tsx new file mode 100644 index 00000000..16112e89 --- /dev/null +++ b/src/components/WorkflowList.stories.tsx @@ -0,0 +1,109 @@ +import type { WorkflowListItem } from "@services/workflows"; +import type { Meta, StoryObj } from "@storybook/react-vite"; + +import WorkflowList from "./WorkflowList"; + +const meta: Meta = { + component: WorkflowList, + parameters: { + layout: "fullscreen", + router: { + routes: ["/", "/workflows", "/workflows/$workflowId"], + }, + }, + title: "Pages/WorkflowList", +}; + +export default meta; + +type Story = StoryObj; + +const workflow = ( + id: string, + name: string, + createdAt: string, + counts: Partial< + Pick< + WorkflowListItem, + | "countAvailable" + | "countCancelled" + | "countCompleted" + | "countDiscarded" + | "countFailedDeps" + | "countPending" + | "countRetryable" + | "countRunning" + | "countScheduled" + > + >, +): WorkflowListItem => ({ + countAvailable: counts.countAvailable ?? 0, + countCancelled: counts.countCancelled ?? 0, + countCompleted: counts.countCompleted ?? 0, + countDiscarded: counts.countDiscarded ?? 0, + countFailedDeps: counts.countFailedDeps ?? 0, + countPending: counts.countPending ?? 0, + countRetryable: counts.countRetryable ?? 0, + countRunning: counts.countRunning ?? 0, + countScheduled: counts.countScheduled ?? 0, + createdAt: new Date(createdAt), + id, + name, +}); + +const workflows: WorkflowListItem[] = [ + workflow("wf-onboarding-2026-05-01", "Customer onboarding", "2026-05-01", { + countCompleted: 5, + countPending: 2, + countRunning: 1, + }), + workflow("wf-nightly-ledger-close", "Nightly ledger close", "2026-04-29", { + countCompleted: 12, + }), + workflow("wf-import-retry-queue", "Import retry queue", "2026-04-28", { + countCompleted: 8, + countFailedDeps: 1, + }), + workflow( + "wf-backfill-with-a-very-long-identifier-for-layout", + "Long-running historical backfill with a verbose display name", + "2026-04-22", + { + countCompleted: 21, + countPending: 4, + countScheduled: 3, + }, + ), +]; + +export const Loading: Story = { + args: { + loading: true, + workflowItems: [], + workflowQueriesEnabled: true, + }, +}; + +export const Populated: Story = { + args: { + loading: false, + workflowItems: workflows, + workflowQueriesEnabled: true, + }, +}; + +export const NoWorkflows: Story = { + args: { + loading: false, + workflowItems: [], + workflowQueriesEnabled: true, + }, +}; + +export const WorkflowsNotEnabled: Story = { + args: { + loading: false, + workflowItems: [], + workflowQueriesEnabled: false, + }, +}; diff --git a/src/components/WorkflowList.tsx b/src/components/WorkflowList.tsx index fbf63d8f..67b76bb7 100644 --- a/src/components/WorkflowList.tsx +++ b/src/components/WorkflowList.tsx @@ -12,8 +12,8 @@ type StateTab = { name: string; state: undefined | WorkflowState }; type WorkflowListProps = { loading: boolean; - showingAll: boolean; workflowItems: WorkflowListItem[]; + workflowQueriesEnabled: boolean; }; const tabs: StateTab[] = [ { name: "All", state: undefined }, @@ -203,8 +203,8 @@ const WorkflowTable = ({ const WorkflowList = ({ loading, - showingAll, workflowItems, + workflowQueriesEnabled, }: WorkflowListProps) => { return (
@@ -225,7 +225,9 @@ const WorkflowList = ({ ) : workflowItems.length > 0 ? ( ) : ( - + )}
diff --git a/src/components/WorkflowListEmptyState.test.tsx b/src/components/WorkflowListEmptyState.test.tsx new file mode 100644 index 00000000..41ed8d02 --- /dev/null +++ b/src/components/WorkflowListEmptyState.test.tsx @@ -0,0 +1,40 @@ +import { render, screen } from "@testing-library/react"; +import { describe, expect, it } from "vitest"; + +import WorkflowListEmptyState from "./WorkflowListEmptyState"; + +describe("WorkflowListEmptyState", () => { + it("shows migration guidance when workflow tables are unavailable", () => { + render(); + + expect( + screen.getByRole("heading", { name: "Build faster with Workflows" }), + ).toBeInTheDocument(); + expect( + screen.getByText(/run all River Pro migrations/i), + ).toBeInTheDocument(); + expect( + screen.queryByRole("heading", { name: "No workflows yet" }), + ).not.toBeInTheDocument(); + }); + + it("shows a neutral empty state when workflow tables are available", () => { + render(); + + expect( + screen.getByRole("heading", { name: "No workflows yet" }), + ).toBeInTheDocument(); + expect( + screen.getByText(/coordinate fan-out, fan-in, retries/i), + ).toBeInTheDocument(); + expect(screen.getByRole("link", { name: "Docs" })).toHaveAttribute( + "href", + "https://riverqueue.com/docs/pro/workflows", + ); + expect( + screen.queryByRole("heading", { + name: "Build faster with Workflows", + }), + ).not.toBeInTheDocument(); + }); +}); diff --git a/src/components/WorkflowListEmptyState.tsx b/src/components/WorkflowListEmptyState.tsx index a3351061..591c079c 100644 --- a/src/components/WorkflowListEmptyState.tsx +++ b/src/components/WorkflowListEmptyState.tsx @@ -2,52 +2,49 @@ import { Badge } from "@components/Badge"; import Logo from "@components/Logo"; import { ArrowRightIcon } from "@heroicons/react/20/solid"; import { RectangleGroupIcon } from "@heroicons/react/24/outline"; -import { listWorkflows, listWorkflowsKey } from "@services/workflows"; -import { queryOptions, useQuery } from "@tanstack/react-query"; export default function WorkflowListEmptyState({ - probeForExistingWorkflows = true, - showingAll, + workflowQueriesEnabled, }: { - probeForExistingWorkflows?: boolean; - showingAll: boolean; + workflowQueriesEnabled: boolean; }) { - const opts = queryOptions({ - enabled: probeForExistingWorkflows && !showingAll, - queryFn: listWorkflows, - queryKey: listWorkflowsKey({ limit: 1, state: undefined }), - refetchInterval: 60000, - }); - - const anyWorkflowsQuery = useQuery(opts); - const hasExistingWorkflows = - anyWorkflowsQuery.isLoading || - (probeForExistingWorkflows && - !showingAll && - (anyWorkflowsQuery.data || []).length > 0); - return ( <> - {hasExistingWorkflows && ( -
-