diff --git a/.changeset/pre.json b/.changeset/pre.json index 2cd89f46a..6f4f6ab40 100644 --- a/.changeset/pre.json +++ b/.changeset/pre.json @@ -25,11 +25,14 @@ "green-lions-relate", "hip-cups-wave", "honest-files-decide", + "itchy-games-sort", "late-chairs-ring", "moody-squids-count", "nice-colts-boil", "polite-impalas-care", "polite-lies-fix", + "real-rats-drop", + "red-chairs-begin", "red-wasps-cover", "shiny-kiwis-beam", "smart-coins-hammer", @@ -42,6 +45,7 @@ "tricky-houses-invite", "two-tigers-dream", "weak-jobs-hide", - "wet-deers-think" + "wet-deers-think", + "wet-steaks-reflect" ] } diff --git a/.changeset/real-rats-drop.md b/.changeset/real-rats-drop.md new file mode 100644 index 000000000..953794afd --- /dev/null +++ b/.changeset/real-rats-drop.md @@ -0,0 +1,5 @@ +--- +"@trigger.dev/sdk": patch +--- + +Add onCancel lifecycle hook diff --git a/.changeset/wet-steaks-reflect.md b/.changeset/wet-steaks-reflect.md new file mode 100644 index 000000000..3a7774168 --- /dev/null +++ b/.changeset/wet-steaks-reflect.md @@ -0,0 +1,5 @@ +--- +"trigger.dev": patch +--- + +If you pass a directory when calling deploy we validate it exists and give helpful hints diff --git a/.github/workflows/publish-worker-re2.yml b/.github/workflows/publish-worker-v4.yml similarity index 98% rename from .github/workflows/publish-worker-re2.yml rename to .github/workflows/publish-worker-v4.yml index bfe429593..ee27e6f86 100644 --- a/.github/workflows/publish-worker-re2.yml +++ b/.github/workflows/publish-worker-v4.yml @@ -1,4 +1,4 @@ -name: "โš’๏ธ Publish Worker RE2" +name: "โš’๏ธ Publish Worker (v4)" on: workflow_call: diff --git a/.github/workflows/unit-tests.yml b/.github/workflows/unit-tests.yml index e638ac787..382ee5617 100644 --- a/.github/workflows/unit-tests.yml +++ b/.github/workflows/unit-tests.yml @@ -8,6 +8,32 @@ jobs: name: "๐Ÿงช Unit Tests" runs-on: ubuntu-latest steps: + - name: ๐Ÿ”ง Disable IPv6 + run: | + sudo sysctl -w net.ipv6.conf.all.disable_ipv6=1 + sudo sysctl -w net.ipv6.conf.default.disable_ipv6=1 + sudo sysctl -w net.ipv6.conf.lo.disable_ipv6=1 + + - name: ๐Ÿ”ง Configure docker address pool + run: | + CONFIG='{ + "default-address-pools" : [ + { + "base" : "172.17.0.0/12", + "size" : 20 + }, + { + "base" : "192.168.0.0/16", + "size" : 24 + } + ] + }' + mkdir -p /etc/docker + echo "$CONFIG" | sudo tee /etc/docker/daemon.json + + - name: ๐Ÿ”ง Restart docker daemon + run: sudo systemctl restart docker + - name: โฌ‡๏ธ Checkout repo uses: actions/checkout@v4 with: diff --git a/apps/webapp/app/assets/icons/TextInlineIcon.tsx b/apps/webapp/app/assets/icons/TextInlineIcon.tsx new file mode 100644 index 000000000..538d9768d --- /dev/null +++ b/apps/webapp/app/assets/icons/TextInlineIcon.tsx @@ -0,0 +1,41 @@ +export function TextInlineIcon({ className }: { className?: string }) { + return ( + + + + + + + + ); +} diff --git a/apps/webapp/app/assets/icons/TextWrapIcon.tsx b/apps/webapp/app/assets/icons/TextWrapIcon.tsx new file mode 100644 index 000000000..ac37867e8 --- /dev/null +++ b/apps/webapp/app/assets/icons/TextWrapIcon.tsx @@ -0,0 +1,34 @@ +export function TextWrapIcon({ className }: { className?: string }) { + return ( + + + + + + + ); +} diff --git a/apps/webapp/app/components/code/CodeBlock.tsx b/apps/webapp/app/components/code/CodeBlock.tsx index eb133105b..ca6200a18 100644 --- a/apps/webapp/app/components/code/CodeBlock.tsx +++ b/apps/webapp/app/components/code/CodeBlock.tsx @@ -3,11 +3,13 @@ import { Clipboard, ClipboardCheck } from "lucide-react"; import type { Language, PrismTheme } from "prism-react-renderer"; import { Highlight, Prism } from "prism-react-renderer"; import { forwardRef, ReactNode, useCallback, useEffect, useState } from "react"; +import { TextWrapIcon } from "~/assets/icons/TextWrapIcon"; import { cn } from "~/utils/cn"; import { Button } from "../primitives/Buttons"; import { Dialog, DialogContent, DialogHeader, DialogTitle } from "../primitives/Dialog"; import { Paragraph } from "../primitives/Paragraph"; import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "../primitives/Tooltip"; +import { TextInlineIcon } from "~/assets/icons/TextInlineIcon"; //This is a fork of https://github.com/mantinedev/mantine/blob/master/src/mantine-prism/src/Prism/Prism.tsx //it didn't support highlighting lines by dimming the rest of the code, or animations on the highlighting @@ -31,6 +33,9 @@ type CodeBlockProps = { /** Show copy to clipboard button */ showCopyButton?: boolean; + /** Show text wrapping button */ + showTextWrapping?: boolean; + /** Display line numbers */ showLineNumbers?: boolean; @@ -183,6 +188,7 @@ export const CodeBlock = forwardRef( ( { showCopyButton = true, + showTextWrapping = false, showLineNumbers = true, showOpenInModal = true, highlightedRanges, @@ -202,6 +208,7 @@ export const CodeBlock = forwardRef( const [copied, setCopied] = useState(false); const [modalCopied, setModalCopied] = useState(false); const [isModalOpen, setIsModalOpen] = useState(false); + const [isWrapped, setIsWrapped] = useState(false); const onCopied = useCallback( (event: React.MouseEvent) => { @@ -263,6 +270,25 @@ export const CodeBlock = forwardRef( showChrome ? "right-1.5 top-1.5" : "top-2.5" )} > + {showTextWrapping && ( + + + setIsWrapped(!isWrapped)} + className="transition-colors focus-custom hover:cursor-pointer hover:text-text-bright" + > + {isWrapped ? ( + + ) : ( + + )} + + + {isWrapped ? "Unwrap" : "Wrap"} + + + + )} {showCopyButton && ( @@ -311,16 +337,27 @@ export const CodeBlock = forwardRef( maxLineWidth={maxLineWidth} className="px-2 py-3" preClassName="text-xs" + isWrapped={isWrapped} /> ) : (
-
+              
                 {code}
               
