Skip to content

Commit 26d1017

Browse files
committed
WIP on complete waitpoint form
1 parent 210319a commit 26d1017

File tree

3 files changed

+214
-11
lines changed
  • apps/webapp/app/routes
    • resources.orgs.$organizationSlug.projects.$projectParam.waitpoints.$waitpointFriendlyId.complete
    • resources.orgs.$organizationSlug.projects.v3.$projectParam.runs.$runParam.spans.$spanParam
  • references/hello-world/src/trigger

3 files changed

+214
-11
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,193 @@
1+
import { conform, useForm } from "@conform-to/react";
2+
import { parse } from "@conform-to/zod";
3+
import { CheckIcon, XMarkIcon } from "@heroicons/react/20/solid";
4+
import { Form, useActionData, useLocation, useNavigation, useSubmit } from "@remix-run/react";
5+
import { ActionFunctionArgs, json } from "@remix-run/server-runtime";
6+
import { useVirtualizer } from "@tanstack/react-virtual";
7+
import { parseExpression } from "cron-parser";
8+
import cronstrue from "cronstrue";
9+
import { useCallback, useRef, useState } from "react";
10+
import {
11+
environmentTextClassName,
12+
environmentTitle,
13+
} from "~/components/environments/EnvironmentLabel";
14+
import { Button, LinkButton } from "~/components/primitives/Buttons";
15+
import { CheckboxWithLabel } from "~/components/primitives/Checkbox";
16+
import { DateTime } from "~/components/primitives/DateTime";
17+
import { Fieldset } from "~/components/primitives/Fieldset";
18+
import { FormError } from "~/components/primitives/FormError";
19+
import { Header2, Header3 } from "~/components/primitives/Headers";
20+
import { Hint } from "~/components/primitives/Hint";
21+
import { Input } from "~/components/primitives/Input";
22+
import { InputGroup } from "~/components/primitives/InputGroup";
23+
import { Label } from "~/components/primitives/Label";
24+
import { Paragraph } from "~/components/primitives/Paragraph";
25+
import { Select, SelectItem } from "~/components/primitives/Select";
26+
import {
27+
Table,
28+
TableBody,
29+
TableCell,
30+
TableHeader,
31+
TableHeaderCell,
32+
TableRow,
33+
} from "~/components/primitives/Table";
34+
import { TextLink } from "~/components/primitives/TextLink";
35+
import { prisma } from "~/db.server";
36+
import { useOrganization } from "~/hooks/useOrganizations";
37+
import { useProject } from "~/hooks/useProject";
38+
import { redirectWithErrorMessage, redirectWithSuccessMessage } from "~/models/message.server";
39+
import { EditableScheduleElements } from "~/presenters/v3/EditSchedulePresenter.server";
40+
import { requireUserId } from "~/services/session.server";
41+
import { cn } from "~/utils/cn";
42+
import { ProjectParamSchema, docsPath, v3SchedulesPath } from "~/utils/pathBuilder";
43+
import { CronPattern, UpsertSchedule } from "~/v3/schedules";
44+
import { UpsertTaskScheduleService } from "~/v3/services/upsertTaskSchedule.server";
45+
import { AIGeneratedCronField } from "../resources.orgs.$organizationSlug.projects.$projectParam.schedules.new.natural-language";
46+
import { TimezoneList } from "~/components/scheduled/timezones";
47+
import { logger } from "~/services/logger.server";
48+
import { Waitpoint } from "@trigger.dev/database";
49+
import { z } from "zod";
50+
import { JSONEditor } from "~/components/code/JSONEditor";
51+
52+
const CompleteWaitpointFormData = z.discriminatedUnion("type", [
53+
z.object({
54+
type: z.literal("MANUAL"),
55+
payload: z.string(),
56+
}),
57+
z.object({
58+
type: z.literal("DATETIME"),
59+
}),
60+
]);
61+
62+
export const action = async ({ request, params }: ActionFunctionArgs) => {
63+
const userId = await requireUserId(request);
64+
const { organizationSlug, projectParam } = ProjectParamSchema.parse(params);
65+
66+
const formData = await request.formData();
67+
const submission = parse(formData, { schema: UpsertSchedule });
68+
69+
if (!submission.value) {
70+
return json(submission);
71+
}
72+
73+
try {
74+
//first check that the user has access to the project
75+
const project = await prisma.project.findUnique({
76+
where: {
77+
slug: projectParam,
78+
organization: {
79+
members: {
80+
some: {
81+
userId,
82+
},
83+
},
84+
},
85+
},
86+
select: { id: true },
87+
});
88+
89+
if (!project) {
90+
throw new Error("Project not found");
91+
}
92+
93+
const createSchedule = new UpsertTaskScheduleService();
94+
const result = await createSchedule.call(project.id, submission.value);
95+
96+
return redirectWithSuccessMessage(
97+
v3SchedulesPath({ slug: organizationSlug }, { slug: projectParam }),
98+
request,
99+
submission.value?.friendlyId === result.id ? "Schedule updated" : "Schedule created"
100+
);
101+
} catch (error: any) {
102+
logger.error("Failed to create schedule", error);
103+
104+
const errorMessage = `Something went wrong. Please try again.`;
105+
return redirectWithErrorMessage(
106+
v3SchedulesPath({ slug: organizationSlug }, { slug: projectParam }),
107+
request,
108+
errorMessage
109+
);
110+
}
111+
};
112+
113+
type FormWaitpoint = Pick<Waitpoint, "friendlyId" | "type">;
114+
115+
export function CompleteWaitpointForm({ waitpoint }: { waitpoint: FormWaitpoint }) {
116+
const navigation = useNavigation();
117+
const submit = useSubmit();
118+
const isLoading = navigation.state !== "idle";
119+
const organization = useOrganization();
120+
const project = useProject();
121+
const currentJson = useRef<string>("");
122+
const formAction = `/resources/orgs/${organization.slug}/projects/${project.slug}/waitpoints/${waitpoint.friendlyId}/complete`;
123+
124+
const submitForm = useCallback(
125+
(e: React.FormEvent<HTMLFormElement>) => {
126+
const formData = new FormData(e.currentTarget);
127+
const data: Record<string, string> = {
128+
type: formData.get("type") as string,
129+
failedRedirect: formData.get("failedRedirect") as string,
130+
successRedirect: formData.get("failedRedirect") as string,
131+
};
132+
133+
data.payload = currentJson.current;
134+
135+
submit(data, {
136+
action: formAction,
137+
method: "post",
138+
});
139+
e.preventDefault();
140+
},
141+
[currentJson]
142+
);
143+
144+
return (
145+
<Form
146+
action={formAction}
147+
method="post"
148+
onSubmit={(e) => submitForm(e)}
149+
className="grid h-full max-h-full grid-rows-[2.5rem_1fr_3.25rem] overflow-hidden bg-background-bright"
150+
>
151+
<div className="mx-3 flex items-center justify-between gap-2 border-b border-grid-dimmed">
152+
<Header2 className={cn("whitespace-nowrap")}>Complete waitpoint</Header2>
153+
</div>
154+
<div className="overflow-y-scroll scrollbar-thin scrollbar-track-transparent scrollbar-thumb-charcoal-600">
155+
<div className="p-3">
156+
<input type="hidden" name="type" value={waitpoint.type} />
157+
<Fieldset>
158+
<InputGroup>
159+
<Label>Payload</Label>
160+
<JSONEditor
161+
autoFocus
162+
defaultValue={currentJson.current}
163+
readOnly={false}
164+
basicSetup
165+
onChange={(v) => {
166+
currentJson.current = v;
167+
}}
168+
showClearButton={false}
169+
showCopyButton={false}
170+
height="100%"
171+
min-height="100%"
172+
max-height="100%"
173+
/>
174+
</InputGroup>
175+
</Fieldset>
176+
</div>
177+
</div>
178+
<div className="flex items-center justify-end gap-2 border-t border-grid-dimmed px-2">
179+
<div className="flex items-center gap-4">
180+
<Button
181+
variant="primary/medium"
182+
type="submit"
183+
disabled={isLoading}
184+
shortcut={{ key: "enter", modifiers: ["mod"] }}
185+
LeadingIcon={isLoading ? "spinner" : undefined}
186+
>
187+
{isLoading ? "Completing…" : "Complete"}
188+
</Button>
189+
</div>
190+
</div>
191+
</Form>
192+
);
193+
}

apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.v3.$projectParam.runs.$runParam.spans.$spanParam/route.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ import {
6666
v3TraceSpanPath,
6767
} from "~/utils/pathBuilder";
6868
import { SpanLink } from "~/v3/eventRepository.server";
69+
import { CompleteWaitpointForm } from "../resources.orgs.$organizationSlug.projects.$projectParam.waitpoints.$waitpointFriendlyId.complete/route";
6970

7071
export const loader = async ({ request, params }: LoaderFunctionArgs) => {
7172
const userId = await requireUserId(request);
@@ -1260,7 +1261,7 @@ function SpanEntity({ span }: { span: Span }) {
12601261
</Property.Item>
12611262
</Property.Table>
12621263
{span.waitpoint.status === "PENDING" ? (
1263-
<div>Manually complete waitpoint</div>
1264+
<CompleteWaitpointForm waitpoint={span.waitpoint} />
12641265
) : span.waitpoint.output ? (
12651266
<PacketDisplay
12661267
title="Waitpoint output"

references/hello-world/src/trigger/wait-tokens.ts

Lines changed: 19 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -6,45 +6,54 @@ type Token = {
66

77
export const waitToken = task({
88
id: "wait-token",
9-
run: async ({ completeBeforeWaiting = false, idempotencyKey, idempotencyKeyTTL }: { completeBeforeWaiting?: boolean, idempotencyKey?: string, idempotencyKeyTTL?: string },) => {
9+
run: async ({
10+
completeBeforeWaiting = false,
11+
idempotencyKey,
12+
idempotencyKeyTTL,
13+
completionDelay,
14+
}: {
15+
completeBeforeWaiting?: boolean;
16+
idempotencyKey?: string;
17+
idempotencyKeyTTL?: string;
18+
completionDelay?: number;
19+
}) => {
1020
logger.log("Hello, world", { completeBeforeWaiting });
1121

1222
const token = await wait.createToken({
1323
idempotencyKey,
1424
idempotencyKeyTTL,
15-
timeout: new Date(Date.now() + 10_000),
25+
timeout: completionDelay ? undefined : new Date(Date.now() + 10_000),
1626
});
1727
logger.log("Token", token);
1828

1929
const token2 = await wait.createToken({
2030
idempotencyKey,
2131
idempotencyKeyTTL,
22-
timeout: "10s" });
32+
timeout: "10s",
33+
});
2334
logger.log("Token2", token2);
2435

2536
if (completeBeforeWaiting) {
2637
await wait.completeToken<Token>(token.id, { status: "approved" });
27-
await wait.for({ seconds: 10 });
38+
await wait.for({ seconds: 5 });
2839
} else {
29-
await completeWaitToken.trigger({ token: token.id, delay: 4 });
40+
await completeWaitToken.trigger({ token: token.id, delay: completionDelay });
3041
}
3142

32-
3343
//wait for the token
3444
const result = await wait.forToken<{ foo: string }>(token);
3545
if (!result.ok) {
3646
logger.log("Token timeout", result);
3747
} else {
3848
logger.log("Token completed", result);
3949
}
40-
4150
},
42-
})
51+
});
4352

4453
export const completeWaitToken = task({
4554
id: "wait-token-complete",
46-
run: async (payload: { token: string; delay: number }) => {
47-
await wait.for({ seconds: payload.delay });
55+
run: async (payload: { token: string; delay?: number }) => {
56+
await wait.for({ seconds: payload.delay ?? 10 });
4857
await wait.completeToken<Token>(payload.token, { status: "approved" });
4958
},
5059
});

0 commit comments

Comments
 (0)