import { agentNativePath } from "@agent-native/core/client/api-path"; import { useActionMutation, useActionQuery, } from "@agent-native/core/client/hooks"; import { useT } from "@agent-native/core/client/i18n"; import { ActionQueryError, DispatchShell, } from "@agent-native/dispatch/components"; import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, } from "@agent-native/dispatch/components/ui/alert-dialog"; import { Badge } from "@agent-native/dispatch/components/ui/badge"; import { Button } from "@agent-native/dispatch/components/ui/button"; import { Checkbox } from "@agent-native/dispatch/components/ui/checkbox"; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from "@agent-native/dispatch/components/ui/dialog"; import { Input } from "@agent-native/dispatch/components/ui/input"; import { Label } from "@agent-native/dispatch/components/ui/label"; import { RadioGroup, RadioGroupItem, } from "@agent-native/dispatch/components/ui/radio-group"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@agent-native/dispatch/components/ui/select"; import { Switch } from "@agent-native/dispatch/components/ui/switch"; import { IconAlertTriangle, IconArrowLeft, IconArrowRight, IconBrain, IconBrandGithub, IconBrandSlack, IconBroadcast, IconBuilding, IconChartBar, IconCheck, IconCircleDashed, IconClock, IconDatabase, IconEdit, IconKey, IconMail, IconPlugConnected, IconPlus, IconRefresh, IconShieldCheck, IconTrash, IconUsersGroup, IconWorld, } from "@tabler/icons-react"; import { useQueryClient } from "@tanstack/react-query"; import { useEffect, useMemo, useState, type ComponentType, type FormEvent, type ReactNode, } from "react"; import { toast } from "sonner"; import { messagesByLocale } from "@/i18n-data"; export function meta() { return [{ title: messagesByLocale["en-US"].routeTitles.integrations }]; } const CONNECTION_QUERY_PARAMS = { includeDisabled: true } as const; const CONNECTION_QUERY_KEY = [ "action", "list-workspace-connections", CONNECTION_QUERY_PARAMS, ] as const; type IconComponent = ComponentType<{ size?: number | string; className?: string; }>; interface WorkspaceConnectionCredentialKey { key: string; label: string; description?: string; required?: boolean; } type WorkspaceConnectionProviderReadinessStatus = | "ready" | "checking" | "needs_credentials" | "needs_attention" | "disabled" | "not_configured"; interface WorkspaceConnectionProviderReadiness { status: WorkspaceConnectionProviderReadinessStatus; connectionCount: number; activeConnectionCount: number; readyConnectionCount: number; requiredCredentialKeys: string[]; missingRequiredCredentialKeys: string[]; } interface WorkspaceConnectionProvider { id: string; label: string; description: string; credentialKeys: WorkspaceConnectionCredentialKey[]; capabilities: string[]; recommendedTemplateUses: string[]; oauth?: { provider: string; authorizationUrl: string; tokenUrl: string; refreshUrl?: string; scopes: string[]; }; readiness?: WorkspaceConnectionProviderReadiness; } interface WorkspaceConnectionCredentialRef { key: string; scope?: "user" | "org" | "workspace"; provider?: string; label?: string; } type WorkspaceConnectionStatus = | "connected" | "checking" | "needs_reauth" | "error" | "disabled"; interface WorkspaceConnection { id: string; provider: string; label: string; accountId: string | null; accountLabel: string | null; status: WorkspaceConnectionStatus; scopes: string[]; config: Record; allowedApps: string[]; credentialRefs: WorkspaceConnectionCredentialRef[]; createdAt: string; updatedAt: string; lastCheckedAt: string | null; lastUsedAt?: string | null; lastError: string | null; } interface SuggestedGrantApp { id: string; label: string; } interface WorkspaceConnectionGrant { id: string; connectionId: string; provider: string; appId: string; access: "all-apps" | "selected-app" | "explicit-grant"; lastUsedAt?: string | null; } interface WorkspaceConnectionGrantSummary { connectionId: string; provider: string; accessMode: "all-apps" | "selected-apps"; allApps: boolean; selectedAppIds: string[]; explicitGrantAppIds: string[]; effectiveAppIds: string[]; trackedApps?: Array<{ appId: string; label: string; granted: boolean; mode: "all-apps" | "allowed-app" | "explicit-grant" | "unavailable"; grantId: string | null; }>; } interface WorkspaceConnectionsResponse { providers: WorkspaceConnectionProvider[]; connections: WorkspaceConnection[]; grants: WorkspaceConnectionGrant[]; grantSummaries?: WorkspaceConnectionGrantSummary[]; suggestedApps: SuggestedGrantApp[]; counts: { providers: number; connections: number; grants: number; allAppConnections?: number; selectedAppConnections?: number; readyProviders?: number; }; } interface WorkspaceConnectionImpactPreview { likelyAffectedApps: Array<{ appId: string; label: string; accessMode: string; lastUsedAt?: string | null; }>; impactSummary: { likelyAffectedCount: number; hasAllAppsAccess: boolean; usageTracked: boolean; lastUsedAt?: string | null; }; recommendedConfirmation: { body: string; }; } interface WorkspaceAppSummary { id: string; name: string; status?: "ready" | "pending"; archived?: boolean; } interface GrantApp { id: string; label: string; icon: IconComponent; } interface ConnectionFormState { id?: string; provider: string; label: string; accountId: string; accountLabel: string; status: WorkspaceConnectionStatus; scopes: string; credentialRefs: WorkspaceConnectionCredentialRef[]; allApps: boolean; selectedApps: string[]; } interface WorkspaceConnectionSetupPlanCredential { key: string; label: string; description?: string; required?: boolean; } interface WorkspaceConnectionSetupPlanApp { id: string; label: string; recommended: boolean; } interface WorkspaceConnectionSetupPlan { provider: WorkspaceConnectionProvider; requiredCredentialRefs: WorkspaceConnectionSetupPlanCredential[]; suggestedCredentialRefs: Array< WorkspaceConnectionCredentialRef & { description?: string; required?: boolean; } >; suggestedApps: WorkspaceConnectionSetupPlanApp[]; grantRecommendation: { accessMode: "all-apps" | "selected-apps"; selectedAppIds: string[]; reason: string; }; warnings: string[]; connection: Pick< WorkspaceConnection, | "id" | "provider" | "label" | "accountId" | "accountLabel" | "status" | "scopes" | "allowedApps" | "credentialRefs" | "lastError" > | null; explicitGrantAppIds: string[]; } interface SetupWizardState { mode: "setup" | "repair"; providerId: string; connectionId?: string; } interface SetupWizardFormState { connectionId?: string; provider: string; label: string; accountId: string; accountLabel: string; status: WorkspaceConnectionStatus; scopes: string; credentialRefs: WorkspaceConnectionCredentialRef[]; grantMode: "all-apps" | "selected-apps"; selectedApps: string[]; } const EMPTY_RESPONSE: WorkspaceConnectionsResponse = { providers: [], connections: [], grants: [], suggestedApps: [ { id: "dispatch", label: "Dispatch" }, { id: "brain", label: "Brain" }, { id: "assets", label: "Assets" }, { id: "analytics", label: "Analytics" }, { id: "mail", label: "Mail" }, ], counts: { providers: 0, connections: 0, grants: 0 }, }; type Translate = ReturnType; const STATUS_CLASSES: Record = { connected: "border-green-500/20 bg-green-500/10 text-green-700 dark:text-green-400", checking: "border-blue-500/20 bg-blue-500/10 text-blue-700 dark:text-blue-400", needs_reauth: "border-amber-500/20 bg-amber-500/10 text-amber-700 dark:text-amber-400", error: "border-red-500/20 bg-red-500/10 text-red-700 dark:text-red-400", disabled: "border-border bg-muted text-muted-foreground", }; const READINESS_CLASSES: Record< WorkspaceConnectionProviderReadinessStatus, string > = { ready: "border-green-500/20 bg-green-500/10 text-green-700 dark:text-green-400", checking: "border-blue-500/20 bg-blue-500/10 text-blue-700 dark:text-blue-400", needs_credentials: "border-amber-500/20 bg-amber-500/10 text-amber-700 dark:text-amber-400", needs_attention: "border-red-500/20 bg-red-500/10 text-red-700 dark:text-red-400", disabled: "border-border bg-muted text-muted-foreground", not_configured: "border-border bg-muted text-muted-foreground", }; const APP_ICONS: Record = { dispatch: IconBroadcast, brain: IconBrain, analytics: IconChartBar, mail: IconMail, }; const PROVIDER_ICONS: Record = { slack: IconBrandSlack, github: IconBrandGithub, gmail: IconMail, google_drive: IconDatabase, hubspot: IconBuilding, granola: IconDatabase, clips: IconDatabase, notion: IconDatabase, generic: IconWorld, }; function cx(...classes: Array) { return classes.filter(Boolean).join(" "); } function iconForProvider(providerId: string): IconComponent { return PROVIDER_ICONS[providerId] ?? IconPlugConnected; } function iconForApp(appId: string): IconComponent { return APP_ICONS[appId] ?? IconUsersGroup; } function statusLabel(t: Translate, status: WorkspaceConnectionStatus): string { return t(`integrations.status.${status}`); } function readinessLabel( t: Translate, status: WorkspaceConnectionProviderReadinessStatus, ): string { return t(`integrations.readiness.${status}`); } function scopeLabel( t: Translate, scope: WorkspaceConnectionCredentialRef["scope"], ): string { switch (scope) { case "org": return t("integrations.scopeOrg"); case "workspace": return t("integrations.scopeWorkspace"); case "user": return t("integrations.scopeUser"); default: return ""; } } function humanizeAppId(appId: string): string { return appId .split(/[-_]/) .filter(Boolean) .map((part) => part.charAt(0).toUpperCase() + part.slice(1)) .join(" "); } function normalizeList(value: string): string[] { return Array.from( new Set( value .split(/[\n,]/) .map((entry) => entry.trim()) .filter(Boolean), ), ); } function hasUsageTimestamp(connection: WorkspaceConnection): boolean { return Object.prototype.hasOwnProperty.call(connection, "lastUsedAt"); } function hasGrantUsageTimestamp(grant: WorkspaceConnectionGrant): boolean { return Object.prototype.hasOwnProperty.call(grant, "lastUsedAt"); } function formatTimestamp( t: Translate, value: string | null | undefined, ): string { if (value == null) return t("integrations.time.neverUsed"); const date = new Date(value); if (Number.isNaN(date.getTime())) return value; const deltaMs = date.getTime() - Date.now(); const absMs = Math.abs(deltaMs); const units: Array<[Intl.RelativeTimeFormatUnit, number]> = [ ["year", 1000 * 60 * 60 * 24 * 365], ["month", 1000 * 60 * 60 * 24 * 30], ["day", 1000 * 60 * 60 * 24], ["hour", 1000 * 60 * 60], ["minute", 1000 * 60], ]; const formatter = new Intl.RelativeTimeFormat(undefined, { numeric: "auto", }); for (const [unit, ms] of units) { if (absMs >= ms) { return formatter.format(Math.round(deltaMs / ms), unit); } } return t("integrations.time.justNow"); } function usageSortValue(connection: WorkspaceConnection): number { if (!hasUsageTimestamp(connection)) return Number.NaN; if (connection.lastUsedAt == null) return 0; const time = new Date(connection.lastUsedAt).getTime(); return Number.isNaN(time) ? 0 : time; } function credentialRefsForProvider( provider: WorkspaceConnectionProvider, ): WorkspaceConnectionCredentialRef[] { return provider.credentialKeys.map((credential) => ({ key: credential.key, label: credential.label, provider: provider.id, scope: "org", })); } function normalizeCredentialRefs( refs: WorkspaceConnectionCredentialRef[], provider?: WorkspaceConnectionProvider, ): WorkspaceConnectionCredentialRef[] { const labels = new Map( provider?.credentialKeys.map((credential) => [ credential.key, credential.label, ]) ?? [], ); const seen = new Set(); return refs .map((ref) => { const key = ref.key.trim(); return { key, label: ref.label?.trim() || labels.get(key) || key, provider: ref.provider?.trim() || provider?.id, scope: ref.scope ?? "org", }; }) .filter((ref) => { if (!ref.key || seen.has(ref.key)) return false; seen.add(ref.key); return true; }); } function upsertCredentialRefAt( refs: WorkspaceConnectionCredentialRef[], index: number, patch: Partial, ): WorkspaceConnectionCredentialRef[] { return refs.map((ref, currentIndex) => currentIndex === index ? { ...ref, ...patch } : ref, ); } function appendCredentialRef( refs: WorkspaceConnectionCredentialRef[], provider?: WorkspaceConnectionProvider, ): WorkspaceConnectionCredentialRef[] { return [ ...refs, { key: "", label: "", provider: provider?.id, scope: "org", }, ]; } function missingRequiredCredentialKeys( provider: WorkspaceConnectionProvider | undefined, refs: WorkspaceConnectionCredentialRef[], ): string[] { if (!provider) return []; const available = new Set(refs.map((ref) => ref.key.trim()).filter(Boolean)); return provider.credentialKeys .filter((credential) => credential.required) .map((credential) => credential.key) .filter((key) => !available.has(key)); } function formFromConnection( connection: WorkspaceConnection, provider?: WorkspaceConnectionProvider, ): ConnectionFormState { return { id: connection.id, provider: connection.provider, label: connection.label, accountId: connection.accountId ?? "", accountLabel: connection.accountLabel ?? "", status: connection.status, scopes: connection.scopes.join(", "), credentialRefs: connection.credentialRefs.length > 0 ? normalizeCredentialRefs(connection.credentialRefs, provider) : provider ? credentialRefsForProvider(provider) : [], allApps: connection.allowedApps.length === 0, selectedApps: connection.allowedApps, }; } function setupFormFromPlan( plan: WorkspaceConnectionSetupPlan, mode: SetupWizardState["mode"], ): SetupWizardFormState { const connection = plan.connection; const selectedApps = plan.grantRecommendation.accessMode === "all-apps" ? [] : plan.grantRecommendation.selectedAppIds; return { connectionId: connection?.id, provider: plan.provider.id, label: connection?.label || plan.provider.label, accountId: connection?.accountId ?? "", accountLabel: connection?.accountLabel ?? "", status: mode === "repair" ? "checking" : (connection?.status ?? "connected"), scopes: connection?.scopes.join(", ") ?? "", credentialRefs: connection?.credentialRefs.length ? normalizeCredentialRefs(connection.credentialRefs, plan.provider) : normalizeCredentialRefs(plan.suggestedCredentialRefs, plan.provider), grantMode: plan.grantRecommendation.accessMode, selectedApps, }; } function appIsGranted( connection: WorkspaceConnection, appId: string, grants: WorkspaceConnectionsResponse["grants"], ): boolean { return ( connection.allowedApps.length === 0 || connection.allowedApps.includes(appId) || grants.some( (grant) => grant.connectionId === connection.id && (grant.appId === appId || grant.appId === "*"), ) ); } function grantModeForApp( connection: WorkspaceConnection, appId: string, grants: WorkspaceConnectionsResponse["grants"], ): "all-apps" | "selected" | "explicit" | "off" { if (connection.allowedApps.length === 0) return "all-apps"; if (connection.allowedApps.includes(appId)) return "selected"; if ( grants.some( (grant) => grant.connectionId === connection.id && (grant.appId === appId || grant.appId === "*"), ) ) { return "explicit"; } return "off"; } function grantModeLabel( t: Translate, mode: ReturnType, ): string { return t(`integrations.grantMode.${mode}`); } function usageForAppGrant( connection: WorkspaceConnection, appId: string, grants: WorkspaceConnectionsResponse["grants"], ): string | null | undefined { const explicitGrant = grants.find( (grant) => grant.connectionId === connection.id && grant.appId === appId, ); if (explicitGrant && hasGrantUsageTimestamp(explicitGrant)) { return explicitGrant.lastUsedAt; } return hasUsageTimestamp(connection) ? connection.lastUsedAt : undefined; } function nextAllowedApps( connection: WorkspaceConnection, appId: string, granted: boolean, knownAppIds: string[], ): string[] { const current = connection.allowedApps.length === 0 ? Array.from(new Set([...knownAppIds, appId])) : connection.allowedApps; if (granted) { return Array.from(new Set([...current, appId])); } return current.filter((id) => id !== appId); } function summarizeGrant( connection: WorkspaceConnection, grantApps: GrantApp[], grants: WorkspaceConnectionsResponse["grants"], t: Translate, ) { if (connection.allowedApps.length === 0) return t("integrations.allApps"); const grantedAppIds = Array.from( new Set([ ...connection.allowedApps, ...grants .filter((grant) => grant.connectionId === connection.id) .map((grant) => grant.appId) .filter((appId) => appId !== "*"), ]), ); const labels = grantedAppIds .map((appId) => grantApps.find((app) => app.id === appId)?.label ?? appId) .slice(0, 3); const suffix = grantedAppIds.length > labels.length ? ` +${grantedAppIds.length - labels.length}` : ""; return `${labels.join(", ")}${suffix}`; } function summarizeAppList( appIds: string[], grantApps: GrantApp[], t: Translate, ): string { const labels = appIds .map((appId) => grantApps.find((app) => app.id === appId)?.label ?? appId) .slice(0, 3); const suffix = appIds.length > labels.length ? ` +${appIds.length - labels.length}` : ""; return labels.length > 0 ? `${labels.join(", ")}${suffix}` : t("integrations.noApps"); } function startWorkspaceProviderOAuth(provider: WorkspaceConnectionProvider) { const returnPath = `${window.location.pathname}${window.location.search}`; if (provider.id === "slack") { const params = new URLSearchParams({ return: returnPath }); window.location.assign( agentNativePath( `/_agent-native/integrations/slack/oauth/install?${params.toString()}`, ), ); return; } const params = new URLSearchParams({ appId: "dispatch", return: returnPath, }); window.location.assign( agentNativePath( `/_agent-native/connections/oauth/${provider.id}/start?${params.toString()}`, ), ); } function providerHasOAuth(provider: WorkspaceConnectionProvider): boolean { return Boolean(provider.oauth) || provider.id === "slack"; } function ProviderCard({ provider, connections, onCreate, }: { provider: WorkspaceConnectionProvider; connections: WorkspaceConnection[]; onCreate: () => void; }) { const t = useT(); const Icon = iconForProvider(provider.id); const active = connections.filter((item) => item.status !== "disabled"); const readiness = provider.readiness; const readinessStatus = readiness?.status ?? (active.length > 0 ? "ready" : "not_configured"); return (
{readinessStatus === "ready" ? ( ) : ( )} {readinessLabel(t, readinessStatus)}

