1
1
import { parse } from "@conform-to/zod" ;
2
- import { InformationCircleIcon } from "@heroicons/react/20/solid" ;
3
2
import { Form , useLocation , useNavigation , useSubmit } from "@remix-run/react" ;
4
3
import { ActionFunctionArgs , json } from "@remix-run/server-runtime" ;
5
- import { conditionallyExportPacket , stringifyIO } from "@trigger.dev/core/v3" ;
4
+ import {
5
+ conditionallyExportPacket ,
6
+ IOPacket ,
7
+ stringifyIO ,
8
+ timeoutError ,
9
+ } from "@trigger.dev/core/v3" ;
6
10
import { WaitpointId } from "@trigger.dev/core/v3/apps" ;
7
11
import { Waitpoint } from "@trigger.dev/database" ;
8
12
import { useCallback , useRef } from "react" ;
9
13
import { z } from "zod" ;
10
14
import { AnimatedHourglassIcon } from "~/assets/icons/AnimatedHourglassIcon" ;
11
- import { CodeBlock } from "~/components/code/CodeBlock" ;
12
15
import { JSONEditor } from "~/components/code/JSONEditor" ;
13
16
import { Button } from "~/components/primitives/Buttons" ;
14
17
import { DateTime } from "~/components/primitives/DateTime" ;
15
18
import { Paragraph } from "~/components/primitives/Paragraph" ;
19
+ import { InfoIconTooltip } from "~/components/primitives/Tooltip" ;
16
20
import { LiveCountdown } from "~/components/runs/v3/LiveTimer" ;
17
21
import { $replica } from "~/db.server" ;
18
22
import { useOrganization } from "~/hooks/useOrganizations" ;
@@ -26,7 +30,8 @@ import { engine } from "~/v3/runEngine.server";
26
30
const CompleteWaitpointFormData = z . discriminatedUnion ( "type" , [
27
31
z . object ( {
28
32
type : z . literal ( "MANUAL" ) ,
29
- payload : z . string ( ) ,
33
+ payload : z . string ( ) . optional ( ) ,
34
+ isTimeout : z . string ( ) . optional ( ) ,
30
35
successRedirect : z . string ( ) ,
31
36
failureRedirect : z . string ( ) ,
32
37
} ) ,
@@ -104,34 +109,58 @@ export const action = async ({ request, params }: ActionFunctionArgs) => {
104
109
) ;
105
110
}
106
111
case "MANUAL" : {
107
- let data : any ;
112
+ if ( submission . value . isTimeout ) {
113
+ try {
114
+ const result = await engine . completeWaitpoint ( {
115
+ id : waitpointId ,
116
+ output : {
117
+ type : "application/json" ,
118
+ value : JSON . stringify ( timeoutError ( new Date ( ) ) ) ,
119
+ isError : true ,
120
+ } ,
121
+ } ) ;
122
+
123
+ return redirectWithSuccessMessage (
124
+ submission . value . successRedirect ,
125
+ request ,
126
+ "Waitpoint timed out"
127
+ ) ;
128
+ } catch ( e ) {
129
+ return redirectWithErrorMessage (
130
+ submission . value . failureRedirect ,
131
+ request ,
132
+ "Invalid payload, must be valid JSON"
133
+ ) ;
134
+ }
135
+ }
136
+
108
137
try {
109
- data = JSON . parse ( submission . value . payload ) ;
138
+ const data = submission . value . payload ? JSON . parse ( submission . value . payload ) : { } ;
139
+ const stringifiedData = await stringifyIO ( data ) ;
140
+ const finalData = await conditionallyExportPacket (
141
+ stringifiedData ,
142
+ `${ waitpointId } /waitpoint/token`
143
+ ) ;
144
+
145
+ const result = await engine . completeWaitpoint ( {
146
+ id : waitpointId ,
147
+ output : finalData . data
148
+ ? { type : finalData . dataType , value : finalData . data , isError : false }
149
+ : undefined ,
150
+ } ) ;
151
+
152
+ return redirectWithSuccessMessage (
153
+ submission . value . successRedirect ,
154
+ request ,
155
+ "Waitpoint completed"
156
+ ) ;
110
157
} catch ( e ) {
111
158
return redirectWithErrorMessage (
112
159
submission . value . failureRedirect ,
113
160
request ,
114
161
"Invalid payload, must be valid JSON"
115
162
) ;
116
163
}
117
- const stringifiedData = await stringifyIO ( data ) ;
118
- const finalData = await conditionallyExportPacket (
119
- stringifiedData ,
120
- `${ waitpointId } /waitpoint/token`
121
- ) ;
122
-
123
- const result = await engine . completeWaitpoint ( {
124
- id : waitpointId ,
125
- output : finalData . data
126
- ? { type : finalData . dataType , value : finalData . data , isError : false }
127
- : undefined ,
128
- } ) ;
129
-
130
- return redirectWithSuccessMessage (
131
- submission . value . successRedirect ,
132
- request ,
133
- "Waitpoint completed"
134
- ) ;
135
164
}
136
165
}
137
166
} catch ( error : any ) {
@@ -199,7 +228,7 @@ function CompleteDateTimeWaitpointForm({
199
228
< Form
200
229
action = { `/resources/orgs/${ organization . slug } /projects/${ project . slug } /waitpoints/${ waitpoint . friendlyId } /complete` }
201
230
method = "post"
202
- className = "grid h-full max-h-full grid-rows-[2.5rem_1fr_2.5rem ] overflow-hidden rounded-md border border-grid-bright"
231
+ className = "grid h-full max-h-full grid-rows-[2.5rem_1fr_3.25rem ] overflow-hidden border-t border-grid-bright"
203
232
>
204
233
< div className = "mx-3 flex items-center" >
205
234
< Paragraph variant = "small/bright" > Manually skip this waitpoint</ Paragraph >
@@ -229,17 +258,15 @@ function CompleteDateTimeWaitpointForm({
229
258
< DateTime date = { waitpoint . completedAfter } />
230
259
</ div >
231
260
</ div >
232
- < div className = "px-2" >
233
- < div className = "mb-2 flex items-center justify-end gap-2 border-t border-grid-dimmed pt-2" >
234
- < Button
235
- variant = "secondary/small"
236
- type = "submit"
237
- disabled = { isLoading }
238
- LeadingIcon = { isLoading ? "spinner" : undefined }
239
- >
240
- { isLoading ? "Completing…" : "Skip waitpoint" }
241
- </ Button >
242
- </ div >
261
+ < div className = "flex items-center justify-end border-t border-grid-dimmed bg-background-dimmed px-2" >
262
+ < Button
263
+ variant = "secondary/medium"
264
+ type = "submit"
265
+ disabled = { isLoading }
266
+ LeadingIcon = { isLoading ? "spinner" : undefined }
267
+ >
268
+ { isLoading ? "Completing…" : "Skip waitpoint" }
269
+ </ Button >
243
270
</ div >
244
271
</ Form >
245
272
) ;
@@ -281,7 +308,7 @@ function CompleteManualWaitpointForm({ waitpoint }: { waitpoint: { friendlyId: s
281
308
action = { formAction }
282
309
method = "post"
283
310
onSubmit = { ( e ) => submitForm ( e ) }
284
- className = "grid h-full max-h-full grid-rows-[2.5rem_1fr_2.5rem ] overflow-hidden rounded-md border border-grid-bright"
311
+ className = "grid h-full max-h-full grid-rows-[2.5rem_1fr_3.25rem ] overflow-hidden border-t border-grid-bright"
285
312
>
286
313
< input type = "hidden" name = "type" value = { "MANUAL" } />
287
314
< input
@@ -294,10 +321,16 @@ function CompleteManualWaitpointForm({ waitpoint }: { waitpoint: { friendlyId: s
294
321
name = "failureRedirect"
295
322
value = { `${ location . pathname } ${ location . search } ` }
296
323
/>
297
- < div className = "mx-3 flex items-center" >
324
+ < div className = "mx-3 flex items-center gap-1 " >
298
325
< Paragraph variant = "small/bright" > Manually complete this waitpoint</ Paragraph >
326
+ < InfoIconTooltip
327
+ content = {
328
+ "This is will immediately complete this waitpoint with the payload you specify. This is useful during development for testing."
329
+ }
330
+ contentClassName = "normal-case tracking-normal max-w-xs"
331
+ />
299
332
</ div >
300
- < div className = "overflow-y-auto border-t border-grid-dimmed bg-charcoal-900 scrollbar-thin scrollbar-track-transparent scrollbar-thumb-charcoal-600" >
333
+ < div className = "overflow-y-auto bg-charcoal-900 scrollbar-thin scrollbar-track-transparent scrollbar-thumb-charcoal-600" >
301
334
< div className = "max-h-[70vh] min-h-40 overflow-y-auto bg-charcoal-900 scrollbar-thin scrollbar-track-transparent scrollbar-thumb-charcoal-600" >
302
335
< JSONEditor
303
336
autoFocus
@@ -315,32 +348,51 @@ function CompleteManualWaitpointForm({ waitpoint }: { waitpoint: { friendlyId: s
315
348
/>
316
349
</ div >
317
350
</ div >
318
- < div className = "bg-charcoal-900 px-2" >
319
- < div className = "mb-2 flex items-center justify-end gap-2 border-t border-grid-dimmed pt-2" >
320
- < Button
321
- variant = "secondary/small"
322
- type = "submit"
323
- disabled = { isLoading }
324
- LeadingIcon = { isLoading ? "spinner" : undefined }
325
- >
326
- { isLoading ? "Completing…" : "Complete waitpoint" }
327
- </ Button >
328
- </ div >
351
+ < div className = "flex items-center justify-end gap-2 border-t border-grid-dimmed bg-background-dimmed px-2" >
352
+ < Button
353
+ variant = "secondary/medium"
354
+ type = "submit"
355
+ disabled = { isLoading }
356
+ LeadingIcon = { isLoading ? "spinner" : undefined }
357
+ >
358
+ { isLoading ? "Completing…" : "Complete waitpoint" }
359
+ </ Button >
329
360
</ div >
330
361
</ Form >
331
- < CodeBlock
332
- rowTitle = {
333
- < span className = "-ml-1 flex items-center gap-1 text-text-dimmed" >
334
- < InformationCircleIcon className = "size-5 shrink-0 text-text-dimmed" />
335
- To complete this waitpoint in your code use:
336
- </ span >
337
- }
338
- code = { `
339
- await wait.completeToken<YourType>(tokenId,
340
- output
341
- );` }
342
- showLineNumbers = { false }
343
- />
344
362
</ >
345
363
) ;
346
364
}
365
+
366
+ export function ForceTimeout ( { waitpoint } : { waitpoint : { friendlyId : string } } ) {
367
+ const location = useLocation ( ) ;
368
+ const navigation = useNavigation ( ) ;
369
+ const isLoading = navigation . state !== "idle" ;
370
+ const organization = useOrganization ( ) ;
371
+ const project = useProject ( ) ;
372
+ const formAction = `/resources/orgs/${ organization . slug } /projects/${ project . slug } /waitpoints/${ waitpoint . friendlyId } /complete` ;
373
+
374
+ return (
375
+ < Form action = { formAction } method = "post" >
376
+ < input type = "hidden" name = "type" value = { "MANUAL" } />
377
+ < input type = "hidden" name = "isTimeout" value = { "1" } />
378
+ < input
379
+ type = "hidden"
380
+ name = "successRedirect"
381
+ value = { `${ location . pathname } ${ location . search } ` }
382
+ />
383
+ < input
384
+ type = "hidden"
385
+ name = "failureRedirect"
386
+ value = { `${ location . pathname } ${ location . search } ` }
387
+ />
388
+ < Button
389
+ variant = "tertiary/small"
390
+ type = "submit"
391
+ disabled = { isLoading }
392
+ LeadingIcon = { isLoading ? "spinner" : undefined }
393
+ >
394
+ { isLoading ? "Forcing timeout…" : "Force timeout" }
395
+ </ Button >
396
+ </ Form >
397
+ ) ;
398
+ }
0 commit comments