Skip to content

Commit 4d11ba5

Browse files
committed
feat(fe): handle runners unavailability (#2875)
Closes FRONT-796 ### TL;DR Added auto-wake functionality for sleeping actors in the actor inspector UI. ### What changed? - Added a new `wakeOnSelect` filter option that automatically wakes up sleeping actors when selected - Created a `GuardConnectableInspector` component that handles different actor states (destroyed, sleeping, pending allocation) - Implemented actor wake-up functionality with both manual and automatic options - Added mutation and query options for waking up actors - Improved error handling and user feedback for various actor states - Refactored the actor context provider system to better handle different actor states - Enhanced filter definitions to support default values ### How to test? 1. Select a sleeping actor in the actors list 2. Observe that it automatically wakes up if the "Auto-wake Actors on select" filter is enabled 3. Disable the auto-wake filter and select a sleeping actor 4. Verify that a "Wake up Actor" button appears and functions correctly 5. Test different actor states (destroyed, sleeping, pending allocation) to ensure appropriate messages are displayed ### Why make this change? This change improves the user experience when working with sleeping actors by providing an automatic wake-up option. Previously, users had to manually wake up actors before being able to inspect them, which added friction to the workflow. The new functionality streamlines the process while still giving users control over when actors are awakened.
1 parent 880e1fa commit 4d11ba5

File tree

8 files changed

+359
-160
lines changed

8 files changed

+359
-160
lines changed

frontend/src/components/actors/actor-filters-context.tsx

Lines changed: 16 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,14 @@
11
import { faHashtag, faKey } from "@rivet-gg/icons";
2-
import { useQuery } from "@tanstack/react-query";
32
import { useSearch } from "@tanstack/react-router";
4-
import { CommandGroup, CommandItem } from "cmdk";
53
import { createContext, useContext } from "react";
6-
import { cn } from "../lib/utils";
7-
import { Checkbox } from "../ui/checkbox";
84
import {
95
createFiltersPicker,
106
createFiltersRemover,
117
createFiltersSchema,
128
type FilterDefinitions,
139
FilterOp,
14-
type OptionsProviderProps,
10+
type PickFiltersOptions,
1511
} from "../ui/filters";
16-
import { ActorRegion } from "./actor-region";
17-
import { ActorStatus } from "./actor-status";
18-
import { useManager } from "./manager-context";
19-
import type { ActorStatus as ActorStatusType } from "./queries";
2012

2113
export const ACTORS_FILTERS_DEFINITIONS = {
2214
id: {
@@ -48,6 +40,13 @@ export const ACTORS_FILTERS_DEFINITIONS = {
4840
category: "display",
4941
ephemeral: true,
5042
},
43+
wakeOnSelect: {
44+
type: "boolean",
45+
label: "Auto-wake Actors on select",
46+
category: "display",
47+
ephemeral: true,
48+
defaultValue: ["1"],
49+
},
5150
// tags: {
5251
// type: "select",
5352
// label: "Tags",
@@ -143,3 +142,11 @@ export const useFilters = (
143142
select: (state) => fn(pick(state)),
144143
});
145144
};
145+
146+
export function useFiltersValue(opts: PickFiltersOptions = {}) {
147+
const { pick } = useActorsFilters();
148+
return useSearch({
149+
from: "/_layout",
150+
select: (state) => pick(state, opts),
151+
});
152+
}

frontend/src/components/actors/actor-queries-context.tsx

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -208,6 +208,44 @@ export const defaultActorContext = {
208208
},
209209
};
210210
},
211+
212+
actorWakeUpMutationOptions(actorId: ActorId) {
213+
return {
214+
mutationKey: ["actor", actorId, "wake-up"],
215+
mutationFn: async () => {
216+
const client = this.createActorInspector(actorId);
217+
try {
218+
await client.ping.$get();
219+
return true;
220+
} catch {
221+
return false;
222+
}
223+
},
224+
};
225+
},
226+
227+
actorAutoWakeUpQueryOptions(
228+
actorId: ActorId,
229+
{ enabled }: { enabled?: boolean } = {},
230+
) {
231+
return queryOptions({
232+
enabled,
233+
refetchInterval: 1000,
234+
staleTime: 0,
235+
gcTime: 0,
236+
queryKey: ["actor", actorId, "auto-wake-up"],
237+
queryFn: async ({ queryKey: [, actorId] }) => {
238+
const client = this.createActorInspector(actorId);
239+
try {
240+
await client.ping.$get();
241+
return true;
242+
} catch {
243+
return false;
244+
}
245+
},
246+
retry: false,
247+
});
248+
},
211249
};
212250

