Skip to content

Commit 45e5764

Browse files
Add tags display and change state functionality to FlowRunHeader (#20337)
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Co-authored-by: alex.s@prefect.io <ajstreed1@gmail.com>
1 parent dd1ed09 commit 45e5764

File tree

3 files changed

+261
-1
lines changed

3 files changed

+261
-1
lines changed

ui-v2/src/components/flow-runs/flow-run-details-page/flow-run-header.stories.tsx

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -236,3 +236,73 @@ export const MinimalRelationships: Story = {
236236
}),
237237
},
238238
};
239+
240+
export const WithTags: Story = {
241+
name: "With Tags",
242+
args: {
243+
flowRun: createFakeFlowRun({
244+
id: "flow-run-with-tags",
245+
name: "my-flow-run-with-tags",
246+
flow_id: "test-flow-id",
247+
tags: ["production", "critical"],
248+
state_type: "COMPLETED",
249+
state_name: "Completed",
250+
state: createFakeState({
251+
type: "COMPLETED",
252+
name: "Completed",
253+
}),
254+
}),
255+
},
256+
};
257+
258+
export const WithManyTags: Story = {
259+
name: "With Many Tags",
260+
args: {
261+
flowRun: createFakeFlowRun({
262+
id: "flow-run-with-many-tags",
263+
name: "my-flow-run-with-many-tags",
264+
flow_id: "test-flow-id",
265+
tags: ["production", "critical", "etl", "daily", "data-pipeline"],
266+
state_type: "COMPLETED",
267+
state_name: "Completed",
268+
state: createFakeState({
269+
type: "COMPLETED",
270+
name: "Completed",
271+
}),
272+
}),
273+
},
274+
};
275+
276+
export const TerminalStateCanChange: Story = {
277+
name: "Terminal State (Can Change State)",
278+
args: {
279+
flowRun: createFakeFlowRun({
280+
id: "flow-run-terminal",
281+
name: "my-completed-flow-run",
282+
flow_id: "test-flow-id",
283+
state_type: "COMPLETED",
284+
state_name: "Completed",
285+
state: createFakeState({
286+
type: "COMPLETED",
287+
name: "Completed",
288+
}),
289+
}),
290+
},
291+
};
292+
293+
export const NonTerminalStateCannotChange: Story = {
294+
name: "Non-Terminal State (Cannot Change State)",
295+
args: {
296+
flowRun: createFakeFlowRun({
297+
id: "flow-run-non-terminal",
298+
name: "my-running-flow-run",
299+
flow_id: "test-flow-id",
300+
state_type: "RUNNING",
301+
state_name: "Running",
302+
state: createFakeState({
303+
type: "RUNNING",
304+
name: "Running",
305+
}),
306+
}),
307+
},
308+
};

ui-v2/src/components/flow-runs/flow-run-details-page/flow-run-header.test.tsx

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,9 @@ describe("FlowRunHeader", () => {
145145
http.post(buildApiUrl("/flow_runs/filter"), () => {
146146
return HttpResponse.json([MOCK_PARENT_FLOW_RUN]);
147147
}),
148+
http.post(buildApiUrl("/flow_runs/:id/set_state"), () => {
149+
return HttpResponse.json({ status: "ACCEPT" });
150+
}),
148151
);
149152
});
150153

