@burdenoff/microfe-devportal 2026.830.1 → 2026.831.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/devportal/AdminDevportalRoot.d.ts.map +1 -1
- package/dist/devportal/AdminDevportalRoot.js +16 -19
- package/dist/devportal/AdminDevportalRoot.js.map +1 -1
- package/dist/devportal/DevportalRoot.d.ts.map +1 -1
- package/dist/devportal/DevportalRoot.js +36 -39
- package/dist/devportal/DevportalRoot.js.map +1 -1
- package/dist/devportal/components/AppCard.js +41 -41
- package/dist/devportal/components/ConfirmDialog.d.ts.map +1 -1
- package/dist/devportal/components/ConfirmDialog.js +19 -22
- package/dist/devportal/components/ConfirmDialog.js.map +1 -1
- package/dist/devportal/components/DataTable.d.ts.map +1 -1
- package/dist/devportal/components/DataTable.js +31 -34
- package/dist/devportal/components/DataTable.js.map +1 -1
- package/dist/devportal/components/FeatureFlaggedRoute.js +8 -8
- package/dist/devportal/components/FormField.d.ts.map +1 -1
- package/dist/devportal/components/FormField.js +26 -32
- package/dist/devportal/components/FormField.js.map +1 -1
- package/dist/devportal/components/GrantDelegationModal.js +64 -64
- package/dist/devportal/components/GrantDelegationModal.js.map +1 -1
- package/dist/devportal/components/LoadingState.d.ts.map +1 -1
- package/dist/devportal/components/LoadingState.js +5 -8
- package/dist/devportal/components/LoadingState.js.map +1 -1
- package/dist/devportal/components/PageLayout.d.ts.map +1 -1
- package/dist/devportal/components/PageLayout.js +10 -13
- package/dist/devportal/components/PageLayout.js.map +1 -1
- package/dist/devportal/components/PermissionGuard.d.ts.map +1 -1
- package/dist/devportal/components/PermissionGuard.js +11 -14
- package/dist/devportal/components/PermissionGuard.js.map +1 -1
- package/dist/devportal/components/SecretDisplay.d.ts.map +1 -1
- package/dist/devportal/components/SecretDisplay.js +24 -30
- package/dist/devportal/components/SecretDisplay.js.map +1 -1
- package/dist/devportal/components/SectionNav.d.ts.map +1 -1
- package/dist/devportal/components/SectionNav.js +24 -27
- package/dist/devportal/components/SectionNav.js.map +1 -1
- package/dist/devportal/components/StatusBadge.d.ts.map +1 -1
- package/dist/devportal/components/StatusBadge.js +24 -33
- package/dist/devportal/components/StatusBadge.js.map +1 -1
- package/dist/devportal/index.d.ts +1 -0
- package/dist/devportal/index.d.ts.map +1 -1
- package/dist/devportal/index.js +1 -0
- package/dist/devportal/pages/AdminReviewPage.js +172 -172
- package/dist/devportal/pages/AnalyticsDashboardPage.js +43 -43
- package/dist/devportal/pages/AnalyticsDashboardPage.js.map +1 -1
- package/dist/devportal/pages/ApiKeysPage.js +119 -119
- package/dist/devportal/pages/ApiKeysPage.js.map +1 -1
- package/dist/devportal/pages/AppLogsPage.js +45 -45
- package/dist/devportal/pages/ApplicationDetailPage.js +607 -607
- package/dist/devportal/pages/ApplicationVersionsPage.js +13 -13
- package/dist/devportal/pages/ApplicationsPage.d.ts.map +1 -1
- package/dist/devportal/pages/ApplicationsPage.js +104 -101
- package/dist/devportal/pages/ApplicationsPage.js.map +1 -1
- package/dist/devportal/pages/CreateApiKeyPage.js +45 -45
- package/dist/devportal/pages/CreateApplicationPage.js +38 -38
- package/dist/devportal/pages/CreateApplicationPage.js.map +1 -1
- package/dist/devportal/pages/CreateOAuthClientPage.js +52 -52
- package/dist/devportal/pages/CreateOrganizationPage.js +26 -26
- package/dist/devportal/pages/CreateWebhookPage.d.ts.map +1 -1
- package/dist/devportal/pages/CreateWebhookPage.js +48 -48
- package/dist/devportal/pages/CreateWebhookPage.js.map +1 -1
- package/dist/devportal/pages/DashboardPage.d.ts.map +1 -1
- package/dist/devportal/pages/DashboardPage.js +66 -72
- package/dist/devportal/pages/DashboardPage.js.map +1 -1
- package/dist/devportal/pages/DelegationsPage.js +174 -174
- package/dist/devportal/pages/DocumentationPage.js +93 -93
- package/dist/devportal/pages/EditApplicationPage.js +14 -14
- package/dist/devportal/pages/EditApplicationPage.js.map +1 -1
- package/dist/devportal/pages/ListingPreviewPage.js +76 -76
- package/dist/devportal/pages/OAuthClientDetailPage.js +75 -75
- package/dist/devportal/pages/OAuthClientDetailPage.js.map +1 -1
- package/dist/devportal/pages/OAuthClientsPage.js +64 -64
- package/dist/devportal/pages/OrganizationDetailPage.js +189 -189
- package/dist/devportal/pages/OrganizationDetailPage.js.map +1 -1
- package/dist/devportal/pages/OrganizationMembersPage.js +10 -10
- package/dist/devportal/pages/OrganizationsPage.d.ts.map +1 -1
- package/dist/devportal/pages/OrganizationsPage.js +163 -154
- package/dist/devportal/pages/OrganizationsPage.js.map +1 -1
- package/dist/devportal/pages/PublishPage.js +104 -104
- package/dist/devportal/pages/PublishPage.js.map +1 -1
- package/dist/devportal/pages/PublisherProfilePage.js +55 -55
- package/dist/devportal/pages/ResourcesPage.js +60 -60
- package/dist/devportal/pages/RevenueDashboardPage.js +60 -60
- package/dist/devportal/pages/RevenueDashboardPage.js.map +1 -1
- package/dist/devportal/pages/SellerDashboardPage.js +83 -83
- package/dist/devportal/pages/SellerItemsPage.js +163 -163
- package/dist/devportal/pages/SellerItemsPage.js.map +1 -1
- package/dist/devportal/pages/SellerOrdersPage.js +63 -63
- package/dist/devportal/pages/SettingsPage.js +21 -21
- package/dist/devportal/pages/SetupPage.js +183 -183
- package/dist/devportal/pages/SetupPage.js.map +1 -1
- package/dist/devportal/pages/StoreSubmissionPage.js +5 -5
- package/dist/devportal/pages/WebhookDetailPage.js +123 -123
- package/dist/devportal/pages/WebhookDetailPage.js.map +1 -1
- package/dist/devportal/pages/WebhooksPage.js +118 -118
- package/dist/devportal/pages/WebhooksPage.js.map +1 -1
- package/dist/devportal/utils/i18n.d.ts +9 -2
- package/dist/devportal/utils/i18n.d.ts.map +1 -1
- package/dist/devportal/utils/i18n.js +13 -10
- package/dist/devportal/utils/i18n.js.map +1 -1
- package/dist/index.js +2 -1
- package/dist/translations/en.d.ts +1251 -0
- package/dist/translations/en.d.ts.map +1 -0
- package/dist/translations/en.js +1235 -0
- package/dist/translations/en.js.map +1 -0
- package/package.json +2 -2
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ApiKeysPage.js","names":[],"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\"\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":[],"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\"\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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"}
|
|
@@ -6,10 +6,10 @@ import { PageLayout as i, PageSection as a } from "../components/PageLayout.js";
|
|
|
6
6
|
import "../hooks/index.js";
|
|
7
7
|
import o from "../../node_modules/date-fns/format.js";
|
|
8
8
|
import { useEffect as s, useMemo as c, useState as l } from "react";
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
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";
|
|
13
13
|
//#region src/devportal/pages/AppLogsPage.tsx
|
|
14
14
|
var y = 25, b = (e) => o(new Date(e), "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 = (e) => o(new Date(e), "MMM d, yyyy HH:mm:ss"), x = (e) => e.repl
|
|
|
19
19
|
return "";
|
|
20
20
|
}
|
|
21
21
|
}, C = () => {
|
|
22
|
-
let { t: o } =
|
|
22
|
+
let { t: o } = v(), { appId: C } = h(), { navigate: w, basePath: T, currentUser: E } = e(), 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 } = n({
|
|
23
23
|
variables: {
|
|
24
24
|
applicationId: C || "",
|
|
25
25
|
limit: y,
|
|
@@ -48,79 +48,79 @@ var y = 25, b = (e) => o(new Date(e), "MMM d, yyyy HH:mm:ss"), x = (e) => e.repl
|
|
|
48
48
|
B,
|
|
49
49
|
A
|
|
50
50
|
]);
|
|
51
|
-
return F && !P ? /* @__PURE__ */
|
|
51
|
+
return F && !P ? /* @__PURE__ */ g(i, {
|
|
52
52
|
title: "Application Activity",
|
|
53
53
|
description: "Loading application audit history",
|
|
54
54
|
backLink: C ? `/apps/${C}` : "/apps",
|
|
55
55
|
backLabel: "Back to Application",
|
|
56
|
-
children: /* @__PURE__ */
|
|
56
|
+
children: /* @__PURE__ */ g("div", {
|
|
57
57
|
className: "flex items-center justify-center py-16 text-text-secondary",
|
|
58
|
-
children: /* @__PURE__ */
|
|
58
|
+
children: /* @__PURE__ */ g(p, { className: "h-8 w-8 animate-spin" })
|
|
59
59
|
})
|
|
60
|
-
}) : I || !R ? /* @__PURE__ */
|
|
60
|
+
}) : I || !R ? /* @__PURE__ */ g(i, {
|
|
61
61
|
title: "Application Activity",
|
|
62
62
|
description: "Review audit events for this application",
|
|
63
63
|
backLink: C ? `/apps/${C}` : "/apps",
|
|
64
64
|
backLabel: "Back to Application",
|
|
65
|
-
children: /* @__PURE__ */
|
|
66
|
-
icon: /* @__PURE__ */
|
|
65
|
+
children: /* @__PURE__ */ g(t, {
|
|
66
|
+
icon: /* @__PURE__ */ g(u, {
|
|
67
67
|
size: 32,
|
|
68
68
|
className: "text-status-error-text"
|
|
69
69
|
}),
|
|
70
70
|
title: "Unable to load activity",
|
|
71
71
|
description: I ? r(I) : "Application not found"
|
|
72
72
|
})
|
|
73
|
-
}) : /* @__PURE__ */
|
|
73
|
+
}) : /* @__PURE__ */ _(i, {
|
|
74
74
|
title: "Application Activity",
|
|
75
75
|
description: `Audit events for ${R.name}`,
|
|
76
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?'",
|
|
77
77
|
backLink: `/apps/${R.id}`,
|
|
78
78
|
backLabel: "Back to Application",
|
|
79
|
-
actions: /* @__PURE__ */
|
|
79
|
+
actions: /* @__PURE__ */ _("div", {
|
|
80
80
|
className: "flex flex-wrap gap-2",
|
|
81
|
-
children: [/* @__PURE__ */
|
|
81
|
+
children: [/* @__PURE__ */ _("button", {
|
|
82
82
|
type: "button",
|
|
83
83
|
onClick: () => void L(),
|
|
84
84
|
className: "inline-flex items-center gap-2 rounded-md border px-3 py-2 text-sm font-medium hover:bg-accent",
|
|
85
|
-
children: [/* @__PURE__ */
|
|
86
|
-
}), /* @__PURE__ */
|
|
85
|
+
children: [/* @__PURE__ */ g(m, { size: 16 }), "Refresh"]
|
|
86
|
+
}), /* @__PURE__ */ _("button", {
|
|
87
87
|
type: "button",
|
|
88
88
|
onClick: () => {
|
|
89
89
|
let e = new Blob([JSON.stringify(H, null, 2)], { type: "application/json" }), t = URL.createObjectURL(e), n = document.createElement("a");
|
|
90
90
|
n.href = t, n.download = `${R?.slug || C || "application"}-audit-log.json`, n.click(), URL.revokeObjectURL(t);
|
|
91
91
|
},
|
|
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__ */
|
|
93
|
+
children: [/* @__PURE__ */ g(d, { size: 16 }), "Export JSON"]
|
|
94
94
|
})]
|
|
95
95
|
}),
|
|
96
96
|
children: [
|
|
97
|
-
/* @__PURE__ */
|
|
97
|
+
/* @__PURE__ */ _("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__ */
|
|
100
|
+
/* @__PURE__ */ _("label", {
|
|
101
101
|
className: "grid gap-2 text-sm font-medium",
|
|
102
|
-
children: [/* @__PURE__ */
|
|
102
|
+
children: [/* @__PURE__ */ _("span", {
|
|
103
103
|
className: "flex items-center gap-2 text-text-secondary",
|
|
104
|
-
children: [/* @__PURE__ */
|
|
105
|
-
}), /* @__PURE__ */
|
|
104
|
+
children: [/* @__PURE__ */ g(f, { size: 16 }), "Filter by action"]
|
|
105
|
+
}), /* @__PURE__ */ _("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",
|
|
109
|
-
children: [/* @__PURE__ */
|
|
109
|
+
children: [/* @__PURE__ */ g("option", {
|
|
110
110
|
value: "all",
|
|
111
111
|
children: "All actions"
|
|
112
|
-
}), V.map((e) => /* @__PURE__ */
|
|
112
|
+
}), V.map((e) => /* @__PURE__ */ g("option", {
|
|
113
113
|
value: e,
|
|
114
114
|
children: x(e)
|
|
115
115
|
}, e))]
|
|
116
116
|
})]
|
|
117
117
|
}),
|
|
118
|
-
/* @__PURE__ */
|
|
118
|
+
/* @__PURE__ */ _("label", {
|
|
119
119
|
className: "grid gap-2 text-sm font-medium",
|
|
120
|
-
children: [/* @__PURE__ */
|
|
120
|
+
children: [/* @__PURE__ */ g("span", {
|
|
121
121
|
className: "text-text-secondary",
|
|
122
122
|
children: "Search metadata"
|
|
123
|
-
}), /* @__PURE__ */
|
|
123
|
+
}), /* @__PURE__ */ g("input", {
|
|
124
124
|
type: "text",
|
|
125
125
|
value: A,
|
|
126
126
|
onChange: (e) => j(e.target.value),
|
|
@@ -128,31 +128,31 @@ var y = 25, b = (e) => o(new Date(e), "MMM d, yyyy HH:mm:ss"), x = (e) => e.repl
|
|
|
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
|
-
/* @__PURE__ */
|
|
131
|
+
/* @__PURE__ */ _("div", {
|
|
132
132
|
className: "rounded-full bg-bg-sunken px-3 py-2 text-sm text-text-secondary",
|
|
133
133
|
children: [z, " total events"]
|
|
134
134
|
})
|
|
135
135
|
]
|
|
136
136
|
}),
|
|
137
|
-
/* @__PURE__ */
|
|
137
|
+
/* @__PURE__ */ g(a, { children: H.length > 0 ? /* @__PURE__ */ g("div", {
|
|
138
138
|
className: "space-y-3 rounded-card border border-border-seam bg-bg-surface p-4",
|
|
139
|
-
children: H.map((e) => /* @__PURE__ */
|
|
139
|
+
children: H.map((e) => /* @__PURE__ */ _("article", {
|
|
140
140
|
className: "rounded-card border border-border-seam bg-bg-surface p-4",
|
|
141
141
|
children: [
|
|
142
|
-
/* @__PURE__ */
|
|
142
|
+
/* @__PURE__ */ _("div", {
|
|
143
143
|
className: "flex flex-col gap-3 lg:flex-row lg:items-start lg:justify-between",
|
|
144
|
-
children: [/* @__PURE__ */
|
|
144
|
+
children: [/* @__PURE__ */ _("div", {
|
|
145
145
|
className: "space-y-2",
|
|
146
|
-
children: [/* @__PURE__ */
|
|
146
|
+
children: [/* @__PURE__ */ _("div", {
|
|
147
147
|
className: "flex flex-wrap items-center gap-2",
|
|
148
|
-
children: [/* @__PURE__ */
|
|
148
|
+
children: [/* @__PURE__ */ g("span", {
|
|
149
149
|
className: "rounded-full bg-action-primary-bg/10 px-2.5 py-1 text-xs font-medium text-primary",
|
|
150
150
|
children: x(e.action)
|
|
151
|
-
}), /* @__PURE__ */
|
|
151
|
+
}), /* @__PURE__ */ _("span", {
|
|
152
152
|
className: "text-xs text-text-secondary",
|
|
153
153
|
children: ["Actor ", D(e.actorId)]
|
|
154
154
|
})]
|
|
155
|
-
}), /* @__PURE__ */
|
|
155
|
+
}), /* @__PURE__ */ _("p", {
|
|
156
156
|
className: "text-sm text-text-secondary",
|
|
157
157
|
children: [
|
|
158
158
|
"Resource: ",
|
|
@@ -160,39 +160,39 @@ var y = 25, b = (e) => o(new Date(e), "MMM d, yyyy HH:mm:ss"), x = (e) => e.repl
|
|
|
160
160
|
e.resourceId ? ` · ${e.resourceId}` : ""
|
|
161
161
|
]
|
|
162
162
|
})]
|
|
163
|
-
}), /* @__PURE__ */
|
|
163
|
+
}), /* @__PURE__ */ g("time", {
|
|
164
164
|
className: "text-xs text-text-secondary",
|
|
165
165
|
children: b(e.createdAt)
|
|
166
166
|
})]
|
|
167
167
|
}),
|
|
168
|
-
e.metadata && /* @__PURE__ */
|
|
168
|
+
e.metadata && /* @__PURE__ */ g("pre", {
|
|
169
169
|
className: "mt-3 overflow-x-auto rounded-md bg-bg-sunken p-3 text-xs text-text-primary",
|
|
170
170
|
children: S(e.metadata)
|
|
171
171
|
}),
|
|
172
|
-
e.changes && /* @__PURE__ */
|
|
172
|
+
e.changes && /* @__PURE__ */ _("details", {
|
|
173
173
|
className: "mt-3 rounded-md border bg-bg-surface p-3",
|
|
174
|
-
children: [/* @__PURE__ */
|
|
174
|
+
children: [/* @__PURE__ */ g("summary", {
|
|
175
175
|
className: "cursor-pointer text-sm font-medium text-text-primary",
|
|
176
176
|
children: "View captured changes"
|
|
177
|
-
}), /* @__PURE__ */
|
|
177
|
+
}), /* @__PURE__ */ g("pre", {
|
|
178
178
|
className: "mt-3 overflow-x-auto text-xs text-text-secondary",
|
|
179
179
|
children: S(e.changes)
|
|
180
180
|
})]
|
|
181
181
|
})
|
|
182
182
|
]
|
|
183
183
|
}, e.id))
|
|
184
|
-
}) : /* @__PURE__ */
|
|
184
|
+
}) : /* @__PURE__ */ g(t, {
|
|
185
185
|
illustration: "empty-data",
|
|
186
186
|
title: "No audit events found",
|
|
187
187
|
description: "Events will appear here after application or webhook actions are performed through the public gateway."
|
|
188
188
|
}) }),
|
|
189
|
-
P.auditLogs?.pageInfo?.hasNextPage && /* @__PURE__ */
|
|
189
|
+
P.auditLogs?.pageInfo?.hasNextPage && /* @__PURE__ */ g("div", {
|
|
190
190
|
className: "flex justify-center",
|
|
191
|
-
children: /* @__PURE__ */
|
|
191
|
+
children: /* @__PURE__ */ _("button", {
|
|
192
192
|
type: "button",
|
|
193
193
|
onClick: () => N((e) => e + 1),
|
|
194
194
|
className: "inline-flex items-center gap-2 rounded-md border px-4 py-2 text-sm font-medium hover:bg-accent",
|
|
195
|
-
children: [/* @__PURE__ */
|
|
195
|
+
children: [/* @__PURE__ */ g(m, { size: 16 }), "Next page"]
|
|
196
196
|
})
|
|
197
197
|
})
|
|
198
198
|
]
|