From 7c3c423e7a048c766afe63885615dfe37394b6a7 Mon Sep 17 00:00:00 2001 From: Kacper Wojciechowski <39823706+jog1t@users.noreply.github.com> Date: Wed, 18 Feb 2026 23:55:16 +0100 Subject: [PATCH] refactor(inspector): separate inspector app from dashboard --- frontend/apps/inspector/index.html | 32 ++ frontend/apps/inspector/public | 1 + frontend/apps/inspector/src/app.tsx | 83 ++++ .../inspector/src/app/data-providers/cache.ts | 19 + .../apps/inspector/src/app/inspector-root.tsx | 70 +++ frontend/apps/inspector/src/app/layout.tsx | 405 ++++++++++++++++++ .../apps/inspector/src/app/route-layout.tsx | 64 +++ frontend/apps/inspector/src/index.css | 78 ++++ frontend/apps/inspector/src/main.tsx | 18 + frontend/apps/inspector/src/routeTree.gen.ts | 83 ++++ frontend/apps/inspector/src/routes/__root.tsx | 29 ++ .../apps/inspector/src/routes/_context.tsx | 125 ++++++ .../inspector/src/routes/_context/index.tsx | 11 + frontend/apps/inspector/src/vite-env.d.ts | 9 + frontend/apps/inspector/tsconfig.json | 10 + frontend/apps/inspector/vite.config.ts | 49 +++ frontend/src/app.tsx | 2 - frontend/src/app/data-providers/cache.ts | 17 - frontend/src/app/layout.tsx | 104 +---- frontend/src/routes/__root.tsx | 5 - frontend/src/routes/_context.tsx | 40 +- frontend/src/routes/_context/index.tsx | 14 - frontend/src/vite-env.d.ts | 2 +- frontend/vite.inspector.config.ts | 23 +- 24 files changed, 1093 insertions(+), 200 deletions(-) create mode 100644 frontend/apps/inspector/index.html create mode 120000 frontend/apps/inspector/public create mode 100644 frontend/apps/inspector/src/app.tsx create mode 100644 frontend/apps/inspector/src/app/data-providers/cache.ts create mode 100644 frontend/apps/inspector/src/app/inspector-root.tsx create mode 100644 frontend/apps/inspector/src/app/layout.tsx create mode 100644 frontend/apps/inspector/src/app/route-layout.tsx create mode 100644 frontend/apps/inspector/src/index.css create mode 100644 frontend/apps/inspector/src/main.tsx create mode 100644 frontend/apps/inspector/src/routeTree.gen.ts create mode 100644 frontend/apps/inspector/src/routes/__root.tsx create mode 100644 frontend/apps/inspector/src/routes/_context.tsx create mode 100644 frontend/apps/inspector/src/routes/_context/index.tsx create mode 100644 frontend/apps/inspector/src/vite-env.d.ts create mode 100644 frontend/apps/inspector/tsconfig.json create mode 100644 frontend/apps/inspector/vite.config.ts diff --git a/frontend/apps/inspector/index.html b/frontend/apps/inspector/index.html new file mode 100644 index 0000000000..b9545d1a36 --- /dev/null +++ b/frontend/apps/inspector/index.html @@ -0,0 +1,32 @@ + + + + + + + + + Rivet Inspector + + +
+ + + + + diff --git a/frontend/apps/inspector/public b/frontend/apps/inspector/public new file mode 120000 index 0000000000..216461a002 --- /dev/null +++ b/frontend/apps/inspector/public @@ -0,0 +1 @@ +/Users/kwojciechowski/Documents/Projects/rivet.gg/rivet/frontend/public \ No newline at end of file diff --git a/frontend/apps/inspector/src/app.tsx b/frontend/apps/inspector/src/app.tsx new file mode 100644 index 0000000000..50ea391dc7 --- /dev/null +++ b/frontend/apps/inspector/src/app.tsx @@ -0,0 +1,83 @@ +import * as Sentry from "@sentry/react"; +import { QueryClientProvider } from "@tanstack/react-query"; +import { ReactQueryDevtools } from "@tanstack/react-query-devtools"; +import { createRouter, RouterProvider } from "@tanstack/react-router"; +import { Suspense } from "react"; +import { NotFoundCard } from "@/app/not-found-card"; +import { RouteLayout } from "@/app/route-layout"; +import { + ConfigProvider, + FullscreenLoading, + getConfig, + ThirdPartyProviders, + Toaster, + TooltipProvider, +} from "@/components"; +import { queryClient } from "@/queries/global"; +import { getOrCreateInspectorContext } from "./app/data-providers/cache"; +import { routeTree } from "./routeTree.gen"; + +declare module "@tanstack/react-router" { + interface Register { + router: typeof router; + } +} + +declare module "@tanstack/react-query" { + interface Register { + queryMeta: { + mightRequireAuth?: boolean; + statusCheck?: boolean; + reportType?: string; + }; + } +} + +export const router = createRouter({ + basepath: import.meta.env.BASE_URL, + routeTree, + context: { + queryClient: queryClient, + getOrCreateInspectorContext, + }, + defaultPreloadStaleTime: 0, + defaultGcTime: 0, + defaultPreloadGcTime: 0, + defaultStaleTime: Infinity, + scrollRestoration: true, + defaultPendingMinMs: 300, + defaultPendingComponent: FullscreenLoading, + defaultOnCatch: (error) => { + console.error("Router caught an error:", error); + Sentry.captureException(error); + }, + defaultNotFoundComponent: () => ( + + + + ), +}); + +function InnerApp() { + return ; +} + +export function App() { + return ( + + + + }> + + + + + + + + + + + + ); +} diff --git a/frontend/apps/inspector/src/app/data-providers/cache.ts b/frontend/apps/inspector/src/app/data-providers/cache.ts new file mode 100644 index 0000000000..968160e945 --- /dev/null +++ b/frontend/apps/inspector/src/app/data-providers/cache.ts @@ -0,0 +1,19 @@ +import { createGlobalContext as createGlobalInspectorContext } from "@/app/data-providers/inspector-data-provider"; + +export type InspectorContext = ReturnType; + +const inspectorContextCache = new Map(); + +export function getOrCreateInspectorContext(opts: { + url?: string; + token?: string; +}): InspectorContext { + const key = `${opts.url ?? ""}:${opts.token ?? ""}`; + const cached = inspectorContextCache.get(key); + if (cached) { + return cached; + } + const context = createGlobalInspectorContext(opts); + inspectorContextCache.set(key, context); + return context; +} diff --git a/frontend/apps/inspector/src/app/inspector-root.tsx b/frontend/apps/inspector/src/app/inspector-root.tsx new file mode 100644 index 0000000000..bb34aaf4d0 --- /dev/null +++ b/frontend/apps/inspector/src/app/inspector-root.tsx @@ -0,0 +1,70 @@ +import { CatchBoundary, useSearch } from "@tanstack/react-router"; +import { useEffect, useRef } from "react"; +import { Actors } from "@/app/actors"; +import { BuildPrefiller } from "@/app/build-prefiller"; +import { Connect } from "@/app/connect"; +import { getInspectorClientEndpoint } from "@/app/data-providers/inspector-data-provider"; +import { useInspectorContext } from "@/app/inspector-context"; +import { Logo } from "@/app/logo"; +import { askForLocalNetworkAccess } from "@/lib/permissions"; +import { RouteLayout } from "./route-layout"; + +export function InspectorRoot() { + const { isInspectorAvailable, connect } = useInspectorContext(); + const search = useSearch({ from: "/_context" }); + + const formRef = useRef(null); + + useEffect(() => { + formRef.current?.requestSubmit(); + }, []); + + if (isInspectorAvailable) { + return ( + + + search.n?.join(",") ?? "no-build-name"} + errorComponent={() => null} + > + {!search.n ? : null} + + + ); + } + + return ( +
+
+ + { + const hasLocalNetworkAccess = + await askForLocalNetworkAccess(); + + if (!hasLocalNetworkAccess) { + form.setError("url", { + message: + "Local network access is required to connect to local RivetKit. Please enable local network access in your browser settings and try again.", + }); + return; + } + + try { + const realUrl = await getInspectorClientEndpoint( + values.url, + ); + + await connect({ url: realUrl }); + } catch { + form.setError("url", { + message: "localhost.cors.error", + }); + } + }} + /> +
+
+ ); +} diff --git a/frontend/apps/inspector/src/app/layout.tsx b/frontend/apps/inspector/src/app/layout.tsx new file mode 100644 index 0000000000..07da8f7fa8 --- /dev/null +++ b/frontend/apps/inspector/src/app/layout.tsx @@ -0,0 +1,405 @@ +import { + faArrowUpRight, + faLink, + faLinkSlash, + faSpinnerThird, + Icon, +} from "@rivet-gg/icons"; +import { Link, useMatchRoute } from "@tanstack/react-router"; +import { + type ComponentProps, + createContext, + type PropsWithChildren, + type ReactNode, + type RefObject, + useContext, + useLayoutEffect, + useRef, + useState, +} from "react"; +import type { ImperativePanelGroupHandle } from "react-resizable-panels"; +import { ActorBuildsList } from "@/app/actor-builds-list"; +import { + useInspectorContext, + useInspectorEndpoint, + useInspectorStatus, +} from "@/app/inspector-context"; +import { + Button, + type ButtonProps, + cn, + type ImperativePanelHandle, + Ping, + ResizableHandle, + ResizablePanel, + ResizablePanelGroup, + ScrollArea, + WithTooltip, +} from "@/components"; +import { useRootLayoutOptional } from "@/components/actors/root-layout-context"; +import type { HeaderLinkProps } from "@/components/header/header-link"; +import { ensureTrailingSlash } from "@/lib/utils"; + +interface RootProps { + children: ReactNode; +} + +const Root = ({ children }: RootProps) => { + return
{children}
; +}; + +const Main = ({ + children, + ref, +}: RootProps & { ref?: RefObject }) => { + return ( + +
+ {children} +
+
+ ); +}; + +const SidebarDimensionsContext = createContext(0); +const SIDEBAR_MIN_WIDTH = 195; /* in px */ + +const VisibleInFull = ({ children }: PropsWithChildren) => { + const groupRef = useRef(null); + + const [sidebarMinWidth, setSidebarMinWidth] = useState(0); + + useLayoutEffect(() => { + const panelGroup = document.querySelector( + '[data-panel-group-id="root"]', + ); + const resizeHandles = panelGroup?.querySelectorAll( + "[data-panel-resize-handle-id]", + ); + + if (!panelGroup || !resizeHandles || resizeHandles?.length === 0) { + return; + } + + const observer = new ResizeObserver(() => { + let width = panelGroup.offsetWidth; + + resizeHandles.forEach((resizeHandle) => { + width -= resizeHandle.offsetWidth; + }); + + setSidebarMinWidth((SIDEBAR_MIN_WIDTH / width) * 100); + }); + observer.observe(panelGroup); + resizeHandles.forEach((resizeHandle) => { + observer.observe(resizeHandle); + }); + + return () => { + observer.unobserve(panelGroup); + resizeHandles.forEach((resizeHandle) => { + observer.unobserve(resizeHandle); + }); + observer.disconnect(); + }; + }, []); + + return ( + // biome-ignore lint/correctness/useUniqueElementIds: id its not html element id + + + {children} + + + ); +}; + +export const Logo = () => { + return ( + + Rivet.gg + + ); +}; + +const Sidebar = ({ + ref, + ...props +}: { + ref?: RefObject; +} & ComponentProps) => { + const sidebarMinWidth = useContext(SidebarDimensionsContext); + return ( + <> + +
+ +
+ + + + +
+
+
+
+ + + + Whats new? + + + + } + > + + Documentation + + + + } + > + + Discord + + + + } + > + + GitHub + + +
+
+
+ + + + ); +}; + +const Header = () => { + return null; +}; + +const Footer = () => { + return null; +}; + +export { Root, Main, Header, Footer, VisibleInFull, Sidebar }; + +const Subnav = () => { + const matchRoute = useMatchRoute(); + const nsMatch = matchRoute({ to: "/", fuzzy: true }); + + if (nsMatch === false) { + return null; + } + + return ( +
+
+ + Instances + + +
+
+ ); +}; + +function HeaderButton({ children, className, ...props }: ButtonProps) { + return ( + + ); +} + +function HeaderLink({ icon, children, className, ...props }: HeaderLinkProps) { + return ( + + ) : undefined + } + > + {children} + + ); +} + +// HeaderLink is exported so route-layout.tsx can use it if needed. +export { HeaderLink }; + +function ConnectionStatus(): ReactNode { + const endpoint = useInspectorEndpoint(); + const { disconnect } = useInspectorContext(); + const status = useInspectorStatus(); + + if (status === "reconnecting") { + return ( +
+
+

