@burdenoff/microfe-devportal 2026.1113.1 → 2026.1113.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 (55) hide show
  1. package/dist/devportal/DevportalRoot.js +65 -119
  2. package/dist/devportal/DevportalRoot.js.map +1 -1
  3. package/dist/devportal/components/AppCard.js +4 -4
  4. package/dist/devportal/components/DeveloperSettingsHub.d.ts +6 -0
  5. package/dist/devportal/components/DeveloperSettingsHub.js +59 -0
  6. package/dist/devportal/components/DeveloperSettingsHub.js.map +1 -0
  7. package/dist/devportal/components/SectionNav.d.ts +1 -1
  8. package/dist/devportal/components/SectionNav.js +60 -36
  9. package/dist/devportal/components/SectionNav.js.map +1 -1
  10. package/dist/devportal/pages/AnalyticsDashboardPage.js +186 -179
  11. package/dist/devportal/pages/AnalyticsDashboardPage.js.map +1 -1
  12. package/dist/devportal/pages/ApiKeysPage.js +113 -111
  13. package/dist/devportal/pages/ApiKeysPage.js.map +1 -1
  14. package/dist/devportal/pages/AppLogsPage.js +80 -84
  15. package/dist/devportal/pages/AppLogsPage.js.map +1 -1
  16. package/dist/devportal/pages/ApplicationDetailPage.js +529 -504
  17. package/dist/devportal/pages/ApplicationDetailPage.js.map +1 -1
  18. package/dist/devportal/pages/ApplicationsPage.js +68 -67
  19. package/dist/devportal/pages/ApplicationsPage.js.map +1 -1
  20. package/dist/devportal/pages/CreateApiKeyPage.js +163 -166
  21. package/dist/devportal/pages/CreateApiKeyPage.js.map +1 -1
  22. package/dist/devportal/pages/CreateOAuthClientPage.js +134 -130
  23. package/dist/devportal/pages/CreateOAuthClientPage.js.map +1 -1
  24. package/dist/devportal/pages/DelegationsPage.js +11 -11
  25. package/dist/devportal/pages/DocumentationPage.js +100 -94
  26. package/dist/devportal/pages/DocumentationPage.js.map +1 -1
  27. package/dist/devportal/pages/EditApplicationPage.js +379 -377
  28. package/dist/devportal/pages/EditApplicationPage.js.map +1 -1
  29. package/dist/devportal/pages/InvitationDecisionPage.js +83 -79
  30. package/dist/devportal/pages/InvitationDecisionPage.js.map +1 -1
  31. package/dist/devportal/pages/OAuthClientDetailPage.js +76 -76
  32. package/dist/devportal/pages/OAuthClientDetailPage.js.map +1 -1
  33. package/dist/devportal/pages/OAuthClientsPage.js +126 -124
  34. package/dist/devportal/pages/OAuthClientsPage.js.map +1 -1
  35. package/dist/devportal/pages/OrganizationDetailPage.js +19 -19
  36. package/dist/devportal/pages/OrganizationMembersPage.js +223 -204
  37. package/dist/devportal/pages/OrganizationMembersPage.js.map +1 -1
  38. package/dist/devportal/pages/ResourcesPage.d.ts +6 -0
  39. package/dist/devportal/pages/ResourcesPage.js +171 -155
  40. package/dist/devportal/pages/ResourcesPage.js.map +1 -1
  41. package/dist/devportal/pages/SellerDashboardPage.js +13 -13
  42. package/dist/devportal/pages/SellerItemsPage.js +4 -4
  43. package/dist/devportal/pages/SettingsPage.js +330 -315
  44. package/dist/devportal/pages/SettingsPage.js.map +1 -1
  45. package/dist/devportal/pages/SetupPage.js +5 -5
  46. package/dist/devportal/pages/WebhookDetailPage.js +6 -6
  47. package/dist/devportal/pages/WebhooksPage.js +39 -39
  48. package/dist/devportal/pages/WebhooksPage.js.map +1 -1
  49. package/dist/devportal/utils/appDetailTabs.d.ts +73 -0
  50. package/dist/devportal/utils/appDetailTabs.js +62 -0
  51. package/dist/devportal/utils/appDetailTabs.js.map +1 -0
  52. package/dist/devportal/utils/developerNav.d.ts +39 -0
  53. package/dist/devportal/utils/developerNav.js +304 -0
  54. package/dist/devportal/utils/developerNav.js.map +1 -0
  55. package/package.json +2 -2
