@stigmer/react 0.0.89 → 0.0.90
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/identity-provider/CreateIdentityProviderForm.d.ts.map +1 -1
- package/identity-provider/CreateIdentityProviderForm.js +60 -2
- package/identity-provider/CreateIdentityProviderForm.js.map +1 -1
- package/identity-provider/IdentityProviderDetailPanel.d.ts.map +1 -1
- package/identity-provider/IdentityProviderDetailPanel.js +87 -4
- package/identity-provider/IdentityProviderDetailPanel.js.map +1 -1
- package/identity-provider/IdentityProviderListPanel.js +5 -3
- package/identity-provider/IdentityProviderListPanel.js.map +1 -1
- package/identity-provider/IdentityProviderWizard.d.ts.map +1 -1
- package/identity-provider/IdentityProviderWizard.js +59 -4
- package/identity-provider/IdentityProviderWizard.js.map +1 -1
- package/index.d.ts +2 -0
- package/index.d.ts.map +1 -1
- package/index.js +2 -0
- package/index.js.map +1 -1
- package/package.json +7 -7
- package/platform-client/CreatePlatformClientForm.d.ts +42 -0
- package/platform-client/CreatePlatformClientForm.d.ts.map +1 -0
- package/platform-client/CreatePlatformClientForm.js +148 -0
- package/platform-client/CreatePlatformClientForm.js.map +1 -0
- package/platform-client/PlatformClientDetailPanel.d.ts +51 -0
- package/platform-client/PlatformClientDetailPanel.d.ts.map +1 -0
- package/platform-client/PlatformClientDetailPanel.js +247 -0
- package/platform-client/PlatformClientDetailPanel.js.map +1 -0
- package/platform-client/PlatformClientListPanel.d.ts +41 -0
- package/platform-client/PlatformClientListPanel.d.ts.map +1 -0
- package/platform-client/PlatformClientListPanel.js +123 -0
- package/platform-client/PlatformClientListPanel.js.map +1 -0
- package/platform-client/PlatformClientSecretAlert.d.ts +39 -0
- package/platform-client/PlatformClientSecretAlert.d.ts.map +1 -0
- package/platform-client/PlatformClientSecretAlert.js +74 -0
- package/platform-client/PlatformClientSecretAlert.js.map +1 -0
- package/platform-client/index.d.ts +11 -0
- package/platform-client/index.d.ts.map +1 -0
- package/platform-client/index.js +11 -0
- package/platform-client/index.js.map +1 -0
- package/platform-client/useCreatePlatformClient.d.ts +42 -0
- package/platform-client/useCreatePlatformClient.d.ts.map +1 -0
- package/platform-client/useCreatePlatformClient.js +49 -0
- package/platform-client/useCreatePlatformClient.js.map +1 -0
- package/platform-client/useDeletePlatformClient.d.ts +31 -0
- package/platform-client/useDeletePlatformClient.d.ts.map +1 -0
- package/platform-client/useDeletePlatformClient.js +42 -0
- package/platform-client/useDeletePlatformClient.js.map +1 -0
- package/platform-client/usePlatformClient.d.ts +37 -0
- package/platform-client/usePlatformClient.d.ts.map +1 -0
- package/platform-client/usePlatformClient.js +62 -0
- package/platform-client/usePlatformClient.js.map +1 -0
- package/platform-client/usePlatformClientList.d.ts +42 -0
- package/platform-client/usePlatformClientList.d.ts.map +1 -0
- package/platform-client/usePlatformClientList.js +71 -0
- package/platform-client/usePlatformClientList.js.map +1 -0
- package/platform-client/useRotatePlatformClientSecret.d.ts +35 -0
- package/platform-client/useRotatePlatformClientSecret.d.ts.map +1 -0
- package/platform-client/useRotatePlatformClientSecret.js +43 -0
- package/platform-client/useRotatePlatformClientSecret.js.map +1 -0
- package/platform-client/useUpdatePlatformClient.d.ts +39 -0
- package/platform-client/useUpdatePlatformClient.d.ts.map +1 -0
- package/platform-client/useUpdatePlatformClient.js +50 -0
- package/platform-client/useUpdatePlatformClient.js.map +1 -0
- package/src/identity-provider/CreateIdentityProviderForm.tsx +220 -0
- package/src/identity-provider/IdentityProviderDetailPanel.tsx +288 -6
- package/src/identity-provider/IdentityProviderListPanel.tsx +9 -2
- package/src/identity-provider/IdentityProviderWizard.tsx +231 -25
- package/src/index.ts +26 -0
- package/src/platform-client/CreatePlatformClientForm.tsx +519 -0
- package/src/platform-client/PlatformClientDetailPanel.tsx +898 -0
- package/src/platform-client/PlatformClientListPanel.tsx +413 -0
- package/src/platform-client/PlatformClientSecretAlert.tsx +252 -0
- package/src/platform-client/index.ts +49 -0
- package/src/platform-client/useCreatePlatformClient.ts +77 -0
- package/src/platform-client/useDeletePlatformClient.ts +64 -0
- package/src/platform-client/usePlatformClient.ts +86 -0
- package/src/platform-client/usePlatformClientList.ts +96 -0
- package/src/platform-client/useRotatePlatformClientSecret.ts +68 -0
- package/src/platform-client/useUpdatePlatformClient.ts +70 -0
- package/src/test/index.ts +6 -0
- package/src/{demo → test}/samples.ts +1 -1
- package/styles.css +1 -1
- package/test/__tests__/samples.test.d.ts.map +1 -0
- package/{demo → test}/__tests__/samples.test.js.map +1 -1
- package/test/index.d.ts +2 -0
- package/test/index.d.ts.map +1 -0
- package/test/index.js +6 -0
- package/test/index.js.map +1 -0
- package/{demo → test}/samples.d.ts +1 -1
- package/{demo → test}/samples.d.ts.map +1 -1
- package/{demo → test}/samples.js +1 -1
- package/{demo → test}/samples.js.map +1 -1
- package/demo/__tests__/demo-client.test.d.ts +0 -2
- package/demo/__tests__/demo-client.test.d.ts.map +0 -1
- package/demo/__tests__/demo-client.test.js +0 -133
- package/demo/__tests__/demo-client.test.js.map +0 -1
- package/demo/__tests__/fixtures.test.d.ts +0 -2
- package/demo/__tests__/fixtures.test.d.ts.map +0 -1
- package/demo/__tests__/fixtures.test.js +0 -135
- package/demo/__tests__/fixtures.test.js.map +0 -1
- package/demo/__tests__/samples.test.d.ts.map +0 -1
- package/demo/client.d.ts +0 -29
- package/demo/client.d.ts.map +0 -1
- package/demo/client.js +0 -52
- package/demo/client.js.map +0 -1
- package/demo/fixtures.d.ts +0 -194
- package/demo/fixtures.d.ts.map +0 -1
- package/demo/fixtures.js +0 -267
- package/demo/fixtures.js.map +0 -1
- package/demo/index.d.ts +0 -6
- package/demo/index.d.ts.map +0 -1
- package/demo/index.js +0 -6
- package/demo/index.js.map +0 -1
- package/demo/transport.d.ts +0 -59
- package/demo/transport.d.ts.map +0 -1
- package/demo/transport.js +0 -75
- package/demo/transport.js.map +0 -1
- package/demo/types.d.ts +0 -62
- package/demo/types.d.ts.map +0 -1
- package/demo/types.js +0 -16
- package/demo/types.js.map +0 -1
- package/src/demo/__tests__/demo-client.test.tsx +0 -213
- package/src/demo/__tests__/fixtures.test.ts +0 -214
- package/src/demo/client.ts +0 -78
- package/src/demo/fixtures.ts +0 -409
- package/src/demo/index.ts +0 -12
- package/src/demo/transport.ts +0 -116
- package/src/demo/types.ts +0 -69
- /package/src/{demo → test}/__tests__/samples.test.ts +0 -0
- /package/{demo → test}/__tests__/samples.test.d.ts +0 -0
- /package/{demo → test}/__tests__/samples.test.js +0 -0
|
@@ -0,0 +1,519 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
useCallback,
|
|
5
|
+
useState,
|
|
6
|
+
type FormEvent,
|
|
7
|
+
type KeyboardEvent,
|
|
8
|
+
} from "react";
|
|
9
|
+
import { cn } from "@stigmer/theme";
|
|
10
|
+
import { getUserMessage } from "@stigmer/sdk";
|
|
11
|
+
import { IamRole } from "@stigmer/protos/ai/stigmer/iam/v1/enum_pb";
|
|
12
|
+
import type { PlatformClientCreateResponse } from "@stigmer/protos/ai/stigmer/iam/platformclient/v1/io_pb";
|
|
13
|
+
import { useCreatePlatformClient } from "./useCreatePlatformClient";
|
|
14
|
+
|
|
15
|
+
/** Props for {@link CreatePlatformClientForm}. */
|
|
16
|
+
export interface CreatePlatformClientFormProps {
|
|
17
|
+
/** Organization slug — the PlatformClient will be created in this org. */
|
|
18
|
+
readonly org: string;
|
|
19
|
+
/**
|
|
20
|
+
* Fired with the full {@link PlatformClientCreateResponse} on
|
|
21
|
+
* success. The response includes the one-time raw `clientSecret`.
|
|
22
|
+
*/
|
|
23
|
+
readonly onCreated?: (response: PlatformClientCreateResponse) => void;
|
|
24
|
+
/** Fired when the user cancels creation. */
|
|
25
|
+
readonly onCancel?: () => void;
|
|
26
|
+
/** Additional CSS class names for the root container. */
|
|
27
|
+
readonly className?: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Form for creating a new platform client within an organization.
|
|
32
|
+
*
|
|
33
|
+
* Collects the required metadata (**name**) and optional spec fields:
|
|
34
|
+
* JIT provisioning toggles, expiry configuration, and allowed
|
|
35
|
+
* origins for CORS.
|
|
36
|
+
*
|
|
37
|
+
* On success it fires `onCreated` with the full
|
|
38
|
+
* {@link PlatformClientCreateResponse}, which includes the one-time
|
|
39
|
+
* raw client secret.
|
|
40
|
+
*
|
|
41
|
+
* All visual properties flow through `--stgm-*` design tokens.
|
|
42
|
+
*
|
|
43
|
+
* @example
|
|
44
|
+
* ```tsx
|
|
45
|
+
* <CreatePlatformClientForm
|
|
46
|
+
* org="acme"
|
|
47
|
+
* onCreated={(resp) => {
|
|
48
|
+
* showSecret(resp.clientSecret);
|
|
49
|
+
* refetch();
|
|
50
|
+
* }}
|
|
51
|
+
* onCancel={() => setShowForm(false)}
|
|
52
|
+
* />
|
|
53
|
+
* ```
|
|
54
|
+
*/
|
|
55
|
+
export function CreatePlatformClientForm({
|
|
56
|
+
org,
|
|
57
|
+
onCreated,
|
|
58
|
+
onCancel,
|
|
59
|
+
className,
|
|
60
|
+
}: CreatePlatformClientFormProps) {
|
|
61
|
+
const { create, isCreating, error, clearError } =
|
|
62
|
+
useCreatePlatformClient();
|
|
63
|
+
|
|
64
|
+
const [name, setName] = useState("");
|
|
65
|
+
const [neverExpires, setNeverExpires] = useState(true);
|
|
66
|
+
const [expiresAt, setExpiresAt] = useState("");
|
|
67
|
+
|
|
68
|
+
// JIT provisioning
|
|
69
|
+
const [autoProvision, setAutoProvision] = useState(true);
|
|
70
|
+
const [autoGrant, setAutoGrant] = useState(true);
|
|
71
|
+
const [autoGrantRole, setAutoGrantRole] = useState<IamRole>(
|
|
72
|
+
IamRole.iam_role_unspecified,
|
|
73
|
+
);
|
|
74
|
+
|
|
75
|
+
// CORS
|
|
76
|
+
const [origins, setOrigins] = useState<string[]>([]);
|
|
77
|
+
const [originInput, setOriginInput] = useState("");
|
|
78
|
+
|
|
79
|
+
const handleAutoProvisionChange = useCallback((v: boolean) => {
|
|
80
|
+
setAutoProvision(v);
|
|
81
|
+
if (!v) {
|
|
82
|
+
setAutoGrant(false);
|
|
83
|
+
setAutoGrantRole(IamRole.iam_role_unspecified);
|
|
84
|
+
}
|
|
85
|
+
}, []);
|
|
86
|
+
|
|
87
|
+
const handleAutoGrantChange = useCallback((v: boolean) => {
|
|
88
|
+
setAutoGrant(v);
|
|
89
|
+
if (v) setAutoProvision(true);
|
|
90
|
+
if (!v) setAutoGrantRole(IamRole.iam_role_unspecified);
|
|
91
|
+
}, []);
|
|
92
|
+
|
|
93
|
+
const addOrigin = useCallback(() => {
|
|
94
|
+
const trimmed = originInput.trim();
|
|
95
|
+
if (trimmed && !origins.includes(trimmed)) {
|
|
96
|
+
setOrigins((prev) => [...prev, trimmed]);
|
|
97
|
+
}
|
|
98
|
+
setOriginInput("");
|
|
99
|
+
}, [originInput, origins]);
|
|
100
|
+
|
|
101
|
+
const handleOriginKeyDown = useCallback(
|
|
102
|
+
(e: KeyboardEvent<HTMLInputElement>) => {
|
|
103
|
+
if (e.key === "Enter") {
|
|
104
|
+
e.preventDefault();
|
|
105
|
+
addOrigin();
|
|
106
|
+
}
|
|
107
|
+
},
|
|
108
|
+
[addOrigin],
|
|
109
|
+
);
|
|
110
|
+
|
|
111
|
+
const removeOrigin = useCallback((origin: string) => {
|
|
112
|
+
setOrigins((prev) => prev.filter((o) => o !== origin));
|
|
113
|
+
}, []);
|
|
114
|
+
|
|
115
|
+
const trimmedName = name.trim();
|
|
116
|
+
const canSubmit = trimmedName !== "" && !isCreating;
|
|
117
|
+
|
|
118
|
+
const handleSubmit = useCallback(
|
|
119
|
+
async (e: FormEvent) => {
|
|
120
|
+
e.preventDefault();
|
|
121
|
+
if (!canSubmit) return;
|
|
122
|
+
|
|
123
|
+
clearError();
|
|
124
|
+
try {
|
|
125
|
+
const response = await create({
|
|
126
|
+
name: trimmedName,
|
|
127
|
+
org,
|
|
128
|
+
neverExpires,
|
|
129
|
+
...(!neverExpires &&
|
|
130
|
+
expiresAt && { expiresAt: new Date(expiresAt).toISOString() }),
|
|
131
|
+
autoProvisionAccounts: autoProvision,
|
|
132
|
+
autoGrantOnOrg: autoGrant,
|
|
133
|
+
...(autoGrant &&
|
|
134
|
+
autoGrantRole !== IamRole.iam_role_unspecified && {
|
|
135
|
+
autoGrantRole,
|
|
136
|
+
}),
|
|
137
|
+
...(origins.length > 0 && { allowedOrigins: origins }),
|
|
138
|
+
});
|
|
139
|
+
onCreated?.(response);
|
|
140
|
+
} catch {
|
|
141
|
+
// error state is managed by useCreatePlatformClient
|
|
142
|
+
}
|
|
143
|
+
},
|
|
144
|
+
[
|
|
145
|
+
canSubmit,
|
|
146
|
+
trimmedName,
|
|
147
|
+
org,
|
|
148
|
+
neverExpires,
|
|
149
|
+
expiresAt,
|
|
150
|
+
autoProvision,
|
|
151
|
+
autoGrant,
|
|
152
|
+
autoGrantRole,
|
|
153
|
+
origins,
|
|
154
|
+
create,
|
|
155
|
+
clearError,
|
|
156
|
+
onCreated,
|
|
157
|
+
],
|
|
158
|
+
);
|
|
159
|
+
|
|
160
|
+
return (
|
|
161
|
+
<form onSubmit={handleSubmit} className={cn("space-y-3", className)}>
|
|
162
|
+
<FormField
|
|
163
|
+
id="stgm-pc-name"
|
|
164
|
+
label="Name"
|
|
165
|
+
value={name}
|
|
166
|
+
onChange={setName}
|
|
167
|
+
placeholder="e.g. my-saas-backend"
|
|
168
|
+
disabled={isCreating}
|
|
169
|
+
required
|
|
170
|
+
/>
|
|
171
|
+
|
|
172
|
+
{/* Expiry */}
|
|
173
|
+
<fieldset className="space-y-2" disabled={isCreating}>
|
|
174
|
+
<legend className="text-xs font-medium text-foreground">
|
|
175
|
+
Expiry
|
|
176
|
+
</legend>
|
|
177
|
+
<ToggleSwitch
|
|
178
|
+
checked={neverExpires}
|
|
179
|
+
onChange={setNeverExpires}
|
|
180
|
+
label="Never expires"
|
|
181
|
+
disabled={isCreating}
|
|
182
|
+
/>
|
|
183
|
+
{!neverExpires && (
|
|
184
|
+
<div className="space-y-1">
|
|
185
|
+
<label
|
|
186
|
+
htmlFor="stgm-pc-expires-at"
|
|
187
|
+
className="text-xs font-medium text-foreground"
|
|
188
|
+
>
|
|
189
|
+
Expires at
|
|
190
|
+
</label>
|
|
191
|
+
<input
|
|
192
|
+
id="stgm-pc-expires-at"
|
|
193
|
+
type="datetime-local"
|
|
194
|
+
value={expiresAt}
|
|
195
|
+
onChange={(e) => setExpiresAt(e.target.value)}
|
|
196
|
+
disabled={isCreating}
|
|
197
|
+
className={cn(
|
|
198
|
+
"w-full rounded-md border border-input bg-background px-2.5 py-1.5 text-xs text-foreground",
|
|
199
|
+
"focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring",
|
|
200
|
+
"disabled:pointer-events-none disabled:opacity-50",
|
|
201
|
+
)}
|
|
202
|
+
/>
|
|
203
|
+
</div>
|
|
204
|
+
)}
|
|
205
|
+
</fieldset>
|
|
206
|
+
|
|
207
|
+
{/* JIT provisioning */}
|
|
208
|
+
<fieldset className="space-y-2.5" disabled={isCreating}>
|
|
209
|
+
<hr className="border-border/40" />
|
|
210
|
+
<legend className="text-xs font-medium text-foreground">
|
|
211
|
+
JIT provisioning
|
|
212
|
+
</legend>
|
|
213
|
+
<p className="text-[0.65rem] text-muted-foreground">
|
|
214
|
+
Configure automatic account creation and role assignment for
|
|
215
|
+
users authenticated via this platform client.
|
|
216
|
+
</p>
|
|
217
|
+
|
|
218
|
+
<ToggleSwitch
|
|
219
|
+
checked={autoProvision}
|
|
220
|
+
onChange={handleAutoProvisionChange}
|
|
221
|
+
label="Auto-provision accounts"
|
|
222
|
+
hint="Create a Stigmer identity account automatically on first token mint"
|
|
223
|
+
disabled={isCreating}
|
|
224
|
+
/>
|
|
225
|
+
|
|
226
|
+
<ToggleSwitch
|
|
227
|
+
checked={autoGrant}
|
|
228
|
+
onChange={handleAutoGrantChange}
|
|
229
|
+
label="Auto-grant on organization"
|
|
230
|
+
hint="Grant a role on the owning organization when an account is provisioned"
|
|
231
|
+
disabled={isCreating || !autoProvision}
|
|
232
|
+
/>
|
|
233
|
+
|
|
234
|
+
{autoGrant && (
|
|
235
|
+
<div className="space-y-1">
|
|
236
|
+
<label
|
|
237
|
+
htmlFor="stgm-pc-grant-role"
|
|
238
|
+
className="text-xs font-medium text-foreground"
|
|
239
|
+
>
|
|
240
|
+
Auto-grant role
|
|
241
|
+
</label>
|
|
242
|
+
<select
|
|
243
|
+
id="stgm-pc-grant-role"
|
|
244
|
+
value={String(autoGrantRole)}
|
|
245
|
+
onChange={(e) =>
|
|
246
|
+
setAutoGrantRole(Number(e.target.value) as IamRole)
|
|
247
|
+
}
|
|
248
|
+
disabled={isCreating}
|
|
249
|
+
className={cn(
|
|
250
|
+
"w-full rounded-md border border-input bg-background px-2.5 py-1.5 text-xs text-foreground",
|
|
251
|
+
"focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring",
|
|
252
|
+
"disabled:pointer-events-none disabled:opacity-50",
|
|
253
|
+
)}
|
|
254
|
+
>
|
|
255
|
+
{JIT_ROLE_OPTIONS.map((opt) => (
|
|
256
|
+
<option key={opt.value} value={opt.value}>
|
|
257
|
+
{opt.label}
|
|
258
|
+
</option>
|
|
259
|
+
))}
|
|
260
|
+
</select>
|
|
261
|
+
<p className="text-[0.65rem] text-muted-foreground">
|
|
262
|
+
Role granted automatically — org admins can upgrade later
|
|
263
|
+
</p>
|
|
264
|
+
</div>
|
|
265
|
+
)}
|
|
266
|
+
</fieldset>
|
|
267
|
+
|
|
268
|
+
{/* Allowed origins */}
|
|
269
|
+
<fieldset className="space-y-2" disabled={isCreating}>
|
|
270
|
+
<hr className="border-border/40" />
|
|
271
|
+
<legend className="text-xs font-medium text-foreground">
|
|
272
|
+
Allowed origins
|
|
273
|
+
</legend>
|
|
274
|
+
<p className="text-[0.65rem] text-muted-foreground">
|
|
275
|
+
Browser origins permitted to use tokens minted by this client.
|
|
276
|
+
Leave empty to allow all origins.
|
|
277
|
+
</p>
|
|
278
|
+
|
|
279
|
+
<div className="flex items-center gap-2">
|
|
280
|
+
<input
|
|
281
|
+
type="text"
|
|
282
|
+
value={originInput}
|
|
283
|
+
onChange={(e) => setOriginInput(e.target.value)}
|
|
284
|
+
onKeyDown={handleOriginKeyDown}
|
|
285
|
+
onBlur={() => {
|
|
286
|
+
if (originInput.trim()) addOrigin();
|
|
287
|
+
}}
|
|
288
|
+
placeholder="https://example.com"
|
|
289
|
+
disabled={isCreating}
|
|
290
|
+
className={cn(
|
|
291
|
+
"min-w-0 flex-1 rounded-md border border-input bg-background px-2.5 py-1.5 text-xs text-foreground",
|
|
292
|
+
"placeholder:text-muted-foreground",
|
|
293
|
+
"focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring",
|
|
294
|
+
"disabled:pointer-events-none disabled:opacity-50",
|
|
295
|
+
)}
|
|
296
|
+
/>
|
|
297
|
+
<button
|
|
298
|
+
type="button"
|
|
299
|
+
onClick={addOrigin}
|
|
300
|
+
disabled={isCreating || !originInput.trim()}
|
|
301
|
+
className={cn(
|
|
302
|
+
"shrink-0 rounded-md px-2.5 py-1.5 text-xs font-medium",
|
|
303
|
+
"text-muted-foreground hover:text-foreground hover:bg-accent/50",
|
|
304
|
+
"disabled:pointer-events-none disabled:opacity-50",
|
|
305
|
+
"transition-colors",
|
|
306
|
+
)}
|
|
307
|
+
>
|
|
308
|
+
Add
|
|
309
|
+
</button>
|
|
310
|
+
</div>
|
|
311
|
+
|
|
312
|
+
{origins.length > 0 && (
|
|
313
|
+
<div className="flex flex-wrap gap-1.5">
|
|
314
|
+
{origins.map((origin) => (
|
|
315
|
+
<span
|
|
316
|
+
key={origin}
|
|
317
|
+
className="inline-flex items-center gap-1 rounded-full border border-border/60 bg-muted/40 px-2 py-0.5 text-[0.65rem] font-mono text-foreground"
|
|
318
|
+
>
|
|
319
|
+
{origin}
|
|
320
|
+
<button
|
|
321
|
+
type="button"
|
|
322
|
+
onClick={() => removeOrigin(origin)}
|
|
323
|
+
disabled={isCreating}
|
|
324
|
+
aria-label={`Remove ${origin}`}
|
|
325
|
+
className="text-muted-foreground hover:text-destructive transition-colors"
|
|
326
|
+
>
|
|
327
|
+
<XIcon />
|
|
328
|
+
</button>
|
|
329
|
+
</span>
|
|
330
|
+
))}
|
|
331
|
+
</div>
|
|
332
|
+
)}
|
|
333
|
+
</fieldset>
|
|
334
|
+
|
|
335
|
+
{error && (
|
|
336
|
+
<p className="text-destructive text-[0.65rem]" role="alert">
|
|
337
|
+
{getUserMessage(error)}
|
|
338
|
+
</p>
|
|
339
|
+
)}
|
|
340
|
+
|
|
341
|
+
<div className="flex items-center gap-2 pt-1">
|
|
342
|
+
<button
|
|
343
|
+
type="submit"
|
|
344
|
+
disabled={!canSubmit}
|
|
345
|
+
className={cn(
|
|
346
|
+
"inline-flex items-center gap-1.5 rounded-md px-3 py-1.5 text-xs font-medium",
|
|
347
|
+
"bg-primary text-primary-foreground hover:bg-primary/90",
|
|
348
|
+
"disabled:pointer-events-none disabled:opacity-40",
|
|
349
|
+
)}
|
|
350
|
+
>
|
|
351
|
+
{isCreating && <SpinnerIcon />}
|
|
352
|
+
Create platform client
|
|
353
|
+
</button>
|
|
354
|
+
|
|
355
|
+
{onCancel && (
|
|
356
|
+
<button
|
|
357
|
+
type="button"
|
|
358
|
+
onClick={onCancel}
|
|
359
|
+
disabled={isCreating}
|
|
360
|
+
className={cn(
|
|
361
|
+
"rounded-md px-3 py-1.5 text-xs",
|
|
362
|
+
"text-muted-foreground hover:text-foreground hover:bg-accent/50",
|
|
363
|
+
"disabled:pointer-events-none disabled:opacity-50",
|
|
364
|
+
)}
|
|
365
|
+
>
|
|
366
|
+
Cancel
|
|
367
|
+
</button>
|
|
368
|
+
)}
|
|
369
|
+
</div>
|
|
370
|
+
</form>
|
|
371
|
+
);
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
// ---------------------------------------------------------------------------
|
|
375
|
+
// Constants
|
|
376
|
+
// ---------------------------------------------------------------------------
|
|
377
|
+
|
|
378
|
+
const JIT_ROLE_OPTIONS: readonly {
|
|
379
|
+
readonly value: string;
|
|
380
|
+
readonly label: string;
|
|
381
|
+
}[] = [
|
|
382
|
+
{ value: String(IamRole.iam_role_unspecified), label: "Default (viewer)" },
|
|
383
|
+
{ value: String(IamRole.viewer), label: "Viewer" },
|
|
384
|
+
{ value: String(IamRole.member), label: "Member" },
|
|
385
|
+
{ value: String(IamRole.admin), label: "Admin" },
|
|
386
|
+
];
|
|
387
|
+
|
|
388
|
+
// ---------------------------------------------------------------------------
|
|
389
|
+
// Internal components
|
|
390
|
+
// ---------------------------------------------------------------------------
|
|
391
|
+
|
|
392
|
+
function FormField({
|
|
393
|
+
id,
|
|
394
|
+
label,
|
|
395
|
+
value,
|
|
396
|
+
onChange,
|
|
397
|
+
placeholder,
|
|
398
|
+
hint,
|
|
399
|
+
disabled,
|
|
400
|
+
required,
|
|
401
|
+
}: {
|
|
402
|
+
id: string;
|
|
403
|
+
label: string;
|
|
404
|
+
value: string;
|
|
405
|
+
onChange: (v: string) => void;
|
|
406
|
+
placeholder: string;
|
|
407
|
+
hint?: string;
|
|
408
|
+
disabled: boolean;
|
|
409
|
+
required?: boolean;
|
|
410
|
+
}) {
|
|
411
|
+
return (
|
|
412
|
+
<div className="space-y-1">
|
|
413
|
+
<label htmlFor={id} className="text-xs font-medium text-foreground">
|
|
414
|
+
{label}
|
|
415
|
+
</label>
|
|
416
|
+
<input
|
|
417
|
+
id={id}
|
|
418
|
+
type="text"
|
|
419
|
+
value={value}
|
|
420
|
+
onChange={(e) => onChange(e.target.value)}
|
|
421
|
+
placeholder={placeholder}
|
|
422
|
+
disabled={disabled}
|
|
423
|
+
required={required}
|
|
424
|
+
className={cn(
|
|
425
|
+
"w-full rounded-md border border-input bg-background px-2.5 py-1.5 text-xs text-foreground",
|
|
426
|
+
"placeholder:text-muted-foreground",
|
|
427
|
+
"focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring",
|
|
428
|
+
"disabled:pointer-events-none disabled:opacity-50",
|
|
429
|
+
)}
|
|
430
|
+
/>
|
|
431
|
+
{hint && (
|
|
432
|
+
<p className="text-[0.65rem] text-muted-foreground">{hint}</p>
|
|
433
|
+
)}
|
|
434
|
+
</div>
|
|
435
|
+
);
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
function ToggleSwitch({
|
|
439
|
+
checked,
|
|
440
|
+
onChange,
|
|
441
|
+
label,
|
|
442
|
+
hint,
|
|
443
|
+
disabled,
|
|
444
|
+
}: {
|
|
445
|
+
checked: boolean;
|
|
446
|
+
onChange: (v: boolean) => void;
|
|
447
|
+
label: string;
|
|
448
|
+
hint?: string;
|
|
449
|
+
disabled?: boolean;
|
|
450
|
+
}) {
|
|
451
|
+
return (
|
|
452
|
+
<div className="space-y-0.5">
|
|
453
|
+
<div className="flex items-center gap-2">
|
|
454
|
+
<button
|
|
455
|
+
type="button"
|
|
456
|
+
role="switch"
|
|
457
|
+
aria-checked={checked}
|
|
458
|
+
onClick={() => onChange(!checked)}
|
|
459
|
+
disabled={disabled}
|
|
460
|
+
className={cn(
|
|
461
|
+
"relative inline-flex h-5 w-9 shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors",
|
|
462
|
+
checked ? "bg-primary" : "bg-muted",
|
|
463
|
+
"disabled:pointer-events-none disabled:opacity-50",
|
|
464
|
+
)}
|
|
465
|
+
>
|
|
466
|
+
<span
|
|
467
|
+
className={cn(
|
|
468
|
+
"pointer-events-none inline-block h-4 w-4 rounded-full bg-background shadow-sm ring-0 transition-transform",
|
|
469
|
+
checked ? "translate-x-4" : "translate-x-0",
|
|
470
|
+
)}
|
|
471
|
+
/>
|
|
472
|
+
</button>
|
|
473
|
+
<span className="text-xs font-medium text-foreground">{label}</span>
|
|
474
|
+
</div>
|
|
475
|
+
{hint && (
|
|
476
|
+
<p className="pl-11 text-[0.65rem] text-muted-foreground">{hint}</p>
|
|
477
|
+
)}
|
|
478
|
+
</div>
|
|
479
|
+
);
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
// ---------------------------------------------------------------------------
|
|
483
|
+
// Icons
|
|
484
|
+
// ---------------------------------------------------------------------------
|
|
485
|
+
|
|
486
|
+
function XIcon() {
|
|
487
|
+
return (
|
|
488
|
+
<svg
|
|
489
|
+
width="10"
|
|
490
|
+
height="10"
|
|
491
|
+
viewBox="0 0 16 16"
|
|
492
|
+
fill="none"
|
|
493
|
+
stroke="currentColor"
|
|
494
|
+
strokeWidth="2"
|
|
495
|
+
strokeLinecap="round"
|
|
496
|
+
aria-hidden="true"
|
|
497
|
+
>
|
|
498
|
+
<path d="M4 4l8 8M12 4l-8 8" />
|
|
499
|
+
</svg>
|
|
500
|
+
);
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
function SpinnerIcon() {
|
|
504
|
+
return (
|
|
505
|
+
<svg
|
|
506
|
+
width="12"
|
|
507
|
+
height="12"
|
|
508
|
+
viewBox="0 0 16 16"
|
|
509
|
+
fill="none"
|
|
510
|
+
stroke="currentColor"
|
|
511
|
+
strokeWidth="2"
|
|
512
|
+
strokeLinecap="round"
|
|
513
|
+
className="animate-spin"
|
|
514
|
+
aria-hidden="true"
|
|
515
|
+
>
|
|
516
|
+
<path d="M8 2a6 6 0 1 0 6 6" />
|
|
517
|
+
</svg>
|
|
518
|
+
);
|
|
519
|
+
}
|