From d30262294d477ca7083f40201393457385e125f1 Mon Sep 17 00:00:00 2001 From: myftija Date: Thu, 3 Jul 2025 10:02:35 +0200 Subject: [PATCH 01/35] Implement a new primitive UI component for picking durations --- .../components/primitives/DurationPicker.tsx | 177 ++++++++++++++++++ 1 file changed, 177 insertions(+) create mode 100644 apps/webapp/app/components/primitives/DurationPicker.tsx diff --git a/apps/webapp/app/components/primitives/DurationPicker.tsx b/apps/webapp/app/components/primitives/DurationPicker.tsx new file mode 100644 index 0000000000..bf405dee36 --- /dev/null +++ b/apps/webapp/app/components/primitives/DurationPicker.tsx @@ -0,0 +1,177 @@ +import { Input } from "~/components/primitives/Input"; +import { cn } from "~/utils/cn"; +import React, { useRef, useState, useEffect } from "react"; +import { Button } from "./Buttons"; + +export interface DurationPickerProps { + id?: string; // used for the hidden input for form submission + name?: string; // used for the hidden input for form submission + defaultValueSeconds?: number; + onChange?: (totalSeconds: number) => void; + variant?: "small" | "medium"; + showClearButton?: boolean; +} + +export function DurationPicker({ + name, + defaultValueSeconds: defaultValue = 0, + onChange, + variant = "small", + showClearButton = true, +}: DurationPickerProps) { + const defaultHours = Math.floor(defaultValue / 3600); + const defaultMinutes = Math.floor((defaultValue % 3600) / 60); + const defaultSeconds = defaultValue % 60; + + const [hours, setHours] = useState(defaultHours); + const [minutes, setMinutes] = useState(defaultMinutes); + const [seconds, setSeconds] = useState(defaultSeconds); + + const minuteRef = useRef(null); + const hourRef = useRef(null); + const secondRef = useRef(null); + + const totalSeconds = hours * 3600 + minutes * 60 + seconds; + + const isEmpty = hours === 0 && minutes === 0 && seconds === 0; + + useEffect(() => { + onChange?.(totalSeconds); + }, [totalSeconds, onChange]); + + const handleHoursChange = (e: React.ChangeEvent) => { + const value = parseInt(e.target.value) || 0; + setHours(Math.max(0, value)); + }; + + const handleMinutesChange = (e: React.ChangeEvent) => { + const value = parseInt(e.target.value) || 0; + if (value >= 60) { + setHours((prev) => prev + Math.floor(value / 60)); + setMinutes(value % 60); + return; + } + + setMinutes(Math.max(0, Math.min(59, value))); + }; + + const handleSecondsChange = (e: React.ChangeEvent) => { + const value = parseInt(e.target.value) || 0; + if (value >= 60) { + setMinutes((prev) => { + const newMinutes = prev + Math.floor(value / 60); + if (newMinutes >= 60) { + setHours((prevHours) => prevHours + Math.floor(newMinutes / 60)); + return newMinutes % 60; + } + return newMinutes; + }); + setSeconds(value % 60); + return; + } + + setSeconds(Math.max(0, Math.min(59, value))); + }; + + const handleKeyDown = ( + e: React.KeyboardEvent, + nextRef?: React.RefObject, + prevRef?: React.RefObject + ) => { + if (e.key === "Tab") { + return; + } + + if (e.key === "ArrowRight" && nextRef) { + e.preventDefault(); + nextRef.current?.focus(); + nextRef.current?.select(); + return; + } + + if (e.key === "ArrowLeft" && prevRef) { + e.preventDefault(); + prevRef.current?.focus(); + prevRef.current?.select(); + return; + } + }; + + const clearDuration = () => { + setHours(0); + setMinutes(0); + setSeconds(0); + hourRef.current?.focus(); + }; + + return ( +
+ + +
+
+ handleKeyDown(e, minuteRef)} + onFocus={(e) => e.target.select()} + type="number" + min={0} + inputMode="numeric" + /> + h +
+
+ handleKeyDown(e, secondRef, hourRef)} + onFocus={(e) => e.target.select()} + type="number" + min={0} + max={59} + inputMode="numeric" + /> + m +
+
+ handleKeyDown(e, undefined, minuteRef)} + onFocus={(e) => e.target.select()} + type="number" + min={0} + max={59} + inputMode="numeric" + /> + s +
+
+ + {showClearButton && ( + + )} +
+ ); +} From c78233675b9139e408d0b666ca692ac444425c07 Mon Sep 17 00:00:00 2001 From: myftija Date: Thu, 3 Jul 2025 10:06:47 +0200 Subject: [PATCH 02/35] Implement a new component to input run tags --- apps/webapp/app/components/runs/v3/RunTag.tsx | 59 +++++++++++++- .../app/components/runs/v3/RunTagInput.tsx | 81 +++++++++++++++++++ 2 files changed, 137 insertions(+), 3 deletions(-) create mode 100644 apps/webapp/app/components/runs/v3/RunTagInput.tsx diff --git a/apps/webapp/app/components/runs/v3/RunTag.tsx b/apps/webapp/app/components/runs/v3/RunTag.tsx index c7aab7cb09..14baeca1a3 100644 --- a/apps/webapp/app/components/runs/v3/RunTag.tsx +++ b/apps/webapp/app/components/runs/v3/RunTag.tsx @@ -3,11 +3,21 @@ import tagLeftPath from "./tag-left.svg"; import { SimpleTooltip } from "~/components/primitives/Tooltip"; import { Link } from "@remix-run/react"; import { cn } from "~/utils/cn"; -import { ClipboardCheckIcon, ClipboardIcon } from "lucide-react"; +import { ClipboardCheckIcon, ClipboardIcon, XIcon } from "lucide-react"; type Tag = string | { key: string; value: string }; -export function RunTag({ tag, to, tooltip }: { tag: string; to?: string; tooltip?: string }) { +export function RunTag({ + tag, + to, + tooltip, + action = { type: "copy" }, +}: { + tag: string; + action?: { type: "copy" } | { type: "delete"; onDelete: (tag: string) => void }; + to?: string; + tooltip?: string; +}) { const tagResult = useMemo(() => splitTag(tag), [tag]); const [isHovered, setIsHovered] = useState(false); @@ -57,7 +67,11 @@ export function RunTag({ tag, to, tooltip }: { tag: string; to?: string; tooltip return (
setIsHovered(false)}> {tagContent} - + {action.type === "delete" ? ( + + ) : ( + + )}
); } @@ -105,6 +119,45 @@ function CopyButton({ textToCopy, isHovered }: { textToCopy: string; isHovered: ); } +function DeleteButton({ + tag, + onDelete, + isHovered, +}: { + tag: string; + onDelete: (tag: string) => void; + isHovered: boolean; +}) { + const handleDelete = useCallback( + (e: React.MouseEvent) => { + e.preventDefault(); + e.stopPropagation(); + onDelete(tag); + }, + [tag, onDelete] + ); + + return ( + e.stopPropagation()} + className={cn( + "absolute -right-6 top-0 z-10 size-6 items-center justify-center rounded-r-sm border-y border-r border-charcoal-650 bg-charcoal-750", + isHovered ? "flex" : "hidden", + "text-text-dimmed hover:border-charcoal-600 hover:bg-charcoal-700 hover:text-rose-400" + )} + > + + + } + content="Remove tag" + disableHoverableContent + /> + ); +} + /** Takes a string and turns it into a tag * * If the string has 12 or fewer alpha characters followed by an underscore or colon then we return an object with a key and value diff --git a/apps/webapp/app/components/runs/v3/RunTagInput.tsx b/apps/webapp/app/components/runs/v3/RunTagInput.tsx new file mode 100644 index 0000000000..f73e3b6379 --- /dev/null +++ b/apps/webapp/app/components/runs/v3/RunTagInput.tsx @@ -0,0 +1,81 @@ +import { useCallback, useState, type KeyboardEvent } from "react"; +import { Input } from "~/components/primitives/Input"; +import { RunTag } from "./RunTag"; + +interface TagInputProps { + id?: string; // used for the hidden input for form submission + name?: string; // used for the hidden input for form submission + defaultTags?: string[]; + placeholder?: string; + variant?: "small" | "medium"; + onTagsChange?: (tags: string[]) => void; +} + +export function RunTagInput({ + id, + name, + defaultTags = [], + placeholder = "Type and press Enter to add tags", + variant = "small", + onTagsChange, +}: TagInputProps) { + const [tags, setTags] = useState(defaultTags); + const [inputValue, setInputValue] = useState(""); + + const addTag = useCallback( + (tagText: string) => { + const trimmedTag = tagText.trim(); + if (trimmedTag && !tags.includes(trimmedTag)) { + const newTags = [...tags, trimmedTag]; + setTags(newTags); + onTagsChange?.(newTags); + } + setInputValue(""); + }, + [tags, onTagsChange] + ); + + const removeTag = useCallback( + (tagToRemove: string) => { + const newTags = tags.filter((tag) => tag !== tagToRemove); + setTags(newTags); + onTagsChange?.(newTags); + }, + [tags, onTagsChange] + ); + + const handleKeyDown = useCallback( + (e: KeyboardEvent) => { + if (e.key === "Enter") { + e.preventDefault(); + addTag(inputValue); + } else if (e.key === "Backspace" && inputValue === "" && tags.length > 0) { + removeTag(tags[tags.length - 1]); + } + }, + [inputValue, addTag, removeTag, tags] + ); + + return ( +
+ + + setInputValue(e.target.value)} + onKeyDown={handleKeyDown} + placeholder={placeholder} + variant={variant} + /> + + {tags.length > 0 && ( +
+ {tags.map((tag, i) => ( + + ))} +
+ )} +
+ ); +} From caa5bcfc645c68b36e29fe500248cf2350939ede Mon Sep 17 00:00:00 2001 From: myftija Date: Thu, 3 Jul 2025 10:13:25 +0200 Subject: [PATCH 03/35] Expose all run options in the test run page --- .../route.tsx | 471 ++++++++++-------- .../webapp/app/v3/services/testTask.server.ts | 15 +- apps/webapp/app/v3/testTask.ts | 37 ++ 3 files changed, 308 insertions(+), 215 deletions(-) diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.test.tasks.$taskParam/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.test.tasks.$taskParam/route.tsx index c9d59a126b..f73bfb6b0f 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.test.tasks.$taskParam/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.test.tasks.$taskParam/route.tsx @@ -1,17 +1,17 @@ import { conform, useForm } from "@conform-to/react"; import { parse } from "@conform-to/zod"; import { BeakerIcon } from "@heroicons/react/20/solid"; -import { Form, useActionData, useSubmit } from "@remix-run/react"; +import { RectangleStackIcon } from "@heroicons/react/20/solid"; +import { Form, useActionData, useSubmit, useFetcher } from "@remix-run/react"; import { type ActionFunction, type LoaderFunctionArgs, json } from "@remix-run/server-runtime"; -import { type TaskRunStatus } from "@trigger.dev/database"; import { useCallback, useEffect, useRef, useState } from "react"; import { typedjson, useTypedLoaderData } from "remix-typedjson"; +import { TaskIcon } from "~/assets/icons/TaskIcon"; import { JSONEditor } from "~/components/code/JSONEditor"; -import { EnvironmentCombo, EnvironmentLabel } from "~/components/environments/EnvironmentLabel"; +import { EnvironmentCombo } from "~/components/environments/EnvironmentLabel"; +import { Badge } from "~/components/primitives/Badge"; import { Button } from "~/components/primitives/Buttons"; -import { Callout } from "~/components/primitives/Callout"; import { DateField } from "~/components/primitives/DateField"; -import { DateTime } from "~/components/primitives/DateTime"; import { Fieldset } from "~/components/primitives/Fieldset"; import { FormError } from "~/components/primitives/FormError"; import { Header2 } from "~/components/primitives/Headers"; @@ -19,17 +19,16 @@ import { Hint } from "~/components/primitives/Hint"; import { Input } from "~/components/primitives/Input"; import { InputGroup } from "~/components/primitives/InputGroup"; import { Label } from "~/components/primitives/Label"; +import { DurationPicker } from "~/components/primitives/DurationPicker"; import { Paragraph } from "~/components/primitives/Paragraph"; -import { RadioButtonCircle } from "~/components/primitives/RadioButton"; import { ResizableHandle, ResizablePanel, ResizablePanelGroup, } from "~/components/primitives/Resizable"; -import { Select } from "~/components/primitives/Select"; +import { Select, SelectItem } from "~/components/primitives/Select"; import { TabButton, TabContainer } from "~/components/primitives/Tabs"; import { TextLink } from "~/components/primitives/TextLink"; -import { TaskRunStatusCombo } from "~/components/runs/v3/TaskRunStatus"; import { TimezoneList } from "~/components/scheduled/timezones"; import { useEnvironment } from "~/hooks/useEnvironment"; import { useSearchParams } from "~/hooks/useSearchParam"; @@ -53,7 +52,7 @@ import { docsPath, v3RunSpanPath, v3TaskParamsSchema, v3TestPath } from "~/utils import { TestTaskService } from "~/v3/services/testTask.server"; import { OutOfEntitlementError } from "~/v3/services/triggerTask.server"; import { TestTaskData } from "~/v3/testTask"; - +import { RunTagInput } from "~/components/runs/v3/RunTagInput"; export const loader = async ({ request, params }: LoaderFunctionArgs) => { const userId = await requireUserId(request); const { projectParam, organizationSlug, envParam, taskParam } = v3TaskParamsSchema.parse(params); @@ -189,7 +188,6 @@ function StandardTaskForm({ task, runs }: { task: TestTask["task"]; runs: Standa const tab = value("tab"); //form submission - const submit = useSubmit(); const lastSubmission = useActionData(); //recent runs @@ -216,43 +214,62 @@ function StandardTaskForm({ task, runs }: { task: TestTask["task"]; runs: Standa const currentMetadataJson = useRef(defaultMetadataJson); - const submitForm = useCallback( - (e: React.FormEvent) => { - submit( - { - triggerSource: "STANDARD", - payload: currentPayloadJson.current, - metadata: currentMetadataJson.current, - taskIdentifier: task.taskIdentifier, - environmentId: environment.id, - }, - { - action: "", - method: "post", - } - ); - e.preventDefault(); + const fetcher = useFetcher(); + const [ + form, + { + environmentId, + payload, + metadata, + taskIdentifier, + delaySeconds, + ttlSeconds, + idempotencyKey, + idempotencyKeyTTLSeconds, + queue, + concurrencyKey, + maxAttempts, + maxDurationSeconds, + triggerSource, + tags, + version, }, - [currentPayloadJson, currentMetadataJson, task] - ); - - const [form, { environmentId, payload }] = useForm({ + ] = useForm({ id: "test-task", // TODO: type this lastSubmission: lastSubmission as any, + onSubmit(event, { formData }) { + event.preventDefault(); + + formData.set(payload.name, currentPayloadJson.current); + formData.set(metadata.name, currentMetadataJson.current); + + fetcher.submit(formData, { method: "POST" }); + }, onValidate({ formData }) { return parse(formData, { schema: TestTaskData }); }, }); + // fetch them in the loader + const dummyQueues = [ + { value: "default", label: "default", type: "task" as const, disabled: false }, + { value: "high-priority", label: "high-priority", type: "custom" as const, disabled: false }, + { value: "background", label: "background", type: "custom" as const, disabled: false }, + { value: "paused-queue", label: "paused-queue", type: "task" as const, disabled: true }, + { + value: "email-processing", + label: "email-processing", + type: "custom" as const, + disabled: false, + }, + ]; + return ( -
submitForm(e)} - > - + + + +
@@ -321,18 +338,118 @@ function StandardTaskForm({ task, runs }: { task: TestTask["task"]; runs: Standa
- - { - const run = runs.find((r) => r.id === id); - if (!run) return; - setPayload(run.payload); - run.seedMetadata && setMetadata(run.seedMetadata); - setSelectedCodeSampleId(id); - }} - /> + +
+
+ Options +
+
+ + + + {delaySeconds.error} + + + + + {ttlSeconds.error} + + + + + {idempotencyKey.error} + + + + + + {idempotencyKeyTTLSeconds.error} + + + + + + + + + + {concurrencyKey.error} + + + + + + + + + {maxDurationSeconds.error} + + + + + {tags.error} + + + + + {version.error} + + {form.error} +
+
@@ -423,118 +540,98 @@ function ScheduledTaskForm({ {...conform.input(environmentId, { type: "hidden" })} value={environment.id} /> - - -
-
- - - - setTimestampValue(val)} - granularity="second" - showNowButton - variant="medium" - utc - /> - - This is the timestamp of the CRON, it will come through to your run in the - payload. - - {timestamp.error} - - - - - setLastTimestampValue(val)} - granularity="second" - showNowButton - showClearButton - variant="medium" - utc - /> - - This is the timestamp of the previous run. You can use this in your code to find - new data since the previous run. This can be undefined if there hasn't been a - previous run. - - {lastTimestamp.error} - - - - - - The Timestamp and Last timestamp are in UTC so this just changes the timezone - string that comes through in the payload. - - {timezone.error} - - - - setExternalIdValue(e.target.value)} - /> - - Optionally, you can specify your own IDs (like a user ID) and then use it inside - the run function of your task. This allows you to have per-user CRON tasks.{" "} - Read the docs. - - {externalId.error} - -
-
-
- - - { - const run = runs.find((r) => r.id === id); - if (!run) return; - setSelectedCodeSampleId(id); - setTimestampValue(run.payload.timestamp); - setLastTimestampValue(run.payload.lastTimestamp); - setExternalIdValue(run.payload.externalId); - }} - /> - -
+
+
+ + + + setTimestampValue(val)} + granularity="second" + showNowButton + variant="medium" + utc + /> + + This is the timestamp of the CRON, it will come through to your run in the payload. + + {timestamp.error} + + + + + setLastTimestampValue(val)} + granularity="second" + showNowButton + showClearButton + variant="medium" + utc + /> + + This is the timestamp of the previous run. You can use this in your code to find new + data since the previous run. This can be undefined if there hasn't been a previous + run. + + {lastTimestamp.error} + + + + + + The Timestamp and Last timestamp are in UTC so this just changes the timezone string + that comes through in the payload. + + {timezone.error} + + + + setExternalIdValue(e.target.value)} + /> + + Optionally, you can specify your own IDs (like a user ID) and then use it inside the + run function of your task. This allows you to have per-user CRON tasks.{" "} + Read the docs. + + {externalId.error} + +
+
@@ -554,55 +651,3 @@ function ScheduledTaskForm({ ); } - -function RecentPayloads({ - runs, - selectedId, - onSelected, -}: { - runs: { - id: string; - createdAt: Date; - number: number; - status: TaskRunStatus; - }[]; - selectedId?: string; - onSelected: (id: string) => void; -}) { - return ( -
-
- Recent payloads -
- {runs.length === 0 ? ( - - Recent payloads will show here once you've completed a Run. - - ) : ( -
- {runs.map((run) => ( - - ))} -
- )} -
- ); -} diff --git a/apps/webapp/app/v3/services/testTask.server.ts b/apps/webapp/app/v3/services/testTask.server.ts index 740f64156e..f799acc2d4 100644 --- a/apps/webapp/app/v3/services/testTask.server.ts +++ b/apps/webapp/app/v3/services/testTask.server.ts @@ -1,6 +1,6 @@ import { stringifyIO } from "@trigger.dev/core/v3"; -import { AuthenticatedEnvironment } from "~/services/apiAuth.server"; -import { TestTaskData } from "../testTask"; +import { type AuthenticatedEnvironment } from "~/services/apiAuth.server"; +import { type TestTaskData } from "../testTask"; import { BaseService } from "./baseService.server"; import { TriggerTaskService } from "./triggerTask.server"; @@ -15,6 +15,17 @@ export class TestTaskService extends BaseService { options: { test: true, metadata: data.metadata, + delay: data.delaySeconds ? new Date(Date.now() + data.delaySeconds * 1000) : undefined, + ttl: data.ttlSeconds, + idempotencyKey: data.idempotencyKey, + idempotencyKeyTTL: data.idempotencyKeyTTLSeconds + ? `${data.idempotencyKeyTTLSeconds}s` + : undefined, + queue: data.queue ? { name: data.queue } : undefined, + concurrencyKey: data.concurrencyKey, + maxAttempts: data.maxAttempts, + maxDuration: data.maxDurationSeconds, + tags: data.tags, }, }); diff --git a/apps/webapp/app/v3/testTask.ts b/apps/webapp/app/v3/testTask.ts index 764f6b09f7..81e838ea43 100644 --- a/apps/webapp/app/v3/testTask.ts +++ b/apps/webapp/app/v3/testTask.ts @@ -56,6 +56,43 @@ export const TestTaskData = z z.object({ taskIdentifier: z.string(), environmentId: z.string(), + delaySeconds: z + .number() + .min(0) + .optional() + .transform((val) => (val === 0 ? undefined : val)), + ttlSeconds: z + .number() + .min(0) + .optional() + .transform((val) => (val === 0 ? undefined : val)), + idempotencyKey: z.string().optional(), + idempotencyKeyTTLSeconds: z + .number() + .min(0) + .optional() + .transform((val) => (val === 0 ? undefined : val)), + queue: z.string().optional(), + concurrencyKey: z.string().optional(), + maxAttempts: z.number().min(1).optional(), + maxDurationSeconds: z + .number() + .min(0) + .optional() + .transform((val) => (val === 0 ? undefined : val)), + tags: z + .string() + .optional() + .transform((val) => { + if (!val || val.trim() === "") { + return undefined; + } + return val + .split(",") + .map((tag) => tag.trim()) + .filter((tag) => tag.length > 0); + }), + version: z.string().optional(), }) ); From 2a89a5872c847823e6a5b62b7e6b07f1b85406fc Mon Sep 17 00:00:00 2001 From: myftija Date: Thu, 3 Jul 2025 10:22:16 +0200 Subject: [PATCH 04/35] Add subtle animations when adding/removing run tags in the test page --- .../app/components/runs/v3/RunTagInput.tsx | 36 +++++++++++++++++-- 1 file changed, 33 insertions(+), 3 deletions(-) diff --git a/apps/webapp/app/components/runs/v3/RunTagInput.tsx b/apps/webapp/app/components/runs/v3/RunTagInput.tsx index f73e3b6379..e6fb96c323 100644 --- a/apps/webapp/app/components/runs/v3/RunTagInput.tsx +++ b/apps/webapp/app/components/runs/v3/RunTagInput.tsx @@ -1,4 +1,5 @@ import { useCallback, useState, type KeyboardEvent } from "react"; +import { AnimatePresence, motion } from "framer-motion"; import { Input } from "~/components/primitives/Input"; import { RunTag } from "./RunTag"; @@ -71,9 +72,38 @@ export function RunTagInput({ {tags.length > 0 && (
- {tags.map((tag, i) => ( - - ))} + + {tags.map((tag, i) => ( + + + + ))} +
)}
From 4da50b8c36587c559439503e1470e874969ba6b1 Mon Sep 17 00:00:00 2001 From: myftija Date: Fri, 4 Jul 2025 13:29:31 +0200 Subject: [PATCH 05/35] Add a new resource endpoint for fetching queues --- .../v3/QueueListPresenter.server.ts | 18 ++++- .../v3/QueueRetrievePresenter.server.ts | 2 +- ...ects.$projectParam.env.$envParam.queues.ts | 70 +++++++++++++++++++ 3 files changed, 86 insertions(+), 4 deletions(-) create mode 100644 apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.queues.ts diff --git a/apps/webapp/app/presenters/v3/QueueListPresenter.server.ts b/apps/webapp/app/presenters/v3/QueueListPresenter.server.ts index 93531934bd..0e13fc6167 100644 --- a/apps/webapp/app/presenters/v3/QueueListPresenter.server.ts +++ b/apps/webapp/app/presenters/v3/QueueListPresenter.server.ts @@ -3,9 +3,16 @@ import { determineEngineVersion } from "~/v3/engineVersion.server"; import { engine } from "~/v3/runEngine.server"; import { BasePresenter } from "./basePresenter.server"; import { toQueueItem } from "./QueueRetrievePresenter.server"; +import { TaskQueueType } from "@trigger.dev/database"; const DEFAULT_ITEMS_PER_PAGE = 25; const MAX_ITEMS_PER_PAGE = 100; + +const typeToDBQueueType: Record<"task" | "custom", TaskQueueType> = { + task: TaskQueueType.VIRTUAL, + custom: TaskQueueType.NAMED, +}; + export class QueueListPresenter extends BasePresenter { private readonly perPage: number; @@ -18,13 +25,15 @@ export class QueueListPresenter extends BasePresenter { environment, query, page, + type, }: { environment: AuthenticatedEnvironment; query?: string; page: number; perPage?: number; + type?: "task" | "custom"; }) { - const hasFilters = query !== undefined && query.length > 0; + const hasFilters = (query !== undefined && query.length > 0) || type !== undefined; // Get total count for pagination const totalQueues = await this._replica.taskQueue.count({ @@ -37,6 +46,7 @@ export class QueueListPresenter extends BasePresenter { mode: "insensitive", } : undefined, + type: type ? typeToDBQueueType[type] : undefined, }, }); @@ -70,7 +80,7 @@ export class QueueListPresenter extends BasePresenter { return { success: true as const, - queues: await this.getQueuesWithPagination(environment, query, page), + queues: await this.getQueuesWithPagination(environment, query, page, type), pagination: { currentPage: page, totalPages: Math.ceil(totalQueues / this.perPage), @@ -84,7 +94,8 @@ export class QueueListPresenter extends BasePresenter { private async getQueuesWithPagination( environment: AuthenticatedEnvironment, query: string | undefined, - page: number + page: number, + type: "task" | "custom" | undefined ) { const queues = await this._replica.taskQueue.findMany({ where: { @@ -96,6 +107,7 @@ export class QueueListPresenter extends BasePresenter { mode: "insensitive", } : undefined, + type: type ? typeToDBQueueType[type] : undefined, }, select: { friendlyId: true, diff --git a/apps/webapp/app/presenters/v3/QueueRetrievePresenter.server.ts b/apps/webapp/app/presenters/v3/QueueRetrievePresenter.server.ts index 12e8e86291..04cc26a5ad 100644 --- a/apps/webapp/app/presenters/v3/QueueRetrievePresenter.server.ts +++ b/apps/webapp/app/presenters/v3/QueueRetrievePresenter.server.ts @@ -82,7 +82,7 @@ export class QueueRetrievePresenter extends BasePresenter { } } -function queueTypeFromType(type: TaskQueueType) { +export function queueTypeFromType(type: TaskQueueType) { switch (type) { case "NAMED": return "custom" as const; diff --git a/apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.queues.ts b/apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.queues.ts new file mode 100644 index 0000000000..4659271e9e --- /dev/null +++ b/apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.queues.ts @@ -0,0 +1,70 @@ +import { type LoaderFunctionArgs } from "@remix-run/server-runtime"; +import { z } from "zod"; +import { findProjectBySlug } from "~/models/project.server"; +import { findEnvironmentBySlug } from "~/models/runtimeEnvironment.server"; +import { QueueListPresenter } from "~/presenters/v3/QueueListPresenter.server"; +import { requireUserId } from "~/services/session.server"; +import { EnvironmentParamSchema } from "~/utils/pathBuilder"; + +const SearchParamsSchema = z.object({ + query: z.string().optional(), + page: z.coerce.number().min(1).default(1), + per_page: z.coerce.number().min(1).default(20), + type: z.enum(["task", "custom"]).optional(), +}); + +export async function loader({ request, params }: LoaderFunctionArgs) { + const userId = await requireUserId(request); + const { organizationSlug, projectParam, envParam } = EnvironmentParamSchema.parse(params); + + const url = new URL(request.url); + const { page, per_page, query, type } = SearchParamsSchema.parse( + Object.fromEntries(url.searchParams) + ); + + const project = await findProjectBySlug(organizationSlug, projectParam, userId); + if (!project) { + throw new Response(undefined, { + status: 404, + statusText: "Project not found", + }); + } + + const environment = await findEnvironmentBySlug(project.id, envParam, userId); + if (!environment) { + throw new Response(undefined, { + status: 404, + statusText: "Environment not found", + }); + } + + const presenter = new QueueListPresenter(per_page); + + const result = await presenter.call({ + environment: environment, + query, + page, + type, + }); + + if (!result.success) { + return { + queues: [], + currentPage: 1, + hasMore: false, + hasFilters: Boolean(query?.trim()) || Boolean(type), + }; + } + + return { + queues: result.queues.map((queue) => ({ + id: queue.id, + name: queue.name, + type: queue.type, + paused: queue.paused, + })), + currentPage: result.pagination.currentPage, + hasMore: result.pagination.currentPage < result.pagination.totalPages, + hasFilters: result.hasFilters, + }; +} From 8493fa0c18bb90a9d403258e4001bb3bcd8c6b53 Mon Sep 17 00:00:00 2001 From: myftija Date: Fri, 4 Jul 2025 13:34:50 +0200 Subject: [PATCH 06/35] Fetch usable queues for the selected task --- .../presenters/v3/TestTaskPresenter.server.ts | 81 ++++++++++---- .../route.tsx | 101 +++++++++++++----- .../app/v3/models/workerDeployment.server.ts | 1 + 3 files changed, 132 insertions(+), 51 deletions(-) diff --git a/apps/webapp/app/presenters/v3/TestTaskPresenter.server.ts b/apps/webapp/app/presenters/v3/TestTaskPresenter.server.ts index 6109cd1b17..47f32778f7 100644 --- a/apps/webapp/app/presenters/v3/TestTaskPresenter.server.ts +++ b/apps/webapp/app/presenters/v3/TestTaskPresenter.server.ts @@ -1,11 +1,14 @@ -import { ScheduledTaskPayload, parsePacket, prettyPrintPacket } from "@trigger.dev/core/v3"; +import { + QueueItem, + ScheduledTaskPayload, + parsePacket, + prettyPrintPacket, +} from "@trigger.dev/core/v3"; import { type RuntimeEnvironmentType, type TaskRunStatus } from "@trigger.dev/database"; import { type PrismaClient, prisma, sqlDatabaseSchema } from "~/db.server"; import { getTimezones } from "~/utils/timezones.server"; -import { - type BackgroundWorkerTaskSlim, - findCurrentWorkerDeployment, -} from "~/v3/models/workerDeployment.server"; +import { findCurrentWorkerDeployment } from "~/v3/models/workerDeployment.server"; +import { queueTypeFromType, toQueueItem } from "./QueueRetrievePresenter.server"; type TestTaskOptions = { userId: string; @@ -27,11 +30,23 @@ type Task = { export type TestTask = | { triggerSource: "STANDARD"; + queue?: { + id: string; + name: string; + type: "custom" | "task"; + paused: boolean; + }; task: Task; runs: StandardRun[]; } | { triggerSource: "SCHEDULED"; + queue?: { + id: string; + name: string; + type: "custom" | "task"; + paused: boolean; + }; task: Task; possibleTimezones: string[]; runs: ScheduledRun[]; @@ -49,6 +64,7 @@ export type TestTaskResult = type RawRun = { id: string; number: BigInt; + queue: string; friendlyId: string; createdAt: Date; status: TaskRunStatus; @@ -86,23 +102,20 @@ export class TestTaskPresenter { environment, taskIdentifier, }: TestTaskOptions): Promise { - let task: BackgroundWorkerTaskSlim | null = null; - if (environment.type !== "DEVELOPMENT") { - const deployment = await findCurrentWorkerDeployment({ environmentId: environment.id }); - if (deployment) { - task = deployment.worker?.tasks.find((t) => t.slug === taskIdentifier) ?? null; - } - } else { - task = await this.#prismaClient.backgroundWorkerTask.findFirst({ - where: { - slug: taskIdentifier, - runtimeEnvironmentId: environment.id, - }, - orderBy: { - createdAt: "desc", - }, - }); - } + const task = + environment.type !== "DEVELOPMENT" + ? ( + await findCurrentWorkerDeployment({ environmentId: environment.id }) + )?.worker?.tasks.find((t) => t.slug === taskIdentifier) + : await this.#prismaClient.backgroundWorkerTask.findFirst({ + where: { + slug: taskIdentifier, + runtimeEnvironmentId: environment.id, + }, + orderBy: { + createdAt: "desc", + }, + }); if (!task) { return { @@ -110,6 +123,21 @@ export class TestTaskPresenter { }; } + const taskQueue = task.queueId + ? await this.#prismaClient.taskQueue.findFirst({ + where: { + runtimeEnvironmentId: environment.id, + id: task.queueId, + }, + select: { + id: true, + name: true, + type: true, + paused: true, + }, + }) + : undefined; + const latestRuns = await this.#prismaClient.$queryRaw` WITH taskruns AS ( SELECT @@ -130,6 +158,7 @@ export class TestTaskPresenter { SELECT taskr.id, taskr.number, + taskr."queue", taskr."friendlyId", taskr."taskIdentifier", taskr."createdAt", @@ -159,6 +188,14 @@ export class TestTaskPresenter { foundTask: true, task: { triggerSource: "STANDARD", + queue: taskQueue + ? { + id: taskQueue.id, + name: taskQueue.name.replace(/^task\//, ""), + type: queueTypeFromType(taskQueue.type), + paused: taskQueue.paused, + } + : undefined, task: taskWithEnvironment, runs: await Promise.all( latestRuns.map(async (r) => { diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.test.tasks.$taskParam/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.test.tasks.$taskParam/route.tsx index f73bfb6b0f..4a4c64dc3e 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.test.tasks.$taskParam/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.test.tasks.$taskParam/route.tsx @@ -4,7 +4,7 @@ import { BeakerIcon } from "@heroicons/react/20/solid"; import { RectangleStackIcon } from "@heroicons/react/20/solid"; import { Form, useActionData, useSubmit, useFetcher } from "@remix-run/react"; import { type ActionFunction, type LoaderFunctionArgs, json } from "@remix-run/server-runtime"; -import { useCallback, useEffect, useRef, useState } from "react"; +import { useCallback, useEffect, useRef, useState, useMemo } from "react"; import { typedjson, useTypedLoaderData } from "remix-typedjson"; import { TaskIcon } from "~/assets/icons/TaskIcon"; import { JSONEditor } from "~/components/code/JSONEditor"; @@ -32,6 +32,7 @@ import { TextLink } from "~/components/primitives/TextLink"; import { TimezoneList } from "~/components/scheduled/timezones"; import { useEnvironment } from "~/hooks/useEnvironment"; import { useSearchParams } from "~/hooks/useSearchParam"; +import { useParams } from "@remix-run/react"; import { redirectBackWithErrorMessage, redirectWithErrorMessage, @@ -53,6 +54,7 @@ import { TestTaskService } from "~/v3/services/testTask.server"; import { OutOfEntitlementError } from "~/v3/services/triggerTask.server"; import { TestTaskData } from "~/v3/testTask"; import { RunTagInput } from "~/components/runs/v3/RunTagInput"; +import { type loader as queuesLoader } from "~/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.queues"; export const loader = async ({ request, params }: LoaderFunctionArgs) => { const userId = await requireUserId(request); const { projectParam, organizationSlug, envParam, taskParam } = v3TaskParamsSchema.parse(params); @@ -166,7 +168,13 @@ export default function Page() { switch (result.task.triggerSource) { case "STANDARD": { - return ; + return ( + + ); } case "SCHEDULED": { return ( @@ -182,10 +190,19 @@ export default function Page() { const startingJson = "{\n\n}"; -function StandardTaskForm({ task, runs }: { task: TestTask["task"]; runs: StandardRun[] }) { +function StandardTaskForm({ + task, + defaultQueue, + runs, +}: { + task: TestTask["task"]; + defaultQueue: TestTask["queue"]; + runs: StandardRun[]; +}) { const environment = useEnvironment(); const { value, replace } = useSearchParams(); const tab = value("tab"); + const params = useParams(); //form submission const lastSubmission = useActionData(); @@ -214,6 +231,46 @@ function StandardTaskForm({ task, runs }: { task: TestTask["task"]; runs: Standa const currentMetadataJson = useRef(defaultMetadataJson); + const queueFetcher = useFetcher(); + + useEffect(() => { + if (params.organizationSlug && params.projectParam && params.envParam) { + const searchParams = new URLSearchParams(); + searchParams.set("type", "custom"); + searchParams.set("per_page", "100"); + + queueFetcher.load( + `/resources/orgs/${params.organizationSlug}/projects/${params.projectParam}/env/${ + params.envParam + }/queues?${searchParams.toString()}` + ); + } + }, [params.organizationSlug, params.projectParam, params.envParam]); + + const queues = useMemo(() => { + const defaultQueueItem = defaultQueue + ? { + value: defaultQueue.type === "task" ? `task/${defaultQueue.name}` : defaultQueue.name, + label: defaultQueue.name, + type: defaultQueue.type, + paused: defaultQueue.paused, + } + : undefined; + + if (!queueFetcher.data?.queues) { + return defaultQueueItem ? [defaultQueueItem] : []; + } + + const customQueues = queueFetcher.data?.queues.map((queue) => ({ + value: queue.name, + label: queue.name, + type: queue.type, + paused: queue.paused, + })); + + return defaultQueueItem ? [defaultQueueItem, ...customQueues] : customQueues; + }, [queueFetcher.data?.queues, defaultQueue]); + const fetcher = useFetcher(); const [ form, @@ -251,20 +308,6 @@ function StandardTaskForm({ task, runs }: { task: TestTask["task"]; runs: Standa }, }); - // fetch them in the loader - const dummyQueues = [ - { value: "default", label: "default", type: "task" as const, disabled: false }, - { value: "high-priority", label: "high-priority", type: "custom" as const, disabled: false }, - { value: "background", label: "background", type: "custom" as const, disabled: false }, - { value: "paused-queue", label: "paused-queue", type: "task" as const, disabled: true }, - { - value: "email-processing", - label: "email-processing", - type: "custom" as const, - disabled: false, - }, - ]; - return (
@@ -372,32 +415,31 @@ function StandardTaskForm({ task, runs }: { task: TestTask["task"]; runs: Standa + {queue.error} diff --git a/apps/webapp/app/v3/models/workerDeployment.server.ts b/apps/webapp/app/v3/models/workerDeployment.server.ts index cdec37d1b7..66e1024eb8 100644 --- a/apps/webapp/app/v3/models/workerDeployment.server.ts +++ b/apps/webapp/app/v3/models/workerDeployment.server.ts @@ -54,6 +54,7 @@ type WorkerDeploymentWithWorkerTasks = Prisma.WorkerDeploymentGetPayload<{ machineConfig: true; maxDurationInSeconds: true; queueConfig: true; + queueId: true; }; }; }; From 67d627bfc69f61a0668283314f75c62d1d0c613e Mon Sep 17 00:00:00 2001 From: myftija Date: Fri, 4 Jul 2025 13:35:16 +0200 Subject: [PATCH 07/35] Fix width display issue in the select component --- apps/webapp/app/components/primitives/Select.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/webapp/app/components/primitives/Select.tsx b/apps/webapp/app/components/primitives/Select.tsx index 2853f0b59c..f471065d01 100644 --- a/apps/webapp/app/components/primitives/Select.tsx +++ b/apps/webapp/app/components/primitives/Select.tsx @@ -615,7 +615,7 @@ export function SelectPopover({ unmountOnHide={unmountOnHide} className={cn( "z-50 flex flex-col overflow-clip rounded border border-charcoal-700 bg-background-bright shadow-md outline-none animate-in fade-in-40", - "min-w-[max(180px,calc(var(--popover-anchor-width)+0.5rem))]", + "min-w-[max(180px,var(--popover-anchor-width))]", "max-w-[min(480px,var(--popover-available-width))]", "max-h-[min(600px,var(--popover-available-height))]", "origin-[var(--popover-transform-origin)]", From 3317686cf2953b45aa975ef2d208fe4cce360cf0 Mon Sep 17 00:00:00 2001 From: myftija Date: Fri, 4 Jul 2025 14:25:45 +0200 Subject: [PATCH 08/35] Enable locking a run to a version from the test page --- .../presenters/v3/TestTaskPresenter.server.ts | 29 ++++++++++++++----- .../route.tsx | 11 +++++-- .../webapp/app/v3/services/testTask.server.ts | 1 + 3 files changed, 31 insertions(+), 10 deletions(-) diff --git a/apps/webapp/app/presenters/v3/TestTaskPresenter.server.ts b/apps/webapp/app/presenters/v3/TestTaskPresenter.server.ts index 47f32778f7..4568613388 100644 --- a/apps/webapp/app/presenters/v3/TestTaskPresenter.server.ts +++ b/apps/webapp/app/presenters/v3/TestTaskPresenter.server.ts @@ -1,14 +1,9 @@ -import { - QueueItem, - ScheduledTaskPayload, - parsePacket, - prettyPrintPacket, -} from "@trigger.dev/core/v3"; +import { ScheduledTaskPayload, parsePacket, prettyPrintPacket } from "@trigger.dev/core/v3"; import { type RuntimeEnvironmentType, type TaskRunStatus } from "@trigger.dev/database"; import { type PrismaClient, prisma, sqlDatabaseSchema } from "~/db.server"; import { getTimezones } from "~/utils/timezones.server"; import { findCurrentWorkerDeployment } from "~/v3/models/workerDeployment.server"; -import { queueTypeFromType, toQueueItem } from "./QueueRetrievePresenter.server"; +import { queueTypeFromType } from "./QueueRetrievePresenter.server"; type TestTaskOptions = { userId: string; @@ -38,6 +33,7 @@ export type TestTask = }; task: Task; runs: StandardRun[]; + latestVersions: string[]; } | { triggerSource: "SCHEDULED"; @@ -50,6 +46,7 @@ export type TestTask = task: Task; possibleTimezones: string[]; runs: ScheduledRun[]; + latestVersions: string[]; }; export type TestTaskResult = @@ -138,6 +135,22 @@ export class TestTaskPresenter { }) : undefined; + // last 20 versions should suffice + const latestVersions = ( + await this.#prismaClient.backgroundWorker.findMany({ + where: { + runtimeEnvironmentId: environment.id, + }, + select: { + version: true, + }, + orderBy: { + createdAt: "desc", + }, + take: 20, + }) + ).map((v) => v.version); + const latestRuns = await this.#prismaClient.$queryRaw` WITH taskruns AS ( SELECT @@ -211,6 +224,7 @@ export class TestTaskPresenter { }; }) ), + latestVersions, }, }; case "SCHEDULED": @@ -238,6 +252,7 @@ export class TestTaskPresenter { }) ) ).filter(Boolean), + latestVersions, }, }; } diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.test.tasks.$taskParam/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.test.tasks.$taskParam/route.tsx index 4a4c64dc3e..514afbfac4 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.test.tasks.$taskParam/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.test.tasks.$taskParam/route.tsx @@ -173,6 +173,7 @@ export default function Page() { task={result.task.task} defaultQueue={result.task.queue} runs={result.task.runs} + versions={result.task.latestVersions} /> ); } @@ -194,10 +195,12 @@ function StandardTaskForm({ task, defaultQueue, runs, + versions, }: { task: TestTask["task"]; defaultQueue: TestTask["queue"]; runs: StandardRun[]; + versions: string[]; }) { const environment = useEnvironment(); const { value, replace } = useSearchParams(); @@ -484,9 +487,11 @@ function StandardTaskForm({ placeholder="Select version" dropdownIcon > - - latest - + {versions.map((version, i) => ( + + {version} {i === 0 && "(latest)"} + + ))} {version.error} diff --git a/apps/webapp/app/v3/services/testTask.server.ts b/apps/webapp/app/v3/services/testTask.server.ts index f799acc2d4..d165db47d7 100644 --- a/apps/webapp/app/v3/services/testTask.server.ts +++ b/apps/webapp/app/v3/services/testTask.server.ts @@ -26,6 +26,7 @@ export class TestTaskService extends BaseService { maxAttempts: data.maxAttempts, maxDuration: data.maxDurationSeconds, tags: data.tags, + lockToVersion: data.version === "latest" ? undefined : data.version, }, }); From 90e903cb21b8a0f34dc32f0f3a3271223c4a78f3 Mon Sep 17 00:00:00 2001 From: myftija Date: Fri, 4 Jul 2025 14:32:43 +0200 Subject: [PATCH 09/35] Disable entering max attemps <0 --- .../route.tsx | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.test.tasks.$taskParam/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.test.tasks.$taskParam/route.tsx index 514afbfac4..83ede8848a 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.test.tasks.$taskParam/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.test.tasks.$taskParam/route.tsx @@ -465,8 +465,21 @@ function StandardTaskForm({ {...conform.input(maxAttempts, { type: "number" })} className="[&::-webkit-inner-spin-button]:appearance-none" variant="small" - min={0} + min={1} + onKeyDown={(e) => { + // only allow entering integers > 1 + if (["-", "+", ".", "e", "E"].includes(e.key)) { + e.preventDefault(); + } + }} + onBlur={(e) => { + const value = parseInt(e.target.value); + if (value < 1 && e.target.value !== "") { + e.target.value = "1"; + } + }} /> + {maxAttempts.error} From 2819ff9c9355fbb0d05e56a5355645b0dd45dbd2 Mon Sep 17 00:00:00 2001 From: myftija Date: Fri, 4 Jul 2025 16:01:32 +0200 Subject: [PATCH 10/35] Validate tags --- apps/webapp/app/components/runs/v3/RunTagInput.tsx | 14 +++++++++++--- apps/webapp/app/v3/testTask.ts | 6 ++++++ 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/apps/webapp/app/components/runs/v3/RunTagInput.tsx b/apps/webapp/app/components/runs/v3/RunTagInput.tsx index e6fb96c323..912bff1b88 100644 --- a/apps/webapp/app/components/runs/v3/RunTagInput.tsx +++ b/apps/webapp/app/components/runs/v3/RunTagInput.tsx @@ -9,6 +9,8 @@ interface TagInputProps { defaultTags?: string[]; placeholder?: string; variant?: "small" | "medium"; + maxTags?: number; + maxTagLength?: number; onTagsChange?: (tags: string[]) => void; } @@ -18,6 +20,8 @@ export function RunTagInput({ defaultTags = [], placeholder = "Type and press Enter to add tags", variant = "small", + maxTags = 10, + maxTagLength = 128, onTagsChange, }: TagInputProps) { const [tags, setTags] = useState(defaultTags); @@ -26,14 +30,14 @@ export function RunTagInput({ const addTag = useCallback( (tagText: string) => { const trimmedTag = tagText.trim(); - if (trimmedTag && !tags.includes(trimmedTag)) { + if (trimmedTag && !tags.includes(trimmedTag) && tags.length < maxTags) { const newTags = [...tags, trimmedTag]; setTags(newTags); onTagsChange?.(newTags); } setInputValue(""); }, - [tags, onTagsChange] + [tags, onTagsChange, maxTags] ); const removeTag = useCallback( @@ -57,6 +61,8 @@ export function RunTagInput({ [inputValue, addTag, removeTag, tags] ); + const maxTagsReached = tags.length >= maxTags; + return (
@@ -66,8 +72,10 @@ export function RunTagInput({ value={inputValue} onChange={(e) => setInputValue(e.target.value)} onKeyDown={handleKeyDown} - placeholder={placeholder} + placeholder={maxTagsReached ? `A maximum of ${maxTags} tags is allowed` : placeholder} variant={variant} + disabled={maxTagsReached} + maxLength={maxTagLength} /> {tags.length > 0 && ( diff --git a/apps/webapp/app/v3/testTask.ts b/apps/webapp/app/v3/testTask.ts index 81e838ea43..7d4e29a9be 100644 --- a/apps/webapp/app/v3/testTask.ts +++ b/apps/webapp/app/v3/testTask.ts @@ -91,6 +91,12 @@ export const TestTaskData = z .split(",") .map((tag) => tag.trim()) .filter((tag) => tag.length > 0); + }) + .refine((tags) => !tags || tags.length <= 10, { + message: "Maximum 10 tags allowed", + }) + .refine((tags) => !tags || tags.every((tag) => tag.length <= 128), { + message: "Each tag must be at most 128 characters long", }), version: z.string().optional(), }) From ceb9bf2343b4577f8b0428e16d86f4b754c1f6b4 Mon Sep 17 00:00:00 2001 From: myftija Date: Fri, 4 Jul 2025 18:38:04 +0200 Subject: [PATCH 11/35] Add recent runs popover --- .../route.tsx | 95 +++++++++++++++---- 1 file changed, 78 insertions(+), 17 deletions(-) diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.test.tasks.$taskParam/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.test.tasks.$taskParam/route.tsx index 83ede8848a..b633a12d85 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.test.tasks.$taskParam/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.test.tasks.$taskParam/route.tsx @@ -1,8 +1,8 @@ import { conform, useForm } from "@conform-to/react"; import { parse } from "@conform-to/zod"; -import { BeakerIcon } from "@heroicons/react/20/solid"; -import { RectangleStackIcon } from "@heroicons/react/20/solid"; -import { Form, useActionData, useSubmit, useFetcher } from "@remix-run/react"; +import { BeakerIcon, RectangleStackIcon } from "@heroicons/react/20/solid"; +import { ClockIcon } from "@heroicons/react/24/outline"; +import { Form, useActionData, useFetcher } from "@remix-run/react"; import { type ActionFunction, type LoaderFunctionArgs, json } from "@remix-run/server-runtime"; import { useCallback, useEffect, useRef, useState, useMemo } from "react"; import { typedjson, useTypedLoaderData } from "remix-typedjson"; @@ -21,6 +21,8 @@ import { InputGroup } from "~/components/primitives/InputGroup"; import { Label } from "~/components/primitives/Label"; import { DurationPicker } from "~/components/primitives/DurationPicker"; import { Paragraph } from "~/components/primitives/Paragraph"; +import { Popover, PopoverContent, PopoverTrigger } from "~/components/primitives/Popover"; +import { SimpleTooltip } from "~/components/primitives/Tooltip"; import { ResizableHandle, ResizablePanel, @@ -55,6 +57,8 @@ import { OutOfEntitlementError } from "~/v3/services/triggerTask.server"; import { TestTaskData } from "~/v3/testTask"; import { RunTagInput } from "~/components/runs/v3/RunTagInput"; import { type loader as queuesLoader } from "~/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.queues"; +import { DateTime } from "~/components/primitives/DateTime"; +import { TaskRunStatusCombo } from "~/components/runs/v3/TaskRunStatus"; export const loader = async ({ request, params }: LoaderFunctionArgs) => { const userId = await requireUserId(request); const { projectParam, organizationSlug, envParam, taskParam } = v3TaskParamsSchema.parse(params); @@ -275,6 +279,7 @@ function StandardTaskForm({ }, [queueFetcher.data?.queues, defaultQueue]); const fetcher = useFetcher(); + const [isRecentRunsPopoverOpen, setIsRecentRunsPopoverOpen] = useState(false); const [ form, { @@ -513,21 +518,77 @@ function StandardTaskForm({
-
-
- - This test will run in - - +
+
+ + + {runs.length === 0 ? ( + + Recent runs + + } + content="No runs yet" + /> + ) : ( + + )} + + +
+
+ {runs.map((run) => ( + + ))} +
+
+
+
+
+
+
+ + This test will run in + + +
+
-
); From 96b85960490d0b291e0365970a890c8e580b2a5b Mon Sep 17 00:00:00 2001 From: myftija Date: Fri, 4 Jul 2025 18:45:16 +0200 Subject: [PATCH 12/35] Only show latest version for development environments --- apps/webapp/app/presenters/v3/TestTaskPresenter.server.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/apps/webapp/app/presenters/v3/TestTaskPresenter.server.ts b/apps/webapp/app/presenters/v3/TestTaskPresenter.server.ts index 4568613388..94a4039792 100644 --- a/apps/webapp/app/presenters/v3/TestTaskPresenter.server.ts +++ b/apps/webapp/app/presenters/v3/TestTaskPresenter.server.ts @@ -147,7 +147,9 @@ export class TestTaskPresenter { orderBy: { createdAt: "desc", }, - take: 20, + // only the latest version has active workers in development, + // so we hide the older versions to avoid runs getting stuck + take: environment.type === "DEVELOPMENT" ? 1 : 20, }) ).map((v) => v.version); From 2da25167379da6c0b0a0ab48cd10b0e388491851 Mon Sep 17 00:00:00 2001 From: myftija Date: Mon, 7 Jul 2025 09:30:21 +0200 Subject: [PATCH 13/35] Update run options when selecting a recent run --- .../components/primitives/DurationPicker.tsx | 24 +++++++- .../app/components/runs/v3/RunTagInput.tsx | 16 +++++- .../presenters/v3/TestTaskPresenter.server.ts | 31 +++++++++-- .../route.tsx | 55 +++++++++++++++++-- 4 files changed, 110 insertions(+), 16 deletions(-) diff --git a/apps/webapp/app/components/primitives/DurationPicker.tsx b/apps/webapp/app/components/primitives/DurationPicker.tsx index bf405dee36..8f6441452a 100644 --- a/apps/webapp/app/components/primitives/DurationPicker.tsx +++ b/apps/webapp/app/components/primitives/DurationPicker.tsx @@ -7,6 +7,7 @@ export interface DurationPickerProps { id?: string; // used for the hidden input for form submission name?: string; // used for the hidden input for form submission defaultValueSeconds?: number; + value?: number; onChange?: (totalSeconds: number) => void; variant?: "small" | "medium"; showClearButton?: boolean; @@ -15,13 +16,17 @@ export interface DurationPickerProps { export function DurationPicker({ name, defaultValueSeconds: defaultValue = 0, + value: controlledValue, onChange, variant = "small", showClearButton = true, }: DurationPickerProps) { - const defaultHours = Math.floor(defaultValue / 3600); - const defaultMinutes = Math.floor((defaultValue % 3600) / 60); - const defaultSeconds = defaultValue % 60; + // Use controlled value if provided, otherwise use default + const initialValue = controlledValue ?? defaultValue; + + const defaultHours = Math.floor(initialValue / 3600); + const defaultMinutes = Math.floor((initialValue % 3600) / 60); + const defaultSeconds = initialValue % 60; const [hours, setHours] = useState(defaultHours); const [minutes, setMinutes] = useState(defaultMinutes); @@ -35,6 +40,19 @@ export function DurationPicker({ const isEmpty = hours === 0 && minutes === 0 && seconds === 0; + // Sync internal state with external value changes + useEffect(() => { + if (controlledValue !== undefined && controlledValue !== totalSeconds) { + const newHours = Math.floor(controlledValue / 3600); + const newMinutes = Math.floor((controlledValue % 3600) / 60); + const newSeconds = controlledValue % 60; + + setHours(newHours); + setMinutes(newMinutes); + setSeconds(newSeconds); + } + }, [controlledValue]); + useEffect(() => { onChange?.(totalSeconds); }, [totalSeconds, onChange]); diff --git a/apps/webapp/app/components/runs/v3/RunTagInput.tsx b/apps/webapp/app/components/runs/v3/RunTagInput.tsx index 912bff1b88..4018813d4d 100644 --- a/apps/webapp/app/components/runs/v3/RunTagInput.tsx +++ b/apps/webapp/app/components/runs/v3/RunTagInput.tsx @@ -1,4 +1,4 @@ -import { useCallback, useState, type KeyboardEvent } from "react"; +import { useCallback, useState, useEffect, type KeyboardEvent } from "react"; import { AnimatePresence, motion } from "framer-motion"; import { Input } from "~/components/primitives/Input"; import { RunTag } from "./RunTag"; @@ -7,6 +7,7 @@ interface TagInputProps { id?: string; // used for the hidden input for form submission name?: string; // used for the hidden input for form submission defaultTags?: string[]; + tags?: string[]; placeholder?: string; variant?: "small" | "medium"; maxTags?: number; @@ -18,15 +19,26 @@ export function RunTagInput({ id, name, defaultTags = [], + tags: controlledTags, placeholder = "Type and press Enter to add tags", variant = "small", maxTags = 10, maxTagLength = 128, onTagsChange, }: TagInputProps) { - const [tags, setTags] = useState(defaultTags); + // Use controlled tags if provided, otherwise use default + const initialTags = controlledTags ?? defaultTags; + + const [tags, setTags] = useState(initialTags); const [inputValue, setInputValue] = useState(""); + // Sync internal state with external tag changes + useEffect(() => { + if (controlledTags !== undefined) { + setTags(controlledTags); + } + }, [controlledTags]); + const addTag = useCallback( (tagText: string) => { const trimmedTag = tagText.trim(); diff --git a/apps/webapp/app/presenters/v3/TestTaskPresenter.server.ts b/apps/webapp/app/presenters/v3/TestTaskPresenter.server.ts index 94a4039792..fd2cb973fa 100644 --- a/apps/webapp/app/presenters/v3/TestTaskPresenter.server.ts +++ b/apps/webapp/app/presenters/v3/TestTaskPresenter.server.ts @@ -4,6 +4,7 @@ import { type PrismaClient, prisma, sqlDatabaseSchema } from "~/db.server"; import { getTimezones } from "~/utils/timezones.server"; import { findCurrentWorkerDeployment } from "~/v3/models/workerDeployment.server"; import { queueTypeFromType } from "./QueueRetrievePresenter.server"; +import parse from "parse-duration"; type TestTaskOptions = { userId: string; @@ -70,13 +71,22 @@ type RawRun = { runtimeEnvironmentId: string; seedMetadata?: string; seedMetadataType?: string; + concurrencyKey?: string; + maxAttempts?: number; + maxDurationInSeconds?: number; + machinePreset?: string; + ttl?: string; + idempotencyKey?: string; + runTags: string[]; }; -export type StandardRun = Omit & { +export type StandardRun = Omit & { number: number; + metadata?: string; + ttlSeconds?: number; }; -export type ScheduledRun = Omit & { +export type ScheduledRun = Omit & { number: number; payload: { timestamp: Date; @@ -84,6 +94,7 @@ export type ScheduledRun = Omit & { externalId?: string; timezone: string; }; + ttlSeconds?: number; }; export class TestTaskPresenter { @@ -148,7 +159,7 @@ export class TestTaskPresenter { createdAt: "desc", }, // only the latest version has active workers in development, - // so we hide the older versions to avoid runs getting stuck + // so we hide the older versions to avoid confusion from stuck runs take: environment.type === "DEVELOPMENT" ? 1 : 20, }) ).map((v) => v.version); @@ -182,7 +193,13 @@ export class TestTaskPresenter { taskr."payloadType", taskr."seedMetadata", taskr."seedMetadataType", - taskr."runtimeEnvironmentId" + taskr."runtimeEnvironmentId", + taskr."concurrencyKey", + taskr."maxAttempts", + taskr."maxDurationInSeconds", + taskr."machinePreset", + taskr."ttl", + taskr."runTags" FROM taskruns AS taskr WHERE @@ -223,7 +240,8 @@ export class TestTaskPresenter { metadata: r.seedMetadata ? await prettyPrintPacket(r.seedMetadata, r.seedMetadataType) : undefined, - }; + ttlSeconds: r.ttl ? parse(r.ttl, "s") ?? undefined : undefined, + } satisfies StandardRun; }) ), latestVersions, @@ -249,7 +267,8 @@ export class TestTaskPresenter { ...r, number, payload: payload.data, - }; + ttlSeconds: r.ttl ? parse(r.ttl, "s") ?? undefined : undefined, + } satisfies ScheduledRun; } }) ) diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.test.tasks.$taskParam/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.test.tasks.$taskParam/route.tsx index b633a12d85..3197728d29 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.test.tasks.$taskParam/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.test.tasks.$taskParam/route.tsx @@ -238,6 +238,19 @@ function StandardTaskForm({ const currentMetadataJson = useRef(defaultMetadataJson); + const [ttlValue, setTtlValue] = useState(selectedCodeSample?.ttlSeconds); + const [concurrencyKeyValue, setConcurrencyKeyValue] = useState( + selectedCodeSample?.concurrencyKey + ); + const [queueValue, setQueueValue] = useState(selectedCodeSample?.queue); + const [maxAttemptsValue, setMaxAttemptsValue] = useState( + selectedCodeSample?.maxAttempts + ); + const [maxDurationValue, setMaxDurationValue] = useState( + selectedCodeSample?.maxDurationInSeconds + ); + const [tagsValue, setTagsValue] = useState(selectedCodeSample?.runTags ?? []); + const queueFetcher = useFetcher(); useEffect(() => { @@ -402,7 +415,12 @@ function StandardTaskForm({ - + {ttlSeconds.error} @@ -430,7 +448,8 @@ function StandardTaskForm({ dropdownIcon items={queues} filter={{ keys: ["label"] }} - defaultValue={undefined} + value={queueValue} + setValue={setQueueValue} > {(matches) => matches.map((queueItem) => ( @@ -461,7 +480,12 @@ function StandardTaskForm({ - + setConcurrencyKeyValue(e.target.value)} + /> {concurrencyKey.error} @@ -471,6 +495,10 @@ function StandardTaskForm({ className="[&::-webkit-inner-spin-button]:appearance-none" variant="small" min={1} + value={maxAttemptsValue} + onChange={(e) => + setMaxAttemptsValue(e.target.value ? parseInt(e.target.value) : undefined) + } onKeyDown={(e) => { // only allow entering integers > 1 if (["-", "+", ".", "e", "E"].includes(e.key)) { @@ -488,12 +516,23 @@ function StandardTaskForm({ - + {maxDurationSeconds.error} - + {tags.error} @@ -554,6 +593,12 @@ function StandardTaskForm({ run.seedMetadata && setMetadata(run.seedMetadata); setSelectedCodeSampleId(run.id); setIsRecentRunsPopoverOpen(false); + setTtlValue(run.ttlSeconds); + setConcurrencyKeyValue(run.concurrencyKey); + setMaxAttemptsValue(run.maxAttempts); + setMaxDurationValue(run.maxDurationInSeconds); + setTagsValue(run.runTags ?? []); + setQueueValue(run.queue); }} className="flex w-full items-center gap-2 rounded-sm px-2 py-2 outline-none transition-colors focus-custom hover:bg-charcoal-900 " > From 60802652da86fe7e2d581a9fc6cacf8cbf0902bf Mon Sep 17 00:00:00 2001 From: myftija Date: Mon, 7 Jul 2025 09:30:21 +0200 Subject: [PATCH 14/35] Rearrange the test page layout --- .../route.tsx | 179 +++++++++--------- 1 file changed, 89 insertions(+), 90 deletions(-) diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.test.tasks.$taskParam/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.test.tasks.$taskParam/route.tsx index 3197728d29..bf456817c0 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.test.tasks.$taskParam/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.test.tasks.$taskParam/route.tsx @@ -330,34 +330,96 @@ function StandardTaskForm({ }); return ( -
+ + +
+ { + replace({ tab: "payload" }); + }} + > + Payload + + + { + replace({ tab: "metadata" }); + }} + > + Metadata + +
+
+ + + {runs.length === 0 ? ( + + Recent runs + + } + content="No runs yet" + /> + ) : ( + + )} + + +
+
+ {runs.map((run) => ( + + ))} +
+
+
+
+
+
- +
- - { - replace({ tab: "payload" }); - }} - > - Payload - - - { - replace({ tab: "metadata" }); - }} - > - Metadata - -
- -
-
- Options -
-
+ +
+
@@ -557,67 +616,7 @@ function StandardTaskForm({
-
-
- - - {runs.length === 0 ? ( - - Recent runs - - } - content="No runs yet" - /> - ) : ( - - )} - - -
-
- {runs.map((run) => ( - - ))} -
-
-
-
-
+
From dddad15a608cefec7640db436c2c9679ea9dce44 Mon Sep 17 00:00:00 2001 From: myftija Date: Mon, 7 Jul 2025 09:30:21 +0200 Subject: [PATCH 15/35] Add subtle animation to the duration picker segments on focus --- .../components/primitives/DurationPicker.tsx | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/apps/webapp/app/components/primitives/DurationPicker.tsx b/apps/webapp/app/components/primitives/DurationPicker.tsx index 8f6441452a..e4f5af6520 100644 --- a/apps/webapp/app/components/primitives/DurationPicker.tsx +++ b/apps/webapp/app/components/primitives/DurationPicker.tsx @@ -127,7 +127,7 @@ export function DurationPicker({
-
+
- h + + h +
-
+
- m + + m +
-
+
- s + + s +
From 794cb82a4e405a2a342e43daab8950110fb0e498 Mon Sep 17 00:00:00 2001 From: myftija Date: Mon, 7 Jul 2025 09:30:21 +0200 Subject: [PATCH 16/35] Improve queue selection dropdown styling --- .../route.tsx | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.test.tasks.$taskParam/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.test.tasks.$taskParam/route.tsx index bf456817c0..e9382c524b 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.test.tasks.$taskParam/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.test.tasks.$taskParam/route.tsx @@ -515,16 +515,17 @@ function StandardTaskForm({ + ) : ( - + ) } >
- {queueItem.label} + {queueItem.label} {queueItem.paused && ( Paused From d3fc160a008774a89def8ffe9d9141f49a6aecac Mon Sep 17 00:00:00 2001 From: myftija Date: Mon, 7 Jul 2025 09:30:21 +0200 Subject: [PATCH 17/35] Fix disabled state issue for the SelectTrigger component --- apps/webapp/app/components/primitives/Select.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/webapp/app/components/primitives/Select.tsx b/apps/webapp/app/components/primitives/Select.tsx index f471065d01..75c6adeed8 100644 --- a/apps/webapp/app/components/primitives/Select.tsx +++ b/apps/webapp/app/components/primitives/Select.tsx @@ -327,6 +327,7 @@ export function SelectTrigger({ className )} ref={ref} + disabled={disabled} {...props} /> } From edcc24690bbc02ec1ec0f1e77ce241f6c3a1f4e7 Mon Sep 17 00:00:00 2001 From: myftija Date: Mon, 7 Jul 2025 09:30:21 +0200 Subject: [PATCH 18/35] Disable version selection field for dev envs --- .../webapp/app/presenters/v3/TestTaskPresenter.server.ts | 9 ++++++--- .../route.tsx | 9 +++++++++ 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/apps/webapp/app/presenters/v3/TestTaskPresenter.server.ts b/apps/webapp/app/presenters/v3/TestTaskPresenter.server.ts index fd2cb973fa..1b5bd20249 100644 --- a/apps/webapp/app/presenters/v3/TestTaskPresenter.server.ts +++ b/apps/webapp/app/presenters/v3/TestTaskPresenter.server.ts @@ -54,6 +54,7 @@ export type TestTaskResult = | { foundTask: true; task: TestTask; + disableVersionSelection: boolean; } | { foundTask: false; @@ -158,12 +159,12 @@ export class TestTaskPresenter { orderBy: { createdAt: "desc", }, - // only the latest version has active workers in development, - // so we hide the older versions to avoid confusion from stuck runs - take: environment.type === "DEVELOPMENT" ? 1 : 20, + take: 20, }) ).map((v) => v.version); + const disableVersionSelection = environment.type === "DEVELOPMENT"; + const latestRuns = await this.#prismaClient.$queryRaw` WITH taskruns AS ( SELECT @@ -246,6 +247,7 @@ export class TestTaskPresenter { ), latestVersions, }, + disableVersionSelection, }; case "SCHEDULED": const possibleTimezones = getTimezones(); @@ -275,6 +277,7 @@ export class TestTaskPresenter { ).filter(Boolean), latestVersions, }, + disableVersionSelection, }; } } diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.test.tasks.$taskParam/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.test.tasks.$taskParam/route.tsx index e9382c524b..e4a82b0565 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.test.tasks.$taskParam/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.test.tasks.$taskParam/route.tsx @@ -178,6 +178,7 @@ export default function Page() { defaultQueue={result.task.queue} runs={result.task.runs} versions={result.task.latestVersions} + disableVersionSelection={result.disableVersionSelection} /> ); } @@ -187,6 +188,7 @@ export default function Page() { task={result.task.task} runs={result.task.runs} possibleTimezones={result.task.possibleTimezones} + disableVersionSelection={result.disableVersionSelection} /> ); } @@ -200,11 +202,13 @@ function StandardTaskForm({ defaultQueue, runs, versions, + disableVersionSelection, }: { task: TestTask["task"]; defaultQueue: TestTask["queue"]; runs: StandardRun[]; versions: string[]; + disableVersionSelection: boolean; }) { const environment = useEnvironment(); const { value, replace } = useSearchParams(); @@ -603,6 +607,7 @@ function StandardTaskForm({ variant="tertiary/small" placeholder="Select version" dropdownIcon + disabled={disableVersionSelection} > {versions.map((version, i) => ( @@ -610,6 +615,9 @@ function StandardTaskForm({ ))} + {disableVersionSelection && ( + Only the latest version is available in the development environment. + )} {version.error} {form.error} @@ -647,6 +655,7 @@ function ScheduledTaskForm({ task: TestTask["task"]; runs: ScheduledRun[]; possibleTimezones: string[]; + disableVersionSelection: boolean; }) { const environment = useEnvironment(); const lastSubmission = useActionData(); From 0cb381b3220142cdb06ca5fabb69b150adaf40eb Mon Sep 17 00:00:00 2001 From: myftija Date: Mon, 7 Jul 2025 09:30:21 +0200 Subject: [PATCH 19/35] Add usage hints next to the run option fields --- .../route.tsx | 79 +++++++++++-------- 1 file changed, 45 insertions(+), 34 deletions(-) diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.test.tasks.$taskParam/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.test.tasks.$taskParam/route.tsx index e4a82b0565..e60b3492bd 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.test.tasks.$taskParam/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.test.tasks.$taskParam/route.tsx @@ -469,8 +469,8 @@ function StandardTaskForm({ -
-
+
+
@@ -484,23 +484,9 @@ function StandardTaskForm({ value={ttlValue} onChange={setTtlValue} /> + The run will expire if it hasn't started within the TTL (time to live). {ttlSeconds.error} - - - - {idempotencyKey.error} - - - - - - {idempotencyKeyTTLSeconds.error} - - Tags + setConcurrencyKeyValue(e.target.value)} + tags={tagsValue} + onTagsChange={setTagsValue} /> - {concurrencyKey.error} + Tags enable you to easily filter runs. + {tags.error} @@ -588,17 +576,6 @@ function StandardTaskForm({ /> {maxDurationSeconds.error} - - - - {tags.error} - + {idempotencyKey.error} + + Specify an idempotency key to ensure that a task is only triggered once with the + same key. + + + + + + By default, idempotency keys expire after 30 days. + + {idempotencyKeyTTLSeconds.error} + + + + + setConcurrencyKeyValue(e.target.value)} + /> + + Concurrency keys enable you limit concurrency by creating a separate queue for + each value of the key. + + {concurrencyKey.error} + {form.error}
From 712bd5ce5926033a85e749fb9c2bd5a953a969a2 Mon Sep 17 00:00:00 2001 From: myftija Date: Mon, 7 Jul 2025 09:30:21 +0200 Subject: [PATCH 20/35] Add machine preset to the run options list --- .../route.tsx | 80 ++++++++++++++----- .../webapp/app/v3/services/testTask.server.ts | 1 + apps/webapp/app/v3/testTask.ts | 2 + 3 files changed, 62 insertions(+), 21 deletions(-) diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.test.tasks.$taskParam/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.test.tasks.$taskParam/route.tsx index e60b3492bd..35b082cfbd 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.test.tasks.$taskParam/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.test.tasks.$taskParam/route.tsx @@ -196,6 +196,15 @@ export default function Page() { } const startingJson = "{\n\n}"; +const machinePresets = [ + "micro", + "small-1x", + "small-2x", + "medium-1x", + "medium-2x", + "large-1x", + "large-2x", +]; function StandardTaskForm({ task, @@ -247,6 +256,9 @@ function StandardTaskForm({ selectedCodeSample?.concurrencyKey ); const [queueValue, setQueueValue] = useState(selectedCodeSample?.queue); + const [machineValue, setMachineValue] = useState( + selectedCodeSample?.machinePreset + ); const [maxAttemptsValue, setMaxAttemptsValue] = useState( selectedCodeSample?.maxAttempts ); @@ -315,6 +327,7 @@ function StandardTaskForm({ triggerSource, tags, version, + machine, }, ] = useForm({ id: "test-task", @@ -401,6 +414,7 @@ function StandardTaskForm({ setMaxDurationValue(run.maxDurationInSeconds); setTagsValue(run.runTags ?? []); setQueueValue(run.queue); + setMachineValue(run.machinePreset); }} className="flex w-full items-center gap-2 rounded-sm px-2 py-2 outline-none transition-colors focus-custom hover:bg-charcoal-900 " > @@ -576,27 +590,6 @@ function StandardTaskForm({ /> {maxDurationSeconds.error} - - - - {disableVersionSelection && ( - Only the latest version is available in the development environment. - )} - {version.error} - @@ -631,6 +624,51 @@ function StandardTaskForm({ {concurrencyKey.error} + + + + This lets you override the machine preset specified in the task. + {machine.error} + + + + + {disableVersionSelection && ( + Only the latest version is available in the development environment. + )} + {version.error} + {form.error}
diff --git a/apps/webapp/app/v3/services/testTask.server.ts b/apps/webapp/app/v3/services/testTask.server.ts index d165db47d7..184ef67abf 100644 --- a/apps/webapp/app/v3/services/testTask.server.ts +++ b/apps/webapp/app/v3/services/testTask.server.ts @@ -26,6 +26,7 @@ export class TestTaskService extends BaseService { maxAttempts: data.maxAttempts, maxDuration: data.maxDurationSeconds, tags: data.tags, + machine: data.machine, lockToVersion: data.version === "latest" ? undefined : data.version, }, }); diff --git a/apps/webapp/app/v3/testTask.ts b/apps/webapp/app/v3/testTask.ts index 7d4e29a9be..b48b1fb59c 100644 --- a/apps/webapp/app/v3/testTask.ts +++ b/apps/webapp/app/v3/testTask.ts @@ -1,4 +1,5 @@ import { z } from "zod"; +import { MachinePresetName } from "@trigger.dev/core/v3/schemas"; export const TestTaskData = z .discriminatedUnion("triggerSource", [ @@ -75,6 +76,7 @@ export const TestTaskData = z queue: z.string().optional(), concurrencyKey: z.string().optional(), maxAttempts: z.number().min(1).optional(), + machine: MachinePresetName.optional(), maxDurationSeconds: z .number() .min(0) From 7b266d91eaaf1ca9f96e4f6af0dd9a42f40db028 Mon Sep 17 00:00:00 2001 From: myftija Date: Mon, 7 Jul 2025 09:30:21 +0200 Subject: [PATCH 21/35] Allow arbitrary queue inputs for v1 engine runs --- .../presenters/v3/TestTaskPresenter.server.ts | 34 ++++---- .../route.tsx | 86 +++++++++++-------- 2 files changed, 68 insertions(+), 52 deletions(-) diff --git a/apps/webapp/app/presenters/v3/TestTaskPresenter.server.ts b/apps/webapp/app/presenters/v3/TestTaskPresenter.server.ts index 1b5bd20249..3efe14bb67 100644 --- a/apps/webapp/app/presenters/v3/TestTaskPresenter.server.ts +++ b/apps/webapp/app/presenters/v3/TestTaskPresenter.server.ts @@ -55,6 +55,7 @@ export type TestTaskResult = foundTask: true; task: TestTask; disableVersionSelection: boolean; + allowArbitraryQueues: boolean; } | { foundTask: false; @@ -147,23 +148,24 @@ export class TestTaskPresenter { }) : undefined; - // last 20 versions should suffice - const latestVersions = ( - await this.#prismaClient.backgroundWorker.findMany({ - where: { - runtimeEnvironmentId: environment.id, - }, - select: { - version: true, - }, - orderBy: { - createdAt: "desc", - }, - take: 20, - }) - ).map((v) => v.version); + const backgroundWorkers = await this.#prismaClient.backgroundWorker.findMany({ + where: { + runtimeEnvironmentId: environment.id, + }, + select: { + version: true, + engine: true, + }, + orderBy: { + createdAt: "desc", + }, + take: 20, // last 20 versions should suffice + }); + + const latestVersions = backgroundWorkers.map((v) => v.version); const disableVersionSelection = environment.type === "DEVELOPMENT"; + const allowArbitraryQueues = backgroundWorkers[0]?.engine === "V1"; const latestRuns = await this.#prismaClient.$queryRaw` WITH taskruns AS ( @@ -248,6 +250,7 @@ export class TestTaskPresenter { latestVersions, }, disableVersionSelection, + allowArbitraryQueues, }; case "SCHEDULED": const possibleTimezones = getTimezones(); @@ -278,6 +281,7 @@ export class TestTaskPresenter { latestVersions, }, disableVersionSelection, + allowArbitraryQueues, }; } } diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.test.tasks.$taskParam/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.test.tasks.$taskParam/route.tsx index 35b082cfbd..b9ba73e7cc 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.test.tasks.$taskParam/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.test.tasks.$taskParam/route.tsx @@ -179,6 +179,7 @@ export default function Page() { runs={result.task.runs} versions={result.task.latestVersions} disableVersionSelection={result.disableVersionSelection} + allowArbitraryQueues={result.allowArbitraryQueues} /> ); } @@ -212,12 +213,14 @@ function StandardTaskForm({ runs, versions, disableVersionSelection, + allowArbitraryQueues, }: { task: TestTask["task"]; defaultQueue: TestTask["queue"]; runs: StandardRun[]; versions: string[]; disableVersionSelection: boolean; + allowArbitraryQueues: boolean; }) { const environment = useEnvironment(); const { value, replace } = useSearchParams(); @@ -503,43 +506,52 @@ function StandardTaskForm({ - + {allowArbitraryQueues ? ( + setQueueValue(e.target.value)} + /> + ) : ( + + )} {queue.error} From 4987c1778c5eca4c5be470459f3bf6efa0db3670 Mon Sep 17 00:00:00 2001 From: myftija Date: Mon, 7 Jul 2025 09:30:21 +0200 Subject: [PATCH 22/35] Show truncated run ID instead of run numbers for recent runs Run numbers will soon get deprecated due to contention issues --- .../presenters/v3/TestTaskPresenter.server.ts | 35 +++++++------------ .../route.tsx | 6 ++-- 2 files changed, 17 insertions(+), 24 deletions(-) diff --git a/apps/webapp/app/presenters/v3/TestTaskPresenter.server.ts b/apps/webapp/app/presenters/v3/TestTaskPresenter.server.ts index 3efe14bb67..d6c58febb6 100644 --- a/apps/webapp/app/presenters/v3/TestTaskPresenter.server.ts +++ b/apps/webapp/app/presenters/v3/TestTaskPresenter.server.ts @@ -63,7 +63,6 @@ export type TestTaskResult = type RawRun = { id: string; - number: BigInt; queue: string; friendlyId: string; createdAt: Date; @@ -82,14 +81,12 @@ type RawRun = { runTags: string[]; }; -export type StandardRun = Omit & { - number: number; +export type StandardRun = Omit & { metadata?: string; ttlSeconds?: number; }; -export type ScheduledRun = Omit & { - number: number; +export type ScheduledRun = Omit & { payload: { timestamp: Date; lastTimestamp?: Date; @@ -186,7 +183,6 @@ export class TestTaskPresenter { ) SELECT taskr.id, - taskr.number, taskr."queue", taskr."friendlyId", taskr."taskIdentifier", @@ -233,19 +229,17 @@ export class TestTaskPresenter { : undefined, task: taskWithEnvironment, runs: await Promise.all( - latestRuns.map(async (r) => { - const number = Number(r.number); - - return { - ...r, - number, - payload: await prettyPrintPacket(r.payload, r.payloadType), - metadata: r.seedMetadata - ? await prettyPrintPacket(r.seedMetadata, r.seedMetadataType) - : undefined, - ttlSeconds: r.ttl ? parse(r.ttl, "s") ?? undefined : undefined, - } satisfies StandardRun; - }) + latestRuns.map( + async (r) => + ({ + ...r, + payload: await prettyPrintPacket(r.payload, r.payloadType), + metadata: r.seedMetadata + ? await prettyPrintPacket(r.seedMetadata, r.seedMetadataType) + : undefined, + ttlSeconds: r.ttl ? parse(r.ttl, "s") ?? undefined : undefined, + } satisfies StandardRun) + ) ), latestVersions, }, @@ -263,14 +257,11 @@ export class TestTaskPresenter { runs: ( await Promise.all( latestRuns.map(async (r) => { - const number = Number(r.number); - const payload = await getScheduleTaskRunPayload(r); if (payload.success) { return { ...r, - number, payload: payload.data, ttlSeconds: r.ttl ? parse(r.ttl, "s") ?? undefined : undefined, } satisfies ScheduledRun; diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.test.tasks.$taskParam/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.test.tasks.$taskParam/route.tsx index b9ba73e7cc..5aab72187c 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.test.tasks.$taskParam/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.test.tasks.$taskParam/route.tsx @@ -425,8 +425,10 @@ function StandardTaskForm({ -
-
Run #{run.number}
+
+
+ Run {run.friendlyId.slice(-8)} +
From 59e95a9918a3ba3fb03bb1bee1465a5f960e2954 Mon Sep 17 00:00:00 2001 From: myftija Date: Mon, 7 Jul 2025 09:30:21 +0200 Subject: [PATCH 23/35] Fix duplicate queue issue --- .../route.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.test.tasks.$taskParam/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.test.tasks.$taskParam/route.tsx index 5aab72187c..babc8a15d4 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.test.tasks.$taskParam/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.test.tasks.$taskParam/route.tsx @@ -307,7 +307,9 @@ function StandardTaskForm({ paused: queue.paused, })); - return defaultQueueItem ? [defaultQueueItem, ...customQueues] : customQueues; + return defaultQueueItem && !customQueues.some((q) => q.value === defaultQueueItem.value) + ? [defaultQueueItem, ...customQueues] + : customQueues; }, [queueFetcher.data?.queues, defaultQueue]); const fetcher = useFetcher(); From 0cd9ffac401e0b2788e1dc9036194714a9549917 Mon Sep 17 00:00:00 2001 From: myftija Date: Mon, 7 Jul 2025 09:30:21 +0200 Subject: [PATCH 24/35] Extract common elements across the standard and scheduled test task forms --- .../presenters/v3/TestTaskPresenter.server.ts | 4 +- .../route.tsx | 227 +++++++++--------- 2 files changed, 121 insertions(+), 110 deletions(-) diff --git a/apps/webapp/app/presenters/v3/TestTaskPresenter.server.ts b/apps/webapp/app/presenters/v3/TestTaskPresenter.server.ts index d6c58febb6..c60986aad8 100644 --- a/apps/webapp/app/presenters/v3/TestTaskPresenter.server.ts +++ b/apps/webapp/app/presenters/v3/TestTaskPresenter.server.ts @@ -137,7 +137,7 @@ export class TestTaskPresenter { id: task.queueId, }, select: { - id: true, + friendlyId: true, name: true, type: true, paused: true, @@ -221,7 +221,7 @@ export class TestTaskPresenter { triggerSource: "STANDARD", queue: taskQueue ? { - id: taskQueue.id, + id: taskQueue.friendlyId, name: taskQueue.name.replace(/^task\//, ""), type: queueTypeFromType(taskQueue.type), paused: taskQueue.paused, diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.test.tasks.$taskParam/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.test.tasks.$taskParam/route.tsx index babc8a15d4..8a585c8a72 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.test.tasks.$taskParam/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.test.tasks.$taskParam/route.tsx @@ -170,12 +170,38 @@ export default function Page() { return
; } + const params = useParams(); + const queueFetcher = useFetcher(); + + useEffect(() => { + if (params.organizationSlug && params.projectParam && params.envParam) { + const searchParams = new URLSearchParams(); + searchParams.set("type", "custom"); + searchParams.set("per_page", "100"); + + queueFetcher.load( + `/resources/orgs/${params.organizationSlug}/projects/${params.projectParam}/env/${ + params.envParam + }/queues?${searchParams.toString()}` + ); + } + }, [params.organizationSlug, params.projectParam, params.envParam]); + + const defaultTaskQueue = result.task.queue; + const queues = useMemo(() => { + const customQueues = queueFetcher.data?.queues ?? []; + + return defaultTaskQueue && !customQueues.some((q) => q.id === defaultTaskQueue.id) + ? [defaultTaskQueue, ...customQueues] + : customQueues; + }, [queueFetcher.data?.queues, defaultTaskQueue]); + switch (result.task.triggerSource) { case "STANDARD": { return ( ); } + default: { + return result.task satisfies never; + } } } @@ -209,14 +238,14 @@ const machinePresets = [ function StandardTaskForm({ task, - defaultQueue, + queues, runs, versions, disableVersionSelection, allowArbitraryQueues, }: { task: TestTask["task"]; - defaultQueue: TestTask["queue"]; + queues: Required["queue"][]; runs: StandardRun[]; versions: string[]; disableVersionSelection: boolean; @@ -225,7 +254,6 @@ function StandardTaskForm({ const environment = useEnvironment(); const { value, replace } = useSearchParams(); const tab = value("tab"); - const params = useParams(); //form submission const lastSubmission = useActionData(); @@ -270,50 +298,14 @@ function StandardTaskForm({ ); const [tagsValue, setTagsValue] = useState(selectedCodeSample?.runTags ?? []); - const queueFetcher = useFetcher(); - - useEffect(() => { - if (params.organizationSlug && params.projectParam && params.envParam) { - const searchParams = new URLSearchParams(); - searchParams.set("type", "custom"); - searchParams.set("per_page", "100"); - - queueFetcher.load( - `/resources/orgs/${params.organizationSlug}/projects/${params.projectParam}/env/${ - params.envParam - }/queues?${searchParams.toString()}` - ); - } - }, [params.organizationSlug, params.projectParam, params.envParam]); - - const queues = useMemo(() => { - const defaultQueueItem = defaultQueue - ? { - value: defaultQueue.type === "task" ? `task/${defaultQueue.name}` : defaultQueue.name, - label: defaultQueue.name, - type: defaultQueue.type, - paused: defaultQueue.paused, - } - : undefined; - - if (!queueFetcher.data?.queues) { - return defaultQueueItem ? [defaultQueueItem] : []; - } - - const customQueues = queueFetcher.data?.queues.map((queue) => ({ - value: queue.name, - label: queue.name, - type: queue.type, - paused: queue.paused, - })); - - return defaultQueueItem && !customQueues.some((q) => q.value === defaultQueueItem.value) - ? [defaultQueueItem, ...customQueues] - : customQueues; - }, [queueFetcher.data?.queues, defaultQueue]); + const queueItems = queues.map((q) => ({ + value: q.type === "task" ? `task/${q.name}` : q.name, + label: q.name, + type: q.type, + paused: q.paused, + })); const fetcher = useFetcher(); - const [isRecentRunsPopoverOpen, setIsRecentRunsPopoverOpen] = useState(false); const [ form, { @@ -379,67 +371,21 @@ function StandardTaskForm({
- - - {runs.length === 0 ? ( - - Recent runs - - } - content="No runs yet" - /> - ) : ( - - )} - - -
-
- {runs.map((run) => ( - - ))} -
-
-
-
+ { + setPayload(run.payload); + run.seedMetadata && setMetadata(run.seedMetadata); + setSelectedCodeSampleId(run.id); + setTtlValue(run.ttlSeconds); + setConcurrencyKeyValue(run.concurrencyKey); + setMaxAttemptsValue(run.maxAttempts); + setMaxDurationValue(run.maxDurationInSeconds); + setTagsValue(run.runTags ?? []); + setQueueValue(run.queue); + setMachineValue(run.machinePreset); + }} + />
@@ -524,7 +470,7 @@ function StandardTaskForm({ placeholder="Select queue" variant="tertiary/small" dropdownIcon - items={queues} + items={queueItems} filter={{ keys: ["label"] }} value={queueValue} setValue={setQueueValue} @@ -892,3 +838,68 @@ function ScheduledTaskForm({ ); } + +function RecentRunsPopover({ + runs, + onRunSelected, +}: { + runs: T[]; + onRunSelected: (run: T) => void; +}) { + const [isRecentRunsPopoverOpen, setIsRecentRunsPopoverOpen] = useState(false); + + return ( + + + {runs.length === 0 ? ( + + Recent runs + + } + content="No runs yet" + /> + ) : ( + + )} + + +
+
+ {runs.map((run) => ( + + ))} +
+
+
+
+ ); +} From a1cebf358dfdadf7701dd26612c30e377d806a9e Mon Sep 17 00:00:00 2001 From: myftija Date: Mon, 7 Jul 2025 09:30:21 +0200 Subject: [PATCH 25/35] Apply values from recent runs to scheduled tasks too --- .../route.tsx | 104 ++++++++---------- 1 file changed, 46 insertions(+), 58 deletions(-) diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.test.tasks.$taskParam/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.test.tasks.$taskParam/route.tsx index 8a585c8a72..3f66db1e64 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.test.tasks.$taskParam/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.test.tasks.$taskParam/route.tsx @@ -2,7 +2,6 @@ import { conform, useForm } from "@conform-to/react"; import { parse } from "@conform-to/zod"; import { BeakerIcon, RectangleStackIcon } from "@heroicons/react/20/solid"; import { ClockIcon } from "@heroicons/react/24/outline"; -import { Form, useActionData, useFetcher } from "@remix-run/react"; import { type ActionFunction, type LoaderFunctionArgs, json } from "@remix-run/server-runtime"; import { useCallback, useEffect, useRef, useState, useMemo } from "react"; import { typedjson, useTypedLoaderData } from "remix-typedjson"; @@ -14,7 +13,6 @@ import { Button } from "~/components/primitives/Buttons"; import { DateField } from "~/components/primitives/DateField"; import { Fieldset } from "~/components/primitives/Fieldset"; import { FormError } from "~/components/primitives/FormError"; -import { Header2 } from "~/components/primitives/Headers"; import { Hint } from "~/components/primitives/Hint"; import { Input } from "~/components/primitives/Input"; import { InputGroup } from "~/components/primitives/InputGroup"; @@ -34,7 +32,7 @@ import { TextLink } from "~/components/primitives/TextLink"; import { TimezoneList } from "~/components/scheduled/timezones"; import { useEnvironment } from "~/hooks/useEnvironment"; import { useSearchParams } from "~/hooks/useSearchParam"; -import { useParams } from "@remix-run/react"; +import { useParams, Form, useActionData, useFetcher } from "@remix-run/react"; import { redirectBackWithErrorMessage, redirectWithErrorMessage, @@ -59,6 +57,7 @@ import { RunTagInput } from "~/components/runs/v3/RunTagInput"; import { type loader as queuesLoader } from "~/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.queues"; import { DateTime } from "~/components/primitives/DateTime"; import { TaskRunStatusCombo } from "~/components/runs/v3/TaskRunStatus"; + export const loader = async ({ request, params }: LoaderFunctionArgs) => { const userId = await requireUserId(request); const { projectParam, organizationSlug, envParam, taskParam } = v3TaskParamsSchema.parse(params); @@ -255,17 +254,11 @@ function StandardTaskForm({ const { value, replace } = useSearchParams(); const tab = value("tab"); - //form submission const lastSubmission = useActionData(); - - //recent runs - const [selectedCodeSampleId, setSelectedCodeSampleId] = useState(runs.at(0)?.id); - const selectedCodeSample = runs.find((r) => r.id === selectedCodeSampleId); - const selectedCodeSamplePayload = selectedCodeSample?.payload; - const selectedCodeSampleMetadata = selectedCodeSample?.seedMetadata; + const lastRun = runs[0]; const [defaultPayloadJson, setDefaultPayloadJson] = useState( - selectedCodeSamplePayload ?? startingJson + lastRun?.payload ?? startingJson ); const setPayload = useCallback((code: string) => { setDefaultPayloadJson(code); @@ -274,7 +267,7 @@ function StandardTaskForm({ const currentPayloadJson = useRef(defaultPayloadJson); const [defaultMetadataJson, setDefaultMetadataJson] = useState( - selectedCodeSampleMetadata ?? "{}" + lastRun?.seedMetadata ?? "{}" ); const setMetadata = useCallback((code: string) => { setDefaultMetadataJson(code); @@ -282,21 +275,19 @@ function StandardTaskForm({ const currentMetadataJson = useRef(defaultMetadataJson); - const [ttlValue, setTtlValue] = useState(selectedCodeSample?.ttlSeconds); + const [ttlValue, setTtlValue] = useState(lastRun?.ttlSeconds); const [concurrencyKeyValue, setConcurrencyKeyValue] = useState( - selectedCodeSample?.concurrencyKey - ); - const [queueValue, setQueueValue] = useState(selectedCodeSample?.queue); - const [machineValue, setMachineValue] = useState( - selectedCodeSample?.machinePreset + lastRun?.concurrencyKey ); + const [queueValue, setQueueValue] = useState(lastRun?.queue); + const [machineValue, setMachineValue] = useState(lastRun?.machinePreset); const [maxAttemptsValue, setMaxAttemptsValue] = useState( - selectedCodeSample?.maxAttempts + lastRun?.maxAttempts ); const [maxDurationValue, setMaxDurationValue] = useState( - selectedCodeSample?.maxDurationInSeconds + lastRun?.maxDurationInSeconds ); - const [tagsValue, setTagsValue] = useState(selectedCodeSample?.runTags ?? []); + const [tagsValue, setTagsValue] = useState(lastRun?.runTags ?? []); const queueItems = queues.map((q) => ({ value: q.type === "task" ? `task/${q.name}` : q.name, @@ -376,7 +367,6 @@ function StandardTaskForm({ onRunSelected={(run) => { setPayload(run.payload); run.seedMetadata && setMetadata(run.seedMetadata); - setSelectedCodeSampleId(run.id); setTtlValue(run.ttlSeconds); setConcurrencyKeyValue(run.concurrencyKey); setMaxAttemptsValue(run.maxAttempts); @@ -388,7 +378,7 @@ function StandardTaskForm({ />
- +
@@ -398,14 +388,7 @@ function StandardTaskForm({ basicSetup onChange={(v) => { currentPayloadJson.current = v; - - //deselect the example if it's been edited - if (selectedCodeSampleId) { - if (v !== selectedCodeSamplePayload) { - setDefaultPayloadJson(v); - setSelectedCodeSampleId(undefined); - } - } + setDefaultPayloadJson(v); }} height="100%" autoFocus={!tab || tab === "payload"} @@ -417,14 +400,7 @@ function StandardTaskForm({ basicSetup onChange={(v) => { currentMetadataJson.current = v; - - //deselect the example if it's been edited - if (selectedCodeSampleId) { - if (v !== selectedCodeSampleMetadata) { - setDefaultMetadataJson(v); - setSelectedCodeSampleId(undefined); - } - } + setDefaultMetadataJson(v); }} height="100%" autoFocus={tab === "metadata"} @@ -670,25 +646,19 @@ function ScheduledTaskForm({ }) { const environment = useEnvironment(); const lastSubmission = useActionData(); - const [selectedCodeSampleId, setSelectedCodeSampleId] = useState(runs.at(0)?.id); - const [timestampValue, setTimestampValue] = useState(); - const [lastTimestampValue, setLastTimestampValue] = useState(); - const [externalIdValue, setExternalIdValue] = useState(); - const [timezoneValue, setTimezoneValue] = useState("UTC"); - //set initial values - useEffect(() => { - const initialRun = runs.find((r) => r.id === selectedCodeSampleId); - if (!initialRun) { - setTimestampValue(new Date()); - return; - } + const lastRun = runs[0]; - setTimestampValue(initialRun.payload.timestamp); - setLastTimestampValue(initialRun.payload.lastTimestamp); - setExternalIdValue(initialRun.payload.externalId); - setTimezoneValue(initialRun.payload.timezone); - }, [selectedCodeSampleId]); + const [timestampValue, setTimestampValue] = useState( + lastRun.payload.timestamp ?? new Date() + ); + const [lastTimestampValue, setLastTimestampValue] = useState( + lastRun.payload.lastTimestamp + ); + const [externalIdValue, setExternalIdValue] = useState( + lastRun.payload.externalId + ); + const [timezoneValue, setTimezoneValue] = useState(lastRun.payload.timezone ?? "UTC"); const [ form, @@ -711,7 +681,7 @@ function ScheduledTaskForm({ }); return ( -
+ -
+ +
+ + Options + +
+
+ { + setTimestampValue(run.payload.timestamp); + setLastTimestampValue(run.payload.lastTimestamp); + setExternalIdValue(run.payload.externalId); + setTimezoneValue(run.payload.timezone); + }} + /> +
+
+
From f2c55c4ebe133475c48dd525a7bc8d62b836cab2 Mon Sep 17 00:00:00 2001 From: myftija Date: Mon, 7 Jul 2025 09:30:21 +0200 Subject: [PATCH 26/35] Add additional run options for scheduled tasks --- .../presenters/v3/TestTaskPresenter.server.ts | 8 + .../route.tsx | 285 ++++++++++++++++-- .../webapp/app/v3/services/testTask.server.ts | 17 +- 3 files changed, 282 insertions(+), 28 deletions(-) diff --git a/apps/webapp/app/presenters/v3/TestTaskPresenter.server.ts b/apps/webapp/app/presenters/v3/TestTaskPresenter.server.ts index c60986aad8..afffe503cf 100644 --- a/apps/webapp/app/presenters/v3/TestTaskPresenter.server.ts +++ b/apps/webapp/app/presenters/v3/TestTaskPresenter.server.ts @@ -252,6 +252,14 @@ export class TestTaskPresenter { foundTask: true, task: { triggerSource: "SCHEDULED", + queue: taskQueue + ? { + id: taskQueue.friendlyId, + name: taskQueue.name.replace(/^task\//, ""), + type: queueTypeFromType(taskQueue.type), + paused: taskQueue.paused, + } + : undefined, task: taskWithEnvironment, possibleTimezones, runs: ( diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.test.tasks.$taskParam/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.test.tasks.$taskParam/route.tsx index 3f66db1e64..1e8a4721fc 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.test.tasks.$taskParam/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.test.tasks.$taskParam/route.tsx @@ -212,9 +212,12 @@ export default function Page() { return ( ); } @@ -638,11 +641,18 @@ function ScheduledTaskForm({ task, runs, possibleTimezones, + queues, + versions, + disableVersionSelection, + allowArbitraryQueues, }: { task: TestTask["task"]; runs: ScheduledRun[]; possibleTimezones: string[]; + queues: Required["queue"][]; + versions: string[]; disableVersionSelection: boolean; + allowArbitraryQueues: boolean; }) { const environment = useEnvironment(); const lastSubmission = useActionData(); @@ -659,6 +669,26 @@ function ScheduledTaskForm({ lastRun.payload.externalId ); const [timezoneValue, setTimezoneValue] = useState(lastRun.payload.timezone ?? "UTC"); + const [ttlValue, setTtlValue] = useState(lastRun?.ttlSeconds); + const [concurrencyKeyValue, setConcurrencyKeyValue] = useState( + lastRun?.concurrencyKey + ); + const [queueValue, setQueueValue] = useState(lastRun?.queue); + const [machineValue, setMachineValue] = useState(lastRun?.machinePreset); + const [maxAttemptsValue, setMaxAttemptsValue] = useState( + lastRun?.maxAttempts + ); + const [maxDurationValue, setMaxDurationValue] = useState( + lastRun?.maxDurationInSeconds + ); + const [tagsValue, setTagsValue] = useState(lastRun?.runTags ?? []); + + const queueItems = queues.map((q) => ({ + value: q.type === "task" ? `task/${q.name}` : q.name, + label: q.name, + type: q.type, + paused: q.paused, + })); const [ form, @@ -670,6 +700,16 @@ function ScheduledTaskForm({ taskIdentifier, environmentId, timezone, + ttlSeconds, + idempotencyKey, + idempotencyKeyTTLSeconds, + queue, + concurrencyKey, + maxAttempts, + maxDurationSeconds, + tags, + version, + machine, }, ] = useForm({ id: "test-task-scheduled", @@ -711,11 +751,18 @@ function ScheduledTaskForm({ setLastTimestampValue(run.payload.lastTimestamp); setExternalIdValue(run.payload.externalId); setTimezoneValue(run.payload.timezone); + setTtlValue(run.ttlSeconds); + setConcurrencyKeyValue(run.concurrencyKey); + setMaxAttemptsValue(run.maxAttempts); + setMaxDurationValue(run.maxDurationInSeconds); + setTagsValue(run.runTags ?? []); + setQueueValue(run.queue); + setMachineValue(run.machinePreset); }} />
-
+
@@ -730,7 +777,7 @@ function ScheduledTaskForm({ onValueChange={(val) => setTimestampValue(val)} granularity="second" showNowButton - variant="medium" + variant="small" utc /> @@ -739,9 +786,7 @@ function ScheduledTaskForm({ {timestamp.error} - + This is the timestamp of the previous run. You can use this in your code to find new - data since the previous run. This can be undefined if there hasn't been a previous - run. + data since the previous run. {lastTimestamp.error} @@ -778,7 +822,7 @@ function ScheduledTaskForm({ items={possibleTimezones} filter={{ keys: [(item) => item.replace(/\//g, " ").replace(/_/g, " ")] }} dropdownIcon - variant="tertiary/medium" + variant="tertiary/small" > {(matches) => } @@ -789,39 +833,226 @@ function ScheduledTaskForm({ {timezone.error} - + setExternalIdValue(e.target.value)} + variant="small" /> Optionally, you can specify your own IDs (like a user ID) and then use it inside the - run function of your task. This allows you to have per-user CRON tasks.{" "} + run function of your task.{" "} Read the docs. {externalId.error} +
+ + + + The run will expire if it hasn't started within the TTL (time to live). + {ttlSeconds.error} + + + + {allowArbitraryQueues ? ( + setQueueValue(e.target.value)} + /> + ) : ( + + )} + {queue.error} + + + + + Tags enable you to easily filter runs. + {tags.error} + + + + + setMaxAttemptsValue(e.target.value ? parseInt(e.target.value) : undefined) + } + onKeyDown={(e) => { + // only allow entering integers > 1 + if (["-", "+", ".", "e", "E"].includes(e.key)) { + e.preventDefault(); + } + }} + onBlur={(e) => { + const value = parseInt(e.target.value); + if (value < 1 && e.target.value !== "") { + e.target.value = "1"; + } + }} + /> + {maxAttempts.error} + + + + + {maxDurationSeconds.error} + + + + + {idempotencyKey.error} + + Specify an idempotency key to ensure that a task is only triggered once with the same + key. + + + + + + By default, idempotency keys expire after 30 days. + + {idempotencyKeyTTLSeconds.error} + + + + + setConcurrencyKeyValue(e.target.value)} + /> + + Concurrency keys enable you limit concurrency by creating a separate queue for each + value of the key. + + {concurrencyKey.error} + + + + + This lets you override the machine preset specified in the task. + {machine.error} + + + + + {disableVersionSelection && ( + Only the latest version is available in the development environment. + )} + {version.error} +
-
-
- - This test will run in - - +
+
+
+ + This test will run in + + +
+
-
); diff --git a/apps/webapp/app/v3/services/testTask.server.ts b/apps/webapp/app/v3/services/testTask.server.ts index 184ef67abf..b6e6410743 100644 --- a/apps/webapp/app/v3/services/testTask.server.ts +++ b/apps/webapp/app/v3/services/testTask.server.ts @@ -50,7 +50,22 @@ export class TestTaskService extends BaseService { environment, { payload: payloadPacket.data, - options: { payloadType: payloadPacket.dataType, test: true }, + options: { + payloadType: payloadPacket.dataType, + test: true, + ttl: data.ttlSeconds, + idempotencyKey: data.idempotencyKey, + idempotencyKeyTTL: data.idempotencyKeyTTLSeconds + ? `${data.idempotencyKeyTTLSeconds}s` + : undefined, + queue: data.queue ? { name: data.queue } : undefined, + concurrencyKey: data.concurrencyKey, + maxAttempts: data.maxAttempts, + maxDuration: data.maxDurationSeconds, + tags: data.tags, + machine: data.machine, + lockToVersion: data.version === "latest" ? undefined : data.version, + }, }, { customIcon: "scheduled" } ); From a1c055b181d87be1b2ed91f52e6fe97c643a8638 Mon Sep 17 00:00:00 2001 From: myftija Date: Mon, 7 Jul 2025 09:30:21 +0200 Subject: [PATCH 27/35] Use a slightly smaller font size for run option labels --- .../app/components/primitives/Label.tsx | 2 +- .../route.tsx | 92 ++++++++++++++----- 2 files changed, 68 insertions(+), 26 deletions(-) diff --git a/apps/webapp/app/components/primitives/Label.tsx b/apps/webapp/app/components/primitives/Label.tsx index 000b407911..a9f15f68e3 100644 --- a/apps/webapp/app/components/primitives/Label.tsx +++ b/apps/webapp/app/components/primitives/Label.tsx @@ -4,7 +4,7 @@ import { InfoIconTooltip, SimpleTooltip } from "./Tooltip"; const variants = { small: { - text: "font-sans text-sm font-normal text-text-bright leading-tight flex items-center gap-1", + text: "font-sans text-[0.8125rem] font-normal text-text-bright leading-tight flex items-center gap-1", }, medium: { text: "font-sans text-sm text-text-bright leading-tight flex items-center gap-1", diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.test.tasks.$taskParam/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.test.tasks.$taskParam/route.tsx index 1e8a4721fc..152c108892 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.test.tasks.$taskParam/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.test.tasks.$taskParam/route.tsx @@ -418,12 +418,12 @@ function StandardTaskForm({
- + {delaySeconds.error} - + {ttlSeconds.error} - + {allowArbitraryQueues ? ( {queue.error} - + {tags.error} - + {maxAttempts.error} - + {maxDurationSeconds.error} - + {idempotencyKey.error} @@ -541,7 +549,7 @@ function StandardTaskForm({ - + - + {concurrencyKey.error} - +
- + {timestamp.error} - + {lastTimestamp.error} - +
- + {ttlSeconds.error} - + {allowArbitraryQueues ? ( {queue.error} - + {tags.error} - + {maxAttempts.error} - + {maxDurationSeconds.error} - + {idempotencyKey.error} @@ -968,7 +1002,9 @@ function ScheduledTaskForm({ - + By default, idempotency keys expire after 30 days. @@ -976,7 +1012,9 @@ function ScheduledTaskForm({ - + {concurrencyKey.error} - + Date: Mon, 7 Jul 2025 09:30:21 +0200 Subject: [PATCH 28/35] Disallow commas in the run tag input field --- apps/webapp/app/components/runs/v3/RunTagInput.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/apps/webapp/app/components/runs/v3/RunTagInput.tsx b/apps/webapp/app/components/runs/v3/RunTagInput.tsx index 4018813d4d..25d818f402 100644 --- a/apps/webapp/app/components/runs/v3/RunTagInput.tsx +++ b/apps/webapp/app/components/runs/v3/RunTagInput.tsx @@ -68,6 +68,8 @@ export function RunTagInput({ addTag(inputValue); } else if (e.key === "Backspace" && inputValue === "" && tags.length > 0) { removeTag(tags[tags.length - 1]); + } else if (e.key === ",") { + e.preventDefault(); } }, [inputValue, addTag, removeTag, tags] From fb7a3ee7af5247ebe01c01f00912205099f7e0bd Mon Sep 17 00:00:00 2001 From: myftija Date: Mon, 7 Jul 2025 10:21:41 +0200 Subject: [PATCH 29/35] Switch to a custom icon for recent runs button --- .../app/assets/icons/ClockRotateLeftIcon.tsx | 15 +++++++++++++++ .../route.tsx | 6 +++--- 2 files changed, 18 insertions(+), 3 deletions(-) create mode 100644 apps/webapp/app/assets/icons/ClockRotateLeftIcon.tsx diff --git a/apps/webapp/app/assets/icons/ClockRotateLeftIcon.tsx b/apps/webapp/app/assets/icons/ClockRotateLeftIcon.tsx new file mode 100644 index 0000000000..0ec8413eba --- /dev/null +++ b/apps/webapp/app/assets/icons/ClockRotateLeftIcon.tsx @@ -0,0 +1,15 @@ +export function ClockRotateLeftIcon({ className }: { className?: string }) { + return ( + + + + + + ); +} diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.test.tasks.$taskParam/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.test.tasks.$taskParam/route.tsx index 152c108892..1f8236ab9b 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.test.tasks.$taskParam/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.test.tasks.$taskParam/route.tsx @@ -1,7 +1,6 @@ import { conform, useForm } from "@conform-to/react"; import { parse } from "@conform-to/zod"; import { BeakerIcon, RectangleStackIcon } from "@heroicons/react/20/solid"; -import { ClockIcon } from "@heroicons/react/24/outline"; import { type ActionFunction, type LoaderFunctionArgs, json } from "@remix-run/server-runtime"; import { useCallback, useEffect, useRef, useState, useMemo } from "react"; import { typedjson, useTypedLoaderData } from "remix-typedjson"; @@ -57,6 +56,7 @@ import { RunTagInput } from "~/components/runs/v3/RunTagInput"; import { type loader as queuesLoader } from "~/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.queues"; import { DateTime } from "~/components/primitives/DateTime"; import { TaskRunStatusCombo } from "~/components/runs/v3/TaskRunStatus"; +import { ClockRotateLeftIcon } from "~/assets/icons/ClockRotateLeftIcon"; export const loader = async ({ request, params }: LoaderFunctionArgs) => { const userId = await requireUserId(request); @@ -1118,7 +1118,7 @@ function RecentRunsPopover({ )} From 425c614a0f014b9df39201e5f3a7be58bf7d9492 Mon Sep 17 00:00:00 2001 From: myftija Date: Mon, 7 Jul 2025 10:43:39 +0200 Subject: [PATCH 30/35] Flatten the load function test task result object --- .../presenters/v3/TestTaskPresenter.server.ts | 152 +++++++++--------- .../route.tsx | 32 ++-- 2 files changed, 95 insertions(+), 89 deletions(-) diff --git a/apps/webapp/app/presenters/v3/TestTaskPresenter.server.ts b/apps/webapp/app/presenters/v3/TestTaskPresenter.server.ts index afffe503cf..79db1b4b78 100644 --- a/apps/webapp/app/presenters/v3/TestTaskPresenter.server.ts +++ b/apps/webapp/app/presenters/v3/TestTaskPresenter.server.ts @@ -23,37 +23,32 @@ type Task = { friendlyId: string; }; -export type TestTask = +type Queue = { + id: string; + name: string; + type: "custom" | "task"; + paused: boolean; +}; + +export type TestTaskResult = | { + foundTask: true; triggerSource: "STANDARD"; - queue?: { - id: string; - name: string; - type: "custom" | "task"; - paused: boolean; - }; + queue?: Queue; task: Task; runs: StandardRun[]; latestVersions: string[]; + disableVersionSelection: boolean; + allowArbitraryQueues: boolean; } | { + foundTask: true; triggerSource: "SCHEDULED"; - queue?: { - id: string; - name: string; - type: "custom" | "task"; - paused: boolean; - }; + queue?: Queue; task: Task; possibleTimezones: string[]; runs: ScheduledRun[]; latestVersions: string[]; - }; - -export type TestTaskResult = - | { - foundTask: true; - task: TestTask; disableVersionSelection: boolean; allowArbitraryQueues: boolean; } @@ -61,6 +56,15 @@ export type TestTaskResult = foundTask: false; }; +export type StandardTaskResult = Extract< + TestTaskResult, + { foundTask: true; triggerSource: "STANDARD" } +>; +export type ScheduledTaskResult = Extract< + TestTaskResult, + { foundTask: true; triggerSource: "SCHEDULED" } +>; + type RawRun = { id: string; queue: string; @@ -217,71 +221,71 @@ export class TestTaskPresenter { case "STANDARD": return { foundTask: true, - task: { - triggerSource: "STANDARD", - queue: taskQueue - ? { - id: taskQueue.friendlyId, - name: taskQueue.name.replace(/^task\//, ""), - type: queueTypeFromType(taskQueue.type), - paused: taskQueue.paused, - } - : undefined, - task: taskWithEnvironment, - runs: await Promise.all( - latestRuns.map( - async (r) => - ({ - ...r, - payload: await prettyPrintPacket(r.payload, r.payloadType), - metadata: r.seedMetadata - ? await prettyPrintPacket(r.seedMetadata, r.seedMetadataType) - : undefined, - ttlSeconds: r.ttl ? parse(r.ttl, "s") ?? undefined : undefined, - } satisfies StandardRun) - ) - ), - latestVersions, - }, + triggerSource: "STANDARD", + queue: taskQueue + ? { + id: taskQueue.friendlyId, + name: taskQueue.name.replace(/^task\//, ""), + type: queueTypeFromType(taskQueue.type), + paused: taskQueue.paused, + } + : undefined, + task: taskWithEnvironment, + runs: await Promise.all( + latestRuns.map( + async (r) => + ({ + ...r, + payload: await prettyPrintPacket(r.payload, r.payloadType), + metadata: r.seedMetadata + ? await prettyPrintPacket(r.seedMetadata, r.seedMetadataType) + : undefined, + ttlSeconds: r.ttl ? parse(r.ttl, "s") ?? undefined : undefined, + } satisfies StandardRun) + ) + ), + latestVersions, disableVersionSelection, allowArbitraryQueues, }; - case "SCHEDULED": + case "SCHEDULED": { const possibleTimezones = getTimezones(); return { foundTask: true, - task: { - triggerSource: "SCHEDULED", - queue: taskQueue - ? { - id: taskQueue.friendlyId, - name: taskQueue.name.replace(/^task\//, ""), - type: queueTypeFromType(taskQueue.type), - paused: taskQueue.paused, - } - : undefined, - task: taskWithEnvironment, - possibleTimezones, - runs: ( - await Promise.all( - latestRuns.map(async (r) => { - const payload = await getScheduleTaskRunPayload(r); + triggerSource: "SCHEDULED", + queue: taskQueue + ? { + id: taskQueue.friendlyId, + name: taskQueue.name.replace(/^task\//, ""), + type: queueTypeFromType(taskQueue.type), + paused: taskQueue.paused, + } + : undefined, + task: taskWithEnvironment, + possibleTimezones, + runs: ( + await Promise.all( + latestRuns.map(async (r) => { + const payload = await getScheduleTaskRunPayload(r); - if (payload.success) { - return { - ...r, - payload: payload.data, - ttlSeconds: r.ttl ? parse(r.ttl, "s") ?? undefined : undefined, - } satisfies ScheduledRun; - } - }) - ) - ).filter(Boolean), - latestVersions, - }, + if (payload.success) { + return { + ...r, + payload: payload.data, + ttlSeconds: r.ttl ? parse(r.ttl, "s") ?? undefined : undefined, + } satisfies ScheduledRun; + } + }) + ) + ).filter(Boolean), + latestVersions, disableVersionSelection, allowArbitraryQueues, }; + } + default: { + return task.triggerSource satisfies never; + } } } } diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.test.tasks.$taskParam/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.test.tasks.$taskParam/route.tsx index 1f8236ab9b..99c325ce53 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.test.tasks.$taskParam/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.test.tasks.$taskParam/route.tsx @@ -42,7 +42,8 @@ import { findEnvironmentBySlug } from "~/models/runtimeEnvironment.server"; import { type ScheduledRun, type StandardRun, - type TestTask, + type StandardTaskResult, + type ScheduledTaskResult, TestTaskPresenter, } from "~/presenters/v3/TestTaskPresenter.server"; import { logger } from "~/services/logger.server"; @@ -186,7 +187,7 @@ export default function Page() { } }, [params.organizationSlug, params.projectParam, params.envParam]); - const defaultTaskQueue = result.task.queue; + const defaultTaskQueue = result.queue; const queues = useMemo(() => { const customQueues = queueFetcher.data?.queues ?? []; @@ -195,14 +196,15 @@ export default function Page() { : customQueues; }, [queueFetcher.data?.queues, defaultTaskQueue]); - switch (result.task.triggerSource) { + const { triggerSource } = result; + switch (triggerSource) { case "STANDARD": { return ( @@ -211,18 +213,18 @@ export default function Page() { case "SCHEDULED": { return ( ); } default: { - return result.task satisfies never; + return triggerSource satisfies never; } } } @@ -246,8 +248,8 @@ function StandardTaskForm({ disableVersionSelection, allowArbitraryQueues, }: { - task: TestTask["task"]; - queues: Required["queue"][]; + task: StandardTaskResult["task"]; + queues: Required["queue"][]; runs: StandardRun[]; versions: string[]; disableVersionSelection: boolean; @@ -660,10 +662,10 @@ function ScheduledTaskForm({ disableVersionSelection, allowArbitraryQueues, }: { - task: TestTask["task"]; + task: ScheduledTaskResult["task"]; runs: ScheduledRun[]; possibleTimezones: string[]; - queues: Required["queue"][]; + queues: Required["queue"][]; versions: string[]; disableVersionSelection: boolean; allowArbitraryQueues: boolean; From 101c8579678eecaffa32f01018d9858e83cd78a3 Mon Sep 17 00:00:00 2001 From: myftija Date: Mon, 7 Jul 2025 10:45:13 +0200 Subject: [PATCH 31/35] Avoid redefining machine presets, use zod schema instead --- .../route.tsx | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.test.tasks.$taskParam/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.test.tasks.$taskParam/route.tsx index 99c325ce53..7b624cbff8 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.test.tasks.$taskParam/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.test.tasks.$taskParam/route.tsx @@ -58,6 +58,7 @@ import { type loader as queuesLoader } from "~/routes/resources.orgs.$organizati import { DateTime } from "~/components/primitives/DateTime"; import { TaskRunStatusCombo } from "~/components/runs/v3/TaskRunStatus"; import { ClockRotateLeftIcon } from "~/assets/icons/ClockRotateLeftIcon"; +import { MachinePresetName } from "@trigger.dev/core/v3"; export const loader = async ({ request, params }: LoaderFunctionArgs) => { const userId = await requireUserId(request); @@ -230,15 +231,7 @@ export default function Page() { } const startingJson = "{\n\n}"; -const machinePresets = [ - "micro", - "small-1x", - "small-2x", - "medium-1x", - "medium-2x", - "large-1x", - "large-2x", -]; +const machinePresets = Object.values(MachinePresetName.enum); function StandardTaskForm({ task, From 36c89dcb173a8e896f2ef009b4cf041d22d76ada Mon Sep 17 00:00:00 2001 From: myftija Date: Tue, 8 Jul 2025 17:01:47 +0200 Subject: [PATCH 32/35] Fix ClockRotateLeftIcon jsx issues --- apps/webapp/app/assets/icons/ClockRotateLeftIcon.tsx | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/apps/webapp/app/assets/icons/ClockRotateLeftIcon.tsx b/apps/webapp/app/assets/icons/ClockRotateLeftIcon.tsx index 0ec8413eba..edef4f87b7 100644 --- a/apps/webapp/app/assets/icons/ClockRotateLeftIcon.tsx +++ b/apps/webapp/app/assets/icons/ClockRotateLeftIcon.tsx @@ -4,12 +4,12 @@ export function ClockRotateLeftIcon({ className }: { className?: string }) { - - + + ); } From cd0738fbed97242c2c4d8c51182f17e6f130bd26 Mon Sep 17 00:00:00 2001 From: myftija Date: Tue, 8 Jul 2025 17:38:33 +0200 Subject: [PATCH 33/35] Remove recent runs button tooltip as it causes nesting errors --- .../route.tsx | 27 ++++++------------- 1 file changed, 8 insertions(+), 19 deletions(-) diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.test.tasks.$taskParam/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.test.tasks.$taskParam/route.tsx index 7b624cbff8..220608d7c7 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.test.tasks.$taskParam/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.test.tasks.$taskParam/route.tsx @@ -1107,25 +1107,14 @@ function RecentRunsPopover({ return ( - {runs.length === 0 ? ( - - Recent runs - - } - content="No runs yet" - /> - ) : ( - - )} +
From 8b4712631660bee8010d1442d96f8d6dd38d0a77 Mon Sep 17 00:00:00 2001 From: myftija Date: Tue, 8 Jul 2025 18:05:35 +0200 Subject: [PATCH 34/35] Adjust the page layout to make it clear which task is currently selected --- .../route.tsx | 71 +++++++++++-------- .../route.tsx | 2 +- 2 files changed, 42 insertions(+), 31 deletions(-) diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.test.tasks.$taskParam/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.test.tasks.$taskParam/route.tsx index 220608d7c7..28a76397fb 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.test.tasks.$taskParam/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.test.tasks.$taskParam/route.tsx @@ -59,6 +59,7 @@ import { DateTime } from "~/components/primitives/DateTime"; import { TaskRunStatusCombo } from "~/components/runs/v3/TaskRunStatus"; import { ClockRotateLeftIcon } from "~/assets/icons/ClockRotateLeftIcon"; import { MachinePresetName } from "@trigger.dev/core/v3"; +import { TaskTriggerSourceIcon } from "~/components/runs/v3/TaskTriggerSource"; export const loader = async ({ request, params }: LoaderFunctionArgs) => { const userId = await requireUserId(request); @@ -337,29 +338,14 @@ function StandardTaskForm({ - -
- { - replace({ tab: "payload" }); - }} - > - Payload - - - { - replace({ tab: "metadata" }); - }} - > - Metadata - +
+
+ + + {task.taskIdentifier} +
-
+
{ @@ -375,10 +361,34 @@ function StandardTaskForm({ }} />
- +
+
+ +
+ { + replace({ tab: "payload" }); + }} + > + Payload + + + { + replace({ tab: "metadata" }); + }} + > + Metadata + +
+
- -
- - Options - +
+
+ + + {task.taskIdentifier} +
-
+
{ @@ -770,7 +781,7 @@ function ScheduledTaskForm({ }} />
- +
diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.test/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.test/route.tsx index 4d33289493..9bd9443e95 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.test/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.test/route.tsx @@ -138,7 +138,7 @@ function TaskSelector({
Date: Tue, 8 Jul 2025 18:51:25 +0200 Subject: [PATCH 35/35] Inline the tab group with the copy/clear buttons --- .../webapp/app/components/code/JSONEditor.tsx | 3 + .../route.tsx | 75 +++++++++---------- 2 files changed, 37 insertions(+), 41 deletions(-) diff --git a/apps/webapp/app/components/code/JSONEditor.tsx b/apps/webapp/app/components/code/JSONEditor.tsx index db85a9cd9f..f40e1ef413 100644 --- a/apps/webapp/app/components/code/JSONEditor.tsx +++ b/apps/webapp/app/components/code/JSONEditor.tsx @@ -21,6 +21,7 @@ export interface JSONEditorProps extends Omit { showClearButton?: boolean; linterEnabled?: boolean; allowEmpty?: boolean; + additionalActions?: React.ReactNode; } const languages = { @@ -64,6 +65,7 @@ export function JSONEditor(opts: JSONEditorProps) { showClearButton = true, linterEnabled, allowEmpty, + additionalActions, } = { ...defaultProps, ...opts, @@ -152,6 +154,7 @@ export function JSONEditor(opts: JSONEditorProps) { > {showButtons && (
+ {additionalActions && additionalActions} {showClearButton && (