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;
}) => {