@opengeni/react 4.0.2 → 5.0.4-canary.0
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 +42 -0
- package/dist/artifacts.js +24 -285
- package/dist/artifacts.js.map +1 -1
- package/dist/chunk-BRFMGY4L.js +274 -0
- package/dist/chunk-BRFMGY4L.js.map +1 -0
- package/dist/chunk-IWIDV7SB.js +1 -0
- package/dist/chunk-IWIDV7SB.js.map +1 -0
- package/dist/{chunk-CQG7YUJB.js → chunk-SCZ6SUBW.js} +1177 -1021
- package/dist/chunk-SCZ6SUBW.js.map +1 -0
- package/dist/clipboard.js +1 -0
- package/dist/components/markdown-table-layout.d.ts +4 -1
- package/dist/components/message-timeline.d.ts +7 -3
- package/dist/connect-accounts.d.ts +10 -0
- package/dist/connect-chooser.d.ts +11 -0
- package/dist/connect-panel.d.ts +10 -0
- package/dist/connect-setup.d.ts +12 -0
- package/dist/connect.d.ts +30 -0
- package/dist/connect.js +831 -0
- package/dist/connect.js.map +1 -0
- package/dist/device-authorization.d.ts +15 -0
- package/dist/identity-link-accounts.d.ts +11 -0
- package/dist/identity-link-consent.d.ts +18 -0
- package/dist/index.js +523 -663
- package/dist/index.js.map +1 -1
- package/dist/{markdown-table-layout-3Z3AWBY7.js → markdown-table-layout-PFUEZ2U4.js} +7 -4
- package/dist/{markdown-table-layout-3Z3AWBY7.js.map → markdown-table-layout-PFUEZ2U4.js.map} +1 -1
- package/dist/session-ui.d.ts +2 -0
- package/dist/session-ui.js +3 -1
- package/dist/sites-ui.d.ts +33 -0
- package/dist/sites.d.ts +3 -0
- package/dist/sites.js +288 -0
- package/dist/sites.js.map +1 -0
- package/package.json +16 -2
- package/src/components/markdown-table-layout.ts +6 -3
- package/src/components/markdown.tsx +11 -3
- package/src/components/message-timeline.tsx +22 -5
- package/src/components/sandbox-workspace.tsx +9 -2
- package/src/connect-accounts.tsx +171 -0
- package/src/connect-chooser.tsx +144 -0
- package/src/connect-panel.tsx +29 -0
- package/src/connect-setup.tsx +260 -0
- package/src/connect.ts +48 -0
- package/src/device-authorization.tsx +90 -0
- package/src/hooks/use-workspace-capture.ts +3 -2
- package/src/identity-link-accounts.tsx +144 -0
- package/src/identity-link-consent.tsx +192 -0
- package/src/session-ui.ts +2 -0
- package/src/sites-ui.tsx +385 -0
- package/src/sites.ts +3 -0
- package/src/timeline/tool-renderers.tsx +1 -1
- package/styles/connect.css +48 -0
- package/styles/connect.d.ts +1 -0
- package/dist/chunk-CQG7YUJB.js.map +0 -1
|
@@ -157,6 +157,8 @@ export type MessageTimelineProps = {
|
|
|
157
157
|
* a Reconnect button without a handler to run it.
|
|
158
158
|
*/
|
|
159
159
|
onReconnect?: ((item: AuthNeededItem) => void | Promise<void>) | undefined;
|
|
160
|
+
/** Host-owned inline connection setup. Return undefined to use the default recovery card. */
|
|
161
|
+
renderAuthNeeded?: ((item: AuthNeededItem) => ReactNode | undefined) | undefined;
|
|
160
162
|
/**
|
|
161
163
|
* Decide which durable authentication notices this timeline presents.
|
|
162
164
|
* Defaults to showing every notice. Embedded hosts can suppress notices for
|
|
@@ -407,6 +409,7 @@ export function MessageTimeline({
|
|
|
407
409
|
onOpenSession,
|
|
408
410
|
onMemoryClick,
|
|
409
411
|
onReconnect,
|
|
412
|
+
renderAuthNeeded,
|
|
410
413
|
shouldRenderAuthNeeded,
|
|
411
414
|
resolveProviderLogo,
|
|
412
415
|
toolRegistry = defaultToolRegistry,
|
|
@@ -910,6 +913,7 @@ export function MessageTimeline({
|
|
|
910
913
|
onOpenSession,
|
|
911
914
|
onMemoryClick,
|
|
912
915
|
onReconnect,
|
|
916
|
+
renderAuthNeeded,
|
|
913
917
|
resolveProviderLogo,
|
|
914
918
|
toolRegistry,
|
|
915
919
|
loadRetainedScreenshot,
|
|
@@ -925,6 +929,7 @@ export function MessageTimeline({
|
|
|
925
929
|
onMemoryClick,
|
|
926
930
|
onOpenSession,
|
|
927
931
|
onReconnect,
|
|
932
|
+
renderAuthNeeded,
|
|
928
933
|
renderMessageActions,
|
|
929
934
|
renderMessageText,
|
|
930
935
|
resolveProviderLogo,
|
|
@@ -2309,6 +2314,7 @@ type TimelineGroupBehaviorProps = {
|
|
|
2309
2314
|
onOpenSession: MessageTimelineProps["onOpenSession"];
|
|
2310
2315
|
onMemoryClick: MessageTimelineProps["onMemoryClick"];
|
|
2311
2316
|
onReconnect: MessageTimelineProps["onReconnect"];
|
|
2317
|
+
renderAuthNeeded: MessageTimelineProps["renderAuthNeeded"];
|
|
2312
2318
|
resolveProviderLogo: MessageTimelineProps["resolveProviderLogo"];
|
|
2313
2319
|
toolRegistry: ToolRegistry;
|
|
2314
2320
|
loadRetainedScreenshot: MessageTimelineProps["loadRetainedScreenshot"];
|
|
@@ -2373,6 +2379,7 @@ const TimelineGroupView = memo(function TimelineGroupView({
|
|
|
2373
2379
|
onOpenSession,
|
|
2374
2380
|
onMemoryClick,
|
|
2375
2381
|
onReconnect,
|
|
2382
|
+
renderAuthNeeded,
|
|
2376
2383
|
resolveProviderLogo,
|
|
2377
2384
|
toolRegistry,
|
|
2378
2385
|
loadRetainedScreenshot,
|
|
@@ -2393,6 +2400,8 @@ const TimelineGroupView = memo(function TimelineGroupView({
|
|
|
2393
2400
|
onOpenSession?: ((sessionId: string) => void) | undefined;
|
|
2394
2401
|
onMemoryClick?: ((memoryId: string) => void) | undefined;
|
|
2395
2402
|
onReconnect?: ((item: AuthNeededItem) => void | Promise<void>) | undefined;
|
|
2403
|
+
/** Host-owned inline connection setup. Return undefined to use the default recovery card. */
|
|
2404
|
+
renderAuthNeeded?: ((item: AuthNeededItem) => ReactNode | undefined) | undefined;
|
|
2396
2405
|
resolveProviderLogo?: ((providerDomain: string) => string | null | undefined) | undefined;
|
|
2397
2406
|
toolRegistry: ToolRegistry;
|
|
2398
2407
|
loadRetainedScreenshot?: RetainedScreenshotLoader | undefined;
|
|
@@ -2550,6 +2559,7 @@ const TimelineGroupView = memo(function TimelineGroupView({
|
|
|
2550
2559
|
onOpenSession,
|
|
2551
2560
|
onMemoryClick,
|
|
2552
2561
|
onReconnect,
|
|
2562
|
+
renderAuthNeeded,
|
|
2553
2563
|
resolveProviderLogo,
|
|
2554
2564
|
toolRegistry,
|
|
2555
2565
|
loadRetainedScreenshot,
|
|
@@ -2565,6 +2575,7 @@ const TimelineGroupView = memo(function TimelineGroupView({
|
|
|
2565
2575
|
onOpenSession={onOpenSession}
|
|
2566
2576
|
onMemoryClick={onMemoryClick}
|
|
2567
2577
|
onReconnect={onReconnect}
|
|
2578
|
+
renderAuthNeeded={renderAuthNeeded}
|
|
2568
2579
|
resolveProviderLogo={resolveProviderLogo}
|
|
2569
2580
|
toolRegistry={toolRegistry}
|
|
2570
2581
|
loadRetainedScreenshot={loadRetainedScreenshot}
|
|
@@ -2610,6 +2621,7 @@ const TimelineGroupView = memo(function TimelineGroupView({
|
|
|
2610
2621
|
renderMessageActions={renderMessageActions}
|
|
2611
2622
|
renderMessageText={renderMessageText}
|
|
2612
2623
|
onReconnect={onReconnect}
|
|
2624
|
+
renderAuthNeeded={renderAuthNeeded}
|
|
2613
2625
|
resolveProviderLogo={resolveProviderLogo}
|
|
2614
2626
|
onOpenSession={onOpenSession}
|
|
2615
2627
|
loadVideoArtifactPlayback={loadVideoArtifactPlayback}
|
|
@@ -2862,6 +2874,7 @@ export function TimelineRow({
|
|
|
2862
2874
|
renderMessageActions,
|
|
2863
2875
|
renderMessageText,
|
|
2864
2876
|
onReconnect,
|
|
2877
|
+
renderAuthNeeded,
|
|
2865
2878
|
resolveProviderLogo,
|
|
2866
2879
|
onOpenSession,
|
|
2867
2880
|
loadVideoArtifactPlayback,
|
|
@@ -2872,6 +2885,8 @@ export function TimelineRow({
|
|
|
2872
2885
|
| ((text: string, item: AgentMessageItem | UserMessageItem) => ReactNode)
|
|
2873
2886
|
| undefined;
|
|
2874
2887
|
onReconnect?: ((item: AuthNeededItem) => void | Promise<void>) | undefined;
|
|
2888
|
+
/** Host-owned inline connection setup. Return undefined to use the default recovery card. */
|
|
2889
|
+
renderAuthNeeded?: ((item: AuthNeededItem) => ReactNode | undefined) | undefined;
|
|
2875
2890
|
resolveProviderLogo?: ((providerDomain: string) => string | null | undefined) | undefined;
|
|
2876
2891
|
onOpenSession?: ((sessionId: string) => void) | undefined;
|
|
2877
2892
|
loadVideoArtifactPlayback?: VideoArtifactPlaybackLoader | undefined;
|
|
@@ -2915,11 +2930,13 @@ export function TimelineRow({
|
|
|
2915
2930
|
return <CompactionRow item={item} />;
|
|
2916
2931
|
case "auth-needed":
|
|
2917
2932
|
return (
|
|
2918
|
-
|
|
2919
|
-
|
|
2920
|
-
|
|
2921
|
-
|
|
2922
|
-
|
|
2933
|
+
renderAuthNeeded?.(item) ?? (
|
|
2934
|
+
<AuthNeededRow
|
|
2935
|
+
item={item}
|
|
2936
|
+
onReconnect={onReconnect}
|
|
2937
|
+
resolveProviderLogo={resolveProviderLogo}
|
|
2938
|
+
/>
|
|
2939
|
+
)
|
|
2923
2940
|
);
|
|
2924
2941
|
default:
|
|
2925
2942
|
return null;
|
|
@@ -699,7 +699,8 @@ export function useSandboxWorkspaceTabs(
|
|
|
699
699
|
(captureState.fileCount ?? 0) > 0 ||
|
|
700
700
|
(captureState.capture?.repos.some((repo) => (repo.branchDiff?.length ?? 0) > 0) ?? false);
|
|
701
701
|
const captureUnavailable =
|
|
702
|
-
|
|
702
|
+
!captureState.loading &&
|
|
703
|
+
((captureState.fileCount === 0 && !captureHasChanges) || captureState.error !== null);
|
|
703
704
|
if (defaultTabRef.current.value === null) {
|
|
704
705
|
if (initialTab && (!isWorkbenchSurface(initialTab) || surfaceSet.has(initialTab))) {
|
|
705
706
|
defaultTabRef.current.value = initialTab;
|
|
@@ -711,7 +712,13 @@ export function useSandboxWorkspaceTabs(
|
|
|
711
712
|
);
|
|
712
713
|
} else if (captureUnavailable && initialWorkspaceTab(events) === WORKBENCH_TAB_CHANGES) {
|
|
713
714
|
defaultTabRef.current.value = sourceDrivenDefaultTab(true, changesEnabled, filesEnabled);
|
|
714
|
-
} else if (
|
|
715
|
+
} else if (
|
|
716
|
+
!captureState.loading &&
|
|
717
|
+
(captureState.fileCount !== null || captureState.error !== null)
|
|
718
|
+
) {
|
|
719
|
+
// Metadata may arrive before a signed manifest. While that manifest is
|
|
720
|
+
// loading, neither a positive count nor zero (committed-only changes)
|
|
721
|
+
// proves an unavailable capture, so leave the default unresolved.
|
|
715
722
|
// No durable review surface exists. Files owns the explicit live-workspace
|
|
716
723
|
// gate; a user can open Changes deliberately if they need a fresh Git read.
|
|
717
724
|
defaultTabRef.current.value = sourceDrivenDefaultTab(false, changesEnabled, filesEnabled);
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import { useEffect, useRef, useState } from "react";
|
|
2
|
+
import type { ConnectAccount, ConnectController } from "@opengeni/connect";
|
|
3
|
+
import { useConnect } from "./connect";
|
|
4
|
+
|
|
5
|
+
export type ConnectAccountsProps = {
|
|
6
|
+
controller: ConnectController;
|
|
7
|
+
className?: string;
|
|
8
|
+
/** Enables explicit account-bound reconnect. Host supplies the exact destination. */
|
|
9
|
+
returnUrl?: string;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
/** Local credential revocation only. Unknown or changed versions must be
|
|
13
|
+
* refreshed; this component never retries a destructive operation implicitly. */
|
|
14
|
+
export function ConnectAccounts(props: ConnectAccountsProps) {
|
|
15
|
+
const [scope, setScope] = useState(props.controller);
|
|
16
|
+
const [generation, setGeneration] = useState(0);
|
|
17
|
+
if (scope !== props.controller) {
|
|
18
|
+
setScope(props.controller);
|
|
19
|
+
setGeneration(generation + 1);
|
|
20
|
+
}
|
|
21
|
+
return <ScopedAccounts key={generation} {...props} />;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function ScopedAccounts({ controller, className, returnUrl }: ConnectAccountsProps) {
|
|
25
|
+
const view = useConnect(controller);
|
|
26
|
+
const [accounts, setAccounts] = useState<ConnectAccount[] | null>(null);
|
|
27
|
+
const [selected, setSelected] = useState<string | null>(null);
|
|
28
|
+
const [busy, setBusy] = useState(false);
|
|
29
|
+
const [error, setError] = useState(false);
|
|
30
|
+
const [reload, setReload] = useState(0);
|
|
31
|
+
const mutation = useRef<AbortController | null>(null);
|
|
32
|
+
useEffect(() => () => mutation.current?.abort(), []);
|
|
33
|
+
useEffect(() => {
|
|
34
|
+
const abort = new AbortController();
|
|
35
|
+
setAccounts(null);
|
|
36
|
+
setSelected(null);
|
|
37
|
+
setError(false);
|
|
38
|
+
void Promise.resolve()
|
|
39
|
+
.then(() => {
|
|
40
|
+
abort.signal.throwIfAborted();
|
|
41
|
+
return controller.transport.accounts(controller.workspaceId, { signal: abort.signal });
|
|
42
|
+
})
|
|
43
|
+
.then((result) => {
|
|
44
|
+
if (!abort.signal.aborted) setAccounts(structuredClone(result));
|
|
45
|
+
})
|
|
46
|
+
.catch(() => {
|
|
47
|
+
if (!abort.signal.aborted) setError(true);
|
|
48
|
+
});
|
|
49
|
+
return () => abort.abort();
|
|
50
|
+
}, [controller, reload]);
|
|
51
|
+
|
|
52
|
+
const disconnect = async (account: ConnectAccount) => {
|
|
53
|
+
const expectedVersion = account.version;
|
|
54
|
+
if (
|
|
55
|
+
mutation.current ||
|
|
56
|
+
view.busy ||
|
|
57
|
+
expectedVersion === undefined ||
|
|
58
|
+
!Number.isSafeInteger(expectedVersion) ||
|
|
59
|
+
expectedVersion < 1
|
|
60
|
+
)
|
|
61
|
+
return;
|
|
62
|
+
const abort = new AbortController();
|
|
63
|
+
mutation.current = abort;
|
|
64
|
+
setBusy(true);
|
|
65
|
+
setError(false);
|
|
66
|
+
try {
|
|
67
|
+
await controller.transport.disconnect(controller.workspaceId, account.id, {
|
|
68
|
+
expectedVersion,
|
|
69
|
+
signal: abort.signal,
|
|
70
|
+
});
|
|
71
|
+
if (!abort.signal.aborted) setReload((value) => value + 1);
|
|
72
|
+
} catch {
|
|
73
|
+
if (!abort.signal.aborted) {
|
|
74
|
+
// Outcome may be unknown. Drop the inventory so a second click cannot
|
|
75
|
+
// replay against the stale selection; let the host reload live state.
|
|
76
|
+
setAccounts(null);
|
|
77
|
+
setError(true);
|
|
78
|
+
}
|
|
79
|
+
} finally {
|
|
80
|
+
if (!abort.signal.aborted) {
|
|
81
|
+
mutation.current = null;
|
|
82
|
+
setBusy(false);
|
|
83
|
+
setSelected(null);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
const reconnect = async (account: ConnectAccount) => {
|
|
88
|
+
if (!returnUrl || mutation.current || view.busy) return;
|
|
89
|
+
setError(false);
|
|
90
|
+
try {
|
|
91
|
+
await controller.begin({
|
|
92
|
+
providerId: account.providerId,
|
|
93
|
+
ownership: account.ownership,
|
|
94
|
+
reconnectAccountId: account.id,
|
|
95
|
+
returnUrl,
|
|
96
|
+
idempotencyKey: crypto.randomUUID(),
|
|
97
|
+
});
|
|
98
|
+
} catch {
|
|
99
|
+
setError(true);
|
|
100
|
+
}
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
return (
|
|
104
|
+
<section
|
|
105
|
+
className={className}
|
|
106
|
+
aria-label="Connected accounts"
|
|
107
|
+
aria-busy={busy || view.busy || (!accounts && !error)}
|
|
108
|
+
>
|
|
109
|
+
{error && (
|
|
110
|
+
<p role="alert">Account state could not be confirmed. Reload before trying again.</p>
|
|
111
|
+
)}
|
|
112
|
+
{!accounts && !error && <p role="status">Loading accounts…</p>}
|
|
113
|
+
{accounts?.length === 0 && <p role="status">No connected accounts.</p>}
|
|
114
|
+
{accounts && accounts.length > 0 && (
|
|
115
|
+
<ul>
|
|
116
|
+
{accounts.map((account) => (
|
|
117
|
+
<li key={`${account.providerId}:${account.id}`}>
|
|
118
|
+
<span>
|
|
119
|
+
{account.label} — {account.providerId} — {account.ownership} —{" "}
|
|
120
|
+
{account.status.replaceAll("_", " ")}
|
|
121
|
+
</span>
|
|
122
|
+
{returnUrl && (
|
|
123
|
+
<button
|
|
124
|
+
type="button"
|
|
125
|
+
disabled={busy || view.busy || account.status === "disabled"}
|
|
126
|
+
onClick={() => void reconnect(account)}
|
|
127
|
+
>
|
|
128
|
+
Reconnect {account.label}
|
|
129
|
+
</button>
|
|
130
|
+
)}
|
|
131
|
+
{selected === `${account.providerId}:${account.id}` ? (
|
|
132
|
+
<div role="group" aria-label={`Disconnect ${account.label}`}>
|
|
133
|
+
<p>
|
|
134
|
+
This removes local OpenGeni access. It does not revoke consent at the provider.
|
|
135
|
+
</p>
|
|
136
|
+
<button
|
|
137
|
+
type="button"
|
|
138
|
+
disabled={busy || view.busy}
|
|
139
|
+
onClick={() => void disconnect(account)}
|
|
140
|
+
>
|
|
141
|
+
Confirm disconnect
|
|
142
|
+
</button>
|
|
143
|
+
<button type="button" disabled={busy} onClick={() => setSelected(null)}>
|
|
144
|
+
Keep account
|
|
145
|
+
</button>
|
|
146
|
+
</div>
|
|
147
|
+
) : (
|
|
148
|
+
<button
|
|
149
|
+
type="button"
|
|
150
|
+
disabled={
|
|
151
|
+
busy ||
|
|
152
|
+
view.busy ||
|
|
153
|
+
account.status === "disabled" ||
|
|
154
|
+
!Number.isSafeInteger(account.version) ||
|
|
155
|
+
account.version! < 1
|
|
156
|
+
}
|
|
157
|
+
onClick={() => setSelected(`${account.providerId}:${account.id}`)}
|
|
158
|
+
>
|
|
159
|
+
Disconnect {account.label}
|
|
160
|
+
</button>
|
|
161
|
+
)}
|
|
162
|
+
</li>
|
|
163
|
+
))}
|
|
164
|
+
</ul>
|
|
165
|
+
)}
|
|
166
|
+
<button type="button" disabled={busy} onClick={() => setReload((value) => value + 1)}>
|
|
167
|
+
Reload accounts
|
|
168
|
+
</button>
|
|
169
|
+
</section>
|
|
170
|
+
);
|
|
171
|
+
}
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import { useEffect, useState, type FormEvent } from "react";
|
|
2
|
+
import type { ConnectController, ConnectProvider } from "@opengeni/connect";
|
|
3
|
+
import { useConnect } from "./connect";
|
|
4
|
+
|
|
5
|
+
export type ConnectChooserProps = {
|
|
6
|
+
controller: ConnectController;
|
|
7
|
+
/** Exact host return string; no completion parameters are added. */
|
|
8
|
+
returnUrl: string;
|
|
9
|
+
className?: string;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
/** Catalog readiness is supplied by the authenticated backend, never inferred
|
|
13
|
+
* from the existence of a provider definition. Host replaces the controller on
|
|
14
|
+
* actor/workspace change. This form does not open OAuth windows itself. */
|
|
15
|
+
export function ConnectChooser(props: ConnectChooserProps) {
|
|
16
|
+
// Remount scope-local selections synchronously when the controller changes,
|
|
17
|
+
// before effects can expose a previous actor's inventory for one frame.
|
|
18
|
+
const [scope, setScope] = useState(props.controller);
|
|
19
|
+
const [generation, setGeneration] = useState(0);
|
|
20
|
+
if (scope !== props.controller) {
|
|
21
|
+
setScope(props.controller);
|
|
22
|
+
setGeneration(generation + 1);
|
|
23
|
+
}
|
|
24
|
+
return <ScopedChooser key={generation} {...props} />;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function ScopedChooser({ controller, returnUrl, className }: ConnectChooserProps) {
|
|
28
|
+
const view = useConnect(controller);
|
|
29
|
+
const [catalog, setCatalog] = useState<ConnectProvider[] | null>(null);
|
|
30
|
+
const [failed, setFailed] = useState(false);
|
|
31
|
+
const [load, setLoad] = useState(0);
|
|
32
|
+
const [providerId, setProviderId] = useState("");
|
|
33
|
+
const [ownership, setOwnership] = useState("");
|
|
34
|
+
useEffect(() => {
|
|
35
|
+
const abort = new AbortController();
|
|
36
|
+
setCatalog(null);
|
|
37
|
+
setFailed(false);
|
|
38
|
+
setProviderId("");
|
|
39
|
+
setOwnership("");
|
|
40
|
+
// Promise boundary also handles a synchronously throwing injected transport.
|
|
41
|
+
void Promise.resolve()
|
|
42
|
+
.then(() => {
|
|
43
|
+
abort.signal.throwIfAborted();
|
|
44
|
+
return controller.transport.catalog(controller.workspaceId, { signal: abort.signal });
|
|
45
|
+
})
|
|
46
|
+
.then((providers) => {
|
|
47
|
+
if (!abort.signal.aborted) setCatalog(structuredClone(providers));
|
|
48
|
+
})
|
|
49
|
+
.catch(() => {
|
|
50
|
+
if (!abort.signal.aborted) setFailed(true);
|
|
51
|
+
});
|
|
52
|
+
return () => abort.abort();
|
|
53
|
+
}, [controller, load]);
|
|
54
|
+
const provider = catalog?.find((entry) => entry.id === providerId);
|
|
55
|
+
const canBegin =
|
|
56
|
+
provider?.readiness === "available" &&
|
|
57
|
+
(ownership === "personal" || ownership === "workspace") &&
|
|
58
|
+
provider.ownership.includes(ownership);
|
|
59
|
+
const submit = (event: FormEvent) => {
|
|
60
|
+
event.preventDefault();
|
|
61
|
+
if (!canBegin || view.busy || (ownership !== "personal" && ownership !== "workspace")) return;
|
|
62
|
+
setFailed(false);
|
|
63
|
+
try {
|
|
64
|
+
void controller
|
|
65
|
+
.begin({ providerId, ownership, returnUrl, idempotencyKey: crypto.randomUUID() })
|
|
66
|
+
.catch(() => setFailed(true));
|
|
67
|
+
} catch {
|
|
68
|
+
setFailed(true);
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
return (
|
|
72
|
+
<section
|
|
73
|
+
className={className}
|
|
74
|
+
aria-label="Choose a connection"
|
|
75
|
+
aria-busy={view.busy || (!catalog && !failed)}
|
|
76
|
+
>
|
|
77
|
+
{failed && (
|
|
78
|
+
<p role="alert">
|
|
79
|
+
Connections could not be loaded or started. Check setup status before retrying.
|
|
80
|
+
</p>
|
|
81
|
+
)}
|
|
82
|
+
{!catalog && !failed && <p role="status">Loading connections…</p>}
|
|
83
|
+
{catalog?.length === 0 && <p role="status">No connections are available.</p>}
|
|
84
|
+
{Boolean(catalog?.length) && (
|
|
85
|
+
<form onSubmit={submit}>
|
|
86
|
+
<fieldset disabled={view.busy}>
|
|
87
|
+
<legend>Provider and ownership</legend>
|
|
88
|
+
<label>
|
|
89
|
+
Provider
|
|
90
|
+
<select
|
|
91
|
+
required
|
|
92
|
+
value={providerId}
|
|
93
|
+
onChange={(event) => {
|
|
94
|
+
setProviderId(event.target.value);
|
|
95
|
+
setOwnership("");
|
|
96
|
+
}}
|
|
97
|
+
>
|
|
98
|
+
<option value="" disabled>
|
|
99
|
+
Choose a provider
|
|
100
|
+
</option>
|
|
101
|
+
{catalog!.map((entry) => (
|
|
102
|
+
<option
|
|
103
|
+
key={entry.id}
|
|
104
|
+
value={entry.id}
|
|
105
|
+
disabled={entry.readiness !== "available" || entry.ownership.length === 0}
|
|
106
|
+
>
|
|
107
|
+
{entry.label} — {entry.id} ({entry.readiness.replaceAll("_", " ")})
|
|
108
|
+
</option>
|
|
109
|
+
))}
|
|
110
|
+
</select>
|
|
111
|
+
</label>
|
|
112
|
+
{provider && (
|
|
113
|
+
<label>
|
|
114
|
+
Ownership
|
|
115
|
+
<select
|
|
116
|
+
required
|
|
117
|
+
value={ownership}
|
|
118
|
+
onChange={(event) => setOwnership(event.target.value)}
|
|
119
|
+
>
|
|
120
|
+
<option value="" disabled>
|
|
121
|
+
Choose ownership
|
|
122
|
+
</option>
|
|
123
|
+
{provider.ownership.map((choice) => (
|
|
124
|
+
<option key={choice} value={choice}>
|
|
125
|
+
{choice === "personal"
|
|
126
|
+
? "Personal — owned by you"
|
|
127
|
+
: "Workspace — shared connection"}
|
|
128
|
+
</option>
|
|
129
|
+
))}
|
|
130
|
+
</select>
|
|
131
|
+
</label>
|
|
132
|
+
)}
|
|
133
|
+
<button type="submit" disabled={!canBegin}>
|
|
134
|
+
Start setup
|
|
135
|
+
</button>
|
|
136
|
+
</fieldset>
|
|
137
|
+
</form>
|
|
138
|
+
)}
|
|
139
|
+
<button type="button" disabled={view.busy} onClick={() => setLoad(load + 1)}>
|
|
140
|
+
Reload providers
|
|
141
|
+
</button>
|
|
142
|
+
</section>
|
|
143
|
+
);
|
|
144
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { ConnectAccounts } from "./connect-accounts";
|
|
2
|
+
import { ConnectChooser } from "./connect-chooser";
|
|
3
|
+
import { ConnectSetup, type ConnectSetupProps } from "./connect-setup";
|
|
4
|
+
|
|
5
|
+
export type ConnectPanelProps = ConnectSetupProps & {
|
|
6
|
+
returnUrl: string;
|
|
7
|
+
title?: string;
|
|
8
|
+
showAccounts?: boolean;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
/** Optional composition over the same unstyled surfaces. Import
|
|
12
|
+
* @opengeni/react/connect.css for styling; the host retains navigation,
|
|
13
|
+
* transport, actor admission, and controller lifetime. */
|
|
14
|
+
export function ConnectPanel({
|
|
15
|
+
returnUrl,
|
|
16
|
+
title = "Connections",
|
|
17
|
+
showAccounts = true,
|
|
18
|
+
className,
|
|
19
|
+
...setup
|
|
20
|
+
}: ConnectPanelProps) {
|
|
21
|
+
return (
|
|
22
|
+
<div className={["og-connect", className].filter(Boolean).join(" ")}>
|
|
23
|
+
<h2>{title}</h2>
|
|
24
|
+
<ConnectChooser controller={setup.controller} returnUrl={returnUrl} />
|
|
25
|
+
<ConnectSetup {...setup} />
|
|
26
|
+
{showAccounts && <ConnectAccounts controller={setup.controller} returnUrl={returnUrl} />}
|
|
27
|
+
</div>
|
|
28
|
+
);
|
|
29
|
+
}
|