@@ -1 +1 @@
1
- {"version":3,"file":"ApiKeysPage.js","names":["EmptyState","EmptyState$1","mapApiKey","key","isExpiringSoon","expiresAt","daysUntilExpiry","isExpired","ApiKeysPage","useDevportal","useI18n","tr","value","t","fallback","searchQuery","setSearchQuery","useState","statusFilter","setStatusFilter","revokeTarget","setRevokeTarget","rotateTarget","setRotateTarget","newSecret","setNewSecret","selectedKeyIds","setSelectedKeyIds","bulkFeedback","setBulkFeedback","showBulkRevokeDialog","setShowBulkRevokeDialog","useMyApiKeysQuery","oauthAppsData","useCurrentUserAppsQuery","orgAppIds","useMemo","apps","a","revokeApiKey","revokeLoading","useRevokeApiKeyAuthMutation","rotateApiKey","rotateLoading","useRotateApiKeyAuthMutation","apiKeys","data","handleNavigate","basePath","path","handleRevoke","refetch","handleRotate","secret","filteredKeys","matchesOrg","matchesSearch","matchesStatus","selectedKeys","selectedActiveKeys","allVisibleSelectableKeys","allVisibleSelected","partiallySelected","toggleKeySelection","next","prev","checked","keyId","toggleAllVisibleKeys","clearSelection","loading","PageLayout","Loader2","error","AlertCircle","getErrorMessage","columns","SelectionCheckbox","event","cn","Key","scope","ActiveIndicator","e","RotateCw","Ban","stats","k","PermissionGuard","Plus","NewSecretAlert","Search","EmphasisPanel","GlassCard","Shield","Clock","AlertTriangle","DataTable","ConfirmDialog","open","results","succeeded","result","failed"],"sources":["../../../src/devportal/pages/ApiKeysPage.tsx"],"sourcesContent":["import type { FC } from \"react\";\nimport { useMemo, useState } from \"react\";\nimport { useI18n } from \"@burdenoff/fe-libs/shared/providers/shell/I18nProvider\";\nimport {\n Plus,\n Search,\n Key,\n Shield,\n Clock,\n AlertTriangle,\n Loader2,\n AlertCircle,\n RotateCw,\n Ban,\n} 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 { ConfirmDialog } from \"../components/ConfirmDialog\";\nimport { NewSecretAlert } from \"../components/SecretDisplay\";\nimport { SelectionCheckbox } from \"../components/SelectionCheckbox\";\nimport { useDevportal } from \"../providers/DevportalProvider\";\nimport { PermissionGuard } from \"../components\";\nimport { DEVPORTAL_PERMISSIONS } from \"../constants/permissions\";\nimport type { AuthApiKey } from \"../types\";\nimport { cn } from \"../utils/cn\";\nimport {\n useMyApiKeysQuery,\n useCurrentUserAppsQuery,\n useRevokeApiKeyAuthMutation,\n useRotateApiKeyAuthMutation,\n getErrorMessage,\n} from \"../hooks\";\n\n/**\n * Map GraphQL APIKey to local AuthApiKey type\n */\nfunction mapApiKey(key: {\n id: string;\n name: string;\n prefix: string;\n scopes: string[];\n appId: string;\n appName?: string | null;\n createdAt: string;\n expiresAt?: string | null;\n lastUsedAt?: string | null;\n}): AuthApiKey {\n return {\n id: key.id,\n name: key.name,\n prefix: key.prefix,\n scopes: key.scopes,\n appId: key.appId,\n appName: key.appName ?? undefined,\n createdAt: key.createdAt,\n expiresAt: key.expiresAt ?? undefined,\n lastUsedAt: key.lastUsedAt ?? undefined,\n };\n}\n\nconst isExpiringSoon = (expiresAt?: string): boolean => {\n if (!expiresAt) return false;\n const daysUntilExpiry = Math.ceil(\n (new Date(expiresAt).getTime() - Date.now()) / (1000 * 60 * 60 * 24),\n );\n return daysUntilExpiry <= 30 && daysUntilExpiry > 0;\n};\n\nconst isExpired = (expiresAt?: string): boolean => {\n if (!expiresAt) return false;\n return new Date(expiresAt).getTime() < Date.now();\n};\n\n/**\n * API Keys listing page\n *\n * Displays all API keys for the current user from auth-svc.\n * Previously showed devportal-svc ApiKeys per application;\n * now shows all user-owned API keys centrally.\n * Supports revoking and rotating API keys.\n */\nexport const ApiKeysPage: FC = () => {\n const { navigate, basePath, activeOrgId } = 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 [statusFilter, setStatusFilter] = useState<\n \"all\" | \"active\" | \"expired\"\n >(\"all\");\n\n // Revoke / Rotate state\n const [revokeTarget, setRevokeTarget] = useState<AuthApiKey | null>(null);\n const [rotateTarget, setRotateTarget] = useState<AuthApiKey | null>(null);\n const [newSecret, setNewSecret] = useState<string | null>(null);\n const [selectedKeyIds, setSelectedKeyIds] = useState<Set<string>>(new Set());\n const [bulkFeedback, setBulkFeedback] = useState<{\n type: \"success\" | \"error\";\n message: string;\n } | null>(null);\n const [showBulkRevokeDialog, setShowBulkRevokeDialog] = useState(false);\n\n // Fetch API keys from auth-svc (user-scoped, no applicationId needed)\n const { data, loading, error, refetch } = useMyApiKeysQuery({\n fetchPolicy: \"cache-and-network\",\n });\n\n // Fetch OAuth apps to map appId → filter; key.appId is an OAuth client UUID\n const { data: oauthAppsData } = useCurrentUserAppsQuery({\n fetchPolicy: \"cache-first\",\n });\n const orgAppIds = useMemo(() => {\n const apps = oauthAppsData?.currentUserApps?.apps;\n if (!apps?.length) return null;\n return new Set(apps.map((a: { id: string }) => a.id));\n }, [oauthAppsData?.currentUserApps?.apps]);\n\n // Mutations\n const [revokeApiKey, { loading: revokeLoading }] =\n useRevokeApiKeyAuthMutation();\n const [rotateApiKey, { loading: rotateLoading }] =\n useRotateApiKeyAuthMutation();\n\n const apiKeys: AuthApiKey[] = useMemo(\n () => data?.myApiKeys?.apiKeys.map(mapApiKey) ?? [],\n [data?.myApiKeys?.apiKeys],\n );\n\n const handleNavigate = (path: string) => {\n navigate?.(`${basePath}${path}`);\n };\n\n // Revoke handler\n const handleRevoke = async () => {\n if (!revokeTarget) return;\n try {\n await revokeApiKey({\n variables: { keyId: revokeTarget.id },\n });\n setRevokeTarget(null);\n await refetch();\n } catch {\n // Error is surfaced by Apollo's error state on the mutation;\n // the ConfirmDialog stays open so the user can retry or cancel.\n }\n };\n\n // Rotate handler\n const handleRotate = async () => {\n if (!rotateTarget) return;\n try {\n const result = await rotateApiKey({\n variables: { keyId: rotateTarget.id },\n });\n const secret = result.data?.rotateAPIKey.apiSecret;\n if (secret) {\n setNewSecret(secret);\n }\n setRotateTarget(null);\n await refetch();\n } catch {\n // Error is surfaced by Apollo's error state on the mutation;\n // the ConfirmDialog stays open so the user can retry or cancel.\n }\n };\n\n // Filter keys: org scope + search + status\n const filteredKeys = apiKeys.filter((key: AuthApiKey) => {\n const matchesOrg = !orgAppIds || orgAppIds.has(key.appId);\n const matchesSearch =\n !searchQuery ||\n key.name.toLowerCase().includes(searchQuery.toLowerCase()) ||\n key.prefix.toLowerCase().includes(searchQuery.toLowerCase()) ||\n (key.appName?.toLowerCase().includes(searchQuery.toLowerCase()) ?? false);\n const matchesStatus =\n statusFilter === \"all\" ||\n (statusFilter === \"active\" && !isExpired(key.expiresAt)) ||\n (statusFilter === \"expired\" && isExpired(key.expiresAt));\n return matchesOrg && matchesSearch && matchesStatus;\n });\n\n const selectedKeys = useMemo(\n () => apiKeys.filter((key) => selectedKeyIds.has(key.id)),\n [apiKeys, selectedKeyIds],\n );\n\n const selectedActiveKeys = useMemo(\n () => selectedKeys.filter((key) => !isExpired(key.expiresAt)),\n [selectedKeys],\n );\n\n const allVisibleSelectableKeys = filteredKeys.filter(\n (key) => !isExpired(key.expiresAt),\n );\n const allVisibleSelected =\n allVisibleSelectableKeys.length > 0 &&\n allVisibleSelectableKeys.every((key) => selectedKeyIds.has(key.id));\n const partiallySelected =\n allVisibleSelectableKeys.some((key) => selectedKeyIds.has(key.id)) &&\n !allVisibleSelected;\n\n const toggleKeySelection = (keyId: string, checked: boolean) => {\n setSelectedKeyIds((prev) => {\n const next = new Set(prev);\n if (checked) {\n next.add(keyId);\n } else {\n next.delete(keyId);\n }\n return next;\n });\n };\n\n const toggleAllVisibleKeys = (checked: boolean) => {\n setSelectedKeyIds((prev) => {\n const next = new Set(prev);\n allVisibleSelectableKeys.forEach((key) => {\n if (checked) {\n next.add(key.id);\n } else {\n next.delete(key.id);\n }\n });\n return next;\n });\n };\n\n const clearSelection = () => setSelectedKeyIds(new Set());\n\n // Loading state\n if (loading && !data) {\n return (\n <PageLayout\n title={tr(\"devportal.apiKeys\", \"API Keys\")}\n description={tr(\n \"devportal.manageApiKeys\",\n \"Manage your API keys for programmatic access\",\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.loadingApiKeys\", \"Loading API keys...\")}\n </span>\n </div>\n </PageLayout>\n );\n }\n\n // Error state\n if (error) {\n return (\n <PageLayout\n title={tr(\"devportal.apiKeys\", \"API Keys\")}\n description={tr(\n \"devportal.manageApiKeys\",\n \"Manage your API keys for programmatic access\",\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.failedToLoadApiKeys\", \"Failed to load API keys\")}\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: \"select\",\n header: (\n <SelectionCheckbox\n checked={allVisibleSelected}\n indeterminate={partiallySelected}\n onChange={toggleAllVisibleKeys}\n ariaLabel=\"Select all visible API keys\"\n />\n ),\n className: \"w-12\",\n render: (key: AuthApiKey) => (\n <div onClick={(event) => event.stopPropagation()}>\n <SelectionCheckbox\n checked={selectedKeyIds.has(key.id)}\n disabled={isExpired(key.expiresAt)}\n onChange={(checked) => toggleKeySelection(key.id, checked)}\n ariaLabel={`Select API key ${key.name}`}\n />\n </div>\n ),\n },\n {\n key: \"name\",\n header: \"API Key\",\n render: (key: AuthApiKey) => (\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 isExpired(key.expiresAt)\n ? \"bg-status-error-bg-subtle text-status-error-text\"\n : \"bg-action-primary-bg/10 text-primary\",\n )}\n >\n <Key size={20} />\n </div>\n <div>\n <div className=\"flex items-center gap-2\">\n <p className=\"font-medium\">{key.name}</p>\n {isExpired(key.expiresAt) && (\n <span className=\"rounded bg-status-error-bg-subtle px-1.5 py-0.5 text-xs text-status-error-text\">\n Expired\n </span>\n )}\n {isExpiringSoon(key.expiresAt) && !isExpired(key.expiresAt) && (\n <span className=\"rounded bg-status-warning-bg-subtle px-1.5 py-0.5 text-xs text-status-warning-text\">\n Expiring Soon\n </span>\n )}\n </div>\n {key.appName && (\n <p className=\"text-xs text-text-secondary\">App: {key.appName}</p>\n )}\n </div>\n </div>\n ),\n },\n {\n key: \"prefix\",\n header: \"Key Prefix\",\n render: (key: AuthApiKey) => (\n <code className=\"text-sm text-text-secondary\">{key.prefix}...</code>\n ),\n },\n {\n key: \"scopes\",\n header: \"Scopes\",\n render: (key: AuthApiKey) => (\n <div className=\"flex flex-wrap gap-1\">\n {key.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 {key.scopes.length > 3 && (\n <span className=\"text-xs text-text-secondary\">\n +{key.scopes.length - 3}\n </span>\n )}\n </div>\n ),\n },\n {\n key: \"lastUsed\",\n header: \"Last Used\",\n render: (key: AuthApiKey) => (\n <span className=\"text-sm text-text-secondary\">\n {key.lastUsedAt\n ? new Date(key.lastUsedAt).toLocaleDateString()\n : \"Never\"}\n </span>\n ),\n },\n {\n key: \"expires\",\n header: \"Expires\",\n render: (key: AuthApiKey) => (\n <span\n className={cn(\n \"text-sm\",\n isExpired(key.expiresAt)\n ? \"text-status-error-text\"\n : isExpiringSoon(key.expiresAt)\n ? \"text-status-warning-text\"\n : \"text-text-secondary\",\n )}\n >\n {key.expiresAt\n ? new Date(key.expiresAt).toLocaleDateString()\n : \"Never\"}\n </span>\n ),\n },\n {\n key: \"status\",\n header: \"Status\",\n render: (key: AuthApiKey) => (\n <div className=\"flex items-center gap-2\">\n <ActiveIndicator active={!isExpired(key.expiresAt)} />\n <span className=\"text-sm\">\n {isExpired(key.expiresAt) ? \"Expired\" : \"Active\"}\n </span>\n </div>\n ),\n },\n {\n key: \"actions\",\n header: \"Actions\",\n className: \"w-28\",\n render: (key: AuthApiKey) => {\n if (isExpired(key.expiresAt)) {\n return (\n <span className=\"text-xs text-text-secondary\">No actions</span>\n );\n }\n\n return (\n <div className=\"flex items-center gap-1\">\n <button\n type=\"button\"\n title=\"Rotate secret\"\n disabled={rotateLoading}\n onClick={(e) => {\n e.stopPropagation();\n setRotateTarget(key);\n }}\n className={cn(\n \"inline-flex items-center gap-1 rounded-md px-2 py-1 text-xs font-medium transition-colors\",\n \"text-status-info-text hover:bg-status-info-bg-subtle\",\n \"disabled:opacity-50 disabled:cursor-not-allowed\",\n )}\n >\n <RotateCw size={14} />\n <span className=\"sr-only sm:not-sr-only\">Rotate</span>\n </button>\n <button\n type=\"button\"\n title=\"Revoke key\"\n disabled={revokeLoading}\n onClick={(e) => {\n e.stopPropagation();\n setRevokeTarget(key);\n }}\n className={cn(\n \"inline-flex items-center gap-1 rounded-md px-2 py-1 text-xs font-medium transition-colors\",\n \"text-status-error-text hover:bg-status-error-bg-subtle\",\n \"disabled:opacity-50 disabled:cursor-not-allowed\",\n )}\n >\n <Ban size={14} />\n <span className=\"sr-only sm:not-sr-only\">Revoke</span>\n </button>\n </div>\n );\n },\n },\n ];\n\n // Stats scoped to the filtered set (org context)\n const stats = {\n total: filteredKeys.length,\n active: filteredKeys.filter((k) => !isExpired(k.expiresAt)).length,\n expiringSoon: filteredKeys.filter((k) => isExpiringSoon(k.expiresAt))\n .length,\n expired: filteredKeys.filter((k) => isExpired(k.expiresAt)).length,\n };\n\n const handleBulkRevoke = async () => {\n if (selectedActiveKeys.length === 0) return;\n\n const results = await Promise.allSettled(\n selectedActiveKeys.map((key) =>\n revokeApiKey({ variables: { keyId: key.id } }),\n ),\n );\n\n const succeeded = results.filter(\n (result) => result.status === \"fulfilled\",\n ).length;\n const failed = results.length - succeeded;\n\n setBulkFeedback({\n type: failed === 0 ? \"success\" : \"error\",\n message: `Revoked ${succeeded} API key${succeeded === 1 ? \"\" : \"s\"}${\n failed > 0 ? ` • ${failed} failed` : \"\"\n }`,\n });\n setShowBulkRevokeDialog(false);\n clearSelection();\n await refetch();\n };\n\n return (\n <PermissionGuard authOnly>\n <PageLayout\n title={tr(\"devportal.apiKeys\", \"API Keys\")}\n description={tr(\n \"devportal.manageApiKeys\",\n \"Manage your API keys for programmatic access\",\n )}\n purpose={tr(\n \"devportal.apiKeysPurpose\",\n \"API keys authenticate server-to-server calls — use them in scripts, CI jobs, or backend services that talk to the platform without a logged-in user. Generate a key here, scope it to an app, and rotate it if it is ever exposed.\",\n )}\n actions={\n <button\n onClick={() => handleNavigate(\"/api-keys/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.generateApiKey\", \"Generate API Key\")}\n </button>\n }\n >\n {/* New secret alert — shown after a successful rotation */}\n {newSecret && (\n <NewSecretAlert\n title=\"API Key Rotated\"\n secret={newSecret}\n description=\"The previous secret has been invalidated. Copy the new secret below — it will not be shown again.\"\n onDismiss={() => setNewSecret(null)}\n />\n )}\n\n {bulkFeedback && (\n <div\n className={cn(\n \"rounded-lg border px-4 py-3 text-sm\",\n bulkFeedback.type === \"success\"\n ? \"border-border-subtle bg-status-success-bg-subtle text-status-success-text\"\n : \"border-status-error-border/40 bg-status-error-bg/10 text-status-error-text\",\n )}\n >\n <div className=\"flex items-center justify-between gap-3\">\n <span>{bulkFeedback.message}</span>\n <button\n onClick={() => setBulkFeedback(null)}\n className=\"text-xs font-medium underline underline-offset-2\"\n >\n Dismiss\n </button>\n </div>\n </div>\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 {/* Search */}\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=\"Search by name, prefix, or app...\"\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\n {/* Status Filter */}\n <select\n value={statusFilter}\n onChange={(e) =>\n setStatusFilter(e.target.value as \"all\" | \"active\" | \"expired\")\n }\n className=\"rounded-md border bg-bg-surface px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-primary scheme-normal dark:scheme-dark [&_option]:bg-bg-surface [&_option]:text-text-primary\"\n >\n <option value=\"all\">All Status</option>\n <option value=\"active\">Active</option>\n <option value=\"expired\">Expired</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 <Key size={16} />\n <span className=\"text-sm\">Total Keys</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\">Active</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-status-warning-text\">\n <Clock size={16} />\n <span className=\"text-sm\">Expiring Soon</span>\n </div>\n <p className=\"mt-1 text-2xl font-semibold tabular-nums\">\n {stats.expiringSoon}\n </p>\n </GlassCard>\n <GlassCard treatment=\"metric\" className=\"p-4\">\n <div className=\"flex items-center gap-2 text-status-error-text\">\n <AlertTriangle size={16} />\n <span className=\"text-sm\">Expired</span>\n </div>\n <p className=\"mt-1 text-2xl font-semibold tabular-nums\">\n {stats.expired}\n </p>\n </GlassCard>\n </div>\n </EmphasisPanel>\n\n {/* Results count */}\n <p className=\"text-sm text-text-secondary\">\n {filteredKeys.length} API key{filteredKeys.length !== 1 ? \"s\" : \"\"}{\" \"}\n found\n </p>\n\n {filteredKeys.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={toggleAllVisibleKeys}\n ariaLabel=\"Select all visible API keys\"\n />\n <span className=\"text-sm font-medium\">\n Select all active keys\n </span>\n </div>\n {selectedKeyIds.size > 0 && (\n <button\n onClick={clearSelection}\n className=\"text-sm text-text-secondary hover:text-text-primary\"\n >\n 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 {selectedKeyIds.size} selected\n </span>\n <button\n onClick={() => setShowBulkRevokeDialog(true)}\n disabled={selectedActiveKeys.length === 0}\n className=\"inline-flex items-center gap-2 rounded-md border border-status-error-bg px-3 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 <Ban size={16} />\n Revoke Selected\n {selectedActiveKeys.length > 0\n ? ` (${selectedActiveKeys.length})`\n : \"\"}\n </button>\n </div>\n </div>\n </div>\n )}\n\n {/* Keys Table */}\n {filteredKeys.length > 0 ? (\n <DataTable data={filteredKeys} columns={columns} keyField=\"id\" />\n ) : (\n <EmptyState\n illustration=\"empty-data\"\n title={tr(\"devportal.noApiKeysFound\", \"No API keys found\")}\n description={\n searchQuery || statusFilter !== \"all\"\n ? tr(\n \"devportal.tryAdjustingFilters\",\n \"Try adjusting your filters\",\n )\n : tr(\n \"devportal.generateFirstApiKey\",\n \"Generate your first API key to start making API calls\",\n )\n }\n action={\n !searchQuery && statusFilter === \"all\" ? (\n <button\n onClick={() => handleNavigate(\"/api-keys/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.generateApiKey\", \"Generate API Key\")}\n </button>\n ) : undefined\n }\n />\n )}\n\n {/* Revoke confirmation dialog */}\n <ConfirmDialog\n open={revokeTarget !== null}\n onOpenChange={(open) => {\n if (!open) setRevokeTarget(null);\n }}\n onConfirm={handleRevoke}\n title=\"Revoke API Key\"\n description={`Are you sure you want to revoke \"${revokeTarget?.name ?? \"\"}\"? This action cannot be undone. Any application or service using this key will immediately lose access.`}\n confirmLabel=\"Revoke Key\"\n cancelLabel=\"Cancel\"\n variant=\"danger\"\n loading={revokeLoading}\n />\n\n {/* Rotate confirmation dialog */}\n <ConfirmDialog\n open={rotateTarget !== null}\n onOpenChange={(open) => {\n if (!open) setRotateTarget(null);\n }}\n onConfirm={handleRotate}\n title=\"Rotate API Key Secret\"\n description={`This will generate a new secret for \"${rotateTarget?.name ?? \"\"}\" and immediately invalidate the current one. Make sure to update all services using this key before the old secret stops working.`}\n confirmLabel=\"Rotate Secret\"\n cancelLabel=\"Cancel\"\n variant=\"warning\"\n loading={rotateLoading}\n />\n\n <ConfirmDialog\n open={showBulkRevokeDialog}\n onOpenChange={setShowBulkRevokeDialog}\n onConfirm={handleBulkRevoke}\n title=\"Revoke selected API keys\"\n description={`Revoke ${selectedActiveKeys.length} selected API key${selectedActiveKeys.length === 1 ? \"\" : \"s\"}? Any service using these keys will immediately lose access.`}\n confirmLabel=\"Revoke Keys\"\n cancelLabel=\"Cancel\"\n variant=\"danger\"\n loading={revokeLoading}\n />\n </PageLayout>\n </PermissionGuard>\n );\n};\n"],"mappings":"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"}
1
+ {"version":3,"file":"ApiKeysPage.js","names":["EmptyState","EmptyState$1","mapApiKey","key","isExpiringSoon","expiresAt","daysUntilExpiry","isExpired","ApiKeysPage","useDevportal","isFluidgrids","useIsFluidgridsVariant","useI18n","tr","value","t","fallback","searchQuery","setSearchQuery","useState","statusFilter","setStatusFilter","revokeTarget","setRevokeTarget","rotateTarget","setRotateTarget","newSecret","setNewSecret","selectedKeyIds","setSelectedKeyIds","bulkFeedback","setBulkFeedback","showBulkRevokeDialog","setShowBulkRevokeDialog","useMyApiKeysQuery","oauthAppsData","useCurrentUserAppsQuery","orgAppIds","useMemo","apps","a","revokeApiKey","revokeLoading","useRevokeApiKeyAuthMutation","rotateApiKey","rotateLoading","useRotateApiKeyAuthMutation","apiKeys","data","handleNavigate","basePath","path","handleRevoke","refetch","handleRotate","secret","filteredKeys","matchesOrg","matchesSearch","matchesStatus","selectedKeys","selectedActiveKeys","allVisibleSelectableKeys","allVisibleSelected","partiallySelected","toggleKeySelection","next","prev","checked","keyId","toggleAllVisibleKeys","clearSelection","loading","PageLayout","Loader2","error","AlertCircle","getErrorMessage","columns","SelectionCheckbox","event","cn","Key","scope","ActiveIndicator","e","RotateCw","Ban","stats","k","PermissionGuard","Plus","NewSecretAlert","Search","EmphasisPanel","GlassCard","Shield","Clock","AlertTriangle","DataTable","ConfirmDialog","open","results","succeeded","result","failed"],"sources":["../../../src/devportal/pages/ApiKeysPage.tsx"],"sourcesContent":["import type { FC } from \"react\";\nimport { useMemo, useState } from \"react\";\nimport { useI18n } from \"@burdenoff/fe-libs/shared/providers/shell/I18nProvider\";\nimport {\n Plus,\n Search,\n Key,\n Shield,\n Clock,\n AlertTriangle,\n Loader2,\n AlertCircle,\n RotateCw,\n Ban,\n} 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 { ConfirmDialog } from \"../components/ConfirmDialog\";\nimport { NewSecretAlert } from \"../components/SecretDisplay\";\nimport { SelectionCheckbox } from \"../components/SelectionCheckbox\";\nimport {\n useDevportal,\n useIsFluidgridsVariant,\n} from \"../providers/DevportalProvider\";\nimport { PermissionGuard } from \"../components\";\nimport { DEVPORTAL_PERMISSIONS } from \"../constants/permissions\";\nimport type { AuthApiKey } from \"../types\";\nimport { cn } from \"../utils/cn\";\nimport {\n useMyApiKeysQuery,\n useCurrentUserAppsQuery,\n useRevokeApiKeyAuthMutation,\n useRotateApiKeyAuthMutation,\n getErrorMessage,\n} from \"../hooks\";\n\n/**\n * Map GraphQL APIKey to local AuthApiKey type\n */\nfunction mapApiKey(key: {\n id: string;\n name: string;\n prefix: string;\n scopes: string[];\n appId: string;\n appName?: string | null;\n createdAt: string;\n expiresAt?: string | null;\n lastUsedAt?: string | null;\n}): AuthApiKey {\n return {\n id: key.id,\n name: key.name,\n prefix: key.prefix,\n scopes: key.scopes,\n appId: key.appId,\n appName: key.appName ?? undefined,\n createdAt: key.createdAt,\n expiresAt: key.expiresAt ?? undefined,\n lastUsedAt: key.lastUsedAt ?? undefined,\n };\n}\n\nconst isExpiringSoon = (expiresAt?: string): boolean => {\n if (!expiresAt) return false;\n const daysUntilExpiry = Math.ceil(\n (new Date(expiresAt).getTime() - Date.now()) / (1000 * 60 * 60 * 24),\n );\n return daysUntilExpiry <= 30 && daysUntilExpiry > 0;\n};\n\nconst isExpired = (expiresAt?: string): boolean => {\n if (!expiresAt) return false;\n return new Date(expiresAt).getTime() < Date.now();\n};\n\n/**\n * API Keys listing page\n *\n * Displays all API keys for the current user from auth-svc.\n * Previously showed devportal-svc ApiKeys per application;\n * now shows all user-owned API keys centrally.\n * Supports revoking and rotating API keys.\n */\nexport const ApiKeysPage: FC = () => {\n const { navigate, basePath, activeOrgId } = useDevportal();\n const isFluidgrids = useIsFluidgridsVariant();\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 [statusFilter, setStatusFilter] = useState<\n \"all\" | \"active\" | \"expired\"\n >(\"all\");\n\n // Revoke / Rotate state\n const [revokeTarget, setRevokeTarget] = useState<AuthApiKey | null>(null);\n const [rotateTarget, setRotateTarget] = useState<AuthApiKey | null>(null);\n const [newSecret, setNewSecret] = useState<string | null>(null);\n const [selectedKeyIds, setSelectedKeyIds] = useState<Set<string>>(new Set());\n const [bulkFeedback, setBulkFeedback] = useState<{\n type: \"success\" | \"error\";\n message: string;\n } | null>(null);\n const [showBulkRevokeDialog, setShowBulkRevokeDialog] = useState(false);\n\n // Fetch API keys from auth-svc (user-scoped, no applicationId needed)\n const { data, loading, error, refetch } = useMyApiKeysQuery({\n fetchPolicy: \"cache-and-network\",\n });\n\n // Fetch OAuth apps to map appId → filter; key.appId is an OAuth client UUID\n const { data: oauthAppsData } = useCurrentUserAppsQuery({\n fetchPolicy: \"cache-first\",\n });\n const orgAppIds = useMemo(() => {\n const apps = oauthAppsData?.currentUserApps?.apps;\n if (!apps?.length) return null;\n return new Set(apps.map((a: { id: string }) => a.id));\n }, [oauthAppsData?.currentUserApps?.apps]);\n\n // Mutations\n const [revokeApiKey, { loading: revokeLoading }] =\n useRevokeApiKeyAuthMutation();\n const [rotateApiKey, { loading: rotateLoading }] =\n useRotateApiKeyAuthMutation();\n\n const apiKeys: AuthApiKey[] = useMemo(\n () => data?.myApiKeys?.apiKeys.map(mapApiKey) ?? [],\n [data?.myApiKeys?.apiKeys],\n );\n\n const handleNavigate = (path: string) => {\n navigate?.(`${basePath}${path}`);\n };\n\n // Revoke handler\n const handleRevoke = async () => {\n if (!revokeTarget) return;\n try {\n await revokeApiKey({\n variables: { keyId: revokeTarget.id },\n });\n setRevokeTarget(null);\n await refetch();\n } catch {\n // Error is surfaced by Apollo's error state on the mutation;\n // the ConfirmDialog stays open so the user can retry or cancel.\n }\n };\n\n // Rotate handler\n const handleRotate = async () => {\n if (!rotateTarget) return;\n try {\n const result = await rotateApiKey({\n variables: { keyId: rotateTarget.id },\n });\n const secret = result.data?.rotateAPIKey.apiSecret;\n if (secret) {\n setNewSecret(secret);\n }\n setRotateTarget(null);\n await refetch();\n } catch {\n // Error is surfaced by Apollo's error state on the mutation;\n // the ConfirmDialog stays open so the user can retry or cancel.\n }\n };\n\n // Filter keys: org scope + search + status\n const filteredKeys = apiKeys.filter((key: AuthApiKey) => {\n const matchesOrg = !orgAppIds || orgAppIds.has(key.appId);\n const matchesSearch =\n !searchQuery ||\n key.name.toLowerCase().includes(searchQuery.toLowerCase()) ||\n key.prefix.toLowerCase().includes(searchQuery.toLowerCase()) ||\n (key.appName?.toLowerCase().includes(searchQuery.toLowerCase()) ?? false);\n const matchesStatus =\n statusFilter === \"all\" ||\n (statusFilter === \"active\" && !isExpired(key.expiresAt)) ||\n (statusFilter === \"expired\" && isExpired(key.expiresAt));\n return matchesOrg && matchesSearch && matchesStatus;\n });\n\n const selectedKeys = useMemo(\n () => apiKeys.filter((key) => selectedKeyIds.has(key.id)),\n [apiKeys, selectedKeyIds],\n );\n\n const selectedActiveKeys = useMemo(\n () => selectedKeys.filter((key) => !isExpired(key.expiresAt)),\n [selectedKeys],\n );\n\n const allVisibleSelectableKeys = filteredKeys.filter(\n (key) => !isExpired(key.expiresAt),\n );\n const allVisibleSelected =\n allVisibleSelectableKeys.length > 0 &&\n allVisibleSelectableKeys.every((key) => selectedKeyIds.has(key.id));\n const partiallySelected =\n allVisibleSelectableKeys.some((key) => selectedKeyIds.has(key.id)) &&\n !allVisibleSelected;\n\n const toggleKeySelection = (keyId: string, checked: boolean) => {\n setSelectedKeyIds((prev) => {\n const next = new Set(prev);\n if (checked) {\n next.add(keyId);\n } else {\n next.delete(keyId);\n }\n return next;\n });\n };\n\n const toggleAllVisibleKeys = (checked: boolean) => {\n setSelectedKeyIds((prev) => {\n const next = new Set(prev);\n allVisibleSelectableKeys.forEach((key) => {\n if (checked) {\n next.add(key.id);\n } else {\n next.delete(key.id);\n }\n });\n return next;\n });\n };\n\n const clearSelection = () => setSelectedKeyIds(new Set());\n\n // Loading state\n if (loading && !data) {\n return (\n <PageLayout\n title={tr(\"devportal.apiKeys\", \"API Keys\")}\n description={tr(\n \"devportal.manageApiKeys\",\n \"Manage your API keys for programmatic access\",\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.loadingApiKeys\", \"Loading API keys...\")}\n </span>\n </div>\n </PageLayout>\n );\n }\n\n // Error state\n if (error) {\n return (\n <PageLayout\n title={tr(\"devportal.apiKeys\", \"API Keys\")}\n description={tr(\n \"devportal.manageApiKeys\",\n \"Manage your API keys for programmatic access\",\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.failedToLoadApiKeys\", \"Failed to load API keys\")}\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: \"select\",\n header: (\n <SelectionCheckbox\n checked={allVisibleSelected}\n indeterminate={partiallySelected}\n onChange={toggleAllVisibleKeys}\n ariaLabel=\"Select all visible API keys\"\n />\n ),\n className: \"w-12\",\n render: (key: AuthApiKey) => (\n <div onClick={(event) => event.stopPropagation()}>\n <SelectionCheckbox\n checked={selectedKeyIds.has(key.id)}\n disabled={isExpired(key.expiresAt)}\n onChange={(checked) => toggleKeySelection(key.id, checked)}\n ariaLabel={`Select API key ${key.name}`}\n />\n </div>\n ),\n },\n {\n key: \"name\",\n header: \"API Key\",\n render: (key: AuthApiKey) => (\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 isExpired(key.expiresAt)\n ? \"bg-status-error-bg-subtle text-status-error-text\"\n : \"bg-action-primary-bg/10 text-primary\",\n )}\n >\n <Key size={20} />\n </div>\n <div>\n <div className=\"flex items-center gap-2\">\n <p className=\"font-medium\">{key.name}</p>\n {isExpired(key.expiresAt) && (\n <span className=\"rounded bg-status-error-bg-subtle px-1.5 py-0.5 text-xs text-status-error-text\">\n Expired\n </span>\n )}\n {isExpiringSoon(key.expiresAt) && !isExpired(key.expiresAt) && (\n <span className=\"rounded bg-status-warning-bg-subtle px-1.5 py-0.5 text-xs text-status-warning-text\">\n Expiring Soon\n </span>\n )}\n </div>\n {key.appName && (\n <p className=\"text-xs text-text-secondary\">App: {key.appName}</p>\n )}\n </div>\n </div>\n ),\n },\n {\n key: \"prefix\",\n header: \"Key Prefix\",\n render: (key: AuthApiKey) => (\n <code className=\"text-sm text-text-secondary\">{key.prefix}...</code>\n ),\n },\n {\n key: \"scopes\",\n header: \"Scopes\",\n render: (key: AuthApiKey) => (\n <div className=\"flex flex-wrap gap-1\">\n {key.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 {key.scopes.length > 3 && (\n <span className=\"text-xs text-text-secondary\">\n +{key.scopes.length - 3}\n </span>\n )}\n </div>\n ),\n },\n {\n key: \"lastUsed\",\n header: \"Last Used\",\n render: (key: AuthApiKey) => (\n <span className=\"text-sm text-text-secondary\">\n {key.lastUsedAt\n ? new Date(key.lastUsedAt).toLocaleDateString()\n : \"Never\"}\n </span>\n ),\n },\n {\n key: \"expires\",\n header: \"Expires\",\n render: (key: AuthApiKey) => (\n <span\n className={cn(\n \"text-sm\",\n isExpired(key.expiresAt)\n ? \"text-status-error-text\"\n : isExpiringSoon(key.expiresAt)\n ? \"text-status-warning-text\"\n : \"text-text-secondary\",\n )}\n >\n {key.expiresAt\n ? new Date(key.expiresAt).toLocaleDateString()\n : \"Never\"}\n </span>\n ),\n },\n {\n key: \"status\",\n header: \"Status\",\n render: (key: AuthApiKey) => (\n <div className=\"flex items-center gap-2\">\n <ActiveIndicator active={!isExpired(key.expiresAt)} />\n <span className=\"text-sm\">\n {isExpired(key.expiresAt) ? \"Expired\" : \"Active\"}\n </span>\n </div>\n ),\n },\n {\n key: \"actions\",\n header: \"Actions\",\n className: \"w-28\",\n render: (key: AuthApiKey) => {\n if (isExpired(key.expiresAt)) {\n return (\n <span className=\"text-xs text-text-secondary\">No actions</span>\n );\n }\n\n return (\n <div className=\"flex items-center gap-1\">\n <button\n type=\"button\"\n title=\"Rotate secret\"\n disabled={rotateLoading}\n onClick={(e) => {\n e.stopPropagation();\n setRotateTarget(key);\n }}\n className={cn(\n \"inline-flex items-center gap-1 rounded-md px-2 py-1 text-xs font-medium transition-colors\",\n \"text-status-info-text hover:bg-status-info-bg-subtle\",\n \"disabled:opacity-50 disabled:cursor-not-allowed\",\n )}\n >\n <RotateCw size={14} />\n <span className=\"sr-only sm:not-sr-only\">Rotate</span>\n </button>\n <button\n type=\"button\"\n title=\"Revoke key\"\n disabled={revokeLoading}\n onClick={(e) => {\n e.stopPropagation();\n setRevokeTarget(key);\n }}\n className={cn(\n \"inline-flex items-center gap-1 rounded-md px-2 py-1 text-xs font-medium transition-colors\",\n \"text-status-error-text hover:bg-status-error-bg-subtle\",\n \"disabled:opacity-50 disabled:cursor-not-allowed\",\n )}\n >\n <Ban size={14} />\n <span className=\"sr-only sm:not-sr-only\">Revoke</span>\n </button>\n </div>\n );\n },\n },\n ];\n\n // Stats scoped to the filtered set (org context)\n const stats = {\n total: filteredKeys.length,\n active: filteredKeys.filter((k) => !isExpired(k.expiresAt)).length,\n expiringSoon: filteredKeys.filter((k) => isExpiringSoon(k.expiresAt))\n .length,\n expired: filteredKeys.filter((k) => isExpired(k.expiresAt)).length,\n };\n\n const handleBulkRevoke = async () => {\n if (selectedActiveKeys.length === 0) return;\n\n const results = await Promise.allSettled(\n selectedActiveKeys.map((key) =>\n revokeApiKey({ variables: { keyId: key.id } }),\n ),\n );\n\n const succeeded = results.filter(\n (result) => result.status === \"fulfilled\",\n ).length;\n const failed = results.length - succeeded;\n\n setBulkFeedback({\n type: failed === 0 ? \"success\" : \"error\",\n message: `Revoked ${succeeded} API key${succeeded === 1 ? \"\" : \"s\"}${\n failed > 0 ? ` • ${failed} failed` : \"\"\n }`,\n });\n setShowBulkRevokeDialog(false);\n clearSelection();\n await refetch();\n };\n\n return (\n <PermissionGuard authOnly>\n <PageLayout\n title={tr(\"devportal.apiKeys\", \"API Keys\")}\n description={tr(\n \"devportal.manageApiKeys\",\n \"Manage your API keys for programmatic access\",\n )}\n backLink={isFluidgrids ? \"/settings\" : undefined}\n backLabel={tr(\"devportal.developerSettings.back\", \"Developer settings\")}\n purpose={tr(\n \"devportal.apiKeysPurpose\",\n \"API keys authenticate server-to-server calls — use them in scripts, CI jobs, or backend services that talk to the platform without a logged-in user. Generate a key here, scope it to an app, and rotate it if it is ever exposed.\",\n )}\n actions={\n <button\n onClick={() => handleNavigate(\"/api-keys/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.generateApiKey\", \"Generate API Key\")}\n </button>\n }\n >\n {/* New secret alert — shown after a successful rotation */}\n {newSecret && (\n <NewSecretAlert\n title=\"API Key Rotated\"\n secret={newSecret}\n description=\"The previous secret has been invalidated. Copy the new secret below — it will not be shown again.\"\n onDismiss={() => setNewSecret(null)}\n />\n )}\n\n {bulkFeedback && (\n <div\n className={cn(\n \"rounded-lg border px-4 py-3 text-sm\",\n bulkFeedback.type === \"success\"\n ? \"border-border-subtle bg-status-success-bg-subtle text-status-success-text\"\n : \"border-status-error-border/40 bg-status-error-bg/10 text-status-error-text\",\n )}\n >\n <div className=\"flex items-center justify-between gap-3\">\n <span>{bulkFeedback.message}</span>\n <button\n onClick={() => setBulkFeedback(null)}\n className=\"text-xs font-medium underline underline-offset-2\"\n >\n Dismiss\n </button>\n </div>\n </div>\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 {/* Search */}\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=\"Search by name, prefix, or app...\"\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\n {/* Status Filter */}\n <select\n value={statusFilter}\n onChange={(e) =>\n setStatusFilter(e.target.value as \"all\" | \"active\" | \"expired\")\n }\n className=\"rounded-md border bg-bg-surface px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-primary scheme-normal dark:scheme-dark [&_option]:bg-bg-surface [&_option]:text-text-primary\"\n >\n <option value=\"all\">All Status</option>\n <option value=\"active\">Active</option>\n <option value=\"expired\">Expired</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 <Key size={16} />\n <span className=\"text-sm\">Total Keys</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\">Active</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-status-warning-text\">\n <Clock size={16} />\n <span className=\"text-sm\">Expiring Soon</span>\n </div>\n <p className=\"mt-1 text-2xl font-semibold tabular-nums\">\n {stats.expiringSoon}\n </p>\n </GlassCard>\n <GlassCard treatment=\"metric\" className=\"p-4\">\n <div className=\"flex items-center gap-2 text-status-error-text\">\n <AlertTriangle size={16} />\n <span className=\"text-sm\">Expired</span>\n </div>\n <p className=\"mt-1 text-2xl font-semibold tabular-nums\">\n {stats.expired}\n </p>\n </GlassCard>\n </div>\n </EmphasisPanel>\n\n {/* Results count */}\n <p className=\"text-sm text-text-secondary\">\n {filteredKeys.length} API key{filteredKeys.length !== 1 ? \"s\" : \"\"}{\" \"}\n found\n </p>\n\n {filteredKeys.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={toggleAllVisibleKeys}\n ariaLabel=\"Select all visible API keys\"\n />\n <span className=\"text-sm font-medium\">\n Select all active keys\n </span>\n </div>\n {selectedKeyIds.size > 0 && (\n <button\n onClick={clearSelection}\n className=\"text-sm text-text-secondary hover:text-text-primary\"\n >\n 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 {selectedKeyIds.size} selected\n </span>\n <button\n onClick={() => setShowBulkRevokeDialog(true)}\n disabled={selectedActiveKeys.length === 0}\n className=\"inline-flex items-center gap-2 rounded-md border border-status-error-bg px-3 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 <Ban size={16} />\n Revoke Selected\n {selectedActiveKeys.length > 0\n ? ` (${selectedActiveKeys.length})`\n : \"\"}\n </button>\n </div>\n </div>\n </div>\n )}\n\n {/* Keys Table */}\n {filteredKeys.length > 0 ? (\n <DataTable data={filteredKeys} columns={columns} keyField=\"id\" />\n ) : (\n <EmptyState\n illustration=\"empty-data\"\n title={tr(\"devportal.noApiKeysFound\", \"No API keys found\")}\n description={\n searchQuery || statusFilter !== \"all\"\n ? tr(\n \"devportal.tryAdjustingFilters\",\n \"Try adjusting your filters\",\n )\n : tr(\n \"devportal.generateFirstApiKey\",\n \"Generate your first API key to start making API calls\",\n )\n }\n action={\n !searchQuery && statusFilter === \"all\" ? (\n <button\n onClick={() => handleNavigate(\"/api-keys/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.generateApiKey\", \"Generate API Key\")}\n </button>\n ) : undefined\n }\n />\n )}\n\n {/* Revoke confirmation dialog */}\n <ConfirmDialog\n open={revokeTarget !== null}\n onOpenChange={(open) => {\n if (!open) setRevokeTarget(null);\n }}\n onConfirm={handleRevoke}\n title=\"Revoke API Key\"\n description={`Are you sure you want to revoke \"${revokeTarget?.name ?? \"\"}\"? This action cannot be undone. Any application or service using this key will immediately lose access.`}\n confirmLabel=\"Revoke Key\"\n cancelLabel=\"Cancel\"\n variant=\"danger\"\n loading={revokeLoading}\n />\n\n {/* Rotate confirmation dialog */}\n <ConfirmDialog\n open={rotateTarget !== null}\n onOpenChange={(open) => {\n if (!open) setRotateTarget(null);\n }}\n onConfirm={handleRotate}\n title=\"Rotate API Key Secret\"\n description={`This will generate a new secret for \"${rotateTarget?.name ?? \"\"}\" and immediately invalidate the current one. Make sure to update all services using this key before the old secret stops working.`}\n confirmLabel=\"Rotate Secret\"\n cancelLabel=\"Cancel\"\n variant=\"warning\"\n loading={rotateLoading}\n />\n\n <ConfirmDialog\n open={showBulkRevokeDialog}\n onOpenChange={setShowBulkRevokeDialog}\n onConfirm={handleBulkRevoke}\n title=\"Revoke selected API keys\"\n description={`Revoke ${selectedActiveKeys.length} selected API key${selectedActiveKeys.length === 1 ? \"\" : \"s\"}? Any service using these keys will immediately lose access.`}\n confirmLabel=\"Revoke Keys\"\n cancelLabel=\"Cancel\"\n variant=\"danger\"\n loading={revokeLoading}\n />\n </PageLayout>\n </PermissionGuard>\n );\n};\n"],"mappings":"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"}
@@ -1,15 +1,15 @@
1
1
  import e from "../../node_modules/date-fns/format.js";
