diff --git a/CHANGELOG.md b/CHANGELOG.md index 7b83601..b0f8ec1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Fixed + +- Workflow detail: add on-canvas zoom controls for click/touch navigation and improve controls styling for dark mode. [PR #524](https://github.com/riverqueue/riverui/pull/524). +- Workflow detail: improve default workflow diagram framing for legibility while still allowing manual zoom-out to view the full graph. [PR #524](https://github.com/riverqueue/riverui/pull/524). +- Workflow detail: truncate long workflow names in the header to prevent overflow and add a copy button for the full name. [PR #524](https://github.com/riverqueue/riverui/pull/524). + ## [v0.15.0] - 2026-02-26 ### Changed diff --git a/src/components/WorkflowDetail.tsx b/src/components/WorkflowDetail.tsx index 7dea1c1..875304d 100644 --- a/src/components/WorkflowDetail.tsx +++ b/src/components/WorkflowDetail.tsx @@ -8,8 +8,14 @@ import TopNavTitleOnly from "@components/TopNavTitleOnly"; import WorkflowDiagram from "@components/workflow-diagram/WorkflowDiagram"; import { useFeatures } from "@contexts/Features.hook"; // (Dialog is now encapsulated in RetryWorkflowDialog) -import { ArrowPathIcon, XCircleIcon } from "@heroicons/react/24/outline"; +import { CheckIcon } from "@heroicons/react/16/solid"; +import { + ArrowPathIcon, + ClipboardIcon, + XCircleIcon, +} from "@heroicons/react/24/outline"; import { JobWithKnownMetadata } from "@services/jobs"; +import { toastSuccess } from "@services/toast"; import { JobState } from "@services/types"; import { Workflow, type WorkflowRetryMode } from "@services/workflows"; import { Link } from "@tanstack/react-router"; @@ -76,6 +82,7 @@ export default function WorkflowDetail({ // Modal state for retry const [retryOpen, setRetryOpen] = useState(false); const [retryMode, setRetryMode] = useState(); + const [workflowNameCopied, setWorkflowNameCopied] = useState(false); if (!features.workflowQueries) { return ( @@ -93,12 +100,12 @@ export default function WorkflowDetail({ return

No workflow data available

; } - const { tasks } = workflow; - // Ensure firstTask exists before rendering if (!firstTask) { return

No tasks available

; } + const { tasks } = workflow; + const workflowName = firstTask.metadata.workflow_name || "Unnamed Workflow"; return ( <> @@ -106,10 +113,53 @@ export default function WorkflowDetail({
{/* Heading */}
-
-

- - {firstTask.metadata.workflow_name || "Unnamed Workflow"} +
+

+ + + {workflowName} + +

diff --git a/src/components/workflow-diagram/WorkflowDiagram.test.tsx b/src/components/workflow-diagram/WorkflowDiagram.test.tsx index 5b47fb6..d0a8be7 100644 --- a/src/components/workflow-diagram/WorkflowDiagram.test.tsx +++ b/src/components/workflow-diagram/WorkflowDiagram.test.tsx @@ -9,6 +9,11 @@ import * as workflowDiagramLayout from "./workflowDiagramLayout"; type MockReactFlowProps = PropsWithChildren<{ edges: unknown[]; + fitViewOptions?: { + minZoom?: number; + padding?: number; + }; + minZoom?: number; nodes: unknown[]; onNodesChange?: (changes: SelectionChange[]) => void; }>; @@ -32,6 +37,7 @@ vi.mock("./WorkflowNode", () => ({ vi.mock("@xyflow/react", () => ({ BaseEdge: () => null, + Controls: () =>

, MiniMap: () =>
, ReactFlow: (props: MockReactFlowProps) => { latestReactFlowProps = props; @@ -71,6 +77,9 @@ describe("WorkflowDiagram", () => { expect(screen.getByTestId("react-flow")).toBeInTheDocument(); expect(screen.getByTestId("node-count")).toHaveTextContent("3"); expect(screen.getByTestId("edge-count")).toHaveTextContent("3"); + expect(screen.getByTestId("diagram-controls")).toBeInTheDocument(); + expect(latestReactFlowProps?.minZoom).toBe(0.2); + expect(latestReactFlowProps?.fitViewOptions?.minZoom).toBe(0.55); }); it("calls setSelectedJobId when a node is selected", () => { diff --git a/src/components/workflow-diagram/WorkflowDiagram.tsx b/src/components/workflow-diagram/WorkflowDiagram.tsx index 9af2a0f..00d5069 100644 --- a/src/components/workflow-diagram/WorkflowDiagram.tsx +++ b/src/components/workflow-diagram/WorkflowDiagram.tsx @@ -8,7 +8,7 @@ import type { import { JobWithKnownMetadata } from "@services/jobs"; import { JobState } from "@services/types"; -import { MiniMap, ReactFlow } from "@xyflow/react"; +import { Controls, MiniMap, ReactFlow } from "@xyflow/react"; import { useTheme } from "next-themes"; import { useCallback, useMemo } from "react"; @@ -46,6 +46,10 @@ const edgeTypes: EdgeTypes = { workflowEdge: WorkflowDiagramEdge, }; +const workflowDiagramMinZoom = 0.2; +const workflowDiagramFitViewMinZoom = 0.55; +const workflowDiagramFitViewPadding = 0.08; + type NodeTypeKey = Extract; const getMiniMapNodeClassName = ( @@ -137,10 +141,13 @@ export default function WorkflowDiagram({ edges={layoutedEdges} edgeTypes={edgeTypes} fitView - fitViewOptions={{ padding: 0.2 }} + fitViewOptions={{ + minZoom: workflowDiagramFitViewMinZoom, + padding: workflowDiagramFitViewPadding, + }} id={`workflow-diagram-${workflowIdForInstance}`} key={`workflow-diagram-${workflowIdForInstance}`} - minZoom={0.8} + minZoom={workflowDiagramMinZoom} nodes={layoutedNodes} nodesFocusable={true} nodeTypes={nodeTypes} @@ -148,6 +155,11 @@ export default function WorkflowDiagram({ onNodesChange={onNodesChange} proOptions={{ hideAttribution: true }} > +