From 044592b9f09433e11d8d86daaa8f381bd31ac380 Mon Sep 17 00:00:00 2001 From: James Ritchie Date: Thu, 2 Apr 2026 14:26:00 +0100 Subject: [PATCH 001/243] Adds animated panel to a storybook --- .../app/components/primitives/Resizable.tsx | 10 +- .../routes/storybook.animated-panel/route.tsx | 175 ++++++++++++++++++ apps/webapp/app/routes/storybook/route.tsx | 4 + 3 files changed, 187 insertions(+), 2 deletions(-) create mode 100644 apps/webapp/app/routes/storybook.animated-panel/route.tsx diff --git a/apps/webapp/app/components/primitives/Resizable.tsx b/apps/webapp/app/components/primitives/Resizable.tsx index ba6ed35490e..8c14b30aa7e 100644 --- a/apps/webapp/app/components/primitives/Resizable.tsx +++ b/apps/webapp/app/components/primitives/Resizable.tsx @@ -1,6 +1,6 @@ "use client"; -import React from "react"; +import React, { useRef } from "react"; import { PanelGroup, Panel, PanelResizer } from "react-window-splitter"; import { cn } from "~/utils/cn"; @@ -69,6 +69,12 @@ const ResizableHandle = ({ ); -export { ResizableHandle, ResizablePanel, ResizablePanelGroup }; +function useFrozenValue(value: T | null | undefined): T | null | undefined { + const ref = useRef(value); + if (value != null) ref.current = value; + return ref.current; +} + +export { ResizableHandle, ResizablePanel, ResizablePanelGroup, useFrozenValue }; export type ResizableSnapshot = React.ComponentProps["snapshot"]; diff --git a/apps/webapp/app/routes/storybook.animated-panel/route.tsx b/apps/webapp/app/routes/storybook.animated-panel/route.tsx new file mode 100644 index 00000000000..b8ea9112652 --- /dev/null +++ b/apps/webapp/app/routes/storybook.animated-panel/route.tsx @@ -0,0 +1,175 @@ +import { useState } from "react"; +import { ExitIcon } from "~/assets/icons/ExitIcon"; +import { Button } from "~/components/primitives/Buttons"; +import { Header2 } from "~/components/primitives/Headers"; +import { Paragraph } from "~/components/primitives/Paragraph"; +import * as Property from "~/components/primitives/PropertyTable"; +import { + ResizableHandle, + ResizablePanel, + ResizablePanelGroup, + useFrozenValue, +} from "~/components/primitives/Resizable"; +import { + Table, + TableBody, + TableCell, + TableHeader, + TableHeaderCell, + TableRow, +} from "~/components/primitives/Table"; +import { cn } from "~/utils/cn"; + +type DemoItem = { + id: string; + name: string; + status: "completed" | "running" | "failed" | "queued"; + duration: string; + task: string; +}; + +const demoItems: DemoItem[] = [ + { id: "run_a1b2c3d4", name: "Process invoices", status: "completed", duration: "2.3s", task: "invoice/process" }, + { id: "run_e5f6g7h8", name: "Send welcome email", status: "running", duration: "0.8s", task: "email/welcome" }, + { id: "run_i9j0k1l2", name: "Generate report", status: "failed", duration: "12.1s", task: "report/generate" }, + { id: "run_m3n4o5p6", name: "Sync inventory", status: "completed", duration: "5.7s", task: "inventory/sync" }, + { id: "run_q7r8s9t0", name: "Resize images", status: "queued", duration: "—", task: "image/resize" }, + { id: "run_u1v2w3x4", name: "Update search index", status: "completed", duration: "1.1s", task: "search/index" }, + { id: "run_y5z6a7b8", name: "Calculate analytics", status: "running", duration: "8.4s", task: "analytics/calc" }, + { id: "run_c9d0e1f2", name: "Deploy preview", status: "completed", duration: "34.2s", task: "deploy/preview" }, + { id: "run_g3h4i5j6", name: "Run migrations", status: "failed", duration: "0.3s", task: "db/migrate" }, + { id: "run_k7l8m9n0", name: "Notify Slack", status: "completed", duration: "0.5s", task: "notify/slack" }, +]; + +const statusColors: Record = { + completed: "text-success", + running: "text-blue-500", + failed: "text-error", + queued: "text-text-dimmed", +}; + +function DetailPanel({ item, onClose }: { item: DemoItem; onClose: () => void }) { + return ( +
+
+ {item.name} +
+
+ + + Run ID + {item.id} + + + Task + {item.task} + + + Status + + + {item.status.charAt(0).toUpperCase() + item.status.slice(1)} + + + + + Duration + {item.duration} + + +
+ + This is a demo detail panel showing the animated slide-in/out behavior using + react-window-splitter's collapseAnimation. Click a different row to change the + detail, or press Esc / click the close button to dismiss. + +
+
+
+ ); +} + +export default function Story() { + const [selectedItem, setSelectedItem] = useState(null); + const show = !!selectedItem; + const frozenItem = useFrozenValue(selectedItem); + const displayItem = selectedItem ?? frozenItem; + + return ( +
+ + +
+
+ Runs +
+ + + + Run ID + Name + Task + Status + Duration + + + + {demoItems.map((item) => ( + + setSelectedItem(item)} isTabbableCell> + {item.id} + + setSelectedItem(item)}>{item.name} + setSelectedItem(item)}>{item.task} + setSelectedItem(item)}> + + {item.status.charAt(0).toUpperCase() + item.status.slice(1)} + + + setSelectedItem(item)} alignment="right"> + {item.duration} + + + ))} + +
+
+
+ + {}} + collapsedSize="0px" + collapseAnimation={{ easing: "ease-in-out", duration: 200 }} + > +
+ {displayItem && ( + setSelectedItem(null)} + /> + )} +
+
+
+
+ ); +} diff --git a/apps/webapp/app/routes/storybook/route.tsx b/apps/webapp/app/routes/storybook/route.tsx index bcaee62d6b0..3efa990548c 100644 --- a/apps/webapp/app/routes/storybook/route.tsx +++ b/apps/webapp/app/routes/storybook/route.tsx @@ -8,6 +8,10 @@ import { requireUser } from "~/services/session.server"; import { cn } from "~/utils/cn"; const stories: Story[] = [ + { + name: "Animated panel", + slug: "animated-panel", + }, { name: "Avatar", slug: "avatar", From e7f974e37c71c3227e78089061dc7e1a8fc32248 Mon Sep 17 00:00:00 2001 From: James Ritchie Date: Thu, 2 Apr 2026 14:58:42 +0100 Subject: [PATCH 002/243] Adds animated resizable to run page --- .../app/components/primitives/Resizable.tsx | 20 +++++++- .../route.tsx | 50 ++++++++++++------- .../routes/storybook.animated-panel/route.tsx | 7 +-- 3 files changed, 55 insertions(+), 22 deletions(-) diff --git a/apps/webapp/app/components/primitives/Resizable.tsx b/apps/webapp/app/components/primitives/Resizable.tsx index 8c14b30aa7e..2efaae4258e 100644 --- a/apps/webapp/app/components/primitives/Resizable.tsx +++ b/apps/webapp/app/components/primitives/Resizable.tsx @@ -69,12 +69,30 @@ const ResizableHandle = ({ ); +const RESIZABLE_PANEL_ANIMATION = { + easing: "ease-in-out" as const, + duration: 200, +}; + +const COLLAPSIBLE_HANDLE_CLASSNAME = "transition-opacity duration-200"; + +function collapsibleHandleClassName(show: boolean) { + return cn(COLLAPSIBLE_HANDLE_CLASSNAME, !show && "pointer-events-none opacity-0"); +} + function useFrozenValue(value: T | null | undefined): T | null | undefined { const ref = useRef(value); if (value != null) ref.current = value; return ref.current; } -export { ResizableHandle, ResizablePanel, ResizablePanelGroup, useFrozenValue }; +export { + RESIZABLE_PANEL_ANIMATION, + ResizableHandle, + ResizablePanel, + ResizablePanelGroup, + collapsibleHandleClassName, + useFrozenValue, +}; export type ResizableSnapshot = React.ComponentProps["snapshot"]; diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs.$runParam/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs.$runParam/route.tsx index dc1f3fa2703..ed1d2f01055 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs.$runParam/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs.$runParam/route.tsx @@ -48,10 +48,12 @@ import { Paragraph } from "~/components/primitives/Paragraph"; import { Popover, PopoverArrowTrigger, PopoverContent } from "~/components/primitives/Popover"; import * as Property from "~/components/primitives/PropertyTable"; import { + RESIZABLE_PANEL_ANIMATION, ResizableHandle, ResizablePanel, ResizablePanelGroup, type ResizableSnapshot, + collapsibleHandleClassName, } from "~/components/primitives/Resizable"; import { ShortcutKey, variants } from "~/components/primitives/ShortcutKey"; import { Slider } from "~/components/primitives/Slider"; @@ -112,7 +114,7 @@ import { SpanView } from "../resources.orgs.$organizationSlug.projects.$projectP const resizableSettings = { parent: { - autosaveId: "panel-run-parent", + autosaveId: "panel-run-parent-v2", handleId: "parent-handle", main: { id: "run", @@ -542,24 +544,36 @@ function TraceView({ treeSnapshot={resizable.tree as ResizableSnapshot} /> - - {selectedSpanId && ( - + {}} + collapsedSize="0px" + collapseAnimation={RESIZABLE_PANEL_ANIMATION} + > +
- {" "} - replaceSearchParam("span")} - linkedRunId={selectedSpanLinkedRunId} - /> - - )} + {selectedSpanId && ( + replaceSearchParam("span")} + linkedRunId={selectedSpanLinkedRunId} + /> + )} +
+
); diff --git a/apps/webapp/app/routes/storybook.animated-panel/route.tsx b/apps/webapp/app/routes/storybook.animated-panel/route.tsx index b8ea9112652..4c037a95783 100644 --- a/apps/webapp/app/routes/storybook.animated-panel/route.tsx +++ b/apps/webapp/app/routes/storybook.animated-panel/route.tsx @@ -5,9 +5,11 @@ import { Header2 } from "~/components/primitives/Headers"; import { Paragraph } from "~/components/primitives/Paragraph"; import * as Property from "~/components/primitives/PropertyTable"; import { + RESIZABLE_PANEL_ANIMATION, ResizableHandle, ResizablePanel, ResizablePanelGroup, + collapsibleHandleClassName, useFrozenValue, } from "~/components/primitives/Resizable"; import { @@ -18,7 +20,6 @@ import { TableHeaderCell, TableRow, } from "~/components/primitives/Table"; -import { cn } from "~/utils/cn"; type DemoItem = { id: string; @@ -145,7 +146,7 @@ export default function Story() {
{}} collapsedSize="0px" - collapseAnimation={{ easing: "ease-in-out", duration: 200 }} + collapseAnimation={RESIZABLE_PANEL_ANIMATION} >
{displayItem && ( From 1bbfcf597ce5d0edfa323df50a4c5a785ba12587 Mon Sep 17 00:00:00 2001 From: James Ritchie Date: Thu, 2 Apr 2026 15:05:51 +0100 Subject: [PATCH 003/243] Animated resizable for Schedules page --- .../route.tsx | 29 ++++++++++++++----- 1 file changed, 21 insertions(+), 8 deletions(-) diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.schedules/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.schedules/route.tsx index d32f9ecb5b4..24608422e1c 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.schedules/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.schedules/route.tsx @@ -25,9 +25,11 @@ import { PaginationControls } from "~/components/primitives/Pagination"; import { Paragraph } from "~/components/primitives/Paragraph"; import * as Property from "~/components/primitives/PropertyTable"; import { + RESIZABLE_PANEL_ANIMATION, ResizableHandle, ResizablePanel, ResizablePanelGroup, + collapsibleHandleClassName, } from "~/components/primitives/Resizable"; import { Table, @@ -304,14 +306,25 @@ export default function Page() { )}
- {(isShowingNewPane || isShowingSchedule) && ( - <> - - - - - - )} + + {}} + collapsedSize="0px" + collapseAnimation={RESIZABLE_PANEL_ANIMATION} + > +
+ +
+
From aa50ab74d80002b5992b3e5b48f732c58250574a Mon Sep 17 00:00:00 2001 From: James Ritchie Date: Thu, 2 Apr 2026 15:09:19 +0100 Subject: [PATCH 004/243] Adds animated panel to Batches page --- .../route.tsx | 29 ++++++++++++++----- 1 file changed, 21 insertions(+), 8 deletions(-) diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.batches/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.batches/route.tsx index a66e85c0f86..1e9bf46ad85 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.batches/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.batches/route.tsx @@ -13,9 +13,11 @@ import { DateTime } from "~/components/primitives/DateTime"; import { NavBar, PageAccessories, PageTitle } from "~/components/primitives/PageHeader"; import { Paragraph } from "~/components/primitives/Paragraph"; import { + RESIZABLE_PANEL_ANIMATION, ResizableHandle, ResizablePanel, ResizablePanelGroup, + collapsibleHandleClassName, } from "~/components/primitives/Resizable"; import { Spinner } from "~/components/primitives/Spinner"; import { @@ -143,14 +145,25 @@ export default function Page() { /> - {isShowingInspector && ( - <> - - - - - - )} + + {}} + collapsedSize="0px" + collapseAnimation={RESIZABLE_PANEL_ANIMATION} + > +
+ +
+
)} From c59e478962dabe777e470fe9fcaba71e4841219b Mon Sep 17 00:00:00 2001 From: James Ritchie Date: Thu, 2 Apr 2026 15:11:53 +0100 Subject: [PATCH 005/243] Animated resizable panel for waitpoints page --- .../route.tsx | 29 ++++++++++++++----- 1 file changed, 21 insertions(+), 8 deletions(-) diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.waitpoints.tokens/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.waitpoints.tokens/route.tsx index b3233abb858..7481ad892e1 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.waitpoints.tokens/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.waitpoints.tokens/route.tsx @@ -13,9 +13,11 @@ import { DateTime } from "~/components/primitives/DateTime"; import { NavBar, PageAccessories, PageTitle } from "~/components/primitives/PageHeader"; import { Paragraph } from "~/components/primitives/Paragraph"; import { + RESIZABLE_PANEL_ANIMATION, ResizableHandle, ResizablePanel, ResizablePanelGroup, + collapsibleHandleClassName, } from "~/components/primitives/Resizable"; import { Table, @@ -240,14 +242,25 @@ export default function Page() { - {isShowingWaitpoint && ( - <> - - - - - - )} + + {}} + collapsedSize="0px" + collapseAnimation={RESIZABLE_PANEL_ANIMATION} + > +
+ +
+
)} From 037352568a6b6fd4af90a94248eea452879ddf00 Mon Sep 17 00:00:00 2001 From: James Ritchie Date: Thu, 2 Apr 2026 15:22:47 +0100 Subject: [PATCH 006/243] Adds animated panel to deployments page --- .../route.tsx | 30 ++++++++++++++----- 1 file changed, 22 insertions(+), 8 deletions(-) diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.deployments/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.deployments/route.tsx index a42b39c4573..b17773b88a2 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.deployments/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.deployments/route.tsx @@ -42,9 +42,11 @@ import { NavBar, PageAccessories, PageTitle } from "~/components/primitives/Page import { PaginationControls } from "~/components/primitives/Pagination"; import { Paragraph } from "~/components/primitives/Paragraph"; import { + RESIZABLE_PANEL_ANIMATION, ResizableHandle, ResizablePanel, ResizablePanelGroup, + collapsibleHandleClassName, } from "~/components/primitives/Resizable"; import { Table, @@ -388,14 +390,26 @@ export default function Page() { )} - {deploymentParam && ( - <> - - - - - - )} + + {}} + collapsedSize="0px" + collapseAnimation={RESIZABLE_PANEL_ANIMATION} + > +
+ +
+
From 02f11985ca85e4f8524549e1e3010fc716581239 Mon Sep 17 00:00:00 2001 From: James Ritchie Date: Thu, 2 Apr 2026 15:44:21 +0100 Subject: [PATCH 007/243] Modal animated panel --- .../route.tsx | 32 ++++++++++++++----- 1 file changed, 24 insertions(+), 8 deletions(-) diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.models._index/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.models._index/route.tsx index 394530b6335..d5769c339c5 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.models._index/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.models._index/route.tsx @@ -36,9 +36,11 @@ import { Header2 } from "~/components/primitives/Headers"; import { NavBar, PageTitle } from "~/components/primitives/PageHeader"; import * as Property from "~/components/primitives/PropertyTable"; import { + RESIZABLE_PANEL_ANIMATION, ResizableHandle, ResizablePanel, ResizablePanelGroup, + collapsibleHandleClassName, } from "~/components/primitives/Resizable"; import { SearchInput } from "~/components/primitives/SearchInput"; import { Switch } from "~/components/primitives/Switch"; @@ -707,7 +709,7 @@ function ModelDetailPanel({ className="pl-1" /> -
+
- {selectedModel && ( - <> - - + + {}} + collapsedSize="0px" + collapseAnimation={RESIZABLE_PANEL_ANIMATION} + > +
+ {selectedModel && ( setSelectedModel(null)} /> - - - )} + )} +
+
Date: Thu, 2 Apr 2026 15:52:10 +0100 Subject: [PATCH 008/243] animated panel to the Logs page --- apps/webapp/app/components/logs/LogsTable.tsx | 4 +- .../route.tsx | 43 +++++++++++-------- 2 files changed, 26 insertions(+), 21 deletions(-) diff --git a/apps/webapp/app/components/logs/LogsTable.tsx b/apps/webapp/app/components/logs/LogsTable.tsx index dcbd2d6868f..ed8e6793e5f 100644 --- a/apps/webapp/app/components/logs/LogsTable.tsx +++ b/apps/webapp/app/components/logs/LogsTable.tsx @@ -167,8 +167,8 @@ export function LogsTable({ > - - + + {log.runId} {log.taskIdentifier} diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.logs/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.logs/route.tsx index 80a5c6ef232..3ff29c23832 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.logs/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.logs/route.tsx @@ -32,9 +32,11 @@ import { LogsTaskFilter } from "~/components/logs/LogsTaskFilter"; import { LogsRunIdFilter } from "~/components/logs/LogsRunIdFilter"; import { TimeFilter } from "~/components/runs/v3/SharedFilters"; import { + RESIZABLE_PANEL_ANIMATION, ResizableHandle, ResizablePanel, ResizablePanelGroup, + collapsibleHandleClassName, } from "~/components/primitives/Resizable"; import { Button } from "~/components/primitives/Buttons"; import { FEATURE_FLAG, validateFeatureFlagValue } from "~/v3/featureFlags"; @@ -148,7 +150,7 @@ export const loader = async ({ request, params }: LoaderFunctionArgs) => { from, to, defaultPeriod: "1h", - retentionLimitDays + retentionLimitDays, }) .catch((error) => { if (error instanceof ServiceValidationError) { @@ -165,8 +167,7 @@ export const loader = async ({ request, params }: LoaderFunctionArgs) => { }; export default function Page() { - const { data, defaultPeriod, retentionLimitDays } = - useTypedLoaderData(); + const { data, defaultPeriod, retentionLimitDays } = useTypedLoaderData(); return ( @@ -192,10 +193,7 @@ export default function Page() { resolve={data} errorElement={
- +
Unable to load your logs. Please refresh the page or try again in a moment. @@ -228,10 +226,7 @@ export default function Page() { defaultPeriod={defaultPeriod} retentionLimitDays={retentionLimitDays} /> - +
); }} @@ -464,11 +459,21 @@ function LogsList({ onLogSelect={handleLogSelect} /> - {/* Side panel for log details */} - {selectedLogId && ( - <> - - + + {}} + collapsedSize="0px" + collapseAnimation={RESIZABLE_PANEL_ANIMATION} + > +
+ {selectedLogId && ( @@ -483,9 +488,9 @@ function LogsList({ searchTerm={list.searchTerm} /> - - - )} + )} +
+
); } From 3f38073552d9d12f1f9fd066ebfedee054386fad Mon Sep 17 00:00:00 2001 From: James Ritchie Date: Thu, 2 Apr 2026 15:57:28 +0100 Subject: [PATCH 009/243] Adds animated resizalbe to bulk actions --- .../route.tsx | 30 ++++++++++++++----- 1 file changed, 22 insertions(+), 8 deletions(-) diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.bulk-actions/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.bulk-actions/route.tsx index f44ce5904dc..a17f3e7d99e 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.bulk-actions/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.bulk-actions/route.tsx @@ -13,9 +13,11 @@ import { NavBar, PageAccessories, PageTitle } from "~/components/primitives/Page import { PaginationControls } from "~/components/primitives/Pagination"; import { Paragraph } from "~/components/primitives/Paragraph"; import { + RESIZABLE_PANEL_ANIMATION, ResizableHandle, ResizablePanel, ResizablePanelGroup, + collapsibleHandleClassName, } from "~/components/primitives/Resizable"; import { Table, @@ -170,14 +172,26 @@ export default function Page() { )}
- {isShowingInspector && ( - <> - - - - - - )} + + {}} + collapsedSize="0px" + collapseAnimation={RESIZABLE_PANEL_ANIMATION} + > +
+ +
+
)} From 4927d49f18dff9b128a6d8bc722f5a1b29d4a631 Mon Sep 17 00:00:00 2001 From: James Ritchie Date: Thu, 2 Apr 2026 16:02:12 +0100 Subject: [PATCH 010/243] Adds animated resizable to the Runs page --- .../route.tsx | 30 ++++++++++++++----- 1 file changed, 23 insertions(+), 7 deletions(-) diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs._index/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs._index/route.tsx index 9f8cf278bef..ca7e8b7b0c1 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs._index/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs._index/route.tsx @@ -20,9 +20,11 @@ import { InfoPanel } from "~/components/primitives/InfoPanel"; import { NavBar, PageAccessories, PageTitle } from "~/components/primitives/PageHeader"; import { Paragraph } from "~/components/primitives/Paragraph"; import { + RESIZABLE_PANEL_ANIMATION, ResizableHandle, ResizablePanel, ResizablePanelGroup, + collapsibleHandleClassName, } from "~/components/primitives/Resizable"; import { SelectedItemsProvider } from "~/components/primitives/SelectedItemsProvider"; import { ShortcutKey } from "~/components/primitives/ShortcutKey"; @@ -305,18 +307,32 @@ function RunsList({
- {isShowingBulkActionInspector && ( - <> - - + + {}} + collapsedSize="0px" + collapseAnimation={RESIZABLE_PANEL_ANIMATION} + > +
+ {isShowingBulkActionInspector && ( 0} /> - - - )} + )} +
+
); } From 957407e2e0b228878999b130f67733a395b1d323 Mon Sep 17 00:00:00 2001 From: James Ritchie Date: Thu, 2 Apr 2026 16:31:59 +0100 Subject: [PATCH 011/243] min-width fixes --- .../route.tsx | 4 ++-- .../route.tsx | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.schedules/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.schedules/route.tsx index 24608422e1c..769aa10cd75 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.schedules/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.schedules/route.tsx @@ -312,7 +312,7 @@ export default function Page() { /> -
+
diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.waitpoints.tokens/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.waitpoints.tokens/route.tsx index 7481ad892e1..782f3b132ff 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.waitpoints.tokens/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.waitpoints.tokens/route.tsx @@ -248,7 +248,7 @@ export default function Page() { /> -
+
From c7f1364a40cc78ba30715d03f3031ed2f1a2a961 Mon Sep 17 00:00:00 2001 From: James Ritchie Date: Thu, 2 Apr 2026 18:02:50 +0100 Subject: [PATCH 012/243] =?UTF-8?q?Deselect=20the=20span=20when=20the=20si?= =?UTF-8?q?de=20menu=20is=20hidden=20(fixes=20a=20bug=20where=20it=20could?= =?UTF-8?q?n=E2=80=99t=20be=20clicked=20again=20as=20the=20span=20stayed?= =?UTF-8?q?=20selected)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../app/components/primitives/TreeView/TreeView.tsx | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/apps/webapp/app/components/primitives/TreeView/TreeView.tsx b/apps/webapp/app/components/primitives/TreeView/TreeView.tsx index d1e002abb58..5f720c24fe9 100644 --- a/apps/webapp/app/components/primitives/TreeView/TreeView.tsx +++ b/apps/webapp/app/components/primitives/TreeView/TreeView.tsx @@ -197,6 +197,18 @@ export function useTree({ concreteStateFromInput({ tree, selectedId, collapsedIds, filter }) ); + //sync external selectedId prop into internal state + useEffect(() => { + const internalSelectedId = selectedIdFromState(state.nodes); + if (selectedId !== internalSelectedId) { + if (selectedId === undefined) { + dispatch({ type: "DESELECT_ALL_NODES" }); + } else { + dispatch({ type: "SELECT_NODE", payload: { id: selectedId, scrollToNode: false, scrollToNodeFn } }); + } + } + }, [selectedId]); + //fire onSelectedIdChanged() useEffect(() => { const selectedId = selectedIdFromState(state.nodes); From ce40729caf5e323d6235ed808ce50be9b3cb973f Mon Sep 17 00:00:00 2001 From: James Ritchie Date: Thu, 2 Apr 2026 18:24:58 +0100 Subject: [PATCH 013/243] Side inspector width improvements --- .../route.tsx | 23 ++++++++++++------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.batches/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.batches/route.tsx index 1e9bf46ad85..eaa040c4081 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.batches/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.batches/route.tsx @@ -1,9 +1,10 @@ -import { ArrowRightIcon, ExclamationCircleIcon } from "@heroicons/react/20/solid"; +import { ExclamationCircleIcon } from "@heroicons/react/20/solid"; import { BookOpenIcon } from "@heroicons/react/24/solid"; -import { type MetaFunction, Outlet, useNavigation, useParams, useLocation } from "@remix-run/react"; +import { type MetaFunction, Outlet, useLocation, useNavigation, useParams } from "@remix-run/react"; import { type LoaderFunctionArgs } from "@remix-run/server-runtime"; import { formatDuration } from "@trigger.dev/core/v3/utils/durations"; import { typedjson, useTypedLoaderData } from "remix-typedjson"; +import { RunsIcon } from "~/assets/icons/RunsIcon"; import { BatchesNone } from "~/components/BlankStatePanels"; import { ListPagination } from "~/components/ListPagination"; import { AdminDebugTooltip } from "~/components/admin/debugTooltip"; @@ -13,11 +14,11 @@ import { DateTime } from "~/components/primitives/DateTime"; import { NavBar, PageAccessories, PageTitle } from "~/components/primitives/PageHeader"; import { Paragraph } from "~/components/primitives/Paragraph"; import { + collapsibleHandleClassName, RESIZABLE_PANEL_ANIMATION, ResizableHandle, ResizablePanel, ResizablePanelGroup, - collapsibleHandleClassName, } from "~/components/primitives/Resizable"; import { Spinner } from "~/components/primitives/Spinner"; import { @@ -151,8 +152,8 @@ export default function Page() { /> -
+
@@ -300,8 +301,14 @@ function BatchActionsCell({ runsPath }: { runsPath: string }) { - View runs + + View runs } /> From 39dee29f8d5b793b0231214d2cc06c40e26e3f61 Mon Sep 17 00:00:00 2001 From: James Ritchie Date: Thu, 2 Apr 2026 18:25:32 +0100 Subject: [PATCH 014/243] View runs button improvements --- .../route.tsx | 28 ++++++++----------- 1 file changed, 11 insertions(+), 17 deletions(-) diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.batches.$batchParam/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.batches.$batchParam/route.tsx index 91403f4597d..6f5fc89341f 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.batches.$batchParam/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.batches.$batchParam/route.tsx @@ -1,4 +1,4 @@ -import { ArrowRightIcon, ExclamationTriangleIcon } from "@heroicons/react/20/solid"; +import { ExclamationTriangleIcon } from "@heroicons/react/20/solid"; import { type LoaderFunctionArgs } from "@remix-run/server-runtime"; import { tryCatch } from "@trigger.dev/core"; import { motion } from "framer-motion"; @@ -12,17 +12,14 @@ import { DateTime } from "~/components/primitives/DateTime"; import { Header2, Header3 } from "~/components/primitives/Headers"; import { Paragraph } from "~/components/primitives/Paragraph"; import * as Property from "~/components/primitives/PropertyTable"; -import { - BatchStatusCombo, - descriptionForBatchStatus, -} from "~/components/runs/v3/BatchStatus"; +import { BatchStatusCombo, descriptionForBatchStatus } from "~/components/runs/v3/BatchStatus"; import { useAutoRevalidate } from "~/hooks/useAutoRevalidate"; import { useEnvironment } from "~/hooks/useEnvironment"; import { useOrganization } from "~/hooks/useOrganizations"; import { useProject } from "~/hooks/useProject"; import { findProjectBySlug } from "~/models/project.server"; import { findEnvironmentBySlug } from "~/models/runtimeEnvironment.server"; -import { BatchPresenter, type BatchPresenterData } from "~/presenters/v3/BatchPresenter.server"; +import { BatchPresenter } from "~/presenters/v3/BatchPresenter.server"; import { requireUserId } from "~/services/session.server"; import { cn } from "~/utils/cn"; import { formatNumber } from "~/utils/numberFormatter"; @@ -35,8 +32,7 @@ const BatchParamSchema = EnvironmentParamSchema.extend({ export const loader = async ({ request, params }: LoaderFunctionArgs) => { const userId = await requireUserId(request); - const { organizationSlug, projectParam, envParam, batchParam } = - BatchParamSchema.parse(params); + const { organizationSlug, projectParam, envParam, batchParam } = BatchParamSchema.parse(params); const project = await findProjectBySlug(organizationSlug, projectParam, userId); if (!project) { @@ -85,7 +81,8 @@ export default function Page() { disabled: batch.hasFinished, }); - const showProgressMeter = batch.isV2 && (batch.status === "PROCESSING" || batch.status === "PARTIAL_FAILED"); + const showProgressMeter = + batch.isV2 && (batch.status === "PROCESSING" || batch.status === "PARTIAL_FAILED"); return (
@@ -141,9 +138,7 @@ export default function Page() { Version - - {batch.isV2 ? "v2 (Run Engine)" : "v1 (Legacy)"} - + {batch.isV2 ? "v2 (Run Engine)" : "v1 (Legacy)"} Total runs @@ -243,11 +238,11 @@ export default function Page() { {/* Footer */}
View runs @@ -304,4 +299,3 @@ function BatchProgressMeter({ successCount, failureCount, totalCount }: BatchPro
); } - From 5f237dba9921fa9ffd9e95819372e9d2cf8c551c Mon Sep 17 00:00:00 2001 From: James Ritchie Date: Thu, 2 Apr 2026 18:37:55 +0100 Subject: [PATCH 015/243] Coderabbit fixes --- .../route.tsx | 12 +++++++++--- .../route.tsx | 9 ++++++--- .../app/routes/storybook.animated-panel/route.tsx | 4 +++- 3 files changed, 18 insertions(+), 7 deletions(-) diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.logs/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.logs/route.tsx index 3ff29c23832..fc9cdb92a5b 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.logs/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.logs/route.tsx @@ -37,6 +37,7 @@ import { ResizablePanel, ResizablePanelGroup, collapsibleHandleClassName, + useFrozenValue, } from "~/components/primitives/Resizable"; import { Button } from "~/components/primitives/Buttons"; import { FEATURE_FLAG, validateFeatureFlagValue } from "~/v3/featureFlags"; @@ -404,6 +405,11 @@ function LogsList({ return accumulatedLogs.find((log) => log.id === selectedLogId); }, [selectedLogId, accumulatedLogs]); + const frozenLogId = useFrozenValue(selectedLogId); + const frozenLog = useFrozenValue(selectedLog); + const displayLogId = selectedLogId ?? frozenLogId; + const displayLog = selectedLog ?? frozenLog; + const updateUrlWithLog = useCallback((logId: string | undefined) => { const url = new URL(window.location.href); if (logId) { @@ -473,7 +479,7 @@ function LogsList({ collapseAnimation={RESIZABLE_PANEL_ANIMATION} >
- {selectedLogId && ( + {displayLogId && ( @@ -482,8 +488,8 @@ function LogsList({ } > diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.models._index/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.models._index/route.tsx index d5769c339c5..04fb26f1873 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.models._index/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.models._index/route.tsx @@ -41,6 +41,7 @@ import { ResizablePanel, ResizablePanelGroup, collapsibleHandleClassName, + useFrozenValue, } from "~/components/primitives/Resizable"; import { SearchInput } from "~/components/primitives/SearchInput"; import { Switch } from "~/components/primitives/Switch"; @@ -1096,6 +1097,8 @@ export default function ModelsPage() { const [showAllDetails, setShowAllDetails] = useState(false); const [compareOpen, setCompareOpen] = useState(false); const [selectedModel, setSelectedModel] = useState(null); + const frozenModel = useFrozenValue(selectedModel); + const displayModel = selectedModel ?? frozenModel; const popularMap = useMemo(() => { const map = new Map(); @@ -1181,10 +1184,10 @@ export default function ModelsPage() { collapseAnimation={RESIZABLE_PANEL_ANIMATION} >
- {selectedModel && ( + {displayModel && ( {}} + onCollapseChange={(isCollapsed) => { + if (isCollapsed) setSelectedItem(null); + }} collapsedSize="0px" collapseAnimation={RESIZABLE_PANEL_ANIMATION} > From e4712f4b6e30bbfe0dc5ec3ea65707c860980c45 Mon Sep 17 00:00:00 2001 From: James Ritchie Date: Thu, 2 Apr 2026 20:18:33 +0100 Subject: [PATCH 016/243] Code review fixes --- .../route.tsx | 4 +++- .../route.tsx | 15 +++++++++++---- 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.models._index/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.models._index/route.tsx index 04fb26f1873..2c39461ab2a 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.models._index/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.models._index/route.tsx @@ -1179,7 +1179,9 @@ export default function ModelsPage() { className="overflow-hidden" collapsible collapsed={!selectedModel} - onCollapseChange={() => {}} + onCollapseChange={(isCollapsed) => { + if (isCollapsed) setSelectedModel(null); + }} collapsedSize="0px" collapseAnimation={RESIZABLE_PANEL_ANIMATION} > diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs.$runParam/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs.$runParam/route.tsx index ed1d2f01055..3a6d3860dc2 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs.$runParam/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs.$runParam/route.tsx @@ -54,6 +54,7 @@ import { ResizablePanelGroup, type ResizableSnapshot, collapsibleHandleClassName, + useFrozenValue, } from "~/components/primitives/Resizable"; import { ShortcutKey, variants } from "~/components/primitives/ShortcutKey"; import { Slider } from "~/components/primitives/Slider"; @@ -470,6 +471,8 @@ function TraceView({ const environment = useEnvironment(); const { searchParams, replaceSearchParam } = useReplaceSearchParams(); const selectedSpanId = searchParams.get("span") ?? undefined; + const frozenSpanId = useFrozenValue(selectedSpanId); + const displaySpanId = selectedSpanId ?? frozenSpanId; if (!trace) { return <>; @@ -500,12 +503,16 @@ function TraceView({ }, [streamedEvents]); // eslint-disable-line react-hooks/exhaustive-deps const spanOverrides = selectedSpanId ? overridesBySpanId?.[selectedSpanId] : undefined; + const frozenSpanOverrides = useFrozenValue(spanOverrides); + const displaySpanOverrides = spanOverrides ?? frozenSpanOverrides; // Get the linked run ID for cached spans (map built during RunPresenter walk) const { linkedRunIdBySpanId } = trace; const selectedSpanLinkedRunId = selectedSpanId ? linkedRunIdBySpanId?.[selectedSpanId] : undefined; + const frozenLinkedRunId = useFrozenValue(selectedSpanLinkedRunId); + const displayLinkedRunId = selectedSpanLinkedRunId ?? frozenLinkedRunId; return (
@@ -563,13 +570,13 @@ function TraceView({ className="h-full" style={{ minWidth: parseInt(resizableSettings.parent.inspector.min) }} > - {selectedSpanId && ( + {displaySpanId && ( replaceSearchParam("span")} - linkedRunId={selectedSpanLinkedRunId} + linkedRunId={displayLinkedRunId} /> )}
From b96b9ebfc5d668872d605c2201f461d3523d570d Mon Sep 17 00:00:00 2001 From: James Ritchie Date: Thu, 2 Apr 2026 20:34:00 +0100 Subject: [PATCH 017/243] Fixed deployed page panel widths --- .../route.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.deployments/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.deployments/route.tsx index b17773b88a2..74b94bade76 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.deployments/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.deployments/route.tsx @@ -396,8 +396,8 @@ export default function Page() { /> -
+
From 064d97343d6a140a1a64ffe225699e4dd3853e17 Mon Sep 17 00:00:00 2001 From: James Ritchie Date: Thu, 2 Apr 2026 20:47:28 +0100 Subject: [PATCH 018/243] table hover improvements --- apps/webapp/app/components/GitMetadata.tsx | 9 ++++++--- .../route.tsx | 6 +++--- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/apps/webapp/app/components/GitMetadata.tsx b/apps/webapp/app/components/GitMetadata.tsx index efe3fb0efb7..fb53ee6bfea 100644 --- a/apps/webapp/app/components/GitMetadata.tsx +++ b/apps/webapp/app/components/GitMetadata.tsx @@ -25,9 +25,10 @@ export function GitMetadataBranch({ } + leadingIconClassName="group-hover/table-row:text-text-bright" iconSpacing="gap-x-1" to={git.branchUrl} - className="pl-1" + className="pl-1 duration-0 [&_span]:duration-0 [&_span]:group-hover/table-row:text-text-bright" > {git.branchName} @@ -49,8 +50,9 @@ export function GitMetadataCommit({ variant="minimal/small" to={git.commitUrl} LeadingIcon={} + leadingIconClassName="group-hover/table-row:text-text-bright" iconSpacing="gap-x-1" - className="pl-1" + className="pl-1 duration-0 [&_span]:duration-0 [&_span]:group-hover/table-row:text-text-bright" > {`${git.shortSha} / ${git.commitMessage}`} @@ -74,8 +76,9 @@ export function GitMetadataPullRequest({ variant="minimal/small" to={git.pullRequestUrl} LeadingIcon={} + leadingIconClassName="group-hover/table-row:text-text-bright" iconSpacing="gap-x-1" - className="pl-1" + className="pl-1 duration-0 [&_span]:duration-0 [&_span]:group-hover/table-row:text-text-bright" > #{git.pullRequestNumber} {git.pullRequestTitle} diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.deployments/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.deployments/route.tsx index 74b94bade76..9dbac88c51a 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.deployments/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.deployments/route.tsx @@ -257,7 +257,7 @@ export default function Page() {
- {deployment.shortCode} + {deployment.shortCode} {deployment.label && ( {titleCase(deployment.label)} )} @@ -419,8 +419,8 @@ export default function Page() { export function UserTag({ name, avatarUrl }: { name: string; avatarUrl?: string }) { return (
- - {name} + + {name}
); } From 0df31cfb16a66c4db6c4b8951a9701156e248b7b Mon Sep 17 00:00:00 2001 From: James Ritchie Date: Thu, 2 Apr 2026 21:25:00 +0100 Subject: [PATCH 019/243] devin fix --- .../route.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs.$runParam/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs.$runParam/route.tsx index 3a6d3860dc2..8a01627f308 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs.$runParam/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs.$runParam/route.tsx @@ -504,7 +504,7 @@ function TraceView({ const spanOverrides = selectedSpanId ? overridesBySpanId?.[selectedSpanId] : undefined; const frozenSpanOverrides = useFrozenValue(spanOverrides); - const displaySpanOverrides = spanOverrides ?? frozenSpanOverrides; + const displaySpanOverrides = selectedSpanId ? spanOverrides : frozenSpanOverrides; // Get the linked run ID for cached spans (map built during RunPresenter walk) const { linkedRunIdBySpanId } = trace; @@ -512,7 +512,7 @@ function TraceView({ ? linkedRunIdBySpanId?.[selectedSpanId] : undefined; const frozenLinkedRunId = useFrozenValue(selectedSpanLinkedRunId); - const displayLinkedRunId = selectedSpanLinkedRunId ?? frozenLinkedRunId; + const displayLinkedRunId = selectedSpanId ? selectedSpanLinkedRunId : frozenLinkedRunId; return (
From c4929c74963f84796ff8a664fb46697850ddee28 Mon Sep 17 00:00:00 2001 From: James Ritchie Date: Thu, 2 Apr 2026 21:48:30 +0100 Subject: [PATCH 020/243] Fixes ilegal DOM nesting --- .../route.tsx | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.regions/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.regions/route.tsx index 3484e1378b4..26daa24df34 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.regions/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.regions/route.tsx @@ -262,7 +262,7 @@ export default function Page() { } /> @@ -309,12 +309,9 @@ export default function Page() { security portal or{" "} + get in touch - + } defaultValue="help" /> @@ -345,20 +342,21 @@ function SetDefaultDialog({ Set as default region - + +
Are you sure you want to set {newDefaultRegion.name} as your new default region? @@ -441,6 +439,7 @@ function SetDefaultDialog({ Runs triggered from now on will execute in "{newDefaultRegion.name}", unless you{" "} override when triggering. +
- {hasTasks && showUsefulLinks ? ( - <> - - - handleUsefulLinksToggle(false)} /> - - - ) : null} + + {}} + collapsedSize="0px" + collapseAnimation={RESIZABLE_PANEL_ANIMATION} + > +
+ {hasTasks && ( + toggleUsefulLinks(false)} /> + )} +
+
@@ -850,3 +867,54 @@ function FailedToLoadStats() { /> ); } + +function AnimatedSearchField({ + value, + onChange, + placeholder, + autoFocus, +}: { + value: string; + onChange: (value: string) => void; + placeholder?: string; + autoFocus?: boolean; +}) { + const [isFocused, setIsFocused] = useState(false); + + return ( + 0 ? "24rem" : "auto" }} + transition={{ type: "spring", stiffness: 300, damping: 30 }} + className="relative h-6 min-w-52" + > + onChange(e.target.value)} + fullWidth + autoFocus={autoFocus} + className={cn(isFocused && "placeholder:text-text-dimmed/70")} + onFocus={() => setIsFocused(true)} + onBlur={() => setIsFocused(false)} + onKeyDown={(e) => { + if (e.key === "Escape") e.currentTarget.blur(); + }} + icon={} + accessory={ + value.length > 0 ? ( + + ) : undefined + } + /> + + ); +} diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.batches.$batchParam/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.batches.$batchParam/route.tsx index 91403f4597d..6f5fc89341f 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.batches.$batchParam/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.batches.$batchParam/route.tsx @@ -1,4 +1,4 @@ -import { ArrowRightIcon, ExclamationTriangleIcon } from "@heroicons/react/20/solid"; +import { ExclamationTriangleIcon } from "@heroicons/react/20/solid"; import { type LoaderFunctionArgs } from "@remix-run/server-runtime"; import { tryCatch } from "@trigger.dev/core"; import { motion } from "framer-motion"; @@ -12,17 +12,14 @@ import { DateTime } from "~/components/primitives/DateTime"; import { Header2, Header3 } from "~/components/primitives/Headers"; import { Paragraph } from "~/components/primitives/Paragraph"; import * as Property from "~/components/primitives/PropertyTable"; -import { - BatchStatusCombo, - descriptionForBatchStatus, -} from "~/components/runs/v3/BatchStatus"; +import { BatchStatusCombo, descriptionForBatchStatus } from "~/components/runs/v3/BatchStatus"; import { useAutoRevalidate } from "~/hooks/useAutoRevalidate"; import { useEnvironment } from "~/hooks/useEnvironment"; import { useOrganization } from "~/hooks/useOrganizations"; import { useProject } from "~/hooks/useProject"; import { findProjectBySlug } from "~/models/project.server"; import { findEnvironmentBySlug } from "~/models/runtimeEnvironment.server"; -import { BatchPresenter, type BatchPresenterData } from "~/presenters/v3/BatchPresenter.server"; +import { BatchPresenter } from "~/presenters/v3/BatchPresenter.server"; import { requireUserId } from "~/services/session.server"; import { cn } from "~/utils/cn"; import { formatNumber } from "~/utils/numberFormatter"; @@ -35,8 +32,7 @@ const BatchParamSchema = EnvironmentParamSchema.extend({ export const loader = async ({ request, params }: LoaderFunctionArgs) => { const userId = await requireUserId(request); - const { organizationSlug, projectParam, envParam, batchParam } = - BatchParamSchema.parse(params); + const { organizationSlug, projectParam, envParam, batchParam } = BatchParamSchema.parse(params); const project = await findProjectBySlug(organizationSlug, projectParam, userId); if (!project) { @@ -85,7 +81,8 @@ export default function Page() { disabled: batch.hasFinished, }); - const showProgressMeter = batch.isV2 && (batch.status === "PROCESSING" || batch.status === "PARTIAL_FAILED"); + const showProgressMeter = + batch.isV2 && (batch.status === "PROCESSING" || batch.status === "PARTIAL_FAILED"); return (
@@ -141,9 +138,7 @@ export default function Page() { Version - - {batch.isV2 ? "v2 (Run Engine)" : "v1 (Legacy)"} - + {batch.isV2 ? "v2 (Run Engine)" : "v1 (Legacy)"} Total runs @@ -243,11 +238,11 @@ export default function Page() { {/* Footer */}
View runs @@ -304,4 +299,3 @@ function BatchProgressMeter({ successCount, failureCount, totalCount }: BatchPro
); } - diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.batches/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.batches/route.tsx index a66e85c0f86..eaa040c4081 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.batches/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.batches/route.tsx @@ -1,9 +1,10 @@ -import { ArrowRightIcon, ExclamationCircleIcon } from "@heroicons/react/20/solid"; +import { ExclamationCircleIcon } from "@heroicons/react/20/solid"; import { BookOpenIcon } from "@heroicons/react/24/solid"; -import { type MetaFunction, Outlet, useNavigation, useParams, useLocation } from "@remix-run/react"; +import { type MetaFunction, Outlet, useLocation, useNavigation, useParams } from "@remix-run/react"; import { type LoaderFunctionArgs } from "@remix-run/server-runtime"; import { formatDuration } from "@trigger.dev/core/v3/utils/durations"; import { typedjson, useTypedLoaderData } from "remix-typedjson"; +import { RunsIcon } from "~/assets/icons/RunsIcon"; import { BatchesNone } from "~/components/BlankStatePanels"; import { ListPagination } from "~/components/ListPagination"; import { AdminDebugTooltip } from "~/components/admin/debugTooltip"; @@ -13,6 +14,8 @@ import { DateTime } from "~/components/primitives/DateTime"; import { NavBar, PageAccessories, PageTitle } from "~/components/primitives/PageHeader"; import { Paragraph } from "~/components/primitives/Paragraph"; import { + collapsibleHandleClassName, + RESIZABLE_PANEL_ANIMATION, ResizableHandle, ResizablePanel, ResizablePanelGroup, @@ -143,14 +146,25 @@ export default function Page() { />
- {isShowingInspector && ( - <> - - - - - - )} + + {}} + collapsedSize="0px" + collapseAnimation={RESIZABLE_PANEL_ANIMATION} + > +
+ +
+
)} @@ -287,8 +301,14 @@ function BatchActionsCell({ runsPath }: { runsPath: string }) { - View runs + + View runs } /> diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.bulk-actions/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.bulk-actions/route.tsx index f44ce5904dc..a17f3e7d99e 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.bulk-actions/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.bulk-actions/route.tsx @@ -13,9 +13,11 @@ import { NavBar, PageAccessories, PageTitle } from "~/components/primitives/Page import { PaginationControls } from "~/components/primitives/Pagination"; import { Paragraph } from "~/components/primitives/Paragraph"; import { + RESIZABLE_PANEL_ANIMATION, ResizableHandle, ResizablePanel, ResizablePanelGroup, + collapsibleHandleClassName, } from "~/components/primitives/Resizable"; import { Table, @@ -170,14 +172,26 @@ export default function Page() { )}
- {isShowingInspector && ( - <> - - - - - - )} + + {}} + collapsedSize="0px" + collapseAnimation={RESIZABLE_PANEL_ANIMATION} + > +
+ +
+
)} diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.deployments/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.deployments/route.tsx index a42b39c4573..9dbac88c51a 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.deployments/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.deployments/route.tsx @@ -42,9 +42,11 @@ import { NavBar, PageAccessories, PageTitle } from "~/components/primitives/Page import { PaginationControls } from "~/components/primitives/Pagination"; import { Paragraph } from "~/components/primitives/Paragraph"; import { + RESIZABLE_PANEL_ANIMATION, ResizableHandle, ResizablePanel, ResizablePanelGroup, + collapsibleHandleClassName, } from "~/components/primitives/Resizable"; import { Table, @@ -255,7 +257,7 @@ export default function Page() {
- {deployment.shortCode} + {deployment.shortCode} {deployment.label && ( {titleCase(deployment.label)} )} @@ -388,14 +390,26 @@ export default function Page() { )} - {deploymentParam && ( - <> - - - - - - )} + + {}} + collapsedSize="0px" + collapseAnimation={RESIZABLE_PANEL_ANIMATION} + > +
+ +
+
@@ -405,8 +419,8 @@ export default function Page() { export function UserTag({ name, avatarUrl }: { name: string; avatarUrl?: string }) { return (
- - {name} + + {name}
); } diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.logs/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.logs/route.tsx index 80a5c6ef232..af3cc30a246 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.logs/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.logs/route.tsx @@ -32,9 +32,12 @@ import { LogsTaskFilter } from "~/components/logs/LogsTaskFilter"; import { LogsRunIdFilter } from "~/components/logs/LogsRunIdFilter"; import { TimeFilter } from "~/components/runs/v3/SharedFilters"; import { + RESIZABLE_PANEL_ANIMATION, ResizableHandle, ResizablePanel, ResizablePanelGroup, + collapsibleHandleClassName, + useFrozenValue, } from "~/components/primitives/Resizable"; import { Button } from "~/components/primitives/Buttons"; import { FEATURE_FLAG, validateFeatureFlagValue } from "~/v3/featureFlags"; @@ -148,7 +151,7 @@ export const loader = async ({ request, params }: LoaderFunctionArgs) => { from, to, defaultPeriod: "1h", - retentionLimitDays + retentionLimitDays, }) .catch((error) => { if (error instanceof ServiceValidationError) { @@ -165,8 +168,7 @@ export const loader = async ({ request, params }: LoaderFunctionArgs) => { }; export default function Page() { - const { data, defaultPeriod, retentionLimitDays } = - useTypedLoaderData(); + const { data, defaultPeriod, retentionLimitDays } = useTypedLoaderData(); return ( @@ -192,10 +194,7 @@ export default function Page() { resolve={data} errorElement={
- +
Unable to load your logs. Please refresh the page or try again in a moment. @@ -228,10 +227,7 @@ export default function Page() { defaultPeriod={defaultPeriod} retentionLimitDays={retentionLimitDays} /> - +
); }} @@ -409,6 +405,11 @@ function LogsList({ return accumulatedLogs.find((log) => log.id === selectedLogId); }, [selectedLogId, accumulatedLogs]); + const frozenLogId = useFrozenValue(selectedLogId); + const frozenLog = useFrozenValue(selectedLog); + const displayLogId = selectedLogId ?? frozenLogId; + const displayLog = selectedLog ?? frozenLog ?? undefined; + const updateUrlWithLog = useCallback((logId: string | undefined) => { const url = new URL(window.location.href); if (logId) { @@ -464,11 +465,21 @@ function LogsList({ onLogSelect={handleLogSelect} /> - {/* Side panel for log details */} - {selectedLogId && ( - <> - - + + {}} + collapsedSize="0px" + collapseAnimation={RESIZABLE_PANEL_ANIMATION} + > +
+ {displayLogId && ( @@ -477,15 +488,15 @@ function LogsList({ } > - - - )} + )} +
+
); } diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.models._index/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.models._index/route.tsx index 4abf40c0335..7bf257f987b 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.models._index/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.models._index/route.tsx @@ -36,9 +36,12 @@ import { Header2 } from "~/components/primitives/Headers"; import { NavBar, PageTitle } from "~/components/primitives/PageHeader"; import * as Property from "~/components/primitives/PropertyTable"; import { + RESIZABLE_PANEL_ANIMATION, ResizableHandle, ResizablePanel, ResizablePanelGroup, + collapsibleHandleClassName, + useFrozenValue, } from "~/components/primitives/Resizable"; import { SearchInput } from "~/components/primitives/SearchInput"; import { Switch } from "~/components/primitives/Switch"; @@ -703,7 +706,7 @@ function ModelDetailPanel({ className="pl-1" />
-
+
(null); + const frozenModel = useFrozenValue(selectedModel); + const displayModel = selectedModel ?? frozenModel; const popularMap = useMemo(() => { const map = new Map(); @@ -1132,21 +1137,37 @@ export default function ModelsPage() { />
- {selectedModel && ( - <> - - + + { + if (isCollapsed) setSelectedModel(null); + }} + collapsedSize="0px" + collapseAnimation={RESIZABLE_PANEL_ANIMATION} + > +
+ {displayModel && ( setSelectedModel(null)} /> - - - )} + )} +
+
} /> @@ -309,12 +309,9 @@ export default function Page() { security portal or{" "} + get in touch - + } defaultValue="help" /> @@ -345,20 +342,21 @@ function SetDefaultDialog({ Set as default region - + +
Are you sure you want to set {newDefaultRegion.name} as your new default region? @@ -441,6 +439,7 @@ function SetDefaultDialog({ Runs triggered from now on will execute in "{newDefaultRegion.name}", unless you{" "} override when triggering. +
- {isShowingBulkActionInspector && ( - <> - - + + {}} + collapsedSize="0px" + collapseAnimation={RESIZABLE_PANEL_ANIMATION} + > +
+ {isShowingBulkActionInspector && ( 0} /> - - - )} + )} +
+
); } diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.schedules/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.schedules/route.tsx index d32f9ecb5b4..769aa10cd75 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.schedules/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.schedules/route.tsx @@ -25,9 +25,11 @@ import { PaginationControls } from "~/components/primitives/Pagination"; import { Paragraph } from "~/components/primitives/Paragraph"; import * as Property from "~/components/primitives/PropertyTable"; import { + RESIZABLE_PANEL_ANIMATION, ResizableHandle, ResizablePanel, ResizablePanelGroup, + collapsibleHandleClassName, } from "~/components/primitives/Resizable"; import { Table, @@ -304,14 +306,25 @@ export default function Page() { )}
- {(isShowingNewPane || isShowingSchedule) && ( - <> - - - - - - )} + + {}} + collapsedSize="0px" + collapseAnimation={RESIZABLE_PANEL_ANIMATION} + > +
+ +
+
diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.waitpoints.tokens/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.waitpoints.tokens/route.tsx index b3233abb858..782f3b132ff 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.waitpoints.tokens/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.waitpoints.tokens/route.tsx @@ -13,9 +13,11 @@ import { DateTime } from "~/components/primitives/DateTime"; import { NavBar, PageAccessories, PageTitle } from "~/components/primitives/PageHeader"; import { Paragraph } from "~/components/primitives/Paragraph"; import { + RESIZABLE_PANEL_ANIMATION, ResizableHandle, ResizablePanel, ResizablePanelGroup, + collapsibleHandleClassName, } from "~/components/primitives/Resizable"; import { Table, @@ -240,14 +242,25 @@ export default function Page() {
- {isShowingWaitpoint && ( - <> - - - - - - )} + + {}} + collapsedSize="0px" + collapseAnimation={RESIZABLE_PANEL_ANIMATION} + > +
+ +
+
)} diff --git a/apps/webapp/app/routes/storybook.animated-panel/route.tsx b/apps/webapp/app/routes/storybook.animated-panel/route.tsx new file mode 100644 index 00000000000..2f136bddb19 --- /dev/null +++ b/apps/webapp/app/routes/storybook.animated-panel/route.tsx @@ -0,0 +1,178 @@ +import { useState } from "react"; +import { ExitIcon } from "~/assets/icons/ExitIcon"; +import { Button } from "~/components/primitives/Buttons"; +import { Header2 } from "~/components/primitives/Headers"; +import { Paragraph } from "~/components/primitives/Paragraph"; +import * as Property from "~/components/primitives/PropertyTable"; +import { + RESIZABLE_PANEL_ANIMATION, + ResizableHandle, + ResizablePanel, + ResizablePanelGroup, + collapsibleHandleClassName, + useFrozenValue, +} from "~/components/primitives/Resizable"; +import { + Table, + TableBody, + TableCell, + TableHeader, + TableHeaderCell, + TableRow, +} from "~/components/primitives/Table"; + +type DemoItem = { + id: string; + name: string; + status: "completed" | "running" | "failed" | "queued"; + duration: string; + task: string; +}; + +const demoItems: DemoItem[] = [ + { id: "run_a1b2c3d4", name: "Process invoices", status: "completed", duration: "2.3s", task: "invoice/process" }, + { id: "run_e5f6g7h8", name: "Send welcome email", status: "running", duration: "0.8s", task: "email/welcome" }, + { id: "run_i9j0k1l2", name: "Generate report", status: "failed", duration: "12.1s", task: "report/generate" }, + { id: "run_m3n4o5p6", name: "Sync inventory", status: "completed", duration: "5.7s", task: "inventory/sync" }, + { id: "run_q7r8s9t0", name: "Resize images", status: "queued", duration: "—", task: "image/resize" }, + { id: "run_u1v2w3x4", name: "Update search index", status: "completed", duration: "1.1s", task: "search/index" }, + { id: "run_y5z6a7b8", name: "Calculate analytics", status: "running", duration: "8.4s", task: "analytics/calc" }, + { id: "run_c9d0e1f2", name: "Deploy preview", status: "completed", duration: "34.2s", task: "deploy/preview" }, + { id: "run_g3h4i5j6", name: "Run migrations", status: "failed", duration: "0.3s", task: "db/migrate" }, + { id: "run_k7l8m9n0", name: "Notify Slack", status: "completed", duration: "0.5s", task: "notify/slack" }, +]; + +const statusColors: Record = { + completed: "text-success", + running: "text-blue-500", + failed: "text-error", + queued: "text-text-dimmed", +}; + +function DetailPanel({ item, onClose }: { item: DemoItem; onClose: () => void }) { + return ( +
+
+ {item.name} +
+
+ + + Run ID + {item.id} + + + Task + {item.task} + + + Status + + + {item.status.charAt(0).toUpperCase() + item.status.slice(1)} + + + + + Duration + {item.duration} + + +
+ + This is a demo detail panel showing the animated slide-in/out behavior using + react-window-splitter's collapseAnimation. Click a different row to change the + detail, or press Esc / click the close button to dismiss. + +
+
+
+ ); +} + +export default function Story() { + const [selectedItem, setSelectedItem] = useState(null); + const show = !!selectedItem; + const frozenItem = useFrozenValue(selectedItem); + const displayItem = selectedItem ?? frozenItem; + + return ( +
+ + +
+
+ Runs +
+ + + + Run ID + Name + Task + Status + Duration + + + + {demoItems.map((item) => ( + + setSelectedItem(item)} isTabbableCell> + {item.id} + + setSelectedItem(item)}>{item.name} + setSelectedItem(item)}>{item.task} + setSelectedItem(item)}> + + {item.status.charAt(0).toUpperCase() + item.status.slice(1)} + + + setSelectedItem(item)} alignment="right"> + {item.duration} + + + ))} + +
+
+
+ + { + if (isCollapsed) setSelectedItem(null); + }} + collapsedSize="0px" + collapseAnimation={RESIZABLE_PANEL_ANIMATION} + > +
+ {displayItem && ( + setSelectedItem(null)} + /> + )} +
+
+
+
+ ); +} diff --git a/apps/webapp/app/routes/storybook/route.tsx b/apps/webapp/app/routes/storybook/route.tsx index bcaee62d6b0..3efa990548c 100644 --- a/apps/webapp/app/routes/storybook/route.tsx +++ b/apps/webapp/app/routes/storybook/route.tsx @@ -8,6 +8,10 @@ import { requireUser } from "~/services/session.server"; import { cn } from "~/utils/cn"; const stories: Story[] = [ + { + name: "Animated panel", + slug: "animated-panel", + }, { name: "Avatar", slug: "avatar", From 4f2ff3d9def90ebcd09cedb42a67eb61612ca99c Mon Sep 17 00:00:00 2001 From: James Ritchie Date: Sat, 4 Apr 2026 11:02:12 +0100 Subject: [PATCH 023/243] fix(wabapp): Fix for wrapping text on run inspector (#3328) ### Text wrapping fix - Fixes message text not wrapping on the run inspector if there were no spaces in the text - Fixes inspector title truncation - Adds a copy text button for the Message property CleanShot 2026-04-04 at 10 19 02@2x --- .../components/primitives/CopyTextLink.tsx | 33 +++++++++++++++++ .../route.tsx | 36 ++++++++++++------- 2 files changed, 57 insertions(+), 12 deletions(-) create mode 100644 apps/webapp/app/components/primitives/CopyTextLink.tsx diff --git a/apps/webapp/app/components/primitives/CopyTextLink.tsx b/apps/webapp/app/components/primitives/CopyTextLink.tsx new file mode 100644 index 00000000000..33818fa6077 --- /dev/null +++ b/apps/webapp/app/components/primitives/CopyTextLink.tsx @@ -0,0 +1,33 @@ +import { ClipboardCheckIcon, ClipboardIcon } from "lucide-react"; +import { useCopy } from "~/hooks/useCopy"; +import { cn } from "~/utils/cn"; + +type CopyTextLinkProps = { + value: string; + className?: string; +}; + +export function CopyTextLink({ value, className }: CopyTextLinkProps) { + const { copy, copied } = useCopy(value); + + return ( + + ); +} diff --git a/apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs.$runParam.spans.$spanParam/route.tsx b/apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs.$runParam.spans.$spanParam/route.tsx index 3d8d7ed4e31..e0bec66ffb2 100644 --- a/apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs.$runParam.spans.$spanParam/route.tsx +++ b/apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs.$runParam.spans.$spanParam/route.tsx @@ -32,6 +32,7 @@ import { MachineTooltipInfo } from "~/components/MachineTooltipInfo"; import { Button, LinkButton } from "~/components/primitives/Buttons"; import { Callout } from "~/components/primitives/Callout"; import { CopyableText } from "~/components/primitives/CopyableText"; +import { CopyTextLink } from "~/components/primitives/CopyTextLink"; import { DateTime, DateTimeAccurate } from "~/components/primitives/DateTime"; import { Header2, Header3 } from "~/components/primitives/Headers"; import { Paragraph } from "~/components/primitives/Paragraph"; @@ -263,19 +264,21 @@ function SpanBody({ span.entity?.type === "prompt"; return ( -
+
-
-
+
+
- +
@@ -311,7 +314,6 @@ function formatSpanDuration(nanoseconds: number): string { return `${mins}m ${secs}s`; } - function applySpanOverrides(span: Span, spanOverrides?: SpanOverride): Span { if (!spanOverrides) { return span; @@ -1259,8 +1261,13 @@ function SpanEntity({ span }: { span: Span }) { )} - Message - {span.message} + + Message + + + + {span.message} + {span.events.length > 0 && } @@ -1416,7 +1423,13 @@ function SpanEntity({ span }: { span: Span }) { @@ -1456,4 +1469,3 @@ function SpanEntity({ span }: { span: Span }) { } } } - From def21b26b64781945acdb0ca352a27213ab94e84 Mon Sep 17 00:00:00 2001 From: Matt Aitken Date: Tue, 7 Apr 2026 15:29:10 +0100 Subject: [PATCH 024/243] fix(batch): retry R2 upload on transient failure in BatchPayloadProcessor (#3331) A single "fetch failed" from the object store was aborting the entire batch stream with no retry. Added p-retry (3 attempts, 500ms-2s backoff) around ploadPacketToObjectStore so transient network errors self-heal server-side instead of propagating to the SDK. --- .server-changes/batch-r2-upload-retry.md | 9 ++ .../routes/api.v3.batches.$batchId.items.ts | 5 +- .../concerns/batchPayloads.server.ts | 45 ++++--- apps/webapp/test/engine/batchPayloads.test.ts | 115 ++++++++++++++++++ 4 files changed, 155 insertions(+), 19 deletions(-) create mode 100644 .server-changes/batch-r2-upload-retry.md create mode 100644 apps/webapp/test/engine/batchPayloads.test.ts diff --git a/.server-changes/batch-r2-upload-retry.md b/.server-changes/batch-r2-upload-retry.md new file mode 100644 index 00000000000..a2c6415635b --- /dev/null +++ b/.server-changes/batch-r2-upload-retry.md @@ -0,0 +1,9 @@ +--- +area: webapp +type: fix +--- + +Fix transient R2/object store upload failures during batchTrigger() item streaming. + +- Added p-retry (3 attempts, 500ms–2s exponential backoff) around `uploadPacketToObjectStore` in `BatchPayloadProcessor.process()` so transient network errors self-heal server-side rather than aborting the entire batch stream. +- Removed `x-should-retry: false` from the 500 response on the batch items route so the SDK's existing 5xx retry path can recover if server-side retries are exhausted. Item deduplication by index makes full-stream retries safe. diff --git a/apps/webapp/app/routes/api.v3.batches.$batchId.items.ts b/apps/webapp/app/routes/api.v3.batches.$batchId.items.ts index b3ed1c22422..2d732d1555a 100644 --- a/apps/webapp/app/routes/api.v3.batches.$batchId.items.ts +++ b/apps/webapp/app/routes/api.v3.batches.$batchId.items.ts @@ -104,10 +104,7 @@ export async function action({ request, params }: ActionFunctionArgs) { return json({ error: error.message }, { status: 400 }); } - return json( - { error: error.message }, - { status: 500, headers: { "x-should-retry": "false" } } - ); + return json({ error: error.message }, { status: 500 }); } return json({ error: "Something went wrong" }, { status: 500 }); diff --git a/apps/webapp/app/runEngine/concerns/batchPayloads.server.ts b/apps/webapp/app/runEngine/concerns/batchPayloads.server.ts index eeb33fa4b41..ab464f03fa2 100644 --- a/apps/webapp/app/runEngine/concerns/batchPayloads.server.ts +++ b/apps/webapp/app/runEngine/concerns/batchPayloads.server.ts @@ -1,4 +1,5 @@ import { type IOPacket, packetRequiresOffloading, tryCatch } from "@trigger.dev/core/v3"; +import pRetry from "p-retry"; import { env } from "~/env.server"; import type { AuthenticatedEnvironment } from "~/services/apiAuth.server"; import { logger } from "~/services/logger.server"; @@ -103,32 +104,46 @@ export class BatchPayloadProcessor { }; } - // Upload to object store + // Upload to object store, retrying on transient network errors + const { data: packetData, dataType: packetDataType } = packet; const filename = `batch_${batchId}/item_${itemIndex}/payload.json`; const [uploadError, uploadedFilename] = await tryCatch( - uploadPacketToObjectStore( - filename, - packet.data, - packet.dataType, - environment, - env.OBJECT_STORE_DEFAULT_PROTOCOL + pRetry( + () => + uploadPacketToObjectStore( + filename, + packetData, + packetDataType, + environment, + env.OBJECT_STORE_DEFAULT_PROTOCOL + ), + { + retries: 3, + minTimeout: 500, + maxTimeout: 2000, + factor: 2, + onFailedAttempt: (error) => { + logger.warn("Batch item payload upload to object store failed, retrying", { + batchId, + itemIndex, + attempt: error.attemptNumber, + retriesLeft: error.retriesLeft, + error: error.message, + }); + }, + } ) ); if (uploadError) { - logger.error("Failed to upload batch item payload to object store", { + logger.error("Failed to upload batch item payload to object store after retries", { batchId, itemIndex, - error: uploadError instanceof Error ? uploadError.message : String(uploadError), + error: uploadError.message, }); - // Throw to fail this item - SDK can retry - throw new Error( - `Failed to upload large payload to object store: ${ - uploadError instanceof Error ? uploadError.message : String(uploadError) - }` - ); + throw new Error(`Failed to upload large payload to object store: ${uploadError.message}`); } logger.debug("Batch item payload offloaded to object store", { diff --git a/apps/webapp/test/engine/batchPayloads.test.ts b/apps/webapp/test/engine/batchPayloads.test.ts new file mode 100644 index 00000000000..69ef6224e74 --- /dev/null +++ b/apps/webapp/test/engine/batchPayloads.test.ts @@ -0,0 +1,115 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; + +// --- Module mocks (must come before imports) --- + +vi.mock("~/v3/objectStore.server", () => ({ + hasObjectStoreClient: vi.fn().mockReturnValue(true), + uploadPacketToObjectStore: vi.fn(), +})); + +// Threshold of 10 bytes so any non-trivial payload triggers offloading +vi.mock("~/env.server", () => ({ + env: { + BATCH_PAYLOAD_OFFLOAD_THRESHOLD: 10, + TASK_PAYLOAD_OFFLOAD_THRESHOLD: 10, + OBJECT_STORE_DEFAULT_PROTOCOL: undefined, + }, +})); + +// Execute the span callback synchronously without real OTel +vi.mock("~/v3/tracer.server", () => ({ + startActiveSpan: vi.fn(async (_name: string, fn: (span: any) => any) => + fn({ setAttribute: vi.fn() }) + ), +})); + +import { BatchPayloadProcessor } from "../../app/runEngine/concerns/batchPayloads.server"; +import * as objectStore from "~/v3/objectStore.server"; + +vi.setConfig({ testTimeout: 30_000 }); + +// Minimal AuthenticatedEnvironment shape required by BatchPayloadProcessor +const mockEnvironment = { + id: "env-test", + slug: "production", + project: { externalRef: "proj-ext-ref" }, +} as any; + +describe("BatchPayloadProcessor", () => { + let mockUpload: ReturnType>; + + beforeEach(() => { + mockUpload = vi.mocked(objectStore.uploadPacketToObjectStore); + mockUpload.mockReset(); + }); + + it("offloads a large payload successfully on first attempt", async () => { + mockUpload.mockResolvedValueOnce("batch_abc/item_0/payload.json"); + + const processor = new BatchPayloadProcessor(); + const result = await processor.process( + '{"message":"hello world"}', + "application/json", + "batch-internal-abc", + 0, + mockEnvironment + ); + + expect(result.wasOffloaded).toBe(true); + expect(result.payloadType).toBe("application/store"); + expect(result.payload).toBe("batch_abc/item_0/payload.json"); + expect(mockUpload).toHaveBeenCalledTimes(1); + }); + + it("retries on transient fetch failure and succeeds on third attempt", async () => { + mockUpload + .mockRejectedValueOnce(new Error("fetch failed")) + .mockRejectedValueOnce(new Error("fetch failed")) + .mockResolvedValueOnce("batch_abc/item_0/payload.json"); + + const processor = new BatchPayloadProcessor(); + const result = await processor.process( + '{"message":"hello world"}', + "application/json", + "batch-internal-abc", + 0, + mockEnvironment + ); + + expect(result.wasOffloaded).toBe(true); + expect(mockUpload).toHaveBeenCalledTimes(3); + }); + + it("throws after exhausting all retry attempts", async () => { + mockUpload.mockRejectedValue(new Error("fetch failed")); + + const processor = new BatchPayloadProcessor(); + + await expect( + processor.process( + '{"message":"hello world"}', + "application/json", + "batch-internal-abc", + 0, + mockEnvironment + ) + ).rejects.toThrow("Failed to upload large payload to object store: fetch failed"); + + // 1 initial attempt + 3 retries = 4 total calls + expect(mockUpload).toHaveBeenCalledTimes(4); + }); + + it("does not offload when there is no payload data", async () => { + const processor = new BatchPayloadProcessor(); + const result = await processor.process( + undefined, + "application/json", + "batch-internal-abc", + 0, + mockEnvironment + ); + + expect(result.wasOffloaded).toBe(false); + expect(mockUpload).not.toHaveBeenCalled(); + }); +}); From bd41bb2cbd734c4fd36f4046f81044ab52f84a14 Mon Sep 17 00:00:00 2001 From: nicktrn <55853254+nicktrn@users.noreply.github.com> Date: Wed, 8 Apr 2026 22:32:46 +0100 Subject: [PATCH 025/243] feat(webapp): set application_name on prisma connections (#3348) Sets `application_name` on the Prisma writer and replica connection strings using the existing `SERVICE_NAME` env var, so DB load can be attributed by service. --- .server-changes/prisma-application-name.md | 6 ++++++ apps/webapp/app/db.server.ts | 2 ++ 2 files changed, 8 insertions(+) create mode 100644 .server-changes/prisma-application-name.md diff --git a/.server-changes/prisma-application-name.md b/.server-changes/prisma-application-name.md new file mode 100644 index 00000000000..825058f3b34 --- /dev/null +++ b/.server-changes/prisma-application-name.md @@ -0,0 +1,6 @@ +--- +area: webapp +type: improvement +--- + +Set `application_name` on Prisma connections from SERVICE_NAME so DB load can be attributed by service diff --git a/apps/webapp/app/db.server.ts b/apps/webapp/app/db.server.ts index 47b67a1a406..4668b58fb02 100644 --- a/apps/webapp/app/db.server.ts +++ b/apps/webapp/app/db.server.ts @@ -113,6 +113,7 @@ function getClient() { connection_limit: env.DATABASE_CONNECTION_LIMIT.toString(), pool_timeout: env.DATABASE_POOL_TIMEOUT.toString(), connection_timeout: env.DATABASE_CONNECTION_TIMEOUT.toString(), + application_name: env.SERVICE_NAME, }); console.log(`🔌 setting up prisma client to ${redactUrlSecrets(databaseUrl)}`); @@ -236,6 +237,7 @@ function getReplicaClient() { connection_limit: env.DATABASE_CONNECTION_LIMIT.toString(), pool_timeout: env.DATABASE_POOL_TIMEOUT.toString(), connection_timeout: env.DATABASE_CONNECTION_TIMEOUT.toString(), + application_name: env.SERVICE_NAME, }); console.log(`🔌 setting up read replica connection to ${redactUrlSecrets(replicaUrl)}`); From e59614a31c1421b0aa8ad54ec030058c2a42b06f Mon Sep 17 00:00:00 2001 From: nicktrn <55853254+nicktrn@users.noreply.github.com> Date: Mon, 13 Apr 2026 11:24:00 +0100 Subject: [PATCH 026/243] feat(webapp): gate microvm regions behind compute access feature flag (#3366) Adds region-level gating so MICROVM regions are only visible and usable by orgs with the `hasComputeAccess` feature flag. Admins and explicit allowlist behavior unchanged. - New shared helper (`regionAccess.server.ts`) with `resolveComputeAccess`, `defaultVisibilityFilter`, and `isComputeRegionAccessible` - `RegionsPresenter` filters out MICROVM regions for non-compute orgs - `SetDefaultRegionService` blocks setting a MICROVM region as default without compute access - `WorkerGroupService` blocks triggering runs in MICROVM regions without compute access - `computeTemplateCreation` refactored to use shared `resolveComputeAccess` - Updated snapshot callback schema --- .../src/services/computeSnapshotService.ts | 8 +-- .../presenters/v3/RegionsPresenter.server.ts | 13 +++-- apps/webapp/app/v3/regionAccess.server.ts | 50 +++++++++++++++++++ .../computeTemplateCreation.server.ts | 20 +++----- .../v3/services/setDefaultRegion.server.ts | 21 +++++++- .../worker/workerGroupService.server.ts | 13 +++++ internal-packages/compute/src/types.ts | 24 ++++++--- 7 files changed, 121 insertions(+), 28 deletions(-) create mode 100644 apps/webapp/app/v3/regionAccess.server.ts diff --git a/apps/supervisor/src/services/computeSnapshotService.ts b/apps/supervisor/src/services/computeSnapshotService.ts index 7206f57fb73..041e2902c75 100644 --- a/apps/supervisor/src/services/computeSnapshotService.ts +++ b/apps/supervisor/src/services/computeSnapshotService.ts @@ -80,11 +80,13 @@ export class ComputeSnapshotService { /** Handle the callback from the gateway after a snapshot completes or fails. */ async handleCallback(body: SnapshotCallbackPayload) { + const snapshotId = body.status === "completed" ? body.snapshot_id : undefined; + this.logger.debug("Snapshot callback", { - snapshotId: body.snapshot_id, + snapshotId, instanceId: body.instance_id, status: body.status, - error: body.error, + error: body.status === "failed" ? body.error : undefined, metadata: body.metadata, durationMs: body.duration_ms, }); @@ -97,7 +99,7 @@ export class ComputeSnapshotService { return { ok: false as const, status: 400 }; } - this.#emitSnapshotSpan(runId, body.duration_ms, body.snapshot_id); + this.#emitSnapshotSpan(runId, body.duration_ms, snapshotId); if (body.status === "completed") { const result = await this.workerClient.submitSuspendCompletion({ diff --git a/apps/webapp/app/presenters/v3/RegionsPresenter.server.ts b/apps/webapp/app/presenters/v3/RegionsPresenter.server.ts index f72b8d2fc53..55bd30e33be 100644 --- a/apps/webapp/app/presenters/v3/RegionsPresenter.server.ts +++ b/apps/webapp/app/presenters/v3/RegionsPresenter.server.ts @@ -2,6 +2,7 @@ import { type Project } from "~/models/project.server"; import { type User } from "~/models/user.server"; import { FEATURE_FLAG } from "~/v3/featureFlags"; import { makeFlag } from "~/v3/featureFlags.server"; +import { defaultVisibilityFilter, resolveComputeAccess } from "~/v3/regionAccess.server"; import { BasePresenter } from "./basePresenter.server"; import { getCurrentPlan } from "~/services/platform.v3.server"; @@ -32,6 +33,9 @@ export class RegionsPresenter extends BasePresenter { organizationId: true, defaultWorkerGroupId: true, allowedWorkerQueues: true, + organization: { + select: { featureFlags: true }, + }, }, where: { slug: projectSlug, @@ -58,6 +62,11 @@ export class RegionsPresenter extends BasePresenter { throw new Error("Default worker instance group not found"); } + const hasComputeAccess = await resolveComputeAccess( + this._replica, + project.organization.featureFlags + ); + const visibleRegions = await this._replica.workerInstanceGroup.findMany({ select: { id: true, @@ -75,9 +84,7 @@ export class RegionsPresenter extends BasePresenter { ? { masterQueue: { in: project.allowedWorkerQueues }, } - : { - hidden: false, - }, + : defaultVisibilityFilter(hasComputeAccess), orderBy: { name: "asc", }, diff --git a/apps/webapp/app/v3/regionAccess.server.ts b/apps/webapp/app/v3/regionAccess.server.ts new file mode 100644 index 00000000000..c3e338cb945 --- /dev/null +++ b/apps/webapp/app/v3/regionAccess.server.ts @@ -0,0 +1,50 @@ +import { type Prisma, type WorkloadType } from "@trigger.dev/database"; +import { type PrismaClientOrTransaction } from "~/db.server"; +import { FEATURE_FLAG } from "./featureFlags"; +import { makeFlag } from "./featureFlags.server"; + +/** + * Resolves whether an org has compute access based on feature flags. + */ +export async function resolveComputeAccess( + prisma: PrismaClientOrTransaction, + orgFeatureFlags: unknown +): Promise { + const flag = makeFlag(prisma); + return flag({ + key: FEATURE_FLAG.hasComputeAccess, + defaultValue: false, + overrides: (orgFeatureFlags as Record) ?? {}, + }); +} + +/** + * Builds a visibility filter for non-admin, non-allowlisted users. + * Without compute access, MICROVM regions are excluded entirely. + * With compute access, hidden flag works normally (existing behavior). + */ +export function defaultVisibilityFilter( + hasComputeAccess: boolean +): Prisma.WorkerInstanceGroupWhereInput { + if (hasComputeAccess) { + return { hidden: false }; + } + + return { hidden: false, workloadType: { not: "MICROVM" } }; +} + +/** + * Whether a region is accessible given compute access. + * MICROVM regions require compute access; all other types pass through. + */ +export function isComputeRegionAccessible( + region: { workloadType: WorkloadType }, + hasComputeAccess: boolean +): boolean { + if (region.workloadType !== "MICROVM") { + return true; + } + + // Allow access to any MICROVM region if the org has compute access + return hasComputeAccess; +} diff --git a/apps/webapp/app/v3/services/computeTemplateCreation.server.ts b/apps/webapp/app/v3/services/computeTemplateCreation.server.ts index 4daa2667f25..37235aa1617 100644 --- a/apps/webapp/app/v3/services/computeTemplateCreation.server.ts +++ b/apps/webapp/app/v3/services/computeTemplateCreation.server.ts @@ -3,11 +3,10 @@ import { machinePresetFromName } from "~/v3/machinePresets.server"; import { env } from "~/env.server"; import { logger } from "~/services/logger.server"; import type { PrismaClientOrTransaction } from "~/db.server"; -import { FEATURE_FLAG } from "~/v3/featureFlags"; -import { makeFlag } from "~/v3/featureFlags.server"; import type { AuthenticatedEnvironment } from "~/services/apiAuth.server"; import { ServiceValidationError } from "./baseService.server"; import { FailDeploymentService } from "./failDeployment.server"; +import { resolveComputeAccess } from "../regionAccess.server"; type TemplateCreationMode = "required" | "shadow" | "skip"; @@ -101,9 +100,7 @@ export class ComputeTemplateCreationService { }, }); - throw new ServiceValidationError( - `Compute template creation failed: ${result.error}` - ); + throw new ServiceValidationError(`Compute template creation failed: ${result.error}`); } logger.info("Compute template created", { @@ -132,16 +129,15 @@ export class ComputeTemplateCreationService { }, }); - if (project?.defaultWorkerGroup?.workloadType === "MICROVM") { + if (!project) { + return "skip"; + } + + if (project.defaultWorkerGroup?.workloadType === "MICROVM") { return "required"; } - const flag = makeFlag(prisma); - const hasComputeAccess = await flag({ - key: FEATURE_FLAG.hasComputeAccess, - defaultValue: false, - overrides: (project?.organization?.featureFlags as Record) ?? {}, - }); + const hasComputeAccess = await resolveComputeAccess(prisma, project.organization.featureFlags); if (hasComputeAccess) { return "shadow"; diff --git a/apps/webapp/app/v3/services/setDefaultRegion.server.ts b/apps/webapp/app/v3/services/setDefaultRegion.server.ts index cada8194527..e484b9c4346 100644 --- a/apps/webapp/app/v3/services/setDefaultRegion.server.ts +++ b/apps/webapp/app/v3/services/setDefaultRegion.server.ts @@ -1,3 +1,4 @@ +import { isComputeRegionAccessible, resolveComputeAccess } from "~/v3/regionAccess.server"; import { BaseService, ServiceValidationError } from "./baseService.server"; export class SetDefaultRegionService extends BaseService { @@ -24,6 +25,9 @@ export class SetDefaultRegionService extends BaseService { where: { id: projectId, }, + include: { + organization: { select: { featureFlags: true } }, + }, }); if (!project) { @@ -36,8 +40,21 @@ export class SetDefaultRegionService extends BaseService { if (!project.allowedWorkerQueues.includes(workerGroup.masterQueue)) { throw new ServiceValidationError("You're not allowed to set this region as default"); } - } else if (workerGroup.hidden) { - throw new ServiceValidationError("This region is not available to you"); + } else { + if (workerGroup.hidden) { + throw new ServiceValidationError("This region is not available to you"); + } + + if (workerGroup.workloadType === "MICROVM") { + const hasComputeAccess = await resolveComputeAccess( + this._prisma, + project.organization.featureFlags + ); + + if (!isComputeRegionAccessible(workerGroup, hasComputeAccess)) { + throw new ServiceValidationError("This region requires compute access"); + } + } } } diff --git a/apps/webapp/app/v3/services/worker/workerGroupService.server.ts b/apps/webapp/app/v3/services/worker/workerGroupService.server.ts index 6a900a16fd7..6a2c19cf243 100644 --- a/apps/webapp/app/v3/services/worker/workerGroupService.server.ts +++ b/apps/webapp/app/v3/services/worker/workerGroupService.server.ts @@ -4,6 +4,7 @@ import { WorkerGroupTokenService } from "./workerGroupTokenService.server"; import { logger } from "~/services/logger.server"; import { FEATURE_FLAG } from "~/v3/featureFlags"; import { makeFlag, makeSetFlag } from "~/v3/featureFlags.server"; +import { isComputeRegionAccessible, resolveComputeAccess } from "~/v3/regionAccess.server"; export class WorkerGroupService extends WithRunEngine { private readonly defaultNamePrefix = "worker_group"; @@ -207,6 +208,7 @@ export class WorkerGroupService extends WithRunEngine { }, include: { defaultWorkerGroup: true, + organization: { select: { featureFlags: true } }, }, }); @@ -243,6 +245,17 @@ export class WorkerGroupService extends WithRunEngine { throw new Error(`The region you specified isn't available to you ("${regionOverride}").`); } + if (workerGroup.workloadType === "MICROVM") { + const hasComputeAccess = await resolveComputeAccess( + this._prisma, + project.organization.featureFlags + ); + + if (!isComputeRegionAccessible(workerGroup, hasComputeAccess)) { + throw new Error(`The region you specified isn't available to you ("${regionOverride}").`); + } + } + return workerGroup; } diff --git a/internal-packages/compute/src/types.ts b/internal-packages/compute/src/types.ts index 296e38b59c1..a2aa4c97608 100644 --- a/internal-packages/compute/src/types.ts +++ b/internal-packages/compute/src/types.ts @@ -62,12 +62,20 @@ export const SnapshotRestoreRequestSchema = z.object({ }); export type SnapshotRestoreRequest = z.infer; -export const SnapshotCallbackPayloadSchema = z.object({ - snapshot_id: z.string(), - instance_id: z.string(), - status: z.enum(["completed", "failed"]), - error: z.string().optional(), - metadata: z.record(z.string()).optional(), - duration_ms: z.number().optional(), -}); +export const SnapshotCallbackPayloadSchema = z.discriminatedUnion("status", [ + z.object({ + status: z.literal("completed"), + snapshot_id: z.string(), + instance_id: z.string(), + metadata: z.record(z.string()).optional(), + duration_ms: z.number().optional(), + }), + z.object({ + status: z.literal("failed"), + instance_id: z.string(), + error: z.string().optional(), + metadata: z.record(z.string()).optional(), + duration_ms: z.number().optional(), + }), +]); export type SnapshotCallbackPayload = z.infer; From 3c9647cb8c522ea3ca3f664295a7caee6c70fee5 Mon Sep 17 00:00:00 2001 From: Oskar Otwinowski Date: Mon, 13 Apr 2026 11:26:15 +0100 Subject: [PATCH 027/243] feat(webapp): Platform notifications admin imporovements (#3324) - bugfix to show the changelog to the target audience - more functionality for admins, to edit, delete and archive notifications --- .../navigation/HelpAndFeedbackPopover.tsx | 6 +- .../OrganizationSettingsSideMenu.tsx | 2 +- .../app/components/navigation/SideMenu.tsx | 6 +- .../webapp/app/routes/admin.notifications.tsx | 1201 +++++++++++------ .../routes/resources.platform-changelogs.tsx | 33 +- .../resources.platform-notifications.tsx | 11 +- .../services/platformNotifications.server.ts | 176 ++- 7 files changed, 979 insertions(+), 456 deletions(-) diff --git a/apps/webapp/app/components/navigation/HelpAndFeedbackPopover.tsx b/apps/webapp/app/components/navigation/HelpAndFeedbackPopover.tsx index 39a3d386783..c205364ecbd 100644 --- a/apps/webapp/app/components/navigation/HelpAndFeedbackPopover.tsx +++ b/apps/webapp/app/components/navigation/HelpAndFeedbackPopover.tsx @@ -33,13 +33,17 @@ import { Badge } from "../primitives/Badge"; export function HelpAndFeedback({ disableShortcut = false, isCollapsed = false, + organizationId, + projectId, }: { disableShortcut?: boolean; isCollapsed?: boolean; + organizationId?: string; + projectId?: string; }) { const [isHelpMenuOpen, setHelpMenuOpen] = useState(false); const currentPlan = useCurrentPlan(); - const { changelogs } = useRecentChangelogs(); + const { changelogs } = useRecentChangelogs(organizationId, projectId); useShortcutKeys({ shortcut: disableShortcut ? undefined : { key: "h", enabledOnInputElements: false }, diff --git a/apps/webapp/app/components/navigation/OrganizationSettingsSideMenu.tsx b/apps/webapp/app/components/navigation/OrganizationSettingsSideMenu.tsx index e274ad20f43..c8cd131d962 100644 --- a/apps/webapp/app/components/navigation/OrganizationSettingsSideMenu.tsx +++ b/apps/webapp/app/components/navigation/OrganizationSettingsSideMenu.tsx @@ -205,7 +205,7 @@ export function OrganizationSettingsSideMenu({ )}
- +
diff --git a/apps/webapp/app/components/navigation/SideMenu.tsx b/apps/webapp/app/components/navigation/SideMenu.tsx index 0693a2418b1..dbc4c213f08 100644 --- a/apps/webapp/app/components/navigation/SideMenu.tsx +++ b/apps/webapp/app/components/navigation/SideMenu.tsx @@ -713,7 +713,7 @@ export function SideMenu({ isCollapsed && "items-center" )} > - + {isFreeUser && (
- +
diff --git a/apps/webapp/app/routes/admin.notifications.tsx b/apps/webapp/app/routes/admin.notifications.tsx index d9b06816953..179ab23c3ee 100644 --- a/apps/webapp/app/routes/admin.notifications.tsx +++ b/apps/webapp/app/routes/admin.notifications.tsx @@ -1,11 +1,21 @@ -import { ChevronRightIcon, XMarkIcon } from "@heroicons/react/20/solid"; +import { ChevronRightIcon, TrashIcon, XMarkIcon } from "@heroicons/react/20/solid"; import { useFetcher, useSearchParams } from "@remix-run/react"; import type { ActionFunctionArgs, LoaderFunctionArgs } from "@remix-run/server-runtime"; import { redirect } from "@remix-run/server-runtime"; -import { useRef, useState, useLayoutEffect } from "react"; +import { useEffect, useRef, useState, useLayoutEffect } from "react"; import ReactMarkdown from "react-markdown"; import { typedjson, useTypedLoaderData } from "remix-typedjson"; import { z } from "zod"; +import { + Alert, + AlertCancel, + AlertContent, + AlertDescription, + AlertFooter, + AlertHeader, + AlertTitle, + AlertTrigger, +} from "~/components/primitives/Alert"; import { Button } from "~/components/primitives/Buttons"; import { Dialog, @@ -29,8 +39,12 @@ import { import { prisma } from "~/db.server"; import { requireUserId } from "~/services/session.server"; import { + archivePlatformNotification, createPlatformNotification, + deletePlatformNotification, getAdminNotificationsList, + publishNowPlatformNotification, + updatePlatformNotification, } from "~/services/platformNotifications.server"; import { createSearchParams } from "~/utils/searchParams"; import { cn } from "~/utils/cn"; @@ -42,7 +56,7 @@ const CLI_TYPES = ["info", "warn", "error", "success"] as const; const SearchParams = z.object({ page: z.coerce.number().optional(), - hideArchived: z.coerce.boolean().optional(), + hideInactive: z.coerce.boolean().optional(), }); export const loader = async ({ request }: LoaderFunctionArgs) => { @@ -52,10 +66,10 @@ export const loader = async ({ request }: LoaderFunctionArgs) => { const searchParams = createSearchParams(request.url, SearchParams); if (!searchParams.success) throw new Error(searchParams.error); - const { page: rawPage, hideArchived } = searchParams.params.getAll(); + const { page: rawPage, hideInactive } = searchParams.params.getAll(); const page = rawPage ?? 1; - const data = await getAdminNotificationsList({ page, pageSize: PAGE_SIZE, hideArchived: hideArchived ?? false }); + const data = await getAdminNotificationsList({ page, pageSize: PAGE_SIZE, hideInactive: hideInactive ?? false }); return typedjson({ ...data, userId }); }; @@ -76,10 +90,22 @@ export async function action({ request }: ActionFunctionArgs) { return handleArchiveAction(formData); } + if (_action === "delete") { + return handleDeleteAction(formData); + } + + if (_action === "publish-now") { + return handlePublishNowAction(formData); + } + + if (_action === "edit") { + return handleEditAction(formData); + } + return typedjson({ error: "Unknown action" }, { status: 400 }); } -async function handleCreateAction(formData: FormData, userId: string, isPreview: boolean) { +function parseNotificationFormData(formData: FormData) { const surface = formData.get("surface") as string; const payloadType = formData.get("payloadType") as string; const adminLabel = formData.get("adminLabel") as string; @@ -91,10 +117,10 @@ async function handleCreateAction(formData: FormData, userId: string, isPreview: const startsAt = formData.get("startsAt") as string; const endsAt = formData.get("endsAt") as string; const priority = Number(formData.get("priority") || "0"); - - if (!adminLabel || !title || !description || !endsAt || !surface || !payloadType) { - return typedjson({ error: "Missing required fields" }, { status: 400 }); - } + const scope = (formData.get("scope") as string) || "GLOBAL"; + const scopeUserId = (formData.get("scopeUserId") as string) || undefined; + const scopeOrganizationId = (formData.get("scopeOrganizationId") as string) || undefined; + const scopeProjectId = (formData.get("scopeProjectId") as string) || undefined; const cliMaxShowCount = formData.get("cliMaxShowCount") ? Number(formData.get("cliMaxShowCount")) @@ -123,39 +149,79 @@ async function handleCreateAction(formData: FormData, userId: string, isPreview: } : undefined; - const result = await createPlatformNotification({ - title: isPreview ? `[Preview] ${adminLabel}` : adminLabel, - payload: { - version: "1" as const, - data: { - type: payloadType as "info" | "warn" | "error" | "success" | "card" | "changelog", - title, - description, - ...(actionUrl ? { actionUrl } : {}), - ...(image ? { image } : {}), - ...(dismissOnAction ? { dismissOnAction: true } : {}), - ...(discovery ? { discovery } : {}), - }, + return { + surface, + payloadType, + adminLabel, + title, + description, + actionUrl, + image, + dismissOnAction, + startsAt, + endsAt, + priority, + scope, + scopeUserId, + scopeOrganizationId, + scopeProjectId, + cliMaxShowCount, + cliMaxDaysAfterFirstSeen, + cliShowEvery, + discovery, + }; +} + +function buildPayloadInput(fields: ReturnType) { + return { + version: "1" as const, + data: { + type: fields.payloadType as "info" | "warn" | "error" | "success" | "card" | "changelog", + title: fields.title, + description: fields.description, + ...(fields.actionUrl ? { actionUrl: fields.actionUrl } : {}), + ...(fields.image ? { image: fields.image } : {}), + ...(fields.dismissOnAction ? { dismissOnAction: true } : {}), + ...(fields.discovery ? { discovery: fields.discovery } : {}), }, - surface: surface as "CLI" | "WEBAPP", - scope: isPreview ? "USER" : "GLOBAL", - ...(isPreview ? { userId } : {}), + }; +} + +async function handleCreateAction(formData: FormData, userId: string, isPreview: boolean) { + const fields = parseNotificationFormData(formData); + + if (!fields.adminLabel || !fields.title || !fields.description || !fields.endsAt || !fields.surface || !fields.payloadType) { + return typedjson({ error: "Missing required fields" }, { status: 400 }); + } + + const result = await createPlatformNotification({ + title: isPreview ? `[Preview] ${fields.adminLabel}` : fields.adminLabel, + payload: buildPayloadInput(fields), + surface: fields.surface as "CLI" | "WEBAPP", + scope: isPreview ? "USER" : (fields.scope as "USER" | "PROJECT" | "ORGANIZATION" | "GLOBAL"), + ...(isPreview + ? { userId } + : { + ...(fields.scope === "USER" && fields.scopeUserId ? { userId: fields.scopeUserId } : {}), + ...(fields.scope === "ORGANIZATION" && fields.scopeOrganizationId ? { organizationId: fields.scopeOrganizationId } : {}), + ...(fields.scope === "PROJECT" && fields.scopeProjectId ? { projectId: fields.scopeProjectId } : {}), + }), startsAt: isPreview ? new Date().toISOString() - : startsAt - ? new Date(startsAt + "Z").toISOString() + : fields.startsAt + ? new Date(fields.startsAt + "Z").toISOString() : new Date().toISOString(), endsAt: isPreview ? new Date(Date.now() + 60 * 60 * 1000).toISOString() - : new Date(endsAt + "Z").toISOString(), - priority, - ...(surface === "CLI" + : new Date(fields.endsAt + "Z").toISOString(), + priority: fields.priority, + ...(fields.surface === "CLI" ? isPreview ? { cliMaxShowCount: 1 } : { - cliMaxShowCount, - cliMaxDaysAfterFirstSeen, - cliShowEvery, + cliMaxShowCount: fields.cliMaxShowCount, + cliMaxDaysAfterFirstSeen: fields.cliMaxDaysAfterFirstSeen, + cliShowEvery: fields.cliShowEvery, } : {}), }); @@ -183,53 +249,89 @@ async function handleArchiveAction(formData: FormData) { return typedjson({ error: "Missing notificationId" }, { status: 400 }); } - await prisma.platformNotification.update({ - where: { id: notificationId }, - data: { archivedAt: new Date() }, - }); + await archivePlatformNotification(notificationId); + return typedjson({ success: true }); +} + +async function handleDeleteAction(formData: FormData) { + const notificationId = formData.get("notificationId") as string; + if (!notificationId) { + return typedjson({ error: "Missing notificationId" }, { status: 400 }); + } + await deletePlatformNotification(notificationId); return typedjson({ success: true }); } +async function handlePublishNowAction(formData: FormData) { + const notificationId = formData.get("notificationId") as string; + if (!notificationId) { + return typedjson({ error: "Missing notificationId" }, { status: 400 }); + } + + await publishNowPlatformNotification(notificationId); + return typedjson({ success: true }); +} + +async function handleEditAction(formData: FormData) { + const notificationId = formData.get("notificationId") as string; + const fields = parseNotificationFormData(formData); + + if (!notificationId || !fields.adminLabel || !fields.title || !fields.description || !fields.endsAt || !fields.surface || !fields.payloadType || !fields.startsAt) { + return typedjson({ error: "Missing required fields" }, { status: 400 }); + } + + const result = await updatePlatformNotification({ + id: notificationId, + title: fields.adminLabel, + payload: buildPayloadInput(fields), + surface: fields.surface as "CLI" | "WEBAPP", + scope: fields.scope as "USER" | "PROJECT" | "ORGANIZATION" | "GLOBAL", + ...(fields.scope === "USER" && fields.scopeUserId ? { userId: fields.scopeUserId } : {}), + ...(fields.scope === "ORGANIZATION" && fields.scopeOrganizationId ? { organizationId: fields.scopeOrganizationId } : {}), + ...(fields.scope === "PROJECT" && fields.scopeProjectId ? { projectId: fields.scopeProjectId } : {}), + startsAt: new Date(fields.startsAt + "Z").toISOString(), + endsAt: new Date(fields.endsAt + "Z").toISOString(), + priority: fields.priority, + ...(fields.surface === "CLI" + ? { + cliMaxShowCount: fields.cliMaxShowCount, + cliMaxDaysAfterFirstSeen: fields.cliMaxDaysAfterFirstSeen, + cliShowEvery: fields.cliShowEvery, + } + : {}), + }); + + if (result.isErr()) { + const err = result.error; + if (err.type === "validation") { + return typedjson( + { error: err.issues.map((i) => `${i.path.join(".")}: ${i.message}`).join("; ") }, + { status: 400 } + ); + } + return typedjson({ error: err.message }, { status: 500 }); + } + + return typedjson({ success: true, id: result.value.id }); +} + export default function AdminNotificationsRoute() { const { notifications, total, page, pageCount } = useTypedLoaderData(); const [showCreate, setShowCreate] = useState(false); - const createFetcher = useFetcher<{ - success?: boolean; - error?: string; - id?: string; - previewId?: string; - }>(); - const archiveFetcher = useFetcher<{ success?: boolean; error?: string }>(); - const [surface, setSurface] = useState<"CLI" | "WEBAPP">("WEBAPP"); - const [title, setTitle] = useState(""); - const [description, setDescription] = useState(""); - const [actionUrl, setActionUrl] = useState(""); - const [image, setImage] = useState(""); - const [payloadType, setPayloadType] = useState("card"); const [detailNotification, setDetailNotification] = useState<(typeof notifications)[number] | null>(null); - - const typeOptions = surface === "WEBAPP" ? WEBAPP_TYPES : CLI_TYPES; - - // Reset type when surface changes if current type isn't valid for new surface - const handleSurfaceChange = (newSurface: "CLI" | "WEBAPP") => { - setSurface(newSurface); - const newTypes = newSurface === "WEBAPP" ? WEBAPP_TYPES : CLI_TYPES; - if (!newTypes.includes(payloadType as any)) { - setPayloadType(newTypes[0]); - } - }; + const [editNotification, setEditNotification] = useState<(typeof notifications)[number] | null>(null); const [urlSearchParams, setUrlSearchParams] = useSearchParams(); - const hideArchived = urlSearchParams.get("hideArchived") === "true"; + const hideInactive = urlSearchParams.get("hideInactive") === "true"; - const toggleHideArchived = () => { + const toggleHideInactive = () => { setUrlSearchParams((prev) => { const next = new URLSearchParams(prev); - if (hideArchived) { - next.delete("hideArchived"); + if (hideInactive) { + next.delete("hideInactive"); } else { - next.set("hideArchived", "true"); + next.set("hideInactive", "true"); } next.delete("page"); return next; @@ -240,341 +342,26 @@ export default function AdminNotificationsRoute() {
-
- {showCreate && ( -
- - -
-
- - -
- -
- - -
- -
- - -
- -
- - -
-
- - {/* CLI live preview */} - {surface === "CLI" && (title || description) && ( -
-

- CLI Preview -

-
- {title && ( -

- -

- )} - {description && ( -

- -

- )} - {actionUrl && ( -

{actionUrl}

- )} -
-
- )} - -
- - setTitle(e.target.value)} - /> -
- - {/* Description + live preview (webapp only) */} -
- - {surface === "WEBAPP" ? ( -
-