@opengeni/react 0.52.1 → 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +51 -1
- package/dist/artifacts.d.ts +1 -1
- package/dist/artifacts.js +43 -14
- package/dist/artifacts.js.map +1 -1
- package/dist/{chunk-ROIRTM37.js → chunk-5UQPNLIO.js} +6 -6
- package/dist/{chunk-JR3QFY6B.js → chunk-6KLX2RH5.js} +49 -2
- package/dist/chunk-6KLX2RH5.js.map +1 -0
- package/dist/{chunk-FWS3KC3Z.js → chunk-B6WPZSTT.js} +104 -104
- package/dist/{chunk-FWS3KC3Z.js.map → chunk-B6WPZSTT.js.map} +1 -1
- package/dist/{chunk-HDBG7XGO.js → chunk-C5ZPSSS5.js} +50 -1
- package/dist/chunk-C5ZPSSS5.js.map +1 -0
- package/dist/{chunk-X4S56Y2H.js → chunk-HA46CYQK.js} +135 -11
- package/dist/chunk-HA46CYQK.js.map +1 -0
- package/dist/{chunk-724BW4TW.js → chunk-IGC6A2ZI.js} +2 -2
- package/dist/chunk-LLOLUHVG.js +5937 -0
- package/dist/chunk-LLOLUHVG.js.map +1 -0
- package/dist/{chunk-WYA65Y3F.js → chunk-MKPIAGVL.js} +13 -3
- package/dist/chunk-MKPIAGVL.js.map +1 -0
- package/dist/chunk-R3AZOAB5.js +933 -0
- package/dist/chunk-R3AZOAB5.js.map +1 -0
- package/dist/{chunk-23EJ676W.js → chunk-XBZWOM2K.js} +9 -4
- package/dist/chunk-XBZWOM2K.js.map +1 -0
- package/dist/{chunk-JAGDUCZQ.js → chunk-Y7OEDUJ5.js} +17 -13
- package/dist/chunk-Y7OEDUJ5.js.map +1 -0
- package/dist/client.d.ts +6 -4
- package/dist/components/artifacts/index.d.ts +1 -0
- package/dist/components/artifacts/published-html-artifact-frame.d.ts +14 -0
- package/dist/components/browser-input.d.ts +7 -0
- package/dist/components/browser-viewer.d.ts +5 -4
- package/dist/components/computer-viewer.d.ts +2 -1
- package/dist/components/desktop-viewer.d.ts +26 -2
- package/dist/components/interaction-intervention-banner.d.ts +11 -0
- package/dist/components/sandbox-terminal.d.ts +4 -1
- package/dist/components/sandbox-workspace.d.ts +3 -0
- package/dist/composer.js +6 -6
- package/dist/hooks/use-auth-runs.d.ts +32 -0
- package/dist/hooks/use-browser-downloads.d.ts +25 -0
- package/dist/hooks/use-browser-identities.d.ts +5 -1
- package/dist/hooks/use-browser-session.d.ts +2 -1
- package/dist/hooks/use-browser-sessions.d.ts +1 -1
- package/dist/hooks/use-channels.d.ts +26 -0
- package/dist/hooks/use-computer-frame-stream.d.ts +1 -1
- package/dist/hooks/use-computer-session.d.ts +2 -1
- package/dist/hooks/use-computer-sessions.d.ts +1 -1
- package/dist/hooks/use-desktop-stream.d.ts +4 -0
- package/dist/hooks/use-interaction-interventions.d.ts +27 -0
- package/dist/hooks/use-interaction-invalidation.d.ts +19 -0
- package/dist/hooks/use-network-routes.d.ts +25 -0
- package/dist/hooks/use-site-auth-connections.d.ts +25 -0
- package/dist/hooks/use-supergrok-accounts.d.ts +26 -0
- package/dist/hooks/use-terminal-stream.d.ts +3 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +1220 -1545
- package/dist/index.js.map +1 -1
- package/dist/interaction.d.ts +13 -3
- package/dist/interaction.js +520 -6
- package/dist/interaction.js.map +1 -1
- package/dist/interactive-workbench-viewers-TOHBXJEE.js +15 -0
- package/dist/lib/host-platform.d.ts +5 -0
- package/dist/lib/relay-wire.d.ts +20 -1
- package/dist/machines.js +3 -3
- package/dist/model-policy.d.ts +1 -1
- package/dist/model-policy.js +1 -1
- package/dist/provider.d.ts +3 -2
- package/dist/realtime/realtime-control.d.ts +6 -2
- package/dist/realtime.js +138 -24
- package/dist/realtime.js.map +1 -1
- package/dist/session-context.d.ts +10 -2
- package/dist/session-ui.js +3 -3
- package/dist/session.js +11 -11
- package/dist/timeline/types.d.ts +5 -0
- package/package.json +2 -2
- package/src/artifacts.ts +3 -0
- package/src/client.ts +73 -3
- package/src/components/artifacts/index.ts +5 -0
- package/src/components/artifacts/published-html-artifact-frame.tsx +38 -0
- package/src/components/browser-input.ts +65 -0
- package/src/components/browser-viewer.tsx +1287 -177
- package/src/components/computer-viewer.tsx +611 -114
- package/src/components/desktop-viewer.tsx +114 -1
- package/src/components/interaction-intervention-banner.tsx +99 -0
- package/src/components/machines/machine-detail.tsx +1 -1
- package/src/components/model-policy-picker.tsx +15 -0
- package/src/components/sandbox-terminal.tsx +24 -7
- package/src/components/sandbox-workspace.tsx +42 -4
- package/src/components/workspace-dock.tsx +59 -5
- package/src/hooks/use-attached-browsers.ts +13 -11
- package/src/hooks/use-auth-runs.ts +278 -0
- package/src/hooks/use-browser-downloads.ts +218 -0
- package/src/hooks/use-browser-frame-stream.ts +168 -59
- package/src/hooks/use-browser-identities.ts +47 -6
- package/src/hooks/use-browser-session.ts +94 -27
- package/src/hooks/use-browser-sessions.ts +19 -14
- package/src/hooks/use-channels.ts +99 -0
- package/src/hooks/use-computer-frame-stream.ts +184 -61
- package/src/hooks/use-computer-session.ts +22 -2
- package/src/hooks/use-computer-sessions.ts +19 -13
- package/src/hooks/use-desktop-stream.ts +39 -7
- package/src/hooks/use-interaction-interventions.ts +242 -0
- package/src/hooks/use-interaction-invalidation.ts +66 -0
- package/src/hooks/use-machine-chip.ts +13 -3
- package/src/hooks/use-network-routes.ts +214 -0
- package/src/hooks/use-site-auth-connections.ts +226 -0
- package/src/hooks/use-supergrok-accounts.ts +81 -0
- package/src/hooks/use-terminal-stream.ts +288 -55
- package/src/index.ts +8 -0
- package/src/interaction.ts +30 -0
- package/src/lib/host-platform.ts +10 -0
- package/src/lib/relay-wire.ts +43 -4
- package/src/lib/terminal-capability.ts +1 -1
- package/src/model-policy.ts +19 -4
- package/src/provider.tsx +133 -1
- package/src/realtime/realtime-control.tsx +181 -26
- package/src/session-context.ts +66 -1
- package/src/timeline/activity-rail.tsx +6 -0
- package/src/timeline/projection.ts +58 -0
- package/src/timeline/types.ts +2 -0
- package/styles/compiled.css +64 -6
- package/dist/chunk-23EJ676W.js.map +0 -1
- package/dist/chunk-6BBWWOOU.js +0 -82
- package/dist/chunk-6BBWWOOU.js.map +0 -1
- package/dist/chunk-7I2KB3OK.js +0 -3895
- package/dist/chunk-7I2KB3OK.js.map +0 -1
- package/dist/chunk-HDBG7XGO.js.map +0 -1
- package/dist/chunk-JAGDUCZQ.js.map +0 -1
- package/dist/chunk-JR3QFY6B.js.map +0 -1
- package/dist/chunk-WYA65Y3F.js.map +0 -1
- package/dist/chunk-X4S56Y2H.js.map +0 -1
- package/dist/interactive-workbench-viewers-KHNOHWQE.js +0 -13
- /package/dist/{chunk-ROIRTM37.js.map → chunk-5UQPNLIO.js.map} +0 -0
- /package/dist/{chunk-724BW4TW.js.map → chunk-IGC6A2ZI.js.map} +0 -0
- /package/dist/{interactive-workbench-viewers-KHNOHWQE.js.map → interactive-workbench-viewers-TOHBXJEE.js.map} +0 -0
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
CreateInteractionInterventionRequest,
|
|
3
|
+
InteractionIntervention,
|
|
4
|
+
InteractionInterventionMutationResponse,
|
|
5
|
+
ResolveInteractionInterventionRequest,
|
|
6
|
+
} from "@opengeni/sdk/interaction";
|
|
7
|
+
import { useCallback, useEffect, useRef, useState } from "react";
|
|
8
|
+
import type { EmbeddedInterventionClientLike } from "../client";
|
|
9
|
+
import {
|
|
10
|
+
type EmbeddedInterventionClientOverride,
|
|
11
|
+
useEmbeddedInterventions,
|
|
12
|
+
} from "../session-context";
|
|
13
|
+
import { useInteractionInvalidation } from "./use-interaction-invalidation";
|
|
14
|
+
|
|
15
|
+
export type UseInteractionInterventionsOptions = EmbeddedInterventionClientOverride & {
|
|
16
|
+
enabled?: boolean | undefined;
|
|
17
|
+
resourceKind?: "browser_session" | "computer_session" | undefined;
|
|
18
|
+
resourceId?: string | null | undefined;
|
|
19
|
+
includeSettled?: boolean | undefined;
|
|
20
|
+
pollIntervalMs?: number | undefined;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export type UseInteractionInterventionsResult = {
|
|
24
|
+
interventions: InteractionIntervention[];
|
|
25
|
+
loading: boolean;
|
|
26
|
+
refreshing: boolean;
|
|
27
|
+
mutating: boolean;
|
|
28
|
+
error: Error | null;
|
|
29
|
+
refresh: () => Promise<void>;
|
|
30
|
+
create: (
|
|
31
|
+
request: Omit<CreateInteractionInterventionRequest, "operationId"> & {
|
|
32
|
+
operationId?: string;
|
|
33
|
+
},
|
|
34
|
+
) => Promise<InteractionInterventionMutationResponse>;
|
|
35
|
+
resolve: (
|
|
36
|
+
interventionId: string,
|
|
37
|
+
request: Omit<ResolveInteractionInterventionRequest, "operationId"> & {
|
|
38
|
+
operationId?: string;
|
|
39
|
+
},
|
|
40
|
+
) => Promise<InteractionInterventionMutationResponse>;
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
/** Human-action bridge shared by Browser and Computer surfaces. */
|
|
44
|
+
export function useInteractionInterventions(
|
|
45
|
+
options: UseInteractionInterventionsOptions = {},
|
|
46
|
+
): UseInteractionInterventionsResult {
|
|
47
|
+
const { client, workspaceId, workspaceInteractionEvent, workspaceInteractionConnectionState } =
|
|
48
|
+
useEmbeddedInterventions(options);
|
|
49
|
+
const enabled = (options.enabled ?? true) && options.resourceId !== null;
|
|
50
|
+
const resourceKind = options.resourceKind;
|
|
51
|
+
const resourceId = options.resourceId ?? undefined;
|
|
52
|
+
const includeSettled = options.includeSettled ?? false;
|
|
53
|
+
const key = `${workspaceId}:${resourceKind ?? "*"}:${resourceId ?? "*"}:${includeSettled}`;
|
|
54
|
+
const [state, setState] = useState(() => emptyState(key, enabled));
|
|
55
|
+
const [mutationCount, setMutationCount] = useState(0);
|
|
56
|
+
const requestRef = useRef<{ id: number; controller: AbortController | null }>({
|
|
57
|
+
id: 0,
|
|
58
|
+
controller: null,
|
|
59
|
+
});
|
|
60
|
+
const mountedRef = useRef(true);
|
|
61
|
+
const visible = state.key === key ? state : emptyState(key, enabled);
|
|
62
|
+
|
|
63
|
+
const cancelLoad = useCallback(() => {
|
|
64
|
+
requestRef.current.controller?.abort();
|
|
65
|
+
requestRef.current = { id: requestRef.current.id + 1, controller: null };
|
|
66
|
+
}, []);
|
|
67
|
+
|
|
68
|
+
const load = useCallback(
|
|
69
|
+
async (foreground: boolean, replace = true): Promise<void> => {
|
|
70
|
+
if (!enabled) return;
|
|
71
|
+
if (requestRef.current.controller && !replace) return;
|
|
72
|
+
const id = requestRef.current.id + 1;
|
|
73
|
+
requestRef.current.controller?.abort();
|
|
74
|
+
const controller = new AbortController();
|
|
75
|
+
requestRef.current = { id, controller };
|
|
76
|
+
if (foreground || replace) {
|
|
77
|
+
setState((current) => ({
|
|
78
|
+
...(current.key === key ? current : emptyState(key, true)),
|
|
79
|
+
loading: foreground,
|
|
80
|
+
refreshing: !foreground,
|
|
81
|
+
error: foreground ? null : current.error,
|
|
82
|
+
}));
|
|
83
|
+
}
|
|
84
|
+
try {
|
|
85
|
+
const response = await client.listInteractionInterventions(workspaceId, {
|
|
86
|
+
...(resourceKind ? { resourceKind } : {}),
|
|
87
|
+
...(resourceId ? { resourceId } : {}),
|
|
88
|
+
includeSettled,
|
|
89
|
+
signal: controller.signal,
|
|
90
|
+
});
|
|
91
|
+
if (!mountedRef.current || requestRef.current.id !== id) return;
|
|
92
|
+
setState({
|
|
93
|
+
key,
|
|
94
|
+
interventions: sortInterventions(response.interventions),
|
|
95
|
+
loading: false,
|
|
96
|
+
refreshing: false,
|
|
97
|
+
error: null,
|
|
98
|
+
});
|
|
99
|
+
} catch (cause) {
|
|
100
|
+
if (controller.signal.aborted || !mountedRef.current || requestRef.current.id !== id)
|
|
101
|
+
return;
|
|
102
|
+
setState((current) => ({
|
|
103
|
+
...(current.key === key ? current : emptyState(key, true)),
|
|
104
|
+
loading: false,
|
|
105
|
+
refreshing: false,
|
|
106
|
+
error: asError(cause),
|
|
107
|
+
}));
|
|
108
|
+
} finally {
|
|
109
|
+
if (requestRef.current.id === id) requestRef.current = { id, controller: null };
|
|
110
|
+
}
|
|
111
|
+
},
|
|
112
|
+
[client, enabled, includeSettled, key, resourceId, resourceKind, workspaceId],
|
|
113
|
+
);
|
|
114
|
+
|
|
115
|
+
const refresh = useCallback(async () => await load(false, true), [load]);
|
|
116
|
+
|
|
117
|
+
useEffect(() => {
|
|
118
|
+
mountedRef.current = true;
|
|
119
|
+
if (!enabled) {
|
|
120
|
+
cancelLoad();
|
|
121
|
+
setState(emptyState(key, false));
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
void load(true);
|
|
125
|
+
return () => {
|
|
126
|
+
mountedRef.current = false;
|
|
127
|
+
cancelLoad();
|
|
128
|
+
};
|
|
129
|
+
}, [cancelLoad, enabled, key, load]);
|
|
130
|
+
|
|
131
|
+
useInteractionInvalidation({
|
|
132
|
+
workspaceId,
|
|
133
|
+
key,
|
|
134
|
+
enabled,
|
|
135
|
+
event: workspaceInteractionEvent,
|
|
136
|
+
connectionState: workspaceInteractionConnectionState,
|
|
137
|
+
refresh,
|
|
138
|
+
fallbackPollIntervalMs: options.pollIntervalMs ?? 2_000,
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
const mutate = useCallback(async <T>(operation: () => Promise<T>): Promise<T> => {
|
|
142
|
+
setMutationCount((count) => count + 1);
|
|
143
|
+
try {
|
|
144
|
+
return await operation();
|
|
145
|
+
} finally {
|
|
146
|
+
if (mountedRef.current) setMutationCount((count) => Math.max(0, count - 1));
|
|
147
|
+
}
|
|
148
|
+
}, []);
|
|
149
|
+
|
|
150
|
+
const merge = useCallback(
|
|
151
|
+
(intervention: InteractionIntervention) => {
|
|
152
|
+
setState((current) => {
|
|
153
|
+
if (current.key !== key) return current;
|
|
154
|
+
const interventions = current.interventions.filter(
|
|
155
|
+
(candidate) => candidate.id !== intervention.id,
|
|
156
|
+
);
|
|
157
|
+
const matches =
|
|
158
|
+
(!resourceKind || intervention.resourceKind === resourceKind) &&
|
|
159
|
+
(!resourceId || intervention.resourceId === resourceId);
|
|
160
|
+
if (matches && (includeSettled || intervention.status === "open")) {
|
|
161
|
+
interventions.push(intervention);
|
|
162
|
+
}
|
|
163
|
+
return {
|
|
164
|
+
...current,
|
|
165
|
+
interventions: sortInterventions(interventions),
|
|
166
|
+
error: null,
|
|
167
|
+
};
|
|
168
|
+
});
|
|
169
|
+
},
|
|
170
|
+
[includeSettled, key, resourceId, resourceKind],
|
|
171
|
+
);
|
|
172
|
+
|
|
173
|
+
const create = useCallback(
|
|
174
|
+
async (
|
|
175
|
+
request: Omit<CreateInteractionInterventionRequest, "operationId"> & {
|
|
176
|
+
operationId?: string;
|
|
177
|
+
},
|
|
178
|
+
): Promise<InteractionInterventionMutationResponse> =>
|
|
179
|
+
await mutate(async () => {
|
|
180
|
+
const response = await client.createInteractionIntervention(workspaceId, {
|
|
181
|
+
...request,
|
|
182
|
+
operationId: request.operationId ?? crypto.randomUUID(),
|
|
183
|
+
});
|
|
184
|
+
merge(response.intervention);
|
|
185
|
+
return response;
|
|
186
|
+
}),
|
|
187
|
+
[client, merge, mutate, workspaceId],
|
|
188
|
+
);
|
|
189
|
+
|
|
190
|
+
const resolve = useCallback(
|
|
191
|
+
async (
|
|
192
|
+
interventionId: string,
|
|
193
|
+
request: Omit<ResolveInteractionInterventionRequest, "operationId"> & {
|
|
194
|
+
operationId?: string;
|
|
195
|
+
},
|
|
196
|
+
): Promise<InteractionInterventionMutationResponse> =>
|
|
197
|
+
await mutate(async () => {
|
|
198
|
+
const response = await client.resolveInteractionIntervention(workspaceId, interventionId, {
|
|
199
|
+
...request,
|
|
200
|
+
operationId: request.operationId ?? crypto.randomUUID(),
|
|
201
|
+
});
|
|
202
|
+
merge(response.intervention);
|
|
203
|
+
return response;
|
|
204
|
+
}),
|
|
205
|
+
[client, merge, mutate, workspaceId],
|
|
206
|
+
);
|
|
207
|
+
|
|
208
|
+
return {
|
|
209
|
+
interventions: visible.interventions,
|
|
210
|
+
loading: visible.loading,
|
|
211
|
+
refreshing: visible.refreshing,
|
|
212
|
+
mutating: mutationCount > 0,
|
|
213
|
+
error: visible.error,
|
|
214
|
+
refresh,
|
|
215
|
+
create,
|
|
216
|
+
resolve,
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function emptyState(key: string, loading: boolean) {
|
|
221
|
+
return {
|
|
222
|
+
key,
|
|
223
|
+
interventions: [] as InteractionIntervention[],
|
|
224
|
+
loading,
|
|
225
|
+
refreshing: false,
|
|
226
|
+
error: null as Error | null,
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function sortInterventions(
|
|
231
|
+
interventions: readonly InteractionIntervention[],
|
|
232
|
+
): InteractionIntervention[] {
|
|
233
|
+
return [...interventions].sort(
|
|
234
|
+
(left, right) => Date.parse(right.updatedAt) - Date.parse(left.updatedAt),
|
|
235
|
+
);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function asError(cause: unknown): Error {
|
|
239
|
+
return cause instanceof Error ? cause : new Error(String(cause));
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
export type InteractionInterventionsClient = EmbeddedInterventionClientLike;
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import type { StreamConnectionState, WorkspaceInteractionRevisionEvent } from "@opengeni/sdk";
|
|
2
|
+
import { useEffect, useRef } from "react";
|
|
3
|
+
import { usePageLiveActivity } from "./internal";
|
|
4
|
+
|
|
5
|
+
export type InteractionInvalidationOptions = {
|
|
6
|
+
workspaceId: string;
|
|
7
|
+
/** Distinguishes filtered projections of the same workspace catalog. */
|
|
8
|
+
key: string;
|
|
9
|
+
enabled: boolean;
|
|
10
|
+
event: WorkspaceInteractionRevisionEvent | null;
|
|
11
|
+
connectionState: StreamConnectionState | "idle" | "error";
|
|
12
|
+
/** Catalog responses carry the workspace revision; live-state lists do not. */
|
|
13
|
+
knownRevision?: number | undefined;
|
|
14
|
+
refresh: () => Promise<void>;
|
|
15
|
+
fallbackPollIntervalMs?: number | undefined;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Refetch authoritative lists on the shared latest-revision stream. Poll only
|
|
20
|
+
* while that stream is unavailable, preserving standalone-hook degradation
|
|
21
|
+
* without multiplying healthy-provider traffic.
|
|
22
|
+
*/
|
|
23
|
+
export function useInteractionInvalidation(options: InteractionInvalidationOptions): void {
|
|
24
|
+
const {
|
|
25
|
+
workspaceId,
|
|
26
|
+
key,
|
|
27
|
+
enabled,
|
|
28
|
+
event,
|
|
29
|
+
connectionState,
|
|
30
|
+
knownRevision,
|
|
31
|
+
refresh,
|
|
32
|
+
fallbackPollIntervalMs,
|
|
33
|
+
} = options;
|
|
34
|
+
const pageLive = usePageLiveActivity();
|
|
35
|
+
const lastUnversionedEvent = useRef<{ key: string; sequence: number }>({
|
|
36
|
+
key,
|
|
37
|
+
sequence: 0,
|
|
38
|
+
});
|
|
39
|
+
const eventSequence = event?.workspaceId === workspaceId ? event.sequence : 0;
|
|
40
|
+
|
|
41
|
+
useEffect(() => {
|
|
42
|
+
if (!enabled || eventSequence <= 0) return;
|
|
43
|
+
if (knownRevision !== undefined) {
|
|
44
|
+
if (eventSequence > knownRevision) void refresh();
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
if (lastUnversionedEvent.current.key !== key) {
|
|
48
|
+
lastUnversionedEvent.current = { key, sequence: 0 };
|
|
49
|
+
}
|
|
50
|
+
if (eventSequence <= lastUnversionedEvent.current.sequence) return;
|
|
51
|
+
lastUnversionedEvent.current = { key, sequence: eventSequence };
|
|
52
|
+
void refresh();
|
|
53
|
+
}, [enabled, eventSequence, key, knownRevision, refresh]);
|
|
54
|
+
|
|
55
|
+
useEffect(() => {
|
|
56
|
+
if (!enabled || !pageLive || connectionState === "live") return;
|
|
57
|
+
const interval = boundedPollInterval(fallbackPollIntervalMs ?? 3_000);
|
|
58
|
+
const timer = setInterval(() => void refresh(), interval);
|
|
59
|
+
return () => clearInterval(timer);
|
|
60
|
+
}, [connectionState, enabled, fallbackPollIntervalMs, pageLive, refresh]);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function boundedPollInterval(value: number): number {
|
|
64
|
+
if (!Number.isFinite(value)) return 3_000;
|
|
65
|
+
return Math.min(60_000, Math.max(1_000, Math.floor(value)));
|
|
66
|
+
}
|
|
@@ -2,6 +2,7 @@ import { useMemo } from "react";
|
|
|
2
2
|
import type { MachineState } from "@opengeni/sdk";
|
|
3
3
|
import type { SessionCapabilitiesState } from "./use-session-capabilities";
|
|
4
4
|
import { sandboxAcceptsLiveIo } from "../lib/sandbox-liveness";
|
|
5
|
+
import { connectionStatusForState } from "../types/machines";
|
|
5
6
|
|
|
6
7
|
/** The one truthful machine indicator. Honest staleness beats
|
|
7
8
|
* fake liveness: a cold/asleep box reads "offline — as of <time>", never "live". */
|
|
@@ -65,9 +66,18 @@ export function deriveMachineChip(input: DeriveMachineChipInput): MachineChip {
|
|
|
65
66
|
const asOf = input.capturedAt ?? null;
|
|
66
67
|
const offlineLabel = asOf ? `Offline — as of ${formatAsOf(asOf, now)}` : "Offline";
|
|
67
68
|
|
|
68
|
-
//
|
|
69
|
-
//
|
|
70
|
-
|
|
69
|
+
// A connected machine has no Modal group lease, so capability negotiation
|
|
70
|
+
// legitimately reports `cold` until a stream holder is minted. Its fleet
|
|
71
|
+
// probe is the authoritative reachability signal; do not label an online Mac
|
|
72
|
+
// "Offline" merely because there is no unrelated Modal lease.
|
|
73
|
+
const selfhostedOnline =
|
|
74
|
+
input.activeIsSelfhosted === true &&
|
|
75
|
+
input.activeMachineState != null &&
|
|
76
|
+
connectionStatusForState(input.activeMachineState) === "online";
|
|
77
|
+
|
|
78
|
+
// 1. A holder-confirmed warm provider or online connected machine is live.
|
|
79
|
+
// Draining remains teardown-owned and may already have lost its instance.
|
|
80
|
+
if (sandboxAcceptsLiveIo(input.liveness) || selfhostedOnline) {
|
|
71
81
|
return { state: "live", label: "Live", asOf: null };
|
|
72
82
|
}
|
|
73
83
|
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
CreateNetworkRouteRequest,
|
|
3
|
+
NetworkRoute,
|
|
4
|
+
NetworkRouteMutationResponse,
|
|
5
|
+
UpdateNetworkRouteRequest,
|
|
6
|
+
} from "@opengeni/sdk/interaction";
|
|
7
|
+
import { useCallback, useEffect, useRef, useState } from "react";
|
|
8
|
+
import type { EmbeddedBrowserInteractionClientLike } from "../client";
|
|
9
|
+
import {
|
|
10
|
+
type EmbeddedBrowserInteractionClientOverride,
|
|
11
|
+
useEmbeddedBrowserInteraction,
|
|
12
|
+
} from "../session-context";
|
|
13
|
+
import { useInteractionInvalidation } from "./use-interaction-invalidation";
|
|
14
|
+
|
|
15
|
+
export type UseNetworkRoutesOptions = EmbeddedBrowserInteractionClientOverride & {
|
|
16
|
+
enabled?: boolean | undefined;
|
|
17
|
+
includeArchived?: boolean | undefined;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export type UseNetworkRoutesResult = {
|
|
21
|
+
revision: number;
|
|
22
|
+
routes: NetworkRoute[];
|
|
23
|
+
loading: boolean;
|
|
24
|
+
refreshing: boolean;
|
|
25
|
+
mutating: boolean;
|
|
26
|
+
error: Error | null;
|
|
27
|
+
refresh: () => Promise<void>;
|
|
28
|
+
create: (
|
|
29
|
+
request: Omit<CreateNetworkRouteRequest, "operationId"> & { operationId?: string },
|
|
30
|
+
) => Promise<NetworkRouteMutationResponse>;
|
|
31
|
+
update: (
|
|
32
|
+
networkRouteId: string,
|
|
33
|
+
request: Omit<UpdateNetworkRouteRequest, "operationId"> & { operationId?: string },
|
|
34
|
+
) => Promise<NetworkRouteMutationResponse>;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
/** Workspace network-route registry. Route mutations never expose brokered credentials. */
|
|
38
|
+
export function useNetworkRoutes(options: UseNetworkRoutesOptions = {}): UseNetworkRoutesResult {
|
|
39
|
+
const { client, workspaceId, workspaceInteractionEvent, workspaceInteractionConnectionState } =
|
|
40
|
+
useEmbeddedBrowserInteraction(options);
|
|
41
|
+
const enabled = options.enabled ?? true;
|
|
42
|
+
const includeArchived = options.includeArchived ?? false;
|
|
43
|
+
const key = `${workspaceId}:${includeArchived ? "all" : "active"}`;
|
|
44
|
+
const [state, setState] = useState(() => emptyState(key, enabled));
|
|
45
|
+
const [mutationCount, setMutationCount] = useState(0);
|
|
46
|
+
const requestRef = useRef<{ id: number; controller: AbortController | null }>({
|
|
47
|
+
id: 0,
|
|
48
|
+
controller: null,
|
|
49
|
+
});
|
|
50
|
+
const mountedRef = useRef(true);
|
|
51
|
+
const visible = state.key === key ? state : emptyState(key, enabled);
|
|
52
|
+
|
|
53
|
+
const cancelLoad = useCallback(() => {
|
|
54
|
+
requestRef.current.controller?.abort();
|
|
55
|
+
requestRef.current = { id: requestRef.current.id + 1, controller: null };
|
|
56
|
+
}, []);
|
|
57
|
+
|
|
58
|
+
const load = useCallback(
|
|
59
|
+
async (foreground: boolean): Promise<void> => {
|
|
60
|
+
if (!enabled) return;
|
|
61
|
+
const id = requestRef.current.id + 1;
|
|
62
|
+
requestRef.current.controller?.abort();
|
|
63
|
+
const controller = new AbortController();
|
|
64
|
+
requestRef.current = { id, controller };
|
|
65
|
+
setState((current) => ({
|
|
66
|
+
...(current.key === key ? current : emptyState(key, true)),
|
|
67
|
+
loading: foreground,
|
|
68
|
+
refreshing: !foreground,
|
|
69
|
+
error: foreground || current.key !== key ? null : current.error,
|
|
70
|
+
}));
|
|
71
|
+
try {
|
|
72
|
+
const response = await client.listNetworkRoutes(workspaceId, {
|
|
73
|
+
includeArchived,
|
|
74
|
+
signal: controller.signal,
|
|
75
|
+
});
|
|
76
|
+
if (!mountedRef.current || requestRef.current.id !== id) return;
|
|
77
|
+
setState({
|
|
78
|
+
key,
|
|
79
|
+
revision: response.revision,
|
|
80
|
+
routes: sortRoutes(response.routes),
|
|
81
|
+
loading: false,
|
|
82
|
+
refreshing: false,
|
|
83
|
+
error: null,
|
|
84
|
+
});
|
|
85
|
+
} catch (cause) {
|
|
86
|
+
if (controller.signal.aborted || !mountedRef.current || requestRef.current.id !== id)
|
|
87
|
+
return;
|
|
88
|
+
setState((current) => ({
|
|
89
|
+
...(current.key === key ? current : emptyState(key, true)),
|
|
90
|
+
loading: false,
|
|
91
|
+
refreshing: false,
|
|
92
|
+
error: asError(cause),
|
|
93
|
+
}));
|
|
94
|
+
} finally {
|
|
95
|
+
if (requestRef.current.id === id) requestRef.current = { id, controller: null };
|
|
96
|
+
}
|
|
97
|
+
},
|
|
98
|
+
[client, enabled, includeArchived, key, workspaceId],
|
|
99
|
+
);
|
|
100
|
+
|
|
101
|
+
useEffect(() => {
|
|
102
|
+
mountedRef.current = true;
|
|
103
|
+
if (!enabled) {
|
|
104
|
+
cancelLoad();
|
|
105
|
+
setState(emptyState(key, false));
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
void load(true);
|
|
109
|
+
return () => {
|
|
110
|
+
mountedRef.current = false;
|
|
111
|
+
cancelLoad();
|
|
112
|
+
};
|
|
113
|
+
}, [cancelLoad, enabled, key, load]);
|
|
114
|
+
|
|
115
|
+
const refresh = useCallback(async () => await load(false), [load]);
|
|
116
|
+
useInteractionInvalidation({
|
|
117
|
+
workspaceId,
|
|
118
|
+
key,
|
|
119
|
+
enabled,
|
|
120
|
+
event: workspaceInteractionEvent,
|
|
121
|
+
connectionState: workspaceInteractionConnectionState,
|
|
122
|
+
knownRevision: visible.revision,
|
|
123
|
+
refresh,
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
const mutate = useCallback(async <T>(operation: () => Promise<T>): Promise<T> => {
|
|
127
|
+
setMutationCount((count) => count + 1);
|
|
128
|
+
try {
|
|
129
|
+
return await operation();
|
|
130
|
+
} finally {
|
|
131
|
+
if (mountedRef.current) setMutationCount((count) => Math.max(0, count - 1));
|
|
132
|
+
}
|
|
133
|
+
}, []);
|
|
134
|
+
|
|
135
|
+
const merge = useCallback(
|
|
136
|
+
(route: NetworkRoute) => {
|
|
137
|
+
setState((current) => {
|
|
138
|
+
if (current.key !== key) return current;
|
|
139
|
+
const routes = current.routes.filter((candidate) => candidate.id !== route.id);
|
|
140
|
+
if (includeArchived || route.status !== "archived") routes.push(route);
|
|
141
|
+
return { ...current, routes: sortRoutes(routes), error: null };
|
|
142
|
+
});
|
|
143
|
+
},
|
|
144
|
+
[includeArchived, key],
|
|
145
|
+
);
|
|
146
|
+
|
|
147
|
+
const create = useCallback(
|
|
148
|
+
async (
|
|
149
|
+
request: Omit<CreateNetworkRouteRequest, "operationId"> & { operationId?: string },
|
|
150
|
+
): Promise<NetworkRouteMutationResponse> =>
|
|
151
|
+
await mutate(async () => {
|
|
152
|
+
const response = await client.createNetworkRoute(workspaceId, {
|
|
153
|
+
...request,
|
|
154
|
+
operationId: request.operationId ?? crypto.randomUUID(),
|
|
155
|
+
});
|
|
156
|
+
merge(response.route);
|
|
157
|
+
return response;
|
|
158
|
+
}),
|
|
159
|
+
[client, merge, mutate, workspaceId],
|
|
160
|
+
);
|
|
161
|
+
|
|
162
|
+
const update = useCallback(
|
|
163
|
+
async (
|
|
164
|
+
networkRouteId: string,
|
|
165
|
+
request: Omit<UpdateNetworkRouteRequest, "operationId"> & { operationId?: string },
|
|
166
|
+
): Promise<NetworkRouteMutationResponse> =>
|
|
167
|
+
await mutate(async () => {
|
|
168
|
+
const response = await client.updateNetworkRoute(workspaceId, networkRouteId, {
|
|
169
|
+
...request,
|
|
170
|
+
operationId: request.operationId ?? crypto.randomUUID(),
|
|
171
|
+
});
|
|
172
|
+
merge(response.route);
|
|
173
|
+
return response;
|
|
174
|
+
}),
|
|
175
|
+
[client, merge, mutate, workspaceId],
|
|
176
|
+
);
|
|
177
|
+
|
|
178
|
+
return {
|
|
179
|
+
revision: visible.revision,
|
|
180
|
+
routes: visible.routes,
|
|
181
|
+
loading: visible.loading,
|
|
182
|
+
refreshing: visible.refreshing,
|
|
183
|
+
mutating: mutationCount > 0,
|
|
184
|
+
error: visible.error,
|
|
185
|
+
refresh,
|
|
186
|
+
create,
|
|
187
|
+
update,
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function emptyState(key: string, loading: boolean) {
|
|
192
|
+
return {
|
|
193
|
+
key,
|
|
194
|
+
revision: 0,
|
|
195
|
+
routes: [] as NetworkRoute[],
|
|
196
|
+
loading,
|
|
197
|
+
refreshing: false,
|
|
198
|
+
error: null as Error | null,
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function sortRoutes(routes: readonly NetworkRoute[]): NetworkRoute[] {
|
|
203
|
+
return [...routes].sort(
|
|
204
|
+
(left, right) =>
|
|
205
|
+
Number(left.status === "archived") - Number(right.status === "archived") ||
|
|
206
|
+
left.name.localeCompare(right.name),
|
|
207
|
+
);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function asError(cause: unknown): Error {
|
|
211
|
+
return cause instanceof Error ? cause : new Error(String(cause));
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
export type NetworkRoutesClient = EmbeddedBrowserInteractionClientLike;
|