@opengeni/react 4.0.2 → 5.0.2-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-WRVKY242.js} +1169 -1017
- package/dist/chunk-WRVKY242.js.map +1 -0
- package/dist/clipboard.js +1 -0
- 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/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/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
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { useEffect, useRef, useState } from "react";
|
|
2
|
+
import { copyTextToClipboard } from "./clipboard";
|
|
3
|
+
|
|
4
|
+
export type DeviceAuthorizationProps = {
|
|
5
|
+
userCode: string;
|
|
6
|
+
verificationUri: string;
|
|
7
|
+
providerLabel?: string;
|
|
8
|
+
description?: string;
|
|
9
|
+
className?: string;
|
|
10
|
+
codeAttributes?: Record<`data-${string}`, string>;
|
|
11
|
+
loadClipboard?: () => Promise<{ copyTextToClipboard(text: string): Promise<boolean> }>;
|
|
12
|
+
onCopyResult?: (copied: boolean) => void;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
/** Optional presentation for the existing model-account device APIs. It neither
|
|
16
|
+
* owns provider state nor assumes that opening a verification page completed it. */
|
|
17
|
+
export function DeviceAuthorization(props: DeviceAuthorizationProps) {
|
|
18
|
+
const [copied, setCopied] = useState(false);
|
|
19
|
+
const [copyError, setCopyError] = useState(false);
|
|
20
|
+
const generation = useRef(0);
|
|
21
|
+
const timer = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
22
|
+
useEffect(() => {
|
|
23
|
+
generation.current++;
|
|
24
|
+
setCopied(false);
|
|
25
|
+
setCopyError(false);
|
|
26
|
+
return () => {
|
|
27
|
+
// This is a live async-operation counter, not a captured DOM ref.
|
|
28
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
29
|
+
generation.current++;
|
|
30
|
+
if (timer.current) clearTimeout(timer.current);
|
|
31
|
+
};
|
|
32
|
+
}, [props.userCode]);
|
|
33
|
+
async function copy() {
|
|
34
|
+
const revision = ++generation.current;
|
|
35
|
+
if (timer.current) clearTimeout(timer.current);
|
|
36
|
+
setCopied(false);
|
|
37
|
+
setCopyError(false);
|
|
38
|
+
try {
|
|
39
|
+
const clipboard = props.loadClipboard ? await props.loadClipboard() : { copyTextToClipboard };
|
|
40
|
+
if (revision !== generation.current) return;
|
|
41
|
+
const success = await clipboard.copyTextToClipboard(props.userCode);
|
|
42
|
+
if (revision !== generation.current) return;
|
|
43
|
+
setCopied(success);
|
|
44
|
+
setCopyError(!success);
|
|
45
|
+
props.onCopyResult?.(success);
|
|
46
|
+
if (success)
|
|
47
|
+
timer.current = setTimeout(() => {
|
|
48
|
+
if (revision === generation.current) setCopied(false);
|
|
49
|
+
}, 1600);
|
|
50
|
+
} catch {
|
|
51
|
+
if (revision !== generation.current) return;
|
|
52
|
+
setCopyError(true);
|
|
53
|
+
props.onCopyResult?.(false);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
let href: string | undefined;
|
|
57
|
+
try {
|
|
58
|
+
const url = new URL(props.verificationUri);
|
|
59
|
+
if (["https:", "http:"].includes(url.protocol) && !url.username && !url.password)
|
|
60
|
+
href = props.verificationUri;
|
|
61
|
+
} catch {
|
|
62
|
+
/* A malformed provider URL must never become an executable link. */
|
|
63
|
+
}
|
|
64
|
+
return (
|
|
65
|
+
<section className={`og-connect ${props.className ?? ""}`} aria-label="Device authorization">
|
|
66
|
+
<p>{props.description ?? `Enter this code at ${props.providerLabel ?? "the provider"}.`}</p>
|
|
67
|
+
<div style={{ display: "flex", flexWrap: "wrap", gap: "0.5rem", alignItems: "center" }}>
|
|
68
|
+
<code {...props.codeAttributes} style={{ overflowWrap: "anywhere" }}>
|
|
69
|
+
{props.userCode}
|
|
70
|
+
</code>
|
|
71
|
+
<button
|
|
72
|
+
type="button"
|
|
73
|
+
aria-label={copied ? "Code copied" : "Copy code"}
|
|
74
|
+
onClick={() => void copy()}
|
|
75
|
+
>
|
|
76
|
+
{copied ? "Copied" : "Copy code"}
|
|
77
|
+
</button>
|
|
78
|
+
{href ? (
|
|
79
|
+
<a href={href} target="_blank" rel="noopener noreferrer">
|
|
80
|
+
Open {props.providerLabel ?? "auth page"}
|
|
81
|
+
</a>
|
|
82
|
+
) : (
|
|
83
|
+
<span role="alert">Authorization address unavailable.</span>
|
|
84
|
+
)}
|
|
85
|
+
</div>
|
|
86
|
+
{copyError ? <p role="alert">Couldn't copy the code. Copy it manually instead.</p> : null}
|
|
87
|
+
<p role="status">Waiting for authorization…</p>
|
|
88
|
+
</section>
|
|
89
|
+
);
|
|
90
|
+
}
|
|
@@ -194,8 +194,9 @@ export function useWorkspaceCapture(
|
|
|
194
194
|
return;
|
|
195
195
|
}
|
|
196
196
|
setDegradedReason(null);
|
|
197
|
-
//
|
|
198
|
-
//
|
|
197
|
+
// Expose metadata while a large signed manifest downloads. Consumers
|
|
198
|
+
// must distinguish this pending count from a resolved capture; zero
|
|
199
|
+
// working-tree files can still have committed-only branch changes.
|
|
199
200
|
setFileCount(res.stats.fileCount);
|
|
200
201
|
// Exactly one of manifest / manifestUrl is non-null (M2 contract). The inline
|
|
201
202
|
// manifest is the <200ms common case; a >2MB manifest is a signed URL hop.
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import { useEffect, useRef, useState } from "react";
|
|
2
|
+
import type { ExternalIdentityLink, ExternalIdentityLinkPage } from "@opengeni/sdk";
|
|
3
|
+
|
|
4
|
+
export type IdentityLinkAccountsClient = {
|
|
5
|
+
listIdentityLinks(workspaceId: string, cursor?: string): Promise<ExternalIdentityLinkPage>;
|
|
6
|
+
revokeIdentityLink(
|
|
7
|
+
workspaceId: string,
|
|
8
|
+
linkId: string,
|
|
9
|
+
expectedRevision: number,
|
|
10
|
+
): Promise<ExternalIdentityLink>;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
/** Participant-scoped inventory. Revocation never deletes native work or merges
|
|
14
|
+
* identities. The caller remounts this component when the signed-in user changes. */
|
|
15
|
+
export function IdentityLinkAccounts({
|
|
16
|
+
client,
|
|
17
|
+
workspaceId,
|
|
18
|
+
}: {
|
|
19
|
+
client: IdentityLinkAccountsClient;
|
|
20
|
+
workspaceId: string;
|
|
21
|
+
}) {
|
|
22
|
+
const [links, setLinks] = useState<ExternalIdentityLink[]>([]);
|
|
23
|
+
const [cursor, setCursor] = useState<string | null>(null);
|
|
24
|
+
const [busy, setBusy] = useState(true);
|
|
25
|
+
const [error, setError] = useState(false);
|
|
26
|
+
const [revision, setRevision] = useState(0);
|
|
27
|
+
const generation = useRef(0);
|
|
28
|
+
const inFlight = useRef(false);
|
|
29
|
+
useEffect(() => {
|
|
30
|
+
const current = ++generation.current;
|
|
31
|
+
inFlight.current = true;
|
|
32
|
+
setLinks([]);
|
|
33
|
+
setCursor(null);
|
|
34
|
+
setBusy(true);
|
|
35
|
+
setError(false);
|
|
36
|
+
void client
|
|
37
|
+
.listIdentityLinks(workspaceId)
|
|
38
|
+
.then(
|
|
39
|
+
(page) => {
|
|
40
|
+
if (current !== generation.current) return;
|
|
41
|
+
setLinks(page.links);
|
|
42
|
+
setCursor(page.nextCursor);
|
|
43
|
+
},
|
|
44
|
+
() => {
|
|
45
|
+
if (current === generation.current) setError(true);
|
|
46
|
+
},
|
|
47
|
+
)
|
|
48
|
+
.finally(() => {
|
|
49
|
+
if (current === generation.current) {
|
|
50
|
+
inFlight.current = false;
|
|
51
|
+
setBusy(false);
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
return () => {
|
|
55
|
+
// Invalidate live async work, rather than cleaning up a captured DOM node.
|
|
56
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
57
|
+
generation.current++;
|
|
58
|
+
};
|
|
59
|
+
}, [client, workspaceId, revision]);
|
|
60
|
+
|
|
61
|
+
async function act(link?: ExternalIdentityLink) {
|
|
62
|
+
if (inFlight.current || (!link && !cursor)) return;
|
|
63
|
+
const current = generation.current;
|
|
64
|
+
inFlight.current = true;
|
|
65
|
+
setBusy(true);
|
|
66
|
+
setError(false);
|
|
67
|
+
try {
|
|
68
|
+
if (link) {
|
|
69
|
+
const updated = await client.revokeIdentityLink(workspaceId, link.id, link.revision);
|
|
70
|
+
if (current === generation.current)
|
|
71
|
+
setLinks((values) =>
|
|
72
|
+
values.map((value) => (value.id === updated.id ? { ...value, ...updated } : value)),
|
|
73
|
+
);
|
|
74
|
+
} else {
|
|
75
|
+
const page = await client.listIdentityLinks(workspaceId, cursor!);
|
|
76
|
+
if (current === generation.current) {
|
|
77
|
+
setLinks((values) => [
|
|
78
|
+
...new Map([...values, ...page.links].map((value) => [value.id, value])).values(),
|
|
79
|
+
]);
|
|
80
|
+
setCursor(page.nextCursor);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
} catch {
|
|
84
|
+
if (current === generation.current) setError(true);
|
|
85
|
+
} finally {
|
|
86
|
+
if (current === generation.current) {
|
|
87
|
+
inFlight.current = false;
|
|
88
|
+
setBusy(false);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return (
|
|
93
|
+
<section
|
|
94
|
+
className="og-connect og-identity-link"
|
|
95
|
+
aria-label="Linked product access"
|
|
96
|
+
aria-busy={busy}
|
|
97
|
+
>
|
|
98
|
+
<h2>Linked product access</h2>
|
|
99
|
+
<p>
|
|
100
|
+
Manage products allowed to act as your account in this organization. Revoking a link stops
|
|
101
|
+
future linked access; it does not delete your work or undo an operation already started.
|
|
102
|
+
</p>
|
|
103
|
+
{error && (
|
|
104
|
+
<div role="alert">
|
|
105
|
+
Could not update account links. Reload to see current access.
|
|
106
|
+
<button type="button" disabled={busy} onClick={() => setRevision((value) => value + 1)}>
|
|
107
|
+
Reload links
|
|
108
|
+
</button>
|
|
109
|
+
</div>
|
|
110
|
+
)}
|
|
111
|
+
{busy && <p role="status">Loading account links…</p>}
|
|
112
|
+
{!busy && !error && links.length === 0 && (
|
|
113
|
+
<p>No products have linked access to this account.</p>
|
|
114
|
+
)}
|
|
115
|
+
<ul>
|
|
116
|
+
{links.map((link) => (
|
|
117
|
+
<li key={link.id}>
|
|
118
|
+
<p>
|
|
119
|
+
{link.externalIdentity?.source || "Product identity"}{" "}
|
|
120
|
+
<code>{link.externalIdentity?.externalId ?? link.externalIdentityId}</code>
|
|
121
|
+
</p>
|
|
122
|
+
<p>
|
|
123
|
+
Status: {link.status}.{" "}
|
|
124
|
+
{link.expiresAt
|
|
125
|
+
? `Expires ${new Date(link.expiresAt).toLocaleString()}.`
|
|
126
|
+
: "No automatic expiry."}
|
|
127
|
+
</p>
|
|
128
|
+
<p>{link.permissions.join(", ")}</p>
|
|
129
|
+
{(link.status === "active" || link.status === "pending") && (
|
|
130
|
+
<button type="button" disabled={busy} onClick={() => void act(link)}>
|
|
131
|
+
Revoke access
|
|
132
|
+
</button>
|
|
133
|
+
)}
|
|
134
|
+
</li>
|
|
135
|
+
))}
|
|
136
|
+
</ul>
|
|
137
|
+
{cursor && (
|
|
138
|
+
<button type="button" disabled={busy} onClick={() => void act()}>
|
|
139
|
+
Load more links
|
|
140
|
+
</button>
|
|
141
|
+
)}
|
|
142
|
+
</section>
|
|
143
|
+
);
|
|
144
|
+
}
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
import { useEffect, useRef, useState } from "react";
|
|
2
|
+
import type {
|
|
3
|
+
ExternalIdentityLink,
|
|
4
|
+
ConfirmExternalIdentityLinkRequest,
|
|
5
|
+
ExternalIdentityLinkPreview,
|
|
6
|
+
} from "@opengeni/sdk";
|
|
7
|
+
|
|
8
|
+
export type IdentityLinkClient = {
|
|
9
|
+
previewIdentityLink(
|
|
10
|
+
workspaceId: string,
|
|
11
|
+
linkId: string,
|
|
12
|
+
challenge: string,
|
|
13
|
+
): Promise<ExternalIdentityLinkPreview>;
|
|
14
|
+
confirmIdentityLink(
|
|
15
|
+
workspaceId: string,
|
|
16
|
+
linkId: string,
|
|
17
|
+
input: ConfirmExternalIdentityLinkRequest,
|
|
18
|
+
): Promise<ExternalIdentityLink>;
|
|
19
|
+
revokeIdentityLink(
|
|
20
|
+
workspaceId: string,
|
|
21
|
+
linkId: string,
|
|
22
|
+
expectedRevision: number,
|
|
23
|
+
): Promise<ExternalIdentityLink>;
|
|
24
|
+
};
|
|
25
|
+
export type IdentityLinkConsentProps = {
|
|
26
|
+
client: IdentityLinkClient;
|
|
27
|
+
workspaceId: string;
|
|
28
|
+
linkId: string;
|
|
29
|
+
challenge: string;
|
|
30
|
+
/** Host label is explanatory, never proof of the requesting application. */
|
|
31
|
+
className?: string;
|
|
32
|
+
onComplete?: (link: ExternalIdentityLink) => void;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
/** Optional native-login consent. Ordinary Connect does not use this surface.
|
|
36
|
+
* Permission choices only narrow the server's request; no automatic approval. */
|
|
37
|
+
export function IdentityLinkConsent({
|
|
38
|
+
client,
|
|
39
|
+
workspaceId,
|
|
40
|
+
linkId,
|
|
41
|
+
challenge,
|
|
42
|
+
className,
|
|
43
|
+
onComplete,
|
|
44
|
+
}: IdentityLinkConsentProps) {
|
|
45
|
+
const [link, setLink] = useState<ExternalIdentityLink | null>(null);
|
|
46
|
+
const [preview, setPreview] = useState<ExternalIdentityLinkPreview | null>(null);
|
|
47
|
+
const [selected, setSelected] = useState<ExternalIdentityLink["permissions"]>([]);
|
|
48
|
+
const [busy, setBusy] = useState(false);
|
|
49
|
+
const [error, setError] = useState(false);
|
|
50
|
+
const [retry, setRetry] = useState(0);
|
|
51
|
+
const generation = useRef(0);
|
|
52
|
+
useEffect(() => {
|
|
53
|
+
const current = ++generation.current;
|
|
54
|
+
setLink(null);
|
|
55
|
+
setPreview(null);
|
|
56
|
+
setSelected([]);
|
|
57
|
+
setBusy(true);
|
|
58
|
+
setError(false);
|
|
59
|
+
void client
|
|
60
|
+
.previewIdentityLink(workspaceId, linkId, challenge)
|
|
61
|
+
.then(
|
|
62
|
+
(result) => {
|
|
63
|
+
if (generation.current !== current) return;
|
|
64
|
+
setPreview(result);
|
|
65
|
+
setLink(result.link);
|
|
66
|
+
setSelected(result.link.permissions);
|
|
67
|
+
},
|
|
68
|
+
() => {
|
|
69
|
+
if (generation.current === current) setError(true);
|
|
70
|
+
},
|
|
71
|
+
)
|
|
72
|
+
.finally(() => {
|
|
73
|
+
if (generation.current === current) setBusy(false);
|
|
74
|
+
});
|
|
75
|
+
return () => {
|
|
76
|
+
// Invalidate live async work, rather than cleaning up a captured DOM node.
|
|
77
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
78
|
+
generation.current++;
|
|
79
|
+
};
|
|
80
|
+
}, [client, workspaceId, linkId, challenge, retry]);
|
|
81
|
+
async function mutate(revoke: boolean) {
|
|
82
|
+
if (!link || busy) return;
|
|
83
|
+
const current = generation.current;
|
|
84
|
+
setBusy(true);
|
|
85
|
+
setError(false);
|
|
86
|
+
try {
|
|
87
|
+
const result = revoke
|
|
88
|
+
? await client.revokeIdentityLink(workspaceId, linkId, link.revision)
|
|
89
|
+
: await client.confirmIdentityLink(workspaceId, linkId, {
|
|
90
|
+
challenge,
|
|
91
|
+
expectedRevision: link.revision,
|
|
92
|
+
permissions: selected,
|
|
93
|
+
});
|
|
94
|
+
if (generation.current !== current) return;
|
|
95
|
+
setLink(result);
|
|
96
|
+
onComplete?.(result);
|
|
97
|
+
} catch {
|
|
98
|
+
if (generation.current === current) setError(true);
|
|
99
|
+
} finally {
|
|
100
|
+
if (generation.current === current) setBusy(false);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
return (
|
|
104
|
+
<section
|
|
105
|
+
className={["og-connect og-identity-link", className].filter(Boolean).join(" ")}
|
|
106
|
+
aria-label="Link your account"
|
|
107
|
+
aria-busy={busy}
|
|
108
|
+
>
|
|
109
|
+
<h2>Link your OpenGeni account</h2>
|
|
110
|
+
<p>
|
|
111
|
+
Only continue if you started this request in a product you trust. Linking lets that product
|
|
112
|
+
act as your account with the permissions you select. It does not merge accounts or move
|
|
113
|
+
existing work.
|
|
114
|
+
</p>
|
|
115
|
+
{preview && (
|
|
116
|
+
<dl>
|
|
117
|
+
<dt>Organization</dt>
|
|
118
|
+
<dd>{preview.organizationId}</dd>
|
|
119
|
+
<dt>Product user</dt>
|
|
120
|
+
<dd>
|
|
121
|
+
{preview.externalIdentity.externalId} ({preview.externalIdentity.source})
|
|
122
|
+
</dd>
|
|
123
|
+
<dt>Your OpenGeni identity</dt>
|
|
124
|
+
<dd>{preview.nativeSubjectId}</dd>
|
|
125
|
+
</dl>
|
|
126
|
+
)}
|
|
127
|
+
{error && (
|
|
128
|
+
<div role="alert">
|
|
129
|
+
This request could not be completed. It may have expired, changed, or require you to sign
|
|
130
|
+
in to the right organization.
|
|
131
|
+
{!link && (
|
|
132
|
+
<button type="button" disabled={busy} onClick={() => setRetry((value) => value + 1)}>
|
|
133
|
+
Try again
|
|
134
|
+
</button>
|
|
135
|
+
)}
|
|
136
|
+
</div>
|
|
137
|
+
)}
|
|
138
|
+
{!link && busy && <p role="status">Loading the account-link request…</p>}
|
|
139
|
+
{link?.status === "pending" && (
|
|
140
|
+
<>
|
|
141
|
+
<p>
|
|
142
|
+
{link.expiresAt
|
|
143
|
+
? `Access ends ${new Date(link.expiresAt).toLocaleString()}.`
|
|
144
|
+
: "Access lasts until you or the product revoke this link."}{" "}
|
|
145
|
+
Your current permissions still apply.
|
|
146
|
+
</p>
|
|
147
|
+
<fieldset disabled={busy}>
|
|
148
|
+
<legend>Allow these permissions</legend>
|
|
149
|
+
{link.permissions.map((permission) => (
|
|
150
|
+
<label key={permission}>
|
|
151
|
+
<input
|
|
152
|
+
type="checkbox"
|
|
153
|
+
checked={selected.includes(permission)}
|
|
154
|
+
onChange={(event) =>
|
|
155
|
+
setSelected((values) =>
|
|
156
|
+
event.target.checked
|
|
157
|
+
? [...values, permission]
|
|
158
|
+
: values.filter((value) => value !== permission),
|
|
159
|
+
)
|
|
160
|
+
}
|
|
161
|
+
/>
|
|
162
|
+
<span>{permission.replaceAll(":", " · ")}</span>
|
|
163
|
+
</label>
|
|
164
|
+
))}
|
|
165
|
+
</fieldset>
|
|
166
|
+
<button
|
|
167
|
+
type="button"
|
|
168
|
+
disabled={busy || selected.length === 0}
|
|
169
|
+
onClick={() => void mutate(false)}
|
|
170
|
+
>
|
|
171
|
+
{busy ? "Saving…" : "Allow selected access"}
|
|
172
|
+
</button>
|
|
173
|
+
<p>You can close this page without granting access.</p>
|
|
174
|
+
</>
|
|
175
|
+
)}
|
|
176
|
+
{link?.status === "active" && (
|
|
177
|
+
<>
|
|
178
|
+
<p role="status">Account linked. Return to your product to continue.</p>
|
|
179
|
+
<button type="button" disabled={busy} onClick={() => void mutate(true)}>
|
|
180
|
+
Revoke this link
|
|
181
|
+
</button>
|
|
182
|
+
</>
|
|
183
|
+
)}
|
|
184
|
+
{link?.status === "revoked" && (
|
|
185
|
+
<p role="status">Link revoked. Your OpenGeni account and existing work are unchanged.</p>
|
|
186
|
+
)}
|
|
187
|
+
{link?.status === "expired" && (
|
|
188
|
+
<p role="status">This request has expired. Start a new request from your product.</p>
|
|
189
|
+
)}
|
|
190
|
+
</section>
|
|
191
|
+
);
|
|
192
|
+
}
|
package/src/session-ui.ts
CHANGED
|
@@ -8,6 +8,8 @@ export type {
|
|
|
8
8
|
HumanInputFormProps,
|
|
9
9
|
} from "./components/human-input-form";
|
|
10
10
|
export { HumanInputSurface } from "./components/human-input-surface";
|
|
11
|
+
export { ApprovalSurface } from "./components/approval-surface";
|
|
12
|
+
export type { ApprovalSurfaceProps, ApprovalSurfaceMessages } from "./components/approval-surface";
|
|
11
13
|
export type { HumanInputSurfaceProps } from "./components/human-input-surface";
|
|
12
14
|
export { MessageTimeline, TimelineRow } from "./components/message-timeline";
|
|
13
15
|
export type { MessageTimelineProps } from "./components/message-timeline";
|