2
2
  import { PageLayout as t, PageSection as n } from "../components/PageLayout.js";
3
- import { useGetApplicationAuditLogsQuery as r } from "../../generated/global-operations.js";
4
- import { getErrorMessage as i } from "../hooks/useGraphQL.js";
5
- import { EmptyState as a } from "../components/EmptyState.js";
3
+ import { EmptyState as r } from "../components/EmptyState.js";
4
+ import { useGetApplicationAuditLogsQuery as i } from "../../generated/global-operations.js";
5
+ import { getErrorMessage as a } from "../hooks/useGraphQL.js";
6
6
  import { useDevportal as o } from "../providers/DevportalProvider.js";
7
7
  import "../hooks/index.js";
8
- import { useEffect as s, useMemo as c, useState as l } from "react";
9
- import { AlertCircle as u, Download as d, Filter as f, Loader2 as p, RefreshCw as m } from "lucide-react";
10
- import { useParams as h } from "react-router";
11
- import { jsx as g, jsxs as _ } from "react/jsx-runtime";
12
- import { useI18n as v } from "@burdenoff/fe-libs/shared/providers/shell/I18nProvider";
8
+ import { useTr as s } from "../utils/i18n.js";
9
+ import { useEffect as c, useMemo as l, useState as u } from "react";
10
+ import { AlertCircle as d, Download as f, Filter as p, Loader2 as m, RefreshCw as h } from "lucide-react";
11
+ import { useParams as g } from "react-router";
12
+ import { jsx as _, jsxs as v } from "react/jsx-runtime";
13
13
  // src/devportal/pages/AppLogsPage.tsx
