From 725a79a1d80c102cece519bb5c294c6d423326dc Mon Sep 17 00:00:00 2001 From: Amp Date: Mon, 7 Sep 2026 06:20:50 +0000 Subject: [PATCH] =?UTF-8?q?fix(frontend):=20dashboard=20audit=20polish=20?= =?UTF-8?q?=E2=80=94=20inspector=20defaults,=20empty=20states,=20OSS=20the?= =?UTF-8?q?me=20toggle?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Default actor tab is State (Workflow for workflows); Database only leads for raw-SQLite actors. The dashboard now canonicalizes the tab order advertised by the runner's inspector bundle so older runners get the same order. - Database tab: user tables first, _rivet_* tables grouped under "Internal", and the first user table is selected by default. - Connections tab: table (ID, type, hibernatable, params, state) with a proper empty state instead of a raw object tree; Queue empty state matches Schedules. - Metadata: Key rendered as copyable mono text instead of a JSON tree. - OSS top bar gets a light/dark toggle (cloud keeps it in the user menu). - ?n=counter (plain string) no longer renders a Zod error page. - Provider/runner copy no longer references Rivet Cloud in OSS; settings drawer description is flavor-aware; Live badge uses primary instead of destructive. - "No more Actors to load." only shows after more than one page. Amp-Thread-ID: https://ampcode.com/threads/T-01a072ad-3c9e-744e-b4b9-a8997aae8809 Co-authored-by: Nicholas Kissel --- frontend/src/app/runner-config-table.tsx | 2 +- frontend/src/app/runners-table.tsx | 4 +- frontend/src/app/settings-drawer.tsx | 5 +- .../app/settings-pages/namespace-settings.tsx | 4 +- frontend/src/app/top-bar-actions.tsx | 43 +++++- .../actors/actor-connections-tab.tsx | 140 ++++++++++++++++-- .../src/components/actors/actor-database.tsx | 49 +++++- .../actors/actor-details-iframe.tsx | 3 +- .../actors/actor-details-shared.tsx | 32 +++- .../actors/actor-details-skeleton.tsx | 2 +- .../src/components/actors/actor-general.tsx | 31 ++-- .../src/components/actors/actor-queue.tsx | 16 +- .../src/components/actors/actors-list.tsx | 6 +- .../actors/inspector-tab-registry.tsx | 20 ++- .../components/actors/no-providers-alert.tsx | 12 +- frontend/src/components/live-badge.tsx | 3 +- frontend/src/routes/_context.tsx | 11 +- 17 files changed, 317 insertions(+), 66 deletions(-) diff --git a/frontend/src/app/runner-config-table.tsx b/frontend/src/app/runner-config-table.tsx index f7179c077b..15f147bcdf 100644 --- a/frontend/src/app/runner-config-table.tsx +++ b/frontend/src/app/runner-config-table.tsx @@ -87,7 +87,7 @@ export function RunnerConfigsTable({ - There's no providers matching criteria. + No providers added yet. diff --git a/frontend/src/app/runners-table.tsx b/frontend/src/app/runners-table.tsx index 59bacab7f5..1490e55034 100644 --- a/frontend/src/app/runners-table.tsx +++ b/frontend/src/app/runners-table.tsx @@ -412,8 +412,8 @@ function EmptyState() { ) : ( - There are no runners connected. You will not be able to - run actors until a runner appears here. + No runners connected yet. Actors can't run until a + runner appears here. )} diff --git a/frontend/src/app/settings-drawer.tsx b/frontend/src/app/settings-drawer.tsx index ff9417d795..380dee2d8a 100644 --- a/frontend/src/app/settings-drawer.tsx +++ b/frontend/src/app/settings-drawer.tsx @@ -92,8 +92,9 @@ const TAB_META: Record = { }, settings: { title: "Settings", - description: - "Connect your RivetKit application to Rivet Cloud. Use your cloud of choice to run Rivet Actors.", + description: features.platform + ? "Connect your RivetKit application to Rivet Cloud. Use your cloud of choice to run Rivet Actors." + : "Connect providers and runners to this namespace. Use your cloud of choice to run Rivet Actors.", }, compute: { title: "Compute", diff --git a/frontend/src/app/settings-pages/namespace-settings.tsx b/frontend/src/app/settings-pages/namespace-settings.tsx index a6451a1f80..ae1a3c31e0 100644 --- a/frontend/src/app/settings-pages/namespace-settings.tsx +++ b/frontend/src/app/settings-pages/namespace-settings.tsx @@ -109,7 +109,7 @@ function Providers() { return ( + } + /> + ); +} + function DocsButton() { return ( ) - ) : ( + ) : count > RECORDS_PER_PAGE ? ( + // Only worth saying once the user has actually scrolled through + // more than one page; for short lists it is just noise. {copy.noMoreActors} - )} + ) : null} ); } diff --git a/frontend/src/components/actors/inspector-tab-registry.tsx b/frontend/src/components/actors/inspector-tab-registry.tsx index 1e560710ab..fd1804bea5 100644 --- a/frontend/src/components/actors/inspector-tab-registry.tsx +++ b/frontend/src/components/actors/inspector-tab-registry.tsx @@ -75,25 +75,29 @@ interface TabRegistration { render: (actorId: ActorId) => ReactNode; } -// Tab list — preserved order matches the dashboard tab strip. Adding a new -// inspector tab here automatically advertises it to the dashboard (in the -// iframe path) and renders it inline (in the legacy path). +// Tab list — preserved order matches the dashboard tab strip, and the first +// available tab is the default the dashboard opens. State sits before +// Database on purpose: every actor has a SQLite database (so Database would +// otherwise always win), but the state object is what the actor's code +// actually defines. Adding a new inspector tab here automatically advertises +// it to the dashboard (in the iframe path) and renders it inline (in the +// legacy path). export const INSPECTOR_TAB_REGISTRATIONS: readonly TabRegistration[] = [ { descriptor: { id: "workflow", label: "Workflow", icon: "workflow" }, available: (caps) => caps.isWorkflowEnabled, render: (actorId) => , }, - { - descriptor: { id: "database", label: "Database", icon: "database" }, - available: (caps) => caps.isDatabaseEnabled, - render: (actorId) => , - }, { descriptor: { id: "state", label: "State", icon: "state" }, available: (caps) => caps.isStateEnabled, render: (actorId) => , }, + { + descriptor: { id: "database", label: "Database", icon: "database" }, + available: (caps) => caps.isDatabaseEnabled, + render: (actorId) => , + }, { descriptor: { id: "queue", label: "Queue", icon: "queue" }, available: (caps) => caps.isQueueSupported, diff --git a/frontend/src/components/actors/no-providers-alert.tsx b/frontend/src/components/actors/no-providers-alert.tsx index 01d6402238..9dd8c80d36 100644 --- a/frontend/src/components/actors/no-providers-alert.tsx +++ b/frontend/src/components/actors/no-providers-alert.tsx @@ -1,4 +1,4 @@ -import { faBook, faExclamationTriangle, faPlus, Icon } from "@rivet-gg/icons"; +import { faBook, faPlug, faPlus, Icon } from "@rivet-gg/icons"; import { Link } from "@tanstack/react-router"; import { ProviderDropdown } from "@/app/provider-dropdown"; import { docsLinks } from "@/content/data"; @@ -16,14 +16,14 @@ export function NoProvidersAlert({

- No Providers Connected + No providers connected

- You can't run any Actors yet. Use provider of your choice to - connect and start deploying and running Rivet Actors. + Actors need somewhere to run. Add a provider to connect a + cloud of your choice and start running Rivet Actors.

@@ -65,7 +65,7 @@ export function NoProvidersAlert({ size="sm" className="w-full" > - Connect Provider + Add Provider )} diff --git a/frontend/src/components/live-badge.tsx b/frontend/src/components/live-badge.tsx index 45e1c1f324..6b201ad084 100644 --- a/frontend/src/components/live-badge.tsx +++ b/frontend/src/components/live-badge.tsx @@ -11,7 +11,8 @@ export function LiveBadge({ className }: LiveBadgeProps) { className={cn(className, "flex justify-center items-center")} variant="outline" > -
+ {/* Accent, not destructive: red reads as an error elsewhere in the app. */} +
Live ); diff --git a/frontend/src/routes/_context.tsx b/frontend/src/routes/_context.tsx index 2bab960caa..15feffba23 100644 --- a/frontend/src/routes/_context.tsx +++ b/frontend/src/routes/_context.tsx @@ -44,7 +44,16 @@ const searchSchema = z export const Route = createFileRoute("/_context")({ component: RouteComponent, validateSearch: (search) => { - const validated = searchSchema.parse(search); + // Hand-typed and shared links often carry `?n=counter` instead of the + // serialized array form. Accept both so a bare string never surfaces + // as a raw validation error page. Normalized before parsing because + // the schema is an intersection, and a per-field transform would + // conflict with the untouched value from the `z.record` half. + const normalized = + typeof search.n === "string" + ? { ...search, n: [search.n] } + : search; + const validated = searchSchema.parse(normalized); // `pool` is scoped to the pages that actually use it: the Logs route // re-declares it in its own validateSearch, and the compute settings tab // needs it while open. Drop it everywhere else so the selected pool does