@burdenoff/microfe-devportal 2026.625.2 → 2026.625.3

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.
Files changed (28) hide show
  1. package/dist/devportal/components/EmptyState.js +14 -8
  2. package/dist/devportal/components/EmptyState.js.map +1 -1
  3. package/dist/devportal/components/LoadingState.js.map +1 -1
  4. package/dist/devportal/components/PageLayout.js +39 -34
  5. package/dist/devportal/components/PageLayout.js.map +1 -1
  6. package/dist/devportal/components/StatCard.js +4 -3
  7. package/dist/devportal/components/StatCard.js.map +1 -1
  8. package/dist/devportal/components/StatusBadge.js +1 -1
  9. package/dist/devportal/components/StatusBadge.js.map +1 -1
  10. package/dist/devportal/pages/ApiKeysPage.js +1 -1
  11. package/dist/devportal/pages/ApiKeysPage.js.map +1 -1
  12. package/dist/devportal/pages/AppLogsPage.js +85 -85
  13. package/dist/devportal/pages/AppLogsPage.js.map +1 -1
  14. package/dist/devportal/pages/ApplicationVersionsPage.js +7 -7
  15. package/dist/devportal/pages/ApplicationVersionsPage.js.map +1 -1
  16. package/dist/devportal/pages/DashboardPage.js +157 -151
  17. package/dist/devportal/pages/DashboardPage.js.map +1 -1
  18. package/dist/devportal/pages/DelegationsPage.js +2 -2
  19. package/dist/devportal/pages/DelegationsPage.js.map +1 -1
  20. package/dist/devportal/pages/OAuthClientsPage.js +1 -1
  21. package/dist/devportal/pages/OAuthClientsPage.js.map +1 -1
  22. package/dist/devportal/pages/OrganizationMembersPage.js +225 -225
  23. package/dist/devportal/pages/OrganizationMembersPage.js.map +1 -1
  24. package/dist/devportal/pages/OrganizationsPage.js +25 -25
  25. package/dist/devportal/pages/OrganizationsPage.js.map +1 -1
  26. package/dist/devportal/pages/WebhooksPage.js +1 -1
  27. package/dist/devportal/pages/WebhooksPage.js.map +1 -1
  28. package/package.json +2 -2
