@stigmer/react 0.0.83 → 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 +5 -3
- package/index.d.ts.map +1 -1
- package/index.js +3 -1
- package/index.js.map +1 -1
- package/library/ResourceListView.d.ts +57 -7
- package/library/ResourceListView.d.ts.map +1 -1
- package/library/ResourceListView.js +147 -37
- package/library/ResourceListView.js.map +1 -1
- package/library/index.d.ts +1 -1
- package/library/index.d.ts.map +1 -1
- package/library/index.js.map +1 -1
- package/mcp-server/McpServerConfigPanel.d.ts +45 -0
- package/mcp-server/McpServerConfigPanel.d.ts.map +1 -1
- package/mcp-server/McpServerConfigPanel.js +90 -14
- package/mcp-server/McpServerConfigPanel.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.d.ts.map +1 -1
- package/mcp-server/McpServerDetailView.js +168 -23
- package/mcp-server/McpServerDetailView.js.map +1 -1
- package/mcp-server/McpServerPicker.d.ts.map +1 -1
- package/mcp-server/McpServerPicker.js +9 -3
- package/mcp-server/McpServerPicker.js.map +1 -1
- package/mcp-server/OAuthAppForm.d.ts +58 -0
- package/mcp-server/OAuthAppForm.d.ts.map +1 -0
- package/mcp-server/OAuthAppForm.js +67 -0
- package/mcp-server/OAuthAppForm.js.map +1 -0
- package/mcp-server/index.d.ts +8 -0
- package/mcp-server/index.d.ts.map +1 -1
- package/mcp-server/index.js +4 -0
- package/mcp-server/index.js.map +1 -1
- package/mcp-server/useDisconnectOAuth.d.ts +40 -0
- package/mcp-server/useDisconnectOAuth.d.ts.map +1 -0
- package/mcp-server/useDisconnectOAuth.js +46 -0
- package/mcp-server/useDisconnectOAuth.js.map +1 -0
- package/mcp-server/useMcpServerCredentials.d.ts +48 -0
- package/mcp-server/useMcpServerCredentials.d.ts.map +1 -1
- package/mcp-server/useMcpServerCredentials.js +18 -2
- package/mcp-server/useMcpServerCredentials.js.map +1 -1
- package/mcp-server/useOAuthGrantStatus.d.ts +9 -0
- package/mcp-server/useOAuthGrantStatus.d.ts.map +1 -1
- package/mcp-server/useOAuthGrantStatus.js +6 -1
- package/mcp-server/useOAuthGrantStatus.js.map +1 -1
- package/mcp-server/useOrgOAuthApp.d.ts +82 -0
- package/mcp-server/useOrgOAuthApp.d.ts.map +1 -0
- package/mcp-server/useOrgOAuthApp.js +160 -0
- package/mcp-server/useOrgOAuthApp.js.map +1 -0
- 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 +25 -0
- package/src/library/ResourceListView.tsx +303 -46
- package/src/library/index.ts +4 -1
- package/src/mcp-server/McpServerConfigPanel.tsx +370 -45
- package/src/mcp-server/McpServerConnectDialog.tsx +527 -0
- package/src/mcp-server/McpServerDetailView.tsx +448 -47
- package/src/mcp-server/McpServerPicker.tsx +10 -3
- package/src/mcp-server/OAuthAppForm.tsx +304 -0
- package/src/mcp-server/index.ts +12 -0
- package/src/mcp-server/useDisconnectOAuth.ts +76 -0
- package/src/mcp-server/useMcpServerCredentials.ts +70 -2
- package/src/mcp-server/useOAuthGrantStatus.ts +19 -1
- package/src/mcp-server/useOrgOAuthApp.ts +250 -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
|
@@ -2,8 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
4
4
|
import { cn } from "@stigmer/theme";
|
|
5
|
+
import { getUserMessage, isRetryableError } from "@stigmer/sdk";
|
|
5
6
|
import { timestampDate } from "@bufbuild/protobuf/wkt";
|
|
6
7
|
import type { McpServer } from "@stigmer/protos/ai/stigmer/agentic/mcpserver/v1/api_pb";
|
|
8
|
+
import { OAuthConnectionHealth } from "@stigmer/protos/ai/stigmer/agentic/mcpserver/v1/io_pb";
|
|
7
9
|
import type {
|
|
8
10
|
DiscoveredTool,
|
|
9
11
|
DiscoveredResourceTemplate,
|
|
@@ -17,6 +19,9 @@ import { useMcpServerConnect } from "./useMcpServerConnect";
|
|
|
17
19
|
import { useMcpServerCredentials } from "./useMcpServerCredentials";
|
|
18
20
|
import { useMcpServerOAuthConnect } from "./useMcpServerOAuthConnect";
|
|
19
21
|
import type { OAuthConnectPhase } from "./useMcpServerOAuthConnect";
|
|
22
|
+
import { useDisconnectOAuth } from "./useDisconnectOAuth";
|
|
23
|
+
import { useOrgOAuthApp } from "./useOrgOAuthApp";
|
|
24
|
+
import { OAuthAppForm } from "./OAuthAppForm";
|
|
20
25
|
import { ErrorMessage } from "../error/ErrorMessage";
|
|
21
26
|
import { EnvVarForm } from "../environment/EnvVarForm";
|
|
22
27
|
import type { EnvVarFormVariable } from "../environment/EnvVarForm";
|
|
@@ -124,9 +129,16 @@ export function McpServerDetailView({
|
|
|
124
129
|
const credentials = useMcpServerCredentials(activeOrg ?? org, mcpServer ?? null);
|
|
125
130
|
const connection = useMcpServerConnect();
|
|
126
131
|
const oauth = useMcpServerOAuthConnect();
|
|
132
|
+
const disconnectOAuth = useDisconnectOAuth();
|
|
133
|
+
const orgOAuthApp = useOrgOAuthApp(
|
|
134
|
+
mcpServer?.metadata?.id ?? null,
|
|
135
|
+
activeOrg ?? org,
|
|
136
|
+
);
|
|
127
137
|
|
|
128
138
|
const [showCredentialForm, setShowCredentialForm] = useState(defaultShowCredentialForm);
|
|
139
|
+
const [showByoaForm, setShowByoaForm] = useState(false);
|
|
129
140
|
const [capabilityTab, setCapabilityTab] = useState<CapabilityTab>(defaultCapabilityTab);
|
|
141
|
+
const byoaDialogRef = useRef<HTMLDialogElement>(null);
|
|
130
142
|
|
|
131
143
|
const onResourceLoadRef = useRef(onResourceLoad);
|
|
132
144
|
onResourceLoadRef.current = onResourceLoad;
|
|
@@ -206,6 +218,55 @@ export function McpServerDetailView({
|
|
|
206
218
|
[credentials, mcpServer, connection, refetch],
|
|
207
219
|
);
|
|
208
220
|
|
|
221
|
+
const handleDisconnect = useCallback(async () => {
|
|
222
|
+
if (!mcpServer?.metadata?.id) return;
|
|
223
|
+
try {
|
|
224
|
+
await disconnectOAuth.disconnect(mcpServer.metadata.id, activeOrg ?? org);
|
|
225
|
+
credentials.refetch();
|
|
226
|
+
refetch();
|
|
227
|
+
} catch {
|
|
228
|
+
// error state is managed by the disconnect hook
|
|
229
|
+
}
|
|
230
|
+
}, [mcpServer, disconnectOAuth, credentials, refetch, activeOrg, org]);
|
|
231
|
+
|
|
232
|
+
// BYOA dialog lifecycle — native <dialog> toggle
|
|
233
|
+
useEffect(() => {
|
|
234
|
+
const dialog = byoaDialogRef.current;
|
|
235
|
+
if (!dialog) return;
|
|
236
|
+
if (showByoaForm && !dialog.open) {
|
|
237
|
+
dialog.showModal();
|
|
238
|
+
} else if (!showByoaForm && dialog.open) {
|
|
239
|
+
dialog.close();
|
|
240
|
+
}
|
|
241
|
+
}, [showByoaForm]);
|
|
242
|
+
|
|
243
|
+
const handleByoaDialogCancel = useCallback(
|
|
244
|
+
(e: React.SyntheticEvent) => {
|
|
245
|
+
e.preventDefault();
|
|
246
|
+
setShowByoaForm(false);
|
|
247
|
+
orgOAuthApp.clearErrors();
|
|
248
|
+
},
|
|
249
|
+
[orgOAuthApp],
|
|
250
|
+
);
|
|
251
|
+
|
|
252
|
+
const handleByoaSubmit = useCallback(
|
|
253
|
+
async (clientId: string, clientSecret: string) => {
|
|
254
|
+
await orgOAuthApp.setOrgOAuthApp(clientId, clientSecret);
|
|
255
|
+
setShowByoaForm(false);
|
|
256
|
+
orgOAuthApp.refetch();
|
|
257
|
+
credentials.refetch();
|
|
258
|
+
refetch();
|
|
259
|
+
},
|
|
260
|
+
[orgOAuthApp, credentials, refetch],
|
|
261
|
+
);
|
|
262
|
+
|
|
263
|
+
const handleRemoveOrgApp = useCallback(async () => {
|
|
264
|
+
await orgOAuthApp.deleteOrgOAuthApp();
|
|
265
|
+
orgOAuthApp.refetch();
|
|
266
|
+
credentials.refetch();
|
|
267
|
+
refetch();
|
|
268
|
+
}, [orgOAuthApp, credentials, refetch]);
|
|
269
|
+
|
|
209
270
|
const spec = mcpServer?.spec;
|
|
210
271
|
const status = mcpServer?.status;
|
|
211
272
|
const hasSource = spec && (spec.repositoryUrl || spec.githubStars > 0);
|
|
@@ -294,10 +355,25 @@ export function McpServerDetailView({
|
|
|
294
355
|
oauthPhase={oauth.phase}
|
|
295
356
|
authMode={credentials.authMode}
|
|
296
357
|
isOAuthConnected={credentials.isOAuthConnected}
|
|
358
|
+
connectionHealth={credentials.connectionHealth}
|
|
359
|
+
canDisconnect={credentials.canDisconnect}
|
|
360
|
+
onDisconnect={handleDisconnect}
|
|
361
|
+
isDisconnecting={disconnectOAuth.isDisconnecting}
|
|
362
|
+
disconnectError={disconnectOAuth.error}
|
|
363
|
+
onClearDisconnectError={disconnectOAuth.clearError}
|
|
364
|
+
serverName={mcpServer?.metadata?.name ?? slug}
|
|
297
365
|
accessTokenExpiresAt={credentials.accessTokenExpiresAt}
|
|
298
366
|
tokenLifetimeHint={credentials.tokenLifetimeHint}
|
|
299
367
|
isVendorApprovalPending={credentials.isVendorApprovalPending}
|
|
368
|
+
isVendorApprovalBlocked={credentials.isVendorApprovalBlocked}
|
|
300
369
|
vendorApprovalDocsUrl={credentials.vendorApprovalDocsUrl}
|
|
370
|
+
canBringOwnApp={credentials.canBringOwnApp}
|
|
371
|
+
isOrgOAuthApp={credentials.isOrgOAuthApp}
|
|
372
|
+
onBringOwnApp={() => setShowByoaForm(true)}
|
|
373
|
+
onRemoveOrgApp={handleRemoveOrgApp}
|
|
374
|
+
isRemovingOrgApp={orgOAuthApp.isDeleting}
|
|
375
|
+
removeOrgAppError={orgOAuthApp.deleteError}
|
|
376
|
+
onClearRemoveOrgAppError={orgOAuthApp.clearErrors}
|
|
301
377
|
manualOverride={credentials.manualOverride}
|
|
302
378
|
onManualOverride={() => {
|
|
303
379
|
credentials.setManualOverride(true);
|
|
@@ -328,6 +404,31 @@ export function McpServerDetailView({
|
|
|
328
404
|
)}
|
|
329
405
|
</Section>
|
|
330
406
|
|
|
407
|
+
{/* BYOA dialog — native <dialog> for zero-dependency modal behavior */}
|
|
408
|
+
<dialog
|
|
409
|
+
ref={byoaDialogRef}
|
|
410
|
+
onCancel={handleByoaDialogCancel}
|
|
411
|
+
className={cn(
|
|
412
|
+
"m-auto w-full max-w-md rounded-lg border border-border bg-background p-6 shadow-lg",
|
|
413
|
+
"backdrop:bg-black/50",
|
|
414
|
+
)}
|
|
415
|
+
>
|
|
416
|
+
<h3 className="mb-4 text-base font-semibold text-foreground">
|
|
417
|
+
Use your own OAuth app
|
|
418
|
+
</h3>
|
|
419
|
+
<OAuthAppForm
|
|
420
|
+
providerName={mcpServer?.metadata?.name ?? slug}
|
|
421
|
+
vendorDocsUrl={credentials.vendorApprovalDocsUrl}
|
|
422
|
+
onSubmit={handleByoaSubmit}
|
|
423
|
+
onCancel={() => {
|
|
424
|
+
setShowByoaForm(false);
|
|
425
|
+
orgOAuthApp.clearErrors();
|
|
426
|
+
}}
|
|
427
|
+
isSubmitting={orgOAuthApp.isSetting}
|
|
428
|
+
error={orgOAuthApp.setError}
|
|
429
|
+
/>
|
|
430
|
+
</dialog>
|
|
431
|
+
|
|
331
432
|
<Section title="Capabilities">
|
|
332
433
|
<Tabs
|
|
333
434
|
tabs={capabilityTabs}
|
|
@@ -362,6 +463,75 @@ export function McpServerDetailView({
|
|
|
362
463
|
// ConnectBar — single entry point for capability discovery
|
|
363
464
|
// ---------------------------------------------------------------------------
|
|
364
465
|
|
|
466
|
+
/** Maps an OAuthConnectionHealth enum to pill display properties. */
|
|
467
|
+
function healthPillProps(
|
|
468
|
+
health: OAuthConnectionHealth,
|
|
469
|
+
isVendorApprovalPending: boolean,
|
|
470
|
+
): { pillClass: string; dotClass: string; label: string } {
|
|
471
|
+
if (isVendorApprovalPending) {
|
|
472
|
+
return {
|
|
473
|
+
pillClass: "bg-amber-500/10 text-amber-600 dark:text-amber-400",
|
|
474
|
+
dotClass: "bg-amber-500",
|
|
475
|
+
label: "Pending approval",
|
|
476
|
+
};
|
|
477
|
+
}
|
|
478
|
+
switch (health) {
|
|
479
|
+
case OAuthConnectionHealth.OAUTH_CONNECTION_HEALTH_HEALTHY:
|
|
480
|
+
return {
|
|
481
|
+
pillClass: "bg-success/10 text-success",
|
|
482
|
+
dotClass: "bg-success",
|
|
483
|
+
label: "Connected",
|
|
484
|
+
};
|
|
485
|
+
case OAuthConnectionHealth.OAUTH_CONNECTION_HEALTH_TOKEN_EXPIRED_REFRESHABLE:
|
|
486
|
+
return {
|
|
487
|
+
pillClass: "bg-amber-500/10 text-amber-600 dark:text-amber-400",
|
|
488
|
+
dotClass: "bg-amber-500",
|
|
489
|
+
label: "Token expired",
|
|
490
|
+
};
|
|
491
|
+
case OAuthConnectionHealth.OAUTH_CONNECTION_HEALTH_TOKEN_EXPIRED:
|
|
492
|
+
return {
|
|
493
|
+
pillClass: "bg-destructive/10 text-destructive",
|
|
494
|
+
dotClass: "bg-destructive",
|
|
495
|
+
label: "Re-auth needed",
|
|
496
|
+
};
|
|
497
|
+
default:
|
|
498
|
+
return {
|
|
499
|
+
pillClass: "bg-muted text-muted-foreground",
|
|
500
|
+
dotClass: "bg-muted-foreground",
|
|
501
|
+
label: "Not connected",
|
|
502
|
+
};
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
/** Health-aware status detail text shown alongside the pill. */
|
|
507
|
+
function healthStatusText(
|
|
508
|
+
health: OAuthConnectionHealth,
|
|
509
|
+
accessTokenExpiresAt: bigint,
|
|
510
|
+
tokenLifetimeHint: string | null,
|
|
511
|
+
): string {
|
|
512
|
+
switch (health) {
|
|
513
|
+
case OAuthConnectionHealth.OAUTH_CONNECTION_HEALTH_HEALTHY: {
|
|
514
|
+
const expiryLabel = formatTokenExpiry(accessTokenExpiresAt);
|
|
515
|
+
if (expiryLabel) return `Tokens refresh automatically \u00B7 ${expiryLabel}`;
|
|
516
|
+
const hint =
|
|
517
|
+
tokenLifetimeHint && tokenLifetimeHint !== "never"
|
|
518
|
+
? ` \u00B7 Session lasts ~${tokenLifetimeHint}`
|
|
519
|
+
: "";
|
|
520
|
+
return `Tokens refresh automatically${hint}`;
|
|
521
|
+
}
|
|
522
|
+
case OAuthConnectionHealth.OAUTH_CONNECTION_HEALTH_TOKEN_EXPIRED_REFRESHABLE:
|
|
523
|
+
return "Will refresh automatically on next use";
|
|
524
|
+
case OAuthConnectionHealth.OAUTH_CONNECTION_HEALTH_TOKEN_EXPIRED:
|
|
525
|
+
return "Token expired \u2014 sign in again to reconnect";
|
|
526
|
+
default:
|
|
527
|
+
return "Not connected yet";
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
type DisconnectPhase = "idle" | "confirming" | "disconnecting";
|
|
532
|
+
|
|
533
|
+
type RemoveOrgAppPhase = "idle" | "confirming" | "removing";
|
|
534
|
+
|
|
365
535
|
function ConnectBar({
|
|
366
536
|
isConnecting,
|
|
367
537
|
connectionError,
|
|
@@ -374,10 +544,25 @@ function ConnectBar({
|
|
|
374
544
|
oauthPhase,
|
|
375
545
|
authMode,
|
|
376
546
|
isOAuthConnected,
|
|
547
|
+
connectionHealth,
|
|
548
|
+
canDisconnect,
|
|
549
|
+
onDisconnect,
|
|
550
|
+
isDisconnecting,
|
|
551
|
+
disconnectError,
|
|
552
|
+
onClearDisconnectError,
|
|
553
|
+
serverName,
|
|
377
554
|
accessTokenExpiresAt,
|
|
378
555
|
tokenLifetimeHint,
|
|
379
556
|
isVendorApprovalPending,
|
|
557
|
+
isVendorApprovalBlocked,
|
|
380
558
|
vendorApprovalDocsUrl,
|
|
559
|
+
canBringOwnApp,
|
|
560
|
+
isOrgOAuthApp,
|
|
561
|
+
onBringOwnApp,
|
|
562
|
+
onRemoveOrgApp,
|
|
563
|
+
isRemovingOrgApp,
|
|
564
|
+
removeOrgAppError,
|
|
565
|
+
onClearRemoveOrgAppError,
|
|
381
566
|
manualOverride,
|
|
382
567
|
onManualOverride,
|
|
383
568
|
onBackToOAuth,
|
|
@@ -393,14 +578,32 @@ function ConnectBar({
|
|
|
393
578
|
readonly oauthPhase: OAuthConnectPhase;
|
|
394
579
|
readonly authMode: "manual" | "oauth";
|
|
395
580
|
readonly isOAuthConnected: boolean;
|
|
581
|
+
readonly connectionHealth: OAuthConnectionHealth;
|
|
582
|
+
readonly canDisconnect: boolean;
|
|
583
|
+
readonly onDisconnect: () => Promise<void>;
|
|
584
|
+
readonly isDisconnecting: boolean;
|
|
585
|
+
readonly disconnectError: Error | null;
|
|
586
|
+
readonly onClearDisconnectError: () => void;
|
|
587
|
+
readonly serverName: string;
|
|
396
588
|
readonly accessTokenExpiresAt: bigint;
|
|
397
589
|
readonly tokenLifetimeHint: string | null;
|
|
398
590
|
readonly isVendorApprovalPending: boolean;
|
|
591
|
+
readonly isVendorApprovalBlocked: boolean;
|
|
399
592
|
readonly vendorApprovalDocsUrl: string | null;
|
|
593
|
+
readonly canBringOwnApp: boolean;
|
|
594
|
+
readonly isOrgOAuthApp: boolean;
|
|
595
|
+
readonly onBringOwnApp: () => void;
|
|
596
|
+
readonly onRemoveOrgApp: () => Promise<void>;
|
|
597
|
+
readonly isRemovingOrgApp: boolean;
|
|
598
|
+
readonly removeOrgAppError: Error | null;
|
|
599
|
+
readonly onClearRemoveOrgAppError: () => void;
|
|
400
600
|
readonly manualOverride: boolean;
|
|
401
601
|
readonly onManualOverride: () => void;
|
|
402
602
|
readonly onBackToOAuth: () => void;
|
|
403
603
|
}) {
|
|
604
|
+
const [disconnectPhase, setDisconnectPhase] = useState<DisconnectPhase>("idle");
|
|
605
|
+
const [removeOrgAppPhase, setRemoveOrgAppPhase] = useState<RemoveOrgAppPhase>("idle");
|
|
606
|
+
|
|
404
607
|
const isOAuthBusy =
|
|
405
608
|
oauthPhase === "initiating" ||
|
|
406
609
|
oauthPhase === "awaiting-callback" ||
|
|
@@ -410,37 +613,188 @@ function ConnectBar({
|
|
|
410
613
|
const showOAuthPrimary =
|
|
411
614
|
authMode === "oauth" && !isOAuthConnected && !manualOverride;
|
|
412
615
|
|
|
413
|
-
const
|
|
616
|
+
const needsReAuth =
|
|
617
|
+
connectionHealth === OAuthConnectionHealth.OAUTH_CONNECTION_HEALTH_TOKEN_EXPIRED;
|
|
618
|
+
|
|
619
|
+
const oauthSignInDisabled =
|
|
620
|
+
isVendorApprovalBlocked && showOAuthPrimary && !isOrgOAuthApp;
|
|
621
|
+
|
|
622
|
+
const anyBusy = isConnecting || isOAuthBusy || isDisconnecting || isRemovingOrgApp;
|
|
414
623
|
|
|
415
624
|
const buttonLabel = (() => {
|
|
416
625
|
if (isOAuthBusy) return oauthPhaseLabel(oauthPhase);
|
|
417
626
|
if (isConnecting) return "Connecting...";
|
|
418
|
-
if (showOAuthPrimary) return "Sign in
|
|
627
|
+
if (isOrgOAuthApp && showOAuthPrimary) return "Sign in with your app";
|
|
628
|
+
if (showOAuthPrimary || needsReAuth) return "Sign in to connect";
|
|
419
629
|
if (hasDiscoveredTools) return "Reconnect";
|
|
420
630
|
return "Connect";
|
|
421
631
|
})();
|
|
422
632
|
|
|
423
633
|
const buttonIcon = (() => {
|
|
424
634
|
if (isOAuthBusy || isConnecting) return <Spinner />;
|
|
425
|
-
if (showOAuthPrimary) return <OAuthIcon className="size-3.5" />;
|
|
635
|
+
if (showOAuthPrimary || needsReAuth) return <OAuthIcon className="size-3.5" />;
|
|
426
636
|
if (hasDiscoveredTools) return <RefreshIcon className="size-3.5" />;
|
|
427
637
|
return <ConnectIcon className="size-3.5" />;
|
|
428
638
|
})();
|
|
429
639
|
|
|
430
640
|
const statusText = (() => {
|
|
431
641
|
if (authMode === "oauth" && isOAuthConnected) {
|
|
432
|
-
const
|
|
433
|
-
|
|
434
|
-
const hint = tokenLifetimeHint && tokenLifetimeHint !== "never"
|
|
435
|
-
? ` \u00B7 Session lasts ~${tokenLifetimeHint}`
|
|
436
|
-
: "";
|
|
437
|
-
return `Tokens refresh automatically${hint}`;
|
|
642
|
+
const base = healthStatusText(connectionHealth, accessTokenExpiresAt, tokenLifetimeHint);
|
|
643
|
+
return isOrgOAuthApp ? `${base} \u00B7 Using your OAuth app` : base;
|
|
438
644
|
}
|
|
645
|
+
if (isOrgOAuthApp && showOAuthPrimary) return "Using your OAuth app";
|
|
439
646
|
if (manualOverride) return "Entering token manually";
|
|
440
647
|
if (hasDiscoveredTools) return formatConnectionSummary(toolCount, policyCount);
|
|
441
648
|
return "Not connected yet";
|
|
442
649
|
})();
|
|
443
650
|
|
|
651
|
+
const pill = healthPillProps(connectionHealth, isVendorApprovalPending && !isOAuthConnected);
|
|
652
|
+
|
|
653
|
+
const showDisconnectLink =
|
|
654
|
+
canDisconnect && !anyBusy && !manualOverride && disconnectPhase === "idle";
|
|
655
|
+
|
|
656
|
+
const showRemoveOrgAppLink =
|
|
657
|
+
isOrgOAuthApp && !anyBusy && removeOrgAppPhase === "idle";
|
|
658
|
+
|
|
659
|
+
// Inline disconnect confirmation replaces the main bar content
|
|
660
|
+
if (disconnectPhase === "confirming" || disconnectPhase === "disconnecting") {
|
|
661
|
+
return (
|
|
662
|
+
<div className="flex flex-col">
|
|
663
|
+
<div className="flex items-center gap-2 px-3 py-2">
|
|
664
|
+
<WarningIcon className="size-3.5 shrink-0 text-destructive" />
|
|
665
|
+
<p className="flex-1 text-xs text-foreground">
|
|
666
|
+
Remove OAuth credentials for <span className="font-medium">{serverName}</span>?
|
|
667
|
+
You can reconnect at any time.
|
|
668
|
+
</p>
|
|
669
|
+
<div className="flex shrink-0 items-center gap-1.5">
|
|
670
|
+
<button
|
|
671
|
+
type="button"
|
|
672
|
+
disabled={isDisconnecting}
|
|
673
|
+
onClick={async () => {
|
|
674
|
+
setDisconnectPhase("disconnecting");
|
|
675
|
+
try {
|
|
676
|
+
await onDisconnect();
|
|
677
|
+
setDisconnectPhase("idle");
|
|
678
|
+
} catch {
|
|
679
|
+
setDisconnectPhase("confirming");
|
|
680
|
+
}
|
|
681
|
+
}}
|
|
682
|
+
className={cn(
|
|
683
|
+
"inline-flex items-center gap-1 rounded-md px-2.5 py-1 text-xs font-medium",
|
|
684
|
+
"bg-destructive text-destructive-foreground hover:bg-destructive/90",
|
|
685
|
+
"disabled:pointer-events-none disabled:opacity-50",
|
|
686
|
+
)}
|
|
687
|
+
>
|
|
688
|
+
{isDisconnecting && <Spinner />}
|
|
689
|
+
Disconnect
|
|
690
|
+
</button>
|
|
691
|
+
<button
|
|
692
|
+
type="button"
|
|
693
|
+
disabled={isDisconnecting}
|
|
694
|
+
onClick={() => {
|
|
695
|
+
setDisconnectPhase("idle");
|
|
696
|
+
onClearDisconnectError();
|
|
697
|
+
}}
|
|
698
|
+
className={cn(
|
|
699
|
+
"inline-flex items-center rounded-md px-2.5 py-1 text-xs font-medium",
|
|
700
|
+
"border border-border bg-background text-foreground hover:bg-accent hover:text-accent-foreground",
|
|
701
|
+
"disabled:pointer-events-none disabled:opacity-50",
|
|
702
|
+
)}
|
|
703
|
+
>
|
|
704
|
+
Cancel
|
|
705
|
+
</button>
|
|
706
|
+
</div>
|
|
707
|
+
</div>
|
|
708
|
+
{disconnectError && (
|
|
709
|
+
<div className="flex items-start gap-2 border-t border-destructive/20 bg-destructive/5 px-3 py-2">
|
|
710
|
+
<WarningIcon className="mt-0.5 size-3.5 shrink-0 text-destructive" />
|
|
711
|
+
<p className="flex-1 text-xs text-destructive">
|
|
712
|
+
{getUserMessage(disconnectError)}
|
|
713
|
+
</p>
|
|
714
|
+
<button
|
|
715
|
+
type="button"
|
|
716
|
+
onClick={onClearDisconnectError}
|
|
717
|
+
className="shrink-0 text-xs text-destructive/70 hover:text-destructive"
|
|
718
|
+
aria-label="Dismiss error"
|
|
719
|
+
>
|
|
720
|
+
Dismiss
|
|
721
|
+
</button>
|
|
722
|
+
</div>
|
|
723
|
+
)}
|
|
724
|
+
</div>
|
|
725
|
+
);
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
// Inline "remove custom app" confirmation
|
|
729
|
+
if (removeOrgAppPhase === "confirming" || removeOrgAppPhase === "removing") {
|
|
730
|
+
return (
|
|
731
|
+
<div className="flex flex-col">
|
|
732
|
+
<div className="flex items-center gap-2 px-3 py-2">
|
|
733
|
+
<WarningIcon className="size-3.5 shrink-0 text-destructive" />
|
|
734
|
+
<p className="flex-1 text-xs text-foreground">
|
|
735
|
+
Remove your custom OAuth app for{" "}
|
|
736
|
+
<span className="font-medium">{serverName}</span>? The server
|
|
737
|
+
will revert to the platform's OAuth app.
|
|
738
|
+
</p>
|
|
739
|
+
<div className="flex shrink-0 items-center gap-1.5">
|
|
740
|
+
<button
|
|
741
|
+
type="button"
|
|
742
|
+
disabled={isRemovingOrgApp}
|
|
743
|
+
onClick={async () => {
|
|
744
|
+
setRemoveOrgAppPhase("removing");
|
|
745
|
+
try {
|
|
746
|
+
await onRemoveOrgApp();
|
|
747
|
+
setRemoveOrgAppPhase("idle");
|
|
748
|
+
} catch {
|
|
749
|
+
setRemoveOrgAppPhase("confirming");
|
|
750
|
+
}
|
|
751
|
+
}}
|
|
752
|
+
className={cn(
|
|
753
|
+
"inline-flex items-center gap-1 rounded-md px-2.5 py-1 text-xs font-medium",
|
|
754
|
+
"bg-destructive text-destructive-foreground hover:bg-destructive/90",
|
|
755
|
+
"disabled:pointer-events-none disabled:opacity-50",
|
|
756
|
+
)}
|
|
757
|
+
>
|
|
758
|
+
{isRemovingOrgApp && <Spinner />}
|
|
759
|
+
Remove
|
|
760
|
+
</button>
|
|
761
|
+
<button
|
|
762
|
+
type="button"
|
|
763
|
+
disabled={isRemovingOrgApp}
|
|
764
|
+
onClick={() => {
|
|
765
|
+
setRemoveOrgAppPhase("idle");
|
|
766
|
+
onClearRemoveOrgAppError();
|
|
767
|
+
}}
|
|
768
|
+
className={cn(
|
|
769
|
+
"inline-flex items-center rounded-md px-2.5 py-1 text-xs font-medium",
|
|
770
|
+
"border border-border bg-background text-foreground hover:bg-accent hover:text-accent-foreground",
|
|
771
|
+
"disabled:pointer-events-none disabled:opacity-50",
|
|
772
|
+
)}
|
|
773
|
+
>
|
|
774
|
+
Cancel
|
|
775
|
+
</button>
|
|
776
|
+
</div>
|
|
777
|
+
</div>
|
|
778
|
+
{removeOrgAppError && (
|
|
779
|
+
<div className="flex items-start gap-2 border-t border-destructive/20 bg-destructive/5 px-3 py-2">
|
|
780
|
+
<WarningIcon className="mt-0.5 size-3.5 shrink-0 text-destructive" />
|
|
781
|
+
<p className="flex-1 text-xs text-destructive">
|
|
782
|
+
{getUserMessage(removeOrgAppError)}
|
|
783
|
+
</p>
|
|
784
|
+
<button
|
|
785
|
+
type="button"
|
|
786
|
+
onClick={onClearRemoveOrgAppError}
|
|
787
|
+
className="shrink-0 text-xs text-destructive/70 hover:text-destructive"
|
|
788
|
+
aria-label="Dismiss error"
|
|
789
|
+
>
|
|
790
|
+
Dismiss
|
|
791
|
+
</button>
|
|
792
|
+
</div>
|
|
793
|
+
)}
|
|
794
|
+
</div>
|
|
795
|
+
);
|
|
796
|
+
}
|
|
797
|
+
|
|
444
798
|
return (
|
|
445
799
|
<div className="flex flex-col">
|
|
446
800
|
<div className="flex items-center justify-between px-3 py-2">
|
|
@@ -449,39 +803,46 @@ function ConnectBar({
|
|
|
449
803
|
<span
|
|
450
804
|
className={cn(
|
|
451
805
|
"inline-flex items-center gap-1.5 rounded-full px-2 py-0.5 text-[10px] font-medium",
|
|
452
|
-
|
|
453
|
-
? "bg-success/10 text-success"
|
|
454
|
-
: oauthSignInDisabled
|
|
455
|
-
? "bg-amber-500/10 text-amber-600 dark:text-amber-400"
|
|
456
|
-
: "bg-muted text-muted-foreground",
|
|
806
|
+
pill.pillClass,
|
|
457
807
|
)}
|
|
458
808
|
>
|
|
459
809
|
<span
|
|
460
|
-
className={cn(
|
|
461
|
-
"size-1.5 rounded-full",
|
|
462
|
-
isOAuthConnected
|
|
463
|
-
? "bg-success"
|
|
464
|
-
: oauthSignInDisabled
|
|
465
|
-
? "bg-amber-500"
|
|
466
|
-
: "bg-muted-foreground",
|
|
467
|
-
)}
|
|
810
|
+
className={cn("size-1.5 rounded-full", pill.dotClass)}
|
|
468
811
|
aria-hidden="true"
|
|
469
812
|
/>
|
|
470
|
-
{
|
|
813
|
+
{pill.label}
|
|
471
814
|
</span>
|
|
472
815
|
)}
|
|
473
816
|
<span className="text-xs text-muted-foreground">
|
|
474
817
|
{oauthSignInDisabled ? "OAuth sign-in is pending vendor approval" : statusText}
|
|
475
818
|
</span>
|
|
819
|
+
{showDisconnectLink && (
|
|
820
|
+
<button
|
|
821
|
+
type="button"
|
|
822
|
+
onClick={() => setDisconnectPhase("confirming")}
|
|
823
|
+
className="text-[11px] text-muted-foreground underline decoration-muted-foreground/40 underline-offset-2 hover:text-foreground hover:decoration-foreground"
|
|
824
|
+
>
|
|
825
|
+
Disconnect
|
|
826
|
+
</button>
|
|
827
|
+
)}
|
|
828
|
+
{showRemoveOrgAppLink && (
|
|
829
|
+
<button
|
|
830
|
+
type="button"
|
|
831
|
+
onClick={() => setRemoveOrgAppPhase("confirming")}
|
|
832
|
+
className="text-[11px] text-muted-foreground underline decoration-muted-foreground/40 underline-offset-2 hover:text-foreground hover:decoration-foreground"
|
|
833
|
+
>
|
|
834
|
+
Remove custom app
|
|
835
|
+
</button>
|
|
836
|
+
)}
|
|
476
837
|
</div>
|
|
477
838
|
<button
|
|
478
839
|
type="button"
|
|
479
840
|
onClick={onConnect}
|
|
480
|
-
disabled={
|
|
841
|
+
disabled={anyBusy || credentialsLoading || oauthSignInDisabled}
|
|
481
842
|
data-cursor-target="connect-button"
|
|
482
843
|
className={cn(
|
|
483
844
|
"inline-flex items-center gap-1.5 rounded-md px-2.5 py-1 text-xs font-medium",
|
|
484
|
-
showOAuthPrimary
|
|
845
|
+
showOAuthPrimary || needsReAuth
|
|
485
846
|
? "bg-primary text-primary-foreground hover:bg-primary-hover"
|
|
486
847
|
: "border border-border bg-background text-foreground hover:bg-accent hover:text-accent-foreground",
|
|
487
848
|
"disabled:pointer-events-none disabled:opacity-50",
|
|
@@ -492,16 +853,28 @@ function ConnectBar({
|
|
|
492
853
|
</button>
|
|
493
854
|
</div>
|
|
494
855
|
|
|
495
|
-
{/* Vendor approval
|
|
856
|
+
{/* Vendor approval blocked banner with BYOA CTA */}
|
|
496
857
|
{oauthSignInDisabled && (
|
|
497
858
|
<div className="flex items-start gap-2 border-t border-amber-500/20 bg-amber-500/5 px-3 py-2">
|
|
498
859
|
<WarningIcon className="mt-0.5 size-3.5 shrink-0 text-amber-600 dark:text-amber-400" />
|
|
499
860
|
<div className="flex-1 text-xs text-amber-700 dark:text-amber-300">
|
|
500
861
|
<p>
|
|
501
862
|
The platform's OAuth app is awaiting vendor approval.
|
|
502
|
-
|
|
863
|
+
{canBringOwnApp
|
|
864
|
+
? " You can use your own OAuth app or enter a token manually."
|
|
865
|
+
: " You can still connect by entering your own token manually."}
|
|
503
866
|
</p>
|
|
504
|
-
{
|
|
867
|
+
{canBringOwnApp && (
|
|
868
|
+
<button
|
|
869
|
+
type="button"
|
|
870
|
+
onClick={onBringOwnApp}
|
|
871
|
+
data-cursor-target="byoa-cta-button"
|
|
872
|
+
className="mt-1.5 inline-flex items-center gap-1 rounded-md bg-amber-600 px-2.5 py-1 text-[11px] font-medium text-white hover:bg-amber-700 dark:bg-amber-500 dark:text-amber-950 dark:hover:bg-amber-400"
|
|
873
|
+
>
|
|
874
|
+
Use your own OAuth app
|
|
875
|
+
</button>
|
|
876
|
+
)}
|
|
877
|
+
{vendorApprovalDocsUrl && !canBringOwnApp && (
|
|
505
878
|
<a
|
|
506
879
|
href={vendorApprovalDocsUrl}
|
|
507
880
|
target="_blank"
|
|
@@ -516,9 +889,9 @@ function ConnectBar({
|
|
|
516
889
|
</div>
|
|
517
890
|
)}
|
|
518
891
|
|
|
519
|
-
{/* Secondary
|
|
892
|
+
{/* Secondary actions: manual entry, BYOA, back to OAuth */}
|
|
520
893
|
{authMode === "oauth" && !isOAuthConnected && !isOAuthBusy && !isConnecting && (
|
|
521
|
-
<div className="border-t border-border px-3 py-1.5">
|
|
894
|
+
<div className="flex items-center gap-3 border-t border-border px-3 py-1.5">
|
|
522
895
|
{manualOverride ? (
|
|
523
896
|
<button
|
|
524
897
|
type="button"
|
|
@@ -528,31 +901,59 @@ function ConnectBar({
|
|
|
528
901
|
{isVendorApprovalPending ? "Back to OAuth status" : "Sign in with OAuth instead"}
|
|
529
902
|
</button>
|
|
530
903
|
) : (
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
904
|
+
<>
|
|
905
|
+
<button
|
|
906
|
+
type="button"
|
|
907
|
+
onClick={onManualOverride}
|
|
908
|
+
className="text-[11px] text-muted-foreground underline decoration-muted-foreground/40 underline-offset-2 hover:text-foreground hover:decoration-foreground"
|
|
909
|
+
>
|
|
910
|
+
Enter token manually
|
|
911
|
+
</button>
|
|
912
|
+
{canBringOwnApp && !isVendorApprovalBlocked && (
|
|
913
|
+
<button
|
|
914
|
+
type="button"
|
|
915
|
+
onClick={onBringOwnApp}
|
|
916
|
+
className="text-[11px] text-muted-foreground underline decoration-muted-foreground/40 underline-offset-2 hover:text-foreground hover:decoration-foreground"
|
|
917
|
+
>
|
|
918
|
+
Use your own OAuth app
|
|
919
|
+
</button>
|
|
920
|
+
)}
|
|
921
|
+
</>
|
|
538
922
|
)}
|
|
539
923
|
</div>
|
|
540
924
|
)}
|
|
541
925
|
|
|
542
926
|
{connectionError && (
|
|
543
|
-
<div
|
|
927
|
+
<div
|
|
928
|
+
className="flex items-start gap-2 border-t border-destructive/20 bg-destructive/5 px-3 py-2"
|
|
929
|
+
role="alert"
|
|
930
|
+
>
|
|
544
931
|
<WarningIcon className="mt-0.5 size-3.5 shrink-0 text-destructive" />
|
|
545
932
|
<p className="flex-1 text-xs text-destructive">
|
|
546
|
-
{connectionError
|
|
933
|
+
{getUserMessage(connectionError)}
|
|
547
934
|
</p>
|
|
548
|
-
<
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
935
|
+
<div className="flex shrink-0 items-center gap-2">
|
|
936
|
+
{isRetryableError(connectionError) && (
|
|
937
|
+
<button
|
|
938
|
+
type="button"
|
|
939
|
+
onClick={() => {
|
|
940
|
+
onClearConnectionError();
|
|
941
|
+
onConnect();
|
|
942
|
+
}}
|
|
943
|
+
className="text-xs font-medium text-destructive underline underline-offset-2 hover:no-underline"
|
|
944
|
+
>
|
|
945
|
+
Try again
|
|
946
|
+
</button>
|
|
947
|
+
)}
|
|
948
|
+
<button
|
|
949
|
+
type="button"
|
|
950
|
+
onClick={onClearConnectionError}
|
|
951
|
+
className="text-xs text-destructive/70 hover:text-destructive"
|
|
952
|
+
aria-label="Dismiss error"
|
|
953
|
+
>
|
|
954
|
+
Dismiss
|
|
955
|
+
</button>
|
|
956
|
+
</div>
|
|
556
957
|
</div>
|
|
557
958
|
)}
|
|
558
959
|
</div>
|