From 9ec0c01dc96d9a204bc266da6a51db6f9ef8b763 Mon Sep 17 00:00:00 2001 From: Chris Arderne Date: Wed, 19 Aug 2026 11:19:14 +0100 Subject: [PATCH 1/2] perf(webapp): stabilize nested component identities --- .oxlintrc.json | 1 + .../app/components/code/TSQLResultsTable.tsx | 24 +++++--- .../errors/ConfigureErrorAlerts.tsx | 23 +++++--- .../primitives/charts/ChartLine.tsx | 21 ++++--- .../components/runs/v3/ReplayRunDialog.tsx | 25 +++++--- .../route.tsx | 14 +++-- .../route.tsx | 57 ++++++++++++------- .../route.tsx | 20 ++++--- .../app/routes/account._index/route.tsx | 16 ++++-- .../app/routes/confirm-basic-details.tsx | 6 +- ...cts.$projectParam.env.$envParam.vercel.tsx | 16 ++++-- ...ces.orgs.$organizationSlug.select-plan.tsx | 10 +++- 12 files changed, 147 insertions(+), 86 deletions(-) diff --git a/.oxlintrc.json b/.oxlintrc.json index aa066dc958f..8d8de0274f2 100644 --- a/.oxlintrc.json +++ b/.oxlintrc.json @@ -115,6 +115,7 @@ { "files": ["apps/webapp/app/**/*.ts", "apps/webapp/app/**/*.tsx"], "rules": { + "react/no-unstable-nested-components": "error", "react/rules-of-hooks": "error", "trigger-runops/no-control-plane-run-graph-access": "error", "trigger-runops/no-control-plane-in-runops-slot": "error" diff --git a/apps/webapp/app/components/code/TSQLResultsTable.tsx b/apps/webapp/app/components/code/TSQLResultsTable.tsx index 87d2003d385..8556d1ff24d 100644 --- a/apps/webapp/app/components/code/TSQLResultsTable.tsx +++ b/apps/webapp/app/components/code/TSQLResultsTable.tsx @@ -241,6 +241,7 @@ const DebouncedInput = forwardRef< interface ColumnMeta { outputColumn: OutputColumnMetadata; alignment: "left" | "right"; + prettyFormatting: boolean; } /** @@ -489,6 +490,19 @@ function CellValueWrapper({ /** * Render a cell value based on its type and optional customRenderType */ +function TSQLResultsCell(info: CellContext) { + const meta = info.column.columnDef.meta as ColumnMeta; + + return ( + + ); +} + function CellValue({ value, column, @@ -1053,17 +1067,11 @@ export const TSQLResultsTable = memo(function TSQLResultsTable({ id: col.name, accessorKey: col.name, header: () => col.name, - cell: (info: CellContext) => ( - - ), + cell: TSQLResultsCell, meta: { outputColumn: col, alignment: isRightAlignedColumn(col) ? "right" : "left", + prettyFormatting, } as ColumnMeta, size: calculateColumnWidth(col.name, rows, col), filterFn: fuzzyFilter, diff --git a/apps/webapp/app/components/errors/ConfigureErrorAlerts.tsx b/apps/webapp/app/components/errors/ConfigureErrorAlerts.tsx index b126e77b34b..08cbcb55ad9 100644 --- a/apps/webapp/app/components/errors/ConfigureErrorAlerts.tsx +++ b/apps/webapp/app/components/errors/ConfigureErrorAlerts.tsx @@ -42,6 +42,19 @@ export const ErrorAlertsFormSchema = z.object({ }, z.string().url().array()), }); +type SlackChannel = { id?: string; name?: string; is_private?: boolean }; + +function renderSlackChannel(channels: SlackChannel[], value: string) { + const channel = channels.find((channel) => value === `${channel.id}/${channel.name}`); + if (!channel) return; + + return ( + + + + ); +} + type ConfigureErrorAlertsProps = ErrorAlertChannelData & { connectToSlackHref?: string; formAction: string; @@ -196,15 +209,7 @@ export function ConfigureErrorAlerts({ filter={(channel, search) => channel.name?.toLowerCase().includes(search.toLowerCase()) ?? false } - text={(value) => { - const channel = slack.channels.find((s) => value === `${s.id}/${s.name}`); - if (!channel) return; - return ( - - - - ); - }} + text={(value) => renderSlackChannel(slack.channels, value)} > {(matches) => ( <> diff --git a/apps/webapp/app/components/primitives/charts/ChartLine.tsx b/apps/webapp/app/components/primitives/charts/ChartLine.tsx index 000d74b91c0..29cbe9ab343 100644 --- a/apps/webapp/app/components/primitives/charts/ChartLine.tsx +++ b/apps/webapp/app/components/primitives/charts/ChartLine.tsx @@ -577,17 +577,16 @@ export function ChartLineRenderer({ // flips at the split; otherwise it's the series colour. The warning overlay draws its // own dot on top where it's active. activeDot={ - gradientLine - ? (props: ActiveDotProps) => ( - - ) - : { r: 4, fill: config[key]?.color, strokeWidth: 0 } + gradientLine ? ( + + ) : ( + { r: 4, fill: config[key]?.color, strokeWidth: 0 } + ) } isAnimationActive={false} /> diff --git a/apps/webapp/app/components/runs/v3/ReplayRunDialog.tsx b/apps/webapp/app/components/runs/v3/ReplayRunDialog.tsx index 86747f9e277..9e22052bbb6 100644 --- a/apps/webapp/app/components/runs/v3/ReplayRunDialog.tsx +++ b/apps/webapp/app/components/runs/v3/ReplayRunDialog.tsx @@ -124,6 +124,22 @@ function ReplayContent({ runFriendlyId, failedRedirect }: ReplayRunDialogProps) const startingJson = "{\n\n}"; const machinePresets = Object.values(MachinePresetName.enum); +type ReplayEnvironment = UseDataFunctionReturn["environments"][number]; + +function renderReplayEnvironment( + environments: ReplayEnvironment[], + value: string +): React.ReactNode { + const environment = environments.find((environment) => environment.id === value); + if (!environment) return; + + return ( +
+ +
+ ); +} + function ReplayForm({ failedRedirect, runFriendlyId, @@ -572,14 +588,7 @@ function ReplayForm({ (item) => item.branchName?.replace(/\//g, " ").replace(/_/g, " ") ?? "", ], }} - text={(value) => { - const env = replayData.environments.find((env) => env.id === value)!; - return ( -
- -
- ); - }} + text={(value) => renderReplayEnvironment(replayData.environments, value)} > {(matches) => matches.map((env) => ( diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.alerts.new/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.alerts.new/route.tsx index 41dd31c5d17..aaf50f69d2f 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.alerts.new/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.alerts.new/route.tsx @@ -48,6 +48,14 @@ import { } from "~/v3/services/alerts/safeWebhookUrl.server"; import { pageMeta } from "~/utils/pageTitle"; +type SlackChannel = { id?: string; name?: string; is_private?: boolean }; + +function renderSlackChannel(channels: SlackChannel[], value: string | string[]) { + if (typeof value !== "string") return; + const channel = channels.find((channel) => value === `${channel.id}/${channel.name}`); + return channel ? : undefined; +} + export const meta = pageMeta("New alert"); const FormSchema = z @@ -342,11 +350,7 @@ export default function Page() { filter={(channel, search) => channel.name?.toLowerCase().includes(search.toLowerCase()) ?? false } - text={(value) => { - const channel = slack.channels.find((s) => value === `${s.id}/${s.name}`); - if (!channel) return; - return ; - }} + text={(value) => renderSlackChannel(slack.channels, value)} > {(matches) => ( <> diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.errors._index/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.errors._index/route.tsx index 3c40957ea69..1825aeb7fe1 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.errors._index/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.errors._index/route.tsx @@ -4,7 +4,16 @@ import { Form, useFetcher, useRevalidator } from "@remix-run/react"; import { type LoaderFunctionArgs } from "@remix-run/server-runtime"; import { ErrorId } from "@trigger.dev/core/v3/isomorphic"; import { type ErrorGroupStatus } from "@trigger.dev/database"; -import { Suspense, useCallback, useEffect, useMemo, useRef, useState, type ReactNode } from "react"; +import { + Suspense, + useCallback, + useEffect, + useMemo, + useRef, + useState, + type ComponentProps, + type ReactNode, +} from "react"; import { Bar, BarChart, @@ -628,6 +637,17 @@ function ErrorGroupRow({ ); } +function renderErrorActionsPopoverContent(props: ComponentProps) { + return ( + <> + +
+ +
+ + ); +} + function ErrorActionsCell({ errorGroup, organizationSlug, @@ -664,26 +684,21 @@ function ErrorActionsCell({ <> ( - <> - -
- { - close(); - pendingToast.current = statusActionToastMessage(data); - fetcher.submit(data, { method: "post", action: actionUrl }); - }} - onCustomIgnore={() => { - close(); - setCustomIgnoreOpen(true); - }} - /> -
- - )} + popoverContent={(close) => + renderErrorActionsPopoverContent({ + status: errorGroup.status, + taskIdentifier: errorGroup.taskIdentifier, + onAction: (data) => { + close(); + pendingToast.current = statusActionToastMessage(data); + fetcher.submit(data, { method: "post", action: actionUrl }); + }, + onCustomIgnore: () => { + close(); + setCustomIgnoreOpen(true); + }, + }) + } /> (arr: T[]): T[] { return shuffled; } +function renderMultiSelectValue(value: string[]) { + if (value.length === 0) return; + + return ( + + {value.slice(0, 2).join(", ")} + {value.length > 2 && +{value.length - 2} more} + + ); +} + function MultiSelectField({ value, setValue, @@ -97,14 +108,7 @@ function MultiSelectField({ icon={icon} items={items} className="h-8 min-w-0 border-0 bg-background-hover pl-2 text-sm text-text-dimmed ring-border-bright transition hover:bg-secondary hover:text-text-dimmed hover:ring-1" - text={(v) => - v.length === 0 ? undefined : ( - - {v.slice(0, 2).join(", ")} - {v.length > 2 && +{v.length - 2} more} - - ) - } + text={renderMultiSelectValue} > {(items) => items.map((item) => ( diff --git a/apps/webapp/app/routes/account._index/route.tsx b/apps/webapp/app/routes/account._index/route.tsx index 39dbf4b4e6e..2021eafe51d 100644 --- a/apps/webapp/app/routes/account._index/route.tsx +++ b/apps/webapp/app/routes/account._index/route.tsx @@ -70,6 +70,15 @@ function themeIcon(value: ThemePreference) { } } +function renderTheme(value: ThemePreference) { + return ( + + {themeIcon(value)} + {themeLabel(value)} + + ); +} + export const meta = pageMeta("Your profile"); function createSchema( @@ -320,12 +329,7 @@ export default function Page() { variant="secondary/small" dropdownIcon items={["classic", "system", "dark", "light"]} - text={(value) => ( - - {themeIcon(value)} - {themeLabel(value)} - - )} + text={renderTheme} className="w-44" > {(items) => diff --git a/apps/webapp/app/routes/confirm-basic-details.tsx b/apps/webapp/app/routes/confirm-basic-details.tsx index 6af3da7bd7b..37c54d4ea0b 100644 --- a/apps/webapp/app/routes/confirm-basic-details.tsx +++ b/apps/webapp/app/routes/confirm-basic-details.tsx @@ -207,6 +207,10 @@ const HandIcon = forwardRef(({}, ref) => { }); const MotionHand = motion(HandIcon); +function renderRole(value: string) { + return value ? {value} : undefined; +} + export default function Page() { const user = useUser(); const lastSubmission = useActionData(); @@ -390,7 +394,7 @@ export default function Page() { icon={} items={shuffledRoles} className="h-8 min-w-0 border-0 bg-background-hover pl-2 text-sm text-text-dimmed ring-border-bright transition hover:bg-secondary hover:text-text-dimmed hover:ring-1" - text={(v) => (v ? {v} : undefined)} + text={renderRole} > {(items) => items.map((item) => ( diff --git a/apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.vercel.tsx b/apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.vercel.tsx index 792275a917d..70e727916b6 100644 --- a/apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.vercel.tsx +++ b/apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.vercel.tsx @@ -541,6 +541,14 @@ function VercelAppInstalledRow() { ); } +function VercelLeadingIcon() { + return ; +} + +function VercelLoadingIcon() { + return ; +} + function VercelSettingsRows({ organizationSlug, projectSlug, @@ -577,7 +585,7 @@ function VercelSettingsRows({ noPermissionTooltip={noPermissionTooltip} to={vercelAppInstallPath(organizationSlug, projectSlug)} variant="secondary/small" - LeadingIcon={() => } + LeadingIcon={VercelLeadingIcon} > Install Vercel app @@ -595,11 +603,7 @@ function VercelSettingsRows({ onClick={() => onOpenModal?.()} disabled={isLoadingProjects || !onOpenModal || !canManageVercel} tooltip={canManageVercel ? undefined : noPermissionTooltip} - LeadingIcon={ - isLoadingProjects - ? () => - : () => - } + LeadingIcon={isLoadingProjects ? VercelLoadingIcon : VercelLeadingIcon} > {isLoadingProjects ? "Loading projects…" : "Connect Vercel project"} diff --git a/apps/webapp/app/routes/resources.orgs.$organizationSlug.select-plan.tsx b/apps/webapp/app/routes/resources.orgs.$organizationSlug.select-plan.tsx index cab7b8b41b2..80635361216 100644 --- a/apps/webapp/app/routes/resources.orgs.$organizationSlug.select-plan.tsx +++ b/apps/webapp/app/routes/resources.orgs.$organizationSlug.select-plan.tsx @@ -43,6 +43,10 @@ import { sendToPlain } from "~/utils/plain.server"; import { formatCurrency } from "~/utils/numberFormatter"; import { EnvironmentLabel } from "~/components/environments/EnvironmentLabel"; +function WhiteSpinnerIcon() { + return ; +} + const Params = z.object({ organizationSlug: z.string(), }); @@ -399,7 +403,7 @@ export function TierFree({