@@ -528,4 +531,115 @@ describe("FlowRunHeader", () => {
528531
expect(screen.getByText("parent-flow-run")).toBeInTheDocument();
529532
});
530533
});
534+
535+
it("displays tags when flow run has tags", async () => {
536+
renderFlowRunHeader({
537+
tags: ["tag1", "tag2"],
538+
});
539+
540+
await waitFor(() => {
541+
expect(screen.getByText("tag1")).toBeInTheDocument();
542+
expect(screen.getByText("tag2")).toBeInTheDocument();
543+
});
544+
});
545+
546+
it("does not display tags section when tags is empty", async () => {
547+
renderFlowRunHeader({
548+
tags: [],
549+
});
550+
551+
await waitFor(() => {
552+
expect(screen.getByText("test-flow-run")).toBeInTheDocument();
553+
});
554+
555+
expect(screen.queryByText("tag1")).not.toBeInTheDocument();
556+
});
557+
558+
it("shows 'Change state' option for terminal states", async () => {
559+
server.use(
560+
http.post(buildApiUrl("/flow_runs/:id/set_state"), () => {
561+
return HttpResponse.json({ status: "ACCEPT" });
562+
}),
563+
);
564+
565+
renderFlowRunHeader({
566+
state_type: "COMPLETED",
567+
state_name: "Completed",
568+
state: createFakeState({
569+
type: "COMPLETED",
570+
name: "Completed",
571+
}),
572+
});
573+
const user = userEvent.setup();
574+
575+
await waitFor(() => {
576+
expect(screen.getByText("test-flow-run")).toBeInTheDocument();
577+
});
578+
579+
const moreButton = screen.getByRole("button", { expanded: false });
580+
await user.click(moreButton);
581+
582+
await waitFor(() => {
583+
expect(screen.getByText("Change state")).toBeInTheDocument();
584+
});
585+
});
586+
587+
it("does not show 'Change state' option for non-terminal states", async () => {
588+
renderFlowRunHeader({
589+
state_type: "RUNNING",
590+
state_name: "Running",
591+
state: createFakeState({
592+
type: "RUNNING",
593+
name: "Running",
594+
}),
595+
});
596+
const user = userEvent.setup();
597+
598+
await waitFor(() => {
599+
expect(screen.getByText("test-flow-run")).toBeInTheDocument();
600+
});
601+
602+
const moreButton = screen.getByRole("button", { expanded: false });
603+
await user.click(moreButton);
604+
605+
await waitFor(() => {
606+
expect(screen.getByText("Copy ID")).toBeInTheDocument();
607+
});
608+
609+
expect(screen.queryByText("Change state")).not.toBeInTheDocument();
610+
});
611+
612+
it("opens change state dialog when 'Change state' is clicked", async () => {
613+
server.use(
614+
http.post(buildApiUrl("/flow_runs/:id/set_state"), () => {
615+
return HttpResponse.json({ status: "ACCEPT" });
616+
}),
617+
);
618+
619+
renderFlowRunHeader({
620+
state_type: "FAILED",
621+
state_name: "Failed",
622+
state: createFakeState({
623+
type: "FAILED",
624+
name: "Failed",
625+
}),
626+
});
627+
const user = userEvent.setup();
628+
629+
await waitFor(() => {
630+
expect(screen.getByText("test-flow-run")).toBeInTheDocument();
631+
});
632+
633+
const moreButton = screen.getByRole("button", { expanded: false });
634+
await user.click(moreButton);
635+
636+
await waitFor(() => {
637+
expect(screen.getByText("Change state")).toBeInTheDocument();
638+
});
639+
await user.click(screen.getByText("Change state"));
640+
641+
await waitFor(() => {
642+
expect(screen.getByText("Change Flow Run State")).toBeInTheDocument();
643+
});
644+
});
531645
});

ui-v2/src/components/flow-runs/flow-run-details-page/flow-run-header.tsx

