Skip to content

Commit aa141a0

Browse files
Add create run form (#308)
1 parent 461d578 commit aa141a0

5 files changed

Lines changed: 522 additions & 18 deletions

File tree

Lines changed: 236 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,236 @@
1+
import { Button } from "@/components/ui/button";
2+
import {
3+
Field,
4+
FieldDescription,
5+
FieldError,
6+
FieldGroup,
7+
FieldLabel,
8+
} from "@/components/ui/field";
9+
import { Input } from "@/components/ui/input";
10+
import { Textarea } from "@/components/ui/textarea";
11+
import { createWorkflowRunServerFn } from "@/lib/api";
12+
import { useNavigate } from "@tanstack/react-router";
13+
import { useState } from "react";
14+
15+
interface CreateRunFormProps {
16+
onCancel?: () => void;
17+
onSuccess?: () => void;
18+
}
19+
20+
function normalizeOptionalField(value: string): string | null {
21+
const trimmed = value.trim();
22+
return trimmed.length > 0 ? trimmed : null;
23+
}
24+
25+
function toIsoDateTime(value: string, fieldName: string): string {
26+
const parsed = new Date(value);
27+
if (Number.isNaN(parsed.getTime())) {
28+
throw new TypeError(`${fieldName} must be a valid date and time`);
29+
}
30+
31+
return parsed.toISOString();
32+
}
33+
34+
function getErrorMessage(error: unknown): string {
35+
if (error instanceof Error && error.message) {
36+
return error.message;
37+
}
38+
39+
return "Unable to create workflow run";
40+
}
41+
42+
export function CreateRunForm({ onCancel, onSuccess }: CreateRunFormProps) {
43+
const navigate = useNavigate();
44+
45+
const [workflowName, setWorkflowName] = useState("");
46+
const [version, setVersion] = useState("");
47+
const [input, setInput] = useState("");
48+
const [availableAt, setAvailableAt] = useState("");
49+
const [deadlineAt, setDeadlineAt] = useState("");
50+
const [inputError, setInputError] = useState<string | null>(null);
51+
const [submitError, setSubmitError] = useState<string | null>(null);
52+
const [isSubmitting, setIsSubmitting] = useState(false);
53+
54+
async function submitForm() {
55+
if (!workflowName.trim()) {
56+
setSubmitError("Workflow name is required");
57+
return;
58+
}
59+
60+
const normalizedAvailableAt = normalizeOptionalField(availableAt);
61+
const normalizedDeadlineAt = normalizeOptionalField(deadlineAt);
62+
let availableAtIso: string | null = null;
63+
let deadlineAtIso: string | null = null;
64+
65+
try {
66+
if (normalizedAvailableAt) {
67+
availableAtIso = toIsoDateTime(normalizedAvailableAt, "Schedule for");
68+
}
69+
if (normalizedDeadlineAt) {
70+
deadlineAtIso = toIsoDateTime(normalizedDeadlineAt, "Deadline");
71+
}
72+
} catch (error) {
73+
setSubmitError(getErrorMessage(error));
74+
return;
75+
}
76+
77+
const normalizedInput = normalizeOptionalField(input);
78+
if (normalizedInput) {
79+
try {
80+
JSON.parse(normalizedInput);
81+
} catch {
82+
setInputError("Input must be valid JSON");
83+
return;
84+
}
85+
}
86+
87+
setInputError(null);
88+
setSubmitError(null);
89+
setIsSubmitting(true);
90+
91+
try {
92+
const run = await createWorkflowRunServerFn({
93+
data: {
94+
workflowName: workflowName.trim(),
95+
version: normalizeOptionalField(version),
96+
input: normalizedInput,
97+
availableAt: availableAtIso,
98+
deadlineAt: deadlineAtIso,
99+
},
100+
});
101+
102+
onSuccess?.();
103+
await navigate({
104+
to: "/runs/$runId",
105+
params: { runId: run.id },
106+
});
107+
} catch (error) {
108+
setSubmitError(getErrorMessage(error));
109+
} finally {
110+
setIsSubmitting(false);
111+
}
112+
}
113+
114+
function handleSubmit(event: React.SyntheticEvent<HTMLFormElement>) {
115+
event.preventDefault();
116+
void submitForm();
117+
}
118+
119+
return (
120+
<form className="grid gap-5" onSubmit={handleSubmit}>
121+
<FieldGroup>
122+
<Field>
123+
<FieldLabel htmlFor="create-run-workflow-name">
124+
Workflow Name *
125+
</FieldLabel>
126+
<Input
127+
id="create-run-workflow-name"
128+
name="workflowName"
129+
value={workflowName}
130+
onChange={(event) => {
131+
setWorkflowName(event.currentTarget.value);
132+
}}
133+
placeholder="e.g. hello-world"
134+
required
135+
disabled={isSubmitting}
136+
/>
137+
</Field>
138+
</FieldGroup>
139+
140+
<div className="space-y-4">
141+
<p className="text-muted-foreground text-xs tracking-wide uppercase">
142+
Optional
143+
</p>
144+
<FieldGroup>
145+
<Field data-invalid={!!inputError}>
146+
<FieldLabel htmlFor="create-run-input">Input (JSON)</FieldLabel>
147+
<Textarea
148+
id="create-run-input"
149+
name="input"
150+
value={input}
151+
onChange={(event) => {
152+
setInput(event.currentTarget.value);
153+
if (inputError) {
154+
setInputError(null);
155+
}
156+
}}
157+
rows={6}
158+
placeholder='{"key":"value"}'
159+
disabled={isSubmitting}
160+
/>
161+
<FieldDescription>
162+
JSON payload passed to the workflow function.
163+
</FieldDescription>
164+
<FieldError>{inputError}</FieldError>
165+
</Field>
166+
167+
<div className="grid gap-5 sm:grid-cols-2">
168+
<Field>
169+
<FieldLabel htmlFor="create-run-available-at">
170+
Schedule For
171+
</FieldLabel>
172+
<Input
173+
id="create-run-available-at"
174+
name="availableAt"
175+
type="datetime-local"
176+
value={availableAt}
177+
onChange={(event) => {
178+
setAvailableAt(event.currentTarget.value);
179+
}}
180+
disabled={isSubmitting}
181+
/>
182+
<FieldDescription>
183+
Leave empty to start immediately.
184+
</FieldDescription>
185+
</Field>
186+
187+
<Field>
188+
<FieldLabel htmlFor="create-run-deadline-at">Deadline</FieldLabel>
189+
<Input
190+
id="create-run-deadline-at"
191+
name="deadlineAt"
192+
type="datetime-local"
193+
value={deadlineAt}
194+
onChange={(event) => {
195+
setDeadlineAt(event.currentTarget.value);
196+
}}
197+
disabled={isSubmitting}
198+
/>
199+
<FieldDescription>Leave empty for no deadline.</FieldDescription>
200+
</Field>
201+
</div>
202+
203+
<Field className="sm:max-w-sm">
204+
<FieldLabel htmlFor="create-run-version">Version</FieldLabel>
205+
<Input
206+
id="create-run-version"
207+
name="version"
208+
value={version}
209+
onChange={(event) => {
210+
setVersion(event.currentTarget.value);
211+
}}
212+
placeholder="e.g. v2"
213+
disabled={isSubmitting}
214+
/>
215+
</Field>
216+
</FieldGroup>
217+
</div>
218+
219+
<FieldError>{submitError}</FieldError>
220+
221+
<div className="flex items-center justify-end gap-2">
222+
<Button
223+
variant="outline"
224+
type="button"
225+
onClick={onCancel}
226+
disabled={isSubmitting}
227+
>
228+
Cancel
229+
</Button>
230+
<Button type="submit" disabled={isSubmitting}>
231+
{isSubmitting ? "Creating..." : "Create Run"}
232+
</Button>
233+
</div>
234+
</form>
235+
);
236+
}

packages/dashboard/src/components/run-list.tsx

Lines changed: 26 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -10,18 +10,27 @@ import type { WorkflowRun } from "openworkflow/internal";
1010
export interface RunListProps {
1111
runs: WorkflowRun[];
1212
title?: string;
13+
showHeader?: boolean;
14+
showCount?: boolean;
1315
}
1416

15-
export function RunList({ runs, title = "Workflow Runs" }: RunListProps) {
17+
export function RunList({
18+
runs,
19+
title = "Workflow Runs",
20+
showHeader = true,
21+
showCount = true,
22+
}: RunListProps) {
1623
if (runs.length === 0) {
1724
return (
1825
<div className="space-y-4">
19-
<div>
20-
<h2 className="text-2xl font-semibold">{title}</h2>
21-
<p className="text-muted-foreground mt-1 text-sm">
22-
No workflow runs found
23-
</p>
24-
</div>
26+
{showHeader && (
27+
<div>
28+
<h2 className="text-2xl font-semibold">{title}</h2>
29+
<p className="text-muted-foreground mt-1 text-sm">
30+
No workflow runs found
31+
</p>
32+
</div>
33+
)}
2534
<Card className="bg-card border-border p-8 text-center">
2635
<p className="text-muted-foreground">
2736
No workflow runs have been created yet.
@@ -33,12 +42,16 @@ export function RunList({ runs, title = "Workflow Runs" }: RunListProps) {
3342

3443
return (
3544
<div className="space-y-4">
36-
<div>
37-
<h2 className="text-2xl font-semibold">{title}</h2>
38-
<p className="text-muted-foreground mt-1 text-sm">
39-
{runs.length} workflow run{runs.length === 1 ? "" : "s"}
40-
</p>
41-
</div>
45+
{showHeader && (
46+
<div>
47+
<h2 className="text-2xl font-semibold">{title}</h2>
48+
{showCount && (
49+
<p className="text-muted-foreground mt-1 text-sm">
50+
{runs.length} workflow run{runs.length === 1 ? "" : "s"}
51+
</p>
52+
)}
53+
</div>
54+
)}
4255

4356
<Card className="bg-card border-border overflow-hidden py-0">
4457
<div className="divide-border divide-y">

0 commit comments

Comments
 (0)