@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,898 @@
|
|
|
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 { timestampDate, type Timestamp } from "@bufbuild/protobuf/wkt";
|
|
13
|
+
import type { PlatformClient } from "@stigmer/protos/ai/stigmer/iam/platformclient/v1/api_pb";
|
|
14
|
+
import type { PlatformClientCreateResponse } from "@stigmer/protos/ai/stigmer/iam/platformclient/v1/io_pb";
|
|
15
|
+
import { useUpdatePlatformClient } from "./useUpdatePlatformClient";
|
|
16
|
+
import { useRotatePlatformClientSecret } from "./useRotatePlatformClientSecret";
|
|
17
|
+
import { useDeletePlatformClient } from "./useDeletePlatformClient";
|
|
18
|
+
|
|
19
|
+
/** Props for {@link PlatformClientDetailPanel}. */
|
|
20
|
+
export interface PlatformClientDetailPanelProps {
|
|
21
|
+
/** The platform client resource to display and edit. */
|
|
22
|
+
readonly platformClient: PlatformClient;
|
|
23
|
+
/** Fired with the updated resource after a successful save. */
|
|
24
|
+
readonly onUpdated?: (pc: PlatformClient) => void;
|
|
25
|
+
/**
|
|
26
|
+
* Fired after a successful secret rotation with the response
|
|
27
|
+
* containing the one-time new raw secret.
|
|
28
|
+
*/
|
|
29
|
+
readonly onSecretRotated?: (
|
|
30
|
+
response: PlatformClientCreateResponse,
|
|
31
|
+
) => void;
|
|
32
|
+
/** Fired after a successful deletion. */
|
|
33
|
+
readonly onDeleted?: () => void;
|
|
34
|
+
/** Fired when the user clicks the back button. */
|
|
35
|
+
readonly onBack?: () => void;
|
|
36
|
+
/** Additional CSS class names for the root container. */
|
|
37
|
+
readonly className?: string;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* View and edit panel for an existing platform client.
|
|
42
|
+
*
|
|
43
|
+
* In **view mode**, displays all configuration fields in a structured
|
|
44
|
+
* label/value layout with "Edit", "Rotate Secret", and "Delete"
|
|
45
|
+
* actions.
|
|
46
|
+
*
|
|
47
|
+
* In **edit mode**, mutable spec fields become editable: JIT
|
|
48
|
+
* provisioning toggles, expiry, auto-grant role, and allowed
|
|
49
|
+
* origins. Credential fields (`clientId`, `secretFingerprint`) are
|
|
50
|
+
* read-only. "Save" submits the update; "Cancel" discards changes.
|
|
51
|
+
*
|
|
52
|
+
* Secret rotation triggers `onSecretRotated` with the full
|
|
53
|
+
* {@link PlatformClientCreateResponse} so the parent can show the
|
|
54
|
+
* one-time secret alert.
|
|
55
|
+
*
|
|
56
|
+
* All visual properties flow through `--stgm-*` design tokens.
|
|
57
|
+
*
|
|
58
|
+
* @example
|
|
59
|
+
* ```tsx
|
|
60
|
+
* <PlatformClientDetailPanel
|
|
61
|
+
* platformClient={pc}
|
|
62
|
+
* onUpdated={(updated) => refetch()}
|
|
63
|
+
* onSecretRotated={(resp) => setFlow({ phase: "revealing", resp })}
|
|
64
|
+
* onDeleted={() => setFlow({ phase: "idle" })}
|
|
65
|
+
* onBack={() => setFlow({ phase: "idle" })}
|
|
66
|
+
* />
|
|
67
|
+
* ```
|
|
68
|
+
*/
|
|
69
|
+
export function PlatformClientDetailPanel({
|
|
70
|
+
platformClient,
|
|
71
|
+
onUpdated,
|
|
72
|
+
onSecretRotated,
|
|
73
|
+
onDeleted,
|
|
74
|
+
onBack,
|
|
75
|
+
className,
|
|
76
|
+
}: PlatformClientDetailPanelProps) {
|
|
77
|
+
const spec = platformClient.spec;
|
|
78
|
+
const meta = platformClient.metadata;
|
|
79
|
+
|
|
80
|
+
const { update, isUpdating, error: updateError, clearError: clearUpdateError } =
|
|
81
|
+
useUpdatePlatformClient();
|
|
82
|
+
const { rotateSecret, isRotating, error: rotateError, clearError: clearRotateError } =
|
|
83
|
+
useRotatePlatformClientSecret();
|
|
84
|
+
const { deletePlatformClient, isDeleting, error: deleteError } =
|
|
85
|
+
useDeletePlatformClient();
|
|
86
|
+
|
|
87
|
+
const [mode, setMode] = useState<"view" | "edit">("view");
|
|
88
|
+
const [confirmingDelete, setConfirmingDelete] = useState(false);
|
|
89
|
+
const [confirmingRotate, setConfirmingRotate] = useState(false);
|
|
90
|
+
|
|
91
|
+
const isBusy = isUpdating || isRotating || isDeleting;
|
|
92
|
+
|
|
93
|
+
// Edit form state
|
|
94
|
+
const [neverExpires, setNeverExpires] = useState(
|
|
95
|
+
spec?.neverExpires ?? true,
|
|
96
|
+
);
|
|
97
|
+
const [expiresAt, setExpiresAt] = useState(() => {
|
|
98
|
+
if (spec?.expiresAt) {
|
|
99
|
+
return toDatetimeLocalValue(timestampDate(spec.expiresAt));
|
|
100
|
+
}
|
|
101
|
+
return "";
|
|
102
|
+
});
|
|
103
|
+
const [autoProvision, setAutoProvision] = useState(
|
|
104
|
+
spec?.autoProvisionAccounts ?? false,
|
|
105
|
+
);
|
|
106
|
+
const [autoGrant, setAutoGrant] = useState(
|
|
107
|
+
spec?.autoGrantOnOrg ?? false,
|
|
108
|
+
);
|
|
109
|
+
const [autoGrantRole, setAutoGrantRole] = useState<IamRole>(
|
|
110
|
+
spec?.autoGrantRole ?? IamRole.iam_role_unspecified,
|
|
111
|
+
);
|
|
112
|
+
const [origins, setOrigins] = useState<string[]>(
|
|
113
|
+
[...(spec?.allowedOrigins ?? [])],
|
|
114
|
+
);
|
|
115
|
+
const [originInput, setOriginInput] = useState("");
|
|
116
|
+
|
|
117
|
+
const handleAutoProvisionChange = useCallback((v: boolean) => {
|
|
118
|
+
setAutoProvision(v);
|
|
119
|
+
if (!v) {
|
|
120
|
+
setAutoGrant(false);
|
|
121
|
+
setAutoGrantRole(IamRole.iam_role_unspecified);
|
|
122
|
+
}
|
|
123
|
+
}, []);
|
|
124
|
+
|
|
125
|
+
const handleAutoGrantChange = useCallback((v: boolean) => {
|
|
126
|
+
setAutoGrant(v);
|
|
127
|
+
if (v) setAutoProvision(true);
|
|
128
|
+
if (!v) setAutoGrantRole(IamRole.iam_role_unspecified);
|
|
129
|
+
}, []);
|
|
130
|
+
|
|
131
|
+
const addOrigin = useCallback(() => {
|
|
132
|
+
const trimmed = originInput.trim();
|
|
133
|
+
if (trimmed && !origins.includes(trimmed)) {
|
|
134
|
+
setOrigins((prev) => [...prev, trimmed]);
|
|
135
|
+
}
|
|
136
|
+
setOriginInput("");
|
|
137
|
+
}, [originInput, origins]);
|
|
138
|
+
|
|
139
|
+
const handleOriginKeyDown = useCallback(
|
|
140
|
+
(e: KeyboardEvent<HTMLInputElement>) => {
|
|
141
|
+
if (e.key === "Enter") {
|
|
142
|
+
e.preventDefault();
|
|
143
|
+
addOrigin();
|
|
144
|
+
}
|
|
145
|
+
},
|
|
146
|
+
[addOrigin],
|
|
147
|
+
);
|
|
148
|
+
|
|
149
|
+
const removeOrigin = useCallback((origin: string) => {
|
|
150
|
+
setOrigins((prev) => prev.filter((o) => o !== origin));
|
|
151
|
+
}, []);
|
|
152
|
+
|
|
153
|
+
const enterEdit = useCallback(() => {
|
|
154
|
+
setNeverExpires(spec?.neverExpires ?? true);
|
|
155
|
+
setExpiresAt(
|
|
156
|
+
spec?.expiresAt
|
|
157
|
+
? toDatetimeLocalValue(timestampDate(spec.expiresAt))
|
|
158
|
+
: "",
|
|
159
|
+
);
|
|
160
|
+
setAutoProvision(spec?.autoProvisionAccounts ?? false);
|
|
161
|
+
setAutoGrant(spec?.autoGrantOnOrg ?? false);
|
|
162
|
+
setAutoGrantRole(
|
|
163
|
+
spec?.autoGrantRole ?? IamRole.iam_role_unspecified,
|
|
164
|
+
);
|
|
165
|
+
setOrigins([...(spec?.allowedOrigins ?? [])]);
|
|
166
|
+
setOriginInput("");
|
|
167
|
+
clearUpdateError();
|
|
168
|
+
setMode("edit");
|
|
169
|
+
}, [spec, clearUpdateError]);
|
|
170
|
+
|
|
171
|
+
const cancelEdit = useCallback(() => {
|
|
172
|
+
clearUpdateError();
|
|
173
|
+
setMode("view");
|
|
174
|
+
}, [clearUpdateError]);
|
|
175
|
+
|
|
176
|
+
const handleSave = useCallback(
|
|
177
|
+
async (e: FormEvent) => {
|
|
178
|
+
e.preventDefault();
|
|
179
|
+
clearUpdateError();
|
|
180
|
+
try {
|
|
181
|
+
const updated = await update({
|
|
182
|
+
name: meta?.name ?? "",
|
|
183
|
+
slug: meta?.slug,
|
|
184
|
+
org: meta?.org ?? "",
|
|
185
|
+
neverExpires,
|
|
186
|
+
...(!neverExpires &&
|
|
187
|
+
expiresAt && {
|
|
188
|
+
expiresAt: new Date(expiresAt).toISOString(),
|
|
189
|
+
}),
|
|
190
|
+
autoProvisionAccounts: autoProvision,
|
|
191
|
+
autoGrantOnOrg: autoGrant,
|
|
192
|
+
...(autoGrant &&
|
|
193
|
+
autoGrantRole !== IamRole.iam_role_unspecified && {
|
|
194
|
+
autoGrantRole,
|
|
195
|
+
}),
|
|
196
|
+
allowedOrigins: origins,
|
|
197
|
+
});
|
|
198
|
+
setMode("view");
|
|
199
|
+
onUpdated?.(updated);
|
|
200
|
+
} catch {
|
|
201
|
+
// error state is managed by useUpdatePlatformClient
|
|
202
|
+
}
|
|
203
|
+
},
|
|
204
|
+
[
|
|
205
|
+
meta,
|
|
206
|
+
neverExpires,
|
|
207
|
+
expiresAt,
|
|
208
|
+
autoProvision,
|
|
209
|
+
autoGrant,
|
|
210
|
+
autoGrantRole,
|
|
211
|
+
origins,
|
|
212
|
+
update,
|
|
213
|
+
clearUpdateError,
|
|
214
|
+
onUpdated,
|
|
215
|
+
],
|
|
216
|
+
);
|
|
217
|
+
|
|
218
|
+
const handleRotateSecret = useCallback(async () => {
|
|
219
|
+
clearRotateError();
|
|
220
|
+
try {
|
|
221
|
+
const response = await rotateSecret(meta?.id ?? "");
|
|
222
|
+
setConfirmingRotate(false);
|
|
223
|
+
onSecretRotated?.(response);
|
|
224
|
+
} catch {
|
|
225
|
+
// error state is managed by the hook
|
|
226
|
+
}
|
|
227
|
+
}, [meta?.id, rotateSecret, clearRotateError, onSecretRotated]);
|
|
228
|
+
|
|
229
|
+
const handleDelete = useCallback(async () => {
|
|
230
|
+
try {
|
|
231
|
+
await deletePlatformClient({ resourceId: meta?.id ?? "" });
|
|
232
|
+
onDeleted?.();
|
|
233
|
+
} catch {
|
|
234
|
+
// error state is surfaced via the hook
|
|
235
|
+
}
|
|
236
|
+
}, [meta?.id, deletePlatformClient, onDeleted]);
|
|
237
|
+
|
|
238
|
+
const createdAt = platformClient.status?.audit?.specAudit?.createdAt;
|
|
239
|
+
const updatedAt = platformClient.status?.audit?.specAudit?.updatedAt;
|
|
240
|
+
|
|
241
|
+
return (
|
|
242
|
+
<div className={cn("space-y-4", className)}>
|
|
243
|
+
{/* Header */}
|
|
244
|
+
<div className="flex items-start justify-between gap-3">
|
|
245
|
+
<div className="min-w-0">
|
|
246
|
+
{onBack && (
|
|
247
|
+
<button
|
|
248
|
+
type="button"
|
|
249
|
+
onClick={onBack}
|
|
250
|
+
className="text-muted-foreground hover:text-foreground mb-1 flex items-center gap-1 text-xs transition-colors"
|
|
251
|
+
>
|
|
252
|
+
<ArrowLeftIcon />
|
|
253
|
+
Back to list
|
|
254
|
+
</button>
|
|
255
|
+
)}
|
|
256
|
+
<h3 className="text-foreground truncate text-sm font-semibold">
|
|
257
|
+
{meta?.name ?? "Platform Client"}
|
|
258
|
+
</h3>
|
|
259
|
+
<div className="flex items-center gap-2">
|
|
260
|
+
{meta?.slug && (
|
|
261
|
+
<span className="text-muted-foreground font-mono text-xs">
|
|
262
|
+
{meta.slug}
|
|
263
|
+
</span>
|
|
264
|
+
)}
|
|
265
|
+
{spec?.autoProvisionAccounts && (
|
|
266
|
+
<span className="inline-flex items-center rounded-full border border-primary/30 bg-primary-subtle px-2 py-0.5 text-[0.65rem] font-medium text-primary">
|
|
267
|
+
JIT
|
|
268
|
+
</span>
|
|
269
|
+
)}
|
|
270
|
+
</div>
|
|
271
|
+
</div>
|
|
272
|
+
|
|
273
|
+
{mode === "view" && (
|
|
274
|
+
<button
|
|
275
|
+
type="button"
|
|
276
|
+
onClick={enterEdit}
|
|
277
|
+
className={cn(
|
|
278
|
+
"shrink-0 rounded-md px-2.5 py-1.5 text-xs font-medium",
|
|
279
|
+
"text-muted-foreground hover:text-foreground hover:bg-accent/50",
|
|
280
|
+
"transition-colors",
|
|
281
|
+
)}
|
|
282
|
+
>
|
|
283
|
+
Edit
|
|
284
|
+
</button>
|
|
285
|
+
)}
|
|
286
|
+
</div>
|
|
287
|
+
|
|
288
|
+
{/* Body */}
|
|
289
|
+
{mode === "view" ? (
|
|
290
|
+
<ViewMode
|
|
291
|
+
spec={spec}
|
|
292
|
+
createdAt={createdAt}
|
|
293
|
+
updatedAt={updatedAt}
|
|
294
|
+
/>
|
|
295
|
+
) : (
|
|
296
|
+
<form onSubmit={handleSave} className="space-y-3">
|
|
297
|
+
{/* Read-only credential info */}
|
|
298
|
+
<div className="rounded-md border border-border/60 bg-muted/30 px-3 py-2 space-y-1">
|
|
299
|
+
<p className="text-[0.65rem] font-medium text-muted-foreground">
|
|
300
|
+
Client ID
|
|
301
|
+
</p>
|
|
302
|
+
<p className="font-mono text-xs text-foreground">
|
|
303
|
+
{spec?.clientId ?? "—"}
|
|
304
|
+
</p>
|
|
305
|
+
</div>
|
|
306
|
+
|
|
307
|
+
{/* Expiry */}
|
|
308
|
+
<fieldset className="space-y-2" disabled={isUpdating}>
|
|
309
|
+
<legend className="text-xs font-medium text-foreground">
|
|
310
|
+
Expiry
|
|
311
|
+
</legend>
|
|
312
|
+
<ToggleSwitch
|
|
313
|
+
checked={neverExpires}
|
|
314
|
+
onChange={setNeverExpires}
|
|
315
|
+
label="Never expires"
|
|
316
|
+
disabled={isUpdating}
|
|
317
|
+
/>
|
|
318
|
+
{!neverExpires && (
|
|
319
|
+
<div className="space-y-1">
|
|
320
|
+
<label
|
|
321
|
+
htmlFor="stgm-pc-edit-expires-at"
|
|
322
|
+
className="text-xs font-medium text-foreground"
|
|
323
|
+
>
|
|
324
|
+
Expires at
|
|
325
|
+
</label>
|
|
326
|
+
<input
|
|
327
|
+
id="stgm-pc-edit-expires-at"
|
|
328
|
+
type="datetime-local"
|
|
329
|
+
value={expiresAt}
|
|
330
|
+
onChange={(e) => setExpiresAt(e.target.value)}
|
|
331
|
+
disabled={isUpdating}
|
|
332
|
+
className={cn(
|
|
333
|
+
"w-full rounded-md border border-input bg-background px-2.5 py-1.5 text-xs text-foreground",
|
|
334
|
+
"focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring",
|
|
335
|
+
"disabled:pointer-events-none disabled:opacity-50",
|
|
336
|
+
)}
|
|
337
|
+
/>
|
|
338
|
+
</div>
|
|
339
|
+
)}
|
|
340
|
+
</fieldset>
|
|
341
|
+
|
|
342
|
+
{/* JIT provisioning */}
|
|
343
|
+
<fieldset className="space-y-2.5" disabled={isUpdating}>
|
|
344
|
+
<hr className="border-border/40" />
|
|
345
|
+
<legend className="text-xs font-medium text-foreground">
|
|
346
|
+
JIT provisioning
|
|
347
|
+
</legend>
|
|
348
|
+
|
|
349
|
+
<ToggleSwitch
|
|
350
|
+
checked={autoProvision}
|
|
351
|
+
onChange={handleAutoProvisionChange}
|
|
352
|
+
label="Auto-provision accounts"
|
|
353
|
+
hint="Create a Stigmer identity account automatically on first token mint"
|
|
354
|
+
disabled={isUpdating}
|
|
355
|
+
/>
|
|
356
|
+
|
|
357
|
+
<ToggleSwitch
|
|
358
|
+
checked={autoGrant}
|
|
359
|
+
onChange={handleAutoGrantChange}
|
|
360
|
+
label="Auto-grant on organization"
|
|
361
|
+
hint="Grant a role on the owning organization when an account is provisioned"
|
|
362
|
+
disabled={isUpdating || !autoProvision}
|
|
363
|
+
/>
|
|
364
|
+
|
|
365
|
+
{autoGrant && (
|
|
366
|
+
<div className="space-y-1">
|
|
367
|
+
<label
|
|
368
|
+
htmlFor="stgm-pc-edit-grant-role"
|
|
369
|
+
className="text-xs font-medium text-foreground"
|
|
370
|
+
>
|
|
371
|
+
Auto-grant role
|
|
372
|
+
</label>
|
|
373
|
+
<select
|
|
374
|
+
id="stgm-pc-edit-grant-role"
|
|
375
|
+
value={String(autoGrantRole)}
|
|
376
|
+
onChange={(e) =>
|
|
377
|
+
setAutoGrantRole(
|
|
378
|
+
Number(e.target.value) as IamRole,
|
|
379
|
+
)
|
|
380
|
+
}
|
|
381
|
+
disabled={isUpdating}
|
|
382
|
+
className={cn(
|
|
383
|
+
"w-full rounded-md border border-input bg-background px-2.5 py-1.5 text-xs text-foreground",
|
|
384
|
+
"focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring",
|
|
385
|
+
"disabled:pointer-events-none disabled:opacity-50",
|
|
386
|
+
)}
|
|
387
|
+
>
|
|
388
|
+
{JIT_ROLE_OPTIONS.map((opt) => (
|
|
389
|
+
<option key={opt.value} value={opt.value}>
|
|
390
|
+
{opt.label}
|
|
391
|
+
</option>
|
|
392
|
+
))}
|
|
393
|
+
</select>
|
|
394
|
+
</div>
|
|
395
|
+
)}
|
|
396
|
+
</fieldset>
|
|
397
|
+
|
|
398
|
+
{/* Allowed origins */}
|
|
399
|
+
<fieldset className="space-y-2" disabled={isUpdating}>
|
|
400
|
+
<hr className="border-border/40" />
|
|
401
|
+
<legend className="text-xs font-medium text-foreground">
|
|
402
|
+
Allowed origins
|
|
403
|
+
</legend>
|
|
404
|
+
<p className="text-[0.65rem] text-muted-foreground">
|
|
405
|
+
Browser origins permitted to use tokens minted by this
|
|
406
|
+
client. Leave empty to allow all origins.
|
|
407
|
+
</p>
|
|
408
|
+
|
|
409
|
+
<div className="flex items-center gap-2">
|
|
410
|
+
<input
|
|
411
|
+
type="text"
|
|
412
|
+
value={originInput}
|
|
413
|
+
onChange={(e) => setOriginInput(e.target.value)}
|
|
414
|
+
onKeyDown={handleOriginKeyDown}
|
|
415
|
+
onBlur={() => {
|
|
416
|
+
if (originInput.trim()) addOrigin();
|
|
417
|
+
}}
|
|
418
|
+
placeholder="https://example.com"
|
|
419
|
+
disabled={isUpdating}
|
|
420
|
+
className={cn(
|
|
421
|
+
"min-w-0 flex-1 rounded-md border border-input bg-background px-2.5 py-1.5 text-xs text-foreground",
|
|
422
|
+
"placeholder:text-muted-foreground",
|
|
423
|
+
"focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring",
|
|
424
|
+
"disabled:pointer-events-none disabled:opacity-50",
|
|
425
|
+
)}
|
|
426
|
+
/>
|
|
427
|
+
<button
|
|
428
|
+
type="button"
|
|
429
|
+
onClick={addOrigin}
|
|
430
|
+
disabled={isUpdating || !originInput.trim()}
|
|
431
|
+
className={cn(
|
|
432
|
+
"shrink-0 rounded-md px-2.5 py-1.5 text-xs font-medium",
|
|
433
|
+
"text-muted-foreground hover:text-foreground hover:bg-accent/50",
|
|
434
|
+
"disabled:pointer-events-none disabled:opacity-50",
|
|
435
|
+
"transition-colors",
|
|
436
|
+
)}
|
|
437
|
+
>
|
|
438
|
+
Add
|
|
439
|
+
</button>
|
|
440
|
+
</div>
|
|
441
|
+
|
|
442
|
+
{origins.length > 0 && (
|
|
443
|
+
<div className="flex flex-wrap gap-1.5">
|
|
444
|
+
{origins.map((origin) => (
|
|
445
|
+
<span
|
|
446
|
+
key={origin}
|
|
447
|
+
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"
|
|
448
|
+
>
|
|
449
|
+
{origin}
|
|
450
|
+
<button
|
|
451
|
+
type="button"
|
|
452
|
+
onClick={() => removeOrigin(origin)}
|
|
453
|
+
disabled={isUpdating}
|
|
454
|
+
aria-label={`Remove ${origin}`}
|
|
455
|
+
className="text-muted-foreground hover:text-destructive transition-colors"
|
|
456
|
+
>
|
|
457
|
+
<XIcon />
|
|
458
|
+
</button>
|
|
459
|
+
</span>
|
|
460
|
+
))}
|
|
461
|
+
</div>
|
|
462
|
+
)}
|
|
463
|
+
</fieldset>
|
|
464
|
+
|
|
465
|
+
{updateError && (
|
|
466
|
+
<p className="text-destructive text-[0.65rem]" role="alert">
|
|
467
|
+
{getUserMessage(updateError)}
|
|
468
|
+
</p>
|
|
469
|
+
)}
|
|
470
|
+
|
|
471
|
+
<div className="flex items-center gap-2 pt-1">
|
|
472
|
+
<button
|
|
473
|
+
type="submit"
|
|
474
|
+
disabled={isUpdating}
|
|
475
|
+
className={cn(
|
|
476
|
+
"inline-flex items-center gap-1.5 rounded-md px-3 py-1.5 text-xs font-medium",
|
|
477
|
+
"bg-primary text-primary-foreground hover:bg-primary/90",
|
|
478
|
+
"disabled:pointer-events-none disabled:opacity-40",
|
|
479
|
+
)}
|
|
480
|
+
>
|
|
481
|
+
{isUpdating && <SpinnerIcon />}
|
|
482
|
+
Save changes
|
|
483
|
+
</button>
|
|
484
|
+
<button
|
|
485
|
+
type="button"
|
|
486
|
+
onClick={cancelEdit}
|
|
487
|
+
disabled={isUpdating}
|
|
488
|
+
className={cn(
|
|
489
|
+
"rounded-md px-2.5 py-1.5 text-xs",
|
|
490
|
+
"text-muted-foreground hover:text-foreground hover:bg-accent/50",
|
|
491
|
+
"disabled:pointer-events-none disabled:opacity-50",
|
|
492
|
+
)}
|
|
493
|
+
>
|
|
494
|
+
Cancel
|
|
495
|
+
</button>
|
|
496
|
+
</div>
|
|
497
|
+
</form>
|
|
498
|
+
)}
|
|
499
|
+
|
|
500
|
+
{/* Actions bar (view mode only) */}
|
|
501
|
+
{mode === "view" && (
|
|
502
|
+
<div className="space-y-2 pt-2">
|
|
503
|
+
<hr className="border-border/40" />
|
|
504
|
+
|
|
505
|
+
{/* Rotate Secret */}
|
|
506
|
+
{confirmingRotate ? (
|
|
507
|
+
<div className="flex items-center justify-between rounded-md border border-warning/30 bg-warning/5 px-3 py-2">
|
|
508
|
+
<p className="text-xs text-foreground">
|
|
509
|
+
Rotate secret? The current secret will be
|
|
510
|
+
<span className="font-medium"> permanently invalidated</span>.
|
|
511
|
+
</p>
|
|
512
|
+
<div className="flex shrink-0 items-center gap-1.5">
|
|
513
|
+
<button
|
|
514
|
+
type="button"
|
|
515
|
+
onClick={handleRotateSecret}
|
|
516
|
+
disabled={isBusy}
|
|
517
|
+
className={cn(
|
|
518
|
+
"inline-flex items-center gap-1 rounded-md px-2.5 py-1 text-xs font-medium",
|
|
519
|
+
"bg-primary text-primary-foreground hover:bg-primary/90",
|
|
520
|
+
"disabled:pointer-events-none disabled:opacity-50",
|
|
521
|
+
)}
|
|
522
|
+
>
|
|
523
|
+
{isRotating && <SpinnerIcon />}
|
|
524
|
+
Rotate
|
|
525
|
+
</button>
|
|
526
|
+
<button
|
|
527
|
+
type="button"
|
|
528
|
+
onClick={() => {
|
|
529
|
+
setConfirmingRotate(false);
|
|
530
|
+
clearRotateError();
|
|
531
|
+
}}
|
|
532
|
+
disabled={isBusy}
|
|
533
|
+
className={cn(
|
|
534
|
+
"rounded-md px-2.5 py-1 text-xs",
|
|
535
|
+
"text-muted-foreground hover:text-foreground hover:bg-accent/50",
|
|
536
|
+
"disabled:pointer-events-none disabled:opacity-50",
|
|
537
|
+
)}
|
|
538
|
+
>
|
|
539
|
+
Cancel
|
|
540
|
+
</button>
|
|
541
|
+
</div>
|
|
542
|
+
</div>
|
|
543
|
+
) : (
|
|
544
|
+
<button
|
|
545
|
+
type="button"
|
|
546
|
+
onClick={() => setConfirmingRotate(true)}
|
|
547
|
+
disabled={isBusy}
|
|
548
|
+
className={cn(
|
|
549
|
+
"rounded-md px-2.5 py-1.5 text-xs font-medium",
|
|
550
|
+
"text-muted-foreground hover:text-foreground hover:bg-accent/50",
|
|
551
|
+
"disabled:pointer-events-none disabled:opacity-50",
|
|
552
|
+
"transition-colors",
|
|
553
|
+
)}
|
|
554
|
+
>
|
|
555
|
+
Rotate secret
|
|
556
|
+
</button>
|
|
557
|
+
)}
|
|
558
|
+
{rotateError && (
|
|
559
|
+
<p className="text-destructive text-[0.65rem]" role="alert">
|
|
560
|
+
{getUserMessage(rotateError)}
|
|
561
|
+
</p>
|
|
562
|
+
)}
|
|
563
|
+
|
|
564
|
+
{/* Delete */}
|
|
565
|
+
{confirmingDelete ? (
|
|
566
|
+
<div className="flex items-center justify-between rounded-md border border-destructive/30 bg-destructive/5 px-3 py-2">
|
|
567
|
+
<div className="min-w-0 flex-1">
|
|
568
|
+
<p className="text-xs text-foreground">
|
|
569
|
+
Delete{" "}
|
|
570
|
+
<span className="font-medium">{meta?.name}</span>?
|
|
571
|
+
This action is permanent.
|
|
572
|
+
</p>
|
|
573
|
+
{deleteError && (
|
|
574
|
+
<p className="mt-0.5 text-[0.65rem] text-destructive">
|
|
575
|
+
{getUserMessage(deleteError)}
|
|
576
|
+
</p>
|
|
577
|
+
)}
|
|
578
|
+
</div>
|
|
579
|
+
<div className="flex shrink-0 items-center gap-1.5">
|
|
580
|
+
<button
|
|
581
|
+
type="button"
|
|
582
|
+
onClick={handleDelete}
|
|
583
|
+
disabled={isBusy}
|
|
584
|
+
className={cn(
|
|
585
|
+
"inline-flex items-center gap-1 rounded-md px-2.5 py-1 text-xs font-medium",
|
|
586
|
+
"bg-destructive text-destructive-foreground hover:bg-destructive/90",
|
|
587
|
+
"disabled:pointer-events-none disabled:opacity-50",
|
|
588
|
+
)}
|
|
589
|
+
>
|
|
590
|
+
{isDeleting && <SpinnerIcon />}
|
|
591
|
+
Delete
|
|
592
|
+
</button>
|
|
593
|
+
<button
|
|
594
|
+
type="button"
|
|
595
|
+
onClick={() => setConfirmingDelete(false)}
|
|
596
|
+
disabled={isBusy}
|
|
597
|
+
className={cn(
|
|
598
|
+
"rounded-md px-2.5 py-1 text-xs",
|
|
599
|
+
"text-muted-foreground hover:text-foreground hover:bg-accent/50",
|
|
600
|
+
"disabled:pointer-events-none disabled:opacity-50",
|
|
601
|
+
)}
|
|
602
|
+
>
|
|
603
|
+
Cancel
|
|
604
|
+
</button>
|
|
605
|
+
</div>
|
|
606
|
+
</div>
|
|
607
|
+
) : (
|
|
608
|
+
<button
|
|
609
|
+
type="button"
|
|
610
|
+
onClick={() => setConfirmingDelete(true)}
|
|
611
|
+
disabled={isBusy}
|
|
612
|
+
className={cn(
|
|
613
|
+
"rounded-md px-2.5 py-1.5 text-xs font-medium",
|
|
614
|
+
"text-destructive hover:text-destructive-foreground hover:bg-destructive/90",
|
|
615
|
+
"disabled:pointer-events-none disabled:opacity-50",
|
|
616
|
+
"transition-colors",
|
|
617
|
+
)}
|
|
618
|
+
>
|
|
619
|
+
Delete platform client
|
|
620
|
+
</button>
|
|
621
|
+
)}
|
|
622
|
+
</div>
|
|
623
|
+
)}
|
|
624
|
+
</div>
|
|
625
|
+
);
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
// ---------------------------------------------------------------------------
|
|
629
|
+
// View mode
|
|
630
|
+
// ---------------------------------------------------------------------------
|
|
631
|
+
|
|
632
|
+
function ViewMode({
|
|
633
|
+
spec,
|
|
634
|
+
createdAt,
|
|
635
|
+
updatedAt,
|
|
636
|
+
}: {
|
|
637
|
+
spec: PlatformClient["spec"];
|
|
638
|
+
createdAt?: Timestamp;
|
|
639
|
+
updatedAt?: Timestamp;
|
|
640
|
+
}) {
|
|
641
|
+
return (
|
|
642
|
+
<dl className="space-y-2.5">
|
|
643
|
+
<Field label="Client ID" value={spec?.clientId} mono />
|
|
644
|
+
<Field
|
|
645
|
+
label="Secret fingerprint"
|
|
646
|
+
value={
|
|
647
|
+
spec?.secretFingerprint
|
|
648
|
+
? `••••${spec.secretFingerprint.slice(-4)}`
|
|
649
|
+
: undefined
|
|
650
|
+
}
|
|
651
|
+
mono
|
|
652
|
+
/>
|
|
653
|
+
|
|
654
|
+
{/* Expiry */}
|
|
655
|
+
{spec?.neverExpires ? (
|
|
656
|
+
<Field label="Expiry" value="Never expires" />
|
|
657
|
+
) : spec?.expiresAt ? (
|
|
658
|
+
<Field
|
|
659
|
+
label="Expires"
|
|
660
|
+
value={formatDate(timestampDate(spec.expiresAt))}
|
|
661
|
+
/>
|
|
662
|
+
) : null}
|
|
663
|
+
|
|
664
|
+
{/* JIT provisioning */}
|
|
665
|
+
<hr className="border-border/40" />
|
|
666
|
+
<Field
|
|
667
|
+
label="Auto-provision accounts"
|
|
668
|
+
value={spec?.autoProvisionAccounts ? "Enabled" : "Disabled"}
|
|
669
|
+
/>
|
|
670
|
+
<Field
|
|
671
|
+
label="Auto-grant on organization"
|
|
672
|
+
value={spec?.autoGrantOnOrg ? "Enabled" : "Disabled"}
|
|
673
|
+
/>
|
|
674
|
+
{spec?.autoGrantOnOrg && (
|
|
675
|
+
<Field
|
|
676
|
+
label="Auto-grant role"
|
|
677
|
+
value={formatIamRole(spec.autoGrantRole)}
|
|
678
|
+
/>
|
|
679
|
+
)}
|
|
680
|
+
|
|
681
|
+
{/* Allowed origins */}
|
|
682
|
+
{(spec?.allowedOrigins.length ?? 0) > 0 && (
|
|
683
|
+
<>
|
|
684
|
+
<hr className="border-border/40" />
|
|
685
|
+
<div>
|
|
686
|
+
<dt className="text-muted-foreground text-[0.65rem] font-medium">
|
|
687
|
+
Allowed origins
|
|
688
|
+
</dt>
|
|
689
|
+
<dd className="mt-0.5 flex flex-wrap gap-1.5">
|
|
690
|
+
{spec!.allowedOrigins.map((origin) => (
|
|
691
|
+
<span
|
|
692
|
+
key={origin}
|
|
693
|
+
className="inline-block rounded-full border border-border/60 bg-muted/40 px-2 py-0.5 text-[0.65rem] font-mono text-foreground"
|
|
694
|
+
>
|
|
695
|
+
{origin}
|
|
696
|
+
</span>
|
|
697
|
+
))}
|
|
698
|
+
</dd>
|
|
699
|
+
</div>
|
|
700
|
+
</>
|
|
701
|
+
)}
|
|
702
|
+
|
|
703
|
+
{/* Timestamps */}
|
|
704
|
+
<div className="flex gap-6">
|
|
705
|
+
{createdAt && (
|
|
706
|
+
<Field
|
|
707
|
+
label="Created"
|
|
708
|
+
value={formatDate(timestampDate(createdAt))}
|
|
709
|
+
/>
|
|
710
|
+
)}
|
|
711
|
+
{updatedAt && (
|
|
712
|
+
<Field
|
|
713
|
+
label="Updated"
|
|
714
|
+
value={formatDate(timestampDate(updatedAt))}
|
|
715
|
+
/>
|
|
716
|
+
)}
|
|
717
|
+
</div>
|
|
718
|
+
</dl>
|
|
719
|
+
);
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
// ---------------------------------------------------------------------------
|
|
723
|
+
// Shared primitives
|
|
724
|
+
// ---------------------------------------------------------------------------
|
|
725
|
+
|
|
726
|
+
function Field({
|
|
727
|
+
label,
|
|
728
|
+
value,
|
|
729
|
+
mono,
|
|
730
|
+
}: {
|
|
731
|
+
label: string;
|
|
732
|
+
value?: string;
|
|
733
|
+
mono?: boolean;
|
|
734
|
+
}) {
|
|
735
|
+
if (!value) return null;
|
|
736
|
+
return (
|
|
737
|
+
<div>
|
|
738
|
+
<dt className="text-muted-foreground text-[0.65rem] font-medium">
|
|
739
|
+
{label}
|
|
740
|
+
</dt>
|
|
741
|
+
<dd
|
|
742
|
+
className={cn(
|
|
743
|
+
"text-foreground mt-0.5 break-all text-xs",
|
|
744
|
+
mono && "font-mono",
|
|
745
|
+
)}
|
|
746
|
+
>
|
|
747
|
+
{value}
|
|
748
|
+
</dd>
|
|
749
|
+
</div>
|
|
750
|
+
);
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
function ToggleSwitch({
|
|
754
|
+
checked,
|
|
755
|
+
onChange,
|
|
756
|
+
label,
|
|
757
|
+
hint,
|
|
758
|
+
disabled,
|
|
759
|
+
}: {
|
|
760
|
+
checked: boolean;
|
|
761
|
+
onChange: (v: boolean) => void;
|
|
762
|
+
label: string;
|
|
763
|
+
hint?: string;
|
|
764
|
+
disabled?: boolean;
|
|
765
|
+
}) {
|
|
766
|
+
return (
|
|
767
|
+
<div className="space-y-0.5">
|
|
768
|
+
<div className="flex items-center gap-2">
|
|
769
|
+
<button
|
|
770
|
+
type="button"
|
|
771
|
+
role="switch"
|
|
772
|
+
aria-checked={checked}
|
|
773
|
+
onClick={() => onChange(!checked)}
|
|
774
|
+
disabled={disabled}
|
|
775
|
+
className={cn(
|
|
776
|
+
"relative inline-flex h-5 w-9 shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors",
|
|
777
|
+
checked ? "bg-primary" : "bg-muted",
|
|
778
|
+
"disabled:pointer-events-none disabled:opacity-50",
|
|
779
|
+
)}
|
|
780
|
+
>
|
|
781
|
+
<span
|
|
782
|
+
className={cn(
|
|
783
|
+
"pointer-events-none inline-block h-4 w-4 rounded-full bg-background shadow-sm ring-0 transition-transform",
|
|
784
|
+
checked ? "translate-x-4" : "translate-x-0",
|
|
785
|
+
)}
|
|
786
|
+
/>
|
|
787
|
+
</button>
|
|
788
|
+
<span className="text-xs font-medium text-foreground">{label}</span>
|
|
789
|
+
</div>
|
|
790
|
+
{hint && (
|
|
791
|
+
<p className="pl-11 text-[0.65rem] text-muted-foreground">{hint}</p>
|
|
792
|
+
)}
|
|
793
|
+
</div>
|
|
794
|
+
);
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
// ---------------------------------------------------------------------------
|
|
798
|
+
// Constants
|
|
799
|
+
// ---------------------------------------------------------------------------
|
|
800
|
+
|
|
801
|
+
const JIT_ROLE_OPTIONS: readonly {
|
|
802
|
+
readonly value: string;
|
|
803
|
+
readonly label: string;
|
|
804
|
+
}[] = [
|
|
805
|
+
{ value: String(IamRole.iam_role_unspecified), label: "Default (viewer)" },
|
|
806
|
+
{ value: String(IamRole.viewer), label: "Viewer" },
|
|
807
|
+
{ value: String(IamRole.member), label: "Member" },
|
|
808
|
+
{ value: String(IamRole.admin), label: "Admin" },
|
|
809
|
+
];
|
|
810
|
+
|
|
811
|
+
// ---------------------------------------------------------------------------
|
|
812
|
+
// Helpers
|
|
813
|
+
// ---------------------------------------------------------------------------
|
|
814
|
+
|
|
815
|
+
function formatIamRole(role: IamRole): string {
|
|
816
|
+
switch (role) {
|
|
817
|
+
case IamRole.viewer:
|
|
818
|
+
return "Viewer";
|
|
819
|
+
case IamRole.member:
|
|
820
|
+
return "Member";
|
|
821
|
+
case IamRole.admin:
|
|
822
|
+
return "Admin";
|
|
823
|
+
case IamRole.owner:
|
|
824
|
+
return "Owner";
|
|
825
|
+
default:
|
|
826
|
+
return "Viewer (default)";
|
|
827
|
+
}
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
function formatDate(date: Date): string {
|
|
831
|
+
return date.toLocaleDateString(undefined, {
|
|
832
|
+
month: "short",
|
|
833
|
+
day: "numeric",
|
|
834
|
+
year: "numeric",
|
|
835
|
+
});
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
function toDatetimeLocalValue(date: Date): string {
|
|
839
|
+
const pad = (n: number) => String(n).padStart(2, "0");
|
|
840
|
+
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}T${pad(date.getHours())}:${pad(date.getMinutes())}`;
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
// ---------------------------------------------------------------------------
|
|
844
|
+
// Icons
|
|
845
|
+
// ---------------------------------------------------------------------------
|
|
846
|
+
|
|
847
|
+
function XIcon() {
|
|
848
|
+
return (
|
|
849
|
+
<svg
|
|
850
|
+
width="10"
|
|
851
|
+
height="10"
|
|
852
|
+
viewBox="0 0 16 16"
|
|
853
|
+
fill="none"
|
|
854
|
+
stroke="currentColor"
|
|
855
|
+
strokeWidth="2"
|
|
856
|
+
strokeLinecap="round"
|
|
857
|
+
aria-hidden="true"
|
|
858
|
+
>
|
|
859
|
+
<path d="M4 4l8 8M12 4l-8 8" />
|
|
860
|
+
</svg>
|
|
861
|
+
);
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
function ArrowLeftIcon() {
|
|
865
|
+
return (
|
|
866
|
+
<svg
|
|
867
|
+
width="12"
|
|
868
|
+
height="12"
|
|
869
|
+
viewBox="0 0 16 16"
|
|
870
|
+
fill="none"
|
|
871
|
+
stroke="currentColor"
|
|
872
|
+
strokeWidth="1.5"
|
|
873
|
+
strokeLinecap="round"
|
|
874
|
+
strokeLinejoin="round"
|
|
875
|
+
aria-hidden="true"
|
|
876
|
+
>
|
|
877
|
+
<path d="M10 3L5 8l5 5" />
|
|
878
|
+
</svg>
|
|
879
|
+
);
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
function SpinnerIcon() {
|
|
883
|
+
return (
|
|
884
|
+
<svg
|
|
885
|
+
width="12"
|
|
886
|
+
height="12"
|
|
887
|
+
viewBox="0 0 16 16"
|
|
888
|
+
fill="none"
|
|
889
|
+
stroke="currentColor"
|
|
890
|
+
strokeWidth="2"
|
|
891
|
+
strokeLinecap="round"
|
|
892
|
+
className="animate-spin"
|
|
893
|
+
aria-hidden="true"
|
|
894
|
+
>
|
|
895
|
+
<path d="M8 2a6 6 0 1 0 6 6" />
|
|
896
|
+
</svg>
|
|
897
|
+
);
|
|
898
|
+
}
|