Lines changed: 77 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,11 @@ import { Link } from "@tanstack/react-router";
33
import { MoreVertical } from "lucide-react";
44
import { toast } from "sonner";
55
import { buildDeploymentDetailsQuery } from "@/api/deployments";
6-
import { buildFilterFlowRunsQuery, type FlowRun } from "@/api/flow-runs";
6+
import {
7+
buildFilterFlowRunsQuery,
8+
type FlowRun,
9+
useSetFlowRunState,
10+
} from "@/api/flow-runs";
711
import { buildCountTaskRunsQuery } from "@/api/task-runs";
812
import { FlowIconText } from "@/components/flows/flow-icon-text";
913
import { Badge } from "@/components/ui/badge";
@@ -16,6 +20,10 @@ import {
1620
BreadcrumbSeparator,
1721
} from "@/components/ui/breadcrumb";
1822
import { Button } from "@/components/ui/button";
23+
import {
24+
ChangeStateDialog,
25+
useChangeStateDialog,
26+
} from "@/components/ui/change-state-dialog";
1927
import {
2028
DeleteConfirmationDialog,
2129
useDeleteConfirmationDialog,
@@ -28,6 +36,7 @@ import {
2836
} from "@/components/ui/dropdown-menu";
2937
import { Icon } from "@/components/ui/icons";
3038
import { StateBadge } from "@/components/ui/state-badge";
39+
import { TagBadgeGroup } from "@/components/ui/tag-badge-group";
3140
import { formatDate, secondsToApproximateString } from "@/utils";
3241

3342
type FlowRunHeaderProps = {
@@ -37,6 +46,45 @@ type FlowRunHeaderProps = {
3746

3847
export function FlowRunHeader({ flowRun, onDeleteClick }: FlowRunHeaderProps) {
3948
const [dialogState, confirmDelete] = useDeleteConfirmationDialog();
49+
const {
50+
open: isChangeStateOpen,
51+
onOpenChange: setChangeStateOpen,
52+
openDialog: openChangeState,
53+
} = useChangeStateDialog();
54+
const { setFlowRunState, isPending: isChangingState } = useSetFlowRunState();
55+
56+
const canChangeState =
57+
flowRun.state_type &&
58+
["COMPLETED", "FAILED", "CANCELLED", "CRASHED"].includes(
59+
flowRun.state_type,
60+
);
61+
62+
const handleChangeState = (newState: { type: string; message?: string }) => {
63+
setFlowRunState(
64+
{
65+
id: flowRun.id,
66+
state: {
67+
type: newState.type as
68+
| "COMPLETED"
69+
| "FAILED"
70+
| "CANCELLED"
71+
| "CRASHED",
72+
name: newState.type.charAt(0) + newState.type.slice(1).toLowerCase(),
73+
message: newState.message,
74+
},
75+
force: true,
76+
},
77+
{
78+
onSuccess: () => {
79+
toast.success("Flow run state changed");
80+
setChangeStateOpen(false);
81+
},
82+
onError: (error) => {
83+
toast.error(error.message || "Failed to change state");
84+
},
85+
},
86+
);
87+
};
4088

4189
const { data: deployment } = useQuery({
4290
...buildDeploymentDetailsQuery(flowRun.deployment_id ?? ""),
@@ -93,6 +141,11 @@ export function FlowRunHeader({ flowRun, onDeleteClick }: FlowRunHeaderProps) {
93141
{flowRun.work_pool_name}
94142
</Badge>
95143
)}
144+
{flowRun.tags && flowRun.tags.length > 0 && (
145+
<div className="ml-2">
146+
<TagBadgeGroup tags={flowRun.tags} maxTagsDisplayed={3} />
147+
</div>
148+
)}
96149
</BreadcrumbItem>
97150
</BreadcrumbList>
98151
</Breadcrumb>
@@ -180,6 +233,11 @@ export function FlowRunHeader({ flowRun, onDeleteClick }: FlowRunHeaderProps) {
180233
</Button>
181234
</DropdownMenuTrigger>
182235
<DropdownMenuContent>
236+
{canChangeState && (
237+
<DropdownMenuItem onClick={openChangeState}>
238+
Change state
239+
</DropdownMenuItem>
240+
)}
183241
<DropdownMenuItem
184242
onClick={() => {
185243
void navigator.clipboard.writeText(flowRun.id);
@@ -202,6 +260,24 @@ export function FlowRunHeader({ flowRun, onDeleteClick }: FlowRunHeaderProps) {
202260
</DropdownMenuContent>
203261
</DropdownMenu>
204262
<DeleteConfirmationDialog {...dialogState} />
263+
<ChangeStateDialog
264+
open={isChangeStateOpen}
265+
onOpenChange={setChangeStateOpen}
266+
currentState={
267+
flowRun.state
268+
? {
269+
type: flowRun.state.type,
270+
name:
271+
flowRun.state.name ??
272+
flowRun.state.type.charAt(0) +
273+
flowRun.state.type.slice(1).toLowerCase(),
274+
}
275+
: null
276+
}
277+
label="Flow Run"
278+
onConfirm={handleChangeState}
279+
isLoading={isChangingState}
280+
/>
205281
</div>
206282
);
207283
}

0 commit comments

Comments
 (0)