{provider.label}

{provider.description}

{provider.capabilities.map((capability) => ( {capability} ))}
{readiness?.missingRequiredCredentialKeys.length ? (
{t("integrations.missingKeys", { keys: readiness.missingRequiredCredentialKeys .slice(0, 2) .join(", "), suffix: readiness.missingRequiredCredentialKeys.length > 2 ? ` +${readiness.missingRequiredCredentialKeys.length - 2}` : "", })}
) : null}
{active.length > 0 ? t("integrations.activeConnection", { count: active.length }) : provider.credentialKeys.length === 0 ? t("integrations.noCredentialRefs") : t("integrations.credentialRefCount", { count: provider.credentialKeys.length, })}
{providerHasOAuth(provider) ? ( ) : null}
); } function ConnectionRow({ connection, provider, grantApps, grants, onEdit, onRepair, onDelete, onToggleGrant, grantPending, }: { connection: WorkspaceConnection; provider?: WorkspaceConnectionProvider; grantApps: GrantApp[]; grants: WorkspaceConnectionsResponse["grants"]; onEdit: () => void; onRepair: () => void; onDelete: () => void; onToggleGrant: (appId: string, granted: boolean) => void; grantPending: boolean; }) { const t = useT(); const Icon = iconForProvider(connection.provider); const missingKeys = missingRequiredCredentialKeys( provider, connection.credentialRefs, ); const needsRepair = missingKeys.length > 0 || connection.status === "error" || connection.status === "needs_reauth" || connection.status === "disabled"; return (

{connection.label}

{statusLabel(t, connection.status)} {missingKeys.length > 0 ? ( {t("integrations.missingRefs")} ) : null}

{provider?.label ?? connection.provider} {connection.accountLabel ? ` · ${connection.accountLabel}` : ""} {connection.accountId ? ` · ${connection.accountId}` : ""}

{needsRepair ? ( ) : null}
ref.key).join(", ") : t("integrations.none") } /> {hasUsageTimestamp(connection) ? ( ) : null}
{connection.credentialRefs.length > 0 ? ( ) : null} {missingKeys.length > 0 ? (
{t("integrations.requiredRefsMissing", { keys: missingKeys.join(", "), })}
) : null} {connection.lastError ? (
{connection.lastError}
) : null}

