Skip to content

Commit c088ef4

Browse files
Add state utilities and resume flow run API hooks (#20436)
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 ebdbb2c commit c088ef4

File tree

3 files changed

+266
-0
lines changed

3 files changed

+266
-0
lines changed

ui-v2/src/api/flow-runs/index.ts

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,8 @@ export const queryKeyFactory = {
113113
[...queryKeyFactory.history(), filter] as const,
114114
details: () => [...queryKeyFactory.all(), "details"] as const,
115115
detail: (id: string) => [...queryKeyFactory.details(), id] as const,
116+
input: (id: string, key: string) =>
117+
[...queryKeyFactory.detail(id), "input", key] as const,
116118
};
117119

118120
/**
@@ -216,6 +218,33 @@ export const buildGetFlowRunDetailsQuery = (id: string) => {
216218
});
217219
};
218220

221+
/**
222+
* Builds a query configuration for fetching flow run input by key
223+
*
224+
* @param id - The flow run id
225+
* @param key - The input key (e.g., "schema", "description")
226+
* @returns Query configuration object for use with TanStack Query
227+
*
228+
* @example
229+
* ```ts
230+
* const { data: schema } = useQuery(buildGetFlowRunInputQuery(flowRunId, "schema"));
231+
* ```
232+
*/
233+
export const buildGetFlowRunInputQuery = (id: string, key: string) => {
234+
return queryOptions({
235+
queryKey: queryKeyFactory.input(id, key),
236+
queryFn: async () => {
237+
const res = await (await getQueryService()).GET(
238+
"/flow_runs/{id}/input/{key}",
239+
{
240+
params: { path: { id, key } },
241+
},
242+
);
243+
return res.data;
244+
},
245+
});
246+
};
247+
219248
/**
220249
* Builds a query configuration for counting flow runs
221250
*
@@ -498,3 +527,73 @@ export const useSetFlowRunState = () => {
498527
...rest,
499528
};
500529
};
530+
531+
/**
532+
* Parameters for resuming a flow run
533+
*/
534+
type ResumeFlowRunParams = {
535+
id: string;
536+
runInput?: Record<string, unknown>;
537+
};
538+
539+
/**
540+
* Hook for resuming a paused flow run
541+
*
542+
* @returns Mutation object for resuming a flow run with loading/error states and trigger function
543+
*
544+
* @example
545+
* ```ts
546+
* const { resumeFlowRun, isPending } = useResumeFlowRun();
547+
*
548+
* resumeFlowRun({ id: "flow-run-id" }, {
549+
* onSuccess: () => toast.success("Flow run resumed"),
550+
* onError: (error) => toast.error(error.message)
551+
* });
552+
* ```
553+
*/
554+
export const useResumeFlowRun = () => {
555+
const queryClient = useQueryClient();
556+
const {
557+
mutate: resumeFlowRun,
558+
mutateAsync: resumeFlowRunAsync,
559+
...rest
560+
} = useMutation({
561+
mutationFn: async ({ id, runInput }: ResumeFlowRunParams) => {
562+
const res = await (await getQueryService()).POST(
563+
"/flow_runs/{id}/resume",
564+
{
565+
params: { path: { id } },
566+
body: runInput ? { run_input: runInput } : undefined,
567+
},
568+
);
569+
570+
if (!res.data) {
571+
throw new Error("'data' expected");
572+
}
573+
574+
// Check orchestration result status
575+
if (res.data.status !== "ACCEPT") {
576+
const reason =
577+
res.data.details && "reason" in res.data.details
578+
? res.data.details.reason
579+
: "Resume request was not accepted";
580+
throw new Error(reason ?? "Resume request was not accepted");
581+
}
582+
583+
return res.data;
584+
},
585+
onSettled: (_data, _error, { id }) => {
586+
void Promise.all([
587+
queryClient.invalidateQueries({ queryKey: queryKeyFactory.lists() }),
588+
queryClient.invalidateQueries({ queryKey: queryKeyFactory.detail(id) }),
589+
]);
590+
},
591+
});
592+
return {
593+
resumeFlowRun,
594+
resumeFlowRunAsync,
595+
...rest,
596+
};
597+
};
598+
599+
export * from "./state-utilities";
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
import { describe, expect, it } from "vitest";
2+
import {
3+
isPausedState,
4+
isRunningState,
5+
isStuckState,
6+
isTerminalState,
7+
STUCK_STATES,
8+
TERMINAL_STATES,
9+
} from "./state-utilities";
10+
11+
describe("state-utilities", () => {
12+
describe("isStuckState", () => {
13+
it.each([
14+
"RUNNING",
15+
"SCHEDULED",
16+
"PENDING",
17+
"PAUSED",
18+
] as const)("returns true for %s", (state) => {
19+
expect(isStuckState(state)).toBe(true);
20+
});
21+
22+
it.each([
23+
"COMPLETED",
24+
"FAILED",
25+
"CANCELLED",
26+
"CRASHED",
27+
] as const)("returns false for terminal state %s", (state) => {
28+
expect(isStuckState(state)).toBe(false);
29+
});
30+
31+
it("returns false for null", () => {
32+
expect(isStuckState(null)).toBe(false);
33+
});
34+
35+
it("returns false for undefined", () => {
36+
expect(isStuckState(undefined)).toBe(false);
37+
});
38+
});
39+
40+
describe("isRunningState", () => {
41+
it("returns true for RUNNING", () => {
42+
expect(isRunningState("RUNNING")).toBe(true);
43+
});
44+
45+
it.each([
46+
"SCHEDULED",
47+
"PENDING",
48+
"PAUSED",
49+
"COMPLETED",
50+
"FAILED",
51+
] as const)("returns false for %s", (state) => {
52+
expect(isRunningState(state)).toBe(false);
53+
});
54+
});
55+
56+
describe("isPausedState", () => {
57+
it("returns true for PAUSED", () => {
58+
expect(isPausedState("PAUSED")).toBe(true);
59+
});
60+
61+
it.each([
62+
"RUNNING",
63+
"SCHEDULED",
64+
"COMPLETED",
65+
"FAILED",
66+
] as const)("returns false for %s", (state) => {
67+
expect(isPausedState(state)).toBe(false);
68+
});
69+
});
70+
71+
describe("isTerminalState", () => {
72+
it.each([
73+
"COMPLETED",
74+
"FAILED",
75+
"CANCELLED",
76+
"CRASHED",
77+
] as const)("returns true for %s", (state) => {
78+
expect(isTerminalState(state)).toBe(true);
79+
});
80+
81+
it.each([
82+
"RUNNING",
83+
"SCHEDULED",
84+
"PENDING",
85+
"PAUSED",
86+
] as const)("returns false for non-terminal state %s", (state) => {
87+
expect(isTerminalState(state)).toBe(false);
88+
});
89+
});
90+
91+
describe("constants", () => {
92+
it("STUCK_STATES contains expected values", () => {
93+
expect(STUCK_STATES).toEqual([
94+
"RUNNING",
95+
"SCHEDULED",
96+
"PENDING",
97+
"PAUSED",
98+
]);
99+
});
100+
101+
it("TERMINAL_STATES contains expected values", () => {
102+
expect(TERMINAL_STATES).toEqual([
103+
"COMPLETED",
104+
"FAILED",
105+
"CANCELLED",
106+
"CRASHED",
107+
]);
108+
});
109+
});
110+
});
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import type { components } from "@/api/prefect";
2+
3+
type StateType = components["schemas"]["StateType"];
4+
5+
/**
6+
* States that can be cancelled - flow runs that are "stuck" or in-progress
7+
*/
8+
export const STUCK_STATES: StateType[] = [
9+
"RUNNING",
10+
"SCHEDULED",
11+
"PENDING",
12+
"PAUSED",
13+
];
14+
15+
/**
16+
* States that are terminal - flow runs that have completed execution
17+
*/
18+
export const TERMINAL_STATES: StateType[] = [
19+
"COMPLETED",
20+
"FAILED",
21+
"CANCELLED",
22+
"CRASHED",
23+
];
24+
25+
/**
26+
* Check if a flow run can be cancelled (is in a stuck/cancellable state)
27+
*/
28+
export function isStuckState(stateType: StateType | null | undefined): boolean {
29+
return stateType != null && STUCK_STATES.includes(stateType);
30+
}
31+
32+
/**
33+
* Check if a flow run can be paused (is currently running)
34+
*/
35+
export function isRunningState(
36+
stateType: StateType | null | undefined,
37+
): boolean {
38+
return stateType === "RUNNING";
39+
}
40+
41+
/**
42+
* Check if a flow run can be resumed (is currently paused)
43+
*/
44+
export function isPausedState(
45+
stateType: StateType | null | undefined,
46+
): boolean {
47+
return stateType === "PAUSED";
48+
}
49+
50+
/**
51+
* Check if a flow run is in a terminal state
52+
*/
53+
export function isTerminalState(
54+
stateType: StateType | null | undefined,
55+
): boolean {
56+
return stateType != null && TERMINAL_STATES.includes(stateType);
57+
}

0 commit comments

Comments
 (0)