diff --git a/frontend/index.html b/frontend/index.html index 5dc2bb52ef..16976ff05f 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -36,7 +36,8 @@ "sentryDsn": "%VITE_APP_SENTRY_DSN%", "sentry": { "dsn": "%VITE_APP_SENTRY_DSN%", - "projectId": "%VITE_APP_SENTRY_PROJECT_ID%" + "projectId": "%VITE_APP_SENTRY_PROJECT_ID%", + "tunnel": "%VITE_APP_SENTRY_TUNNEL%" }, "posthog": { "apiKey": "%VITE_APP_POSTHOG_API_KEY%", diff --git a/frontend/packages/components/src/lib/config.ts b/frontend/packages/components/src/lib/config.ts index 2131a0d0fc..bd02d6e968 100644 --- a/frontend/packages/components/src/lib/config.ts +++ b/frontend/packages/components/src/lib/config.ts @@ -11,6 +11,7 @@ interface Config { sentry?: { dsn: string; projectId: string; + tunnel?: string; }; outerbaseProviderToken: string; } diff --git a/frontend/packages/components/src/third-party-providers.tsx b/frontend/packages/components/src/third-party-providers.tsx index 93356b620a..b823450cad 100644 --- a/frontend/packages/components/src/third-party-providers.tsx +++ b/frontend/packages/components/src/third-party-providers.tsx @@ -35,6 +35,7 @@ export function initThirdPartyProviders(router: unknown, debug: boolean) { Sentry.init({ dsn: config.sentry.dsn, integrations, + tunnel: getConfig().sentry?.tunnel, }); } } diff --git a/frontend/src/app/data-providers/cloud-data-provider.tsx b/frontend/src/app/data-providers/cloud-data-provider.tsx index 2068702ffd..87dbeabc4c 100644 --- a/frontend/src/app/data-providers/cloud-data-provider.tsx +++ b/frontend/src/app/data-providers/cloud-data-provider.tsx @@ -357,6 +357,9 @@ export const createNamespaceContext = ({ namespaceQueryOptions() { return parent.currentProjectNamespaceQueryOptions({ namespace }); }, + currentNamespaceAccessTokenQueryOptions() { + return parent.accessTokenQueryOptions({ namespace }); + }, engineAdminTokenQueryOptions() { return queryOptions({ staleTime: 5 * 60 * 1000, // 5 minutes diff --git a/frontend/src/app/data-providers/engine-data-provider.tsx b/frontend/src/app/data-providers/engine-data-provider.tsx index 0ebd8ae5fe..59c71c7a3c 100644 --- a/frontend/src/app/data-providers/engine-data-provider.tsx +++ b/frontend/src/app/data-providers/engine-data-provider.tsx @@ -469,16 +469,13 @@ export const createNamespaceContext = ({ }, }); }, - runnerByNameQueryOptions(opts: { - namespace: string; - runnerName: string; - }) { + runnerByNameQueryOptions(opts: { runnerName: string }) { return queryOptions({ - queryKey: [opts.namespace, "runner", opts.runnerName], + queryKey: [{ namespace }, "runner", opts.runnerName], enabled: !!opts.runnerName, queryFn: async ({ signal: abortSignal }) => { const data = await client.runners.list( - { namespace: opts.namespace, name: opts.runnerName }, + { namespace, name: opts.runnerName }, { abortSignal, }, diff --git a/frontend/src/components/actors/actor-queries-context.tsx b/frontend/src/components/actors/actor-queries-context.tsx index 4b9d7bf198..44479f989e 100644 --- a/frontend/src/components/actors/actor-queries-context.tsx +++ b/frontend/src/components/actors/actor-queries-context.tsx @@ -5,7 +5,7 @@ import type { ActorId } from "./queries"; type RequestOptions = Parameters[1]; -export const defaultActorContext = { +export const createDefaultActorContext = ({ hash }: { hash: string } = {}) => ({ createActorInspectorFetchConfiguration: ( actorId: ActorId | string, ): RequestOptions => ({ @@ -32,7 +32,7 @@ export const defaultActorContext = { enabled: false, refetchInterval: 1000, ...opts, - queryKey: ["actor", actorId, "ping"], + queryKey: [hash, "actor", actorId, "ping"], queryFn: async ({ queryKey: [, actorId] }) => { const client = this.createActorInspector(actorId); const response = await client.ping.$get(); @@ -51,7 +51,7 @@ export const defaultActorContext = { return queryOptions({ enabled, refetchInterval: 1000, - queryKey: ["actor", actorId, "state"], + queryKey: [hash, "actor", actorId, "state"], queryFn: async ({ queryKey: [, actorId] }) => { const client = this.createActorInspector(actorId); const response = await client.state.$get(); @@ -74,7 +74,7 @@ export const defaultActorContext = { return queryOptions({ enabled, refetchInterval: 1000, - queryKey: ["actor", actorId, "connections"], + queryKey: [hash, "actor", actorId, "connections"], queryFn: async ({ queryKey: [, actorId] }) => { const client = this.createActorInspector(actorId); const response = await client.connections.$get(); @@ -93,7 +93,7 @@ export const defaultActorContext = { ) { return queryOptions({ enabled, - queryKey: ["actor", actorId, "database"], + queryKey: [hash, "actor", actorId, "database"], queryFn: async ({ queryKey: [, actorId] }) => { const client = this.createActorInspector(actorId); const response = await client.db.$get(); @@ -142,7 +142,7 @@ export const defaultActorContext = { enabled, staleTime: 0, gcTime: 5000, - queryKey: ["actor", actorId, "database", table], + queryKey: [hash, "actor", actorId, "database", table], queryFn: async ({ queryKey: [, actorId, , table] }) => { const client = this.createActorInspector(actorId); const response = await client.db.$post({ @@ -163,7 +163,7 @@ export const defaultActorContext = { return queryOptions({ enabled, refetchInterval: 1000, - queryKey: ["actor", actorId, "events"], + queryKey: [hash, "actor", actorId, "events"], queryFn: async ({ queryKey: [, actorId] }) => { const client = this.createActorInspector(actorId); const response = await client.events.$get(); @@ -182,7 +182,7 @@ export const defaultActorContext = { ) { return queryOptions({ enabled, - queryKey: ["actor", actorId, "rpcs"], + queryKey: [hash, "actor", actorId, "rpcs"], queryFn: async ({ queryKey: [, actorId] }) => { const client = this.createActorInspector(actorId); const response = await client.rpcs.$get(); @@ -197,7 +197,7 @@ export const defaultActorContext = { actorClearEventsMutationOptions(actorId: ActorId) { return { - mutationKey: ["actor", actorId, "clear-events"], + mutationKey: [hash, "actor", actorId, "clear-events"], mutationFn: async () => { const client = this.createActorInspector(actorId); const response = await client.events.clear.$post(); @@ -211,7 +211,7 @@ export const defaultActorContext = { actorWakeUpMutationOptions(actorId: ActorId) { return { - mutationKey: ["actor", actorId, "wake-up"], + mutationKey: [hash, "actor", actorId, "wake-up"], mutationFn: async () => { const client = this.createActorInspector(actorId); try { @@ -233,7 +233,7 @@ export const defaultActorContext = { refetchInterval: 1000, staleTime: 0, gcTime: 0, - queryKey: ["actor", actorId, "auto-wake-up"], + queryKey: [hash, "actor", actorId, "auto-wake-up"], queryFn: async ({ queryKey: [, actorId] }) => { const client = this.createActorInspector(actorId); try { @@ -246,13 +246,9 @@ export const defaultActorContext = { retry: false, }); }, -}; +}); -export type ActorContext = typeof defaultActorContext; - -export function createDefaultActorContext(): ActorContext { - return defaultActorContext; -} +export type ActorContext = ReturnType; const ActorContext = createContext({} as ActorContext); diff --git a/frontend/src/components/actors/actors-list-row.tsx b/frontend/src/components/actors/actors-list-row.tsx index 09cea8963e..8ee25f7f0d 100644 --- a/frontend/src/components/actors/actors-list-row.tsx +++ b/frontend/src/components/actors/actors-list-row.tsx @@ -64,7 +64,7 @@ export const ActorsListRow = memo( /> } /> -
+
@@ -128,11 +128,13 @@ function Datacenter({ actorId }: { actorId: ActorId }) { } if (!datacenter) { - return -; + return -; } return ( - {datacenter} + + {datacenter} + ); } diff --git a/frontend/src/components/actors/guard-connectable-inspector.tsx b/frontend/src/components/actors/guard-connectable-inspector.tsx index 1fa782acd6..e720138d59 100644 --- a/frontend/src/components/actors/guard-connectable-inspector.tsx +++ b/frontend/src/components/actors/guard-connectable-inspector.tsx @@ -1,3 +1,4 @@ +/** biome-ignore-all lint/correctness/useHookAtTopLevel: safe guarded by build consts */ import { faPowerOff, faSpinnerThird, Icon } from "@rivet-gg/icons"; import { useInfiniteQuery, @@ -5,8 +6,9 @@ import { useQuery, useSuspenseQuery, } from "@tanstack/react-query"; -import { useMatch } from "@tanstack/react-router"; +import { useMatch, useRouteContext } from "@tanstack/react-router"; import { createContext, type ReactNode, useContext, useMemo } from "react"; +import { useLocalStorage } from "usehooks-ts"; import { useInspectorCredentials } from "@/app/credentials-context"; import { createInspectorActorContext } from "@/queries/actor-inspector"; import { DiscreteCopyButton } from "../copy-area"; @@ -32,19 +34,17 @@ export function GuardConnectableInspector({ actorId, children, }: GuardConnectableInspectorProps) { - const filters = useFiltersValue({ includeEphemeral: true }); - const { - data: { destroyedAt, sleepingAt, pendingAllocationAt, startedAt } = {}, - } = useQuery({ - ...useDataProvider().actorQueryOptions(actorId), - refetchInterval: 1000, - select: (data) => ({ - destroyedAt: data.destroyedAt, - sleepingAt: data.sleepingAt, - pendingAllocationAt: data.pendingAllocationAt, - startedAt: data.startedAt, - }), - }); + const { data: { destroyedAt, pendingAllocationAt, startedAt } = {} } = + useQuery({ + ...useDataProvider().actorQueryOptions(actorId), + refetchInterval: 1000, + select: (data) => ({ + destroyedAt: data.destroyedAt, + sleepingAt: data.sleepingAt, + pendingAllocationAt: data.pendingAllocationAt, + startedAt: data.startedAt, + }), + }); if (destroyedAt) { return ( @@ -56,34 +56,6 @@ export function GuardConnectableInspector({ ); } - if (sleepingAt) { - if (filters.wakeOnSelect?.value?.[0] === "1") { - return ( - - - - } - > - {children} - - ); - } - return ( - -

Unavailable for sleeping Actors.

- - - } - > - {children} -
- ); - } - if (pendingAllocationAt && !startedAt) { return ( }> @@ -187,7 +159,6 @@ function useActorRunner({ actorId }: { actorId: ActorId }) { } = useQuery({ ...useEngineCompatDataProvider().runnerByNameQueryOptions({ runnerName: actor.runner, - namespace: match.params.namespace, }), retryDelay: 10_000, refetchInterval: 1000, @@ -201,16 +172,36 @@ function useActorRunner({ actorId }: { actorId: ActorId }) { }; } +function useEngineToken() { + if (__APP_TYPE__ === "cloud") { + const { data } = useQuery( + useRouteContext({ + from: "/_context/_cloud/orgs/$organization/projects/$project/ns/$namespace", + }).dataProvider.publishableTokenQueryOptions(), + ); + return data; + } + const [data] = useLocalStorage( + ls.engineCredentials.key(getConfig().apiUrl), + "", + { serializer: JSON.stringify, deserializer: JSON.parse }, + ); + return data; +} + function useActorEngineContext({ actorId }: { actorId: ActorId }) { const { actor, runner, isLoading } = useActorRunner({ actorId }); + const engineToken = useEngineToken(); const actorContext = useMemo(() => { - return createInspectorActorContext({ - url: getConfig().apiUrl, - token: (runner?.metadata?.inspectorToken as string) || "", - engineToken: ls.engineCredentials.get(getConfig().apiUrl) || "", - }); - }, [runner?.metadata?.inspectorToken]); + return engineToken + ? createInspectorActorContext({ + url: getConfig().apiUrl, + token: (runner?.metadata?.inspectorToken as string) || "", + engineToken, + }) + : null; + }, [runner?.metadata?.inspectorToken, engineToken]); return { actorContext, actor, runner, isLoading }; } @@ -240,6 +231,24 @@ function ActorEngineProvider({ ); } + if (!actorContext) { + return ( + +

Unable to connect to the Actor's Inspector.

+

+ Check that your application is running and that your + network allows connections to the Inspector URL. +

+ + } + > + {children} +
+ ); + } + return ( {children} @@ -267,7 +276,8 @@ function NoRunnerInfo({ runner }: { runner: string }) { } function WakeUpActorButton({ actorId }: { actorId: ActorId }) { - const { runner, actorContext } = useActorEngineContext({ actorId }); + const actorContext = useActor(); + const { runner } = useActorRunner({ actorId }); const { mutate, isPending } = useMutation( actorContext.actorWakeUpMutationOptions(actorId), @@ -287,9 +297,8 @@ function WakeUpActorButton({ actorId }: { actorId: ActorId }) { } function AutoWakeUpActor({ actorId }: { actorId: ActorId }) { - const { runner, actor, actorContext } = useActorEngineContext({ - actorId, - }); + const actorContext = useActor(); + const { actor, runner } = useActorRunner({ actorId }); useQuery( actorContext.actorAutoWakeUpQueryOptions(actorId, { @@ -315,12 +324,63 @@ function InspectorGuard({ }: { actorId: ActorId; children: ReactNode; +}) { + const filters = useFiltersValue({ includeEphemeral: true }); + + const { data: { sleepingAt } = {} } = useQuery({ + ...useDataProvider().actorQueryOptions(actorId), + refetchInterval: 1000, + select: (data) => ({ + destroyedAt: data.destroyedAt, + sleepingAt: data.sleepingAt, + pendingAllocationAt: data.pendingAllocationAt, + startedAt: data.startedAt, + }), + }); + + if (sleepingAt) { + if (filters.wakeOnSelect?.value?.[0] === "1") { + return ( + + + + } + > + {children} + + ); + } + return ( + +

Unavailable for sleeping Actors.

+ + + } + > + {children} +
+ ); + } + return ( + {children} + ); +} + +function InspectorGuardInner({ + actorId, + children, +}: { + actorId: ActorId; + children: ReactNode; }) { const { isError } = useQuery({ ...useActor().actorPingQueryOptions(actorId), enabled: true, }); - if (isError) { return ( ); } + return children; } diff --git a/frontend/src/components/lib/utils.ts b/frontend/src/components/lib/utils.ts index 0279c02bc8..a56ac55a4e 100644 --- a/frontend/src/components/lib/utils.ts +++ b/frontend/src/components/lib/utils.ts @@ -25,6 +25,7 @@ export const ls = { localStorage.clear(); }, engineCredentials: { + key: (url: string) => btoa(`engine-credentials-${JSON.stringify(url)}`), set: (url: string, token: string) => { ls.set( btoa(`engine-credentials-${JSON.stringify(url)}`), diff --git a/frontend/src/queries/actor-inspector.ts b/frontend/src/queries/actor-inspector.ts index 6dd8ab9dc4..4b842a4590 100644 --- a/frontend/src/queries/actor-inspector.ts +++ b/frontend/src/queries/actor-inspector.ts @@ -13,7 +13,9 @@ export const createInspectorActorContext = ({ token: string; engineToken?: string; }) => { - const def = createDefaultActorContext(); + const def = createDefaultActorContext({ + hash: btoa(url + inspectorToken + (engineToken || "")).slice(0, 8), + }); const newUrl = new URL(url); if (!newUrl.pathname.endsWith("inspect")) { newUrl.pathname = `${ensureTrailingSlash(newUrl.pathname)}inspect`;