{t("integrations.appGrants")}

{connection.allowedApps.length === 0 ? t("integrations.everyWorkspaceAppCanReuse") : t("integrations.selectedAppsCanReuse", { apps: summarizeGrant(connection, grantApps, grants, t), })}

{connection.allowedApps.length === 0 ? t("integrations.allApps") : t("integrations.selected")}
{grantApps.map((app) => { const AppIcon = app.icon; const granted = appIsGranted(connection, app.id, grants); const mode = grantModeForApp(connection, app.id, grants); const usage = usageForAppGrant(connection, app.id, grants); return (
{app.label} {usage !== undefined ? ( {formatTimestamp(t, usage)} ) : null} {grantModeLabel(t, mode)}
onToggleGrant(app.id, checked) } aria-label={t("integrations.toggleGrantAria", { action: granted ? t("integrations.revoke") : t("integrations.grant"), app: app.label, connection: connection.label, })} />
); })}
); } function ConnectionMeta({ icon: Icon, label, value, }: { icon: IconComponent; label: string; value: string; }) { return (
{label}
{value}
); } function CredentialRefsPreview({ refs, }: { refs: WorkspaceConnectionCredentialRef[]; }) { const t = useT(); return (
{refs.map((ref) => ( {ref.key} {ref.scope ? ( {scopeLabel(t, ref.scope)} ) : null} ))}
); } function Pill({ children, className, }: { children: ReactNode; className?: string; }) { return ( {children} ); } function Modal({ title, description, open, onClose, children, }: { title: string; description?: string; open: boolean; onClose: () => void; children: ReactNode; }) { return ( !next && onClose()}> {title} {description ? ( {description} ) : null} {children} ); } function ConnectionForm({ open, form, providers, grantApps, saving, onChange, onClose, onSubmit, }: { open: boolean; form: ConnectionFormState | null; providers: WorkspaceConnectionProvider[]; grantApps: GrantApp[]; saving: boolean; onChange: (form: ConnectionFormState) => void; onClose: () => void; onSubmit: (event: FormEvent) => void; }) { const t = useT(); if (!form) return null; const provider = providers.find((item) => item.id === form.provider); const credentialRefs = normalizeCredentialRefs(form.credentialRefs, provider); const missingCredentialRefs = missingRequiredCredentialKeys( provider, credentialRefs, ); return (
onChange({ ...form, label: value })} required /> onChange({ ...form, accountLabel: value })} placeholder={t("integrations.accountLabelPlaceholder")} />
onChange({ ...form, accountId: value })} placeholder={t("integrations.accountIdPlaceholder")} /> onChange({ ...form, scopes: value })} placeholder={t("integrations.scopesPlaceholder")} />
{t("integrations.accessMode")}
{form.allApps ? t("integrations.allAppsCanReuseConnection") : t("integrations.onlySelectedAppsCanReuseConnection")}
onChange({ ...form, allApps: checked }) } aria-label={t("integrations.grantAllWorkspaceAppsAria")} />
{!form.allApps ? (
{grantApps.map((app) => { const AppIcon = app.icon; const selected = form.selectedApps.includes(app.id); return ( ); })}
) : null}
onChange({ ...form, credentialRefs })} />
); } function TextField({ label, value, onChange, placeholder, required, }: { label: string; value: string; onChange: (value: string) => void; placeholder?: string; required?: boolean; }) { return (
onChange(event.target.value)} placeholder={placeholder} required={required} />
); } function CredentialRefsEditor({ provider, refs, missingRefs, onChange, }: { provider?: WorkspaceConnectionProvider; refs: WorkspaceConnectionCredentialRef[]; missingRefs: string[]; onChange: (refs: WorkspaceConnectionCredentialRef[]) => void; }) { const t = useT(); const providerKeys = new Map( provider?.credentialKeys.map((credential) => [ credential.key, credential, ]) ?? [], ); const rows = refs.length > 0 ? refs : appendCredentialRef([], provider); return (
{t("integrations.credentialRefs")}
{t("integrations.credentialRefsDescription")}
{rows.map((ref, index) => { const credential = providerKeys.get(ref.key); return (
onChange( upsertCredentialRefAt(rows, index, { key: event.target.value, label: providerKeys.get(event.target.value)?.label ?? ref.label, provider: provider?.id ?? ref.provider, }), ) } placeholder={ provider?.credentialKeys[index]?.key ?? "VAULT_KEY_NAME" } spellCheck={false} className="font-mono" /> {credential?.description ? ( {credential.description} ) : null}
); })}
{missingRefs.length > 0 ? (
{t("integrations.missingRequiredRefs", { refs: missingRefs.join(", "), })}
) : null}
); } function SetupWizard({ open, state, step, plan, form, providers, grantApps, loading, saving, onStepChange, onProviderChange, onChange, onClose, onSubmit, }: { open: boolean; state: SetupWizardState | null; step: number; plan?: WorkspaceConnectionSetupPlan; form: SetupWizardFormState | null; providers: WorkspaceConnectionProvider[]; grantApps: GrantApp[]; loading: boolean; saving: boolean; onStepChange: (step: number) => void; onProviderChange: (providerId: string) => void; onChange: (form: SetupWizardFormState) => void; onClose: () => void; onSubmit: () => void; }) { const t = useT(); const provider = plan?.provider ?? providers.find((item) => item.id === state?.providerId); const credentialRefs = form ? normalizeCredentialRefs(form.credentialRefs, provider) : []; const missingCredentialRefs = missingRequiredCredentialKeys( provider, credentialRefs, ); const selectedApps = form?.selectedApps ?? []; const stepItems = [ { label: t("integrations.provider"), icon: IconPlugConnected }, { label: t("integrations.refs"), icon: IconKey }, { label: t("integrations.access"), icon: IconShieldCheck }, ]; const canAdvance = step === 0 ? Boolean(form?.label.trim()) : step === 1 ? missingCredentialRefs.length === 0 : form?.grantMode === "all-apps" || selectedApps.length > 0; const suggestedGrantApps = useMemo(() => { const map = new Map< string, GrantApp & { recommended: boolean; } >(); for (const app of plan?.suggestedApps ?? []) { map.set(app.id, { id: app.id, label: app.label, icon: iconForApp(app.id), recommended: app.recommended, }); } for (const app of grantApps) { const current = map.get(app.id); map.set(app.id, { ...app, recommended: current?.recommended ?? false, }); } return Array.from(map.values()); }, [grantApps, plan?.suggestedApps]); return (
{stepItems.map((item, index) => { const Icon = item.icon; const active = step === index; const complete = step > index; return ( ); })}
{loading ? (
{t("integrations.loadingSetupPlan")}
) : null} {!loading && form && provider ? ( <> {plan?.warnings.length ? (
{plan.warnings.map((warning) => (
{warning}
))}
) : null} {step === 0 ? (
onChange({ ...form, label: value })} required />
onChange({ ...form, accountLabel: value }) } placeholder={t("integrations.accountLabelPlaceholder")} /> onChange({ ...form, accountId: value }) } placeholder={t("integrations.accountIdPlaceholder")} />
{t("integrations.providerPlan")}
{provider.capabilities.map((capability) => ( {capability} ))}

{plan?.grantRecommendation.reason}

) : null} {step === 1 ? ( onChange({ ...form, credentialRefs }) } /> ) : null} {step === 2 ? (
onChange({ ...form, grantMode: value as SetupWizardFormState["grantMode"], }) } className="grid gap-2" > {form.grantMode === "selected-apps" ? (
{t("integrations.appGrants")}
{suggestedGrantApps.map((app) => { const AppIcon = app.icon; const selected = selectedApps.includes(app.id); return ( ); })}
{selectedApps.length === 0 ? (
{t("integrations.chooseOneApp")}
) : null}
) : null}
) : null} ) : null}
{step > 0 ? ( ) : null} {step < 2 ? ( ) : ( )}
); } function GrantPreview({ providerLabel, grantMode, selectedAppIds, grantApps, }: { providerLabel: string; grantMode: SetupWizardFormState["grantMode"]; selectedAppIds: string[]; grantApps: GrantApp[]; }) { const t = useT(); const selectedLabel = summarizeAppList(selectedAppIds, grantApps, t); return (
{t("integrations.grantPreview")}

{grantMode === "all-apps" ? t("integrations.grantPreviewAllApps", { provider: providerLabel, }) : selectedAppIds.length > 0 ? t("integrations.grantPreviewSelected", { apps: selectedLabel, provider: providerLabel, }) : t("integrations.grantPreviewEmpty", { provider: providerLabel, })}

); } function DeleteConfirm({ connection, deleting, onClose, onConfirm, }: { connection: WorkspaceConnection | null; deleting: boolean; onClose: () => void; onConfirm: () => void; }) { const t = useT(); const { data: impact, isLoading: impactLoading } = useActionQuery( "preview-workspace-connection-impact", { connectionId: connection?.id ?? "", operation: "delete-connection", }, { enabled: Boolean(connection?.id) }, ); return ( !open && onClose()} > {t("integrations.deleteConnection")} {connection?.label ? t("integrations.deleteConnectionDescription", { connection: connection.label, }) : t("integrations.deleteSharedConnectionDescription")} {t("integrations.cancel")} { event.preventDefault(); onConfirm(); }} className="bg-destructive text-destructive-foreground hover:bg-destructive/90" > {deleting ? ( ) : null} {t("integrations.delete")} ); } function ConnectionDeleteImpact({ impact, loading, }: { impact?: WorkspaceConnectionImpactPreview; loading: boolean; }) { const t = useT(); if (loading) { return (
{t("integrations.checkingAffectedApps")}
); } if (!impact) return null; const appLabels = impact.likelyAffectedApps .map((app) => app.label) .slice(0, 4); const extraCount = Math.max( 0, impact.likelyAffectedApps.length - appLabels.length, ); return (
{impact.impactSummary.hasAllAppsAccess ? t("integrations.allAppConnection") : t("integrations.selectedGrants")} {impact.impactSummary.usageTracked ? ( {formatTimestamp(t, impact.impactSummary.lastUsedAt)} ) : null}

{impact.likelyAffectedApps.length > 0 ? t("integrations.likelyAffected", { apps: appLabels.join(", "), suffix: extraCount ? ` +${extraCount}` : "", }) : t("integrations.noCurrentAppGrants")}

{impact.recommendedConfirmation.body}

); } export default function WorkspaceIntegrationsRoute() { const t = useT(); const queryClient = useQueryClient(); const [form, setForm] = useState(null); const [setupWizard, setSetupWizard] = useState(null); const [setupStep, setSetupStep] = useState(0); const [setupForm, setSetupForm] = useState(null); const [setupFormKey, setSetupFormKey] = useState(""); const [deleteTarget, setDeleteTarget] = useState( null, ); const connectionsQuery = useActionQuery( "list-workspace-connections", CONNECTION_QUERY_PARAMS, ); const appsQuery = useActionQuery("list-workspace-apps", { includeAgentCards: false, audience: "all", }); const setupPlanQuery = useActionQuery( "plan-workspace-connection-setup", { provider: setupWizard?.providerId, connectionId: setupWizard?.connectionId, }, { enabled: Boolean(setupWizard) }, ); const data = (connectionsQuery.data ?? EMPTY_RESPONSE) as WorkspaceConnectionsResponse; const providers = data.providers; const connections = data.connections; const apps = (appsQuery.data ?? []) as WorkspaceAppSummary[]; const providersById = useMemo( () => new Map(providers.map((provider) => [provider.id, provider])), [providers], ); const grantApps = useMemo(() => { const map = new Map(); for (const app of apps) { if (app.archived || app.status === "pending") continue; map.set(app.id, { id: app.id, label: app.name || humanizeAppId(app.id), icon: iconForApp(app.id), }); } return Array.from(map.values()); }, [apps]); const providerConnections = useMemo(() => { const map = new Map(); for (const connection of connections) { const items = map.get(connection.provider) ?? []; items.push(connection); map.set(connection.provider, items); } return map; }, [connections]); const upsertConnection = useActionMutation("upsert-workspace-connection"); const applySetup = useActionMutation("apply-workspace-connection-setup"); const setGrant = useActionMutation("set-workspace-connection-grant"); const deleteConnection = useActionMutation("delete-workspace-connection"); const currentSetupKey = setupWizard ? `${setupWizard.mode}:${setupWizard.connectionId ?? ""}:${ setupWizard.providerId }` : ""; useEffect(() => { if (!setupWizard || !setupPlanQuery.data) return; if (setupFormKey === currentSetupKey) return; setSetupForm(setupFormFromPlan(setupPlanQuery.data, setupWizard.mode)); setSetupFormKey(currentSetupKey); }, [currentSetupKey, setupFormKey, setupPlanQuery.data, setupWizard]); function openSetup(provider: WorkspaceConnectionProvider) { setSetupWizard({ mode: "setup", providerId: provider.id }); setSetupStep(0); setSetupForm(null); setSetupFormKey(""); } function openRepair(connection: WorkspaceConnection) { setSetupWizard({ mode: "repair", providerId: connection.provider, connectionId: connection.id, }); setSetupStep(0); setSetupForm(null); setSetupFormKey(""); } function openEdit(connection: WorkspaceConnection) { setForm( formFromConnection(connection, providersById.get(connection.provider)), ); } async function handleSubmit(event: FormEvent) { event.preventDefault(); if (!form) return; try { const provider = providersById.get(form.provider); const credentialRefs = normalizeCredentialRefs( form.credentialRefs, provider, ); await upsertConnection.mutateAsync({ id: form.id, provider: form.provider, label: form.label.trim(), accountId: form.accountId.trim() || null, accountLabel: form.accountLabel.trim() || null, status: form.status, scopes: normalizeList(form.scopes), credentialRefs, allowedApps: form.allApps ? [] : form.selectedApps, }); toast.success( form.id ? t("integrations.connectionUpdated") : t("integrations.connectionCreated"), ); setForm(null); } catch (error) { toast.error( error instanceof Error ? error.message : t("integrations.failedToSave"), ); } } async function applySetupWizard() { if (!setupForm) return; try { await applySetup.mutateAsync({ connectionId: setupForm.connectionId, provider: setupForm.provider, label: setupForm.label.trim(), accountId: setupForm.accountId.trim() || null, accountLabel: setupForm.accountLabel.trim() || null, status: setupForm.status, scopes: normalizeList(setupForm.scopes), credentialRefs: normalizeCredentialRefs( setupForm.credentialRefs, providersById.get(setupForm.provider), ), grantMode: setupForm.grantMode, selectedApps: setupForm.selectedApps, }); toast.success( setupForm.connectionId ? t("integrations.connectionRepaired") : t("integrations.connectionCreated"), ); setSetupWizard(null); setSetupForm(null); setSetupFormKey(""); queryClient.invalidateQueries({ queryKey: CONNECTION_QUERY_KEY }); } catch (error) { toast.error( error instanceof Error ? error.message : t("integrations.failedToApplySetup"), ); } } async function toggleGrant( connection: WorkspaceConnection, appId: string, granted: boolean, ) { const previous = queryClient.getQueryData( CONNECTION_QUERY_KEY, ); const knownAppIds = grantApps.map((app) => app.id); queryClient.setQueryData( CONNECTION_QUERY_KEY, (current) => { if (!current) return current; const existingGrant = current.grants.find( (grant) => grant.connectionId === connection.id && grant.appId === appId, ); return { ...current, connections: current.connections.map((item) => item.id === connection.id ? { ...item, allowedApps: nextAllowedApps( item, appId, granted, knownAppIds, ), } : item, ), grants: granted ? existingGrant ? current.grants : [ ...current.grants, { id: `${connection.id}:${appId}:optimistic`, connectionId: connection.id, provider: connection.provider, appId, access: "explicit-grant", }, ] : current.grants.filter( (grant) => !( grant.connectionId === connection.id && (grant.appId === appId || (connection.allowedApps.length === 0 && grant.appId === "*")) ), ), }; }, ); try { await setGrant.mutateAsync({ connectionId: connection.id, appId, granted, knownAppIds, }); queryClient.invalidateQueries({ queryKey: CONNECTION_QUERY_KEY }); toast.success( granted ? t("integrations.grantAdded") : t("integrations.grantRevoked"), ); } catch (error) { if (previous) { queryClient.setQueryData(CONNECTION_QUERY_KEY, previous); } toast.error( error instanceof Error ? error.message : t("integrations.failedToUpdateGrant"), ); } } async function confirmDelete() { if (!deleteTarget) return; const previous = queryClient.getQueryData( CONNECTION_QUERY_KEY, ); queryClient.setQueryData( CONNECTION_QUERY_KEY, (current) => current ? { ...current, connections: current.connections.filter( (item) => item.id !== deleteTarget.id, ), } : current, ); try { await deleteConnection.mutateAsync({ id: deleteTarget.id }); toast.success(t("integrations.connectionDeleted")); setDeleteTarget(null); } catch (error) { if (previous) { queryClient.setQueryData(CONNECTION_QUERY_KEY, previous); } toast.error( error instanceof Error ? error.message : t("integrations.failedToDeleteConnection"), ); } } const connectedCount = connections.filter( (connection) => connection.status === "connected", ).length; const attentionCount = connections.filter((connection) => ["needs_reauth", "error", "disabled"].includes(connection.status), ).length; const connectionsMissingKeys = connections.filter( (connection) => missingRequiredCredentialKeys( providersById.get(connection.provider), connection.credentialRefs, ).length, ).length; const usageTracked = connections.some(hasUsageTimestamp); const neverUsedCount = connections.filter( (connection) => hasUsageTimestamp(connection) && connection.lastUsedAt == null, ).length; const mostRecentUsage = Math.max( ...connections .map(usageSortValue) .filter((value) => Number.isFinite(value) && value > 0), ); return (
{connectionsQuery.isError || appsQuery.isError ? ( { void connectionsQuery.refetch(); void appsQuery.refetch(); }} /> ) : null} {!connectionsQuery.isError && !appsQuery.isError ? ( <>
{usageTracked ? ( ) : null}
{connectionsQuery.isLoading ? (
{t("integrations.loadingWorkspaceIntegrations")}
) : null}

{t("integrations.providerCatalog")}

{t("integrations.providersAvailable", { count: providers.length, })}

{providers.map((provider) => ( openSetup(provider)} /> ))}

{t("integrations.connectedAccounts")}

{connections.length === 0 ? t("integrations.noSharedConnectionsYet") : t("integrations.savedConnectionCount", { count: connections.length, })}

{connections.length === 0 && !connectionsQuery.isLoading ? (

{t("integrations.noSharedAccountsYet")}

{t("integrations.emptyConnectedAccountsDescription")}

) : (
{connections.map((connection) => ( openEdit(connection)} onRepair={() => openRepair(connection)} onDelete={() => setDeleteTarget(connection)} onToggleGrant={(appId, granted) => toggleGrant(connection, appId, granted) } /> ))}
)}
) : null}
{ setSetupWizard({ mode: "setup", providerId }); setSetupStep(0); setSetupForm(null); setSetupFormKey(""); }} onChange={setSetupForm} onClose={() => { setSetupWizard(null); setSetupForm(null); setSetupFormKey(""); }} onSubmit={applySetupWizard} /> setForm(null)} onSubmit={handleSubmit} /> setDeleteTarget(null)} onConfirm={confirmDelete} />
); } function IntegrationOnboarding() { const t = useT(); const steps: Array<{ icon: IconComponent; title: string; detail: string; }> = [ { icon: IconPlugConnected, title: t("integrations.onboardingConnectTitle"), detail: t("integrations.onboardingConnectDetail"), }, { icon: IconShieldCheck, title: t("integrations.onboardingGrantTitle"), detail: t("integrations.onboardingGrantDetail"), }, { icon: IconDatabase, title: t("integrations.onboardingLocalTitle"), detail: t("integrations.onboardingLocalDetail"), }, ]; return (

{t("integrations.onboardingTitle")}

{t("integrations.onboardingDescription")}

{steps.map((step) => { const Icon = step.icon; return (
{step.title}
{step.detail}
); })}
); } function SummaryCard({ icon: Icon, label, value, detail, }: { icon: IconComponent; label: string; value: string; detail: string; }) { return (
{label}
{value}
{detail}
); }