diff --git a/frontend/src/components/actors/actor-filters-context.tsx b/frontend/src/components/actors/actor-filters-context.tsx index 22d0c46507..c6a67b77a7 100644 --- a/frontend/src/components/actors/actor-filters-context.tsx +++ b/frontend/src/components/actors/actor-filters-context.tsx @@ -1,22 +1,14 @@ import { faHashtag, faKey } from "@rivet-gg/icons"; -import { useQuery } from "@tanstack/react-query"; import { useSearch } from "@tanstack/react-router"; -import { CommandGroup, CommandItem } from "cmdk"; import { createContext, useContext } from "react"; -import { cn } from "../lib/utils"; -import { Checkbox } from "../ui/checkbox"; import { createFiltersPicker, createFiltersRemover, createFiltersSchema, type FilterDefinitions, FilterOp, - type OptionsProviderProps, + type PickFiltersOptions, } from "../ui/filters"; -import { ActorRegion } from "./actor-region"; -import { ActorStatus } from "./actor-status"; -import { useManager } from "./manager-context"; -import type { ActorStatus as ActorStatusType } from "./queries"; export const ACTORS_FILTERS_DEFINITIONS = { id: { @@ -48,6 +40,13 @@ export const ACTORS_FILTERS_DEFINITIONS = { category: "display", ephemeral: true, }, + wakeOnSelect: { + type: "boolean", + label: "Auto-wake Actors on select", + category: "display", + ephemeral: true, + defaultValue: ["1"], + }, // tags: { // type: "select", // label: "Tags", @@ -143,3 +142,11 @@ export const useFilters = ( select: (state) => fn(pick(state)), }); }; + +export function useFiltersValue(opts: PickFiltersOptions = {}) { + const { pick } = useActorsFilters(); + return useSearch({ + from: "/_layout", + select: (state) => pick(state, opts), + }); +} diff --git a/frontend/src/components/actors/actor-queries-context.tsx b/frontend/src/components/actors/actor-queries-context.tsx index df74b02f2e..63a1d2b8df 100644 --- a/frontend/src/components/actors/actor-queries-context.tsx +++ b/frontend/src/components/actors/actor-queries-context.tsx @@ -208,6 +208,44 @@ export const defaultActorContext = { }, }; }, + + actorWakeUpMutationOptions(actorId: ActorId) { + return { + mutationKey: ["actor", actorId, "wake-up"], + mutationFn: async () => { + const client = this.createActorInspector(actorId); + try { + await client.ping.$get(); + return true; + } catch { + return false; + } + }, + }; + }, + + actorAutoWakeUpQueryOptions( + actorId: ActorId, + { enabled }: { enabled?: boolean } = {}, + ) { + return queryOptions({ + enabled, + refetchInterval: 1000, + staleTime: 0, + gcTime: 0, + queryKey: ["actor", actorId, "auto-wake-up"], + queryFn: async ({ queryKey: [, actorId] }) => { + const client = this.createActorInspector(actorId); + try { + await client.ping.$get(); + return true; + } catch { + return false; + } + }, + retry: false, + }); + }, }; export type ActorContext = typeof defaultActorContext; diff --git a/frontend/src/components/actors/actor-state-tab.tsx b/frontend/src/components/actors/actor-state-tab.tsx index af3ce20cff..0cdb436b6b 100644 --- a/frontend/src/components/actors/actor-state-tab.tsx +++ b/frontend/src/components/actors/actor-state-tab.tsx @@ -5,7 +5,6 @@ import { Button } from "../ui/button"; import { ActorEditableState } from "./actor-editable-state"; import { useActor } from "./actor-queries-context"; import { useActorsView } from "./actors-view-context-provider"; -import { useManager } from "./manager-context"; import type { ActorId } from "./queries"; interface ActorStateTabProps { @@ -13,10 +12,6 @@ interface ActorStateTabProps { } export function ActorStateTab({ actorId }: ActorStateTabProps) { - const { data: destroyedAt } = useQuery( - useManager().actorDestroyedAtQueryOptions(actorId), - ); - const { links } = useActorsView(); const actorQueries = useActor(); @@ -24,13 +19,7 @@ export function ActorStateTab({ actorId }: ActorStateTabProps) { data: state, isError, isLoading, - } = useQuery( - actorQueries.actorStateQueryOptions(actorId, { enabled: !destroyedAt }), - ); - - if (destroyedAt) { - return State Preview is unavailable for inactive Actors.; - } + } = useQuery(actorQueries.actorStateQueryOptions(actorId)); if (isError) { return ( @@ -69,7 +58,7 @@ export function ActorStateTab({ actorId }: ActorStateTabProps) { export function Info({ children }: PropsWithChildren) { return ( -
+
{children}
); diff --git a/frontend/src/components/actors/actors-actor-details.tsx b/frontend/src/components/actors/actors-actor-details.tsx index ef84c444db..7764e0fbf1 100644 --- a/frontend/src/components/actors/actors-actor-details.tsx +++ b/frontend/src/components/actors/actors-actor-details.tsx @@ -1,12 +1,6 @@ import { faQuestionSquare, Icon } from "@rivet-gg/icons"; -import { - useQuery, - useSuspenseInfiniteQuery, - useSuspenseQuery, -} from "@tanstack/react-query"; -import { useMatch } from "@tanstack/react-router"; -import { memo, type ReactNode, Suspense, useMemo } from "react"; -import { useInspectorCredentials } from "@/app/credentials-context"; +import { useQuery } from "@tanstack/react-query"; +import { memo, type ReactNode, Suspense } from "react"; import { cn, Flex, @@ -15,12 +9,6 @@ import { TabsList, TabsTrigger, } from "@/components"; -import { createEngineActorContext } from "@/queries/actor-engine"; -import { createInspectorActorContext } from "@/queries/actor-inspector"; -import { - type NamespaceNameId, - runnersQueryOptions, -} from "@/queries/manager-engine"; import { ActorConfigTab } from "./actor-config-tab"; import { ActorConnectionsTab } from "./actor-connections-tab"; import { ActorDatabaseTab } from "./actor-db-tab"; @@ -28,13 +16,13 @@ import { ActorDetailsSettingsProvider } from "./actor-details-settings"; import { ActorEventsTab } from "./actor-events-tab"; import { ActorLogsTab } from "./actor-logs-tab"; import { ActorMetricsTab } from "./actor-metrics-tab"; -import { ActorProvider } from "./actor-queries-context"; import { ActorStateTab } from "./actor-state-tab"; import { QueriedActorStatus } from "./actor-status"; import { ActorStopButton } from "./actor-stop-button"; import { ActorsSidebarToggleButton } from "./actors-sidebar-toggle-button"; import { useActorsView } from "./actors-view-context-provider"; import { ActorConsole } from "./console/actor-console"; +import { GuardConnectableInspector } from "./guard-connectable-inspector"; import { useManager } from "./manager-context"; import { ActorFeature, type ActorId } from "./queries"; import { ActorWorkerContextProvider } from "./worker/actor-worker-context"; @@ -57,34 +45,32 @@ export const ActorsActorDetails = memo( useManager().actorFeaturesQueryOptions(actorId), ); - const supportsConsole = features?.includes(ActorFeature.Console); + const supportsConsole = features.includes(ActorFeature.Console); return ( - - - +
+ -
- + tab={tab} + onTabChange={onTabChange} + // onExportLogs={onExportLogs} + // isExportingLogs={isExportingLogs} + /> - {supportsConsole ? ( - - ) : null} -
- - - + {supportsConsole ? ( + + + + ) : null} +
+
); }, ); @@ -234,7 +220,9 @@ export function ActorTabs({ className="min-h-0 flex-1 mt-0 h-full" > }> - + + + ) : null} @@ -251,7 +239,9 @@ export function ActorTabs({ value="connections" className="min-h-0 flex-1 mt-0" > - + + + ) : null} {supportsEvents ? ( @@ -259,7 +249,9 @@ export function ActorTabs({ value="events" className="min-h-0 flex-1 mt-0" > - + + + ) : null} {supportsDatabase ? ( @@ -267,7 +259,9 @@ export function ActorTabs({ value="database" className="min-h-0 min-w-0 flex-1 mt-0 h-full" > - + + + ) : null} {supportsState ? ( @@ -275,7 +269,9 @@ export function ActorTabs({ value="state" className="min-h-0 flex-1 mt-0" > - + + + ) : null} {supportsMetrics ? ( @@ -283,7 +279,9 @@ export function ActorTabs({ value="metrics" className="min-h-0 flex-1 mt-0 h-full" > - + + + ) : null} @@ -292,77 +290,3 @@ export function ActorTabs({ ); } - -function ActorContextProvider(props: { - actorId: ActorId; - children: ReactNode; -}) { - return __APP_TYPE__ === "inspector" ? ( - - ) : ( - - ); -} - -function ActorInspectorProvider({ - actorId, - children, -}: { - actorId: ActorId; - children: ReactNode; -}) { - const { data } = useSuspenseQuery(useManager().actorQueryOptions(actorId)); - const { credentials } = useInspectorCredentials(); - - if (!credentials?.url || !credentials?.token) { - throw new Error("Missing inspector credentials"); - } - - const actorContext = useMemo(() => { - return createInspectorActorContext({ - ...credentials, - name: data.name || "", - }); - }, [credentials, data.name]); - - return {children}; -} - -function ActorEngineProvider({ - actorId, - children, -}: { - actorId: ActorId; - children: ReactNode; -}) { - const { data: actor } = useSuspenseQuery( - useManager().actorQueryOptions(actorId), - ); - - const match = useMatch({ - from: "/_layout/ns/$namespace", - }); - - if (!match.params.namespace || !actor.runner) { - throw new Error("Actor is missing required fields"); - } - - const { data: runners } = useSuspenseInfiniteQuery( - runnersQueryOptions({ - namespace: match.params.namespace as NamespaceNameId, - }), - ); - - const runner = runners.find((runner) => runner.name === actor.runner); - - if (!runner) { - throw new Error("Runner not found"); - } - - const actorContext = useMemo(() => { - return createEngineActorContext({ - token: (runner.metadata?.inspectorToken as string) || "", - }); - }, [runner.metadata?.inspectorToken]); - return {children}; -} diff --git a/frontend/src/components/actors/actors-list.tsx b/frontend/src/components/actors/actors-list.tsx index ba634652f4..d1f62251e3 100644 --- a/frontend/src/components/actors/actors-list.tsx +++ b/frontend/src/components/actors/actors-list.tsx @@ -21,14 +21,13 @@ import { FilterCreator, FiltersDisplay, type OnFiltersChange, - type PickFiltersOptions, ScrollArea, ShimmerLine, SmallText, WithTooltip, } from "@/components"; import { VisibilitySensor } from "../visibility-sensor"; -import { useActorsFilters } from "./actor-filters-context"; +import { useActorsFilters, useFiltersValue } from "./actor-filters-context"; import { useActorsLayout } from "./actors-layout-context"; import { ActorsListRow, ActorsListRowSkeleton } from "./actors-list-row"; import { useActorsView } from "./actors-view-context-provider"; @@ -200,14 +199,6 @@ export function ListSkeleton() { ); } -function useFiltersValue(opts: PickFiltersOptions = {}) { - const { pick } = useActorsFilters(); - return useSearch({ - from: "/_layout", - select: (state) => pick(state, opts), - }); -} - function EmptyState({ count }: { count: number }) { const navigate = useNavigate(); const names = useSearch({ diff --git a/frontend/src/components/actors/guard-connectable-inspector.tsx b/frontend/src/components/actors/guard-connectable-inspector.tsx new file mode 100644 index 0000000000..fb3092a968 --- /dev/null +++ b/frontend/src/components/actors/guard-connectable-inspector.tsx @@ -0,0 +1,219 @@ +import { faPowerOff, faSpinnerThird, Icon } from "@rivet-gg/icons"; +import { useMutation, useQuery, useSuspenseQuery } from "@tanstack/react-query"; +import { useMatch } from "@tanstack/react-router"; +import { type ReactNode, useMemo } from "react"; +import { useInspectorCredentials } from "@/app/credentials-context"; +import { createEngineActorContext } from "@/queries/actor-engine"; +import { createInspectorActorContext } from "@/queries/actor-inspector"; +import { + type NamespaceNameId, + runnerByNameQueryOptions, +} from "@/queries/manager-engine"; +import { DiscreteCopyButton } from "../copy-area"; +import { Button } from "../ui/button"; +import { useFiltersValue } from "./actor-filters-context"; +import { ActorProvider } from "./actor-queries-context"; +import { Info } from "./actor-state-tab"; +import { useManager } from "./manager-context"; +import type { ActorId } from "./queries"; + +interface GuardConnectableInspectorProps { + actorId: ActorId; + children: ReactNode; +} + +export function GuardConnectableInspector({ + actorId, + children, +}: GuardConnectableInspectorProps) { + const filters = useFiltersValue({ includeEphemeral: true }); + const { + data: { destroyedAt, sleepingAt, pendingAllocationAt, startedAt } = {}, + } = useQuery({ + ...useManager().actorQueryOptions(actorId), + refetchInterval: 1000, + }); + + if (destroyedAt) { + return Unavailable for inactive Actors.; + } + + if (sleepingAt) { + if (filters.wakeOnSelect?.value?.[0] === "1") { + return ( + + + + ); + } + return ( + +

Unavailable for sleeping Actors.

+ +
+ ); + } + + if (pendingAllocationAt && !startedAt) { + return ( + + Cannot start Actor, runners are out of capacity. Add more + runners to run the Actor or increase runner capacity. + + ); + } + + return ( + + {children} + + ); +} + +function ActorContextProvider(props: { + actorId: ActorId; + children: ReactNode; +}) { + return __APP_TYPE__ === "inspector" ? ( + + ) : ( + + ); +} + +function ActorInspectorProvider({ + actorId, + children, +}: { + actorId: ActorId; + children: ReactNode; +}) { + const { data } = useSuspenseQuery(useManager().actorQueryOptions(actorId)); + const { credentials } = useInspectorCredentials(); + + if (!credentials?.url || !credentials?.token) { + throw new Error("Missing inspector credentials"); + } + + const actorContext = useMemo(() => { + return createInspectorActorContext({ + ...credentials, + name: data.name || "", + }); + }, [credentials, data.name]); + + return {children}; +} + +function useActorRunner({ actorId }: { actorId: ActorId }) { + const { data: actor } = useSuspenseQuery( + useManager().actorQueryOptions(actorId), + ); + + const match = useMatch({ + from: "/_layout/ns/$namespace", + }); + + if (!match.params.namespace || !actor.runner) { + throw new Error("Actor is missing required fields"); + } + + const { data: runner } = useQuery({ + ...runnerByNameQueryOptions({ + runnerName: actor.runner, + namespace: match.params.namespace as NamespaceNameId, + }), + refetchInterval: 1000, + }); + + return { actor, runner }; +} + +function useActorEngineContext({ actorId }: { actorId: ActorId }) { + const { actor, runner } = useActorRunner({ actorId }); + + const actorContext = useMemo(() => { + return createEngineActorContext({ + token: (runner?.metadata?.inspectorToken as string) || "", + }); + }, [runner?.metadata?.inspectorToken]); + + return { actorContext, actor, runner }; +} + +function ActorEngineProvider({ + actorId, + children, +}: { + actorId: ActorId; + children: ReactNode; +}) { + const { actorContext, actor, runner } = useActorEngineContext({ actorId }); + + if (!runner || !actor.runner) { + return ( + + ); + } + + return {children}; +} + +function NoRunnerInfo({ runner }: { runner: string }) { + return ( + +

There are no runners connected to run this Actor.

+

+ Check that your application is running and the + runner name is  + + {runner} + +

+
+ ); +} + +function WakeUpActorButton({ actorId }: { actorId: ActorId }) { + const { runner, actorContext } = useActorEngineContext({ actorId }); + + const { mutate, isPending } = useMutation( + actorContext.actorWakeUpMutationOptions(actorId), + ); + if (!runner) return null; + return ( + + ); +} + +function AutoWakeUpActor({ actorId }: { actorId: ActorId }) { + const { runner, actor, actorContext } = useActorEngineContext({ actorId }); + + const { isPending } = useQuery( + actorContext.actorAutoWakeUpQueryOptions(actorId, { + enabled: !!runner, + }), + ); + + if (!runner) return ; + + return isPending ? ( + +
+ + Waiting for Actor to wake... +
+
+ ) : null; +} diff --git a/frontend/src/components/ui/filters.tsx b/frontend/src/components/ui/filters.tsx index 5ea92c340f..f847e77c24 100644 --- a/frontend/src/components/ui/filters.tsx +++ b/frontend/src/components/ui/filters.tsx @@ -116,7 +116,6 @@ function filterDefinitionToOptions(definition: FilterDefinition) { function defaultFilterDefinitionOperator({ definition, - filterValues, }: { definition: FilterDefinition; filterValues: string[]; @@ -129,7 +128,6 @@ function defaultFilterDefinitionOperator({ const filterOperators = ({ definition, - filterValues, }: { definition: FilterDefinition; filterValues: string[]; @@ -311,7 +309,6 @@ const FilterValueCombobox = ({ function FilterDateValueCombobox({ id, - definition, operator, value, onChange, @@ -390,8 +387,6 @@ function FilterBooleanValue({ function FilterStringValue({ id, - operator, - definition, value, onChange, }: { @@ -687,13 +682,14 @@ export type FilterDefinition = // a filter that's only applied client-side and not sent to the server ephemeral?: boolean; excludes?: string[]; + defaultValue?: string[]; } | FilterSelectDefinition; type FilterSelectDefinition = ( | FilterSelectStaticDefinition | FilterSelectDynamicDefinition -) & { ephemeral?: boolean }; +) & { ephemeral?: boolean; defaultValue?: string[] }; type FilterSelectStaticDefinition = { type: "select"; @@ -730,14 +726,12 @@ export const FilterCreator = ({ value, text = "Filter", icon = , - showExcluded, onChange, }: { definitions: FilterDefinitions; value: Partial; text?: string; icon?: FunctionComponentElement; - showExcluded?: boolean; onChange: OnFiltersChange; }) => { const [open, setOpen] = useState(false); @@ -1329,7 +1323,7 @@ export type FilterValue = z.infer; export function createFiltersSchema(definitions: FilterDefinitions) { const filters: Record = {}; - for (const [key, definition] of Object.entries(definitions)) { + for (const [key] of Object.entries(definitions)) { filters[key] = FilterValueSchema; } @@ -1348,6 +1342,21 @@ export function createFiltersPicker(definitions: FilterDefinitions) { ); const keys = Object.keys(defs); + const filtersWithDefaultValues = Object.fromEntries( + Object.entries(defs) + .filter(([, def]) => def.defaultValue !== undefined) + .map(([key, def]) => [ + key, + { + value: def.defaultValue!, + operator: def.operators?.[0] ?? FilterOp.EQUAL, + }, + ]), + ); + + // add missing default values to the object + object = { ...filtersWithDefaultValues, ...object }; + return _.pick(object, keys); }; } diff --git a/frontend/src/queries/manager-engine.ts b/frontend/src/queries/manager-engine.ts index 66543a9385..6d644f103b 100644 --- a/frontend/src/queries/manager-engine.ts +++ b/frontend/src/queries/manager-engine.ts @@ -281,6 +281,28 @@ export const runnerQueryOptions = (opts: { }); }; +export const runnerByNameQueryOptions = (opts: { + namespace: NamespaceNameId; + runnerName: string; +}) => { + return queryOptions({ + queryKey: [opts.namespace, "runner", opts.runnerName], + enabled: !!opts.runnerName, + queryFn: async ({ signal: abortSignal }) => { + const data = await client.runners.list( + { namespace: opts.namespace, name: opts.runnerName }, + { + abortSignal, + }, + ); + if (!data.runners[0]) { + throw new Error("Runner not found"); + } + return data.runners[0]; + }, + }); +}; + export const runnerNamesQueryOptions = (opts: { namespace: NamespaceNameId; }) => {