@@ -355,6 +392,7 @@ export const CodeBlock = forwardRef( maxLineWidth={maxLineWidth} className="min-h-full" preClassName="text-sm" + isWrapped={isWrapped} /> ) : (
{ - // This ensures the language definitions are loaded Promise.all([ //@ts-ignore import("prismjs/components/prism-json"), @@ -434,16 +473,23 @@ function HighlightCode({ ]).then(() => setIsLoaded(true)); }, []); + const containerClasses = cn( + "px-3 py-3 scrollbar-thin scrollbar-track-transparent scrollbar-thumb-charcoal-600", + !isWrapped && "overflow-x-auto", + isWrapped && "overflow-y-auto", + className + ); + + const preClasses = cn( + "relative mr-2 font-mono leading-relaxed", + preClassName, + isWrapped && "[&_span]:whitespace-pre-wrap [&_span]:break-words" + ); + if (!isLoaded) { return ( -
-
{code}
+
+
{code}
); } @@ -457,22 +503,8 @@ function HighlightCode({ getLineProps, getTokenProps, }) => ( -
-
+        
+
             {tokens
               .map((line, index) => {
                 if (index === tokens.length - 1 && line.length === 1 && line[0].content === "\n") {
@@ -495,7 +527,8 @@ function HighlightCode({
                     {...lineProps}
                     className={cn(
                       "flex w-full justify-start transition-opacity duration-500",
-                      lineProps.className
+                      lineProps.className,
+                      isWrapped && "flex-wrap"
                     )}
                     style={{
                       opacity: shouldDim ? dimAmount : undefined,
@@ -504,9 +537,10 @@ function HighlightCode({
                   >
                     {showLineNumbers && (
                       
= compactThreshold; + return ( -
-
-
{title}
+
+
+ {title} {accessory &&
{accessory}
}
{loading ? ( ) : v !== undefined ? ( -
- {animate ? : v} +
+ {shouldCompact ? ( + : formatNumberCompact(v)} + content={formatNumber(v)} + /> + ) : animate ? ( + + ) : ( + formatNumber(v) + )} {suffix &&
{suffix}
}
) : ( diff --git a/apps/webapp/app/components/primitives/AnimatedNumber.tsx b/apps/webapp/app/components/primitives/AnimatedNumber.tsx index a58567050..f2f309a52 100644 --- a/apps/webapp/app/components/primitives/AnimatedNumber.tsx +++ b/apps/webapp/app/components/primitives/AnimatedNumber.tsx @@ -1,4 +1,4 @@ -import { motion, useSpring, useTransform, useMotionValue, animate } from "framer-motion"; +import { animate, motion, useMotionValue, useTransform } from "framer-motion"; import { useEffect } from "react"; export function AnimatedNumber({ value }: { value: number }) { diff --git a/apps/webapp/app/components/primitives/DateTime.tsx b/apps/webapp/app/components/primitives/DateTime.tsx index 11a7ab990..7d75bc735 100644 --- a/apps/webapp/app/components/primitives/DateTime.tsx +++ b/apps/webapp/app/components/primitives/DateTime.tsx @@ -98,7 +98,45 @@ export function formatDateTime( } export function formatDateTimeISO(date: Date, timeZone: string): string { - return new Date(date.toLocaleString("en-US", { timeZone })).toISOString(); + // Special handling for UTC + if (timeZone === "UTC") { + return date.toISOString(); + } + + // Get the date parts in the target timezone + const dateFormatter = new Intl.DateTimeFormat("en-US", { + timeZone, + year: "numeric", + month: "2-digit", + day: "2-digit", + hour: "2-digit", + minute: "2-digit", + second: "2-digit", + hour12: false, + }); + + // Get the timezone offset for this specific date + const timeZoneFormatter = new Intl.DateTimeFormat("en-US", { + timeZone, + timeZoneName: "longOffset", + }); + + const dateParts = Object.fromEntries( + dateFormatter.formatToParts(date).map(({ type, value }) => [type, value]) + ); + + const timeZoneParts = timeZoneFormatter.formatToParts(date); + const offset = + timeZoneParts.find((part) => part.type === "timeZoneName")?.value.replace("GMT", "") || + "+00:00"; + + // Format: YYYY-MM-DDThh:mm:ss.sssยฑhh:mm + return ( + `${dateParts.year}-${dateParts.month}-${dateParts.day}T` + + `${dateParts.hour}:${dateParts.minute}:${dateParts.second}.${String( + date.getMilliseconds() + ).padStart(3, "0")}${offset}` + ); } // New component that only shows date when it changes diff --git a/apps/webapp/app/components/primitives/LoadingBarDivider.tsx b/apps/webapp/app/components/primitives/LoadingBarDivider.tsx index f227f9e73..d35067ea6 100644 --- a/apps/webapp/app/components/primitives/LoadingBarDivider.tsx +++ b/apps/webapp/app/components/primitives/LoadingBarDivider.tsx @@ -7,7 +7,7 @@ type LoadingBarDividerProps = { export function LoadingBarDivider({ isLoading }: LoadingBarDividerProps) { return ( -
+
); @@ -21,11 +21,9 @@ export function AnimationDivider({ isLoading }: LoadingBarDividerProps) { if (isPresent) { const enterAnimation = async () => { await animate( - [ - [scope.current, { width: "30%", left: "35%" }, { duration: 1, ease: "easeIn" }], - [scope.current, { width: 0, left: "100%" }, { duration: 1, ease: "easeOut" }], - ], - { repeat: Infinity, repeatType: "reverse" } + scope.current, + { left: ["-100%", "100%"], width: "100%" }, + { duration: 2, ease: "easeOut", repeat: Infinity } ); }; enterAnimation(); diff --git a/apps/webapp/app/components/runs/v3/PacketDisplay.tsx b/apps/webapp/app/components/runs/v3/PacketDisplay.tsx index 430402c89..4da733f2c 100644 --- a/apps/webapp/app/components/runs/v3/PacketDisplay.tsx +++ b/apps/webapp/app/components/runs/v3/PacketDisplay.tsx @@ -44,6 +44,7 @@ export function PacketDisplay({ code={data} maxLines={20} showLineNumbers={false} + showTextWrapping /> ); } diff --git a/apps/webapp/app/components/runs/v3/RunIcon.tsx b/apps/webapp/app/components/runs/v3/RunIcon.tsx index c03b32731..8a1924b3e 100644 --- a/apps/webapp/app/components/runs/v3/RunIcon.tsx +++ b/apps/webapp/app/components/runs/v3/RunIcon.tsx @@ -97,6 +97,7 @@ export function RunIcon({ name, className, spanName }: TaskIconProps) { case "task-hook-onResume": case "task-hook-onComplete": case "task-hook-cleanup": + case "task-hook-onCancel": return ; case "task-hook-onFailure": case "task-hook-catchError": diff --git a/apps/webapp/app/components/runs/v3/TaskTriggerSource.tsx b/apps/webapp/app/components/runs/v3/TaskTriggerSource.tsx index 23948508b..0dfc5f10e 100644 --- a/apps/webapp/app/components/runs/v3/TaskTriggerSource.tsx +++ b/apps/webapp/app/components/runs/v3/TaskTriggerSource.tsx @@ -12,10 +12,12 @@ export function TaskTriggerSourceIcon({ }) { switch (source) { case "STANDARD": { - return ; + return ; } case "SCHEDULED": { - return ; + return ( + + ); } } } diff --git a/apps/webapp/app/env.server.ts b/apps/webapp/app/env.server.ts index f0931683b..589c9a0a3 100644 --- a/apps/webapp/app/env.server.ts +++ b/apps/webapp/app/env.server.ts @@ -460,6 +460,7 @@ const EnvironmentSchema = z.object({ RUN_ENGINE_REUSE_SNAPSHOT_COUNT: z.coerce.number().int().default(0), RUN_ENGINE_MAXIMUM_ENV_COUNT: z.coerce.number().int().optional(), RUN_ENGINE_WORKER_SHUTDOWN_TIMEOUT_MS: z.coerce.number().int().default(60_000), + RUN_ENGINE_RETRY_WARM_START_THRESHOLD_MS: z.coerce.number().int().default(30_000), RUN_ENGINE_WORKER_REDIS_HOST: z .string() @@ -717,7 +718,7 @@ const EnvironmentSchema = z.object({ SLACK_BOT_TOKEN: z.string().optional(), SLACK_SIGNUP_REASON_CHANNEL_ID: z.string().optional(), - + // kapa.ai KAPA_AI_WEBSITE_ID: z.string().optional(), }); diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.queues/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.queues/route.tsx index 00ec1336a..24446a7ee 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.queues/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.queues/route.tsx @@ -69,6 +69,7 @@ import { docsPath, EnvironmentParamSchema, v3BillingPath } from "~/utils/pathBui import { PauseEnvironmentService } from "~/v3/services/pauseEnvironment.server"; import { PauseQueueService } from "~/v3/services/pauseQueue.server"; import { useCurrentPlan } from "../_app.orgs.$organizationSlug/route"; +import { Header3 } from "~/components/primitives/Headers"; import { Input } from "~/components/primitives/Input"; import { useThrottle } from "~/hooks/useThrottle"; @@ -257,13 +258,30 @@ export default function Page() { suffix={env.paused && environment.queued > 0 ? "paused" : undefined} animate accessory={} - valueClassName={env.paused ? "text-amber-500" : undefined} + valueClassName={env.paused ? "text-warning" : undefined} + compactThreshold={1000000} + /> + - Name Queued - Running + Running/limit + +
+ Environment + + This queue is limited by your environment's concurrency limit of{" "} + {environment.concurrencyLimit}. + +
+
+ User + + This queue is limited by a concurrency limit set in your code. + +
+
+ } + > + Limited by + Release on waitpoint - Concurrency limit Pause/resume @@ -342,80 +389,103 @@ export default function Page() { {queues.length > 0 ? ( - queues.map((queue) => ( - - - - {queue.type === "task" ? ( - - } - content={`This queue was automatically created from your "${queue.name}" task`} - /> - ) : ( - - } - content={`This is a custom queue you added in your code.`} - /> + queues.map((queue) => { + const limit = queue.concurrencyLimit ?? environment.concurrencyLimit; + const isAtLimit = queue.running === limit; + return ( + + + + {queue.type === "task" ? ( + + } + content={`This queue was automatically created from your "${queue.name}" task`} + /> + ) : ( + + } + content={`This is a custom queue you added in your code.`} + /> + )} + + {queue.name} + + {queue.paused ? ( + + Paused + + ) : null} + {isAtLimit ? ( + + At concurrency limit + + ) : null} + + + + {queue.queued} + + - {queue.name} + > + {queue.running}/ + + {limit} - {queue.paused ? ( - - Paused - - ) : null} - - - - {queue.queued} - - - {queue.running} - - - {queue.releaseConcurrencyOnWaitpoint ? "Yes" : "No"} - - - {queue.concurrencyLimit ?? ( - - Max ({environment.concurrencyLimit}) - - )} - - } - hiddenButtons={!queue.paused && } - /> - - )) + + + {queue.concurrencyLimit ? "User" : "Environment"} + + + {queue.releaseConcurrencyOnWaitpoint ? "Yes" : "No"} + + + } + hiddenButtons={ + !queue.paused && + } + /> + + ); + }) ) : ( @@ -503,7 +573,7 @@ function EnvironmentPauseResumeButton({ type="button" variant="secondary/small" LeadingIcon={env.paused ? PlayIcon : PauseIcon} - leadingIconClassName={env.paused ? "text-success" : "text-amber-500"} + leadingIconClassName={env.paused ? "text-success" : "text-warning"} > {env.paused ? "Resume..." : "Pause environment..."} @@ -512,8 +582,8 @@ function EnvironmentPauseResumeButton({ {env.paused - ? `Resume processing runs in ${environmentFullTitle(env)}.` - : `Pause processing runs in ${environmentFullTitle(env)}.`} + ? `Resume processing runs in ${environmentFullTitle(env)}` + : `Pause processing runs in ${environmentFullTitle(env)}`} @@ -540,7 +610,9 @@ function EnvironmentPauseResumeButton({ type="submit" disabled={isLoading} variant={env.paused ? "primary/medium" : "danger/medium"} - LeadingIcon={isLoading ? : env.paused ? PlayIcon : PauseIcon} + LeadingIcon={ + isLoading ? : env.paused ? PlayIcon : PauseIcon + } shortcut={{ modifiers: ["mod"], key: "enter" }} > {env.paused ? "Resume environment" : "Pause environment"} @@ -582,7 +654,7 @@ function QueuePauseResumeButton({ type="button" variant="tertiary/small" LeadingIcon={queue.paused ? PlayIcon : PauseIcon} - leadingIconClassName={queue.paused ? "text-success" : "text-amber-500"} + leadingIconClassName={queue.paused ? "text-success" : "text-warning"} > {queue.paused ? "Resume..." : "Pause..."} @@ -703,7 +775,7 @@ export function QueueFilters() { const search = searchParams.get("query") ?? ""; return ( -
+
Max duration - {run.maxDurationInSeconds ? `${run.maxDurationInSeconds}s` : "โ€“"} + {run.maxDurationInSeconds + ? `${run.maxDurationInSeconds}s (${formatDurationMilliseconds( + run.maxDurationInSeconds * 1000, + { style: "short" } + )})` + : "โ€“"} diff --git a/apps/webapp/app/routes/resources.orgs.$organizationSlug.select-plan.tsx b/apps/webapp/app/routes/resources.orgs.$organizationSlug.select-plan.tsx index 1ac03e1ac..1d96293d0 100644 --- a/apps/webapp/app/routes/resources.orgs.$organizationSlug.select-plan.tsx +++ b/apps/webapp/app/routes/resources.orgs.$organizationSlug.select-plan.tsx @@ -678,7 +678,7 @@ export function TierPro({