diff --git a/frontend/AGENTS.md b/frontend/AGENTS.md new file mode 120000 index 0000000000..681311eb9c --- /dev/null +++ b/frontend/AGENTS.md @@ -0,0 +1 @@ +CLAUDE.md \ No newline at end of file diff --git a/frontend/CLAUDE.md b/frontend/CLAUDE.md new file mode 100644 index 0000000000..c65e8e07ea --- /dev/null +++ b/frontend/CLAUDE.md @@ -0,0 +1,3 @@ +# Frontend + +- When building new UI components, ship a Ladle story alongside the component (`*.stories.tsx` next to the source). Cover the meaningful states (empty, loading, error, success, edge cases) so the component can be reviewed and iterated on in isolation without booting the whole dashboard. Run with `pnpm dev:ladle` from `frontend/`. Existing example: [src/app/runner-pool-error-popover.stories.tsx](src/app/runner-pool-error-popover.stories.tsx). diff --git a/frontend/src/app/forms/edit-shared-runner-config-form.tsx b/frontend/src/app/forms/edit-shared-runner-config-form.tsx index 7e2d0c5be2..9e30d2e6ab 100644 --- a/frontend/src/app/forms/edit-shared-runner-config-form.tsx +++ b/frontend/src/app/forms/edit-shared-runner-config-form.tsx @@ -465,7 +465,7 @@ export const MaxConcurrentActors = < /> - Maximum actors allowed to run concurrently per runner. + Maximum actors allowed to run concurrently in total. Leave blank for unlimited. diff --git a/frontend/src/app/layout.tsx b/frontend/src/app/layout.tsx index d5ee2c2482..5b06f82134 100644 --- a/frontend/src/app/layout.tsx +++ b/frontend/src/app/layout.tsx @@ -12,7 +12,7 @@ import { faWallet, Icon, } from "@rivet-gg/icons"; -import { useSuspenseQuery } from "@tanstack/react-query"; +import { useInfiniteQuery, useSuspenseQuery } from "@tanstack/react-query"; import { Link, useMatch, @@ -46,9 +46,11 @@ import { Skeleton, } from "@/components"; import { + ActorRegion, useCloudNamespaceDataProvider, useDataProvider, useDataProviderCheck, + useEngineCompatDataProvider, } from "@/components/actors"; import { useRootLayoutOptional } from "@/components/actors/root-layout-context"; import type { HeaderLinkProps } from "@/components/header/header-link"; @@ -56,7 +58,9 @@ import { authClient } from "@/lib/auth"; import { features } from "@/lib/features"; import { ensureTrailingSlash } from "@/lib/utils"; import { TEST_IDS } from "@/utils/test-ids"; +import type { RivetActorError } from "@/queries/types"; import { ActorBuildsList } from "./actor-builds-list"; +import { RunnerPoolErrorPopover } from "./runner-pool-error-popover"; import { BillingLimitAlert } from "./billing/billing-limit-alert"; import { BillingPlanBadge } from "./billing/billing-plan-badge"; import { BillingUsageGauge } from "./billing/billing-usage-gauge"; @@ -554,8 +558,11 @@ function HeaderLink({ icon, children, className, ...props }: HeaderLinkProps) { = {}; + for (const page of data.pages) { + for (const config of Object.values(page.runnerConfigs)) { + for (const [dc, dcConfig] of Object.entries( + config.datacenters, + )) { + if (dcConfig.runnerPoolError && !map[dc]) { + map[dc] = dcConfig.runnerPoolError; + } + } + } + } + return Object.keys(map).length > 0 ? map : null; + }, + }); + + if (!errors) return null; + + return ( + ( + + )} + /> + ); +} + function HeaderButton({ children, className, ...props }: ButtonProps) { return ( + + + + +
+ {summary} + + Click to view details + +
+
+
+ + ) : ( + + + + )} + e.preventDefault()} + > + { + setOpen(false); + onEditConfig(); + } + : undefined + } + /> + + + ); +} + +function ErrorPopoverBody({ + groups, + renderRegion, + onEditConfig, +}: { + groups: ErrorGroup[]; + renderRegion: (regionId: string) => ReactNode; + onEditConfig?: () => void; +}) { + const [activeFingerprint, setActiveFingerprint] = useState( + groups[0].classified.fingerprint, + ); + + const showTabs = groups.length > 1; + + return ( +
+
+
+
+ Runner pool errors +
+
+ {groups.length === 1 + ? `${formatRegionCount(groups[0].regions.length)} affected` + : `${groups.length} distinct errors across ${formatRegionCount( + groups.reduce( + (sum, g) => sum + g.regions.length, + 0, + ), + )}`} +
+
+
+ + {showTabs ? ( + + + {groups.map((g) => ( + + + + {g.classified.title} + + + {g.regions.length} + + + ))} + + {groups.map((g) => ( + + + + ))} + + ) : ( + + )} + + {onEditConfig ? ( +
+ +
+ ) : null} +
+ ); +} + +function GroupBody({ + group, + renderRegion, +}: { + group: ErrorGroup; + renderRegion: (regionId: string) => ReactNode; +}) { + const { classified, regions } = group; + + return ( +
+
+ + {regions.length === 1 ? "Region:" : "Regions:"} + + {regions.map((r) => ( + + {renderRegion(r)} + + ))} +
+ + {classified.body ? ( + + ) : ( +

+ {describeKind(classified.kind)} +

+ )} +
+ ); +} + +function ErrorBody({ body, label }: { body: string; label: string }) { + const [copied, setCopied] = useState(false); + const formatted = useMemo(() => formatBody(body), [body]); + + const handleCopied = () => { + setCopied(true); + setTimeout(() => setCopied(false), 1500); + }; + + return ( +
+
+ + {label} + + + + +
+ + {formatted.language === "json" ? ( + + ) : ( + + {formatted.code} + + )} + +
+ ); +} + +function formatBody(body: string): { code: string; language: "json" | "text" } { + const trimmed = body.trim(); + if ( + (trimmed.startsWith("{") && trimmed.endsWith("}")) || + (trimmed.startsWith("[") && trimmed.endsWith("]")) + ) { + try { + return { + code: JSON.stringify(JSON.parse(trimmed), null, 2), + language: "json", + }; + } catch { + // Fall through. + } + } + return { code: body, language: "text" }; +} + +function SeverityDot({ severity }: { severity: Severity }) { + return ( + + ); +} + +function formatRegionCount(n: number) { + return n === 1 ? "1 region" : `${n} regions`; +} + +function describeKind(kind: ClassifiedError["kind"]): string { + switch (kind) { + case "downgrade": + return "Runner pool was downgraded to an unsupported version. Revert to a higher version."; + case "serverless_stream_ended_early": + return "Connection terminated before the runner stopped. Check the request lifespan limits on your serverless provider."; + case "internal": + return "An internal error occurred in the runner pool."; + default: + return "Unknown error."; + } +} diff --git a/frontend/src/app/runners-table.tsx b/frontend/src/app/runners-table.tsx index dc3e34136d..3601c5a2f5 100644 --- a/frontend/src/app/runners-table.tsx +++ b/frontend/src/app/runners-table.tsx @@ -56,7 +56,7 @@ export function RunnersTable({ ID Name Datacenter - Slots + Actors Version Created