Skip to content

Commit 2da2516

Browse files
committed
Update run options when selecting a recent run
1 parent 96b8596 commit 2da2516

4 files changed

Lines changed: 110 additions & 16 deletions

File tree

apps/webapp/app/components/primitives/DurationPicker.tsx

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ export interface DurationPickerProps {
77
id?: string; // used for the hidden input for form submission
88
name?: string; // used for the hidden input for form submission
99
defaultValueSeconds?: number;
10+
value?: number;
1011
onChange?: (totalSeconds: number) => void;
1112
variant?: "small" | "medium";
1213
showClearButton?: boolean;
@@ -15,13 +16,17 @@ export interface DurationPickerProps {
1516
export function DurationPicker({
1617
name,
1718
defaultValueSeconds: defaultValue = 0,
19+
value: controlledValue,
1820
onChange,
1921
variant = "small",
2022
showClearButton = true,
2123
}: DurationPickerProps) {
22-
const defaultHours = Math.floor(defaultValue / 3600);
23-
const defaultMinutes = Math.floor((defaultValue % 3600) / 60);
24-
const defaultSeconds = defaultValue % 60;
24+
// Use controlled value if provided, otherwise use default
25+
const initialValue = controlledValue ?? defaultValue;
26+
27+
const defaultHours = Math.floor(initialValue / 3600);
28+
const defaultMinutes = Math.floor((initialValue % 3600) / 60);
29+
const defaultSeconds = initialValue % 60;
2530

2631
const [hours, setHours] = useState<number>(defaultHours);
2732
const [minutes, setMinutes] = useState<number>(defaultMinutes);
@@ -35,6 +40,19 @@ export function DurationPicker({
3540

3641
const isEmpty = hours === 0 && minutes === 0 && seconds === 0;
3742

43+
// Sync internal state with external value changes
44+
useEffect(() => {
45+
if (controlledValue !== undefined && controlledValue !== totalSeconds) {
46+
const newHours = Math.floor(controlledValue / 3600);
47+
const newMinutes = Math.floor((controlledValue % 3600) / 60);
48+
const newSeconds = controlledValue % 60;
49+
50+
setHours(newHours);
51+
setMinutes(newMinutes);
52+
setSeconds(newSeconds);
53+
}
54+
}, [controlledValue]);
55+
3856
useEffect(() => {
3957
onChange?.(totalSeconds);
4058
}, [totalSeconds, onChange]);

apps/webapp/app/components/runs/v3/RunTagInput.tsx

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { useCallback, useState, type KeyboardEvent } from "react";
1+
import { useCallback, useState, useEffect, type KeyboardEvent } from "react";
22
import { AnimatePresence, motion } from "framer-motion";
33
import { Input } from "~/components/primitives/Input";
44
import { RunTag } from "./RunTag";
@@ -7,6 +7,7 @@ interface TagInputProps {
77
id?: string; // used for the hidden input for form submission
88
name?: string; // used for the hidden input for form submission
99
defaultTags?: string[];
10+
tags?: string[];
1011
placeholder?: string;
1112
variant?: "small" | "medium";
1213
maxTags?: number;
@@ -18,15 +19,26 @@ export function RunTagInput({
1819
id,
1920
name,
2021
defaultTags = [],
22+
tags: controlledTags,
2123
placeholder = "Type and press Enter to add tags",
2224
variant = "small",
2325
maxTags = 10,
2426
maxTagLength = 128,
2527
onTagsChange,
2628
}: TagInputProps) {
27-
const [tags, setTags] = useState<string[]>(defaultTags);
29+
// Use controlled tags if provided, otherwise use default
30+
const initialTags = controlledTags ?? defaultTags;
31+
32+
const [tags, setTags] = useState<string[]>(initialTags);
2833
const [inputValue, setInputValue] = useState("");
2934

35+
// Sync internal state with external tag changes
36+
useEffect(() => {
37+
if (controlledTags !== undefined) {
38+
setTags(controlledTags);
39+
}
40+
}, [controlledTags]);
41+
3042
const addTag = useCallback(
3143
(tagText: string) => {
3244
const trimmedTag = tagText.trim();

apps/webapp/app/presenters/v3/TestTaskPresenter.server.ts

Lines changed: 25 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { type PrismaClient, prisma, sqlDatabaseSchema } from "~/db.server";
44
import { getTimezones } from "~/utils/timezones.server";
55
import { findCurrentWorkerDeployment } from "~/v3/models/workerDeployment.server";
66
import { queueTypeFromType } from "./QueueRetrievePresenter.server";
7+
import parse from "parse-duration";
78

89
type TestTaskOptions = {
910
userId: string;
@@ -70,20 +71,30 @@ type RawRun = {
7071
runtimeEnvironmentId: string;
7172
seedMetadata?: string;
7273
seedMetadataType?: string;
74+
concurrencyKey?: string;
75+
maxAttempts?: number;
76+
maxDurationInSeconds?: number;
77+
machinePreset?: string;
78+
ttl?: string;
79+
idempotencyKey?: string;
80+
runTags: string[];
7381
};
7482

75-
export type StandardRun = Omit<RawRun, "number"> & {
83+
export type StandardRun = Omit<RawRun, "number" | "ttl"> & {
7684
number: number;
85+
metadata?: string;
86+
ttlSeconds?: number;
7787
};
7888

79-
export type ScheduledRun = Omit<RawRun, "number" | "payload"> & {
89+
export type ScheduledRun = Omit<RawRun, "number" | "payload" | "ttl"> & {
8090
number: number;
8191
payload: {
8292
timestamp: Date;
8393
lastTimestamp?: Date;
8494
externalId?: string;
8595
timezone: string;
8696
};
97+
ttlSeconds?: number;
8798
};
8899

89100
export class TestTaskPresenter {
@@ -148,7 +159,7 @@ export class TestTaskPresenter {
148159
createdAt: "desc",
149160
},
150161
// only the latest version has active workers in development,
151-
// so we hide the older versions to avoid runs getting stuck
162+
// so we hide the older versions to avoid confusion from stuck runs
152163
take: environment.type === "DEVELOPMENT" ? 1 : 20,
153164
})
154165
).map((v) => v.version);
@@ -182,7 +193,13 @@ export class TestTaskPresenter {
182193
taskr."payloadType",
183194
taskr."seedMetadata",
184195
taskr."seedMetadataType",
185-
taskr."runtimeEnvironmentId"
196+
taskr."runtimeEnvironmentId",
197+
taskr."concurrencyKey",
198+
taskr."maxAttempts",
199+
taskr."maxDurationInSeconds",
200+
taskr."machinePreset",
201+
taskr."ttl",
202+
taskr."runTags"
186203
FROM
187204
taskruns AS taskr
188205
WHERE
@@ -223,7 +240,8 @@ export class TestTaskPresenter {
223240
metadata: r.seedMetadata
224241
? await prettyPrintPacket(r.seedMetadata, r.seedMetadataType)
225242
: undefined,
226-
};
243+
ttlSeconds: r.ttl ? parse(r.ttl, "s") ?? undefined : undefined,
244+
} satisfies StandardRun;
227245
})
228246
),
229247
latestVersions,
@@ -249,7 +267,8 @@ export class TestTaskPresenter {
249267
...r,
250268
number,
251269
payload: payload.data,
252-
};
270+
ttlSeconds: r.ttl ? parse(r.ttl, "s") ?? undefined : undefined,
271+
} satisfies ScheduledRun;
253272
}
254273
})
255274
)

apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.test.tasks.$taskParam/route.tsx

Lines changed: 50 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -238,6 +238,19 @@ function StandardTaskForm({
238238

239239
const currentMetadataJson = useRef<string>(defaultMetadataJson);
240240

241+
const [ttlValue, setTtlValue] = useState<number | undefined>(selectedCodeSample?.ttlSeconds);
242+
const [concurrencyKeyValue, setConcurrencyKeyValue] = useState<string | undefined>(
243+
selectedCodeSample?.concurrencyKey
244+
);
245+
const [queueValue, setQueueValue] = useState<string | undefined>(selectedCodeSample?.queue);
246+
const [maxAttemptsValue, setMaxAttemptsValue] = useState<number | undefined>(
247+
selectedCodeSample?.maxAttempts
248+
);
249+
const [maxDurationValue, setMaxDurationValue] = useState<number | undefined>(
250+
selectedCodeSample?.maxDurationInSeconds
251+
);
252+
const [tagsValue, setTagsValue] = useState<string[]>(selectedCodeSample?.runTags ?? []);
253+
241254
const queueFetcher = useFetcher<typeof queuesLoader>();
242255

243256
useEffect(() => {
@@ -402,7 +415,12 @@ function StandardTaskForm({
402415
</InputGroup>
403416
<InputGroup>
404417
<Label>TTL</Label>
405-
<DurationPicker name={ttlSeconds.name} id={ttlSeconds.id} />
418+
<DurationPicker
419+
name={ttlSeconds.name}
420+
id={ttlSeconds.id}
421+
value={ttlValue}
422+
onChange={setTtlValue}
423+
/>
406424
<FormError id={ttlSeconds.errorId}>{ttlSeconds.error}</FormError>
407425
</InputGroup>
408426
<InputGroup>
@@ -430,7 +448,8 @@ function StandardTaskForm({
430448
dropdownIcon
431449
items={queues}
432450
filter={{ keys: ["label"] }}
433-
defaultValue={undefined}
451+
value={queueValue}
452+
setValue={setQueueValue}
434453
>
435454
{(matches) =>
436455
matches.map((queueItem) => (
@@ -461,7 +480,12 @@ function StandardTaskForm({
461480
</InputGroup>
462481
<InputGroup>
463482
<Label htmlFor={concurrencyKey.id}>Concurrency key</Label>
464-
<Input {...conform.input(concurrencyKey, { type: "text" })} variant="small" />
483+
<Input
484+
{...conform.input(concurrencyKey, { type: "text" })}
485+
variant="small"
486+
value={concurrencyKeyValue ?? ""}
487+
onChange={(e) => setConcurrencyKeyValue(e.target.value)}
488+
/>
465489
<FormError id={concurrencyKey.errorId}>{concurrencyKey.error}</FormError>
466490
</InputGroup>
467491
<InputGroup>
@@ -471,6 +495,10 @@ function StandardTaskForm({
471495
className="[&::-webkit-inner-spin-button]:appearance-none"
472496
variant="small"
473497
min={1}
498+
value={maxAttemptsValue}
499+
onChange={(e) =>
500+
setMaxAttemptsValue(e.target.value ? parseInt(e.target.value) : undefined)
501+
}
474502
onKeyDown={(e) => {
475503
// only allow entering integers > 1
476504
if (["-", "+", ".", "e", "E"].includes(e.key)) {
@@ -488,12 +516,23 @@ function StandardTaskForm({
488516
</InputGroup>
489517
<InputGroup>
490518
<Label>Max duration</Label>
491-
<DurationPicker name={maxDurationSeconds.name} id={maxDurationSeconds.id} />
519+
<DurationPicker
520+
name={maxDurationSeconds.name}
521+
id={maxDurationSeconds.id}
522+
value={maxDurationValue}
523+
onChange={setMaxDurationValue}
524+
/>
492525
<FormError id={maxDurationSeconds.errorId}>{maxDurationSeconds.error}</FormError>
493526
</InputGroup>
494527
<InputGroup>
495528
<Label htmlFor={tags.id}>Tags</Label>
496-
<RunTagInput name={tags.name} id={tags.id} variant="small" />
529+
<RunTagInput
530+
name={tags.name}
531+
id={tags.id}
532+
variant="small"
533+
tags={tagsValue}
534+
onTagsChange={setTagsValue}
535+
/>
497536
<FormError id={tags.errorId}>{tags.error}</FormError>
498537
</InputGroup>
499538
<InputGroup>
@@ -554,6 +593,12 @@ function StandardTaskForm({
554593
run.seedMetadata && setMetadata(run.seedMetadata);
555594
setSelectedCodeSampleId(run.id);
556595
setIsRecentRunsPopoverOpen(false);
596+
setTtlValue(run.ttlSeconds);
597+
setConcurrencyKeyValue(run.concurrencyKey);
598+
setMaxAttemptsValue(run.maxAttempts);
599+
setMaxDurationValue(run.maxDurationInSeconds);
600+
setTagsValue(run.runTags ?? []);
601+
setQueueValue(run.queue);
557602
}}
558603
className="flex w-full items-center gap-2 rounded-sm px-2 py-2 outline-none transition-colors focus-custom hover:bg-charcoal-900 "
559604
>

0 commit comments

Comments
 (0)