From 0f13720284c011d8d70d30dc8a056436208e07f9 Mon Sep 17 00:00:00 2001 From: Kacper Wojciechowski <39823706+jog1t@users.noreply.github.com> Date: Wed, 24 Sep 2025 23:11:35 +0200 Subject: [PATCH] feat(engine): ask for token when unauth --- frontend/package.json | 3 +- frontend/src/app.tsx | 8 + .../provide-engine-credentials-dialog.tsx | 82 ++++++++++ .../src/app/forms/engine-credentials-form.tsx | 47 ++++++ frontend/src/app/use-dialog.tsx | 21 ++- .../src/components/actors/actor-context.tsx | 2 +- .../components/actors/actor-events-list.tsx | 2 +- .../actors/actor-queries-context.tsx | 2 +- .../actors/database/database-table.tsx | 2 +- .../actors/guard-connectable-inspector.tsx | 2 + .../src/components/actors/manager-context.tsx | 2 +- .../src/components/actors/queries/actor.ts | 6 +- .../src/components/actors/queries/index.ts | 9 +- .../actors/worker/actor-repl.worker.ts | 9 +- .../actors/worker/actor-worker-container.ts | 6 +- .../actors/worker/actor-worker-context.tsx | 4 +- .../actors/worker/actor-worker-schema.ts | 1 + .../src/components/lib/create-schema-form.tsx | 4 + frontend/src/components/lib/utils.ts | 21 +++ frontend/src/components/modal-renderer.tsx | 45 ++++++ frontend/src/queries/actor-inspector.ts | 9 +- frontend/src/queries/global.ts | 14 +- frontend/src/queries/manager-engine.ts | 101 +++++++++++-- frontend/src/queries/manager-inspector.ts | 4 +- frontend/src/queries/utils.ts | 30 ++++ frontend/src/routes/_layout.tsx | 6 +- frontend/src/routes/_layout/index.tsx | 42 +++--- frontend/src/stores/modal-store.ts | 48 ++++++ frontend/src/utils/modal-utils.ts | 17 +++ package.json | 3 + pnpm-lock.yaml | 141 +++++++++++------- 31 files changed, 569 insertions(+), 124 deletions(-) create mode 100644 frontend/src/app/dialogs/provide-engine-credentials-dialog.tsx create mode 100644 frontend/src/app/forms/engine-credentials-form.tsx create mode 100644 frontend/src/components/modal-renderer.tsx create mode 100644 frontend/src/queries/utils.ts create mode 100644 frontend/src/stores/modal-store.ts create mode 100644 frontend/src/utils/modal-utils.ts diff --git a/frontend/package.json b/frontend/package.json index da4ee5c607..9171c999d1 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -50,8 +50,7 @@ "@radix-ui/react-tooltip": "^1.1.1", "@radix-ui/react-visually-hidden": "^1.0.3", "@rivet-gg/icons": "file:./vendor/rivet-icons.tgz", - "@rivetkit/actor": "file:./vendor/rivetkit-actor.tgz", - "@rivetkit/core": "file:./vendor/rivetkit-core.tgz", + "rivetkit": "*", "@rivetkit/engine-api-full": "workspace:*", "@sentry/react": "^8.26.0", "@sentry/vite-plugin": "^2.22.2", diff --git a/frontend/src/app.tsx b/frontend/src/app.tsx index 42b634ea31..b8fa63e2e1 100644 --- a/frontend/src/app.tsx +++ b/frontend/src/app.tsx @@ -22,6 +22,14 @@ declare module "@tanstack/react-router" { } } +declare module "@tanstack/react-query" { + interface Register { + queryMeta: { + mightRequireAuth?: boolean; + }; + } +} + export const router = createRouter({ basepath: import.meta.env.BASE_URL, routeTree, diff --git a/frontend/src/app/dialogs/provide-engine-credentials-dialog.tsx b/frontend/src/app/dialogs/provide-engine-credentials-dialog.tsx new file mode 100644 index 0000000000..5303a90a2e --- /dev/null +++ b/frontend/src/app/dialogs/provide-engine-credentials-dialog.tsx @@ -0,0 +1,82 @@ +import * as EngineCredentialsForm from "@/app/forms/engine-credentials-form"; +import { + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, + Flex, + getConfig, + ls, + toast, +} from "@/components"; +import type { DialogContentProps } from "@/components/actors/hooks"; +import { queryClient } from "@/queries/global"; +import { createClient } from "@/queries/manager-engine"; + +interface ProvideEngineCredentialsDialogContentProps + extends DialogContentProps {} + +export default function ProvideEngineCredentialsDialogContent({ + onClose, +}: ProvideEngineCredentialsDialogContentProps) { + return ( + { + const client = createClient({ + token: values.token, + }); + + try { + await client.namespaces.list(); + + ls.engineCredentials.set(getConfig().apiUrl, values.token); + + toast.success( + "Successfully authenticated with Rivet Engine", + ); + + await queryClient.refetchQueries(); + + onClose?.(); + } catch (e) { + if (e && typeof e === "object" && "statusCode" in e) { + if (e.statusCode === 403) { + form.setError("token", { + message: "Invalid token.", + }); + return; + } + } + + form.setError("token", { + message: "Failed to connect. Please try again.", + }); + return; + } + }} + > + + Missing Rivet Engine credentials + + It looks like the instance of Rivet Engine that you're + connected to requires additional credentials, please provide + them below. + + + + + + + + Save + + + + ); +} diff --git a/frontend/src/app/forms/engine-credentials-form.tsx b/frontend/src/app/forms/engine-credentials-form.tsx new file mode 100644 index 0000000000..570340203f --- /dev/null +++ b/frontend/src/app/forms/engine-credentials-form.tsx @@ -0,0 +1,47 @@ +import { type UseFormReturn, useFormContext } from "react-hook-form"; +import z from "zod"; +import { + createSchemaForm, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, + Input, +} from "@/components"; + +export const formSchema = z.object({ + token: z.string().nonempty("Token is required"), +}); + +export type FormValues = z.infer; +export type SubmitHandler = ( + values: FormValues, + form: UseFormReturn, +) => Promise; + +const { Form, Submit, SetValue } = createSchemaForm(formSchema); +export { Form, Submit, SetValue }; + +export const Token = ({ className }: { className?: string }) => { + const { control } = useFormContext(); + return ( + ( + + Token + + + + + + )} + /> + ); +}; diff --git a/frontend/src/app/use-dialog.tsx b/frontend/src/app/use-dialog.tsx index 1bafdb0e83..10bd1252a9 100644 --- a/frontend/src/app/use-dialog.tsx +++ b/frontend/src/app/use-dialog.tsx @@ -1,9 +1,14 @@ -import { createDialogHook, useDialog } from "@/components/actors"; +import { + useDialog as baseUseDialog, + createDialogHook, +} from "@/components/actors"; -const d = useDialog as typeof useDialog & - Record>; -d.CreateNamespace = createDialogHook( - import("@/app/dialogs/create-namespace-dialog"), -); - -export { d as useDialog }; +export const useDialog = { + ...baseUseDialog, + CreateNamespace: createDialogHook( + import("@/app/dialogs/create-namespace-dialog"), + ), + ProvideEngineCredentials: createDialogHook( + import("@/app/dialogs/provide-engine-credentials-dialog"), + ), +}; diff --git a/frontend/src/components/actors/actor-context.tsx b/frontend/src/components/actors/actor-context.tsx index be3b960f60..d62efbb389 100644 --- a/frontend/src/components/actors/actor-context.tsx +++ b/frontend/src/components/actors/actor-context.tsx @@ -1,4 +1,4 @@ export { createActorInspectorClient, createManagerInspectorClient, -} from "@rivetkit/core/inspector"; +} from "rivetkit/inspector"; diff --git a/frontend/src/components/actors/actor-events-list.tsx b/frontend/src/components/actors/actor-events-list.tsx index 2ac30d5b13..83ddf4bd21 100644 --- a/frontend/src/components/actors/actor-events-list.tsx +++ b/frontend/src/components/actors/actor-events-list.tsx @@ -6,10 +6,10 @@ import { faUnlink, Icon, } from "@rivet-gg/icons"; -import type { RecordedRealtimeEvent } from "@rivetkit/core/inspector"; import { useQuery } from "@tanstack/react-query"; import { format } from "date-fns"; import { type PropsWithChildren, useEffect, useRef } from "react"; +import type { RecordedRealtimeEvent } from "rivetkit/inspector"; import { Badge } from "../ui/badge"; import { useActor } from "./actor-queries-context"; import { ActorObjectInspector } from "./console/actor-inspector"; diff --git a/frontend/src/components/actors/actor-queries-context.tsx b/frontend/src/components/actors/actor-queries-context.tsx index 63a1d2b8df..4b9d7bf198 100644 --- a/frontend/src/components/actors/actor-queries-context.tsx +++ b/frontend/src/components/actors/actor-queries-context.tsx @@ -1,6 +1,6 @@ -import { createActorInspectorClient } from "@rivetkit/core/inspector"; import { queryOptions } from "@tanstack/react-query"; import { createContext, useContext } from "react"; +import { createActorInspectorClient } from "rivetkit/inspector"; import type { ActorId } from "./queries"; type RequestOptions = Parameters[1]; diff --git a/frontend/src/components/actors/database/database-table.tsx b/frontend/src/components/actors/database/database-table.tsx index dfe55f4edd..bbf4a39efc 100644 --- a/frontend/src/components/actors/database/database-table.tsx +++ b/frontend/src/components/actors/database/database-table.tsx @@ -5,7 +5,6 @@ import { faLink, Icon, } from "@rivet-gg/icons"; -import type { Column, Columns, ForeignKeys } from "@rivetkit/core/inspector"; import { createColumnHelper, // SortingState, @@ -19,6 +18,7 @@ import { useReactTable as useTable, } from "@tanstack/react-table"; import { Fragment, useMemo, useState } from "react"; +import type { Column, Columns, ForeignKeys } from "rivetkit/inspector"; import { Badge, Button, diff --git a/frontend/src/components/actors/guard-connectable-inspector.tsx b/frontend/src/components/actors/guard-connectable-inspector.tsx index d7dca760ab..5056981ac1 100644 --- a/frontend/src/components/actors/guard-connectable-inspector.tsx +++ b/frontend/src/components/actors/guard-connectable-inspector.tsx @@ -10,6 +10,7 @@ import { } from "@/queries/manager-engine"; import { DiscreteCopyButton } from "../copy-area"; import { getConfig } from "../lib/config"; +import { ls } from "../lib/utils"; import { Button } from "../ui/button"; import { useFiltersValue } from "./actor-filters-context"; import { ActorProvider } from "./actor-queries-context"; @@ -157,6 +158,7 @@ function useActorEngineContext({ actorId }: { actorId: ActorId }) { return createInspectorActorContext({ url: getConfig().apiUrl, token: (runner?.metadata?.inspectorToken as string) || "", + engineToken: ls.engineCredentials.get(getConfig().apiUrl) || "", }); }, [runner?.metadata?.inspectorToken]); diff --git a/frontend/src/components/actors/manager-context.tsx b/frontend/src/components/actors/manager-context.tsx index d00d92b009..fef73597ec 100644 --- a/frontend/src/components/actors/manager-context.tsx +++ b/frontend/src/components/actors/manager-context.tsx @@ -1,4 +1,3 @@ -import type { CreateActor as InspectorCreateActor } from "@rivetkit/core/inspector"; import { infiniteQueryOptions, type MutationOptions, @@ -6,6 +5,7 @@ import { queryOptions, } from "@tanstack/react-query"; import { createContext, useContext } from "react"; +import type { CreateActor as InspectorCreateActor } from "rivetkit/inspector"; import { z } from "zod"; import { queryClient } from "@/queries/global"; import { diff --git a/frontend/src/components/actors/queries/actor.ts b/frontend/src/components/actors/queries/actor.ts index 3d5fc0982c..ae76eb0340 100644 --- a/frontend/src/components/actors/queries/actor.ts +++ b/frontend/src/components/actors/queries/actor.ts @@ -1,12 +1,8 @@ import { fetchEventSource } from "@microsoft/fetch-event-source"; -import type { - ActorId, - Patch, - RecordedRealtimeEvent, -} from "@rivetkit/core/inspector"; import { useMutation, useQueryClient } from "@tanstack/react-query"; import { applyPatch, compare } from "fast-json-patch"; import { useCallback, useEffect, useMemo } from "react"; +import type { ActorId, Patch, RecordedRealtimeEvent } from "rivetkit/inspector"; import { useActor } from "../actor-queries-context"; export const useActorClearEventsMutation = ( diff --git a/frontend/src/components/actors/queries/index.ts b/frontend/src/components/actors/queries/index.ts index 8726acfa66..693c8993a0 100644 --- a/frontend/src/components/actors/queries/index.ts +++ b/frontend/src/components/actors/queries/index.ts @@ -1,10 +1,9 @@ -import type { Actor as InspectorActor } from "@rivetkit/core/inspector"; -import type { NamespaceNameId } from "@/queries/manager-engine"; +import type { Actor as InspectorActor } from "rivetkit/inspector"; -export type { ActorLogEntry } from "@rivetkit/core/inspector"; -export { ActorFeature } from "@rivetkit/core/inspector"; +export type { ActorLogEntry } from "rivetkit/inspector"; +export { ActorFeature } from "rivetkit/inspector"; -import type { ActorId } from "@rivetkit/core/inspector"; +import type { ActorId } from "rivetkit/inspector"; export type { ActorId }; diff --git a/frontend/src/components/actors/worker/actor-repl.worker.ts b/frontend/src/components/actors/worker/actor-repl.worker.ts index ef066cafb1..fc37a0e106 100644 --- a/frontend/src/components/actors/worker/actor-repl.worker.ts +++ b/frontend/src/components/actors/worker/actor-repl.worker.ts @@ -1,13 +1,11 @@ -import { createClient } from "@rivetkit/actor/client"; import { fromJs } from "esast-util-from-js"; import { toJs } from "estree-util-to-js"; +import { createClient } from "rivetkit/client"; import { createHighlighterCore, createOnigurumaEngine, type HighlighterCore, } from "shiki"; -import { getConfig } from "@/components"; -import { createEngineActorContext } from "@/queries/actor-engine"; import { type InitMessage, MessageSchema, @@ -183,6 +181,9 @@ function respond(msg: Response) { async function callAction({ name, args }: { name: string; args: unknown[] }) { if (!init) throw new Error("Actor not initialized"); - const client = createClient(init.endpoint).getForId(init.name, init.id); + const client = createClient({ + endpoint: init.endpoint, + token: init.engineToken, + }).getForId(init.name, init.id); return await client.action({ name, args }); } diff --git a/frontend/src/components/actors/worker/actor-worker-container.ts b/frontend/src/components/actors/worker/actor-worker-container.ts index c928583b44..dc987063df 100644 --- a/frontend/src/components/actors/worker/actor-worker-container.ts +++ b/frontend/src/components/actors/worker/actor-worker-container.ts @@ -44,6 +44,7 @@ export class ActorWorkerContainer { rpcs: string[]; endpoint?: string; name?: string; + engineToken?: string; } | null = null; #listeners: (() => void)[] = []; @@ -56,16 +57,18 @@ export class ActorWorkerContainer { rpcs = [], endpoint, name, + engineToken, }: { actorId: string; signal: AbortSignal; rpcs?: string[]; endpoint?: string; name?: string; + engineToken?: string; }) { this.terminate(); - this.#meta = { actorId, rpcs, endpoint, name }; + this.#meta = { actorId, rpcs, endpoint, name, engineToken }; this.#state.status = { type: "pending" }; this.#update(); try { @@ -142,6 +145,7 @@ export class ActorWorkerContainer { id: this.#meta?.actorId ?? "", endpoint: this.#meta?.endpoint ?? "", name: this.#meta?.name ?? "", + engineToken: this.#meta?.engineToken ?? "", } satisfies InitMessage); } diff --git a/frontend/src/components/actors/worker/actor-worker-context.tsx b/frontend/src/components/actors/worker/actor-worker-context.tsx index 0e2170f5ab..6a3038e940 100644 --- a/frontend/src/components/actors/worker/actor-worker-context.tsx +++ b/frontend/src/components/actors/worker/actor-worker-context.tsx @@ -8,7 +8,8 @@ import { useState, useSyncExternalStore, } from "react"; -import { assertNonNullable } from "../../lib/utils"; +import { getConfig } from "@/components/lib/config"; +import { assertNonNullable, ls } from "../../lib/utils"; import { useActor } from "../actor-queries-context"; import { useManager } from "../manager-context"; import { ActorFeature, type ActorId } from "../queries"; @@ -69,6 +70,7 @@ export const ActorWorkerContextProvider = ({ name, signal: ctrl.signal, rpcs, + engineToken: ls.engineCredentials.get(getConfig().apiUrl) || "", }); } diff --git a/frontend/src/components/actors/worker/actor-worker-schema.ts b/frontend/src/components/actors/worker/actor-worker-schema.ts index ef1d16d504..aee77201f7 100644 --- a/frontend/src/components/actors/worker/actor-worker-schema.ts +++ b/frontend/src/components/actors/worker/actor-worker-schema.ts @@ -10,6 +10,7 @@ const CodeMessageSchema = z.object({ const InitMessageSchema = z.object({ type: z.literal("init"), rpcs: z.array(z.string()).optional(), + engineToken: z.string().optional(), endpoint: z.string(), name: z.string(), id: z.string(), diff --git a/frontend/src/components/lib/create-schema-form.tsx b/frontend/src/components/lib/create-schema-form.tsx index 704ae5b264..9f314696ac 100644 --- a/frontend/src/components/lib/create-schema-form.tsx +++ b/frontend/src/components/lib/create-schema-form.tsx @@ -6,6 +6,7 @@ import { type FieldPath, type FieldValues, type PathValue, + type UseFormProps, type UseFormReturn, useForm, useFormContext, @@ -18,6 +19,7 @@ interface FormProps extends Omit, "onSubmit"> { onSubmit: SubmitHandler; defaultValues: DefaultValues; + errors?: UseFormProps["errors"]; values?: FormValues; children: ReactNode; } @@ -36,6 +38,7 @@ export const createSchemaForm = ( values, children, onSubmit, + errors, ...props }: FormProps>) => { const form = useForm>({ @@ -43,6 +46,7 @@ export const createSchemaForm = ( resolver: zodResolver(schema), defaultValues, values, + errors, }); return ( diff --git a/frontend/src/components/lib/utils.ts b/frontend/src/components/lib/utils.ts index f86a3bc223..0279c02bc8 100644 --- a/frontend/src/components/lib/utils.ts +++ b/frontend/src/components/lib/utils.ts @@ -1,4 +1,5 @@ import { type ClassValue, clsx } from "clsx"; +import { set } from "lodash"; import { twMerge } from "tailwind-merge"; export function cn(...inputs: ClassValue[]) { @@ -23,6 +24,26 @@ export const ls = { clear: () => { localStorage.clear(); }, + engineCredentials: { + set: (url: string, token: string) => { + ls.set( + btoa(`engine-credentials-${JSON.stringify(url)}`), + JSON.stringify({ token }), + ); + }, + get: (url: string) => { + try { + const value = JSON.parse( + ls.get(btoa(`engine-credentials-${JSON.stringify(url)}`)), + ); + if (value && typeof value === "object" && "token" in value) { + return (value as { token: string }).token; + } + } catch { + return null; + } + }, + }, actorsList: { set: (width: number, folded: boolean) => { ls.set("actors-list-preview-width", width); diff --git a/frontend/src/components/modal-renderer.tsx b/frontend/src/components/modal-renderer.tsx new file mode 100644 index 0000000000..658d439df8 --- /dev/null +++ b/frontend/src/components/modal-renderer.tsx @@ -0,0 +1,45 @@ +"use client"; +import { useDialog } from "@/app/use-dialog"; +import { modalActions, useOpenModal } from "@/stores/modal-store"; + +export function ModalRenderer() { + const openModal = useOpenModal(); + + if (!openModal) { + return null; + } + + const DialogComponent = getDialogComponent(openModal.dialogKey); + if (!DialogComponent) { + console.warn( + `Dialog component not found for key: ${openModal.dialogKey}`, + ); + return null; + } + + return ( + { + if (!open) { + modalActions.closeModal(); + } + }, + }} + /> + ); +} + +function getDialogComponent(dialogKey: string) { + const dialogs = useDialog; + const dialog = dialogs[dialogKey]; + + if (!dialog || typeof dialog !== "function") { + return null; + } + + // Access the Dialog component from the hook + return dialog.Dialog; +} diff --git a/frontend/src/queries/actor-inspector.ts b/frontend/src/queries/actor-inspector.ts index ebf8566d05..6dd8ab9dc4 100644 --- a/frontend/src/queries/actor-inspector.ts +++ b/frontend/src/queries/actor-inspector.ts @@ -6,10 +6,12 @@ import { ensureTrailingSlash } from "@/lib/utils"; export const createInspectorActorContext = ({ url, - token, + token: inspectorToken, + engineToken, }: { url: string; token: string; + engineToken?: string; }) => { const def = createDefaultActorContext(); const newUrl = new URL(url); @@ -23,7 +25,10 @@ export const createInspectorActorContext = ({ headers: { "x-rivet-actor": actorId, "x-rivet-target": "actor", - ...(token ? { authorization: `Bearer ${token}` } : {}), + ...(engineToken ? { "x-rivet-token": engineToken } : {}), + ...(inspectorToken + ? { authorization: `Bearer ${inspectorToken}` } + : {}), }, }; }, diff --git a/frontend/src/queries/global.ts b/frontend/src/queries/global.ts index ebb6c15407..7c4dfe4573 100644 --- a/frontend/src/queries/global.ts +++ b/frontend/src/queries/global.ts @@ -1,7 +1,19 @@ import { MutationCache, QueryCache, QueryClient } from "@tanstack/react-query"; import { toast } from "@/components"; +import { modal } from "@/utils/modal-utils"; -const queryCache = new QueryCache(); +const queryCache = new QueryCache({ + onError(error, query) { + if ( + query.meta?.mightRequireAuth && + "statusCode" in error && + error.statusCode === 403 + ) { + modal.open("ProvideEngineCredentials"); + return; + } + }, +}); const mutationCache = new MutationCache({ onError(error, variables, context, mutation) { diff --git a/frontend/src/queries/manager-engine.ts b/frontend/src/queries/manager-engine.ts index 6d644f103b..043c73e2f1 100644 --- a/frontend/src/queries/manager-engine.ts +++ b/frontend/src/queries/manager-engine.ts @@ -1,12 +1,12 @@ -import { ActorFeature } from "@rivetkit/core/inspector"; import { type Rivet, RivetClient } from "@rivetkit/engine-api-full"; import { infiniteQueryOptions, queryOptions, skipToken, } from "@tanstack/react-query"; +import { ActorFeature } from "rivetkit/inspector"; import z from "zod"; -import { getConfig } from "@/components"; +import { getConfig, ls } from "@/components"; import type { Actor, ActorId, @@ -18,10 +18,17 @@ import { ActorQueryOptionsSchema, createDefaultManagerContext, } from "@/components/actors/manager-context"; +import { shouldRetryAllExpect403, throwAllExpect403 } from "./utils"; -const client = new RivetClient({ - baseUrl: () => getConfig().apiUrl, - environment: "", +export const createClient = (opts: { token: (() => string) | string }) => + new RivetClient({ + baseUrl: () => getConfig().apiUrl, + environment: "", + ...opts, + }); + +const client = createClient({ + token: () => ls.engineCredentials.get(getConfig().apiUrl) || "", }); export { client as managerClient }; @@ -45,6 +52,11 @@ export const createEngineManagerContext = ({ queryFn: async () => { return true; }, + retry: shouldRetryAllExpect403, + throwOnError: throwAllExpect403, + meta: { + mightRequireAuth: true, + }, }); }, regionsQueryOptions() { @@ -61,6 +73,11 @@ export const createEngineManagerContext = ({ pagination: data.pagination, }; }, + retry: shouldRetryAllExpect403, + throwOnError: throwAllExpect403, + meta: { + mightRequireAuth: true, + }, }); }, regionQueryOptions(regionId: string) { @@ -82,6 +99,11 @@ export const createEngineManagerContext = ({ throw new Error(`Region not found: ${regionId}`); }, + retry: shouldRetryAllExpect403, + throwOnError: throwAllExpect403, + meta: { + mightRequireAuth: true, + }, }); }, actorQueryOptions(actorId) { @@ -90,13 +112,21 @@ export const createEngineManagerContext = ({ queryKey: [namespace, "actor", actorId], enabled: true, queryFn: async ({ signal: abortSignal }) => { - const data = await client.actorsGet( - actorId, - { namespace }, + const data = await client.actorsList( + { actorIds: actorId as string, namespace }, { abortSignal }, ); - return transformActor(data.actor); + if (!data.actors[0]) { + throw new Error("Actor not found"); + } + + return transformActor(data.actors[0]); + }, + retry: shouldRetryAllExpect403, + throwOnError: throwAllExpect403, + meta: { + mightRequireAuth: true, }, }); }, @@ -168,6 +198,11 @@ export const createEngineManagerContext = ({ } return lastPage.pagination.cursor; }, + retry: shouldRetryAllExpect403, + throwOnError: throwAllExpect403, + meta: { + mightRequireAuth: true, + }, }); }, buildsQueryOptions() { @@ -201,6 +236,11 @@ export const createEngineManagerContext = ({ } return lastPage.pagination.cursor; }, + retry: shouldRetryAllExpect403, + throwOnError: throwAllExpect403, + meta: { + mightRequireAuth: true, + }, }); }, createActorMutationOptions() { @@ -220,11 +260,21 @@ export const createEngineManagerContext = ({ return response.actor.actorId; }, onSuccess: () => {}, + throwOnError: throwAllExpect403, + retry: shouldRetryAllExpect403, + meta: { + mightRequireAuth: true, + }, }; }, actorDestroyMutationOptions(actorId) { return { ...def.actorDestroyMutationOptions(actorId), + throwOnError: throwAllExpect403, + retry: shouldRetryAllExpect403, + meta: { + mightRequireAuth: true, + }, mutationFn: async () => { await client.actorsDelete(actorId); }, @@ -258,6 +308,11 @@ export const runnersQueryOptions = (opts: { namespace: NamespaceNameId }) => { return lastPage.pagination.cursor; }, select: (data) => data.pages.flatMap((page) => page.runners), + retry: shouldRetryAllExpect403, + throwOnError: throwAllExpect403, + meta: { + mightRequireAuth: true, + }, }); }; @@ -278,6 +333,11 @@ export const runnerQueryOptions = (opts: { ); return data.runner; }, + throwOnError: throwAllExpect403, + retry: shouldRetryAllExpect403, + meta: { + mightRequireAuth: true, + }, }); }; @@ -300,6 +360,11 @@ export const runnerByNameQueryOptions = (opts: { } return data.runners[0]; }, + throwOnError: throwAllExpect403, + retry: shouldRetryAllExpect403, + meta: { + mightRequireAuth: true, + }, }); }; @@ -329,6 +394,11 @@ export const runnerNamesQueryOptions = (opts: { return lastPage.pagination.cursor; }, select: (data) => data.pages.flatMap((page) => page.names), + throwOnError: throwAllExpect403, + retry: shouldRetryAllExpect403, + meta: { + mightRequireAuth: true, + }, }); }; @@ -347,12 +417,17 @@ export const namespacesQueryOptions = () => { return data; }, getNextPageParam: (lastPage) => { - if (lastPage.namespaces.length < ACTORS_PER_PAGE) { + if (lastPage?.namespaces?.length < ACTORS_PER_PAGE) { return undefined; } return lastPage.pagination.cursor; }, select: (data) => data.pages.flatMap((page) => page.namespaces), + throwOnError: throwAllExpect403, + retry: shouldRetryAllExpect403, + meta: { + mightRequireAuth: true, + }, }); }; @@ -370,6 +445,12 @@ export const namespaceQueryOptions = ( return data.namespace; } : skipToken, + + retry: shouldRetryAllExpect403, + throwOnError: throwAllExpect403, + meta: { + mightRequireAuth: true, + }, }); }; diff --git a/frontend/src/queries/manager-inspector.ts b/frontend/src/queries/manager-inspector.ts index 7822205c20..9bfef45a06 100644 --- a/frontend/src/queries/manager-inspector.ts +++ b/frontend/src/queries/manager-inspector.ts @@ -1,8 +1,8 @@ +import { infiniteQueryOptions } from "@tanstack/react-query"; import { createManagerInspectorClient, type Actor as InspectorActor, -} from "@rivetkit/core/inspector"; -import { infiniteQueryOptions } from "@tanstack/react-query"; +} from "rivetkit/inspector"; import type { Actor, ActorId, ManagerContext } from "@/components/actors"; import { createDefaultManagerContext } from "@/components/actors/manager-context"; import { ensureTrailingSlash } from "@/lib/utils"; diff --git a/frontend/src/queries/utils.ts b/frontend/src/queries/utils.ts new file mode 100644 index 0000000000..86e0317b01 --- /dev/null +++ b/frontend/src/queries/utils.ts @@ -0,0 +1,30 @@ +import type { Query } from "@tanstack/react-query"; + +export const shouldRetryAllExpect403 = (failureCount: number, error: Error) => { + if (error && "statusCode" in error) { + if (error.statusCode === 403) { + // Don't retry on auth errors + return false; + } + } + + if (failureCount >= 3) { + return false; + } + + return true; +}; + +export const throwAllExpect403 = >( + error: Error, + _query: T, +) => { + if (error && "statusCode" in error) { + if (error.statusCode === 403) { + // Don't throw on auth errors + return false; + } + } + + return true; +}; diff --git a/frontend/src/routes/_layout.tsx b/frontend/src/routes/_layout.tsx index e2fefa900c..aad64c9f85 100644 --- a/frontend/src/routes/_layout.tsx +++ b/frontend/src/routes/_layout.tsx @@ -30,12 +30,11 @@ import { H1, type ImperativePanelHandle, } from "@/components"; -import { ActorProvider, ManagerProvider } from "@/components/actors"; +import { ManagerProvider } from "@/components/actors"; import { RootLayoutContextProvider } from "@/components/actors/root-layout-context"; import { ConnectionForm } from "@/components/connection-form"; +import { ModalRenderer } from "@/components/modal-renderer"; import { docsLinks } from "@/content/data"; -import { createEngineActorContext } from "@/queries/actor-engine"; -import { createInspectorActorContext } from "@/queries/actor-inspector"; import { createEngineManagerContext, type NamespaceNameId, @@ -76,6 +75,7 @@ function RouteComponent() { const content = ( <> + diff --git a/frontend/src/routes/_layout/index.tsx b/frontend/src/routes/_layout/index.tsx index a3be56d54f..7691fa1ad0 100644 --- a/frontend/src/routes/_layout/index.tsx +++ b/frontend/src/routes/_layout/index.tsx @@ -1,8 +1,7 @@ -import { useSuspenseInfiniteQuery } from "@tanstack/react-query"; +import { useInfiniteQuery } from "@tanstack/react-query"; import { createFileRoute, Navigate } from "@tanstack/react-router"; import { Card, CardContent, CardHeader, CardTitle } from "@/components"; import { namespacesQueryOptions } from "@/queries/manager-engine"; - import { RouteComponent as NamespaceRouteComponent } from "./ns.$namespace/index"; export const Route = createFileRoute("/_layout/")({ @@ -11,32 +10,29 @@ export const Route = createFileRoute("/_layout/")({ }); function RouteComponent() { - const { data: namespaces } = useSuspenseInfiniteQuery( - namespacesQueryOptions(), - ); + const { data: namespaces } = useInfiniteQuery(namespacesQueryOptions()); - if (namespaces.length <= 0) { + if (namespaces && namespaces?.length > 0) { return ( - - - - No namespaces found - - - - Please consult the documentation for more - information. - - - - + ); } return ( - + + + + No namespaces found + + + + Please consult the documentation for more information. + + + + ); } diff --git a/frontend/src/stores/modal-store.ts b/frontend/src/stores/modal-store.ts new file mode 100644 index 0000000000..6e40d3d9ca --- /dev/null +++ b/frontend/src/stores/modal-store.ts @@ -0,0 +1,48 @@ +import { useStore } from "@tanstack/react-store"; +import { Store } from "@tanstack/store"; + +interface ModalState { + openModal: { + id: string; + dialogKey: string; + props?: Record; + } | null; +} + +const initialState: ModalState = { + openModal: null, +}; + +export const modalStore = new Store(initialState); + +let modalIdCounter = 0; + +export const modalActions = { + openModal: (dialogKey: string, props?: Record) => { + const id = `modal-${++modalIdCounter}`; + modalStore.setState((state) => ({ + ...state, + openModal: { + id, + dialogKey, + props, + }, + })); + return id; + }, + + closeModal: () => { + modalStore.setState((state) => ({ + ...state, + openModal: null, + })); + }, +}; + +export const useModalStore = () => { + return useStore(modalStore); +}; + +export const useOpenModal = () => { + return useStore(modalStore, (state) => state.openModal); +}; \ No newline at end of file diff --git a/frontend/src/utils/modal-utils.ts b/frontend/src/utils/modal-utils.ts new file mode 100644 index 0000000000..d8afb696dd --- /dev/null +++ b/frontend/src/utils/modal-utils.ts @@ -0,0 +1,17 @@ +import type { useDialog } from "@/app/use-dialog"; +import { modalActions } from "@/stores/modal-store"; + +export const modal = { + open: ( + dialogKey: keyof typeof useDialog, + props?: Record, + ) => { + return modalActions.openModal(dialogKey, props); + }, + + close: () => { + modalActions.closeModal(); + }, +}; + +export type ModalApi = typeof modal; diff --git a/package.json b/package.json index dd94eb7c9c..25bdb0d145 100644 --- a/package.json +++ b/package.json @@ -17,5 +17,8 @@ "tsup": "^8.5.0", "turbo": "^2.0.1", "typescript": "^5.0.0" + }, + "resolutions": { + "rivetkit": "https://pkg.pr.new/rivet-dev/rivetkit@1296" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 22d7db208e..c2dc24f6c8 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -4,6 +4,9 @@ settings: autoInstallPeers: true excludeLinksFromLockfile: false +overrides: + rivetkit: https://pkg.pr.new/rivet-dev/rivetkit@1296 + importers: .: @@ -153,12 +156,6 @@ importers: '@rivet-gg/icons': specifier: file:./vendor/rivet-icons.tgz version: file:frontend/vendor/rivet-icons.tgz(@fortawesome/fontawesome-svg-core@6.7.2)(@fortawesome/free-brands-svg-icons@6.7.2)(@fortawesome/free-solid-svg-icons@6.7.2)(@fortawesome/react-fontawesome@0.2.3(@fortawesome/fontawesome-svg-core@6.7.2)(react@19.1.1))(@types/node@20.19.9)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)(terser@5.43.1) - '@rivetkit/actor': - specifier: file:./vendor/rivetkit-actor.tgz - version: file:frontend/vendor/rivetkit-actor.tgz(@rivetkit/core@file:frontend/vendor/rivetkit-core.tgz(@hono/node-server@1.18.2(hono@4.8.12))(@standard-schema/spec@1.0.0)(ws@8.18.3)) - '@rivetkit/core': - specifier: file:./vendor/rivetkit-core.tgz - version: file:frontend/vendor/rivetkit-core.tgz(@hono/node-server@1.18.2(hono@4.8.12))(@standard-schema/spec@1.0.0)(ws@8.18.3) '@rivetkit/engine-api-full': specifier: workspace:* version: link:../sdks/typescript/api-full @@ -318,6 +315,9 @@ importers: recharts: specifier: ^2.12.7 version: 2.15.4(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + rivetkit: + specifier: https://pkg.pr.new/rivet-dev/rivetkit@1296 + version: https://pkg.pr.new/rivet-dev/rivetkit@1296(@hono/node-server@1.18.2(hono@4.8.12))(@standard-schema/spec@1.0.0)(ws@8.18.3) shiki: specifier: ^3.8.1 version: 3.9.2 @@ -1174,6 +1174,10 @@ packages: resolution: {integrity: sha512-ruv7Ae4J5dUYULmeXw1gmb7rYRz57OWCPM57pHojnLq/3Z1CK2lNSLTCVjxVk1F/TZHwOZZrOWi0ur95BbLxNQ==} engines: {node: '>=6.9.0'} + '@bare-ts/lib@0.3.0': + resolution: {integrity: sha512-0JlIu0R26CTMdEnmmKIuFvAS9Cd554MvH4BjI6iL+74TDYBCHhaCWIDJXU6+VuYPu0+/JDLV/KXQstK9Sxn+9A==} + engines: {node: '>= 12'} + '@bare-ts/lib@0.4.0': resolution: {integrity: sha512-uTb12lcDkvwwwGb/4atNy/+2Xuksx88tlxKuwnDuxmsOFVo4R/WsE+fmAf2nWtzHDi7m6NDw8Ke9j+XlNBLoJg==} engines: {node: ^14.18.0 || >=16.0.0} @@ -1813,8 +1817,8 @@ packages: peerDependencies: hono: ^4 - '@hono/standard-validator@0.1.4': - resolution: {integrity: sha512-NPxSO9/Z1FFaJ34/0WJA0JoFRrVrJOG5P/i+PBJu7jCw9v0u8WmGiJu0zv+bTk1pjplBjjczAd59x2X50spRmQ==} + '@hono/standard-validator@0.1.5': + resolution: {integrity: sha512-EIyZPPwkyLn6XKwFj5NBEWHXhXbgmnVh2ceIFo5GO7gKI9WmzTjPDKnppQB0KrqKeAkq3kpoW4SIbu5X1dgx3w==} peerDependencies: '@standard-schema/spec': 1.0.0 hono: '>=3.9.0' @@ -1826,8 +1830,8 @@ packages: hono: '>=4.3.6' zod: '>=3.0.0' - '@hono/zod-validator@0.7.2': - resolution: {integrity: sha512-ub5eL/NeZ4eLZawu78JpW/J+dugDAYhwqUIdp9KYScI6PZECij4Hx4UsrthlEUutqDDhPwRI0MscUfNkvn/mqQ==} + '@hono/zod-validator@0.7.3': + resolution: {integrity: sha512-uYGdgVib3RlGD698WR5dVM0zB3UuPY5vHKXffGUbUh7r4xY+mFIhF3/v4AcQVLrU5CQdBso8BJr4wuVoCrjTuQ==} peerDependencies: hono: '>=3.9.0' zod: ^3.25.0 || ^4.0.0 @@ -2906,30 +2910,20 @@ packages: react: ^19 react-dom: ^19 - '@rivetkit/actor@file:frontend/vendor/rivetkit-actor.tgz': - resolution: {integrity: sha512-Ued5r1IQcR4zfUmVhy5uDMXQ4NqsGLHMZ5VJjXrFGY1DqT3XfpmYH33MrQBnUVnde8Jv8kvIyQwczDPm1myUog==, tarball: file:frontend/vendor/rivetkit-actor.tgz} - version: 0.9.1 - peerDependencies: - '@rivetkit/core': '*' + '@rivetkit/engine-runner-protocol@https://pkg.pr.new/rivet-dev/engine/@rivetkit/engine-runner-protocol@b72b2324c50c5449ed1844a060928d80d1151839': + resolution: {tarball: https://pkg.pr.new/rivet-dev/engine/@rivetkit/engine-runner-protocol@b72b2324c50c5449ed1844a060928d80d1151839} + version: 25.6.1 - '@rivetkit/core@file:frontend/vendor/rivetkit-core.tgz': - resolution: {integrity: sha512-elE/lA7Og0yXfx5DFO2xshtzKD6UXhju91sK6orvYb1iV7lSRlhH5XLRjeNlHEjfq2rGA/EyI11rug5n9OVl6Q==, tarball: file:frontend/vendor/rivetkit-core.tgz} - version: 0.9.3 - engines: {node: '>=22.0.0'} - peerDependencies: - '@hono/node-server': ^1.14.0 - '@hono/node-ws': ^1.1.1 - eventsource: ^3.0.5 - ws: ^8.0.0 - peerDependenciesMeta: - '@hono/node-server': - optional: true - '@hono/node-ws': - optional: true - eventsource: - optional: true - ws: - optional: true + '@rivetkit/engine-runner@https://pkg.pr.new/rivet-dev/engine/@rivetkit/engine-runner@b72b232': + resolution: {tarball: https://pkg.pr.new/rivet-dev/engine/@rivetkit/engine-runner@b72b232} + version: 25.6.1 + + '@rivetkit/engine-tunnel-protocol@https://pkg.pr.new/rivet-dev/engine/@rivetkit/engine-tunnel-protocol@b72b2324c50c5449ed1844a060928d80d1151839': + resolution: {tarball: https://pkg.pr.new/rivet-dev/engine/@rivetkit/engine-tunnel-protocol@b72b2324c50c5449ed1844a060928d80d1151839} + version: 25.6.1 + + '@rivetkit/fast-json-patch@3.1.2': + resolution: {integrity: sha512-CtA50xgsSSzICQduF/NDShPRzvucnNvsW/lQO0WgMTT1XAj9Lfae4pm7r3llFwilgG+9iq76Hv1LUqNy72v6yw==} '@rolldown/pluginutils@1.0.0-beta.27': resolution: {integrity: sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==} @@ -6565,6 +6559,25 @@ packages: deprecated: Rimraf versions prior to v4 are no longer supported hasBin: true + rivetkit@https://pkg.pr.new/rivet-dev/rivetkit@1296: + resolution: {tarball: https://pkg.pr.new/rivet-dev/rivetkit@1296} + version: 2.0.6 + engines: {node: '>=22.0.0'} + peerDependencies: + '@hono/node-server': ^1.14.0 + '@hono/node-ws': ^1.1.1 + eventsource: ^4.0.0 + ws: ^8.0.0 + peerDependenciesMeta: + '@hono/node-server': + optional: true + '@hono/node-ws': + optional: true + eventsource: + optional: true + ws: + optional: true + rollup@4.46.2: resolution: {integrity: sha512-WMmLFI+Boh6xbop+OAGo9cQ3OgX9MIg7xOQjn+pTCwOkk+FNDAeAemXkJ3HzDJrVXleLOFVa1ipuc1AmEx1Dwg==} engines: {node: '>=18.0.0', npm: '>=8.0.0'} @@ -7797,6 +7810,8 @@ snapshots: '@babel/helper-string-parser': 7.27.1 '@babel/helper-validator-identifier': 7.27.1 + '@bare-ts/lib@0.3.0': {} + '@bare-ts/lib@0.4.0': {} '@bare-ts/tools@0.15.0(@bare-ts/lib@0.4.0)': @@ -8241,7 +8256,7 @@ snapshots: dependencies: hono: 4.8.12 - '@hono/standard-validator@0.1.4(@standard-schema/spec@1.0.0)(hono@4.8.12)': + '@hono/standard-validator@0.1.5(@standard-schema/spec@1.0.0)(hono@4.8.12)': dependencies: '@standard-schema/spec': 1.0.0 hono: 4.8.12 @@ -8249,12 +8264,12 @@ snapshots: '@hono/zod-openapi@0.19.10(hono@4.8.12)(zod@3.25.76)': dependencies: '@asteasolutions/zod-to-openapi': 7.3.4(zod@3.25.76) - '@hono/zod-validator': 0.7.2(hono@4.8.12)(zod@3.25.76) + '@hono/zod-validator': 0.7.3(hono@4.8.12)(zod@3.25.76) hono: 4.8.12 openapi3-ts: 4.5.0 zod: 3.25.76 - '@hono/zod-validator@0.7.2(hono@4.8.12)(zod@3.25.76)': + '@hono/zod-validator@0.7.3(hono@4.8.12)(zod@3.25.76)': dependencies: hono: 4.8.12 zod: 3.25.76 @@ -9450,27 +9465,26 @@ snapshots: - sugarss - terser - '@rivetkit/actor@file:frontend/vendor/rivetkit-actor.tgz(@rivetkit/core@file:frontend/vendor/rivetkit-core.tgz(@hono/node-server@1.18.2(hono@4.8.12))(@standard-schema/spec@1.0.0)(ws@8.18.3))': + '@rivetkit/engine-runner-protocol@https://pkg.pr.new/rivet-dev/engine/@rivetkit/engine-runner-protocol@b72b2324c50c5449ed1844a060928d80d1151839': dependencies: - '@rivetkit/core': file:frontend/vendor/rivetkit-core.tgz(@hono/node-server@1.18.2(hono@4.8.12))(@standard-schema/spec@1.0.0)(ws@8.18.3) + '@bare-ts/lib': 0.4.0 - '@rivetkit/core@file:frontend/vendor/rivetkit-core.tgz(@hono/node-server@1.18.2(hono@4.8.12))(@standard-schema/spec@1.0.0)(ws@8.18.3)': + '@rivetkit/engine-runner@https://pkg.pr.new/rivet-dev/engine/@rivetkit/engine-runner@b72b232': dependencies: - '@hono/standard-validator': 0.1.4(@standard-schema/spec@1.0.0)(hono@4.8.12) - '@hono/zod-openapi': 0.19.10(hono@4.8.12)(zod@3.25.76) - cbor-x: 1.6.0 - fast-json-patch: 3.1.1 - hono: 4.8.12 - invariant: 2.2.4 - nanoevents: 9.1.0 - on-change: 5.0.1 - p-retry: 6.2.1 - zod: 3.25.76 - optionalDependencies: - '@hono/node-server': 1.18.2(hono@4.8.12) + '@rivetkit/engine-runner-protocol': https://pkg.pr.new/rivet-dev/engine/@rivetkit/engine-runner-protocol@b72b2324c50c5449ed1844a060928d80d1151839 + '@rivetkit/engine-tunnel-protocol': https://pkg.pr.new/rivet-dev/engine/@rivetkit/engine-tunnel-protocol@b72b2324c50c5449ed1844a060928d80d1151839 + pino: 9.9.5 + uuid: 12.0.0 ws: 8.18.3 transitivePeerDependencies: - - '@standard-schema/spec' + - bufferutil + - utf-8-validate + + '@rivetkit/engine-tunnel-protocol@https://pkg.pr.new/rivet-dev/engine/@rivetkit/engine-tunnel-protocol@b72b2324c50c5449ed1844a060928d80d1151839': + dependencies: + '@bare-ts/lib': 0.4.0 + + '@rivetkit/fast-json-patch@3.1.2': {} '@rolldown/pluginutils@1.0.0-beta.27': {} @@ -13855,6 +13869,29 @@ snapshots: dependencies: glob: 7.2.3 + rivetkit@https://pkg.pr.new/rivet-dev/rivetkit@1296(@hono/node-server@1.18.2(hono@4.8.12))(@standard-schema/spec@1.0.0)(ws@8.18.3): + dependencies: + '@bare-ts/lib': 0.3.0 + '@hono/standard-validator': 0.1.5(@standard-schema/spec@1.0.0)(hono@4.8.12) + '@hono/zod-openapi': 0.19.10(hono@4.8.12)(zod@3.25.76) + '@rivetkit/engine-runner': https://pkg.pr.new/rivet-dev/engine/@rivetkit/engine-runner@b72b232 + '@rivetkit/fast-json-patch': 3.1.2 + cbor-x: 1.6.0 + hono: 4.8.12 + invariant: 2.2.4 + nanoevents: 9.1.0 + on-change: 5.0.1 + p-retry: 6.2.1 + pino: 9.9.5 + zod: 3.25.76 + optionalDependencies: + '@hono/node-server': 1.18.2(hono@4.8.12) + ws: 8.18.3 + transitivePeerDependencies: + - '@standard-schema/spec' + - bufferutil + - utf-8-validate + rollup@4.46.2: dependencies: '@types/estree': 1.0.8
- Please consult the documentation for more - information. -
+ Please consult the documentation for more information. +