213251
export type ActorContext = typeof defaultActorContext;

frontend/src/components/actors/actor-state-tab.tsx

Lines changed: 2 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -5,32 +5,21 @@ import { Button } from "../ui/button";
55
import { ActorEditableState } from "./actor-editable-state";
66
import { useActor } from "./actor-queries-context";
77
import { useActorsView } from "./actors-view-context-provider";
8-
import { useManager } from "./manager-context";
98
import type { ActorId } from "./queries";
109

1110
interface ActorStateTabProps {
1211
actorId: ActorId;
1312
}
1413

1514
export function ActorStateTab({ actorId }: ActorStateTabProps) {
16-
const { data: destroyedAt } = useQuery(
17-
useManager().actorDestroyedAtQueryOptions(actorId),
18-
);
19-
2015
const { links } = useActorsView();
2116

2217
const actorQueries = useActor();
2318
const {
2419
data: state,
2520
isError,
2621
isLoading,
27-
} = useQuery(
28-
actorQueries.actorStateQueryOptions(actorId, { enabled: !destroyedAt }),
29-
);
30-
31-
if (destroyedAt) {
32-
return <Info>State Preview is unavailable for inactive Actors.</Info>;
33-
}
22+
} = useQuery(actorQueries.actorStateQueryOptions(actorId));
3423