Connecting

+

{endpoint}

+
+ +
+ ); + } + + if (status === "disconnected") { + return ( +
+
+
+

Disconnected

+

+ {endpoint} +

+
+
+ + disconnect()} + > + + + } + content="Reconnect" + /> +
+ ); + } + + if (status === "connected") { + return ( +
+
+

Connected

+

{endpoint}

+
+ + disconnect()} + > + + + } + content="Disconnect" + /> +
+ ); + } + + return null; +} + +export const Content = ({ + className, + children, +}: { + className?: string; + children: ReactNode; +}) => { + const isInRootLayout = !!useRootLayoutOptional(); + const { isSidebarCollapsed } = useRootLayoutOptional() || {}; + return ( +
+ {children} +
+ ); +}; diff --git a/frontend/apps/inspector/src/app/route-layout.tsx b/frontend/apps/inspector/src/app/route-layout.tsx new file mode 100644 index 0000000000..46522bf767 --- /dev/null +++ b/frontend/apps/inspector/src/app/route-layout.tsx @@ -0,0 +1,64 @@ +import { Outlet } from "@tanstack/react-router"; +import { useRef, useState } from "react"; +import type { ImperativePanelHandle } from "react-resizable-panels"; +import { H2, Skeleton } from "@/components"; +import { RootLayoutContextProvider } from "@/components/actors/root-layout-context"; +import * as Layout from "./layout"; + +export function RouteLayout({ + children = , +}: { + children?: React.ReactNode; +}) { + const sidebarRef = useRef(null); + const [isSidebarCollapsed, setIsSidebarCollapsed] = useState(false); + + return ( + + + { + setIsSidebarCollapsed(true); + }} + onExpand={() => setIsSidebarCollapsed(false)} + /> + + + {children} + + + + + + ); +} + +export function PendingRouteLayout() { + return ( + +
+
+

+ +

+
+

+ +

+
+
+ +
+ + + +
+
+
+
+ ); +} diff --git a/frontend/apps/inspector/src/index.css b/frontend/apps/inspector/src/index.css new file mode 100644 index 0000000000..52aa134b3d --- /dev/null +++ b/frontend/apps/inspector/src/index.css @@ -0,0 +1,78 @@ +@import "../../../packages/components/public/theme.css"; + +@tailwind base; +@tailwind components; +@tailwind utilities; + +@layer base { + * { + @apply border-border; + } + body { + @apply bg-background text-foreground min-h-screen; + } + #root { + @apply min-h-screen; + } +} + +@layer utilities { + .bg-stripes { + background: repeating-linear-gradient( + 45deg, + /* biome-ignore lint/correctness/noUnknownFunction: tailwind functions */ + theme("colors.primary.DEFAULT" / 5%), + /* biome-ignore lint/correctness/noUnknownFunction: tailwind functions */ + theme("colors.primary.DEFAULT" / 5%) 20px, + transparent 20px, + transparent 40px + ); + } + + .bg-stripes-destructive { + background: repeating-linear-gradient( + 45deg, + /* biome-ignore lint/correctness/noUnknownFunction: tailwind functions */ + theme("colors.destructive.DEFAULT" / 5%), + /* biome-ignore lint/correctness/noUnknownFunction: tailwind functions */ + theme("colors.destructive.DEFAULT" / 5%) 20px, + transparent 20px, + transparent 40px + ); + } +} + +:root { + /* biome-ignore lint/correctness/noUnknownFunction: tailwind functions */ + --shiki-color-text: theme("colors.white"); + /* biome-ignore lint/correctness/noUnknownFunction: tailwind functions */ + --shiki-token-constant: theme("colors.violet.300"); + /* biome-ignore lint/correctness/noUnknownFunction: tailwind functions */ + --shiki-token-string: theme("colors.violet.300"); + /* biome-ignore lint/correctness/noUnknownFunction: tailwind functions */ + --shiki-token-comment: theme("colors.zinc.500"); + /* biome-ignore lint/correctness/noUnknownFunction: tailwind functions */ + --shiki-token-keyword: theme("colors.sky.300"); + /* biome-ignore lint/correctness/noUnknownFunction: tailwind functions */ + --shiki-token-parameter: theme("colors.pink.300"); + /* biome-ignore lint/correctness/noUnknownFunction: tailwind functions */ + --shiki-token-function: theme("colors.violet.300"); + /* biome-ignore lint/correctness/noUnknownFunction: tailwind functions */ + --shiki-token-string-expression: theme("colors.violet.300"); + /* biome-ignore lint/correctness/noUnknownFunction: tailwind functions */ + --shiki-token-punctuation: theme("colors.zinc.200"); + + --spacing: 0.25rem; + + ::-webkit-scrollbar { + @apply w-2.5 h-2.5; + } + + ::-webkit-scrollbar-track { + @apply bg-transparent; + } + + ::-webkit-scrollbar-thumb { + @apply rounded-full bg-border border-[1px] border-transparent border-solid bg-clip-padding; + } +} diff --git a/frontend/apps/inspector/src/main.tsx b/frontend/apps/inspector/src/main.tsx new file mode 100644 index 0000000000..67ac1337fb --- /dev/null +++ b/frontend/apps/inspector/src/main.tsx @@ -0,0 +1,18 @@ +import { StrictMode } from "react"; +import ReactDOM from "react-dom/client"; +import { App, router } from "./app"; +import "./index.css"; +import { initThirdPartyProviders } from "@/components"; + +initThirdPartyProviders(router, false); + +// biome-ignore lint/style/noNonNullAssertion: it should always be present +const rootElement = document.getElementById("root")!; +if (!rootElement.innerHTML) { + const root = ReactDOM.createRoot(rootElement); + root.render( + + + , + ); +} diff --git a/frontend/apps/inspector/src/routeTree.gen.ts b/frontend/apps/inspector/src/routeTree.gen.ts new file mode 100644 index 0000000000..a6b841e39e --- /dev/null +++ b/frontend/apps/inspector/src/routeTree.gen.ts @@ -0,0 +1,83 @@ +/* eslint-disable */ + +// @ts-nocheck + +// noinspection JSUnusedGlobalSymbols + +// This file was automatically generated by TanStack Router. +// You should NOT make any changes in this file as it will be overwritten. +// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified. + +import { Route as rootRouteImport } from './routes/__root' +import { Route as ContextRouteImport } from './routes/_context' +import { Route as ContextIndexRouteImport } from './routes/_context/index' + +const ContextRoute = ContextRouteImport.update({ + id: '/_context', + getParentRoute: () => rootRouteImport, +} as any) +const ContextIndexRoute = ContextIndexRouteImport.update({ + id: '/', + path: '/', + getParentRoute: () => ContextRoute, +} as any) + +export interface FileRoutesByFullPath { + '/': typeof ContextIndexRoute +} +export interface FileRoutesByTo { + '/': typeof ContextIndexRoute +} +export interface FileRoutesById { + __root__: typeof rootRouteImport + '/_context': typeof ContextRouteWithChildren + '/_context/': typeof ContextIndexRoute +} +export interface FileRouteTypes { + fileRoutesByFullPath: FileRoutesByFullPath + fullPaths: '/' + fileRoutesByTo: FileRoutesByTo + to: '/' + id: '__root__' | '/_context' | '/_context/' + fileRoutesById: FileRoutesById +} +export interface RootRouteChildren { + ContextRoute: typeof ContextRouteWithChildren +} + +declare module '@tanstack/react-router' { + interface FileRoutesByPath { + '/_context': { + id: '/_context' + path: '' + fullPath: '' + preLoaderRoute: typeof ContextRouteImport + parentRoute: typeof rootRouteImport + } + '/_context/': { + id: '/_context/' + path: '/' + fullPath: '/' + preLoaderRoute: typeof ContextIndexRouteImport + parentRoute: typeof ContextRoute + } + } +} + +interface ContextRouteChildren { + ContextIndexRoute: typeof ContextIndexRoute +} + +const ContextRouteChildren: ContextRouteChildren = { + ContextIndexRoute: ContextIndexRoute, +} + +const ContextRouteWithChildren = + ContextRoute._addFileChildren(ContextRouteChildren) + +const rootRouteChildren: RootRouteChildren = { + ContextRoute: ContextRouteWithChildren, +} +export const routeTree = rootRouteImport + ._addFileChildren(rootRouteChildren) + ._addFileTypes() diff --git a/frontend/apps/inspector/src/routes/__root.tsx b/frontend/apps/inspector/src/routes/__root.tsx new file mode 100644 index 0000000000..bd77e66bf8 --- /dev/null +++ b/frontend/apps/inspector/src/routes/__root.tsx @@ -0,0 +1,29 @@ +import type { QueryClient } from "@tanstack/react-query"; +import { createRootRouteWithContext, Outlet } from "@tanstack/react-router"; +import { TanStackRouterDevtools } from "@tanstack/react-router-devtools"; +import type { InspectorContext } from "@/app/data-providers/cache"; +import { FullscreenLoading } from "@/components"; + +function RootRoute() { + return ( + <> + + {import.meta.env.DEV ? ( + + ) : null} + + ); +} + +interface RootRouteContext { + queryClient: QueryClient; + getOrCreateInspectorContext: (opts: { + url?: string; + token?: string; + }) => InspectorContext; +} + +export const Route = createRootRouteWithContext()({ + component: RootRoute, + pendingComponent: FullscreenLoading, +}); diff --git a/frontend/apps/inspector/src/routes/_context.tsx b/frontend/apps/inspector/src/routes/_context.tsx new file mode 100644 index 0000000000..030e777391 --- /dev/null +++ b/frontend/apps/inspector/src/routes/_context.tsx @@ -0,0 +1,125 @@ +import { + createFileRoute, + isRedirect, + Outlet, + redirect, + useNavigate, +} from "@tanstack/react-router"; +import { zodValidator } from "@tanstack/zod-adapter"; +import z from "zod"; +import { getInspectorClientEndpoint } from "@/app/data-providers/inspector-data-provider"; +import { useDialog } from "@/components"; +import { ModalRenderer } from "@/components/modal-renderer"; + +const searchSchema = z + .object({ + modal: z + .enum([ + "go-to-actor", + "feedback", + "create-ns", + "create-project", + "billing", + ]) + .or(z.string()) + .optional(), + utm_source: z.string().optional(), + actorId: z.string().optional(), + tab: z.string().optional(), + n: z.array(z.string()).optional(), + u: z.string().optional(), + t: z.string().optional(), + }) + .and(z.record(z.string(), z.any())); + +export const Route = createFileRoute("/_context")({ + component: RouteComponent, + validateSearch: zodValidator(searchSchema), + context: ({ location: { search }, context }) => { + const typedSearch = search as z.infer; + return { + dataProvider: context.getOrCreateInspectorContext({ + url: typedSearch.u || "http://localhost:6420", + token: typedSearch.t, + }), + __type: "inspector" as const, + }; + }, + beforeLoad: async (route) => { + if (route.search.u) { + try { + const realUrl = await getInspectorClientEndpoint(route.search.u); + if (realUrl !== route.search.u) { + throw redirect({ + to: route.location.pathname, + search: { + ...route.search, + u: realUrl, + }, + }); + } + } catch (e) { + if (isRedirect(e)) { + throw e; + } + // Ignore errors here. + } + } + }, + loaderDeps: (route) => ({ token: route.search.t, url: route.search.u }), +}); + +function RouteComponent() { + return ( + <> + + + + + ); +} + +function Modals() { + const navigate = useNavigate(); + const search = Route.useSearch(); + + const CreateActorDialog = useDialog.CreateActor.Dialog; + const FeedbackDialog = useDialog.Feedback.Dialog; + + return ( + <> + { + if (!value) { + return navigate({ + to: ".", + search: (old) => ({ + ...old, + modal: undefined, + }), + }); + } + }, + }} + /> + { + if (!value) { + return navigate({ + to: ".", + search: (old) => ({ + ...old, + modal: undefined, + }), + }); + } + }, + }} + /> + + ); +} diff --git a/frontend/apps/inspector/src/routes/_context/index.tsx b/frontend/apps/inspector/src/routes/_context/index.tsx new file mode 100644 index 0000000000..263e19e881 --- /dev/null +++ b/frontend/apps/inspector/src/routes/_context/index.tsx @@ -0,0 +1,11 @@ +import { createFileRoute } from "@tanstack/react-router"; +import { InspectorContextProvider } from "@/app/inspector-context"; +import { InspectorRoot } from "@/app/inspector-root"; + +export const Route = createFileRoute("/_context/")({ + component: () => ( + + + + ), +}); diff --git a/frontend/apps/inspector/src/vite-env.d.ts b/frontend/apps/inspector/src/vite-env.d.ts new file mode 100644 index 0000000000..6e5c466242 --- /dev/null +++ b/frontend/apps/inspector/src/vite-env.d.ts @@ -0,0 +1,9 @@ +/// + +declare const __APP_BUILD_ID__: string; +declare const __APP_TYPE__: "engine" | "inspector" | "cloud"; + +declare module "*.module.css" { + const classes: { [key: string]: string }; + export default classes; +} diff --git a/frontend/apps/inspector/tsconfig.json b/frontend/apps/inspector/tsconfig.json new file mode 100644 index 0000000000..6d4d3f38c6 --- /dev/null +++ b/frontend/apps/inspector/tsconfig.json @@ -0,0 +1,10 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "baseUrl": ".", + "paths": { + "@/*": ["./src/*", "../../src/*"] + } + }, + "include": ["src", "vite.config.ts"] +} diff --git a/frontend/apps/inspector/vite.config.ts b/frontend/apps/inspector/vite.config.ts new file mode 100644 index 0000000000..0c38cbec60 --- /dev/null +++ b/frontend/apps/inspector/vite.config.ts @@ -0,0 +1,49 @@ +import * as crypto from "node:crypto"; +import path from "node:path"; +import { tanstackRouter } from "@tanstack/router-plugin/vite"; +import react from "@vitejs/plugin-react"; +import { defineConfig } from "vite"; +import tsconfigPaths from "vite-tsconfig-paths"; + +// https://vitejs.dev/config/ +export default defineConfig({ + root: path.resolve(__dirname), + base: "/", + envDir: path.resolve(__dirname, "../.."), + plugins: [ + tanstackRouter({ + target: "react", + autoCodeSplitting: true, + routesDirectory: path.resolve(__dirname, "src/routes"), + generatedRouteTree: path.resolve(__dirname, "src/routeTree.gen.ts"), + }), + react(), + tsconfigPaths(), + ], + define: { + __APP_TYPE__: JSON.stringify("inspector"), + __APP_BUILD_ID__: JSON.stringify( + `${new Date().toISOString()}@${crypto.randomUUID()}`, + ), + }, + optimizeDeps: { + include: ["@fortawesome/*", "@rivet-gg/icons", "@rivet-gg/cloud"], + }, + worker: { + format: "es", + }, + server: { + port: 43709, + proxy: {}, + }, + preview: { + port: 43709, + }, + build: { + outDir: "../../dist/inspector", + sourcemap: true, + commonjsOptions: { + include: [/@rivet-gg\/components/, /node_modules/], + }, + }, +}); diff --git a/frontend/src/app.tsx b/frontend/src/app.tsx index 578621d77f..de2caab47d 100644 --- a/frontend/src/app.tsx +++ b/frontend/src/app.tsx @@ -17,7 +17,6 @@ import { getOrCreateCloudNamespaceContext, getOrCreateEngineContext, getOrCreateEngineNamespaceContext, - getOrCreateInspectorContext, getOrCreateOrganizationContext, getOrCreateProjectContext, } from "./app/data-providers/cache"; @@ -52,7 +51,6 @@ export const router = createRouter({ queryClient: queryClient, getOrCreateCloudContext, getOrCreateEngineContext, - getOrCreateInspectorContext, getOrCreateOrganizationContext, getOrCreateProjectContext, getOrCreateCloudNamespaceContext, diff --git a/frontend/src/app/data-providers/cache.ts b/frontend/src/app/data-providers/cache.ts index b3235f2412..f1ae90d5f0 100644 --- a/frontend/src/app/data-providers/cache.ts +++ b/frontend/src/app/data-providers/cache.ts @@ -9,7 +9,6 @@ import { createNamespaceContext as createEngineNamespaceContext, createGlobalContext as createGlobalEngineContext, } from "@/app/data-providers/engine-data-provider"; -import { createGlobalContext as createGlobalInspectorContext } from "@/app/data-providers/inspector-data-provider"; // Cache factories for data providers to maintain stable references across navigation export type CloudContext = ReturnType; @@ -20,7 +19,6 @@ export type EngineContext = ReturnType; export type EngineNamespaceContext = ReturnType< typeof createEngineNamespaceContext >; -export type InspectorContext = ReturnType; export type OrganizationContext = ReturnType; export type ProjectContext = ReturnType; @@ -28,7 +26,6 @@ let cloudContextCache: CloudContext | null = null; const cloudNamespaceContextCache = new Map(); const engineContextCache = new Map(); const engineNamespaceContextCache = new Map(); -const inspectorContextCache = new Map(); const organizationContextCache = new Map(); const projectContextCache = new Map(); @@ -55,20 +52,6 @@ export function getOrCreateEngineContext( return context; } -export function getOrCreateInspectorContext(opts: { - url?: string; - token?: string; -}): InspectorContext { - const key = `${opts.url ?? ""}:${opts.token ?? ""}`; - const cached = inspectorContextCache.get(key); - if (cached) { - return cached; - } - const context = createGlobalInspectorContext(opts); - inspectorContextCache.set(key, context); - return context; -} - export function getOrCreateOrganizationContext( parent: CloudContext, organization: string, diff --git a/frontend/src/app/layout.tsx b/frontend/src/app/layout.tsx index 4a6436c611..029140af57 100644 --- a/frontend/src/app/layout.tsx +++ b/frontend/src/app/layout.tsx @@ -4,10 +4,7 @@ import { faCog, faGift, faHome, - faLink, - faLinkSlash, faMessageSmile, - faSpinnerThird, faWallet, Icon, } from "@rivet-gg/icons"; @@ -50,11 +47,6 @@ import { BillingUsageGauge } from "./billing/billing-usage-gauge"; import { Changelog } from "./changelog"; import { ContextSwitcher } from "./context-switcher"; import { HelpDropdown } from "./help-dropdown"; -import { - useInspectorContext, - useInspectorEndpoint, - useInspectorStatus, -} from "./inspector-context"; import { NamespaceSelect } from "./namespace-select"; import { UserDropdown } from "./user-dropdown"; @@ -171,14 +163,6 @@ const Sidebar = ({
{match(__APP_TYPE__) - .with("inspector", () => ( - <> - - - - - - )) .with("engine", () => ( <> @@ -476,14 +460,10 @@ const NamespaceBreadcrumbs = ({ const Subnav = () => { const matchRoute = useMatchRoute(); - const nsMatch = matchRoute( - __APP_TYPE__ === "engine" - ? { - to: "/ns/$namespace", - fuzzy: true, - } - : { to: "/", fuzzy: true }, - ); + const nsMatch = matchRoute({ + to: "/ns/$namespace", + fuzzy: true, + }); if (nsMatch === false) { return null; @@ -549,82 +529,6 @@ function HeaderButton({ children, className, ...props }: ButtonProps) { ); } -function ConnectionStatus(): ReactNode { - const endpoint = useInspectorEndpoint(); - - const { disconnect } = useInspectorContext(); - const status = useInspectorStatus(); - - if (status === "reconnecting") { - return ( -
-
-

Connecting

-

{endpoint}

-
- -
- ); - } - - if (status === "disconnected") { - return ( -
-
-
-

Disconnected

-

- {endpoint} -

-
-
- - disconnect()} - > - - - } - content="Reconnect" - /> -
- ); - } - - if (status === "connected") { - return ( -
-
-

Connected

-

{endpoint}

-
- - disconnect()} - > - - - } - content="Disconnect" - /> -
- ); - } - - return null; -} function CloudSidebar(): ReactNode { return ( diff --git a/frontend/src/routes/__root.tsx b/frontend/src/routes/__root.tsx index bcd96a4a37..bea7d11387 100644 --- a/frontend/src/routes/__root.tsx +++ b/frontend/src/routes/__root.tsx @@ -14,7 +14,6 @@ import type { CloudNamespaceContext, EngineContext, EngineNamespaceContext, - InspectorContext, OrganizationContext, ProjectContext, } from "@/app/data-providers/cache"; @@ -82,10 +81,6 @@ interface RootRouteContext { getOrCreateEngineContext: ( engineToken: (() => string) | string | (() => Promise), ) => EngineContext; - getOrCreateInspectorContext: (opts: { - url?: string; - token?: string; - }) => InspectorContext; getOrCreateOrganizationContext: ( parent: CloudContext, organization: string, diff --git a/frontend/src/routes/_context.tsx b/frontend/src/routes/_context.tsx index 7c01c00a40..a636579099 100644 --- a/frontend/src/routes/_context.tsx +++ b/frontend/src/routes/_context.tsx @@ -1,6 +1,5 @@ import { createFileRoute, - isRedirect, Outlet, redirect, useNavigate, @@ -8,7 +7,6 @@ import { import { zodValidator } from "@tanstack/zod-adapter"; import { match } from "ts-pattern"; import z from "zod"; -import { getInspectorClientEndpoint } from "@/app/data-providers/inspector-data-provider"; import { getConfig, ls, useDialog } from "@/components"; import { ModalRenderer } from "@/components/modal-renderer"; import { waitForClerk } from "@/lib/waitForClerk"; @@ -29,8 +27,6 @@ const searchSchema = z actorId: z.string().optional(), tab: z.string().optional(), n: z.array(z.string()).optional(), - u: z.string().optional(), - t: z.string().optional(), // clerk related __clerk_ticket: z.string().optional(), __clerk_status: z.string().optional(), @@ -40,7 +36,7 @@ const searchSchema = z export const Route = createFileRoute("/_context")({ component: RouteComponent, validateSearch: zodValidator(searchSchema), - context: ({ location: { search }, context }) => { + context: ({ context }) => { return match(__APP_TYPE__) .with("engine", () => ({ dataProvider: context.getOrCreateEngineContext( @@ -52,16 +48,6 @@ export const Route = createFileRoute("/_context")({ dataProvider: context.getOrCreateCloudContext(context.clerk), __type: "cloud" as const, })) - .with("inspector", () => { - const typedSearch = search as z.infer; - return { - dataProvider: context.getOrCreateInspectorContext({ - url: typedSearch.u || "http://localhost:6420", - token: typedSearch.t, - }), - __type: "inspector" as const, - }; - }) .exhaustive(); }, beforeLoad: async (route) => { @@ -89,32 +75,8 @@ export const Route = createFileRoute("/_context")({ }); } }) - .with({ __type: "inspector" }, () => async () => { - if (route.search.u) { - try { - const realUrl = await getInspectorClientEndpoint( - route.search.u, - ); - if (realUrl !== route.search.u) { - throw redirect({ - to: route.location.pathname, - search: { - ...route.search, - u: realUrl, - }, - }); - } - } catch (e) { - if (isRedirect(e)) { - throw e; - } - // ignore errors here - } - } - }) .otherwise(() => () => {})(); }, - loaderDeps: (route) => ({ token: route.search.t, url: route.search.u }), }); function RouteComponent() { diff --git a/frontend/src/routes/_context/index.tsx b/frontend/src/routes/_context/index.tsx index e80b3bfb31..f604aa96f6 100644 --- a/frontend/src/routes/_context/index.tsx +++ b/frontend/src/routes/_context/index.tsx @@ -1,8 +1,6 @@ import { createFileRoute, isRedirect, redirect } from "@tanstack/react-router"; import { match } from "ts-pattern"; import CreateNamespacesFrameContent from "@/app/dialogs/create-namespace-frame"; -import { InspectorContextProvider } from "@/app/inspector-context"; -import { InspectorRoot } from "@/app/inspector-root"; import { Logo } from "@/app/logo"; import { Card } from "@/components"; import { redirectToOrganization } from "@/lib/auth"; @@ -12,7 +10,6 @@ export const Route = createFileRoute("/_context/")({ match(__APP_TYPE__) .with("cloud", () => ) .with("engine", () => ) - .with("inspector", () => ) .exhaustive(), beforeLoad: async ({ context, search }) => { return await match(context) @@ -49,9 +46,6 @@ export const Route = createFileRoute("/_context/")({ return; } }) - .with({ __type: "inspector" }, async () => { - return {}; - }) .exhaustive(); }, }); @@ -72,11 +66,3 @@ function EngineRoute() {
); } - -function InspectorRoute() { - return ( - - - - ); -} diff --git a/frontend/src/vite-env.d.ts b/frontend/src/vite-env.d.ts index f6aabeedf7..86d01374b3 100644 --- a/frontend/src/vite-env.d.ts +++ b/frontend/src/vite-env.d.ts @@ -1,7 +1,7 @@ /// declare const __APP_BUILD_ID__: string; -declare const __APP_TYPE__: "engine" | "inspector" | "cloud"; +declare const __APP_TYPE__: "engine" | "cloud"; declare const Plain: { // This takes the same arguments as Plain.init. It will update the chat widget in-place with the new configuration. // Only top-level fields are updated, nested fields are not merged. diff --git a/frontend/vite.inspector.config.ts b/frontend/vite.inspector.config.ts index 046df729c6..b0bd40caab 100644 --- a/frontend/vite.inspector.config.ts +++ b/frontend/vite.inspector.config.ts @@ -1,22 +1 @@ -import { defineConfig, mergeConfig } from "vite"; -import engineConfig from "./vite.engine.config"; - -// https://vitejs.dev/config/ -export default defineConfig((args) => { - return mergeConfig( - engineConfig(args), - defineConfig({ - base: "/", - define: { - __APP_TYPE__: JSON.stringify("inspector"), - }, - server: { - port: 43709, - proxy: {}, - }, - preview: { - port: 43709, - }, - }), - ); -}); +export { default } from "./apps/inspector/vite.config";