@@ -1 +1 @@
1
- {"version":3,"file":"DelegationsPage.js","names":[],"sources":["../../../src/devportal/pages/DelegationsPage.tsx"],"sourcesContent":["import type { FC } from \"react\";\nimport { useState, useCallback, useEffect, useMemo } from \"react\";\nimport { useI18n } from \"@burdenoff/fe-libs/shared/providers/shell/I18nProvider\";\nimport {\n Shield,\n Users,\n Clock,\n XCircle,\n AlertTriangle,\n Loader2,\n AlertCircle,\n Bot,\n Zap,\n Cog,\n} from \"lucide-react\";\nimport { EmphasisPanel, GlassCard } from \"@burdenoff/fe-libs/ui\";\nimport { PageLayout } from \"../components/PageLayout\";\nimport { EmptyState } from \"../components/EmptyState\";\nimport { LoadingState } from \"../components/LoadingState\";\nimport { ConfirmDialog } from \"../components/ConfirmDialog\";\nimport { SelectionCheckbox } from \"../components/SelectionCheckbox\";\nimport { cn } from \"../utils/cn\";\nimport { PermissionGuard } from \"../components\";\nimport { DEVPORTAL_PERMISSIONS } from \"../constants/permissions\";\nimport { useDevportalPermission } from \"../hooks/usePermission\";\nimport type { AppUserDelegation, DelegationStatus } from \"../types\";\nimport {\n useCurrentUserDelegationsQuery,\n useCurrentUserGrantedDelegationsQuery,\n useRevokeDelegationMutation,\n} from \"../../generated/global-operations\";\n\n// =============================================================================\n// HELPERS\n// =============================================================================\n\ntype ActiveTab = \"acting-as-me\" | \"granted-by-me\";\n\n/**\n * Map a GraphQL delegation fragment to the local AppUserDelegation type\n */\nfunction mapDelegation(d: {\n id: string;\n appId: string;\n userId: string;\n scopes: string[];\n workspaceIds?: string[] | null;\n status: string;\n grantedAt: string;\n grantedBy: string;\n expiresAt?: string | null;\n revokedAt?: string | null;\n revokedBy?: string | null;\n app: {\n id: string;\n name: string;\n description?: string | null;\n type: string;\n status: string;\n };\n}): AppUserDelegation {\n return {\n id: d.id,\n appId: d.appId,\n userId: d.userId,\n scopes: d.scopes,\n workspaceIds: d.workspaceIds ?? [],\n status: d.status as DelegationStatus,\n grantedAt: d.grantedAt,\n grantedBy: d.grantedBy,\n expiresAt: d.expiresAt ?? undefined,\n revokedAt: d.revokedAt ?? undefined,\n revokedBy: d.revokedBy ?? undefined,\n app: {\n id: d.app.id,\n name: d.app.name,\n description: d.app.description ?? undefined,\n type: d.app.type,\n status: d.app.status,\n },\n };\n}\n\n/**\n * Format a date string to relative time (\"2 hours ago\", \"3 days ago\", etc.)\n */\nfunction formatRelativeTime(dateString: string): string {\n const date = new Date(dateString);\n const now = new Date();\n const diffMs = now.getTime() - date.getTime();\n const diffSeconds = Math.floor(diffMs / 1000);\n const diffMinutes = Math.floor(diffSeconds / 60);\n const diffHours = Math.floor(diffMinutes / 60);\n const diffDays = Math.floor(diffHours / 24);\n const diffWeeks = Math.floor(diffDays / 7);\n const diffMonths = Math.floor(diffDays / 30);\n\n if (diffSeconds < 60) return \"just now\";\n if (diffMinutes < 60)\n return `${diffMinutes} minute${diffMinutes !== 1 ? \"s\" : \"\"} ago`;\n if (diffHours < 24)\n return `${diffHours} hour${diffHours !== 1 ? \"s\" : \"\"} ago`;\n if (diffDays < 7) return `${diffDays} day${diffDays !== 1 ? \"s\" : \"\"} ago`;\n if (diffWeeks < 5)\n return `${diffWeeks} week${diffWeeks !== 1 ? \"s\" : \"\"} ago`;\n if (diffMonths < 12)\n return `${diffMonths} month${diffMonths !== 1 ? \"s\" : \"\"} ago`;\n\n return date.toLocaleDateString();\n}\n\n/**\n * Format a future date as relative time (\"in 3 days\", \"in 2 hours\", etc.)\n */\nfunction formatExpirationTime(dateString: string): string {\n const date = new Date(dateString);\n const now = new Date();\n const diffMs = date.getTime() - now.getTime();\n\n if (diffMs <= 0) return \"expired\";\n\n const diffMinutes = Math.floor(diffMs / (1000 * 60));\n const diffHours = Math.floor(diffMs / (1000 * 60 * 60));\n const diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24));\n\n if (diffMinutes < 60)\n return `in ${diffMinutes} minute${diffMinutes !== 1 ? \"s\" : \"\"}`;\n if (diffHours < 24)\n return `in ${diffHours} hour${diffHours !== 1 ? \"s\" : \"\"}`;\n return `in ${diffDays} day${diffDays !== 1 ? \"s\" : \"\"}`;\n}\n\n// =============================================================================\n// SUB-COMPONENTS\n// =============================================================================\n\n/**\n * Icon for app type (SERVICE, BOT, INTEGRATION, etc.)\n */\nconst AppTypeIcon: FC<{ type: string; className?: string }> = ({\n type,\n className,\n}) => {\n const iconProps = { className: cn(\"h-5 w-5\", className) };\n\n switch (type) {\n case \"SERVICE\":\n return <Cog {...iconProps} />;\n case \"BOT\":\n return <Bot {...iconProps} />;\n case \"INTEGRATION\":\n return <Zap {...iconProps} />;\n default:\n return <Shield {...iconProps} />;\n }\n};\n\n/**\n * Delegation status badge with color coding\n */\nconst DelegationStatusBadge: FC<{ status: DelegationStatus }> = ({\n status,\n}) => {\n const styles: Record<DelegationStatus, string> = {\n ACTIVE: \"bg-status-success-bg-subtle text-status-success-text\",\n REVOKED: \"bg-status-error-bg-subtle text-status-error-text\",\n EXPIRED: \"bg-bg-sunken text-text-primary\",\n };\n\n return (\n <span\n className={cn(\n \"inline-flex items-center rounded-full px-2.5 py-0.5 text-xs font-medium\",\n styles[status],\n )}\n >\n {status}\n </span>\n );\n};\n\n/**\n * Success toast that auto-dismisses\n */\nconst SuccessToast: FC<{ message: string; onDismiss: () => void }> = ({\n message,\n onDismiss,\n}) => {\n useEffect(() => {\n const timer = setTimeout(onDismiss, 5000);\n return () => clearTimeout(timer);\n }, [onDismiss]);\n\n return (\n <div className=\"rounded-lg border border-border-subtle bg-status-success-bg-subtle p-4\">\n <div className=\"flex items-center gap-3\">\n <Shield className=\"h-5 w-5 text-status-success-text\" />\n <p className=\"text-sm font-medium text-status-success-text\">\n {message}\n </p>\n </div>\n </div>\n );\n};\n\n/**\n * Error banner\n */\nconst ErrorBanner: FC<{ message: string; onRetry?: () => void }> = ({\n message,\n onRetry,\n}) => {\n const { t } = useI18n();\n const tr = (key: string, fallback: string): string => {\n const value = t(key);\n return value === key ? fallback : value;\n };\n return (\n <div className=\"rounded-lg border border-status-error-border/50 bg-status-error-bg/10 p-4\">\n <div className=\"flex items-center gap-3\">\n <AlertCircle className=\"h-5 w-5 text-status-error-text\" />\n <p className=\"flex-1 text-sm text-status-error-text\">{message}</p>\n {onRetry && (\n <button\n onClick={onRetry}\n className=\"rounded-md border px-3 py-1.5 text-xs font-medium hover:bg-bg-sunken transition-colors\"\n >\n {tr(\"devportal.retry\", \"Retry\")}\n </button>\n )}\n </div>\n </div>\n );\n};\n\n/**\n * Single delegation card for \"Apps Acting as Me\" tab\n */\nconst DelegationCard: FC<{\n delegation: AppUserDelegation;\n onRevoke: (delegation: AppUserDelegation) => void;\n isRevoking: boolean;\n selected: boolean;\n onToggleSelect: (checked: boolean) => void;\n canRevoke: boolean;\n}> = ({\n delegation,\n onRevoke,\n isRevoking,\n selected,\n onToggleSelect,\n canRevoke,\n}) => {\n const { t } = useI18n();\n const tr = (key: string, fallback: string): string => {\n const value = t(key);\n return value === key ? fallback : value;\n };\n return (\n <div\n className={cn(\n \"rounded-card border border-border-seam bg-bg-surface p-5 transition-colors hover:border-border-subtle/80\",\n selected && \"border-primary bg-action-primary-bg/5\",\n delegation.status !== \"ACTIVE\" && \"opacity-60\",\n )}\n >\n <div className=\"flex items-start justify-between gap-4\">\n {/* Left: App info */}\n <div className=\"flex items-start gap-4 min-w-0\">\n {delegation.status === \"ACTIVE\" && (\n <div onClick={(event) => event.stopPropagation()}>\n <SelectionCheckbox\n checked={selected}\n onChange={onToggleSelect}\n ariaLabel={tr(\n \"devportal.selectDelegationAriaLabel\",\n `Select delegation ${delegation.app?.name ?? delegation.id}`,\n )}\n className=\"mt-1\"\n />\n </div>\n )}\n <div\n className={cn(\n \"flex h-12 w-12 shrink-0 items-center justify-center rounded-lg\",\n delegation.status === \"ACTIVE\"\n ? \"bg-action-primary-bg/10 text-primary\"\n : \"bg-bg-sunken text-text-secondary\",\n )}\n >\n <AppTypeIcon\n type={delegation.app?.type ?? \"SERVICE\"}\n className=\"h-6 w-6\"\n />\n </div>\n\n <div className=\"min-w-0 space-y-2\">\n {/* Name + status */}\n <div className=\"flex items-center gap-2\">\n <h3 className=\"truncate font-semibold\">\n {delegation.app?.name ??\n tr(\"devportal.unknownApp\", \"Unknown App\")}\n </h3>\n <DelegationStatusBadge status={delegation.status} />\n </div>\n\n {/* Description */}\n {delegation.app?.description && (\n <p className=\"text-sm text-text-secondary line-clamp-2\">\n {delegation.app.description}\n </p>\n )}\n\n {/* Metadata row */}\n <div className=\"flex flex-wrap items-center gap-x-4 gap-y-1 text-sm text-text-secondary\">\n <span className=\"flex items-center gap-1\">\n <Clock className=\"h-3.5 w-3.5\" />\n {tr(\"devportal.granted\", \"Granted\")}{\" \"}\n {formatRelativeTime(delegation.grantedAt)}\n </span>\n {delegation.expiresAt && (\n <span className=\"flex items-center gap-1\">\n <AlertTriangle className=\"h-3.5 w-3.5\" />\n {tr(\"devportal.expires\", \"Expires\")}{\" \"}\n {formatExpirationTime(delegation.expiresAt)}\n </span>\n )}\n <span\n className={cn(\n \"rounded-md px-2 py-0.5 text-xs font-mono\",\n \"bg-bg-sunken text-text-secondary\",\n )}\n >\n {delegation.app?.type ?? \"UNKNOWN\"}\n </span>\n </div>\n\n {/* Scopes */}\n {delegation.scopes.length > 0 && (\n <div>\n <p className=\"mb-1.5 text-xs text-text-secondary\">\n {tr(\"devportal.permissions\", \"Permissions:\")}\n </p>\n <div className=\"flex flex-wrap gap-1.5\">\n {delegation.scopes.map((scope) => (\n <span\n key={scope}\n className=\"rounded-md bg-status-info-bg-subtle px-2 py-0.5 text-xs text-status-info-text\"\n >\n {scope}\n </span>\n ))}\n </div>\n </div>\n )}\n\n {/* Workspace count */}\n {delegation.workspaceIds.length > 0 && (\n <p className=\"flex items-center gap-1 text-xs text-text-secondary\">\n <Users className=\"h-3.5 w-3.5\" />\n {tr(\"devportal.accessToWorkspaces\", \"Access to\")}{\" \"}\n {delegation.workspaceIds.length}{\" \"}\n {tr(\"devportal.workspace\", \"workspace\")}\n {delegation.workspaceIds.length !== 1\n ? tr(\"devportal.workspacePluralSuffix\", \"s\")\n : \"\"}\n </p>\n )}\n </div>\n </div>\n\n {/* Right: Revoke button (only for active delegations) */}\n {delegation.status === \"ACTIVE\" && (\n <button\n onClick={() => onRevoke(delegation)}\n disabled={isRevoking || !canRevoke}\n aria-disabled={!canRevoke}\n className=\"inline-flex shrink-0 items-center gap-2 rounded-md border border-border-subtle px-4 py-2 text-sm font-medium text-status-error-text transition-colors hover:bg-status-error-bg-subtle disabled:cursor-not-allowed disabled:opacity-50\"\n >\n {isRevoking ? (\n <Loader2 className=\"h-4 w-4 animate-spin\" />\n ) : (\n <XCircle className=\"h-4 w-4\" />\n )}\n {tr(\"devportal.revoke\", \"Revoke\")}\n </button>\n )}\n </div>\n </div>\n );\n};\n\n/**\n * Single delegation card for \"Delegations I Granted\" tab\n */\nconst GrantedDelegationCard: FC<{\n delegation: AppUserDelegation;\n onRevoke: (delegation: AppUserDelegation) => void;\n isRevoking: boolean;\n selected: boolean;\n onToggleSelect: (checked: boolean) => void;\n canRevoke: boolean;\n}> = ({\n delegation,\n onRevoke,\n isRevoking,\n selected,\n onToggleSelect,\n canRevoke,\n}) => {\n const { t } = useI18n();\n const tr = (key: string, fallback: string): string => {\n const value = t(key);\n return value === key ? fallback : value;\n };\n return (\n <div\n className={cn(\n \"rounded-card border border-border-seam bg-bg-surface p-5 transition-colors hover:border-border-subtle/80\",\n selected && \"border-primary bg-action-primary-bg/5\",\n )}\n >\n <div className=\"flex items-start justify-between gap-4\">\n {/* Left: App info */}\n <div className=\"flex items-start gap-4 min-w-0\">\n {delegation.status === \"ACTIVE\" && (\n <div onClick={(event) => event.stopPropagation()}>\n <SelectionCheckbox\n checked={selected}\n onChange={onToggleSelect}\n ariaLabel={tr(\n \"devportal.selectDelegationAriaLabel\",\n `Select delegation ${delegation.app?.name ?? delegation.id}`,\n )}\n className=\"mt-1\"\n />\n </div>\n )}\n <div className=\"flex h-12 w-12 shrink-0 items-center justify-center rounded-lg bg-accent-purple-subtle text-accent-purple\">\n <AppTypeIcon\n type={delegation.app?.type ?? \"SERVICE\"}\n className=\"h-6 w-6\"\n />\n </div>\n\n <div className=\"min-w-0 space-y-2\">\n {/* Name + status */}\n <div className=\"flex items-center gap-2\">\n <h3 className=\"truncate font-semibold\">\n {delegation.app?.name ??\n tr(\"devportal.unknownApp\", \"Unknown App\")}\n </h3>\n <DelegationStatusBadge status={delegation.status} />\n </div>\n\n {/* Target user */}\n <p className=\"text-sm text-text-secondary\">\n {tr(\"devportal.grantedToUser\", \"Granted to user:\")}{\" \"}\n <code className=\"rounded bg-bg-sunken px-1.5 py-0.5 text-xs\">\n {delegation.userId}\n </code>\n </p>\n\n {/* Time */}\n <div className=\"flex flex-wrap items-center gap-x-4 gap-y-1 text-sm text-text-secondary\">\n <span className=\"flex items-center gap-1\">\n <Clock className=\"h-3.5 w-3.5\" />\n {tr(\"devportal.granted\", \"Granted\")}{\" \"}\n {formatRelativeTime(delegation.grantedAt)}\n </span>\n {delegation.expiresAt && (\n <span className=\"flex items-center gap-1\">\n <AlertTriangle className=\"h-3.5 w-3.5\" />\n {tr(\"devportal.expires\", \"Expires\")}{\" \"}\n {new Date(delegation.expiresAt).toLocaleDateString()}\n </span>\n )}\n </div>\n\n {/* Scopes */}\n {delegation.scopes.length > 0 && (\n <div>\n <p className=\"mb-1.5 text-xs text-text-secondary\">\n {tr(\"devportal.permissionsGranted\", \"Permissions granted:\")}\n </p>\n <div className=\"flex flex-wrap gap-1.5\">\n {delegation.scopes.map((scope) => (\n <span\n key={scope}\n className=\"rounded-md bg-accent-purple-subtle px-2 py-0.5 text-xs text-accent-purple\"\n >\n {scope}\n </span>\n ))}\n </div>\n </div>\n )}\n </div>\n </div>\n\n {/* Right: Revoke button */}\n {delegation.status === \"ACTIVE\" && (\n <button\n onClick={() => onRevoke(delegation)}\n disabled={isRevoking || !canRevoke}\n aria-disabled={!canRevoke}\n className=\"inline-flex shrink-0 items-center gap-2 rounded-md border border-border-subtle px-4 py-2 text-sm font-medium text-status-error-text transition-colors hover:bg-status-error-bg-subtle disabled:cursor-not-allowed disabled:opacity-50\"\n >\n {isRevoking ? (\n <Loader2 className=\"h-4 w-4 animate-spin\" />\n ) : (\n <XCircle className=\"h-4 w-4\" />\n )}\n {tr(\"devportal.revoke\", \"Revoke\")}\n </button>\n )}\n </div>\n </div>\n );\n};\n\n/**\n * Revocation history section (dimmed, for revoked/expired delegations)\n */\nconst RevocationHistory: FC<{ delegations: AppUserDelegation[] }> = ({\n delegations,\n}) => {\n const { t } = useI18n();\n const tr = (key: string, fallback: string): string => {\n const value = t(key);\n return value === key ? fallback : value;\n };\n\n if (delegations.length === 0) return null;\n\n return (\n <div className=\"space-y-4\">\n <h2 className=\"text-lg font-semibold\">\n {tr(\"devportal.revocationHistory\", \"Revocation History\")}\n </h2>\n <div className=\"space-y-3 opacity-60\">\n {delegations.map((delegation) => (\n <div\n key={delegation.id}\n className=\"flex items-center justify-between rounded-lg border bg-bg-sunken/30 p-4\"\n >\n <div className=\"flex items-center gap-3\">\n <div className=\"flex h-10 w-10 items-center justify-center rounded-lg bg-bg-sunken text-text-secondary\">\n <AppTypeIcon\n type={delegation.app?.type ?? \"SERVICE\"}\n className=\"h-5 w-5\"\n />\n </div>\n <div>\n <h3 className=\"font-medium\">\n {delegation.app?.name ??\n tr(\"devportal.unknownApp\", \"Unknown App\")}\n </h3>\n <p className=\"text-sm text-text-secondary\">\n {delegation.status === \"REVOKED\"\n ? tr(\"devportal.revoked\", \"Revoked\")\n : tr(\"devportal.expired\", \"Expired\")}{\" \"}\n {delegation.revokedAt\n ? formatRelativeTime(delegation.revokedAt)\n : \"\"}\n </p>\n </div>\n </div>\n <DelegationStatusBadge status={delegation.status} />\n </div>\n ))}\n </div>\n </div>\n );\n};\n\n// =============================================================================\n// MAIN COMPONENT\n// =============================================================================\n\n/**\n * Delegations management page for the Developer Portal.\n *\n * Consolidates delegation management previously at /auth/delegations in\n * microfe-auth. Shows two tabs:\n * - \"Apps Acting as Me\": delegations where the current user is the subject\n * - \"Delegations I Granted\": delegations the current user granted to others\n */\nexport const DelegationsPage: FC = () => {\n const { t } = useI18n();\n const tr = (key: string, fallback: string): string => {\n const value = t(key);\n return value === key ? fallback : value;\n };\n const { allowed: canRevoke } = useDevportalPermission(\n DEVPORTAL_PERMISSIONS.DELEGATION_REVOKE,\n );\n // ---------------------------------------------------------------------------\n // State\n // ---------------------------------------------------------------------------\n const [activeTab, setActiveTab] = useState<ActiveTab>(\"acting-as-me\");\n const [revokeTargets, setRevokeTargets] = useState<AppUserDelegation[]>([]);\n const [successMessage, setSuccessMessage] = useState<string | null>(null);\n const [selectedDelegationIds, setSelectedDelegationIds] = useState<\n Set<string>\n >(new Set());\n\n // ---------------------------------------------------------------------------\n // Queries\n // ---------------------------------------------------------------------------\n const {\n data: delegationsData,\n loading: delegationsLoading,\n error: delegationsError,\n refetch: refetchDelegations,\n } = useCurrentUserDelegationsQuery({\n fetchPolicy: \"cache-and-network\",\n });\n\n const {\n data: grantedData,\n loading: grantedLoading,\n error: grantedError,\n refetch: refetchGranted,\n } = useCurrentUserGrantedDelegationsQuery({\n fetchPolicy: \"cache-and-network\",\n });\n\n // ---------------------------------------------------------------------------\n // Mutation\n // ---------------------------------------------------------------------------\n const [revokeDelegationMutation, { loading: revokeLoading }] =\n useRevokeDelegationMutation();\n\n // ---------------------------------------------------------------------------\n // Derived data\n // ---------------------------------------------------------------------------\n const delegations: AppUserDelegation[] = (\n delegationsData?.currentUserDelegations?.delegations ?? []\n ).map(mapDelegation);\n\n const grantedDelegations: AppUserDelegation[] = (\n grantedData?.currentUserGrantedDelegations?.delegations ?? []\n ).map(mapDelegation);\n\n const activeDelegations = delegations.filter((d) => d.status === \"ACTIVE\");\n const inactiveDelegations = delegations.filter((d) => d.status !== \"ACTIVE\");\n const activeGrantedDelegations = grantedDelegations.filter(\n (d) => d.status === \"ACTIVE\",\n );\n const visibleActiveDelegations =\n activeTab === \"acting-as-me\" ? activeDelegations : activeGrantedDelegations;\n const selectedActiveDelegations = useMemo(\n () =>\n visibleActiveDelegations.filter((delegation) =>\n selectedDelegationIds.has(delegation.id),\n ),\n [selectedDelegationIds, visibleActiveDelegations],\n );\n const allVisibleSelected =\n visibleActiveDelegations.length > 0 &&\n visibleActiveDelegations.every((delegation) =>\n selectedDelegationIds.has(delegation.id),\n );\n const partiallySelected =\n visibleActiveDelegations.some((delegation) =>\n selectedDelegationIds.has(delegation.id),\n ) && !allVisibleSelected;\n\n const isLoading =\n activeTab === \"acting-as-me\" ? delegationsLoading : grantedLoading;\n const queryError =\n activeTab === \"acting-as-me\" ? delegationsError : grantedError;\n\n // ---------------------------------------------------------------------------\n // Handlers\n // ---------------------------------------------------------------------------\n const handleRevokeClick = useCallback((delegation: AppUserDelegation) => {\n setRevokeTargets([delegation]);\n }, []);\n\n const toggleDelegationSelection = useCallback(\n (delegationId: string, checked: boolean) => {\n setSelectedDelegationIds((prev) => {\n const next = new Set(prev);\n if (checked) {\n next.add(delegationId);\n } else {\n next.delete(delegationId);\n }\n return next;\n });\n },\n [],\n );\n\n const toggleAllVisibleDelegations = useCallback(\n (checked: boolean) => {\n setSelectedDelegationIds((prev) => {\n const next = new Set(prev);\n visibleActiveDelegations.forEach((delegation) => {\n if (checked) {\n next.add(delegation.id);\n } else {\n next.delete(delegation.id);\n }\n });\n return next;\n });\n },\n [visibleActiveDelegations],\n );\n\n const clearSelection = useCallback(() => {\n setSelectedDelegationIds(new Set());\n }, []);\n\n const handleBulkRevokeClick = useCallback(() => {\n if (selectedActiveDelegations.length === 0) return;\n setRevokeTargets(selectedActiveDelegations);\n }, [selectedActiveDelegations]);\n\n const handleRevokeConfirm = useCallback(async () => {\n if (revokeTargets.length === 0) return;\n try {\n const results = await Promise.allSettled(\n revokeTargets.map((delegation) =>\n revokeDelegationMutation({\n variables: {\n id: delegation.id,\n reason: tr(\n \"devportal.userRevokedAccessReason\",\n \"User revoked access via Developer Portal\",\n ),\n },\n }),\n ),\n );\n const succeeded = results.filter(\n (result: PromiseSettledResult<unknown>) =>\n result.status === \"fulfilled\",\n ).length;\n const failed = results.length - succeeded;\n setSuccessMessage(\n `Revoked ${succeeded} delegation${succeeded === 1 ? \"\" : \"s\"}${failed > 0 ? ` • ${failed} failed` : \"\"}`,\n );\n setSelectedDelegationIds((prev) => {\n const next = new Set(prev);\n revokeTargets.forEach((delegation) => next.delete(delegation.id));\n return next;\n });\n setRevokeTargets([]);\n // Refetch both queries to keep data fresh\n void refetchDelegations();\n void refetchGranted();\n } catch {\n // Error is available via the mutation hook's error state;\n // the ConfirmDialog will close and we show the error via queryError fallback\n setRevokeTargets([]);\n }\n }, [\n revokeTargets,\n revokeDelegationMutation,\n refetchDelegations,\n refetchGranted,\n ]);\n\n const handleDismissSuccess = useCallback(() => {\n setSuccessMessage(null);\n }, []);\n\n // ---------------------------------------------------------------------------\n // Summary stats for header\n // ---------------------------------------------------------------------------\n const totalActive = activeDelegations.length;\n const totalGranted = activeGrantedDelegations.length;\n\n // ---------------------------------------------------------------------------\n // Render\n // ---------------------------------------------------------------------------\n return (\n <PermissionGuard permission={DEVPORTAL_PERMISSIONS.INSTALLATION_READ}>\n <PageLayout\n title={tr(\"devportal.appDelegationsTitle\", \"App Delegations\")}\n description={tr(\n \"devportal.appDelegationsDescription\",\n \"Manage which apps can act on your behalf. Revoke access at any time.\",\n )}\n >\n {/* Success message */}\n {successMessage && (\n <SuccessToast\n message={successMessage}\n onDismiss={handleDismissSuccess}\n />\n )}\n\n {/* Error message */}\n {queryError && (\n <ErrorBanner\n message={\n queryError.message ||\n tr(\n \"devportal.failedToLoadDelegations\",\n \"Failed to load delegations\",\n )\n }\n onRetry={() => {\n void refetchDelegations();\n void refetchGranted();\n }}\n />\n )}\n\n {/* Summary stats — primary emphasis zone */}\n <EmphasisPanel>\n <div className=\"grid grid-cols-2 gap-4 sm:grid-cols-4\">\n <GlassCard treatment=\"metric\" className=\"p-4\">\n <div className=\"flex items-center gap-2 text-text-secondary\">\n <Shield className=\"h-4 w-4\" />\n <span className=\"text-sm\">\n {tr(\"devportal.activeDelegations\", \"Active Delegations\")}\n </span>\n </div>\n <p className=\"mt-1 text-2xl font-semibold tabular-nums\">\n {totalActive}\n </p>\n </GlassCard>\n <GlassCard treatment=\"metric\" className=\"p-4\">\n <div className=\"flex items-center gap-2 text-text-secondary\">\n <Users className=\"h-4 w-4\" />\n <span className=\"text-sm\">\n {tr(\"devportal.grantedByMe\", \"Granted by Me\")}\n </span>\n </div>\n <p className=\"mt-1 text-2xl font-semibold tabular-nums\">\n {totalGranted}\n </p>\n </GlassCard>\n <GlassCard treatment=\"metric\" className=\"p-4\">\n <div className=\"flex items-center gap-2 text-status-warning-text\">\n <AlertTriangle className=\"h-4 w-4\" />\n <span className=\"text-sm\">\n {tr(\"devportal.revokedExpired\", \"Revoked / Expired\")}\n </span>\n </div>\n <p className=\"mt-1 text-2xl font-semibold tabular-nums\">\n {inactiveDelegations.length}\n </p>\n </GlassCard>\n <GlassCard treatment=\"metric\" className=\"p-4\">\n <div className=\"flex items-center gap-2 text-text-secondary\">\n <Clock className=\"h-4 w-4\" />\n <span className=\"text-sm\">\n {tr(\"devportal.totalAllTime\", \"Total All-Time\")}\n </span>\n </div>\n <p className=\"mt-1 text-2xl font-semibold tabular-nums\">\n {delegations.length}\n </p>\n </GlassCard>\n </div>\n </EmphasisPanel>\n\n {/* Tabs */}\n <div className=\"border-b\">\n <div\n className=\"flex overflow-x-auto\"\n role=\"tablist\"\n aria-label={tr(\n \"devportal.delegationSections\",\n \"Delegation sections\",\n )}\n >\n <button\n id=\"delegations-tab-acting-as-me\"\n role=\"tab\"\n aria-selected={activeTab === \"acting-as-me\"}\n onClick={() => {\n clearSelection();\n setActiveTab(\"acting-as-me\");\n }}\n className={cn(\n \"-mb-px border-b-2 px-4 py-3 text-sm font-medium transition-colors\",\n activeTab === \"acting-as-me\"\n ? \"border-primary text-primary\"\n : \"border-transparent text-text-secondary hover:text-text-primary\",\n )}\n >\n {tr(\"devportal.appsActingAsMe\", \"Apps Acting as Me\")}\n {totalActive > 0 && (\n <span className=\"ml-2 inline-flex items-center justify-center rounded-full bg-action-primary-bg/10 px-2 py-0.5 text-xs font-medium text-primary\">\n {totalActive}\n </span>\n )}\n </button>\n <button\n id=\"delegations-tab-granted-by-me\"\n role=\"tab\"\n aria-selected={activeTab === \"granted-by-me\"}\n onClick={() => {\n clearSelection();\n setActiveTab(\"granted-by-me\");\n }}\n className={cn(\n \"-mb-px border-b-2 px-4 py-3 text-sm font-medium transition-colors\",\n activeTab === \"granted-by-me\"\n ? \"border-primary text-primary\"\n : \"border-transparent text-text-secondary hover:text-text-primary\",\n )}\n >\n {tr(\"devportal.delegationsIGranted\", \"Delegations I Granted\")}\n {totalGranted > 0 && (\n <span className=\"ml-2 inline-flex items-center justify-center rounded-full bg-action-primary-bg/10 px-2 py-0.5 text-xs font-medium text-primary\">\n {totalGranted}\n </span>\n )}\n </button>\n </div>\n </div>\n\n {visibleActiveDelegations.length > 0 && (\n <div className=\"rounded-card border border-border-seam bg-bg-surface p-4\">\n <div className=\"flex flex-col gap-3 lg:flex-row lg:items-center lg:justify-between\">\n <div className=\"flex flex-wrap items-center gap-3\">\n <div className=\"flex items-center gap-2\">\n <SelectionCheckbox\n checked={allVisibleSelected}\n indeterminate={partiallySelected}\n onChange={toggleAllVisibleDelegations}\n ariaLabel={tr(\n \"devportal.selectAllVisibleDelegations\",\n \"Select all visible delegations\",\n )}\n />\n <span className=\"text-sm font-medium\">\n {tr(\"devportal.selectAllVisible\", \"Select all visible\")}\n </span>\n </div>\n {selectedDelegationIds.size > 0 && (\n <button\n onClick={clearSelection}\n className=\"text-sm text-text-secondary hover:text-text-primary\"\n >\n {tr(\"devportal.clearSelection\", \"Clear selection\")}\n </button>\n )}\n </div>\n\n <div className=\"flex flex-wrap items-center gap-2\">\n <span className=\"rounded-full bg-bg-sunken px-3 py-1 text-sm font-medium text-text-secondary\">\n {selectedDelegationIds.size}{\" \"}\n {tr(\"devportal.selected\", \"selected\")}\n </span>\n </div>\n <p className=\"mt-1 text-2xl font-semibold\">{totalActive}</p>\n </div>\n </div>\n )}\n\n {/* Loading */}\n {isLoading && !delegationsData && !grantedData && (\n <LoadingState\n message={tr(\n \"devportal.loadingDelegations\",\n \"Loading delegations...\",\n )}\n />\n )}\n\n {/* ------------------------------------------------------------------- */}\n {/* Tab 1: Apps Acting as Me */}\n {/* ------------------------------------------------------------------- */}\n {!isLoading && activeTab === \"acting-as-me\" && (\n <div\n className=\"space-y-8\"\n id=\"delegations-panel-acting-as-me\"\n role=\"tabpanel\"\n aria-labelledby=\"delegations-tab-acting-as-me\"\n tabIndex={0}\n >\n {activeDelegations.length === 0 &&\n inactiveDelegations.length === 0 ? (\n <EmptyState\n icon={<Shield size={32} />}\n title={tr(\n \"devportal.noActiveDelegations\",\n \"No Active Delegations\",\n )}\n description={tr(\n \"devportal.noAppsAuthorizedOnYourBehalf\",\n \"No apps are currently authorized to act on your behalf.\",\n )}\n />\n ) : (\n <>\n {/* Active delegation cards */}\n {activeDelegations.length === 0 ? (\n <EmptyState\n icon={<Shield size={32} />}\n title={tr(\n \"devportal.noActiveDelegations\",\n \"No Active Delegations\",\n )}\n description={tr(\n \"devportal.allPreviousDelegationsRevokedOrExpired\",\n \"All previous delegations have been revoked or expired.\",\n )}\n />\n ) : (\n <div className=\"space-y-4\">\n {activeDelegations.map((delegation) => (\n <DelegationCard\n key={delegation.id}\n delegation={delegation}\n onRevoke={handleRevokeClick}\n isRevoking={\n revokeLoading &&\n revokeTargets.some(\n (item) => item.id === delegation.id,\n )\n }\n selected={selectedDelegationIds.has(delegation.id)}\n onToggleSelect={(checked) =>\n toggleDelegationSelection(delegation.id, checked)\n }\n canRevoke={canRevoke}\n />\n ))}\n </div>\n )}\n\n {/* Revocation history */}\n <RevocationHistory delegations={inactiveDelegations} />\n </>\n )}\n </div>\n )}\n\n {/* ------------------------------------------------------------------- */}\n {/* Tab 2: Delegations I Granted */}\n {/* ------------------------------------------------------------------- */}\n {!isLoading && activeTab === \"granted-by-me\" && (\n <div\n className=\"space-y-4\"\n id=\"delegations-panel-granted-by-me\"\n role=\"tabpanel\"\n aria-labelledby=\"delegations-tab-granted-by-me\"\n tabIndex={0}\n >\n {grantedDelegations.length === 0 ? (\n <EmptyState\n icon={<Users size={32} />}\n title={tr(\n \"devportal.noDelegationsGranted\",\n \"No Delegations Granted\",\n )}\n description={tr(\n \"devportal.noGrantedDelegationsDescription\",\n \"You haven't granted any app delegations to other users.\",\n )}\n />\n ) : (\n grantedDelegations.map((delegation) => (\n <GrantedDelegationCard\n key={delegation.id}\n delegation={delegation}\n onRevoke={handleRevokeClick}\n isRevoking={\n revokeLoading &&\n revokeTargets.some((item) => item.id === delegation.id)\n }\n selected={selectedDelegationIds.has(delegation.id)}\n onToggleSelect={(checked) =>\n toggleDelegationSelection(delegation.id, checked)\n }\n canRevoke={canRevoke}\n />\n ))\n )}\n </div>\n )}\n\n {/* Info card */}\n <div className=\"rounded-lg border border-border-subtle bg-status-info-bg-subtle p-5\">\n <div className=\"flex gap-4\">\n <AlertCircle className=\"h-6 w-6 shrink-0 text-accent-blue\" />\n <div>\n <h3 className=\"font-semibold text-status-info-text\">\n {tr(\"devportal.aboutAppDelegations\", \"About App Delegations\")}\n </h3>\n <p className=\"mt-1 text-sm text-status-info-text\">\n {tr(\n \"devportal.aboutAppDelegationsText\",\n \"App delegations allow automated services, bots, and integrations to perform actions on your behalf. Each delegation specifies which permissions (scopes) and workspaces the app can access. You can revoke access at any time, which immediately prevents the app from acting as you.\",\n )}\n </p>\n </div>\n </div>\n </div>\n\n {/* Revoke confirmation dialog */}\n <ConfirmDialog\n open={revokeTargets.length > 0}\n onOpenChange={(open) => {\n if (!open) setRevokeTargets([]);\n }}\n onConfirm={handleRevokeConfirm}\n title={\n revokeTargets.length > 1\n ? tr(\"devportal.revokeDelegations\", \"Revoke Delegations\")\n : tr(\"devportal.revokeDelegation\", \"Revoke Delegation\")\n }\n description={\n revokeTargets.length > 1\n ? tr(\n \"devportal.revokeDelegationsBulkConfirm\",\n `Are you sure you want to revoke ${revokeTargets.length} selected delegations? Those apps will no longer be able to act on the delegated users' behalf.`,\n )\n : tr(\n \"devportal.revokeDelegationSingleConfirm\",\n `Are you sure you want to revoke access for \"${revokeTargets[0]?.app?.name ?? \"this app\"}\"? This app will no longer be able to act on your behalf.`,\n )\n }\n confirmLabel={tr(\"devportal.revokeAccess\", \"Revoke Access\")}\n cancelLabel={tr(\"devportal.cancel\", \"Cancel\")}\n variant=\"danger\"\n loading={revokeLoading}\n />\n </PageLayout>\n </PermissionGuard>\n );\n};\n"],"mappings":"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"}
1
+ {"version":3,"file":"DelegationsPage.js","names":[],"sources":["../../../src/devportal/pages/DelegationsPage.tsx"],"sourcesContent":["import type { FC } from \"react\";\nimport { useState, useCallback, useEffect, useMemo } from \"react\";\nimport { useI18n } from \"@burdenoff/fe-libs/shared/providers/shell/I18nProvider\";\nimport {\n Shield,\n Users,\n Clock,\n XCircle,\n AlertTriangle,\n Loader2,\n AlertCircle,\n Bot,\n Zap,\n Cog,\n} from \"lucide-react\";\nimport { EmphasisPanel, GlassCard } from \"@burdenoff/fe-libs/ui\";\nimport { PageLayout } from \"../components/PageLayout\";\nimport { EmptyState } from \"../components/EmptyState\";\nimport { LoadingState } from \"../components/LoadingState\";\nimport { ConfirmDialog } from \"../components/ConfirmDialog\";\nimport { SelectionCheckbox } from \"../components/SelectionCheckbox\";\nimport { cn } from \"../utils/cn\";\nimport { PermissionGuard } from \"../components\";\nimport { DEVPORTAL_PERMISSIONS } from \"../constants/permissions\";\nimport { useDevportalPermission } from \"../hooks/usePermission\";\nimport type { AppUserDelegation, DelegationStatus } from \"../types\";\nimport {\n useCurrentUserDelegationsQuery,\n useCurrentUserGrantedDelegationsQuery,\n useRevokeDelegationMutation,\n} from \"../../generated/global-operations\";\n\n// =============================================================================\n// HELPERS\n// =============================================================================\n\ntype ActiveTab = \"acting-as-me\" | \"granted-by-me\";\n\n/**\n * Map a GraphQL delegation fragment to the local AppUserDelegation type\n */\nfunction mapDelegation(d: {\n id: string;\n appId: string;\n userId: string;\n scopes: string[];\n workspaceIds?: string[] | null;\n status: string;\n grantedAt: string;\n grantedBy: string;\n expiresAt?: string | null;\n revokedAt?: string | null;\n revokedBy?: string | null;\n app: {\n id: string;\n name: string;\n description?: string | null;\n type: string;\n status: string;\n };\n}): AppUserDelegation {\n return {\n id: d.id,\n appId: d.appId,\n userId: d.userId,\n scopes: d.scopes,\n workspaceIds: d.workspaceIds ?? [],\n status: d.status as DelegationStatus,\n grantedAt: d.grantedAt,\n grantedBy: d.grantedBy,\n expiresAt: d.expiresAt ?? undefined,\n revokedAt: d.revokedAt ?? undefined,\n revokedBy: d.revokedBy ?? undefined,\n app: {\n id: d.app.id,\n name: d.app.name,\n description: d.app.description ?? undefined,\n type: d.app.type,\n status: d.app.status,\n },\n };\n}\n\n/**\n * Format a date string to relative time (\"2 hours ago\", \"3 days ago\", etc.)\n */\nfunction formatRelativeTime(dateString: string): string {\n const date = new Date(dateString);\n const now = new Date();\n const diffMs = now.getTime() - date.getTime();\n const diffSeconds = Math.floor(diffMs / 1000);\n const diffMinutes = Math.floor(diffSeconds / 60);\n const diffHours = Math.floor(diffMinutes / 60);\n const diffDays = Math.floor(diffHours / 24);\n const diffWeeks = Math.floor(diffDays / 7);\n const diffMonths = Math.floor(diffDays / 30);\n\n if (diffSeconds < 60) return \"just now\";\n if (diffMinutes < 60)\n return `${diffMinutes} minute${diffMinutes !== 1 ? \"s\" : \"\"} ago`;\n if (diffHours < 24)\n return `${diffHours} hour${diffHours !== 1 ? \"s\" : \"\"} ago`;\n if (diffDays < 7) return `${diffDays} day${diffDays !== 1 ? \"s\" : \"\"} ago`;\n if (diffWeeks < 5)\n return `${diffWeeks} week${diffWeeks !== 1 ? \"s\" : \"\"} ago`;\n if (diffMonths < 12)\n return `${diffMonths} month${diffMonths !== 1 ? \"s\" : \"\"} ago`;\n\n return date.toLocaleDateString();\n}\n\n/**\n * Format a future date as relative time (\"in 3 days\", \"in 2 hours\", etc.)\n */\nfunction formatExpirationTime(dateString: string): string {\n const date = new Date(dateString);\n const now = new Date();\n const diffMs = date.getTime() - now.getTime();\n\n if (diffMs <= 0) return \"expired\";\n\n const diffMinutes = Math.floor(diffMs / (1000 * 60));\n const diffHours = Math.floor(diffMs / (1000 * 60 * 60));\n const diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24));\n\n if (diffMinutes < 60)\n return `in ${diffMinutes} minute${diffMinutes !== 1 ? \"s\" : \"\"}`;\n if (diffHours < 24)\n return `in ${diffHours} hour${diffHours !== 1 ? \"s\" : \"\"}`;\n return `in ${diffDays} day${diffDays !== 1 ? \"s\" : \"\"}`;\n}\n\n// =============================================================================\n// SUB-COMPONENTS\n// =============================================================================\n\n/**\n * Icon for app type (SERVICE, BOT, INTEGRATION, etc.)\n */\nconst AppTypeIcon: FC<{ type: string; className?: string }> = ({\n type,\n className,\n}) => {\n const iconProps = { className: cn(\"h-5 w-5\", className) };\n\n switch (type) {\n case \"SERVICE\":\n return <Cog {...iconProps} />;\n case \"BOT\":\n return <Bot {...iconProps} />;\n case \"INTEGRATION\":\n return <Zap {...iconProps} />;\n default:\n return <Shield {...iconProps} />;\n }\n};\n\n/**\n * Delegation status badge with color coding\n */\nconst DelegationStatusBadge: FC<{ status: DelegationStatus }> = ({\n status,\n}) => {\n const styles: Record<DelegationStatus, string> = {\n ACTIVE: \"bg-status-success-bg-subtle text-status-success-text\",\n REVOKED: \"bg-status-error-bg-subtle text-status-error-text\",\n EXPIRED: \"bg-bg-sunken text-text-primary\",\n };\n\n return (\n <span\n className={cn(\n \"inline-flex items-center rounded-full px-2.5 py-0.5 text-xs font-medium\",\n styles[status],\n )}\n >\n {status}\n </span>\n );\n};\n\n/**\n * Success toast that auto-dismisses\n */\nconst SuccessToast: FC<{ message: string; onDismiss: () => void }> = ({\n message,\n onDismiss,\n}) => {\n useEffect(() => {\n const timer = setTimeout(onDismiss, 5000);\n return () => clearTimeout(timer);\n }, [onDismiss]);\n\n return (\n <div className=\"rounded-lg border border-border-subtle bg-status-success-bg-subtle p-4\">\n <div className=\"flex items-center gap-3\">\n <Shield className=\"h-5 w-5 text-status-success-text\" />\n <p className=\"text-sm font-medium text-status-success-text\">\n {message}\n </p>\n </div>\n </div>\n );\n};\n\n/**\n * Error banner\n */\nconst ErrorBanner: FC<{ message: string; onRetry?: () => void }> = ({\n message,\n onRetry,\n}) => {\n const { t } = useI18n();\n const tr = (key: string, fallback: string): string => {\n const value = t(key);\n return value === key ? fallback : value;\n };\n return (\n <div className=\"rounded-lg border border-status-error-border/50 bg-status-error-bg/10 p-4\">\n <div className=\"flex items-center gap-3\">\n <AlertCircle className=\"h-5 w-5 text-status-error-text\" />\n <p className=\"flex-1 text-sm text-status-error-text\">{message}</p>\n {onRetry && (\n <button\n onClick={onRetry}\n className=\"rounded-md border px-3 py-1.5 text-xs font-medium hover:bg-bg-sunken transition-colors\"\n >\n {tr(\"devportal.retry\", \"Retry\")}\n </button>\n )}\n </div>\n </div>\n );\n};\n\n/**\n * Single delegation card for \"Apps Acting as Me\" tab\n */\nconst DelegationCard: FC<{\n delegation: AppUserDelegation;\n onRevoke: (delegation: AppUserDelegation) => void;\n isRevoking: boolean;\n selected: boolean;\n onToggleSelect: (checked: boolean) => void;\n canRevoke: boolean;\n}> = ({\n delegation,\n onRevoke,\n isRevoking,\n selected,\n onToggleSelect,\n canRevoke,\n}) => {\n const { t } = useI18n();\n const tr = (key: string, fallback: string): string => {\n const value = t(key);\n return value === key ? fallback : value;\n };\n return (\n <div\n className={cn(\n \"rounded-card border border-border-seam bg-bg-surface p-5 transition-colors hover:border-border-subtle/80\",\n selected && \"border-primary bg-action-primary-bg/5\",\n delegation.status !== \"ACTIVE\" && \"opacity-60\",\n )}\n >\n <div className=\"flex items-start justify-between gap-4\">\n {/* Left: App info */}\n <div className=\"flex items-start gap-4 min-w-0\">\n {delegation.status === \"ACTIVE\" && (\n <div onClick={(event) => event.stopPropagation()}>\n <SelectionCheckbox\n checked={selected}\n onChange={onToggleSelect}\n ariaLabel={tr(\n \"devportal.selectDelegationAriaLabel\",\n `Select delegation ${delegation.app?.name ?? delegation.id}`,\n )}\n className=\"mt-1\"\n />\n </div>\n )}\n <div\n className={cn(\n \"flex h-12 w-12 shrink-0 items-center justify-center rounded-lg\",\n delegation.status === \"ACTIVE\"\n ? \"bg-action-primary-bg/10 text-primary\"\n : \"bg-bg-sunken text-text-secondary\",\n )}\n >\n <AppTypeIcon\n type={delegation.app?.type ?? \"SERVICE\"}\n className=\"h-6 w-6\"\n />\n </div>\n\n <div className=\"min-w-0 space-y-2\">\n {/* Name + status */}\n <div className=\"flex items-center gap-2\">\n <h3 className=\"truncate font-semibold\">\n {delegation.app?.name ??\n tr(\"devportal.unknownApp\", \"Unknown App\")}\n </h3>\n <DelegationStatusBadge status={delegation.status} />\n </div>\n\n {/* Description */}\n {delegation.app?.description && (\n <p className=\"text-sm text-text-secondary line-clamp-2\">\n {delegation.app.description}\n </p>\n )}\n\n {/* Metadata row */}\n <div className=\"flex flex-wrap items-center gap-x-4 gap-y-1 text-sm text-text-secondary\">\n <span className=\"flex items-center gap-1\">\n <Clock className=\"h-3.5 w-3.5\" />\n {tr(\"devportal.granted\", \"Granted\")}{\" \"}\n {formatRelativeTime(delegation.grantedAt)}\n </span>\n {delegation.expiresAt && (\n <span className=\"flex items-center gap-1\">\n <AlertTriangle className=\"h-3.5 w-3.5\" />\n {tr(\"devportal.expires\", \"Expires\")}{\" \"}\n {formatExpirationTime(delegation.expiresAt)}\n </span>\n )}\n <span\n className={cn(\n \"rounded-md px-2 py-0.5 text-xs font-mono\",\n \"bg-bg-sunken text-text-secondary\",\n )}\n >\n {delegation.app?.type ?? \"UNKNOWN\"}\n </span>\n </div>\n\n {/* Scopes */}\n {delegation.scopes.length > 0 && (\n <div>\n <p className=\"mb-1.5 text-xs text-text-secondary\">\n {tr(\"devportal.permissions\", \"Permissions:\")}\n </p>\n <div className=\"flex flex-wrap gap-1.5\">\n {delegation.scopes.map((scope) => (\n <span\n key={scope}\n className=\"rounded-md bg-status-info-bg-subtle px-2 py-0.5 text-xs text-status-info-text\"\n >\n {scope}\n </span>\n ))}\n </div>\n </div>\n )}\n\n {/* Workspace count */}\n {delegation.workspaceIds.length > 0 && (\n <p className=\"flex items-center gap-1 text-xs text-text-secondary\">\n <Users className=\"h-3.5 w-3.5\" />\n {tr(\"devportal.accessToWorkspaces\", \"Access to\")}{\" \"}\n {delegation.workspaceIds.length}{\" \"}\n {tr(\"devportal.workspace\", \"workspace\")}\n {delegation.workspaceIds.length !== 1\n ? tr(\"devportal.workspacePluralSuffix\", \"s\")\n : \"\"}\n </p>\n )}\n </div>\n </div>\n\n {/* Right: Revoke button (only for active delegations) */}\n {delegation.status === \"ACTIVE\" && (\n <button\n onClick={() => onRevoke(delegation)}\n disabled={isRevoking || !canRevoke}\n aria-disabled={!canRevoke}\n className=\"inline-flex shrink-0 items-center gap-2 rounded-md border border-border-subtle px-4 py-2 text-sm font-medium text-status-error-text transition-colors hover:bg-status-error-bg-subtle disabled:cursor-not-allowed disabled:opacity-50\"\n >\n {isRevoking ? (\n <Loader2 className=\"h-4 w-4 animate-spin\" />\n ) : (\n <XCircle className=\"h-4 w-4\" />\n )}\n {tr(\"devportal.revoke\", \"Revoke\")}\n </button>\n )}\n </div>\n </div>\n );\n};\n\n/**\n * Single delegation card for \"Delegations I Granted\" tab\n */\nconst GrantedDelegationCard: FC<{\n delegation: AppUserDelegation;\n onRevoke: (delegation: AppUserDelegation) => void;\n isRevoking: boolean;\n selected: boolean;\n onToggleSelect: (checked: boolean) => void;\n canRevoke: boolean;\n}> = ({\n delegation,\n onRevoke,\n isRevoking,\n selected,\n onToggleSelect,\n canRevoke,\n}) => {\n const { t } = useI18n();\n const tr = (key: string, fallback: string): string => {\n const value = t(key);\n return value === key ? fallback : value;\n };\n return (\n <div\n className={cn(\n \"rounded-card border border-border-seam bg-bg-surface p-5 transition-colors hover:border-border-subtle/80\",\n selected && \"border-primary bg-action-primary-bg/5\",\n )}\n >\n <div className=\"flex items-start justify-between gap-4\">\n {/* Left: App info */}\n <div className=\"flex items-start gap-4 min-w-0\">\n {delegation.status === \"ACTIVE\" && (\n <div onClick={(event) => event.stopPropagation()}>\n <SelectionCheckbox\n checked={selected}\n onChange={onToggleSelect}\n ariaLabel={tr(\n \"devportal.selectDelegationAriaLabel\",\n `Select delegation ${delegation.app?.name ?? delegation.id}`,\n )}\n className=\"mt-1\"\n />\n </div>\n )}\n <div className=\"flex h-12 w-12 shrink-0 items-center justify-center rounded-lg bg-accent-purple-subtle text-accent-purple\">\n <AppTypeIcon\n type={delegation.app?.type ?? \"SERVICE\"}\n className=\"h-6 w-6\"\n />\n </div>\n\n <div className=\"min-w-0 space-y-2\">\n {/* Name + status */}\n <div className=\"flex items-center gap-2\">\n <h3 className=\"truncate font-semibold\">\n {delegation.app?.name ??\n tr(\"devportal.unknownApp\", \"Unknown App\")}\n </h3>\n <DelegationStatusBadge status={delegation.status} />\n </div>\n\n {/* Target user */}\n <p className=\"text-sm text-text-secondary\">\n {tr(\"devportal.grantedToUser\", \"Granted to user:\")}{\" \"}\n <code className=\"rounded bg-bg-sunken px-1.5 py-0.5 text-xs\">\n {delegation.userId}\n </code>\n </p>\n\n {/* Time */}\n <div className=\"flex flex-wrap items-center gap-x-4 gap-y-1 text-sm text-text-secondary\">\n <span className=\"flex items-center gap-1\">\n <Clock className=\"h-3.5 w-3.5\" />\n {tr(\"devportal.granted\", \"Granted\")}{\" \"}\n {formatRelativeTime(delegation.grantedAt)}\n </span>\n {delegation.expiresAt && (\n <span className=\"flex items-center gap-1\">\n <AlertTriangle className=\"h-3.5 w-3.5\" />\n {tr(\"devportal.expires\", \"Expires\")}{\" \"}\n {new Date(delegation.expiresAt).toLocaleDateString()}\n </span>\n )}\n </div>\n\n {/* Scopes */}\n {delegation.scopes.length > 0 && (\n <div>\n <p className=\"mb-1.5 text-xs text-text-secondary\">\n {tr(\"devportal.permissionsGranted\", \"Permissions granted:\")}\n </p>\n <div className=\"flex flex-wrap gap-1.5\">\n {delegation.scopes.map((scope) => (\n <span\n key={scope}\n className=\"rounded-md bg-accent-purple-subtle px-2 py-0.5 text-xs text-accent-purple\"\n >\n {scope}\n </span>\n ))}\n </div>\n </div>\n )}\n </div>\n </div>\n\n {/* Right: Revoke button */}\n {delegation.status === \"ACTIVE\" && (\n <button\n onClick={() => onRevoke(delegation)}\n disabled={isRevoking || !canRevoke}\n aria-disabled={!canRevoke}\n className=\"inline-flex shrink-0 items-center gap-2 rounded-md border border-border-subtle px-4 py-2 text-sm font-medium text-status-error-text transition-colors hover:bg-status-error-bg-subtle disabled:cursor-not-allowed disabled:opacity-50\"\n >\n {isRevoking ? (\n <Loader2 className=\"h-4 w-4 animate-spin\" />\n ) : (\n <XCircle className=\"h-4 w-4\" />\n )}\n {tr(\"devportal.revoke\", \"Revoke\")}\n </button>\n )}\n </div>\n </div>\n );\n};\n\n/**\n * Revocation history section (dimmed, for revoked/expired delegations)\n */\nconst RevocationHistory: FC<{ delegations: AppUserDelegation[] }> = ({\n delegations,\n}) => {\n const { t } = useI18n();\n const tr = (key: string, fallback: string): string => {\n const value = t(key);\n return value === key ? fallback : value;\n };\n\n if (delegations.length === 0) return null;\n\n return (\n <div className=\"space-y-4\">\n <h2 className=\"text-lg font-semibold\">\n {tr(\"devportal.revocationHistory\", \"Revocation History\")}\n </h2>\n <div className=\"space-y-3 opacity-60\">\n {delegations.map((delegation) => (\n <div\n key={delegation.id}\n className=\"flex items-center justify-between rounded-lg border bg-bg-sunken/30 p-4\"\n >\n <div className=\"flex items-center gap-3\">\n <div className=\"flex h-10 w-10 items-center justify-center rounded-lg bg-bg-sunken text-text-secondary\">\n <AppTypeIcon\n type={delegation.app?.type ?? \"SERVICE\"}\n className=\"h-5 w-5\"\n />\n </div>\n <div>\n <h3 className=\"font-medium\">\n {delegation.app?.name ??\n tr(\"devportal.unknownApp\", \"Unknown App\")}\n </h3>\n <p className=\"text-sm text-text-secondary\">\n {delegation.status === \"REVOKED\"\n ? tr(\"devportal.revoked\", \"Revoked\")\n : tr(\"devportal.expired\", \"Expired\")}{\" \"}\n {delegation.revokedAt\n ? formatRelativeTime(delegation.revokedAt)\n : \"\"}\n </p>\n </div>\n </div>\n <DelegationStatusBadge status={delegation.status} />\n </div>\n ))}\n </div>\n </div>\n );\n};\n\n// =============================================================================\n// MAIN COMPONENT\n// =============================================================================\n\n/**\n * Delegations management page for the Developer Portal.\n *\n * Consolidates delegation management previously at /auth/delegations in\n * microfe-auth. Shows two tabs:\n * - \"Apps Acting as Me\": delegations where the current user is the subject\n * - \"Delegations I Granted\": delegations the current user granted to others\n */\nexport const DelegationsPage: FC = () => {\n const { t } = useI18n();\n const tr = (key: string, fallback: string): string => {\n const value = t(key);\n return value === key ? fallback : value;\n };\n const { allowed: canRevoke } = useDevportalPermission(\n DEVPORTAL_PERMISSIONS.DELEGATION_REVOKE,\n );\n // ---------------------------------------------------------------------------\n // State\n // ---------------------------------------------------------------------------\n const [activeTab, setActiveTab] = useState<ActiveTab>(\"acting-as-me\");\n const [revokeTargets, setRevokeTargets] = useState<AppUserDelegation[]>([]);\n const [successMessage, setSuccessMessage] = useState<string | null>(null);\n const [selectedDelegationIds, setSelectedDelegationIds] = useState<\n Set<string>\n >(new Set());\n\n // ---------------------------------------------------------------------------\n // Queries\n // ---------------------------------------------------------------------------\n const {\n data: delegationsData,\n loading: delegationsLoading,\n error: delegationsError,\n refetch: refetchDelegations,\n } = useCurrentUserDelegationsQuery({\n fetchPolicy: \"cache-and-network\",\n });\n\n const {\n data: grantedData,\n loading: grantedLoading,\n error: grantedError,\n refetch: refetchGranted,\n } = useCurrentUserGrantedDelegationsQuery({\n fetchPolicy: \"cache-and-network\",\n });\n\n // ---------------------------------------------------------------------------\n // Mutation\n // ---------------------------------------------------------------------------\n const [revokeDelegationMutation, { loading: revokeLoading }] =\n useRevokeDelegationMutation();\n\n // ---------------------------------------------------------------------------\n // Derived data\n // ---------------------------------------------------------------------------\n const delegations: AppUserDelegation[] = (\n delegationsData?.currentUserDelegations?.delegations ?? []\n ).map(mapDelegation);\n\n const grantedDelegations: AppUserDelegation[] = (\n grantedData?.currentUserGrantedDelegations?.delegations ?? []\n ).map(mapDelegation);\n\n const activeDelegations = delegations.filter((d) => d.status === \"ACTIVE\");\n const inactiveDelegations = delegations.filter((d) => d.status !== \"ACTIVE\");\n const activeGrantedDelegations = grantedDelegations.filter(\n (d) => d.status === \"ACTIVE\",\n );\n const visibleActiveDelegations =\n activeTab === \"acting-as-me\" ? activeDelegations : activeGrantedDelegations;\n const selectedActiveDelegations = useMemo(\n () =>\n visibleActiveDelegations.filter((delegation) =>\n selectedDelegationIds.has(delegation.id),\n ),\n [selectedDelegationIds, visibleActiveDelegations],\n );\n const allVisibleSelected =\n visibleActiveDelegations.length > 0 &&\n visibleActiveDelegations.every((delegation) =>\n selectedDelegationIds.has(delegation.id),\n );\n const partiallySelected =\n visibleActiveDelegations.some((delegation) =>\n selectedDelegationIds.has(delegation.id),\n ) && !allVisibleSelected;\n\n const isLoading =\n activeTab === \"acting-as-me\" ? delegationsLoading : grantedLoading;\n const queryError =\n activeTab === \"acting-as-me\" ? delegationsError : grantedError;\n\n // ---------------------------------------------------------------------------\n // Handlers\n // ---------------------------------------------------------------------------\n const handleRevokeClick = useCallback((delegation: AppUserDelegation) => {\n setRevokeTargets([delegation]);\n }, []);\n\n const toggleDelegationSelection = useCallback(\n (delegationId: string, checked: boolean) => {\n setSelectedDelegationIds((prev) => {\n const next = new Set(prev);\n if (checked) {\n next.add(delegationId);\n } else {\n next.delete(delegationId);\n }\n return next;\n });\n },\n [],\n );\n\n const toggleAllVisibleDelegations = useCallback(\n (checked: boolean) => {\n setSelectedDelegationIds((prev) => {\n const next = new Set(prev);\n visibleActiveDelegations.forEach((delegation) => {\n if (checked) {\n next.add(delegation.id);\n } else {\n next.delete(delegation.id);\n }\n });\n return next;\n });\n },\n [visibleActiveDelegations],\n );\n\n const clearSelection = useCallback(() => {\n setSelectedDelegationIds(new Set());\n }, []);\n\n const handleBulkRevokeClick = useCallback(() => {\n if (selectedActiveDelegations.length === 0) return;\n setRevokeTargets(selectedActiveDelegations);\n }, [selectedActiveDelegations]);\n\n const handleRevokeConfirm = useCallback(async () => {\n if (revokeTargets.length === 0) return;\n try {\n const results = await Promise.allSettled(\n revokeTargets.map((delegation) =>\n revokeDelegationMutation({\n variables: {\n id: delegation.id,\n reason: tr(\n \"devportal.userRevokedAccessReason\",\n \"User revoked access via Developer Portal\",\n ),\n },\n }),\n ),\n );\n const succeeded = results.filter(\n (result: PromiseSettledResult<unknown>) =>\n result.status === \"fulfilled\",\n ).length;\n const failed = results.length - succeeded;\n setSuccessMessage(\n `Revoked ${succeeded} delegation${succeeded === 1 ? \"\" : \"s\"}${failed > 0 ? ` • ${failed} failed` : \"\"}`,\n );\n setSelectedDelegationIds((prev) => {\n const next = new Set(prev);\n revokeTargets.forEach((delegation) => next.delete(delegation.id));\n return next;\n });\n setRevokeTargets([]);\n // Refetch both queries to keep data fresh\n void refetchDelegations();\n void refetchGranted();\n } catch {\n // Error is available via the mutation hook's error state;\n // the ConfirmDialog will close and we show the error via queryError fallback\n setRevokeTargets([]);\n }\n }, [\n revokeTargets,\n revokeDelegationMutation,\n refetchDelegations,\n refetchGranted,\n ]);\n\n const handleDismissSuccess = useCallback(() => {\n setSuccessMessage(null);\n }, []);\n\n // ---------------------------------------------------------------------------\n // Summary stats for header\n // ---------------------------------------------------------------------------\n const totalActive = activeDelegations.length;\n const totalGranted = activeGrantedDelegations.length;\n\n // ---------------------------------------------------------------------------\n // Render\n // ---------------------------------------------------------------------------\n return (\n <PermissionGuard permission={DEVPORTAL_PERMISSIONS.INSTALLATION_READ}>\n <PageLayout\n title={tr(\"devportal.appDelegationsTitle\", \"App Delegations\")}\n description={tr(\n \"devportal.appDelegationsDescription\",\n \"Manage which apps can act on your behalf. Revoke access at any time.\",\n )}\n >\n {/* Success message */}\n {successMessage && (\n <SuccessToast\n message={successMessage}\n onDismiss={handleDismissSuccess}\n />\n )}\n\n {/* Error message */}\n {queryError && (\n <ErrorBanner\n message={\n queryError.message ||\n tr(\n \"devportal.failedToLoadDelegations\",\n \"Failed to load delegations\",\n )\n }\n onRetry={() => {\n void refetchDelegations();\n void refetchGranted();\n }}\n />\n )}\n\n {/* Summary stats — primary emphasis zone */}\n <EmphasisPanel>\n <div className=\"grid grid-cols-2 gap-4 sm:grid-cols-4\">\n <GlassCard treatment=\"metric\" className=\"p-4\">\n <div className=\"flex items-center gap-2 text-text-secondary\">\n <Shield className=\"h-4 w-4\" />\n <span className=\"text-sm\">\n {tr(\"devportal.activeDelegations\", \"Active Delegations\")}\n </span>\n </div>\n <p className=\"mt-1 text-2xl font-semibold tabular-nums\">\n {totalActive}\n </p>\n </GlassCard>\n <GlassCard treatment=\"metric\" className=\"p-4\">\n <div className=\"flex items-center gap-2 text-text-secondary\">\n <Users className=\"h-4 w-4\" />\n <span className=\"text-sm\">\n {tr(\"devportal.grantedByMe\", \"Granted by Me\")}\n </span>\n </div>\n <p className=\"mt-1 text-2xl font-semibold tabular-nums\">\n {totalGranted}\n </p>\n </GlassCard>\n <GlassCard treatment=\"metric\" className=\"p-4\">\n <div className=\"flex items-center gap-2 text-status-warning-text\">\n <AlertTriangle className=\"h-4 w-4\" />\n <span className=\"text-sm\">\n {tr(\"devportal.revokedExpired\", \"Revoked / Expired\")}\n </span>\n </div>\n <p className=\"mt-1 text-2xl font-semibold tabular-nums\">\n {inactiveDelegations.length}\n </p>\n </GlassCard>\n <GlassCard treatment=\"metric\" className=\"p-4\">\n <div className=\"flex items-center gap-2 text-text-secondary\">\n <Clock className=\"h-4 w-4\" />\n <span className=\"text-sm\">\n {tr(\"devportal.totalAllTime\", \"Total All-Time\")}\n </span>\n </div>\n <p className=\"mt-1 text-2xl font-semibold tabular-nums\">\n {delegations.length}\n </p>\n </GlassCard>\n </div>\n </EmphasisPanel>\n\n {/* Tabs */}\n <div className=\"border-b\">\n <div\n className=\"flex overflow-x-auto\"\n role=\"tablist\"\n aria-label={tr(\n \"devportal.delegationSections\",\n \"Delegation sections\",\n )}\n >\n <button\n id=\"delegations-tab-acting-as-me\"\n role=\"tab\"\n aria-selected={activeTab === \"acting-as-me\"}\n onClick={() => {\n clearSelection();\n setActiveTab(\"acting-as-me\");\n }}\n className={cn(\n \"-mb-px border-b-2 px-4 py-3 text-sm font-medium transition-colors\",\n activeTab === \"acting-as-me\"\n ? \"border-primary text-primary\"\n : \"border-transparent text-text-secondary hover:text-text-primary\",\n )}\n >\n {tr(\"devportal.appsActingAsMe\", \"Apps Acting as Me\")}\n {totalActive > 0 && (\n <span className=\"ml-2 inline-flex items-center justify-center rounded-full bg-action-primary-bg/10 px-2 py-0.5 text-xs font-medium text-primary\">\n {totalActive}\n </span>\n )}\n </button>\n <button\n id=\"delegations-tab-granted-by-me\"\n role=\"tab\"\n aria-selected={activeTab === \"granted-by-me\"}\n onClick={() => {\n clearSelection();\n setActiveTab(\"granted-by-me\");\n }}\n className={cn(\n \"-mb-px border-b-2 px-4 py-3 text-sm font-medium transition-colors\",\n activeTab === \"granted-by-me\"\n ? \"border-primary text-primary\"\n : \"border-transparent text-text-secondary hover:text-text-primary\",\n )}\n >\n {tr(\"devportal.delegationsIGranted\", \"Delegations I Granted\")}\n {totalGranted > 0 && (\n <span className=\"ml-2 inline-flex items-center justify-center rounded-full bg-action-primary-bg/10 px-2 py-0.5 text-xs font-medium text-primary\">\n {totalGranted}\n </span>\n )}\n </button>\n </div>\n </div>\n\n {visibleActiveDelegations.length > 0 && (\n <div className=\"rounded-card border border-border-seam bg-bg-surface p-4\">\n <div className=\"flex flex-col gap-3 lg:flex-row lg:items-center lg:justify-between\">\n <div className=\"flex flex-wrap items-center gap-3\">\n <div className=\"flex items-center gap-2\">\n <SelectionCheckbox\n checked={allVisibleSelected}\n indeterminate={partiallySelected}\n onChange={toggleAllVisibleDelegations}\n ariaLabel={tr(\n \"devportal.selectAllVisibleDelegations\",\n \"Select all visible delegations\",\n )}\n />\n <span className=\"text-sm font-medium\">\n {tr(\"devportal.selectAllVisible\", \"Select all visible\")}\n </span>\n </div>\n {selectedDelegationIds.size > 0 && (\n <button\n onClick={clearSelection}\n className=\"text-sm text-text-secondary hover:text-text-primary\"\n >\n {tr(\"devportal.clearSelection\", \"Clear selection\")}\n </button>\n )}\n </div>\n\n <div className=\"flex flex-wrap items-center gap-2\">\n <span className=\"rounded-full bg-bg-sunken px-3 py-1 text-sm font-medium text-text-secondary\">\n {selectedDelegationIds.size}{\" \"}\n {tr(\"devportal.selected\", \"selected\")}\n </span>\n </div>\n <p className=\"mt-1 text-2xl font-semibold\">{totalActive}</p>\n </div>\n </div>\n )}\n\n {/* Loading */}\n {isLoading && !delegationsData && !grantedData && (\n <LoadingState\n message={tr(\n \"devportal.loadingDelegations\",\n \"Loading delegations...\",\n )}\n />\n )}\n\n {/* ------------------------------------------------------------------- */}\n {/* Tab 1: Apps Acting as Me */}\n {/* ------------------------------------------------------------------- */}\n {!isLoading && activeTab === \"acting-as-me\" && (\n <div\n className=\"space-y-8\"\n id=\"delegations-panel-acting-as-me\"\n role=\"tabpanel\"\n aria-labelledby=\"delegations-tab-acting-as-me\"\n tabIndex={0}\n >\n {activeDelegations.length === 0 &&\n inactiveDelegations.length === 0 ? (\n <EmptyState\n illustration=\"empty-people\"\n title={tr(\n \"devportal.noActiveDelegations\",\n \"No Active Delegations\",\n )}\n description={tr(\n \"devportal.noAppsAuthorizedOnYourBehalf\",\n \"No apps are currently authorized to act on your behalf.\",\n )}\n />\n ) : (\n <>\n {/* Active delegation cards */}\n {activeDelegations.length === 0 ? (\n <EmptyState\n icon={<Shield size={32} />}\n title={tr(\n \"devportal.noActiveDelegations\",\n \"No Active Delegations\",\n )}\n description={tr(\n \"devportal.allPreviousDelegationsRevokedOrExpired\",\n \"All previous delegations have been revoked or expired.\",\n )}\n />\n ) : (\n <div className=\"space-y-4\">\n {activeDelegations.map((delegation) => (\n <DelegationCard\n key={delegation.id}\n delegation={delegation}\n onRevoke={handleRevokeClick}\n isRevoking={\n revokeLoading &&\n revokeTargets.some(\n (item) => item.id === delegation.id,\n )\n }\n selected={selectedDelegationIds.has(delegation.id)}\n onToggleSelect={(checked) =>\n toggleDelegationSelection(delegation.id, checked)\n }\n canRevoke={canRevoke}\n />\n ))}\n </div>\n )}\n\n {/* Revocation history */}\n <RevocationHistory delegations={inactiveDelegations} />\n </>\n )}\n </div>\n )}\n\n {/* ------------------------------------------------------------------- */}\n {/* Tab 2: Delegations I Granted */}\n {/* ------------------------------------------------------------------- */}\n {!isLoading && activeTab === \"granted-by-me\" && (\n <div\n className=\"space-y-4\"\n id=\"delegations-panel-granted-by-me\"\n role=\"tabpanel\"\n aria-labelledby=\"delegations-tab-granted-by-me\"\n tabIndex={0}\n >\n {grantedDelegations.length === 0 ? (\n <EmptyState\n illustration=\"empty-people\"\n title={tr(\n \"devportal.noDelegationsGranted\",\n \"No Delegations Granted\",\n )}\n description={tr(\n \"devportal.noGrantedDelegationsDescription\",\n \"You haven't granted any app delegations to other users.\",\n )}\n />\n ) : (\n grantedDelegations.map((delegation) => (\n <GrantedDelegationCard\n key={delegation.id}\n delegation={delegation}\n onRevoke={handleRevokeClick}\n isRevoking={\n revokeLoading &&\n revokeTargets.some((item) => item.id === delegation.id)\n }\n selected={selectedDelegationIds.has(delegation.id)}\n onToggleSelect={(checked) =>\n toggleDelegationSelection(delegation.id, checked)\n }\n canRevoke={canRevoke}\n />\n ))\n )}\n </div>\n )}\n\n {/* Info card */}\n <div className=\"rounded-lg border border-border-subtle bg-status-info-bg-subtle p-5\">\n <div className=\"flex gap-4\">\n <AlertCircle className=\"h-6 w-6 shrink-0 text-accent-blue\" />\n <div>\n <h3 className=\"font-semibold text-status-info-text\">\n {tr(\"devportal.aboutAppDelegations\", \"About App Delegations\")}\n </h3>\n <p className=\"mt-1 text-sm text-status-info-text\">\n {tr(\n \"devportal.aboutAppDelegationsText\",\n \"App delegations allow automated services, bots, and integrations to perform actions on your behalf. Each delegation specifies which permissions (scopes) and workspaces the app can access. You can revoke access at any time, which immediately prevents the app from acting as you.\",\n )}\n </p>\n </div>\n </div>\n </div>\n\n {/* Revoke confirmation dialog */}\n <ConfirmDialog\n open={revokeTargets.length > 0}\n onOpenChange={(open) => {\n if (!open) setRevokeTargets([]);\n }}\n onConfirm={handleRevokeConfirm}\n title={\n revokeTargets.length > 1\n ? tr(\"devportal.revokeDelegations\", \"Revoke Delegations\")\n : tr(\"devportal.revokeDelegation\", \"Revoke Delegation\")\n }\n description={\n revokeTargets.length > 1\n ? tr(\n \"devportal.revokeDelegationsBulkConfirm\",\n `Are you sure you want to revoke ${revokeTargets.length} selected delegations? Those apps will no longer be able to act on the delegated users' behalf.`,\n )\n : tr(\n \"devportal.revokeDelegationSingleConfirm\",\n `Are you sure you want to revoke access for \"${revokeTargets[0]?.app?.name ?? \"this app\"}\"? This app will no longer be able to act on your behalf.`,\n )\n }\n confirmLabel={tr(\"devportal.revokeAccess\", \"Revoke Access\")}\n cancelLabel={tr(\"devportal.cancel\", \"Cancel\")}\n variant=\"danger\"\n loading={revokeLoading}\n />\n </PageLayout>\n </PermissionGuard>\n );\n};\n"],"mappings":"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"}
@@ -275,7 +275,7 @@ var x = () => {
275
275
  keyField: "id",
276
276
  onRowClick: (e) => P(`/oauth/${e.id}`)
277
277
  }) : /* @__PURE__ */ _(n, {
278
- icon: /* @__PURE__ */ _(g, { size: 32 }),
278
+ illustration: "empty-generic",
279
279
  title: w("devportal.noOAuthAppsFound", "No OAuth apps found"),
280
280
  description: T || D ? w("devportal.tryAdjustingFilters", "Try adjusting your filters") : w("devportal.createFirstOAuthApp", "Create your first OAuth app to enable authentication"),
281
281
  action: !T && !D ? /* @__PURE__ */ v("button", {
@@ -1 +1 @@
1
- {"version":3,"file":"OAuthClientsPage.js","names":[],"sources":["../../../src/devportal/pages/OAuthClientsPage.tsx"],"sourcesContent":["import type { FC } from \"react\";\nimport { useState } from \"react\";\nimport { useI18n } from \"@burdenoff/fe-libs/shared/providers/shell/I18nProvider\";\nimport { Plus, Search, Shield, Loader2, AlertCircle } from \"lucide-react\";\nimport { EmphasisPanel, GlassCard } from \"@burdenoff/fe-libs/ui\";\nimport { PageLayout } from \"../components/PageLayout\";\nimport { DataTable } from \"../components/DataTable\";\nimport { ActiveIndicator } from \"../components/StatusBadge\";\nimport { EmptyState } from \"../components/EmptyState\";\nimport { useDevportal } from \"../providers/DevportalProvider\";\nimport { PermissionGuard } from \"../components\";\nimport { DEVPORTAL_PERMISSIONS } from \"../constants/permissions\";\nimport type { AuthApp } from \"../types\";\nimport { cn } from \"../utils/cn\";\nimport { useCurrentUserAppsQuery, getErrorMessage } from \"../hooks\";\n\n/**\n * OAuth Apps listing page\n *\n * Displays the current user's OAuth apps from auth-svc.\n * Previously showed devportal-svc OAuthClients per application;\n * now shows all user-owned apps centrally.\n */\nexport const OAuthClientsPage: FC = () => {\n const { navigate, basePath } = useDevportal();\n const { t } = useI18n();\n const tr = (key: string, fallback: string): string => {\n const value = t(key);\n return value === key ? fallback : value;\n };\n const [searchQuery, setSearchQuery] = useState(\"\");\n const [typeFilter, setTypeFilter] = useState<string>(\"\");\n\n // Fetch apps from auth-svc (user-scoped, no applicationId needed)\n const { data, loading, error, refetch } = useCurrentUserAppsQuery({\n fetchPolicy: \"cache-and-network\",\n });\n\n const apps: AuthApp[] =\n data?.currentUserApps?.apps.map(\n (app: (typeof data.currentUserApps.apps)[number]) => ({\n id: app.id,\n name: app.name,\n description: app.description ?? undefined,\n clientId: app.clientId,\n type: app.type as AuthApp[\"type\"],\n status: app.status as AuthApp[\"status\"],\n scopes: app.scopes ?? [],\n redirectUris: app.redirectUris ?? [],\n ownerUserId: app.ownerUserId,\n tenantId: app.tenantId ?? \"\",\n createdAt: app.createdAt,\n updatedAt: app.updatedAt,\n }),\n ) ?? [];\n\n const handleNavigate = (path: string) => {\n navigate?.(`${basePath}${path}`);\n };\n\n // Filter apps\n const filteredApps = apps.filter((app) => {\n const matchesSearch =\n !searchQuery ||\n app.name.toLowerCase().includes(searchQuery.toLowerCase()) ||\n app.clientId.toLowerCase().includes(searchQuery.toLowerCase());\n const matchesType = !typeFilter || app.type === typeFilter;\n return matchesSearch && matchesType;\n });\n\n // Loading state\n if (loading && !data) {\n return (\n <PageLayout\n title={tr(\"devportal.oauthApps\", \"OAuth Apps\")}\n description={tr(\n \"devportal.manageOAuthApps\",\n \"Manage your OAuth applications and credentials\",\n )}\n >\n <div className=\"flex items-center justify-center py-12\">\n <Loader2 className=\"h-8 w-8 animate-spin text-text-secondary\" />\n <span className=\"ml-2 text-text-secondary\">\n {tr(\"devportal.loadingApps\", \"Loading apps...\")}\n </span>\n </div>\n </PageLayout>\n );\n }\n\n // Error state\n if (error) {\n return (\n <PageLayout\n title={tr(\"devportal.oauthApps\", \"OAuth Apps\")}\n description={tr(\n \"devportal.manageOAuthApps\",\n \"Manage your OAuth applications and credentials\",\n )}\n >\n <div className=\"rounded-lg border border-status-error-border/50 bg-status-error-bg/10 p-6 text-center\">\n <AlertCircle className=\"mx-auto h-8 w-8 text-status-error-text\" />\n <h3 className=\"mt-2 font-medium text-status-error-text\">\n {tr(\"devportal.failedToLoadApps\", \"Failed to load apps\")}\n </h3>\n <p className=\"mt-1 text-sm text-text-secondary\">\n {getErrorMessage(error)}\n </p>\n <button\n onClick={() => refetch()}\n className=\"mt-4 rounded-md bg-action-primary-bg px-4 py-2 text-sm font-medium text-action-primary-text hover:bg-action-primary-bg/90\"\n >\n {tr(\"devportal.tryAgain\", \"Try Again\")}\n </button>\n </div>\n </PageLayout>\n );\n }\n\n const columns = [\n {\n key: \"name\",\n header: \"App\",\n render: (app: AuthApp) => (\n <div className=\"flex items-center gap-3\">\n <div\n className={cn(\n \"flex h-10 w-10 items-center justify-center rounded-lg\",\n app.status === \"ACTIVE\"\n ? \"bg-action-primary-bg/10 text-primary\"\n : \"bg-bg-sunken text-text-tertiary\",\n )}\n >\n <Shield size={20} />\n </div>\n <div>\n <div className=\"flex items-center gap-2\">\n <p className=\"font-medium\">{app.name}</p>\n <span\n className={cn(\n \"rounded px-1.5 py-0.5 text-xs\",\n app.type === \"SERVICE\"\n ? \"bg-accent-purple-subtle text-accent-purple\"\n : app.type === \"INTEGRATION\" ||\n app.type === \"API_INTEGRATION\"\n ? \"bg-status-info-bg-subtle text-status-info-text\"\n : \"bg-status-success-bg-subtle text-status-success-text\",\n )}\n >\n {app.type}\n </span>\n </div>\n <code className=\"text-xs text-text-secondary\">\n {app.clientId}\n </code>\n </div>\n </div>\n ),\n },\n {\n key: \"scopes\",\n header: \"Scopes\",\n render: (app: AuthApp) => (\n <div className=\"flex flex-wrap gap-1\">\n {app.scopes.slice(0, 3).map((scope) => (\n <span\n key={scope}\n className=\"inline-flex rounded bg-bg-sunken px-1.5 py-0.5 text-xs\"\n >\n {scope}\n </span>\n ))}\n {app.scopes.length > 3 && (\n <span className=\"text-xs text-text-secondary\">\n +{app.scopes.length - 3}\n </span>\n )}\n </div>\n ),\n },\n {\n key: \"redirectUris\",\n header: \"Redirect URIs\",\n render: (app: AuthApp) => (\n <span className=\"text-sm\">\n {app.redirectUris.length} URI\n {app.redirectUris.length !== 1 ? \"s\" : \"\"}\n </span>\n ),\n },\n {\n key: \"status\",\n header: \"Status\",\n render: (app: AuthApp) => (\n <div className=\"flex items-center gap-2\">\n <ActiveIndicator active={app.status === \"ACTIVE\"} />\n <span className=\"text-sm\">{app.status}</span>\n </div>\n ),\n },\n ];\n\n // Stats\n const stats = {\n total: apps.length,\n active: apps.filter((a) => a.status === \"ACTIVE\").length,\n integration: apps.filter(\n (a) => a.type === \"INTEGRATION\" || a.type === \"API_INTEGRATION\",\n ).length,\n service: apps.filter((a) => a.type === \"SERVICE\").length,\n };\n\n return (\n <PermissionGuard permission={DEVPORTAL_PERMISSIONS.APP_READ}>\n <PageLayout\n title={tr(\"devportal.oauthApps\", \"OAuth Apps\")}\n description={tr(\n \"devportal.manageOAuthApps\",\n \"Manage your OAuth applications and credentials\",\n )}\n actions={\n <button\n onClick={() => handleNavigate(\"/oauth/new\")}\n className=\"inline-flex items-center gap-2 rounded-md bg-action-primary-bg px-4 py-2 text-sm font-medium text-action-primary-text hover:bg-action-primary-bg/90 transition-colors\"\n >\n <Plus size={16} />\n {tr(\"devportal.createOAuthApp\", \"Create OAuth App\")}\n </button>\n }\n >\n {/* Filters */}\n <div className=\"flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between\">\n <div className=\"flex flex-1 items-center gap-4\">\n <div className=\"relative flex-1 max-w-md\">\n <Search\n size={16}\n className=\"absolute left-3 top-1/2 -translate-y-1/2 text-text-secondary\"\n />\n <input\n type=\"text\"\n placeholder={tr(\n \"devportal.searchByNameOrClientId\",\n \"Search by name or client ID...\",\n )}\n value={searchQuery}\n onChange={(e) => setSearchQuery(e.target.value)}\n className=\"w-full rounded-md border bg-bg-surface pl-9 pr-4 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-primary\"\n />\n </div>\n <select\n value={typeFilter}\n onChange={(e) => setTypeFilter(e.target.value)}\n className=\"rounded-md border bg-bg-surface px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-primary\"\n >\n <option value=\"\">{tr(\"devportal.allTypes\", \"All Types\")}</option>\n <option value=\"SERVICE\">\n {tr(\"devportal.service\", \"Service\")}\n </option>\n <option value=\"INTEGRATION\">\n {tr(\"devportal.integration\", \"Integration\")}\n </option>\n <option value=\"BOT\">{tr(\"devportal.bot\", \"Bot\")}</option>\n <option value=\"WEB_APP\">\n {tr(\"devportal.webApp\", \"Web App\")}\n </option>\n <option value=\"AUTOMATION\">\n {tr(\"devportal.automation\", \"Automation\")}\n </option>\n </select>\n </div>\n </div>\n\n {/* Summary Stats — primary emphasis zone */}\n <EmphasisPanel>\n <div className=\"grid grid-cols-2 gap-4 sm:grid-cols-4\">\n <GlassCard treatment=\"metric\" className=\"p-4\">\n <div className=\"flex items-center gap-2 text-text-secondary\">\n <Shield size={16} />\n <span className=\"text-sm\">\n {tr(\"devportal.totalApps\", \"Total Apps\")}\n </span>\n </div>\n <p className=\"mt-1 text-2xl font-semibold tabular-nums\">\n {stats.total}\n </p>\n </GlassCard>\n <GlassCard treatment=\"metric\" className=\"p-4\">\n <div className=\"flex items-center gap-2 text-status-success-text\">\n <Shield size={16} />\n <span className=\"text-sm\">\n {tr(\"devportal.active\", \"Active\")}\n </span>\n </div>\n <p className=\"mt-1 text-2xl font-semibold tabular-nums\">\n {stats.active}\n </p>\n </GlassCard>\n <GlassCard treatment=\"metric\" className=\"p-4\">\n <div className=\"flex items-center gap-2 text-accent-blue\">\n <Shield size={16} />\n <span className=\"text-sm\">\n {tr(\"devportal.integration\", \"Integration\")}\n </span>\n </div>\n <p className=\"mt-1 text-2xl font-semibold tabular-nums\">\n {stats.integration}\n </p>\n </GlassCard>\n <GlassCard treatment=\"metric\" className=\"p-4\">\n <div className=\"flex items-center gap-2 text-accent-purple\">\n <Shield size={16} />\n <span className=\"text-sm\">\n {tr(\"devportal.service\", \"Service\")}\n </span>\n </div>\n <p className=\"mt-1 text-2xl font-semibold tabular-nums\">\n {stats.service}\n </p>\n </GlassCard>\n </div>\n </EmphasisPanel>\n\n <p className=\"text-sm text-text-secondary\">\n {filteredApps.length}{\" \"}\n {filteredApps.length !== 1\n ? tr(\"devportal.appsFound\", \"apps found\")\n : tr(\"devportal.appFound\", \"app found\")}\n </p>\n\n {/* Apps Table */}\n {filteredApps.length > 0 ? (\n <DataTable\n data={filteredApps}\n columns={columns}\n keyField=\"id\"\n onRowClick={(app) => handleNavigate(`/oauth/${app.id}`)}\n />\n ) : (\n <EmptyState\n icon={<Shield size={32} />}\n title={tr(\"devportal.noOAuthAppsFound\", \"No OAuth apps found\")}\n description={\n searchQuery || typeFilter\n ? tr(\n \"devportal.tryAdjustingFilters\",\n \"Try adjusting your filters\",\n )\n : tr(\n \"devportal.createFirstOAuthApp\",\n \"Create your first OAuth app to enable authentication\",\n )\n }\n action={\n !searchQuery && !typeFilter ? (\n <button\n onClick={() => handleNavigate(\"/oauth/new\")}\n className=\"inline-flex items-center gap-2 rounded-md bg-action-primary-bg px-4 py-2 text-sm font-medium text-action-primary-text hover:bg-action-primary-bg/90\"\n >\n <Plus size={16} />\n {tr(\"devportal.createOAuthApp\", \"Create OAuth App\")}\n </button>\n ) : undefined\n }\n />\n )}\n </PageLayout>\n </PermissionGuard>\n );\n};\n"],"mappings":"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"}
1
+ {"version":3,"file":"OAuthClientsPage.js","names":[],"sources":["../../../src/devportal/pages/OAuthClientsPage.tsx"],"sourcesContent":["import type { FC } from \"react\";\nimport { useState } from \"react\";\nimport { useI18n } from \"@burdenoff/fe-libs/shared/providers/shell/I18nProvider\";\nimport { Plus, Search, Shield, Loader2, AlertCircle } from \"lucide-react\";\nimport { EmphasisPanel, GlassCard } from \"@burdenoff/fe-libs/ui\";\nimport { PageLayout } from \"../components/PageLayout\";\nimport { DataTable } from \"../components/DataTable\";\nimport { ActiveIndicator } from \"../components/StatusBadge\";\nimport { EmptyState } from \"../components/EmptyState\";\nimport { useDevportal } from \"../providers/DevportalProvider\";\nimport { PermissionGuard } from \"../components\";\nimport { DEVPORTAL_PERMISSIONS } from \"../constants/permissions\";\nimport type { AuthApp } from \"../types\";\nimport { cn } from \"../utils/cn\";\nimport { useCurrentUserAppsQuery, getErrorMessage } from \"../hooks\";\n\n/**\n * OAuth Apps listing page\n *\n * Displays the current user's OAuth apps from auth-svc.\n * Previously showed devportal-svc OAuthClients per application;\n * now shows all user-owned apps centrally.\n */\nexport const OAuthClientsPage: FC = () => {\n const { navigate, basePath } = useDevportal();\n const { t } = useI18n();\n const tr = (key: string, fallback: string): string => {\n const value = t(key);\n return value === key ? fallback : value;\n };\n const [searchQuery, setSearchQuery] = useState(\"\");\n const [typeFilter, setTypeFilter] = useState<string>(\"\");\n\n // Fetch apps from auth-svc (user-scoped, no applicationId needed)\n const { data, loading, error, refetch } = useCurrentUserAppsQuery({\n fetchPolicy: \"cache-and-network\",\n });\n\n const apps: AuthApp[] =\n data?.currentUserApps?.apps.map(\n (app: (typeof data.currentUserApps.apps)[number]) => ({\n id: app.id,\n name: app.name,\n description: app.description ?? undefined,\n clientId: app.clientId,\n type: app.type as AuthApp[\"type\"],\n status: app.status as AuthApp[\"status\"],\n scopes: app.scopes ?? [],\n redirectUris: app.redirectUris ?? [],\n ownerUserId: app.ownerUserId,\n tenantId: app.tenantId ?? \"\",\n createdAt: app.createdAt,\n updatedAt: app.updatedAt,\n }),\n ) ?? [];\n\n const handleNavigate = (path: string) => {\n navigate?.(`${basePath}${path}`);\n };\n\n // Filter apps\n const filteredApps = apps.filter((app) => {\n const matchesSearch =\n !searchQuery ||\n app.name.toLowerCase().includes(searchQuery.toLowerCase()) ||\n app.clientId.toLowerCase().includes(searchQuery.toLowerCase());\n const matchesType = !typeFilter || app.type === typeFilter;\n return matchesSearch && matchesType;\n });\n\n // Loading state\n if (loading && !data) {\n return (\n <PageLayout\n title={tr(\"devportal.oauthApps\", \"OAuth Apps\")}\n description={tr(\n \"devportal.manageOAuthApps\",\n \"Manage your OAuth applications and credentials\",\n )}\n >\n <div className=\"flex items-center justify-center py-12\">\n <Loader2 className=\"h-8 w-8 animate-spin text-text-secondary\" />\n <span className=\"ml-2 text-text-secondary\">\n {tr(\"devportal.loadingApps\", \"Loading apps...\")}\n </span>\n </div>\n </PageLayout>\n );\n }\n\n // Error state\n if (error) {\n return (\n <PageLayout\n title={tr(\"devportal.oauthApps\", \"OAuth Apps\")}\n description={tr(\n \"devportal.manageOAuthApps\",\n \"Manage your OAuth applications and credentials\",\n )}\n >\n <div className=\"rounded-lg border border-status-error-border/50 bg-status-error-bg/10 p-6 text-center\">\n <AlertCircle className=\"mx-auto h-8 w-8 text-status-error-text\" />\n <h3 className=\"mt-2 font-medium text-status-error-text\">\n {tr(\"devportal.failedToLoadApps\", \"Failed to load apps\")}\n </h3>\n <p className=\"mt-1 text-sm text-text-secondary\">\n {getErrorMessage(error)}\n </p>\n <button\n onClick={() => refetch()}\n className=\"mt-4 rounded-md bg-action-primary-bg px-4 py-2 text-sm font-medium text-action-primary-text hover:bg-action-primary-bg/90\"\n >\n {tr(\"devportal.tryAgain\", \"Try Again\")}\n </button>\n </div>\n </PageLayout>\n );\n }\n\n const columns = [\n {\n key: \"name\",\n header: \"App\",\n render: (app: AuthApp) => (\n <div className=\"flex items-center gap-3\">\n <div\n className={cn(\n \"flex h-10 w-10 items-center justify-center rounded-lg\",\n app.status === \"ACTIVE\"\n ? \"bg-action-primary-bg/10 text-primary\"\n : \"bg-bg-sunken text-text-tertiary\",\n )}\n >\n <Shield size={20} />\n </div>\n <div>\n <div className=\"flex items-center gap-2\">\n <p className=\"font-medium\">{app.name}</p>\n <span\n className={cn(\n \"rounded px-1.5 py-0.5 text-xs\",\n app.type === \"SERVICE\"\n ? \"bg-accent-purple-subtle text-accent-purple\"\n : app.type === \"INTEGRATION\" ||\n app.type === \"API_INTEGRATION\"\n ? \"bg-status-info-bg-subtle text-status-info-text\"\n : \"bg-status-success-bg-subtle text-status-success-text\",\n )}\n >\n {app.type}\n </span>\n </div>\n <code className=\"text-xs text-text-secondary\">\n {app.clientId}\n </code>\n </div>\n </div>\n ),\n },\n {\n key: \"scopes\",\n header: \"Scopes\",\n render: (app: AuthApp) => (\n <div className=\"flex flex-wrap gap-1\">\n {app.scopes.slice(0, 3).map((scope) => (\n <span\n key={scope}\n className=\"inline-flex rounded bg-bg-sunken px-1.5 py-0.5 text-xs\"\n >\n {scope}\n </span>\n ))}\n {app.scopes.length > 3 && (\n <span className=\"text-xs text-text-secondary\">\n +{app.scopes.length - 3}\n </span>\n )}\n </div>\n ),\n },\n {\n key: \"redirectUris\",\n header: \"Redirect URIs\",\n render: (app: AuthApp) => (\n <span className=\"text-sm\">\n {app.redirectUris.length} URI\n {app.redirectUris.length !== 1 ? \"s\" : \"\"}\n </span>\n ),\n },\n {\n key: \"status\",\n header: \"Status\",\n render: (app: AuthApp) => (\n <div className=\"flex items-center gap-2\">\n <ActiveIndicator active={app.status === \"ACTIVE\"} />\n <span className=\"text-sm\">{app.status}</span>\n </div>\n ),\n },\n ];\n\n // Stats\n const stats = {\n total: apps.length,\n active: apps.filter((a) => a.status === \"ACTIVE\").length,\n integration: apps.filter(\n (a) => a.type === \"INTEGRATION\" || a.type === \"API_INTEGRATION\",\n ).length,\n service: apps.filter((a) => a.type === \"SERVICE\").length,\n };\n\n return (\n <PermissionGuard permission={DEVPORTAL_PERMISSIONS.APP_READ}>\n <PageLayout\n title={tr(\"devportal.oauthApps\", \"OAuth Apps\")}\n description={tr(\n \"devportal.manageOAuthApps\",\n \"Manage your OAuth applications and credentials\",\n )}\n actions={\n <button\n onClick={() => handleNavigate(\"/oauth/new\")}\n className=\"inline-flex items-center gap-2 rounded-md bg-action-primary-bg px-4 py-2 text-sm font-medium text-action-primary-text hover:bg-action-primary-bg/90 transition-colors\"\n >\n <Plus size={16} />\n {tr(\"devportal.createOAuthApp\", \"Create OAuth App\")}\n </button>\n }\n >\n {/* Filters */}\n <div className=\"flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between\">\n <div className=\"flex flex-1 items-center gap-4\">\n <div className=\"relative flex-1 max-w-md\">\n <Search\n size={16}\n className=\"absolute left-3 top-1/2 -translate-y-1/2 text-text-secondary\"\n />\n <input\n type=\"text\"\n placeholder={tr(\n \"devportal.searchByNameOrClientId\",\n \"Search by name or client ID...\",\n )}\n value={searchQuery}\n onChange={(e) => setSearchQuery(e.target.value)}\n className=\"w-full rounded-md border bg-bg-surface pl-9 pr-4 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-primary\"\n />\n </div>\n <select\n value={typeFilter}\n onChange={(e) => setTypeFilter(e.target.value)}\n className=\"rounded-md border bg-bg-surface px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-primary\"\n >\n <option value=\"\">{tr(\"devportal.allTypes\", \"All Types\")}</option>\n <option value=\"SERVICE\">\n {tr(\"devportal.service\", \"Service\")}\n </option>\n <option value=\"INTEGRATION\">\n {tr(\"devportal.integration\", \"Integration\")}\n </option>\n <option value=\"BOT\">{tr(\"devportal.bot\", \"Bot\")}</option>\n <option value=\"WEB_APP\">\n {tr(\"devportal.webApp\", \"Web App\")}\n </option>\n <option value=\"AUTOMATION\">\n {tr(\"devportal.automation\", \"Automation\")}\n </option>\n </select>\n </div>\n </div>\n\n {/* Summary Stats — primary emphasis zone */}\n <EmphasisPanel>\n <div className=\"grid grid-cols-2 gap-4 sm:grid-cols-4\">\n <GlassCard treatment=\"metric\" className=\"p-4\">\n <div className=\"flex items-center gap-2 text-text-secondary\">\n <Shield size={16} />\n <span className=\"text-sm\">\n {tr(\"devportal.totalApps\", \"Total Apps\")}\n </span>\n </div>\n <p className=\"mt-1 text-2xl font-semibold tabular-nums\">\n {stats.total}\n </p>\n </GlassCard>\n <GlassCard treatment=\"metric\" className=\"p-4\">\n <div className=\"flex items-center gap-2 text-status-success-text\">\n <Shield size={16} />\n <span className=\"text-sm\">\n {tr(\"devportal.active\", \"Active\")}\n </span>\n </div>\n <p className=\"mt-1 text-2xl font-semibold tabular-nums\">\n {stats.active}\n </p>\n </GlassCard>\n <GlassCard treatment=\"metric\" className=\"p-4\">\n <div className=\"flex items-center gap-2 text-accent-blue\">\n <Shield size={16} />\n <span className=\"text-sm\">\n {tr(\"devportal.integration\", \"Integration\")}\n </span>\n </div>\n <p className=\"mt-1 text-2xl font-semibold tabular-nums\">\n {stats.integration}\n </p>\n </GlassCard>\n <GlassCard treatment=\"metric\" className=\"p-4\">\n <div className=\"flex items-center gap-2 text-accent-purple\">\n <Shield size={16} />\n <span className=\"text-sm\">\n {tr(\"devportal.service\", \"Service\")}\n </span>\n </div>\n <p className=\"mt-1 text-2xl font-semibold tabular-nums\">\n {stats.service}\n </p>\n </GlassCard>\n </div>\n </EmphasisPanel>\n\n <p className=\"text-sm text-text-secondary\">\n {filteredApps.length}{\" \"}\n {filteredApps.length !== 1\n ? tr(\"devportal.appsFound\", \"apps found\")\n : tr(\"devportal.appFound\", \"app found\")}\n </p>\n\n {/* Apps Table */}\n {filteredApps.length > 0 ? (\n <DataTable\n data={filteredApps}\n columns={columns}\n keyField=\"id\"\n onRowClick={(app) => handleNavigate(`/oauth/${app.id}`)}\n />\n ) : (\n <EmptyState\n illustration=\"empty-generic\"\n title={tr(\"devportal.noOAuthAppsFound\", \"No OAuth apps found\")}\n description={\n searchQuery || typeFilter\n ? tr(\n \"devportal.tryAdjustingFilters\",\n \"Try adjusting your filters\",\n )\n : tr(\n \"devportal.createFirstOAuthApp\",\n \"Create your first OAuth app to enable authentication\",\n )\n }\n action={\n !searchQuery && !typeFilter ? (\n <button\n onClick={() => handleNavigate(\"/oauth/new\")}\n className=\"inline-flex items-center gap-2 rounded-md bg-action-primary-bg px-4 py-2 text-sm font-medium text-action-primary-text hover:bg-action-primary-bg/90\"\n >\n <Plus size={16} />\n {tr(\"devportal.createOAuthApp\", \"Create OAuth App\")}\n </button>\n ) : undefined\n }\n />\n )}\n </PageLayout>\n </PermissionGuard>\n );\n};\n"],"mappings":"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"}