14
14
  var y = 25, b = (t) => e(new Date(t), "MMM d, yyyy HH:mm:ss"), x = (e) => e.replace(/_/g, " ").toLowerCase().replace(/\b\w/g, (e) => e.toUpperCase()), S = (e) => {
15
15
  if (!e) return "";
@@ -19,7 +19,7 @@ var y = 25, b = (t) => e(new Date(t), "MMM d, yyyy HH:mm:ss"), x = (e) => e.repl
19
19
  return "";
20
20
  }
21
21
  }, C = () => {
22
- let { t: e } = v(), { appId: C } = h(), { navigate: w, basePath: T, currentUser: E } = o(), D = (e) => E?.id === e && E.email ? E.email : e, [O, k] = l("all"), [A, j] = l(""), [M, N] = l(1), { data: P, loading: F, error: I, refetch: L } = r({
22
+ let e = s(), { appId: C } = g(), { navigate: w, basePath: T, currentUser: E } = o(), D = (e) => E?.id === e && E.email ? E.email : e, [O, k] = u("all"), [A, j] = u(""), [M, N] = u(1), { data: P, loading: F, error: I, refetch: L } = i({
23
23
  variables: {
24
24
  applicationId: C || "",
25
25
  limit: y,
@@ -28,13 +28,13 @@ var y = 25, b = (t) => e(new Date(t), "MMM d, yyyy HH:mm:ss"), x = (e) => e.repl
28
28
  skip: !C,
29
29
  fetchPolicy: "cache-and-network"
30
30
  }), R = P?.application;
31
- s(() => {
31
+ c(() => {
32
32
  let e = R?.name;
33
33
  if (!C || !e) return;
34
34
  let t = window.__breadcrumbRegistry;
35
35
  return t?.register(C, e), () => t?.unregister(C);
36
36
  }, [C, R?.name]);
37
- let z = P?.auditLogs?.totalCount ?? 0, B = P?.auditLogs?.edges?.map((e) => e.node) ?? [], V = c(() => Array.from(new Set(B.map((e) => e.action))).sort((e, t) => e.localeCompare(t)), [B]), H = c(() => {
37
+ let z = P?.auditLogs?.totalCount ?? 0, B = P?.auditLogs?.edges?.map((e) => e.node) ?? [], V = l(() => Array.from(new Set(B.map((e) => e.action))).sort((e, t) => e.localeCompare(t)), [B]), H = l(() => {
38
38
  let e = A.trim().toLowerCase();
39
39
  return B.filter((t) => O !== "all" && t.action !== O ? !1 : !e || [
40
40
  t.action,
@@ -47,152 +47,148 @@ var y = 25, b = (t) => e(new Date(t), "MMM d, yyyy HH:mm:ss"), x = (e) => e.repl
47
47
  O,
48
48
  B,
49
49
  A
50
- ]);
51
- return F && !P ? /* @__PURE__ */ g(t, {
52
- title: "Application Activity",
53
- description: "Loading application audit history",
50
+ ]), U = () => {
51
+ let e = new Blob([JSON.stringify(H, null, 2)], { type: "application/json" }), t = URL.createObjectURL(e), n = document.createElement("a");
52
+ n.href = t, n.download = `${R?.slug || C || "application"}-audit-log.json`, n.click(), URL.revokeObjectURL(t);
53
+ }, W = e("devportal.appLogs.title", "Application Activity"), G = e("devportal.appLogs.backToApp", "Back to Application");
54
+ return F && !P ? /* @__PURE__ */ _(t, {
55
+ title: W,
56
+ description: e("devportal.appLogs.loadingDescription", "Loading application audit history"),
54
57
  backLink: C ? `/apps/${C}` : "/apps",
55
- backLabel: "Back to Application",
56
- children: /* @__PURE__ */ g("div", {
58
+ backLabel: G,
59
+ children: /* @__PURE__ */ _("div", {
57
60
  className: "flex items-center justify-center py-16 text-text-secondary",
58
- children: /* @__PURE__ */ g(p, { className: "h-8 w-8 animate-spin" })
61
+ children: /* @__PURE__ */ _(m, { className: "h-8 w-8 animate-spin" })
59
62
  })
60
- }) : I || !R ? /* @__PURE__ */ g(t, {
61
- title: "Application Activity",
62
- description: "Review audit events for this application",
63
+ }) : I || !R ? /* @__PURE__ */ _(t, {
64
+ title: W,
65
+ description: e("devportal.appLogs.reviewDescription", "Review audit events for this application"),
63
66
  backLink: C ? `/apps/${C}` : "/apps",
64
- backLabel: "Back to Application",
65
- children: /* @__PURE__ */ g(a, {
66
- icon: /* @__PURE__ */ g(u, {
67
+ backLabel: G,
68
+ children: /* @__PURE__ */ _(r, {
69
+ icon: /* @__PURE__ */ _(d, {
67
70
  size: 32,
68
71
  className: "text-status-error-text"
69
72
  }),
70
- title: "Unable to load activity",
71
- description: I ? i(I) : "Application not found"
73
+ title: e("devportal.appLogs.errorTitle", "Unable to load activity"),
74
+ description: I ? a(I) : e("devportal.appLogs.notFound", "Application not found")
72
75
  })
73
- }) : /* @__PURE__ */ _(t, {
74
- title: "Application Activity",
75
- description: `Audit events for ${R.name}`,
76
- purpose: "A chronological audit trail of everything that happened to this app — config changes, secret rotations, webhook deliveries, and access events. Use it to debug integrations and to answer 'who changed what, and when?'",
76
+ }) : /* @__PURE__ */ v(t, {
77
+ title: W,
78
+ description: e("devportal.appLogs.eventsFor", "Audit events for {{name}}", { name: R.name }),
79
+ purpose: e("devportal.appLogs.purpose", "A chronological audit trail of everything that happened to this app — config changes, secret rotations, webhook deliveries, and access events. Use it to debug integrations and to answer 'who changed what, and when?'"),
77
80
  backLink: `/apps/${R.id}`,
78
- backLabel: "Back to Application",
79
- actions: /* @__PURE__ */ _("div", {
81
+ backLabel: G,
82
+ actions: /* @__PURE__ */ v("div", {
80
83
  className: "flex flex-wrap gap-2",
81
- children: [/* @__PURE__ */ _("button", {
84
+ children: [/* @__PURE__ */ v("button", {
82
85
  type: "button",
83
86
  onClick: () => void L(),
84
87
  className: "inline-flex items-center gap-2 rounded-md border px-3 py-2 text-sm font-medium hover:bg-accent",
85
- children: [/* @__PURE__ */ g(m, { size: 16 }), "Refresh"]
86
- }), /* @__PURE__ */ _("button", {
88
+ children: [/* @__PURE__ */ _(h, { size: 16 }), e("devportal.appLogs.refresh", "Refresh")]
89
+ }), /* @__PURE__ */ v("button", {
87
90
  type: "button",
88
- onClick: () => {
89
- let e = new Blob([JSON.stringify(H, null, 2)], { type: "application/json" }), t = URL.createObjectURL(e), n = document.createElement("a");
90
- n.href = t, n.download = `${R?.slug || C || "application"}-audit-log.json`, n.click(), URL.revokeObjectURL(t);
91
- },
91
+ onClick: U,
92
92
  className: "inline-flex items-center gap-2 rounded-md border px-3 py-2 text-sm font-medium hover:bg-accent",
93
- children: [/* @__PURE__ */ g(d, { size: 16 }), "Export JSON"]
93
+ children: [/* @__PURE__ */ _(f, { size: 16 }), e("devportal.appLogs.exportJson", "Export JSON")]
94
94
  })]
95
95
  }),
96
96
  children: [
97
- /* @__PURE__ */ _("div", {
97
+ /* @__PURE__ */ v("div", {
98
98
  className: "grid gap-4 rounded-card border border-border-seam bg-bg-surface p-4 lg:grid-cols-[220px_minmax(0,1fr)_auto] lg:items-center",
99
99
  children: [
100
- /* @__PURE__ */ _("label", {
100
+ /* @__PURE__ */ v("label", {
101
101
  className: "grid gap-2 text-sm font-medium",
102
- children: [/* @__PURE__ */ _("span", {
102
+ children: [/* @__PURE__ */ v("span", {
103
103
  className: "flex items-center gap-2 text-text-secondary",
104
- children: [/* @__PURE__ */ g(f, { size: 16 }), "Filter by action"]
105
- }), /* @__PURE__ */ _("select", {
104
+ children: [/* @__PURE__ */ _(p, { size: 16 }), e("devportal.appLogs.filterByAction", "Filter by action")]
105
+ }), /* @__PURE__ */ v("select", {
106
106
  value: O,
107
107
  onChange: (e) => k(e.target.value),
108
108
  className: "rounded-md border bg-bg-surface px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-primary scheme-normal dark:scheme-dark [&_option]:bg-bg-surface [&_option]:text-text-primary",
109
- children: [/* @__PURE__ */ g("option", {
109
+ children: [/* @__PURE__ */ _("option", {
110
110
  value: "all",
111
- children: "All actions"
112
- }), V.map((e) => /* @__PURE__ */ g("option", {
111
+ children: e("devportal.appLogs.allActions", "All actions")
112
+ }), V.map((e) => /* @__PURE__ */ _("option", {
113
113
  value: e,
114
114
  children: x(e)
115
115
  }, e))]
116
116
  })]
117
117
  }),
118
- /* @__PURE__ */ _("label", {
118
+ /* @__PURE__ */ v("label", {
119
119
  className: "grid gap-2 text-sm font-medium",
120
- children: [/* @__PURE__ */ g("span", {
120
+ children: [/* @__PURE__ */ _("span", {
121
121
  className: "text-text-secondary",
122
- children: "Search metadata"
123
- }), /* @__PURE__ */ g("input", {
122
+ children: e("devportal.appLogs.searchMetadata", "Search metadata")
123
+ }), /* @__PURE__ */ _("input", {
124
124
  type: "text",
125
125
  value: A,
126
126
  onChange: (e) => j(e.target.value),
127
- placeholder: "Search actor, action, or metadata",
127
+ placeholder: e("devportal.appLogs.searchPlaceholder", "Search actor, action, or metadata"),
128
128
  className: "rounded-md border bg-bg-surface px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-primary"
129
129
  })]
130
130
  }),
131
131
  /* @__PURE__ */ _("div", {
132
132
  className: "rounded-full bg-bg-sunken px-3 py-2 text-sm text-text-secondary",
133
- children: [z, " total events"]
133
+ children: e("devportal.appLogs.totalEvents", "{{count}} total events", { count: z })
134
134
  })
135
135
  ]
136
136
  }),
137
- /* @__PURE__ */ g(n, { children: H.length > 0 ? /* @__PURE__ */ g("div", {
137
+ /* @__PURE__ */ _(n, { children: H.length > 0 ? /* @__PURE__ */ _("div", {
138
138
  className: "space-y-3 rounded-card border border-border-seam bg-bg-surface p-4",
139
- children: H.map((e) => /* @__PURE__ */ _("article", {
139
+ children: H.map((t) => /* @__PURE__ */ v("article", {
140
140
  className: "rounded-card border border-border-seam bg-bg-surface p-4",
141
141
  children: [
142
- /* @__PURE__ */ _("div", {
142
+ /* @__PURE__ */ v("div", {
143
143
  className: "flex flex-col gap-3 lg:flex-row lg:items-start lg:justify-between",
144
- children: [/* @__PURE__ */ _("div", {
144
+ children: [/* @__PURE__ */ v("div", {
145
145
  className: "space-y-2",
146
- children: [/* @__PURE__ */ _("div", {
146
+ children: [/* @__PURE__ */ v("div", {
147
147
  className: "flex flex-wrap items-center gap-2",
148
- children: [/* @__PURE__ */ g("span", {
148
+ children: [/* @__PURE__ */ _("span", {
149
149
  className: "rounded-full bg-action-primary-bg/10 px-2.5 py-1 text-xs font-medium text-primary",
150
- children: x(e.action)
150
+ children: x(t.action)
151
151
  }), /* @__PURE__ */ _("span", {
152
152
  className: "text-xs text-text-secondary",
153
- children: ["Actor ", D(e.actorId)]
153
+ children: e("devportal.appLogs.actor", "Actor {{actor}}", { actor: D(t.actorId) })
154
154
  })]
155
- }), /* @__PURE__ */ _("p", {
155
+ }), /* @__PURE__ */ v("p", {
156
156
  className: "text-sm text-text-secondary",
157
- children: [
158
- "Resource: ",
159
- e.resource,
160
- e.resourceId ? ` · ${e.resourceId}` : ""
161
- ]
157
+ children: [e("devportal.appLogs.resource", "Resource: {{resource}}", { resource: t.resource }), t.resourceId ? ` · ${t.resourceId}` : ""]
162
158
  })]
163
- }), /* @__PURE__ */ g("time", {
159
+ }), /* @__PURE__ */ _("time", {
164
160
  className: "text-xs text-text-secondary",
165
- children: b(e.createdAt)
161
+ children: b(t.createdAt)
166
162
  })]
167
163
  }),
168
- e.metadata && /* @__PURE__ */ g("pre", {
164
+ t.metadata && /* @__PURE__ */ _("pre", {
169
165
  className: "mt-3 overflow-x-auto rounded-md bg-bg-sunken p-3 text-xs text-text-primary",
170
- children: S(e.metadata)
166
+ children: S(t.metadata)
171
167
  }),
172
- e.changes && /* @__PURE__ */ _("details", {
168
+ t.changes && /* @__PURE__ */ v("details", {
173
169
  className: "mt-3 rounded-md border bg-bg-surface p-3",
174
- children: [/* @__PURE__ */ g("summary", {
170
+ children: [/* @__PURE__ */ _("summary", {
175
171
  className: "cursor-pointer text-sm font-medium text-text-primary",
176
- children: "View captured changes"
177
- }), /* @__PURE__ */ g("pre", {
172
+ children: e("devportal.appLogs.viewChanges", "View captured changes")
173
+ }), /* @__PURE__ */ _("pre", {
178
174
  className: "mt-3 overflow-x-auto text-xs text-text-secondary",
179
- children: S(e.changes)
175
+ children: S(t.changes)
180
176
  })]
181
177
  })
182
178
  ]
183
- }, e.id))
184
- }) : /* @__PURE__ */ g(a, {
179
+ }, t.id))
180
+ }) : /* @__PURE__ */ _(r, {
185
181
  illustration: "empty-data",
186
- title: "No audit events found",
187
- description: "Events will appear here after application or webhook actions are performed through the public gateway."
182
+ title: e("devportal.appLogs.emptyTitle", "No audit events found"),
183
+ description: e("devportal.appLogs.emptyDescription", "Events will appear here after application or webhook actions are performed through the public gateway.")
188
184
  }) }),
189
- P.auditLogs?.pageInfo?.hasNextPage && /* @__PURE__ */ g("div", {
185
+ P.auditLogs?.pageInfo?.hasNextPage && /* @__PURE__ */ _("div", {
190
186
  className: "flex justify-center",
191
- children: /* @__PURE__ */ _("button", {
187
+ children: /* @__PURE__ */ v("button", {
192
188
  type: "button",
193
189
  onClick: () => N((e) => e + 1),
194
190
  className: "inline-flex items-center gap-2 rounded-md border px-4 py-2 text-sm font-medium hover:bg-accent",
195
- children: [/* @__PURE__ */ g(m, { size: 16 }), "Next page"]
191
+ children: [/* @__PURE__ */ _(h, { size: 16 }), e("devportal.appLogs.nextPage", "Next page")]
196
192
  })
197
193
  })
198
194
  ]
@@ -1 +1 @@
1
- {"version":3,"file":"AppLogsPage.js","names":["format","PAGE_SIZE","formatTimestamp","value","formatAction","character","metadataPreview","metadata","AppLogsPage","useI18n","useParams","useDevportal","resolveActorLabel","currentUser","actorId","actionFilter","setActionFilter","useState","searchQuery","setSearchQuery","page","setPage","useGetApplicationAuditLogsQuery","appId","application","data","name","reg","totalCount","auditLogs","edge","availableActions","useMemo","entry","left","right","filteredLogs","normalizedQuery","loading","PageLayout","Loader2","error","EmptyState","AlertCircle","getErrorMessage","refetch","RefreshCw","blob","url","anchor","Download","Filter","event","action","PageSection","previous"],"sources":["../../../src/devportal/pages/AppLogsPage.tsx"],"sourcesContent":["import type { FC } from \"react\";\nimport { useEffect, useMemo, useState } from \"react\";\nimport { useParams } from \"react-router\";\nimport { useI18n } from \"@burdenoff/fe-libs/shared/providers/shell/I18nProvider\";\nimport {\n Download,\n Filter,\n Loader2,\n RefreshCw,\n AlertCircle,\n FileClock,\n} from \"lucide-react\";\nimport { format } from \"date-fns\";\nimport { PageLayout, PageSection } from \"../components/PageLayout\";\nimport { EmptyState } from \"../components/EmptyState\";\nimport { useDevportal } from \"../providers/DevportalProvider\";\nimport {\n type GetApplicationAuditLogsQuery,\n getErrorMessage,\n useGetApplicationAuditLogsQuery,\n} from \"../hooks\";\n\nconst PAGE_SIZE = 25;\ntype AuditLogEntry =\n GetApplicationAuditLogsQuery[\"auditLogs\"][\"edges\"][number][\"node\"];\n\nconst formatTimestamp = (value: string) =>\n format(new Date(value), \"MMM d, yyyy HH:mm:ss\");\n\nconst formatAction = (value: string) =>\n value\n .replace(/_/g, \" \")\n .toLowerCase()\n .replace(/\\b\\w/g, (character) => character.toUpperCase());\n\nconst metadataPreview = (metadata: unknown): string => {\n if (!metadata) return \"\";\n\n try {\n return JSON.stringify(metadata, null, 2);\n } catch {\n return \"\";\n }\n};\n\nexport const AppLogsPage: 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 { appId } = useParams<{ appId: string }>();\n const { navigate, basePath, currentUser } = useDevportal();\n const resolveActorLabel = (actorId: string): string => {\n if (currentUser?.id === actorId && currentUser.email)\n return currentUser.email;\n return actorId;\n };\n const [actionFilter, setActionFilter] = useState(\"all\");\n const [searchQuery, setSearchQuery] = useState(\"\");\n const [page, setPage] = useState(1);\n\n const { data, loading, error, refetch } = useGetApplicationAuditLogsQuery({\n variables: {\n applicationId: appId || \"\",\n limit: PAGE_SIZE,\n offset: (page - 1) * PAGE_SIZE,\n },\n skip: !appId,\n fetchPolicy: \"cache-and-network\",\n });\n\n const application = data?.application;\n\n useEffect(() => {\n const name = application?.name;\n if (!appId || !name) return;\n const reg = (\n window as unknown as {\n __breadcrumbRegistry?: {\n register: (id: string, name: string) => void;\n unregister: (id: string) => void;\n };\n }\n ).__breadcrumbRegistry;\n reg?.register(appId, name);\n return () => reg?.unregister(appId);\n }, [appId, application?.name]);\n\n const totalCount = data?.auditLogs?.totalCount ?? 0;\n const auditLogs: AuditLogEntry[] =\n data?.auditLogs?.edges?.map(\n (edge: GetApplicationAuditLogsQuery[\"auditLogs\"][\"edges\"][number]) =>\n edge.node,\n ) ?? [];\n\n const availableActions = useMemo(\n () =>\n Array.from(\n new Set(auditLogs.map((entry: AuditLogEntry) => entry.action)),\n ).sort((left, right) => left.localeCompare(right)),\n [auditLogs],\n );\n\n const filteredLogs = useMemo(() => {\n const normalizedQuery = searchQuery.trim().toLowerCase();\n\n return auditLogs.filter((entry: AuditLogEntry) => {\n const matchesAction =\n actionFilter === \"all\" || entry.action === actionFilter;\n if (!matchesAction) return false;\n\n if (!normalizedQuery) return true;\n\n const haystack = [\n entry.action,\n entry.actorId,\n resolveActorLabel(entry.actorId),\n entry.resource,\n metadataPreview(entry.metadata),\n ]\n .join(\" \")\n .toLowerCase();\n\n return haystack.includes(normalizedQuery);\n });\n }, [actionFilter, auditLogs, searchQuery]);\n\n const exportPayload = () => {\n const blob = new Blob([JSON.stringify(filteredLogs, null, 2)], {\n type: \"application/json\",\n });\n const url = URL.createObjectURL(blob);\n const anchor = document.createElement(\"a\");\n anchor.href = url;\n anchor.download = `${application?.slug || appId || \"application\"}-audit-log.json`;\n anchor.click();\n URL.revokeObjectURL(url);\n };\n\n if (loading && !data) {\n return (\n <PageLayout\n title=\"Application Activity\"\n description=\"Loading application audit history\"\n backLink={appId ? `/apps/${appId}` : \"/apps\"}\n backLabel=\"Back to Application\"\n >\n <div className=\"flex items-center justify-center py-16 text-text-secondary\">\n <Loader2 className=\"h-8 w-8 animate-spin\" />\n </div>\n </PageLayout>\n );\n }\n\n if (error || !application) {\n return (\n <PageLayout\n title=\"Application Activity\"\n description=\"Review audit events for this application\"\n backLink={appId ? `/apps/${appId}` : \"/apps\"}\n backLabel=\"Back to Application\"\n >\n <EmptyState\n icon={<AlertCircle size={32} className=\"text-status-error-text\" />}\n title=\"Unable to load activity\"\n description={error ? getErrorMessage(error) : \"Application not found\"}\n />\n </PageLayout>\n );\n }\n\n return (\n <PageLayout\n title=\"Application Activity\"\n description={`Audit events for ${application.name}`}\n purpose=\"A chronological audit trail of everything that happened to this app — config changes, secret rotations, webhook deliveries, and access events. Use it to debug integrations and to answer 'who changed what, and when?'\"\n backLink={`/apps/${application.id}`}\n backLabel=\"Back to Application\"\n actions={\n <div className=\"flex flex-wrap gap-2\">\n <button\n type=\"button\"\n onClick={() => void refetch()}\n className=\"inline-flex items-center gap-2 rounded-md border px-3 py-2 text-sm font-medium hover:bg-accent\"\n >\n <RefreshCw size={16} />\n Refresh\n </button>\n <button\n type=\"button\"\n onClick={exportPayload}\n className=\"inline-flex items-center gap-2 rounded-md border px-3 py-2 text-sm font-medium hover:bg-accent\"\n >\n <Download size={16} />\n Export JSON\n </button>\n </div>\n }\n >\n <div className=\"grid gap-4 rounded-card border border-border-seam bg-bg-surface p-4 lg:grid-cols-[220px_minmax(0,1fr)_auto] lg:items-center\">\n <label className=\"grid gap-2 text-sm font-medium\">\n <span className=\"flex items-center gap-2 text-text-secondary\">\n <Filter size={16} />\n Filter by action\n </span>\n <select\n value={actionFilter}\n onChange={(event) => setActionFilter(event.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 scheme-normal dark:scheme-dark [&_option]:bg-bg-surface [&_option]:text-text-primary\"\n >\n <option value=\"all\">All actions</option>\n {availableActions.map((action) => (\n <option key={action} value={action}>\n {formatAction(action)}\n </option>\n ))}\n </select>\n </label>\n\n <label className=\"grid gap-2 text-sm font-medium\">\n <span className=\"text-text-secondary\">Search metadata</span>\n <input\n type=\"text\"\n value={searchQuery}\n onChange={(event) => setSearchQuery(event.target.value)}\n placeholder=\"Search actor, action, or metadata\"\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 </label>\n\n <div className=\"rounded-full bg-bg-sunken px-3 py-2 text-sm text-text-secondary\">\n {totalCount} total events\n </div>\n </div>\n\n <PageSection>\n {filteredLogs.length > 0 ? (\n <div className=\"space-y-3 rounded-card border border-border-seam bg-bg-surface p-4\">\n {filteredLogs.map((entry: AuditLogEntry) => (\n <article\n key={entry.id}\n className=\"rounded-card border border-border-seam bg-bg-surface p-4\"\n >\n <div className=\"flex flex-col gap-3 lg:flex-row lg:items-start lg:justify-between\">\n <div className=\"space-y-2\">\n <div className=\"flex flex-wrap items-center gap-2\">\n <span className=\"rounded-full bg-action-primary-bg/10 px-2.5 py-1 text-xs font-medium text-primary\">\n {formatAction(entry.action)}\n </span>\n <span className=\"text-xs text-text-secondary\">\n Actor {resolveActorLabel(entry.actorId)}\n </span>\n </div>\n <p className=\"text-sm text-text-secondary\">\n Resource: {entry.resource}\n {entry.resourceId ? ` · ${entry.resourceId}` : \"\"}\n </p>\n </div>\n\n <time className=\"text-xs text-text-secondary\">\n {formatTimestamp(entry.createdAt)}\n </time>\n </div>\n\n {entry.metadata && (\n <pre className=\"mt-3 overflow-x-auto rounded-md bg-bg-sunken p-3 text-xs text-text-primary\">\n {metadataPreview(entry.metadata)}\n </pre>\n )}\n\n {entry.changes && (\n <details className=\"mt-3 rounded-md border bg-bg-surface p-3\">\n <summary className=\"cursor-pointer text-sm font-medium text-text-primary\">\n View captured changes\n </summary>\n <pre className=\"mt-3 overflow-x-auto text-xs text-text-secondary\">\n {metadataPreview(entry.changes)}\n </pre>\n </details>\n )}\n </article>\n ))}\n </div>\n ) : (\n <EmptyState\n illustration=\"empty-data\"\n title=\"No audit events found\"\n description=\"Events will appear here after application or webhook actions are performed through the public gateway.\"\n />\n )}\n </PageSection>\n\n {data.auditLogs?.pageInfo?.hasNextPage && (\n <div className=\"flex justify-center\">\n <button\n type=\"button\"\n onClick={() => setPage((previous) => previous + 1)}\n className=\"inline-flex items-center gap-2 rounded-md border px-4 py-2 text-sm font-medium hover:bg-accent\"\n >\n <RefreshCw size={16} />\n Next page\n </button>\n </div>\n )}\n </PageLayout>\n );\n};\n"],"mappings":"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"}
1
+ {"version":3,"file":"AppLogsPage.js","names":["format","PAGE_SIZE","formatTimestamp","value","formatAction","character","metadataPreview","metadata","AppLogsPage","tr","useTr","useParams","useDevportal","resolveActorLabel","currentUser","actorId","actionFilter","setActionFilter","useState","searchQuery","setSearchQuery","page","setPage","useGetApplicationAuditLogsQuery","appId","application","data","name","reg","totalCount","auditLogs","edge","availableActions","useMemo","entry","left","right","filteredLogs","normalizedQuery","exportPayload","blob","url","anchor","title","backLabel","loading","PageLayout","Loader2","error","EmptyState","AlertCircle","getErrorMessage","refetch","RefreshCw","Download","Filter","event","action","PageSection","previous"],"sources":["../../../src/devportal/pages/AppLogsPage.tsx"],"sourcesContent":["import type { FC } from \"react\";\nimport { useEffect, useMemo, useState } from \"react\";\nimport { useParams } from \"react-router\";\nimport {\n Download,\n Filter,\n Loader2,\n RefreshCw,\n AlertCircle,\n FileClock,\n} from \"lucide-react\";\nimport { format } from \"date-fns\";\nimport { PageLayout, PageSection } from \"../components/PageLayout\";\nimport { EmptyState } from \"../components/EmptyState\";\nimport { useDevportal } from \"../providers/DevportalProvider\";\nimport {\n type GetApplicationAuditLogsQuery,\n getErrorMessage,\n useGetApplicationAuditLogsQuery,\n} from \"../hooks\";\nimport { useTr } from \"../utils/i18n\";\n\nconst PAGE_SIZE = 25;\ntype AuditLogEntry =\n GetApplicationAuditLogsQuery[\"auditLogs\"][\"edges\"][number][\"node\"];\n\nconst formatTimestamp = (value: string) =>\n format(new Date(value), \"MMM d, yyyy HH:mm:ss\");\n\nconst formatAction = (value: string) =>\n value\n .replace(/_/g, \" \")\n .toLowerCase()\n .replace(/\\b\\w/g, (character) => character.toUpperCase());\n\nconst metadataPreview = (metadata: unknown): string => {\n if (!metadata) return \"\";\n\n try {\n return JSON.stringify(metadata, null, 2);\n } catch {\n return \"\";\n }\n};\n\nexport const AppLogsPage: FC = () => {\n const tr = useTr();\n const { appId } = useParams<{ appId: string }>();\n const { navigate, basePath, currentUser } = useDevportal();\n const resolveActorLabel = (actorId: string): string => {\n if (currentUser?.id === actorId && currentUser.email)\n return currentUser.email;\n return actorId;\n };\n const [actionFilter, setActionFilter] = useState(\"all\");\n const [searchQuery, setSearchQuery] = useState(\"\");\n const [page, setPage] = useState(1);\n\n const { data, loading, error, refetch } = useGetApplicationAuditLogsQuery({\n variables: {\n applicationId: appId || \"\",\n limit: PAGE_SIZE,\n offset: (page - 1) * PAGE_SIZE,\n },\n skip: !appId,\n fetchPolicy: \"cache-and-network\",\n });\n\n const application = data?.application;\n\n useEffect(() => {\n const name = application?.name;\n if (!appId || !name) return;\n const reg = (\n window as unknown as {\n __breadcrumbRegistry?: {\n register: (id: string, name: string) => void;\n unregister: (id: string) => void;\n };\n }\n ).__breadcrumbRegistry;\n reg?.register(appId, name);\n return () => reg?.unregister(appId);\n }, [appId, application?.name]);\n\n const totalCount = data?.auditLogs?.totalCount ?? 0;\n const auditLogs: AuditLogEntry[] =\n data?.auditLogs?.edges?.map(\n (edge: GetApplicationAuditLogsQuery[\"auditLogs\"][\"edges\"][number]) =>\n edge.node,\n ) ?? [];\n\n const availableActions = useMemo(\n () =>\n Array.from(\n new Set(auditLogs.map((entry: AuditLogEntry) => entry.action)),\n ).sort((left, right) => left.localeCompare(right)),\n [auditLogs],\n );\n\n const filteredLogs = useMemo(() => {\n const normalizedQuery = searchQuery.trim().toLowerCase();\n\n return auditLogs.filter((entry: AuditLogEntry) => {\n const matchesAction =\n actionFilter === \"all\" || entry.action === actionFilter;\n if (!matchesAction) return false;\n\n if (!normalizedQuery) return true;\n\n const haystack = [\n entry.action,\n entry.actorId,\n resolveActorLabel(entry.actorId),\n entry.resource,\n metadataPreview(entry.metadata),\n ]\n .join(\" \")\n .toLowerCase();\n\n return haystack.includes(normalizedQuery);\n });\n }, [actionFilter, auditLogs, searchQuery]);\n\n const exportPayload = () => {\n const blob = new Blob([JSON.stringify(filteredLogs, null, 2)], {\n type: \"application/json\",\n });\n const url = URL.createObjectURL(blob);\n const anchor = document.createElement(\"a\");\n anchor.href = url;\n anchor.download = `${application?.slug || appId || \"application\"}-audit-log.json`;\n anchor.click();\n URL.revokeObjectURL(url);\n };\n\n const title = tr(\"devportal.appLogs.title\", \"Application Activity\");\n const backLabel = tr(\"devportal.appLogs.backToApp\", \"Back to Application\");\n\n if (loading && !data) {\n return (\n <PageLayout\n title={title}\n description={tr(\n \"devportal.appLogs.loadingDescription\",\n \"Loading application audit history\",\n )}\n backLink={appId ? `/apps/${appId}` : \"/apps\"}\n backLabel={backLabel}\n >\n <div className=\"flex items-center justify-center py-16 text-text-secondary\">\n <Loader2 className=\"h-8 w-8 animate-spin\" />\n </div>\n </PageLayout>\n );\n }\n\n if (error || !application) {\n return (\n <PageLayout\n title={title}\n description={tr(\n \"devportal.appLogs.reviewDescription\",\n \"Review audit events for this application\",\n )}\n backLink={appId ? `/apps/${appId}` : \"/apps\"}\n backLabel={backLabel}\n >\n <EmptyState\n icon={<AlertCircle size={32} className=\"text-status-error-text\" />}\n title={tr(\"devportal.appLogs.errorTitle\", \"Unable to load activity\")}\n description={\n error\n ? getErrorMessage(error)\n : tr(\"devportal.appLogs.notFound\", \"Application not found\")\n }\n />\n </PageLayout>\n );\n }\n\n return (\n <PageLayout\n title={title}\n description={tr(\n \"devportal.appLogs.eventsFor\",\n \"Audit events for {{name}}\",\n { name: application.name },\n )}\n purpose={tr(\n \"devportal.appLogs.purpose\",\n \"A chronological audit trail of everything that happened to this app — config changes, secret rotations, webhook deliveries, and access events. Use it to debug integrations and to answer 'who changed what, and when?'\",\n )}\n backLink={`/apps/${application.id}`}\n backLabel={backLabel}\n actions={\n <div className=\"flex flex-wrap gap-2\">\n <button\n type=\"button\"\n onClick={() => void refetch()}\n className=\"inline-flex items-center gap-2 rounded-md border px-3 py-2 text-sm font-medium hover:bg-accent\"\n >\n <RefreshCw size={16} />\n {tr(\"devportal.appLogs.refresh\", \"Refresh\")}\n </button>\n <button\n type=\"button\"\n onClick={exportPayload}\n className=\"inline-flex items-center gap-2 rounded-md border px-3 py-2 text-sm font-medium hover:bg-accent\"\n >\n <Download size={16} />\n {tr(\"devportal.appLogs.exportJson\", \"Export JSON\")}\n </button>\n </div>\n }\n >\n <div className=\"grid gap-4 rounded-card border border-border-seam bg-bg-surface p-4 lg:grid-cols-[220px_minmax(0,1fr)_auto] lg:items-center\">\n <label className=\"grid gap-2 text-sm font-medium\">\n <span className=\"flex items-center gap-2 text-text-secondary\">\n <Filter size={16} />\n {tr(\"devportal.appLogs.filterByAction\", \"Filter by action\")}\n </span>\n <select\n value={actionFilter}\n onChange={(event) => setActionFilter(event.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 scheme-normal dark:scheme-dark [&_option]:bg-bg-surface [&_option]:text-text-primary\"\n >\n <option value=\"all\">\n {tr(\"devportal.appLogs.allActions\", \"All actions\")}\n </option>\n {availableActions.map((action) => (\n <option key={action} value={action}>\n {formatAction(action)}\n </option>\n ))}\n </select>\n </label>\n\n <label className=\"grid gap-2 text-sm font-medium\">\n <span className=\"text-text-secondary\">\n {tr(\"devportal.appLogs.searchMetadata\", \"Search metadata\")}\n </span>\n <input\n type=\"text\"\n value={searchQuery}\n onChange={(event) => setSearchQuery(event.target.value)}\n placeholder={tr(\n \"devportal.appLogs.searchPlaceholder\",\n \"Search actor, action, or metadata\",\n )}\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 </label>\n\n <div className=\"rounded-full bg-bg-sunken px-3 py-2 text-sm text-text-secondary\">\n {tr(\"devportal.appLogs.totalEvents\", \"{{count}} total events\", {\n count: totalCount,\n })}\n </div>\n </div>\n\n <PageSection>\n {filteredLogs.length > 0 ? (\n <div className=\"space-y-3 rounded-card border border-border-seam bg-bg-surface p-4\">\n {filteredLogs.map((entry: AuditLogEntry) => (\n <article\n key={entry.id}\n className=\"rounded-card border border-border-seam bg-bg-surface p-4\"\n >\n <div className=\"flex flex-col gap-3 lg:flex-row lg:items-start lg:justify-between\">\n <div className=\"space-y-2\">\n <div className=\"flex flex-wrap items-center gap-2\">\n <span className=\"rounded-full bg-action-primary-bg/10 px-2.5 py-1 text-xs font-medium text-primary\">\n {formatAction(entry.action)}\n </span>\n <span className=\"text-xs text-text-secondary\">\n {tr(\"devportal.appLogs.actor\", \"Actor {{actor}}\", {\n actor: resolveActorLabel(entry.actorId),\n })}\n </span>\n </div>\n <p className=\"text-sm text-text-secondary\">\n {tr(\n \"devportal.appLogs.resource\",\n \"Resource: {{resource}}\",\n {\n resource: entry.resource,\n },\n )}\n {entry.resourceId ? ` · ${entry.resourceId}` : \"\"}\n </p>\n </div>\n\n <time className=\"text-xs text-text-secondary\">\n {formatTimestamp(entry.createdAt)}\n </time>\n </div>\n\n {entry.metadata && (\n <pre className=\"mt-3 overflow-x-auto rounded-md bg-bg-sunken p-3 text-xs text-text-primary\">\n {metadataPreview(entry.metadata)}\n </pre>\n )}\n\n {entry.changes && (\n <details className=\"mt-3 rounded-md border bg-bg-surface p-3\">\n <summary className=\"cursor-pointer text-sm font-medium text-text-primary\">\n {tr(\n \"devportal.appLogs.viewChanges\",\n \"View captured changes\",\n )}\n </summary>\n <pre className=\"mt-3 overflow-x-auto text-xs text-text-secondary\">\n {metadataPreview(entry.changes)}\n </pre>\n </details>\n )}\n </article>\n ))}\n </div>\n ) : (\n <EmptyState\n illustration=\"empty-data\"\n title={tr(\"devportal.appLogs.emptyTitle\", \"No audit events found\")}\n description={tr(\n \"devportal.appLogs.emptyDescription\",\n \"Events will appear here after application or webhook actions are performed through the public gateway.\",\n )}\n />\n )}\n </PageSection>\n\n {data.auditLogs?.pageInfo?.hasNextPage && (\n <div className=\"flex justify-center\">\n <button\n type=\"button\"\n onClick={() => setPage((previous) => previous + 1)}\n className=\"inline-flex items-center gap-2 rounded-md border px-4 py-2 text-sm font-medium hover:bg-accent\"\n >\n <RefreshCw size={16} />\n {tr(\"devportal.appLogs.nextPage\", \"Next page\")}\n </button>\n </div>\n )}\n </PageLayout>\n );\n};\n"],"mappings":"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"}