@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,413 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useCallback, useState } from "react";
|
|
4
|
+
import { cn } from "@stigmer/theme";
|
|
5
|
+
import { getUserMessage } from "@stigmer/sdk";
|
|
6
|
+
import { timestampDate } from "@bufbuild/protobuf/wkt";
|
|
7
|
+
import type { PlatformClient } from "@stigmer/protos/ai/stigmer/iam/platformclient/v1/api_pb";
|
|
8
|
+
import { usePlatformClientList } from "./usePlatformClientList";
|
|
9
|
+
import { useDeletePlatformClient } from "./useDeletePlatformClient";
|
|
10
|
+
|
|
11
|
+
/** Props for {@link PlatformClientListPanel}. */
|
|
12
|
+
export interface PlatformClientListPanelProps {
|
|
13
|
+
/** Organization slug whose platform clients should be listed. */
|
|
14
|
+
readonly org: string;
|
|
15
|
+
/** Fired when the user wants to edit a platform client. */
|
|
16
|
+
readonly onEdit?: (pc: PlatformClient) => void;
|
|
17
|
+
/** Expose the refetch function so parents can trigger a list refresh. */
|
|
18
|
+
readonly onRefetchRef?: (refetch: () => void) => void;
|
|
19
|
+
/** Additional CSS class names for the root container. */
|
|
20
|
+
readonly className?: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Displays a list of {@link PlatformClient} resources for an
|
|
25
|
+
* organization with inline delete confirmation.
|
|
26
|
+
*
|
|
27
|
+
* Each row shows the client name, `client_id` (monospace), secret
|
|
28
|
+
* fingerprint, expiry status, and creation date. A delete button
|
|
29
|
+
* triggers an inline confirmation flow.
|
|
30
|
+
*
|
|
31
|
+
* Platform clients are admin-level resources with small cardinality
|
|
32
|
+
* (typically 1–5 per org), so the list is rendered without pagination.
|
|
33
|
+
*
|
|
34
|
+
* All visual properties flow through `--stgm-*` design tokens.
|
|
35
|
+
*
|
|
36
|
+
* @example
|
|
37
|
+
* ```tsx
|
|
38
|
+
* <PlatformClientListPanel org="acme" />
|
|
39
|
+
* ```
|
|
40
|
+
*
|
|
41
|
+
* @example
|
|
42
|
+
* ```tsx
|
|
43
|
+
* <PlatformClientListPanel
|
|
44
|
+
* org="acme"
|
|
45
|
+
* onEdit={(pc) => setFlow({ phase: "editing", platformClient: pc })}
|
|
46
|
+
* onRefetchRef={(refetch) => { listRefetchRef.current = refetch; }}
|
|
47
|
+
* />
|
|
48
|
+
* ```
|
|
49
|
+
*/
|
|
50
|
+
export function PlatformClientListPanel({
|
|
51
|
+
org,
|
|
52
|
+
onEdit,
|
|
53
|
+
onRefetchRef,
|
|
54
|
+
className,
|
|
55
|
+
}: PlatformClientListPanelProps) {
|
|
56
|
+
const { platformClients, isLoading, error, refetch } =
|
|
57
|
+
usePlatformClientList(org);
|
|
58
|
+
const [confirmingId, setConfirmingId] = useState<string | null>(null);
|
|
59
|
+
|
|
60
|
+
if (onRefetchRef) {
|
|
61
|
+
onRefetchRef(refetch);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (isLoading) {
|
|
65
|
+
return (
|
|
66
|
+
<div
|
|
67
|
+
className={cn("space-y-2", className)}
|
|
68
|
+
aria-busy="true"
|
|
69
|
+
aria-label="Loading platform clients"
|
|
70
|
+
>
|
|
71
|
+
{Array.from({ length: 2 }, (_, i) => (
|
|
72
|
+
<div
|
|
73
|
+
key={i}
|
|
74
|
+
className="bg-muted/40 h-14 animate-pulse rounded-lg"
|
|
75
|
+
/>
|
|
76
|
+
))}
|
|
77
|
+
</div>
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (error) {
|
|
82
|
+
return (
|
|
83
|
+
<p className={cn("text-destructive text-xs", className)} role="alert">
|
|
84
|
+
{getUserMessage(error)}
|
|
85
|
+
</p>
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (platformClients.length === 0) {
|
|
90
|
+
return (
|
|
91
|
+
<p
|
|
92
|
+
className={cn(
|
|
93
|
+
"text-muted-foreground py-4 text-center text-xs",
|
|
94
|
+
className,
|
|
95
|
+
)}
|
|
96
|
+
>
|
|
97
|
+
No platform clients configured.
|
|
98
|
+
</p>
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return (
|
|
103
|
+
<div
|
|
104
|
+
className={cn("space-y-2", className)}
|
|
105
|
+
role="list"
|
|
106
|
+
aria-label="Platform clients"
|
|
107
|
+
>
|
|
108
|
+
{platformClients.map((pc) => {
|
|
109
|
+
const id = pc.metadata?.id ?? "";
|
|
110
|
+
return (
|
|
111
|
+
<PlatformClientRow
|
|
112
|
+
key={id}
|
|
113
|
+
platformClient={pc}
|
|
114
|
+
isConfirming={confirmingId === id}
|
|
115
|
+
onEdit={onEdit ? () => onEdit(pc) : undefined}
|
|
116
|
+
onConfirmDelete={() => setConfirmingId(id)}
|
|
117
|
+
onCancelDelete={() => setConfirmingId(null)}
|
|
118
|
+
onDeleted={() => {
|
|
119
|
+
setConfirmingId(null);
|
|
120
|
+
refetch();
|
|
121
|
+
}}
|
|
122
|
+
/>
|
|
123
|
+
);
|
|
124
|
+
})}
|
|
125
|
+
</div>
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// ---------------------------------------------------------------------------
|
|
130
|
+
// PlatformClientRow (internal)
|
|
131
|
+
// ---------------------------------------------------------------------------
|
|
132
|
+
|
|
133
|
+
function PlatformClientRow({
|
|
134
|
+
platformClient,
|
|
135
|
+
isConfirming,
|
|
136
|
+
onEdit,
|
|
137
|
+
onConfirmDelete,
|
|
138
|
+
onCancelDelete,
|
|
139
|
+
onDeleted,
|
|
140
|
+
}: {
|
|
141
|
+
platformClient: PlatformClient;
|
|
142
|
+
isConfirming: boolean;
|
|
143
|
+
onEdit?: () => void;
|
|
144
|
+
onConfirmDelete: () => void;
|
|
145
|
+
onCancelDelete: () => void;
|
|
146
|
+
onDeleted: () => void;
|
|
147
|
+
}) {
|
|
148
|
+
const { deletePlatformClient, isDeleting, error } =
|
|
149
|
+
useDeletePlatformClient();
|
|
150
|
+
|
|
151
|
+
const id = platformClient.metadata?.id ?? "";
|
|
152
|
+
const spec = platformClient.spec;
|
|
153
|
+
const name = platformClient.metadata?.name ?? "Unnamed client";
|
|
154
|
+
const clientId = spec?.clientId ?? "";
|
|
155
|
+
const fingerprint = spec?.secretFingerprint ?? "";
|
|
156
|
+
const createdAt =
|
|
157
|
+
platformClient.status?.audit?.specAudit?.createdAt;
|
|
158
|
+
|
|
159
|
+
const handleDelete = useCallback(async () => {
|
|
160
|
+
try {
|
|
161
|
+
await deletePlatformClient({ resourceId: id });
|
|
162
|
+
onDeleted();
|
|
163
|
+
} catch {
|
|
164
|
+
// error state is surfaced via the hook
|
|
165
|
+
}
|
|
166
|
+
}, [id, deletePlatformClient, onDeleted]);
|
|
167
|
+
|
|
168
|
+
if (isConfirming) {
|
|
169
|
+
return (
|
|
170
|
+
<div
|
|
171
|
+
role="listitem"
|
|
172
|
+
className="flex items-center justify-between rounded-lg border border-destructive/30 bg-destructive/5 px-3 py-2.5"
|
|
173
|
+
>
|
|
174
|
+
<div className="min-w-0 flex-1">
|
|
175
|
+
<p className="text-xs text-foreground">
|
|
176
|
+
Delete <span className="font-medium">{name}</span>?
|
|
177
|
+
{clientId && (
|
|
178
|
+
<span className="ml-1 text-muted-foreground">
|
|
179
|
+
This will invalidate client ID{" "}
|
|
180
|
+
<code className="font-mono">{clientId}</code>.
|
|
181
|
+
</span>
|
|
182
|
+
)}
|
|
183
|
+
</p>
|
|
184
|
+
{error && (
|
|
185
|
+
<p className="mt-0.5 text-[0.65rem] text-destructive">
|
|
186
|
+
{getUserMessage(error)}
|
|
187
|
+
</p>
|
|
188
|
+
)}
|
|
189
|
+
</div>
|
|
190
|
+
|
|
191
|
+
<div className="flex shrink-0 items-center gap-1.5">
|
|
192
|
+
<button
|
|
193
|
+
type="button"
|
|
194
|
+
onClick={handleDelete}
|
|
195
|
+
disabled={isDeleting}
|
|
196
|
+
className={cn(
|
|
197
|
+
"inline-flex items-center gap-1 rounded-md px-2.5 py-1 text-xs font-medium",
|
|
198
|
+
"bg-destructive text-destructive-foreground hover:bg-destructive/90",
|
|
199
|
+
"disabled:pointer-events-none disabled:opacity-50",
|
|
200
|
+
)}
|
|
201
|
+
>
|
|
202
|
+
{isDeleting && <SpinnerIcon />}
|
|
203
|
+
Delete
|
|
204
|
+
</button>
|
|
205
|
+
<button
|
|
206
|
+
type="button"
|
|
207
|
+
onClick={onCancelDelete}
|
|
208
|
+
disabled={isDeleting}
|
|
209
|
+
className={cn(
|
|
210
|
+
"rounded-md px-2.5 py-1 text-xs",
|
|
211
|
+
"text-muted-foreground hover:text-foreground hover:bg-accent/50",
|
|
212
|
+
"disabled:pointer-events-none disabled:opacity-50",
|
|
213
|
+
)}
|
|
214
|
+
>
|
|
215
|
+
Cancel
|
|
216
|
+
</button>
|
|
217
|
+
</div>
|
|
218
|
+
</div>
|
|
219
|
+
);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
return (
|
|
223
|
+
<div
|
|
224
|
+
role="listitem"
|
|
225
|
+
className="flex items-center gap-3 rounded-lg border border-border/60 px-3 py-2.5 hover:border-border transition-colors"
|
|
226
|
+
>
|
|
227
|
+
<KeyIcon />
|
|
228
|
+
|
|
229
|
+
<div className="min-w-0 flex-1">
|
|
230
|
+
<span className="block truncate text-sm font-medium text-foreground">
|
|
231
|
+
{name}
|
|
232
|
+
</span>
|
|
233
|
+
{clientId && (
|
|
234
|
+
<span className="block truncate text-xs text-muted-foreground font-mono">
|
|
235
|
+
{clientId}
|
|
236
|
+
</span>
|
|
237
|
+
)}
|
|
238
|
+
</div>
|
|
239
|
+
|
|
240
|
+
<div className="hidden sm:flex shrink-0 items-center gap-3 text-xs text-muted-foreground">
|
|
241
|
+
{fingerprint && (
|
|
242
|
+
<span
|
|
243
|
+
className="font-mono"
|
|
244
|
+
title={`Secret fingerprint: ${fingerprint}`}
|
|
245
|
+
>
|
|
246
|
+
••••{fingerprint.slice(-4)}
|
|
247
|
+
</span>
|
|
248
|
+
)}
|
|
249
|
+
<ExpiryBadge spec={spec} />
|
|
250
|
+
{spec?.autoProvisionAccounts && (
|
|
251
|
+
<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">
|
|
252
|
+
JIT
|
|
253
|
+
</span>
|
|
254
|
+
)}
|
|
255
|
+
{createdAt && (
|
|
256
|
+
<span title={`Created ${timestampDate(createdAt).toISOString()}`}>
|
|
257
|
+
{formatShortDate(timestampDate(createdAt))}
|
|
258
|
+
</span>
|
|
259
|
+
)}
|
|
260
|
+
</div>
|
|
261
|
+
|
|
262
|
+
<div className="flex shrink-0 items-center gap-1">
|
|
263
|
+
{onEdit && (
|
|
264
|
+
<button
|
|
265
|
+
type="button"
|
|
266
|
+
onClick={onEdit}
|
|
267
|
+
aria-label={`Edit ${name}`}
|
|
268
|
+
className={cn(
|
|
269
|
+
"shrink-0 rounded p-1",
|
|
270
|
+
"text-muted-foreground hover:text-foreground hover:bg-accent/50",
|
|
271
|
+
"transition-colors",
|
|
272
|
+
)}
|
|
273
|
+
>
|
|
274
|
+
<PencilIcon />
|
|
275
|
+
</button>
|
|
276
|
+
)}
|
|
277
|
+
<button
|
|
278
|
+
type="button"
|
|
279
|
+
onClick={onConfirmDelete}
|
|
280
|
+
aria-label={`Delete ${name}`}
|
|
281
|
+
className={cn(
|
|
282
|
+
"shrink-0 rounded p-1",
|
|
283
|
+
"text-muted-foreground hover:text-destructive hover:bg-destructive/10",
|
|
284
|
+
"transition-colors",
|
|
285
|
+
)}
|
|
286
|
+
>
|
|
287
|
+
<TrashIcon />
|
|
288
|
+
</button>
|
|
289
|
+
</div>
|
|
290
|
+
</div>
|
|
291
|
+
);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// ---------------------------------------------------------------------------
|
|
295
|
+
// ExpiryBadge (internal)
|
|
296
|
+
// ---------------------------------------------------------------------------
|
|
297
|
+
|
|
298
|
+
function ExpiryBadge({ spec }: { spec: PlatformClient["spec"] }) {
|
|
299
|
+
if (spec?.neverExpires) {
|
|
300
|
+
return (
|
|
301
|
+
<span className="text-[0.65rem] text-muted-foreground">No expiry</span>
|
|
302
|
+
);
|
|
303
|
+
}
|
|
304
|
+
if (spec?.expiresAt) {
|
|
305
|
+
const date = timestampDate(spec.expiresAt);
|
|
306
|
+
const isExpired = date < new Date();
|
|
307
|
+
return (
|
|
308
|
+
<span
|
|
309
|
+
className={cn(
|
|
310
|
+
"text-[0.65rem]",
|
|
311
|
+
isExpired ? "text-destructive font-medium" : "text-muted-foreground",
|
|
312
|
+
)}
|
|
313
|
+
title={`Expires ${date.toISOString()}`}
|
|
314
|
+
>
|
|
315
|
+
{isExpired ? "Expired" : `Exp ${formatShortDate(date)}`}
|
|
316
|
+
</span>
|
|
317
|
+
);
|
|
318
|
+
}
|
|
319
|
+
return null;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
// ---------------------------------------------------------------------------
|
|
323
|
+
// Formatting helpers
|
|
324
|
+
// ---------------------------------------------------------------------------
|
|
325
|
+
|
|
326
|
+
function formatShortDate(date: Date): string {
|
|
327
|
+
return date.toLocaleDateString(undefined, {
|
|
328
|
+
month: "short",
|
|
329
|
+
day: "numeric",
|
|
330
|
+
year: "numeric",
|
|
331
|
+
});
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
// ---------------------------------------------------------------------------
|
|
335
|
+
// Icons
|
|
336
|
+
// ---------------------------------------------------------------------------
|
|
337
|
+
|
|
338
|
+
function KeyIcon() {
|
|
339
|
+
return (
|
|
340
|
+
<svg
|
|
341
|
+
width="14"
|
|
342
|
+
height="14"
|
|
343
|
+
viewBox="0 0 16 16"
|
|
344
|
+
fill="none"
|
|
345
|
+
stroke="currentColor"
|
|
346
|
+
strokeWidth="1.5"
|
|
347
|
+
strokeLinecap="round"
|
|
348
|
+
strokeLinejoin="round"
|
|
349
|
+
aria-hidden="true"
|
|
350
|
+
className="shrink-0 text-muted-foreground"
|
|
351
|
+
>
|
|
352
|
+
<circle cx="10.5" cy="5.5" r="3" />
|
|
353
|
+
<path d="M8.5 7.5L3 13l-.5-2.5L5 10l1-1-1-1 3.5.5z" />
|
|
354
|
+
</svg>
|
|
355
|
+
);
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
function PencilIcon() {
|
|
359
|
+
return (
|
|
360
|
+
<svg
|
|
361
|
+
width="14"
|
|
362
|
+
height="14"
|
|
363
|
+
viewBox="0 0 16 16"
|
|
364
|
+
fill="none"
|
|
365
|
+
stroke="currentColor"
|
|
366
|
+
strokeWidth="1.5"
|
|
367
|
+
strokeLinecap="round"
|
|
368
|
+
strokeLinejoin="round"
|
|
369
|
+
aria-hidden="true"
|
|
370
|
+
>
|
|
371
|
+
<path d="M11 2.5l2.5 2.5L5 13.5H2.5V11L11 2.5z" />
|
|
372
|
+
</svg>
|
|
373
|
+
);
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
function TrashIcon() {
|
|
377
|
+
return (
|
|
378
|
+
<svg
|
|
379
|
+
width="14"
|
|
380
|
+
height="14"
|
|
381
|
+
viewBox="0 0 16 16"
|
|
382
|
+
fill="none"
|
|
383
|
+
stroke="currentColor"
|
|
384
|
+
strokeWidth="1.5"
|
|
385
|
+
strokeLinecap="round"
|
|
386
|
+
strokeLinejoin="round"
|
|
387
|
+
aria-hidden="true"
|
|
388
|
+
>
|
|
389
|
+
<path d="M2.5 4h11M5.5 4V2.5a1 1 0 0 1 1-1h3a1 1 0 0 1 1 1V4" />
|
|
390
|
+
<path d="M12.5 4v9a1 1 0 0 1-1 1h-7a1 1 0 0 1-1-1V4" />
|
|
391
|
+
<line x1="6.5" y1="7" x2="6.5" y2="11" />
|
|
392
|
+
<line x1="9.5" y1="7" x2="9.5" y2="11" />
|
|
393
|
+
</svg>
|
|
394
|
+
);
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
function SpinnerIcon() {
|
|
398
|
+
return (
|
|
399
|
+
<svg
|
|
400
|
+
width="12"
|
|
401
|
+
height="12"
|
|
402
|
+
viewBox="0 0 16 16"
|
|
403
|
+
fill="none"
|
|
404
|
+
stroke="currentColor"
|
|
405
|
+
strokeWidth="2"
|
|
406
|
+
strokeLinecap="round"
|
|
407
|
+
className="animate-spin"
|
|
408
|
+
aria-hidden="true"
|
|
409
|
+
>
|
|
410
|
+
<path d="M8 2a6 6 0 1 0 6 6" />
|
|
411
|
+
</svg>
|
|
412
|
+
);
|
|
413
|
+
}
|
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useCallback, useState } from "react";
|
|
4
|
+
import { cn } from "@stigmer/theme";
|
|
5
|
+
|
|
6
|
+
/** Props for {@link PlatformClientSecretAlert}. */
|
|
7
|
+
export interface PlatformClientSecretAlertProps {
|
|
8
|
+
/** The `client_id` to display alongside the secret for pairing context. */
|
|
9
|
+
readonly clientId: string;
|
|
10
|
+
/** The raw client secret value to display. Shown exactly once. */
|
|
11
|
+
readonly clientSecret: string;
|
|
12
|
+
/** Whether this alert follows a creation or a secret rotation. */
|
|
13
|
+
readonly context: "created" | "rotated";
|
|
14
|
+
/** Fired when the user dismisses the alert. */
|
|
15
|
+
readonly onDismiss: () => void;
|
|
16
|
+
/** Additional CSS class names for the root container. */
|
|
17
|
+
readonly className?: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const COPIED_FEEDBACK_MS = 2000;
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* One-time reveal component displayed after a platform client is
|
|
24
|
+
* created or its secret is rotated.
|
|
25
|
+
*
|
|
26
|
+
* Shows both the `client_id` (read-only, for context) and the raw
|
|
27
|
+
* `client_secret` with a **Copy** button. The secret is only
|
|
28
|
+
* available once — the server never returns it again — so this
|
|
29
|
+
* component prominently warns the user to copy it immediately.
|
|
30
|
+
*
|
|
31
|
+
* This is a standalone alert component, not a modal — the parent
|
|
32
|
+
* decides how to present and position it.
|
|
33
|
+
*
|
|
34
|
+
* All visual properties flow through `--stgm-*` design tokens.
|
|
35
|
+
*
|
|
36
|
+
* @example
|
|
37
|
+
* ```tsx
|
|
38
|
+
* <PlatformClientSecretAlert
|
|
39
|
+
* clientId="stgm_pc_abc123"
|
|
40
|
+
* clientSecret="stgm_secret_xyz..."
|
|
41
|
+
* context="created"
|
|
42
|
+
* onDismiss={() => setFlow({ phase: "idle" })}
|
|
43
|
+
* />
|
|
44
|
+
* ```
|
|
45
|
+
*/
|
|
46
|
+
export function PlatformClientSecretAlert({
|
|
47
|
+
clientId,
|
|
48
|
+
clientSecret,
|
|
49
|
+
context,
|
|
50
|
+
onDismiss,
|
|
51
|
+
className,
|
|
52
|
+
}: PlatformClientSecretAlertProps) {
|
|
53
|
+
const [copiedField, setCopiedField] = useState<
|
|
54
|
+
"id" | "secret" | null
|
|
55
|
+
>(null);
|
|
56
|
+
|
|
57
|
+
const handleCopy = useCallback(
|
|
58
|
+
async (value: string, field: "id" | "secret") => {
|
|
59
|
+
try {
|
|
60
|
+
await navigator.clipboard.writeText(value);
|
|
61
|
+
setCopiedField(field);
|
|
62
|
+
setTimeout(() => setCopiedField(null), COPIED_FEEDBACK_MS);
|
|
63
|
+
} catch {
|
|
64
|
+
const el = document.getElementById(
|
|
65
|
+
field === "secret"
|
|
66
|
+
? "stgm-pc-secret-reveal"
|
|
67
|
+
: "stgm-pc-client-id-reveal",
|
|
68
|
+
);
|
|
69
|
+
if (el) {
|
|
70
|
+
const selection = window.getSelection();
|
|
71
|
+
const range = document.createRange();
|
|
72
|
+
range.selectNodeContents(el);
|
|
73
|
+
selection?.removeAllRanges();
|
|
74
|
+
selection?.addRange(range);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
},
|
|
78
|
+
[],
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
const title =
|
|
82
|
+
context === "created"
|
|
83
|
+
? "Platform client created"
|
|
84
|
+
: "Client secret rotated";
|
|
85
|
+
|
|
86
|
+
return (
|
|
87
|
+
<div
|
|
88
|
+
role="alert"
|
|
89
|
+
className={cn(
|
|
90
|
+
"rounded-lg border border-primary/30 bg-primary-subtle p-4",
|
|
91
|
+
className,
|
|
92
|
+
)}
|
|
93
|
+
>
|
|
94
|
+
<div className="mb-3 flex items-start justify-between gap-3">
|
|
95
|
+
<div className="min-w-0">
|
|
96
|
+
<p className="text-sm font-medium text-foreground">{title}</p>
|
|
97
|
+
<p className="mt-0.5 text-xs text-muted-foreground">
|
|
98
|
+
Copy the client secret now. It will not be shown again.
|
|
99
|
+
</p>
|
|
100
|
+
</div>
|
|
101
|
+
<button
|
|
102
|
+
type="button"
|
|
103
|
+
onClick={onDismiss}
|
|
104
|
+
aria-label="Dismiss"
|
|
105
|
+
className={cn(
|
|
106
|
+
"shrink-0 rounded p-1",
|
|
107
|
+
"text-muted-foreground hover:text-foreground hover:bg-accent/50",
|
|
108
|
+
"transition-colors",
|
|
109
|
+
)}
|
|
110
|
+
>
|
|
111
|
+
<CloseIcon />
|
|
112
|
+
</button>
|
|
113
|
+
</div>
|
|
114
|
+
|
|
115
|
+
<div className="space-y-2">
|
|
116
|
+
<CopyableField
|
|
117
|
+
id="stgm-pc-client-id-reveal"
|
|
118
|
+
label="Client ID"
|
|
119
|
+
value={clientId}
|
|
120
|
+
copied={copiedField === "id"}
|
|
121
|
+
onCopy={() => handleCopy(clientId, "id")}
|
|
122
|
+
/>
|
|
123
|
+
<CopyableField
|
|
124
|
+
id="stgm-pc-secret-reveal"
|
|
125
|
+
label="Client secret"
|
|
126
|
+
value={clientSecret}
|
|
127
|
+
copied={copiedField === "secret"}
|
|
128
|
+
onCopy={() => handleCopy(clientSecret, "secret")}
|
|
129
|
+
/>
|
|
130
|
+
</div>
|
|
131
|
+
|
|
132
|
+
<div
|
|
133
|
+
role="status"
|
|
134
|
+
aria-live="polite"
|
|
135
|
+
aria-atomic="true"
|
|
136
|
+
className="sr-only"
|
|
137
|
+
>
|
|
138
|
+
{copiedField === "id" && "Client ID copied to clipboard"}
|
|
139
|
+
{copiedField === "secret" && "Client secret copied to clipboard"}
|
|
140
|
+
</div>
|
|
141
|
+
</div>
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// ---------------------------------------------------------------------------
|
|
146
|
+
// CopyableField (internal)
|
|
147
|
+
// ---------------------------------------------------------------------------
|
|
148
|
+
|
|
149
|
+
function CopyableField({
|
|
150
|
+
id,
|
|
151
|
+
label,
|
|
152
|
+
value,
|
|
153
|
+
copied,
|
|
154
|
+
onCopy,
|
|
155
|
+
}: {
|
|
156
|
+
id: string;
|
|
157
|
+
label: string;
|
|
158
|
+
value: string;
|
|
159
|
+
copied: boolean;
|
|
160
|
+
onCopy: () => void;
|
|
161
|
+
}) {
|
|
162
|
+
return (
|
|
163
|
+
<div>
|
|
164
|
+
<span className="text-[0.65rem] font-medium text-muted-foreground">
|
|
165
|
+
{label}
|
|
166
|
+
</span>
|
|
167
|
+
<div className="mt-0.5 flex items-center gap-2">
|
|
168
|
+
<code
|
|
169
|
+
id={id}
|
|
170
|
+
className={cn(
|
|
171
|
+
"min-w-0 flex-1 select-all truncate rounded-md",
|
|
172
|
+
"border border-input bg-background px-2.5 py-1.5",
|
|
173
|
+
"font-mono text-xs text-foreground",
|
|
174
|
+
)}
|
|
175
|
+
>
|
|
176
|
+
{value}
|
|
177
|
+
</code>
|
|
178
|
+
<button
|
|
179
|
+
type="button"
|
|
180
|
+
onClick={onCopy}
|
|
181
|
+
aria-label={`Copy ${label}`}
|
|
182
|
+
className={cn(
|
|
183
|
+
"inline-flex shrink-0 items-center gap-1.5 rounded-md px-3 py-1.5 text-xs font-medium",
|
|
184
|
+
"bg-primary text-primary-foreground hover:bg-primary/90",
|
|
185
|
+
"transition-colors",
|
|
186
|
+
)}
|
|
187
|
+
>
|
|
188
|
+
{copied ? <CheckIcon /> : <CopyIcon />}
|
|
189
|
+
{copied ? "Copied" : "Copy"}
|
|
190
|
+
</button>
|
|
191
|
+
</div>
|
|
192
|
+
</div>
|
|
193
|
+
);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// ---------------------------------------------------------------------------
|
|
197
|
+
// Icons
|
|
198
|
+
// ---------------------------------------------------------------------------
|
|
199
|
+
|
|
200
|
+
function CopyIcon() {
|
|
201
|
+
return (
|
|
202
|
+
<svg
|
|
203
|
+
width="12"
|
|
204
|
+
height="12"
|
|
205
|
+
viewBox="0 0 16 16"
|
|
206
|
+
fill="none"
|
|
207
|
+
stroke="currentColor"
|
|
208
|
+
strokeWidth="1.5"
|
|
209
|
+
strokeLinecap="round"
|
|
210
|
+
strokeLinejoin="round"
|
|
211
|
+
aria-hidden="true"
|
|
212
|
+
>
|
|
213
|
+
<rect x="5" y="5" width="9" height="9" rx="1.5" />
|
|
214
|
+
<path d="M5 11H3.5A1.5 1.5 0 0 1 2 9.5V3.5A1.5 1.5 0 0 1 3.5 2h6A1.5 1.5 0 0 1 11 3.5V5" />
|
|
215
|
+
</svg>
|
|
216
|
+
);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function CheckIcon() {
|
|
220
|
+
return (
|
|
221
|
+
<svg
|
|
222
|
+
width="12"
|
|
223
|
+
height="12"
|
|
224
|
+
viewBox="0 0 16 16"
|
|
225
|
+
fill="none"
|
|
226
|
+
stroke="currentColor"
|
|
227
|
+
strokeWidth="2"
|
|
228
|
+
strokeLinecap="round"
|
|
229
|
+
strokeLinejoin="round"
|
|
230
|
+
aria-hidden="true"
|
|
231
|
+
>
|
|
232
|
+
<path d="M3 8.5l3.5 3.5L13 5" />
|
|
233
|
+
</svg>
|
|
234
|
+
);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function CloseIcon() {
|
|
238
|
+
return (
|
|
239
|
+
<svg
|
|
240
|
+
width="14"
|
|
241
|
+
height="14"
|
|
242
|
+
viewBox="0 0 16 16"
|
|
243
|
+
fill="none"
|
|
244
|
+
stroke="currentColor"
|
|
245
|
+
strokeWidth="1.5"
|
|
246
|
+
strokeLinecap="round"
|
|
247
|
+
aria-hidden="true"
|
|
248
|
+
>
|
|
249
|
+
<path d="M4 4l8 8M12 4l-8 8" />
|
|
250
|
+
</svg>
|
|
251
|
+
);
|
|
252
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
export {
|
|
2
|
+
usePlatformClientList,
|
|
3
|
+
type UsePlatformClientListReturn,
|
|
4
|
+
} from "./usePlatformClientList";
|
|
5
|
+
|
|
6
|
+
export {
|
|
7
|
+
usePlatformClient,
|
|
8
|
+
type UsePlatformClientReturn,
|
|
9
|
+
} from "./usePlatformClient";
|
|
10
|
+
|
|
11
|
+
export {
|
|
12
|
+
useCreatePlatformClient,
|
|
13
|
+
type UseCreatePlatformClientReturn,
|
|
14
|
+
} from "./useCreatePlatformClient";
|
|
15
|
+
|
|
16
|
+
export {
|
|
17
|
+
useUpdatePlatformClient,
|
|
18
|
+
type UseUpdatePlatformClientReturn,
|
|
19
|
+
} from "./useUpdatePlatformClient";
|
|
20
|
+
|
|
21
|
+
export {
|
|
22
|
+
useDeletePlatformClient,
|
|
23
|
+
type UseDeletePlatformClientReturn,
|
|
24
|
+
} from "./useDeletePlatformClient";
|
|
25
|
+
|
|
26
|
+
export {
|
|
27
|
+
useRotatePlatformClientSecret,
|
|
28
|
+
type UseRotatePlatformClientSecretReturn,
|
|
29
|
+
} from "./useRotatePlatformClientSecret";
|
|
30
|
+
|
|
31
|
+
export {
|
|
32
|
+
PlatformClientListPanel,
|
|
33
|
+
type PlatformClientListPanelProps,
|
|
34
|
+
} from "./PlatformClientListPanel";
|
|
35
|
+
|
|
36
|
+
export {
|
|
37
|
+
CreatePlatformClientForm,
|
|
38
|
+
type CreatePlatformClientFormProps,
|
|
39
|
+
} from "./CreatePlatformClientForm";
|
|
40
|
+
|
|
41
|
+
export {
|
|
42
|
+
PlatformClientDetailPanel,
|
|
43
|
+
type PlatformClientDetailPanelProps,
|
|
44
|
+
} from "./PlatformClientDetailPanel";
|
|
45
|
+
|
|
46
|
+
export {
|
|
47
|
+
PlatformClientSecretAlert,
|
|
48
|
+
type PlatformClientSecretAlertProps,
|
|
49
|
+
} from "./PlatformClientSecretAlert";
|