@stigmer/react 0.0.84 → 0.0.85
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/demo/fixtures.d.ts +4 -0
- package/demo/fixtures.d.ts.map +1 -1
- package/demo/fixtures.js +4 -0
- package/demo/fixtures.js.map +1 -1
- package/index.d.ts +4 -2
- package/index.d.ts.map +1 -1
- package/index.js +3 -1
- package/index.js.map +1 -1
- package/library/ResourceListView.js +1 -1
- package/library/ResourceListView.js.map +1 -1
- package/mcp-server/McpServerConnectDialog.d.ts +51 -0
- package/mcp-server/McpServerConnectDialog.d.ts.map +1 -0
- package/mcp-server/McpServerConnectDialog.js +164 -0
- package/mcp-server/McpServerConnectDialog.js.map +1 -0
- package/mcp-server/McpServerDetailView.js +2 -2
- package/mcp-server/McpServerDetailView.js.map +1 -1
- package/mcp-server/McpServerPicker.d.ts.map +1 -1
- package/mcp-server/McpServerPicker.js +7 -1
- package/mcp-server/McpServerPicker.js.map +1 -1
- package/mcp-server/index.d.ts +2 -0
- package/mcp-server/index.d.ts.map +1 -1
- package/mcp-server/index.js +1 -0
- package/mcp-server/index.js.map +1 -1
- package/oauth-app/CreateOAuthAppForm.d.ts +41 -0
- package/oauth-app/CreateOAuthAppForm.d.ts.map +1 -0
- package/oauth-app/CreateOAuthAppForm.js +140 -0
- package/oauth-app/CreateOAuthAppForm.js.map +1 -0
- package/oauth-app/OAuthAppDetailPanel.d.ts +43 -0
- package/oauth-app/OAuthAppDetailPanel.d.ts.map +1 -0
- package/oauth-app/OAuthAppDetailPanel.js +202 -0
- package/oauth-app/OAuthAppDetailPanel.js.map +1 -0
- package/oauth-app/OAuthAppListPanel.d.ts +43 -0
- package/oauth-app/OAuthAppListPanel.d.ts.map +1 -0
- package/oauth-app/OAuthAppListPanel.js +79 -0
- package/oauth-app/OAuthAppListPanel.js.map +1 -0
- package/oauth-app/index.d.ts +15 -0
- package/oauth-app/index.d.ts.map +1 -0
- package/oauth-app/index.js +8 -0
- package/oauth-app/index.js.map +1 -0
- package/oauth-app/useCreateOAuthApp.d.ts +39 -0
- package/oauth-app/useCreateOAuthApp.d.ts.map +1 -0
- package/oauth-app/useCreateOAuthApp.js +50 -0
- package/oauth-app/useCreateOAuthApp.js.map +1 -0
- package/oauth-app/useDeleteOAuthApp.d.ts +31 -0
- package/oauth-app/useDeleteOAuthApp.d.ts.map +1 -0
- package/oauth-app/useDeleteOAuthApp.js +43 -0
- package/oauth-app/useDeleteOAuthApp.js.map +1 -0
- package/oauth-app/useOAuthAppList.d.ts +32 -0
- package/oauth-app/useOAuthAppList.d.ts.map +1 -0
- package/oauth-app/useOAuthAppList.js +61 -0
- package/oauth-app/useOAuthAppList.js.map +1 -0
- package/oauth-app/useUpdateOAuthApp.d.ts +38 -0
- package/oauth-app/useUpdateOAuthApp.d.ts.map +1 -0
- package/oauth-app/useUpdateOAuthApp.js +49 -0
- package/oauth-app/useUpdateOAuthApp.js.map +1 -0
- package/package.json +4 -4
- package/src/demo/fixtures.ts +8 -0
- package/src/index.ts +22 -0
- package/src/library/ResourceListView.tsx +8 -8
- package/src/mcp-server/McpServerConnectDialog.tsx +527 -0
- package/src/mcp-server/McpServerDetailView.tsx +2 -1
- package/src/mcp-server/McpServerPicker.tsx +8 -1
- package/src/mcp-server/index.ts +3 -0
- package/src/oauth-app/CreateOAuthAppForm.tsx +449 -0
- package/src/oauth-app/OAuthAppDetailPanel.tsx +671 -0
- package/src/oauth-app/OAuthAppListPanel.tsx +237 -0
- package/src/oauth-app/index.ts +14 -0
- package/src/oauth-app/useCreateOAuthApp.ts +70 -0
- package/src/oauth-app/useDeleteOAuthApp.ts +62 -0
- package/src/oauth-app/useOAuthAppList.ts +84 -0
- package/src/oauth-app/useUpdateOAuthApp.ts +69 -0
- package/styles.css +1 -1
|
@@ -0,0 +1,671 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useCallback, useState, type FormEvent } from "react";
|
|
4
|
+
import { cn } from "@stigmer/theme";
|
|
5
|
+
import { getUserMessage } from "@stigmer/sdk";
|
|
6
|
+
import type { OAuthApp } from "@stigmer/protos/ai/stigmer/iam/oauthapp/v1/api_pb";
|
|
7
|
+
import { VendorApprovalStatus } from "@stigmer/protos/ai/stigmer/iam/oauthapp/v1/spec_pb";
|
|
8
|
+
import { timestampDate, type Timestamp } from "@bufbuild/protobuf/wkt";
|
|
9
|
+
import { useUpdateOAuthApp } from "./useUpdateOAuthApp";
|
|
10
|
+
import { useDeleteOAuthApp } from "./useDeleteOAuthApp";
|
|
11
|
+
|
|
12
|
+
// ---------------------------------------------------------------------------
|
|
13
|
+
// Public API
|
|
14
|
+
// ---------------------------------------------------------------------------
|
|
15
|
+
|
|
16
|
+
/** Props for {@link OAuthAppDetailPanel}. */
|
|
17
|
+
export interface OAuthAppDetailPanelProps {
|
|
18
|
+
/** The OAuth app resource to display and edit. */
|
|
19
|
+
readonly oauthApp: OAuthApp;
|
|
20
|
+
/** Fired with the updated resource after a successful save. */
|
|
21
|
+
readonly onUpdated?: (app: OAuthApp) => void;
|
|
22
|
+
/** Fired after the resource is successfully deleted. */
|
|
23
|
+
readonly onDeleted?: () => void;
|
|
24
|
+
/** Fired when the user clicks the back button. */
|
|
25
|
+
readonly onBack?: () => void;
|
|
26
|
+
/** Additional CSS class names for the root container. */
|
|
27
|
+
readonly className?: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* View and edit panel for an existing OAuth app.
|
|
32
|
+
*
|
|
33
|
+
* In **view mode**, displays all OAuth configuration fields in a
|
|
34
|
+
* structured label/value layout with "Edit" and "Delete" buttons.
|
|
35
|
+
*
|
|
36
|
+
* In **edit mode**, fields become editable inputs. The client secret
|
|
37
|
+
* field shows a placeholder — leave it empty to keep the existing
|
|
38
|
+
* secret, or enter a new value to replace it. "Save" submits the
|
|
39
|
+
* update via {@link useUpdateOAuthApp}; "Cancel" discards changes
|
|
40
|
+
* and returns to view mode.
|
|
41
|
+
*
|
|
42
|
+
* Delete uses an inline confirmation pattern (no modal) to avoid
|
|
43
|
+
* portal/z-index issues for SDK embedders.
|
|
44
|
+
*
|
|
45
|
+
* All visual properties flow through `--stgm-*` design tokens.
|
|
46
|
+
*
|
|
47
|
+
* @example
|
|
48
|
+
* ```tsx
|
|
49
|
+
* <OAuthAppDetailPanel
|
|
50
|
+
* oauthApp={app}
|
|
51
|
+
* onUpdated={(updated) => refetch()}
|
|
52
|
+
* onDeleted={() => { refetch(); setFlow({ phase: "idle" }); }}
|
|
53
|
+
* onBack={() => setFlow({ phase: "idle" })}
|
|
54
|
+
* />
|
|
55
|
+
* ```
|
|
56
|
+
*/
|
|
57
|
+
export function OAuthAppDetailPanel({
|
|
58
|
+
oauthApp,
|
|
59
|
+
onUpdated,
|
|
60
|
+
onDeleted,
|
|
61
|
+
onBack,
|
|
62
|
+
className,
|
|
63
|
+
}: OAuthAppDetailPanelProps) {
|
|
64
|
+
const spec = oauthApp.spec;
|
|
65
|
+
const meta = oauthApp.metadata;
|
|
66
|
+
|
|
67
|
+
const { update, isUpdating, error: updateError, clearError: clearUpdateError } =
|
|
68
|
+
useUpdateOAuthApp();
|
|
69
|
+
const { deleteApp, isDeleting, error: deleteError, clearError: clearDeleteError } =
|
|
70
|
+
useDeleteOAuthApp();
|
|
71
|
+
|
|
72
|
+
const [mode, setMode] = useState<"view" | "edit">("view");
|
|
73
|
+
const [confirmingDelete, setConfirmingDelete] = useState(false);
|
|
74
|
+
|
|
75
|
+
// Edit form state — initialized from current resource
|
|
76
|
+
const [provider, setProvider] = useState(spec?.provider ?? "");
|
|
77
|
+
const [clientId, setClientId] = useState(spec?.clientId ?? "");
|
|
78
|
+
const [clientSecret, setClientSecret] = useState("");
|
|
79
|
+
const [authorizationUrl, setAuthorizationUrl] = useState(
|
|
80
|
+
spec?.authorizationUrl ?? "",
|
|
81
|
+
);
|
|
82
|
+
const [tokenUrl, setTokenUrl] = useState(spec?.tokenUrl ?? "");
|
|
83
|
+
const [scopes, setScopes] = useState(spec?.scopes.join(", ") ?? "");
|
|
84
|
+
const [userinfoUrl, setUserinfoUrl] = useState(spec?.userinfoUrl ?? "");
|
|
85
|
+
const [scopeParameterName, setScopeParameterName] = useState(
|
|
86
|
+
spec?.scopeParameterName ?? "",
|
|
87
|
+
);
|
|
88
|
+
const [vendorApprovalStatus, setVendorApprovalStatus] = useState(
|
|
89
|
+
approvalStatusToKey(spec?.vendorApprovalStatus),
|
|
90
|
+
);
|
|
91
|
+
const [vendorApprovalDocsUrl, setVendorApprovalDocsUrl] = useState(
|
|
92
|
+
spec?.vendorApprovalDocsUrl ?? "",
|
|
93
|
+
);
|
|
94
|
+
|
|
95
|
+
const enterEdit = useCallback(() => {
|
|
96
|
+
setProvider(spec?.provider ?? "");
|
|
97
|
+
setClientId(spec?.clientId ?? "");
|
|
98
|
+
setClientSecret("");
|
|
99
|
+
setAuthorizationUrl(spec?.authorizationUrl ?? "");
|
|
100
|
+
setTokenUrl(spec?.tokenUrl ?? "");
|
|
101
|
+
setScopes(spec?.scopes.join(", ") ?? "");
|
|
102
|
+
setUserinfoUrl(spec?.userinfoUrl ?? "");
|
|
103
|
+
setScopeParameterName(spec?.scopeParameterName ?? "");
|
|
104
|
+
setVendorApprovalStatus(approvalStatusToKey(spec?.vendorApprovalStatus));
|
|
105
|
+
setVendorApprovalDocsUrl(spec?.vendorApprovalDocsUrl ?? "");
|
|
106
|
+
clearUpdateError();
|
|
107
|
+
setMode("edit");
|
|
108
|
+
}, [spec, clearUpdateError]);
|
|
109
|
+
|
|
110
|
+
const cancelEdit = useCallback(() => {
|
|
111
|
+
clearUpdateError();
|
|
112
|
+
setMode("view");
|
|
113
|
+
}, [clearUpdateError]);
|
|
114
|
+
|
|
115
|
+
const handleSave = useCallback(
|
|
116
|
+
async (e: FormEvent) => {
|
|
117
|
+
e.preventDefault();
|
|
118
|
+
clearUpdateError();
|
|
119
|
+
|
|
120
|
+
const parsedScopes = scopes
|
|
121
|
+
.split(",")
|
|
122
|
+
.map((s) => s.trim())
|
|
123
|
+
.filter(Boolean);
|
|
124
|
+
|
|
125
|
+
try {
|
|
126
|
+
const updated = await update({
|
|
127
|
+
name: meta?.name ?? "",
|
|
128
|
+
slug: meta?.slug,
|
|
129
|
+
org: meta?.org ?? "",
|
|
130
|
+
provider: provider.trim(),
|
|
131
|
+
clientId: clientId.trim(),
|
|
132
|
+
...(clientSecret.trim() && { clientSecret: clientSecret.trim() }),
|
|
133
|
+
authorizationUrl: authorizationUrl.trim(),
|
|
134
|
+
tokenUrl: tokenUrl.trim(),
|
|
135
|
+
...(parsedScopes.length > 0 && { scopes: parsedScopes }),
|
|
136
|
+
...(userinfoUrl.trim() && { userinfoUrl: userinfoUrl.trim() }),
|
|
137
|
+
...(scopeParameterName.trim() && {
|
|
138
|
+
scopeParameterName: scopeParameterName.trim(),
|
|
139
|
+
}),
|
|
140
|
+
...(vendorApprovalStatus !== "unspecified" && {
|
|
141
|
+
vendorApprovalStatus: APPROVAL_STATUS_MAP[vendorApprovalStatus],
|
|
142
|
+
}),
|
|
143
|
+
...(vendorApprovalDocsUrl.trim() && {
|
|
144
|
+
vendorApprovalDocsUrl: vendorApprovalDocsUrl.trim(),
|
|
145
|
+
}),
|
|
146
|
+
});
|
|
147
|
+
setMode("view");
|
|
148
|
+
onUpdated?.(updated);
|
|
149
|
+
} catch {
|
|
150
|
+
// error state is managed by useUpdateOAuthApp
|
|
151
|
+
}
|
|
152
|
+
},
|
|
153
|
+
[
|
|
154
|
+
meta, provider, clientId, clientSecret, authorizationUrl, tokenUrl,
|
|
155
|
+
scopes, userinfoUrl, scopeParameterName, vendorApprovalStatus,
|
|
156
|
+
vendorApprovalDocsUrl, update, clearUpdateError, onUpdated,
|
|
157
|
+
],
|
|
158
|
+
);
|
|
159
|
+
|
|
160
|
+
const handleDelete = useCallback(async () => {
|
|
161
|
+
const id = meta?.id;
|
|
162
|
+
if (!id) return;
|
|
163
|
+
|
|
164
|
+
clearDeleteError();
|
|
165
|
+
try {
|
|
166
|
+
await deleteApp(id);
|
|
167
|
+
onDeleted?.();
|
|
168
|
+
} catch {
|
|
169
|
+
// error state is managed by useDeleteOAuthApp
|
|
170
|
+
}
|
|
171
|
+
}, [meta, deleteApp, clearDeleteError, onDeleted]);
|
|
172
|
+
|
|
173
|
+
const canSave =
|
|
174
|
+
provider.trim() !== "" &&
|
|
175
|
+
clientId.trim() !== "" &&
|
|
176
|
+
authorizationUrl.trim() !== "" &&
|
|
177
|
+
tokenUrl.trim() !== "" &&
|
|
178
|
+
!isUpdating;
|
|
179
|
+
|
|
180
|
+
const createdAt = oauthApp.status?.audit?.specAudit?.createdAt;
|
|
181
|
+
const updatedAt = oauthApp.status?.audit?.specAudit?.updatedAt;
|
|
182
|
+
|
|
183
|
+
return (
|
|
184
|
+
<div className={cn("space-y-4", className)}>
|
|
185
|
+
{/* Header */}
|
|
186
|
+
<div className="flex items-start justify-between gap-3">
|
|
187
|
+
<div className="min-w-0">
|
|
188
|
+
{onBack && (
|
|
189
|
+
<button
|
|
190
|
+
type="button"
|
|
191
|
+
onClick={onBack}
|
|
192
|
+
className="text-muted-foreground hover:text-foreground mb-1 flex items-center gap-1 text-xs transition-colors"
|
|
193
|
+
>
|
|
194
|
+
<ArrowLeftIcon />
|
|
195
|
+
Back to list
|
|
196
|
+
</button>
|
|
197
|
+
)}
|
|
198
|
+
<h3 className="text-foreground truncate text-sm font-semibold">
|
|
199
|
+
{spec?.provider || meta?.name || "OAuth App"}
|
|
200
|
+
</h3>
|
|
201
|
+
{meta?.slug && (
|
|
202
|
+
<span className="text-muted-foreground font-mono text-xs">
|
|
203
|
+
{meta.slug}
|
|
204
|
+
</span>
|
|
205
|
+
)}
|
|
206
|
+
</div>
|
|
207
|
+
|
|
208
|
+
{mode === "view" && (
|
|
209
|
+
<div className="flex items-center gap-1">
|
|
210
|
+
<button
|
|
211
|
+
type="button"
|
|
212
|
+
onClick={enterEdit}
|
|
213
|
+
className={cn(
|
|
214
|
+
"shrink-0 rounded-md px-2.5 py-1.5 text-xs font-medium",
|
|
215
|
+
"text-muted-foreground hover:text-foreground hover:bg-accent/50",
|
|
216
|
+
"transition-colors",
|
|
217
|
+
)}
|
|
218
|
+
>
|
|
219
|
+
Edit
|
|
220
|
+
</button>
|
|
221
|
+
<button
|
|
222
|
+
type="button"
|
|
223
|
+
onClick={() => {
|
|
224
|
+
clearDeleteError();
|
|
225
|
+
setConfirmingDelete(true);
|
|
226
|
+
}}
|
|
227
|
+
className={cn(
|
|
228
|
+
"shrink-0 rounded-md px-2.5 py-1.5 text-xs font-medium",
|
|
229
|
+
"text-destructive/70 hover:text-destructive hover:bg-destructive/10",
|
|
230
|
+
"transition-colors",
|
|
231
|
+
)}
|
|
232
|
+
>
|
|
233
|
+
Delete
|
|
234
|
+
</button>
|
|
235
|
+
</div>
|
|
236
|
+
)}
|
|
237
|
+
</div>
|
|
238
|
+
|
|
239
|
+
{/* Inline delete confirmation */}
|
|
240
|
+
{confirmingDelete && (
|
|
241
|
+
<div
|
|
242
|
+
className="rounded-md border border-destructive/30 bg-destructive/5 p-3"
|
|
243
|
+
role="alert"
|
|
244
|
+
>
|
|
245
|
+
<p className="text-foreground mb-2 text-xs font-medium">
|
|
246
|
+
Delete this OAuth app?
|
|
247
|
+
</p>
|
|
248
|
+
<p className="text-muted-foreground mb-3 text-[0.65rem]">
|
|
249
|
+
This action is permanent. Any MCP server overrides referencing
|
|
250
|
+
this app will lose their binding.
|
|
251
|
+
</p>
|
|
252
|
+
{deleteError && (
|
|
253
|
+
<p className="text-destructive mb-2 text-[0.65rem]" role="alert">
|
|
254
|
+
{getUserMessage(deleteError)}
|
|
255
|
+
</p>
|
|
256
|
+
)}
|
|
257
|
+
<div className="flex items-center gap-2">
|
|
258
|
+
<button
|
|
259
|
+
type="button"
|
|
260
|
+
onClick={handleDelete}
|
|
261
|
+
disabled={isDeleting}
|
|
262
|
+
className={cn(
|
|
263
|
+
"inline-flex items-center gap-1.5 rounded-md px-3 py-1.5 text-xs font-medium",
|
|
264
|
+
"bg-destructive text-destructive-foreground hover:bg-destructive/90",
|
|
265
|
+
"disabled:pointer-events-none disabled:opacity-40",
|
|
266
|
+
)}
|
|
267
|
+
>
|
|
268
|
+
{isDeleting && <SpinnerIcon />}
|
|
269
|
+
Delete permanently
|
|
270
|
+
</button>
|
|
271
|
+
<button
|
|
272
|
+
type="button"
|
|
273
|
+
onClick={() => {
|
|
274
|
+
setConfirmingDelete(false);
|
|
275
|
+
clearDeleteError();
|
|
276
|
+
}}
|
|
277
|
+
disabled={isDeleting}
|
|
278
|
+
className={cn(
|
|
279
|
+
"rounded-md px-2.5 py-1.5 text-xs",
|
|
280
|
+
"text-muted-foreground hover:text-foreground hover:bg-accent/50",
|
|
281
|
+
"disabled:pointer-events-none disabled:opacity-50",
|
|
282
|
+
)}
|
|
283
|
+
>
|
|
284
|
+
Cancel
|
|
285
|
+
</button>
|
|
286
|
+
</div>
|
|
287
|
+
</div>
|
|
288
|
+
)}
|
|
289
|
+
|
|
290
|
+
{/* Body */}
|
|
291
|
+
{mode === "view" ? (
|
|
292
|
+
<ViewMode
|
|
293
|
+
spec={spec}
|
|
294
|
+
createdAt={createdAt}
|
|
295
|
+
updatedAt={updatedAt}
|
|
296
|
+
/>
|
|
297
|
+
) : (
|
|
298
|
+
<form onSubmit={handleSave} className="space-y-3">
|
|
299
|
+
<FieldInput
|
|
300
|
+
id="stgm-oauth-edit-provider"
|
|
301
|
+
label="Provider"
|
|
302
|
+
value={provider}
|
|
303
|
+
onChange={setProvider}
|
|
304
|
+
placeholder="e.g. Slack"
|
|
305
|
+
disabled={isUpdating}
|
|
306
|
+
required
|
|
307
|
+
/>
|
|
308
|
+
<FieldInput
|
|
309
|
+
id="stgm-oauth-edit-client-id"
|
|
310
|
+
label="Client ID"
|
|
311
|
+
value={clientId}
|
|
312
|
+
onChange={setClientId}
|
|
313
|
+
placeholder="OAuth client identifier"
|
|
314
|
+
disabled={isUpdating}
|
|
315
|
+
required
|
|
316
|
+
/>
|
|
317
|
+
<FieldInput
|
|
318
|
+
id="stgm-oauth-edit-client-secret"
|
|
319
|
+
label="Client secret"
|
|
320
|
+
value={clientSecret}
|
|
321
|
+
onChange={setClientSecret}
|
|
322
|
+
placeholder="Leave empty to keep existing secret"
|
|
323
|
+
type="password"
|
|
324
|
+
hint="Only enter a value to replace the existing secret"
|
|
325
|
+
disabled={isUpdating}
|
|
326
|
+
/>
|
|
327
|
+
<FieldInput
|
|
328
|
+
id="stgm-oauth-edit-auth-url"
|
|
329
|
+
label="Authorization URL"
|
|
330
|
+
value={authorizationUrl}
|
|
331
|
+
onChange={setAuthorizationUrl}
|
|
332
|
+
placeholder="https://vendor.com/oauth/authorize"
|
|
333
|
+
disabled={isUpdating}
|
|
334
|
+
required
|
|
335
|
+
/>
|
|
336
|
+
<FieldInput
|
|
337
|
+
id="stgm-oauth-edit-token-url"
|
|
338
|
+
label="Token URL"
|
|
339
|
+
value={tokenUrl}
|
|
340
|
+
onChange={setTokenUrl}
|
|
341
|
+
placeholder="https://vendor.com/oauth/token"
|
|
342
|
+
disabled={isUpdating}
|
|
343
|
+
required
|
|
344
|
+
/>
|
|
345
|
+
<FieldInput
|
|
346
|
+
id="stgm-oauth-edit-scopes"
|
|
347
|
+
label="Scopes"
|
|
348
|
+
value={scopes}
|
|
349
|
+
onChange={setScopes}
|
|
350
|
+
placeholder="read, write, admin"
|
|
351
|
+
hint="Comma-separated OAuth scopes"
|
|
352
|
+
disabled={isUpdating}
|
|
353
|
+
/>
|
|
354
|
+
<FieldInput
|
|
355
|
+
id="stgm-oauth-edit-userinfo-url"
|
|
356
|
+
label="Userinfo URL"
|
|
357
|
+
value={userinfoUrl}
|
|
358
|
+
onChange={setUserinfoUrl}
|
|
359
|
+
placeholder="https://vendor.com/userinfo"
|
|
360
|
+
hint="OIDC endpoint for fetching user profile data (optional)"
|
|
361
|
+
disabled={isUpdating}
|
|
362
|
+
/>
|
|
363
|
+
<FieldInput
|
|
364
|
+
id="stgm-oauth-edit-scope-param"
|
|
365
|
+
label="Scope parameter name"
|
|
366
|
+
value={scopeParameterName}
|
|
367
|
+
onChange={setScopeParameterName}
|
|
368
|
+
placeholder="scope"
|
|
369
|
+
hint='Defaults to "scope". Some vendors use a non-standard name.'
|
|
370
|
+
disabled={isUpdating}
|
|
371
|
+
/>
|
|
372
|
+
|
|
373
|
+
<div className="space-y-1">
|
|
374
|
+
<label
|
|
375
|
+
htmlFor="stgm-oauth-edit-approval-status"
|
|
376
|
+
className="text-xs font-medium text-foreground"
|
|
377
|
+
>
|
|
378
|
+
Vendor approval status
|
|
379
|
+
</label>
|
|
380
|
+
<select
|
|
381
|
+
id="stgm-oauth-edit-approval-status"
|
|
382
|
+
value={vendorApprovalStatus}
|
|
383
|
+
onChange={(e) =>
|
|
384
|
+
setVendorApprovalStatus(
|
|
385
|
+
e.target.value as typeof vendorApprovalStatus,
|
|
386
|
+
)
|
|
387
|
+
}
|
|
388
|
+
disabled={isUpdating}
|
|
389
|
+
className={cn(
|
|
390
|
+
"w-full rounded-md border border-input bg-background px-2.5 py-1.5 text-xs text-foreground",
|
|
391
|
+
"focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring",
|
|
392
|
+
"disabled:pointer-events-none disabled:opacity-50",
|
|
393
|
+
)}
|
|
394
|
+
>
|
|
395
|
+
<option value="unspecified">Unspecified (treated as approved)</option>
|
|
396
|
+
<option value="pending">Pending</option>
|
|
397
|
+
<option value="approved">Approved</option>
|
|
398
|
+
<option value="rejected">Rejected</option>
|
|
399
|
+
</select>
|
|
400
|
+
</div>
|
|
401
|
+
|
|
402
|
+
<FieldInput
|
|
403
|
+
id="stgm-oauth-edit-approval-docs"
|
|
404
|
+
label="Vendor approval docs URL"
|
|
405
|
+
value={vendorApprovalDocsUrl}
|
|
406
|
+
onChange={setVendorApprovalDocsUrl}
|
|
407
|
+
placeholder="https://docs.example.com/byoa"
|
|
408
|
+
hint="Help link shown when vendor approval is pending"
|
|
409
|
+
disabled={isUpdating}
|
|
410
|
+
/>
|
|
411
|
+
|
|
412
|
+
{updateError && (
|
|
413
|
+
<p className="text-destructive text-[0.65rem]" role="alert">
|
|
414
|
+
{getUserMessage(updateError)}
|
|
415
|
+
</p>
|
|
416
|
+
)}
|
|
417
|
+
|
|
418
|
+
<div className="flex items-center gap-2 pt-1">
|
|
419
|
+
<button
|
|
420
|
+
type="submit"
|
|
421
|
+
disabled={!canSave}
|
|
422
|
+
className={cn(
|
|
423
|
+
"inline-flex items-center gap-1.5 rounded-md px-3 py-1.5 text-xs font-medium",
|
|
424
|
+
"bg-primary text-primary-foreground hover:bg-primary/90",
|
|
425
|
+
"disabled:pointer-events-none disabled:opacity-40",
|
|
426
|
+
)}
|
|
427
|
+
>
|
|
428
|
+
{isUpdating && <SpinnerIcon />}
|
|
429
|
+
Save changes
|
|
430
|
+
</button>
|
|
431
|
+
<button
|
|
432
|
+
type="button"
|
|
433
|
+
onClick={cancelEdit}
|
|
434
|
+
disabled={isUpdating}
|
|
435
|
+
className={cn(
|
|
436
|
+
"rounded-md px-2.5 py-1.5 text-xs",
|
|
437
|
+
"text-muted-foreground hover:text-foreground hover:bg-accent/50",
|
|
438
|
+
"disabled:pointer-events-none disabled:opacity-50",
|
|
439
|
+
)}
|
|
440
|
+
>
|
|
441
|
+
Cancel
|
|
442
|
+
</button>
|
|
443
|
+
</div>
|
|
444
|
+
</form>
|
|
445
|
+
)}
|
|
446
|
+
</div>
|
|
447
|
+
);
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
// ---------------------------------------------------------------------------
|
|
451
|
+
// Constants
|
|
452
|
+
// ---------------------------------------------------------------------------
|
|
453
|
+
|
|
454
|
+
const APPROVAL_STATUS_MAP = {
|
|
455
|
+
pending: VendorApprovalStatus.PENDING,
|
|
456
|
+
approved: VendorApprovalStatus.APPROVED,
|
|
457
|
+
rejected: VendorApprovalStatus.REJECTED,
|
|
458
|
+
} as const;
|
|
459
|
+
|
|
460
|
+
const APPROVAL_STATUS_LABELS: Record<number, string> = {
|
|
461
|
+
[VendorApprovalStatus.UNSPECIFIED]: "Unspecified",
|
|
462
|
+
[VendorApprovalStatus.PENDING]: "Pending",
|
|
463
|
+
[VendorApprovalStatus.APPROVED]: "Approved",
|
|
464
|
+
[VendorApprovalStatus.REJECTED]: "Rejected",
|
|
465
|
+
};
|
|
466
|
+
|
|
467
|
+
function approvalStatusToKey(
|
|
468
|
+
status?: VendorApprovalStatus,
|
|
469
|
+
): "unspecified" | "pending" | "approved" | "rejected" {
|
|
470
|
+
switch (status) {
|
|
471
|
+
case VendorApprovalStatus.PENDING:
|
|
472
|
+
return "pending";
|
|
473
|
+
case VendorApprovalStatus.APPROVED:
|
|
474
|
+
return "approved";
|
|
475
|
+
case VendorApprovalStatus.REJECTED:
|
|
476
|
+
return "rejected";
|
|
477
|
+
default:
|
|
478
|
+
return "unspecified";
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
// ---------------------------------------------------------------------------
|
|
483
|
+
// View mode
|
|
484
|
+
// ---------------------------------------------------------------------------
|
|
485
|
+
|
|
486
|
+
function ViewMode({
|
|
487
|
+
spec,
|
|
488
|
+
createdAt,
|
|
489
|
+
updatedAt,
|
|
490
|
+
}: {
|
|
491
|
+
spec: OAuthApp["spec"];
|
|
492
|
+
createdAt?: Timestamp;
|
|
493
|
+
updatedAt?: Timestamp;
|
|
494
|
+
}) {
|
|
495
|
+
return (
|
|
496
|
+
<dl className="space-y-2.5">
|
|
497
|
+
<Field label="Provider" value={spec?.provider} />
|
|
498
|
+
<Field label="Client ID" value={spec?.clientId} mono />
|
|
499
|
+
<Field label="Client secret" value="••••••••" />
|
|
500
|
+
<Field label="Authorization URL" value={spec?.authorizationUrl} mono />
|
|
501
|
+
<Field label="Token URL" value={spec?.tokenUrl} mono />
|
|
502
|
+
{spec?.scopes && spec.scopes.length > 0 && (
|
|
503
|
+
<Field label="Scopes" value={spec.scopes.join(", ")} mono />
|
|
504
|
+
)}
|
|
505
|
+
{spec?.userinfoUrl && (
|
|
506
|
+
<Field label="Userinfo URL" value={spec.userinfoUrl} mono />
|
|
507
|
+
)}
|
|
508
|
+
{spec?.scopeParameterName && (
|
|
509
|
+
<Field
|
|
510
|
+
label="Scope parameter name"
|
|
511
|
+
value={spec.scopeParameterName}
|
|
512
|
+
mono
|
|
513
|
+
/>
|
|
514
|
+
)}
|
|
515
|
+
{spec?.vendorApprovalStatus !== undefined &&
|
|
516
|
+
spec.vendorApprovalStatus !== VendorApprovalStatus.UNSPECIFIED && (
|
|
517
|
+
<Field
|
|
518
|
+
label="Vendor approval"
|
|
519
|
+
value={
|
|
520
|
+
APPROVAL_STATUS_LABELS[spec.vendorApprovalStatus] ??
|
|
521
|
+
"Unknown"
|
|
522
|
+
}
|
|
523
|
+
/>
|
|
524
|
+
)}
|
|
525
|
+
{spec?.vendorApprovalDocsUrl && (
|
|
526
|
+
<Field label="Approval docs" value={spec.vendorApprovalDocsUrl} mono />
|
|
527
|
+
)}
|
|
528
|
+
<div className="flex gap-6">
|
|
529
|
+
{createdAt && (
|
|
530
|
+
<Field
|
|
531
|
+
label="Created"
|
|
532
|
+
value={formatDate(timestampDate(createdAt))}
|
|
533
|
+
/>
|
|
534
|
+
)}
|
|
535
|
+
{updatedAt && (
|
|
536
|
+
<Field
|
|
537
|
+
label="Updated"
|
|
538
|
+
value={formatDate(timestampDate(updatedAt))}
|
|
539
|
+
/>
|
|
540
|
+
)}
|
|
541
|
+
</div>
|
|
542
|
+
</dl>
|
|
543
|
+
);
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
// ---------------------------------------------------------------------------
|
|
547
|
+
// Shared primitives
|
|
548
|
+
// ---------------------------------------------------------------------------
|
|
549
|
+
|
|
550
|
+
function Field({
|
|
551
|
+
label,
|
|
552
|
+
value,
|
|
553
|
+
mono,
|
|
554
|
+
}: {
|
|
555
|
+
label: string;
|
|
556
|
+
value?: string;
|
|
557
|
+
mono?: boolean;
|
|
558
|
+
}) {
|
|
559
|
+
if (!value) return null;
|
|
560
|
+
return (
|
|
561
|
+
<div>
|
|
562
|
+
<dt className="text-muted-foreground text-[0.65rem] font-medium">
|
|
563
|
+
{label}
|
|
564
|
+
</dt>
|
|
565
|
+
<dd
|
|
566
|
+
className={cn(
|
|
567
|
+
"text-foreground mt-0.5 break-all text-xs",
|
|
568
|
+
mono && "font-mono",
|
|
569
|
+
)}
|
|
570
|
+
>
|
|
571
|
+
{value}
|
|
572
|
+
</dd>
|
|
573
|
+
</div>
|
|
574
|
+
);
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
function FieldInput({
|
|
578
|
+
id,
|
|
579
|
+
label,
|
|
580
|
+
value,
|
|
581
|
+
onChange,
|
|
582
|
+
placeholder,
|
|
583
|
+
hint,
|
|
584
|
+
type = "text",
|
|
585
|
+
disabled,
|
|
586
|
+
required,
|
|
587
|
+
}: {
|
|
588
|
+
id: string;
|
|
589
|
+
label: string;
|
|
590
|
+
value: string;
|
|
591
|
+
onChange: (v: string) => void;
|
|
592
|
+
placeholder: string;
|
|
593
|
+
hint?: string;
|
|
594
|
+
type?: "text" | "password";
|
|
595
|
+
disabled?: boolean;
|
|
596
|
+
required?: boolean;
|
|
597
|
+
}) {
|
|
598
|
+
return (
|
|
599
|
+
<div className="space-y-1">
|
|
600
|
+
<label htmlFor={id} className="text-xs font-medium text-foreground">
|
|
601
|
+
{label}
|
|
602
|
+
</label>
|
|
603
|
+
<input
|
|
604
|
+
id={id}
|
|
605
|
+
type={type}
|
|
606
|
+
value={value}
|
|
607
|
+
onChange={(e) => onChange(e.target.value)}
|
|
608
|
+
placeholder={placeholder}
|
|
609
|
+
disabled={disabled}
|
|
610
|
+
required={required}
|
|
611
|
+
className={cn(
|
|
612
|
+
"w-full rounded-md border border-input bg-background px-2.5 py-1.5 text-xs text-foreground",
|
|
613
|
+
"placeholder:text-muted-foreground",
|
|
614
|
+
"focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring",
|
|
615
|
+
"disabled:pointer-events-none disabled:opacity-50",
|
|
616
|
+
)}
|
|
617
|
+
/>
|
|
618
|
+
{hint && (
|
|
619
|
+
<p className="text-[0.65rem] text-muted-foreground">{hint}</p>
|
|
620
|
+
)}
|
|
621
|
+
</div>
|
|
622
|
+
);
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
function formatDate(date: Date): string {
|
|
626
|
+
return date.toLocaleDateString(undefined, {
|
|
627
|
+
month: "short",
|
|
628
|
+
day: "numeric",
|
|
629
|
+
year: "numeric",
|
|
630
|
+
});
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
// ---------------------------------------------------------------------------
|
|
634
|
+
// Icons
|
|
635
|
+
// ---------------------------------------------------------------------------
|
|
636
|
+
|
|
637
|
+
function ArrowLeftIcon() {
|
|
638
|
+
return (
|
|
639
|
+
<svg
|
|
640
|
+
width="12"
|
|
641
|
+
height="12"
|
|
642
|
+
viewBox="0 0 16 16"
|
|
643
|
+
fill="none"
|
|
644
|
+
stroke="currentColor"
|
|
645
|
+
strokeWidth="1.5"
|
|
646
|
+
strokeLinecap="round"
|
|
647
|
+
strokeLinejoin="round"
|
|
648
|
+
aria-hidden="true"
|
|
649
|
+
>
|
|
650
|
+
<path d="M10 3L5 8l5 5" />
|
|
651
|
+
</svg>
|
|
652
|
+
);
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
function SpinnerIcon() {
|
|
656
|
+
return (
|
|
657
|
+
<svg
|
|
658
|
+
width="12"
|
|
659
|
+
height="12"
|
|
660
|
+
viewBox="0 0 16 16"
|
|
661
|
+
fill="none"
|
|
662
|
+
stroke="currentColor"
|
|
663
|
+
strokeWidth="2"
|
|
664
|
+
strokeLinecap="round"
|
|
665
|
+
className="animate-spin"
|
|
666
|
+
aria-hidden="true"
|
|
667
|
+
>
|
|
668
|
+
<path d="M8 2a6 6 0 1 0 6 6" />
|
|
669
|
+
</svg>
|
|
670
|
+
);
|
|
671
|
+
}
|