3524
if (isError) {
3625
return (
@@ -69,7 +58,7 @@ export function ActorStateTab({ actorId }: ActorStateTabProps) {
6958

7059
export function Info({ children }: PropsWithChildren) {
7160
return (
72-
<div className="flex-1 flex flex-col gap-2 items-center justify-center h-full text-center">
61+
<div className="flex-1 flex flex-col gap-2 items-center justify-center h-full text-center max-w-md mx-auto">
7362
{children}
7463
</div>
7564
);

frontend/src/components/actors/actors-actor-details.tsx

Lines changed: 43 additions & 119 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,6 @@
11
import { faQuestionSquare, Icon } from "@rivet-gg/icons";
2-
import {
3-
useQuery,
4-
useSuspenseInfiniteQuery,
5-
useSuspenseQuery,
6-
} from "@tanstack/react-query";
7-
import { useMatch } from "@tanstack/react-router";
8-
import { memo, type ReactNode, Suspense, useMemo } from "react";
9-
import { useInspectorCredentials } from "@/app/credentials-context";
2+
import { useQuery } from "@tanstack/react-query";
3+
import { memo, type ReactNode, Suspense } from "react";
104
import {
115
cn,
126
Flex,
@@ -15,26 +9,20 @@ import {
159
TabsList,
1610
TabsTrigger,
1711
} from "@/components";
18-
import { createEngineActorContext } from "@/queries/actor-engine";
19-
import { createInspectorActorContext } from "@/queries/actor-inspector";
20-
import {
21-
type NamespaceNameId,
22-
runnersQueryOptions,
23-
} from "@/queries/manager-engine";
2412
import { ActorConfigTab } from "./actor-config-tab";
2513
import { ActorConnectionsTab } from "./actor-connections-tab";
2614
import { ActorDatabaseTab } from "./actor-db-tab";
2715
import { ActorDetailsSettingsProvider } from "./actor-details-settings";
2816
import { ActorEventsTab } from "./actor-events-tab";
2917
import { ActorLogsTab } from "./actor-logs-tab";
3018
import { ActorMetricsTab } from "./actor-metrics-tab";
31-
import { ActorProvider } from "./actor-queries-context";
3219
import { ActorStateTab } from "./actor-state-tab";
3320
import { QueriedActorStatus } from "./actor-status";
3421
import { ActorStopButton } from "./actor-stop-button";
3522
import { ActorsSidebarToggleButton } from "./actors-sidebar-toggle-button";
3623
import { useActorsView } from "./actors-view-context-provider";
3724
import { ActorConsole } from "./console/actor-console";
25+
import { GuardConnectableInspector } from "./guard-connectable-inspector";
3826
import { useManager } from "./manager-context";
3927
import { ActorFeature, type ActorId } from "./queries";
4028
import { ActorWorkerContextProvider } from "./worker/actor-worker-context";
@@ -57,34 +45,32 @@ export const ActorsActorDetails = memo(
5745
useManager().actorFeaturesQueryOptions(actorId),
5846
);
5947

60-
const supportsConsole = features?.includes(ActorFeature.Console);
48+
const supportsConsole = features.includes(ActorFeature.Console);
6149

6250
return (
63-
<ActorContextProvider actorId={actorId}>
64-
<ActorDetailsSettingsProvider>
65-
<ActorWorkerContextProvider
51+
<ActorDetailsSettingsProvider>
52+
<div className="flex flex-col h-full flex-1">
53+
<ActorTabs
54+
features={features}
6655
actorId={actorId}
67-
// notifyOnReconnect={features?.includes(
68-
// ActorFeature.InspectReconnectNotification,
69-
// )}
70-
>
71-
<div className="flex flex-col h-full flex-1">
72-
<ActorTabs
73-
features={features}
74-
actorId={actorId}
75-
tab={tab}
76-
onTabChange={onTabChange}
77-
// onExportLogs={onExportLogs}
78-
// isExportingLogs={isExportingLogs}
79-
/>
56+
tab={tab}
57+
onTabChange={onTabChange}
58+
// onExportLogs={onExportLogs}
59+
// isExportingLogs={isExportingLogs}
60+
/>
8061

81-
{supportsConsole ? (
82-
<ActorConsole actorId={actorId} />
83-
) : null}
84-
</div>
85-
</ActorWorkerContextProvider>
86-
</ActorDetailsSettingsProvider>
87-
</ActorContextProvider>
62+
{supportsConsole ? (
63+
<ActorWorkerContextProvider
64+
actorId={actorId}
65+
// notifyOnReconnect={features?.includes(
66+
// ActorFeature.InspectReconnectNotification,
67+
// )}
68+
>
69+
<ActorConsole actorId={actorId} />
70+
</ActorWorkerContextProvider>
71+
) : null}
72+
</div>
73+
</ActorDetailsSettingsProvider>
8874
);
8975
},
9076
);
@@ -234,7 +220,9 @@ export function ActorTabs({
234220
className="min-h-0 flex-1 mt-0 h-full"
235221
>
236222
<Suspense fallback={<ActorLogsTab.Skeleton />}>
237-
<ActorLogsTab actorId={actorId} />
223+
<GuardConnectableInspector actorId={actorId}>
224+
<ActorLogsTab actorId={actorId} />
225+
</GuardConnectableInspector>
238226
</Suspense>
239227
</TabsContent>
240228
) : null}
@@ -251,39 +239,49 @@ export function ActorTabs({
251239
value="connections"
252240
className="min-h-0 flex-1 mt-0"
253241
>
254-
<ActorConnectionsTab actorId={actorId} />
242+
<GuardConnectableInspector actorId={actorId}>
243+
<ActorConnectionsTab actorId={actorId} />
244+
</GuardConnectableInspector>
255245
</TabsContent>
256246
) : null}
257247
{supportsEvents ? (
258248
<TabsContent
259249
value="events"
260250
className="min-h-0 flex-1 mt-0"
261251
>
262-
<ActorEventsTab actorId={actorId} />
252+
<GuardConnectableInspector actorId={actorId}>
253+
<ActorEventsTab actorId={actorId} />
254+
</GuardConnectableInspector>
263255
</TabsContent>
264256
) : null}
265257
{supportsDatabase ? (
266258
<TabsContent
267259
value="database"
268260
className="min-h-0 min-w-0 flex-1 mt-0 h-full"
269261
>
270-
<ActorDatabaseTab actorId={actorId} />
262+
<GuardConnectableInspector actorId={actorId}>
263+
<ActorDatabaseTab actorId={actorId} />
264+
</GuardConnectableInspector>
271265
</TabsContent>
272266
) : null}
273267
{supportsState ? (
274268
<TabsContent
275269
value="state"
276270
className="min-h-0 flex-1 mt-0"
277271
>
278-
<ActorStateTab actorId={actorId} />
272+
<GuardConnectableInspector actorId={actorId}>
273+
<ActorStateTab actorId={actorId} />
274+
</GuardConnectableInspector>
279275
</TabsContent>
280276
) : null}
281277
{supportsMetrics ? (
282278
<TabsContent
283279
value="metrics"
284280
className="min-h-0 flex-1 mt-0 h-full"
285281
>
286-
<ActorMetricsTab actorId={actorId} />
282+
<GuardConnectableInspector actorId={actorId}>
283+
<ActorMetricsTab actorId={actorId} />
284+
</GuardConnectableInspector>
287285
</TabsContent>
288286
) : null}
289287
</>
@@ -292,77 +290,3 @@ export function ActorTabs({
292290
</Tabs>
293291
);
294292
}
295-
296-
function ActorContextProvider(props: {
297-
actorId: ActorId;
298-
children: ReactNode;
299-
}) {
300-
return __APP_TYPE__ === "inspector" ? (
301-
<ActorInspectorProvider {...props} />
302-
) : (
303-
<ActorEngineProvider {...props} />
304-
);
305-
}
306-
307-
function ActorInspectorProvider({
308-
actorId,
309-
children,
310-
}: {
311-
actorId: ActorId;
312-
children: ReactNode;
313-
}) {
314-
const { data } = useSuspenseQuery(useManager().actorQueryOptions(actorId));
315-
const { credentials } = useInspectorCredentials();
316-
317-
if (!credentials?.url || !credentials?.token) {
318-
throw new Error("Missing inspector credentials");
319-
}
320-
321-
const actorContext = useMemo(() => {
322-
return createInspectorActorContext({
323-
...credentials,
324-
name: data.name || "",
325-
});
326-
}, [credentials, data.name]);
327-
328-
return <ActorProvider value={actorContext}>{children}</ActorProvider>;
329-
}
330-
331-
function ActorEngineProvider({
332-
actorId,
333-
children,
334-
}: {
335-
actorId: ActorId;
336-
children: ReactNode;
337-
}) {
338-
const { data: actor } = useSuspenseQuery(
339-
useManager().actorQueryOptions(actorId),
340-
);
341-
342-
const match = useMatch({
343-
from: "/_layout/ns/$namespace",
344-
});
345-
346-
if (!match.params.namespace || !actor.runner) {
347-
throw new Error("Actor is missing required fields");
348-
}
349-
350-
const { data: runners } = useSuspenseInfiniteQuery(
351-
runnersQueryOptions({
352-
namespace: match.params.namespace as NamespaceNameId,
353-
}),
354-
);
355-
356-
const runner = runners.find((runner) => runner.name === actor.runner);
357-
358-
if (!runner) {
359-
throw new Error("Runner not found");
360-
}
361-
362-
const actorContext = useMemo(() => {
363-
return createEngineActorContext({
364-
token: (runner.metadata?.inspectorToken as string) || "",
365-
});
366-
}, [runner.metadata?.inspectorToken]);
367-
return <ActorProvider value={actorContext}>{children}</ActorProvider>;
368-
}

0 commit comments

Comments
 (0)