@burdenoff/microfe-devportal 2026.625.10 → 2026.626.2
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/DevportalRoutes.js +38 -34
- package/dist/devportal/DevportalRoutes.js.map +1 -1
- package/dist/devportal/components/ConfirmDialog.js.map +1 -1
- package/dist/devportal/components/DataTable.js.map +1 -1
- package/dist/devportal/components/FormField.js.map +1 -1
- package/dist/devportal/components/GrantDelegationModal.js +2 -1
- package/dist/devportal/components/GrantDelegationModal.js.map +1 -1
- package/dist/devportal/components/LoadingState.js.map +1 -1
- package/dist/devportal/components/PageLayout.js.map +1 -1
- package/dist/devportal/components/SelectionCheckbox.js.map +1 -1
- package/dist/devportal/hooks/usePermission.js +2 -2
- package/dist/devportal/hooks/usePermission.js.map +1 -1
- package/dist/devportal/pages/AnalyticsDashboardPage.js.map +1 -1
- package/dist/devportal/pages/ApiKeysPage.js.map +1 -1
- package/dist/devportal/pages/CreateApplicationPage.js +185 -104
- package/dist/devportal/pages/CreateApplicationPage.js.map +1 -1
- package/dist/devportal/pages/CreateWebhookPage.js +6 -5
- package/dist/devportal/pages/CreateWebhookPage.js.map +1 -1
- package/dist/devportal/pages/DashboardPage.js.map +1 -1
- package/dist/devportal/pages/OAuthCallbackPage.js +64 -0
- package/dist/devportal/pages/OAuthCallbackPage.js.map +1 -0
- package/dist/devportal/pages/OAuthClientsPage.js.map +1 -1
- package/dist/devportal/pages/OrganizationDetailPage.js.map +1 -1
- package/dist/devportal/pages/PublishPage.js +392 -187
- package/dist/devportal/pages/PublishPage.js.map +1 -1
- package/dist/devportal/pages/ResourcesPage.js.map +1 -1
- package/dist/devportal/pages/RevenueDashboardPage.js.map +1 -1
- package/dist/devportal/pages/WebhookDetailPage.js.map +1 -1
- package/dist/devportal/pages/WebhooksPage.js +1 -1
- package/dist/devportal/pages/WebhooksPage.js.map +1 -1
- package/dist/devportal/types/index.js.map +1 -1
- package/dist/generated/global-operations.js +260 -174
- package/dist/generated/global-operations.js.map +1 -1
- package/dist/node_modules/@apollo/client/version.js +1 -1
- package/dist/node_modules/@apollo/client/version.js.map +1 -1
- package/package.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"GrantDelegationModal.js","names":[],"sources":["../../../src/devportal/components/GrantDelegationModal.tsx"],"sourcesContent":["import {\n useState,\n useCallback,\n useEffect,\n useId,\n useRef,\n type FC,\n} from \"react\";\nimport {\n Shield,\n X,\n Loader2,\n CheckCircle,\n AlertCircle,\n Search,\n Check,\n} from \"lucide-react\";\nimport { useI18n } from \"@burdenoff/fe-libs/shared/providers/shell/I18nProvider\";\nimport { useGatewayClient } from \"@burdenoff/fe-libs/shared/graphql\";\nimport { cn } from \"../utils/cn\";\nimport { useGrantDelegationMutation } from \"../../generated/global-operations\";\nimport { useMyWorkspacesQuery } from \"../../generated/wspace-operations\";\n\ninterface WorkspaceOption {\n id: string;\n name: string;\n}\n\n// ============================================================================\n// Scope grouping helper\n// ============================================================================\n\nfunction groupScopes(scopes: string[]): Map<string, string[]> {\n const groups = new Map<string, string[]>();\n for (const scope of scopes) {\n const [resource] = scope.split(\":\");\n const group = resource || \"other\";\n if (!groups.has(group)) groups.set(group, []);\n groups.get(group)!.push(scope);\n }\n return groups;\n}\n\n// ============================================================================\n// Props\n// ============================================================================\n\nexport interface GrantDelegationModalProps {\n open: boolean;\n onOpenChange: (open: boolean) => void;\n appId: string;\n appName: string;\n appScopes: string[];\n /** Pre-selected workspace IDs (e.g., from installation flow) */\n preSelectedWorkspaceIds?: string[];\n /** Pre-selected scopes (e.g., required scopes from app) */\n preSelectedScopes?: string[];\n onSuccess?: (delegation: unknown) => void;\n}\n\n/**\n * Reusable modal for granting delegation access to an app.\n *\n * Allows users to select scopes, specify workspace IDs,\n * and optionally set an expiration date before granting\n * an app permission to act on their behalf.\n */\nexport const GrantDelegationModal: FC<GrantDelegationModalProps> = ({\n open,\n onOpenChange,\n appId,\n appName,\n appScopes,\n preSelectedWorkspaceIds,\n preSelectedScopes,\n onSuccess,\n}) => {\n const titleId = useId();\n const descriptionId = useId();\n const dialogRef = useRef<HTMLDivElement | null>(null);\n const previousFocusRef = useRef<HTMLElement | null>(null);\n const [selectedScopes, setSelectedScopes] = useState<string[]>(\n preSelectedScopes ?? [],\n );\n const [selectedWorkspaceIds, setSelectedWorkspaceIds] = useState<string[]>(\n preSelectedWorkspaceIds ?? [],\n );\n const [allWorkspaces, setAllWorkspaces] = useState(false);\n const [workspaceSearch, setWorkspaceSearch] = useState(\"\");\n const [expiresAt, setExpiresAt] = useState(\"\");\n const [error, setError] = useState<string | null>(null);\n const [successMessage, setSuccessMessage] = useState<string | null>(null);\n\n // useGatewayClient throws when no MultiGatewayProvider is mounted (e.g. in\n // standalone tests). We intentionally swallow that to render in degraded\n // mode without the workspaces dropdown.\n // eslint-disable-next-line react-hooks/rules-of-hooks\n const workspaceClient = (() => {\n try {\n // eslint-disable-next-line react-hooks/rules-of-hooks\n return useGatewayClient(\"workspace\");\n } catch {\n return null;\n }\n })();\n const { data: workspacesData, loading: loadingWorkspaces } =\n useMyWorkspacesQuery({\n client: workspaceClient ?? undefined,\n skip: !open || !workspaceClient,\n variables: {\n pagination: {\n limit: 100,\n offset: 0,\n },\n },\n fetchPolicy: \"cache-and-network\",\n });\n const workspaces: WorkspaceOption[] =\n workspacesData?.myWorkspaces?.items?.map(\n (workspace: { id: string; name: string }) => ({\n id: workspace.id,\n name: workspace.name,\n }),\n ) ?? [];\n const [grantDelegationMutation, { loading }] = useGrantDelegationMutation({\n refetchQueries: [\"CurrentUserDelegations\"],\n awaitRefetchQueries: true,\n });\n const { t } = useI18n();\n const tr = (key: string, fallback: string): string => {\n const value = t(key);\n return value === key ? fallback : value;\n };\n\n // Apply pre-selections when they change\n useEffect(() => {\n if (preSelectedWorkspaceIds?.length)\n setSelectedWorkspaceIds(preSelectedWorkspaceIds);\n }, [preSelectedWorkspaceIds]);\n useEffect(() => {\n if (preSelectedScopes?.length) setSelectedScopes(preSelectedScopes);\n }, [preSelectedScopes]);\n\n const resetState = useCallback(() => {\n setSelectedScopes(preSelectedScopes ?? []);\n setSelectedWorkspaceIds(preSelectedWorkspaceIds ?? []);\n setAllWorkspaces(false);\n setWorkspaceSearch(\"\");\n setExpiresAt(\"\");\n setError(null);\n setSuccessMessage(null);\n }, [preSelectedScopes, preSelectedWorkspaceIds]);\n\n const handleClose = useCallback(() => {\n if (loading) return;\n resetState();\n onOpenChange(false);\n }, [loading, onOpenChange, resetState]);\n\n useEffect(() => {\n if (!open) return undefined;\n\n previousFocusRef.current = document.activeElement as HTMLElement | null;\n const originalOverflow = document.body.style.overflow;\n document.body.style.overflow = \"hidden\";\n\n window.requestAnimationFrame(() => {\n dialogRef.current?.focus();\n });\n\n const onKeyDown = (event: KeyboardEvent) => {\n if (event.key === \"Escape\" && !loading) {\n event.preventDefault();\n handleClose();\n }\n };\n\n document.addEventListener(\"keydown\", onKeyDown);\n\n return () => {\n document.body.style.overflow = originalOverflow;\n document.removeEventListener(\"keydown\", onKeyDown);\n previousFocusRef.current?.focus();\n };\n }, [handleClose, loading, open]);\n\n const toggleScope = useCallback((scope: string) => {\n setSelectedScopes((prev) =>\n prev.includes(scope) ? prev.filter((s) => s !== scope) : [...prev, scope],\n );\n }, []);\n\n const toggleWorkspace = useCallback((wsId: string) => {\n setSelectedWorkspaceIds((prev) =>\n prev.includes(wsId) ? prev.filter((id) => id !== wsId) : [...prev, wsId],\n );\n setAllWorkspaces(false);\n }, []);\n\n const handleAllWorkspacesToggle = useCallback(() => {\n setAllWorkspaces((prev) => {\n if (!prev) setSelectedWorkspaceIds([]);\n return !prev;\n });\n }, []);\n\n const filteredWorkspaces = workspaces.filter((ws) =>\n ws.name.toLowerCase().includes(workspaceSearch.toLowerCase()),\n );\n\n const scopeGroups = groupScopes(appScopes);\n\n const handleGrant = useCallback(async () => {\n setError(null);\n\n if (selectedScopes.length === 0) {\n setError(\n tr(\n \"devportal.selectAtLeastOneScope\",\n \"Please select at least one scope to grant.\",\n ),\n );\n return;\n }\n\n let parsedExpiresAt: string | undefined;\n if (expiresAt) {\n const date = new Date(expiresAt);\n if (Number.isNaN(date.getTime())) {\n setError(\n tr(\n \"devportal.invalidExpirationDate\",\n \"Invalid expiration date. Please use a valid date format.\",\n ),\n );\n return;\n }\n if (date <= new Date()) {\n setError(\n tr(\n \"devportal.expirationDateFuture\",\n \"Expiration date must be in the future.\",\n ),\n );\n return;\n }\n parsedExpiresAt = date.toISOString();\n }\n\n try {\n const result = await grantDelegationMutation({\n variables: {\n input: {\n appId,\n scopes: selectedScopes,\n workspaceIds:\n !allWorkspaces && selectedWorkspaceIds.length > 0\n ? selectedWorkspaceIds\n : undefined,\n expiresAt: parsedExpiresAt,\n },\n },\n });\n\n if (result.error) {\n setError(\n result.error.message ??\n tr(\n \"devportal.failedToGrantDelegation\",\n \"Failed to grant delegation.\",\n ),\n );\n return;\n }\n\n const delegation = result.data?.grantDelegation;\n if (!delegation) {\n setError(\n tr(\n \"devportal.failedToGrantDelegation\",\n \"Failed to grant delegation.\",\n ),\n );\n return;\n }\n\n const wsCount = allWorkspaces\n ? \"all workspaces\"\n : selectedWorkspaceIds.length > 0\n ? `${selectedWorkspaceIds.length} workspace${selectedWorkspaceIds.length !== 1 ? \"s\" : \"\"}`\n : \"all workspaces\";\n setSuccessMessage(\n `Delegation granted to \"${appName}\" with ${selectedScopes.length} scope${selectedScopes.length !== 1 ? \"s\" : \"\"} for ${wsCount}.`,\n );\n onSuccess?.(delegation);\n } catch (err) {\n setError(\n err instanceof Error\n ? err.message\n : tr(\n \"devportal.failedToGrantDelegation\",\n \"Failed to grant delegation.\",\n ),\n );\n }\n }, [\n selectedScopes,\n selectedWorkspaceIds,\n allWorkspaces,\n expiresAt,\n appId,\n appName,\n grantDelegationMutation,\n onSuccess,\n ]);\n\n if (!open) return null;\n\n return (\n <div className=\"fixed inset-0 z-50 flex items-center justify-center\">\n {/* Backdrop */}\n <button\n type=\"button\"\n aria-label=\"Close delegation dialog backdrop\"\n className=\"absolute inset-0 bg-bg-overlay\"\n onClick={handleClose}\n />\n\n {/* Dialog */}\n <div\n ref={dialogRef}\n role=\"dialog\"\n aria-modal=\"true\"\n aria-labelledby={titleId}\n aria-describedby={descriptionId}\n tabIndex={-1}\n className=\"relative z-10 w-full max-w-lg rounded-lg bg-bg-surface shadow-xl\"\n >\n {/* Header */}\n <div className=\"flex items-start gap-4 border-b p-6\">\n <div className=\"rounded-full bg-status-success-bg-subtle p-2\">\n <Shield className=\"h-6 w-6 text-status-success-text\" />\n </div>\n <div className=\"flex-1\">\n <h3 id={titleId} className=\"text-lg font-semibold\">\n Grant Delegation\n </h3>\n <p\n id={descriptionId}\n className=\"mt-1 text-sm text-text-secondary\"\n >\n Allow{\" \"}\n <span className=\"font-medium text-text-primary\">{appName}</span> to\n act on your behalf\n </p>\n </div>\n <button\n type=\"button\"\n onClick={handleClose}\n disabled={loading}\n className=\"rounded-md p-1 text-text-secondary hover:bg-bg-sunken transition-colors disabled:opacity-50\"\n >\n <X className=\"h-5 w-5\" />\n </button>\n </div>\n\n {/* Body */}\n <div className=\"space-y-5 p-6\">\n {/* Success state */}\n {successMessage && (\n <div className=\"flex items-start gap-3 rounded-md border border-border-subtle bg-status-success-bg-subtle p-4\">\n <CheckCircle className=\"h-5 w-5 shrink-0 text-status-success-text\" />\n <div className=\"flex-1\">\n <p className=\"text-sm font-medium text-status-success-text\">\n {successMessage}\n </p>\n <button\n type=\"button\"\n onClick={handleClose}\n className=\"mt-2 text-sm font-medium text-status-success-text hover:underline\"\n >\n {tr(\"devportal.close\", \"Close\")}\n </button>\n </div>\n </div>\n )}\n\n {/* Form fields (hidden after success) */}\n {!successMessage && (\n <>\n {/* Scope selection (grouped by resource) */}\n <div>\n <label className=\"mb-2 block text-sm font-medium\">\n {tr(\"devportal.permissionsScopes\", \"Permissions (Scopes)\")}\n <span className=\"ml-1.5 text-xs font-normal text-text-secondary\">\n {selectedScopes.length} {tr(\"devportal.of\", \"of\")}{\" \"}\n {appScopes.length} {tr(\"devportal.selected\", \"selected\")}\n </span>\n </label>\n {appScopes.length === 0 ? (\n <p className=\"text-sm text-text-secondary\">\n This app has no scopes available.\n </p>\n ) : (\n <div className=\"space-y-3\">\n {/* Quick actions */}\n <div className=\"flex gap-2 text-xs\">\n <button\n type=\"button\"\n onClick={() => setSelectedScopes([...appScopes])}\n className=\"text-status-success-text hover:underline\"\n >\n {tr(\"devportal.selectAll\", \"Select all\")}\n </button>\n <button\n type=\"button\"\n onClick={() =>\n setSelectedScopes(\n appScopes.filter(\n (s) => s.includes(\":read\") || s.includes(\":list\"),\n ),\n )\n }\n className=\"text-status-success-text hover:underline\"\n >\n {tr(\"devportal.readOnly\", \"Read-only\")}\n </button>\n <button\n type=\"button\"\n onClick={() => setSelectedScopes([])}\n className=\"text-text-secondary hover:underline\"\n >\n {tr(\"devportal.clear\", \"Clear\")}\n </button>\n </div>\n {/* Grouped scopes */}\n {[...scopeGroups.entries()].map(([group, scopes]) => (\n <div key={group}>\n <p className=\"mb-1 text-xs font-semibold uppercase text-text-secondary\">\n {group}\n </p>\n <div className=\"flex flex-wrap gap-1.5\">\n {scopes.map((scope) => {\n const isSelected = selectedScopes.includes(scope);\n const action = scope.split(\":\")[1] || scope;\n const isWrite =\n action.includes(\"write\") ||\n action.includes(\"create\") ||\n action.includes(\"delete\") ||\n action.includes(\"admin\");\n return (\n <button\n key={scope}\n type=\"button\"\n onClick={() => toggleScope(scope)}\n className={cn(\n \"rounded-md px-2.5 py-1 text-xs font-medium transition-colors\",\n isSelected\n ? isWrite\n ? \"bg-status-warning-bg text-white\"\n : \"bg-status-success-bg text-white\"\n : \"border bg-bg-sunken text-text-secondary hover:bg-bg-sunken/80\",\n )}\n >\n {action}\n </button>\n );\n })}\n </div>\n </div>\n ))}\n </div>\n )}\n </div>\n\n {/* Workspace Selection (multi-select picker) */}\n <div>\n <label className=\"mb-2 block text-sm font-medium\">\n {tr(\"devportal.workspaceAccess\", \"Workspace Access\")}\n </label>\n\n {/* All workspaces toggle */}\n <button\n type=\"button\"\n onClick={handleAllWorkspacesToggle}\n disabled={loading}\n className={cn(\n \"mb-2 flex w-full items-center gap-2 rounded-md border px-3 py-2 text-sm transition-colors\",\n allWorkspaces\n ? \"border-status-success-bg bg-status-success-bg-subtle text-status-success-text\"\n : \"hover:bg-bg-sunken\",\n )}\n >\n {allWorkspaces && (\n <Check className=\"h-4 w-4 text-status-success-text\" />\n )}\n <span>\n {tr(\n \"devportal.allWorkspacesUnrestricted\",\n \"All workspaces (unrestricted)\",\n )}\n </span>\n </button>\n\n {!allWorkspaces && (\n <>\n {/* Search */}\n <div className=\"relative mb-2\">\n <Search className=\"absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-text-secondary\" />\n <input\n type=\"text\"\n value={workspaceSearch}\n onChange={(e) => setWorkspaceSearch(e.target.value)}\n placeholder={tr(\n \"devportal.searchWorkspaces\",\n \"Search workspaces...\",\n )}\n disabled={loading}\n className=\"block w-full rounded-md border py-2 pl-9 pr-3 text-sm placeholder:text-text-secondary focus:outline-none focus:ring-2 focus:ring-status-success-bg/20 focus:border-status-success-bg transition-colors disabled:opacity-50\"\n />\n </div>\n\n {/* Workspace list */}\n <div className=\"max-h-40 overflow-y-auto rounded-md border\">\n {loadingWorkspaces ? (\n <div className=\"flex items-center justify-center p-4\">\n <Loader2 className=\"h-4 w-4 animate-spin text-text-secondary\" />\n <span className=\"ml-2 text-sm text-text-secondary\">\n {tr(\n \"devportal.loadingWorkspaces\",\n \"Loading workspaces...\",\n )}\n </span>\n </div>\n ) : filteredWorkspaces.length === 0 ? (\n <p className=\"p-4 text-center text-sm text-text-secondary\">\n {workspaces.length === 0\n ? tr(\n \"devportal.noWorkspacesFound\",\n \"No workspaces found\",\n )\n : tr(\n \"devportal.noMatchingWorkspaces\",\n \"No matching workspaces\",\n )}\n </p>\n ) : (\n filteredWorkspaces.map((ws) => {\n const isSelected = selectedWorkspaceIds.includes(\n ws.id,\n );\n return (\n <button\n key={ws.id}\n type=\"button\"\n onClick={() => toggleWorkspace(ws.id)}\n disabled={loading}\n className={cn(\n \"flex w-full items-center gap-2 border-b px-3 py-2 text-left text-sm transition-colors last:border-b-0\",\n isSelected\n ? \"bg-status-success-bg-subtle text-status-success-text\"\n : \"hover:bg-bg-sunken\",\n )}\n >\n <div\n className={cn(\n \"flex h-4 w-4 shrink-0 items-center justify-center rounded border\",\n isSelected\n ? \"border-status-success-bg bg-status-success-bg\"\n : \"border-muted-foreground/30\",\n )}\n >\n {isSelected && (\n <Check className=\"h-3 w-3 text-white\" />\n )}\n </div>\n <span className=\"font-medium\">{ws.name}</span>\n </button>\n );\n })\n )}\n </div>\n\n {/* Selected count */}\n {selectedWorkspaceIds.length > 0 && (\n <p className=\"mt-1.5 text-xs text-text-secondary\">\n {selectedWorkspaceIds.length} workspace\n {selectedWorkspaceIds.length !== 1 ? \"s\" : \"\"} selected\n </p>\n )}\n </>\n )}\n </div>\n\n {/* Expiration */}\n <div>\n <label className=\"mb-2 block text-sm font-medium\">\n {tr(\"devportal.expiration\", \"Expiration\")}\n <span className=\"ml-1.5 text-xs font-normal text-text-secondary\">\n {tr(\"devportal.optional\", \"(optional)\")}\n </span>\n </label>\n <input\n type=\"datetime-local\"\n value={expiresAt}\n onChange={(e) => setExpiresAt(e.target.value)}\n disabled={loading}\n min={new Date().toISOString().slice(0, 16)}\n className=\"block w-full rounded-md border px-3 py-2 text-sm placeholder:text-text-secondary focus:outline-none focus:ring-2 focus:ring-status-success-bg/20 focus:border-status-success-bg transition-colors disabled:opacity-50\"\n />\n <p className=\"mt-1.5 text-xs text-text-secondary\">\n {tr(\n \"devportal.leaveEmptyForNonExpiring\",\n \"Leave empty for a delegation that does not expire.\",\n )}\n </p>\n </div>\n\n {/* Error */}\n {error && (\n <div className=\"flex items-start gap-3 rounded-md border border-border-subtle bg-status-error-bg-subtle p-4\">\n <AlertCircle className=\"h-5 w-5 shrink-0 text-status-error-text\" />\n <p className=\"text-sm text-status-error-text\">{error}</p>\n </div>\n )}\n </>\n )}\n </div>\n\n {/* Footer */}\n {!successMessage && (\n <div className=\"flex justify-end gap-3 border-t p-6\">\n <button\n type=\"button\"\n onClick={handleClose}\n disabled={loading}\n className=\"rounded-md border px-4 py-2 text-sm font-medium hover:bg-bg-sunken transition-colors disabled:opacity-50\"\n >\n {tr(\"devportal.cancel\", \"Cancel\")}\n </button>\n <button\n type=\"button\"\n onClick={handleGrant}\n disabled={loading || selectedScopes.length === 0}\n className={cn(\n \"flex items-center gap-2 rounded-md px-4 py-2 text-sm font-medium text-white transition-colors disabled:opacity-50 disabled:cursor-not-allowed\",\n \"bg-status-success-bg hover:bg-status-success-bg/90\",\n )}\n >\n {loading ? (\n <>\n <Loader2 className=\"h-4 w-4 animate-spin\" />\n {tr(\"devportal.granting\", \"Granting...\")}\n </>\n ) : (\n tr(\"devportal.grant\", \"Grant\")\n )}\n </button>\n </div>\n )}\n </div>\n </div>\n );\n};\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"GrantDelegationModal.js","names":[],"sources":["../../../src/devportal/components/GrantDelegationModal.tsx"],"sourcesContent":["import {\n useState,\n useCallback,\n useEffect,\n useId,\n useRef,\n type FC,\n} from \"react\";\nimport {\n Shield,\n X,\n Loader2,\n CheckCircle,\n AlertCircle,\n Search,\n Check,\n} from \"lucide-react\";\nimport { useI18n } from \"@burdenoff/fe-libs/shared/providers/shell/I18nProvider\";\nimport { useGatewayClient } from \"@burdenoff/fe-libs/shared/graphql\";\nimport { cn } from \"../utils/cn\";\nimport { useGrantDelegationMutation } from \"../../generated/global-operations\";\nimport { useMyWorkspacesQuery } from \"../../generated/wspace-operations\";\n\ninterface WorkspaceOption {\n id: string;\n name: string;\n}\n\n// ============================================================================\n// Scope grouping helper\n// ============================================================================\n\nfunction groupScopes(scopes: string[]): Map<string, string[]> {\n const groups = new Map<string, string[]>();\n for (const scope of scopes) {\n const [resource] = scope.split(\":\");\n const group = resource || \"other\";\n if (!groups.has(group)) groups.set(group, []);\n groups.get(group)!.push(scope);\n }\n return groups;\n}\n\n// ============================================================================\n// Props\n// ============================================================================\n\nexport interface GrantDelegationModalProps {\n open: boolean;\n onOpenChange: (open: boolean) => void;\n appId: string;\n appName: string;\n appScopes: string[];\n /** Pre-selected workspace IDs (e.g., from installation flow) */\n preSelectedWorkspaceIds?: string[];\n /** Pre-selected scopes (e.g., required scopes from app) */\n preSelectedScopes?: string[];\n onSuccess?: (delegation: unknown) => void;\n}\n\n/**\n * Reusable modal for granting delegation access to an app.\n *\n * Allows users to select scopes, specify workspace IDs,\n * and optionally set an expiration date before granting\n * an app permission to act on their behalf.\n */\nexport const GrantDelegationModal: FC<GrantDelegationModalProps> = ({\n open,\n onOpenChange,\n appId,\n appName,\n appScopes,\n preSelectedWorkspaceIds,\n preSelectedScopes,\n onSuccess,\n}) => {\n const titleId = useId();\n const descriptionId = useId();\n const dialogRef = useRef<HTMLDivElement | null>(null);\n const previousFocusRef = useRef<HTMLElement | null>(null);\n const [selectedScopes, setSelectedScopes] = useState<string[]>(\n preSelectedScopes ?? [],\n );\n const [selectedWorkspaceIds, setSelectedWorkspaceIds] = useState<string[]>(\n preSelectedWorkspaceIds ?? [],\n );\n const [allWorkspaces, setAllWorkspaces] = useState(false);\n const [workspaceSearch, setWorkspaceSearch] = useState(\"\");\n const [expiresAt, setExpiresAt] = useState(\"\");\n const [error, setError] = useState<string | null>(null);\n const [successMessage, setSuccessMessage] = useState<string | null>(null);\n\n // useGatewayClient throws when no MultiGatewayProvider is mounted (e.g. in\n // standalone tests). We intentionally swallow that to render in degraded\n // mode without the workspaces dropdown.\n // eslint-disable-next-line react-hooks/rules-of-hooks\n const workspaceClient = (() => {\n try {\n // eslint-disable-next-line react-hooks/rules-of-hooks\n return useGatewayClient(\"workspace\");\n } catch {\n return null;\n }\n })();\n const { data: workspacesData, loading: loadingWorkspaces } =\n useMyWorkspacesQuery({\n client: workspaceClient ?? undefined,\n skip: !open || !workspaceClient,\n variables: {\n pagination: {\n limit: 100,\n offset: 0,\n },\n },\n fetchPolicy: \"cache-and-network\",\n });\n const workspaces: WorkspaceOption[] =\n workspacesData?.myWorkspaces?.items?.map(\n (workspace: { id: string; name: string }) => ({\n id: workspace.id,\n name: workspace.name,\n }),\n ) ?? [];\n const [grantDelegationMutation, { loading }] = useGrantDelegationMutation({\n refetchQueries: [\"CurrentUserDelegations\"],\n awaitRefetchQueries: true,\n });\n const { t } = useI18n();\n const tr = (key: string, fallback: string): string => {\n const value = t(key);\n return value === key ? fallback : value;\n };\n\n // Apply pre-selections when they change\n useEffect(() => {\n if (preSelectedWorkspaceIds?.length)\n setSelectedWorkspaceIds(preSelectedWorkspaceIds);\n }, [preSelectedWorkspaceIds]);\n useEffect(() => {\n if (preSelectedScopes?.length) setSelectedScopes(preSelectedScopes);\n }, [preSelectedScopes]);\n\n const resetState = useCallback(() => {\n setSelectedScopes(preSelectedScopes ?? []);\n setSelectedWorkspaceIds(preSelectedWorkspaceIds ?? []);\n setAllWorkspaces(false);\n setWorkspaceSearch(\"\");\n setExpiresAt(\"\");\n setError(null);\n setSuccessMessage(null);\n }, [preSelectedScopes, preSelectedWorkspaceIds]);\n\n const handleClose = useCallback(() => {\n if (loading) return;\n resetState();\n onOpenChange(false);\n }, [loading, onOpenChange, resetState]);\n\n useEffect(() => {\n if (!open) return undefined;\n\n previousFocusRef.current = document.activeElement as HTMLElement | null;\n const originalOverflow = document.body.style.overflow;\n document.body.style.overflow = \"hidden\";\n\n window.requestAnimationFrame(() => {\n dialogRef.current?.focus();\n });\n\n const onKeyDown = (event: KeyboardEvent) => {\n if (event.key === \"Escape\" && !loading) {\n event.preventDefault();\n handleClose();\n }\n };\n\n document.addEventListener(\"keydown\", onKeyDown);\n\n return () => {\n document.body.style.overflow = originalOverflow;\n document.removeEventListener(\"keydown\", onKeyDown);\n previousFocusRef.current?.focus();\n };\n }, [handleClose, loading, open]);\n\n const toggleScope = useCallback((scope: string) => {\n setSelectedScopes((prev) =>\n prev.includes(scope) ? prev.filter((s) => s !== scope) : [...prev, scope],\n );\n }, []);\n\n const toggleWorkspace = useCallback((wsId: string) => {\n setSelectedWorkspaceIds((prev) =>\n prev.includes(wsId) ? prev.filter((id) => id !== wsId) : [...prev, wsId],\n );\n setAllWorkspaces(false);\n }, []);\n\n const handleAllWorkspacesToggle = useCallback(() => {\n setAllWorkspaces((prev) => {\n if (!prev) setSelectedWorkspaceIds([]);\n return !prev;\n });\n }, []);\n\n const filteredWorkspaces = workspaces.filter((ws) =>\n ws.name.toLowerCase().includes(workspaceSearch.toLowerCase()),\n );\n\n const scopeGroups = groupScopes(appScopes);\n\n const handleGrant = useCallback(async () => {\n setError(null);\n\n if (selectedScopes.length === 0) {\n setError(\n tr(\n \"devportal.selectAtLeastOneScope\",\n \"Please select at least one scope to grant.\",\n ),\n );\n return;\n }\n\n let parsedExpiresAt: string | undefined;\n if (expiresAt) {\n const date = new Date(expiresAt);\n if (Number.isNaN(date.getTime())) {\n setError(\n tr(\n \"devportal.invalidExpirationDate\",\n \"Invalid expiration date. Please use a valid date format.\",\n ),\n );\n return;\n }\n if (date <= new Date()) {\n setError(\n tr(\n \"devportal.expirationDateFuture\",\n \"Expiration date must be in the future.\",\n ),\n );\n return;\n }\n parsedExpiresAt = date.toISOString();\n }\n\n try {\n const result = await grantDelegationMutation({\n variables: {\n input: {\n appId,\n scopes: selectedScopes,\n workspaceIds:\n !allWorkspaces && selectedWorkspaceIds.length > 0\n ? selectedWorkspaceIds\n : undefined,\n expiresAt: parsedExpiresAt,\n },\n },\n });\n\n if (result.error) {\n setError(\n result.error.message ??\n tr(\n \"devportal.failedToGrantDelegation\",\n \"Failed to grant delegation.\",\n ),\n );\n return;\n }\n\n const delegation = result.data?.grantDelegation;\n if (!delegation) {\n setError(\n tr(\n \"devportal.failedToGrantDelegation\",\n \"Failed to grant delegation.\",\n ),\n );\n return;\n }\n\n const wsCount = allWorkspaces\n ? \"all workspaces\"\n : selectedWorkspaceIds.length > 0\n ? `${selectedWorkspaceIds.length} workspace${selectedWorkspaceIds.length !== 1 ? \"s\" : \"\"}`\n : \"all workspaces\";\n setSuccessMessage(\n `Delegation granted to \"${appName}\" with ${selectedScopes.length} scope${selectedScopes.length !== 1 ? \"s\" : \"\"} for ${wsCount}.`,\n );\n onSuccess?.(delegation);\n } catch (err) {\n setError(\n err instanceof Error\n ? err.message\n : tr(\n \"devportal.failedToGrantDelegation\",\n \"Failed to grant delegation.\",\n ),\n );\n }\n }, [\n selectedScopes,\n selectedWorkspaceIds,\n allWorkspaces,\n expiresAt,\n appId,\n appName,\n grantDelegationMutation,\n onSuccess,\n ]);\n\n if (!open) return null;\n\n return (\n <div className=\"fixed inset-0 z-50 flex items-center justify-center\">\n {/* Backdrop */}\n <button\n type=\"button\"\n aria-label=\"Close delegation dialog backdrop\"\n className=\"absolute inset-0 bg-bg-overlay\"\n onClick={handleClose}\n />\n\n {/* Dialog */}\n <div\n ref={dialogRef}\n role=\"dialog\"\n aria-modal=\"true\"\n aria-labelledby={titleId}\n aria-describedby={descriptionId}\n tabIndex={-1}\n className=\"relative z-10 w-full max-w-lg rounded-lg bg-bg-surface shadow-xl\"\n >\n {/* Header */}\n <div className=\"flex items-start gap-4 border-b p-6\">\n <div className=\"rounded-full bg-status-success-bg-subtle p-2\">\n <Shield className=\"h-6 w-6 text-status-success-text\" />\n </div>\n <div className=\"flex-1\">\n <h3 id={titleId} className=\"text-lg font-semibold\">\n Grant Delegation\n </h3>\n <p id={descriptionId} className=\"mt-1 text-sm text-text-secondary\">\n Allow{\" \"}\n <span className=\"font-medium text-text-primary\">{appName}</span>{\" \"}\n to act on your behalf\n </p>\n </div>\n <button\n type=\"button\"\n onClick={handleClose}\n disabled={loading}\n className=\"rounded-md p-1 text-text-secondary hover:bg-bg-sunken transition-colors disabled:opacity-50\"\n >\n <X className=\"h-5 w-5\" />\n </button>\n </div>\n\n {/* Body */}\n <div className=\"space-y-5 p-6\">\n {/* Success state */}\n {successMessage && (\n <div className=\"flex items-start gap-3 rounded-md border border-border-subtle bg-status-success-bg-subtle p-4\">\n <CheckCircle className=\"h-5 w-5 shrink-0 text-status-success-text\" />\n <div className=\"flex-1\">\n <p className=\"text-sm font-medium text-status-success-text\">\n {successMessage}\n </p>\n <button\n type=\"button\"\n onClick={handleClose}\n className=\"mt-2 text-sm font-medium text-status-success-text hover:underline\"\n >\n {tr(\"devportal.close\", \"Close\")}\n </button>\n </div>\n </div>\n )}\n\n {/* Form fields (hidden after success) */}\n {!successMessage && (\n <>\n {/* Scope selection (grouped by resource) */}\n <div>\n <label className=\"mb-2 block text-sm font-medium\">\n {tr(\"devportal.permissionsScopes\", \"Permissions (Scopes)\")}\n <span className=\"ml-1.5 text-xs font-normal text-text-secondary\">\n {selectedScopes.length} {tr(\"devportal.of\", \"of\")}{\" \"}\n {appScopes.length} {tr(\"devportal.selected\", \"selected\")}\n </span>\n </label>\n {appScopes.length === 0 ? (\n <p className=\"text-sm text-text-secondary\">\n This app has no scopes available.\n </p>\n ) : (\n <div className=\"space-y-3\">\n {/* Quick actions */}\n <div className=\"flex gap-2 text-xs\">\n <button\n type=\"button\"\n onClick={() => setSelectedScopes([...appScopes])}\n className=\"text-status-success-text hover:underline\"\n >\n {tr(\"devportal.selectAll\", \"Select all\")}\n </button>\n <button\n type=\"button\"\n onClick={() =>\n setSelectedScopes(\n appScopes.filter(\n (s) => s.includes(\":read\") || s.includes(\":list\"),\n ),\n )\n }\n className=\"text-status-success-text hover:underline\"\n >\n {tr(\"devportal.readOnly\", \"Read-only\")}\n </button>\n <button\n type=\"button\"\n onClick={() => setSelectedScopes([])}\n className=\"text-text-secondary hover:underline\"\n >\n {tr(\"devportal.clear\", \"Clear\")}\n </button>\n </div>\n {/* Grouped scopes */}\n {[...scopeGroups.entries()].map(([group, scopes]) => (\n <div key={group}>\n <p className=\"mb-1 text-xs font-semibold uppercase text-text-secondary\">\n {group}\n </p>\n <div className=\"flex flex-wrap gap-1.5\">\n {scopes.map((scope) => {\n const isSelected = selectedScopes.includes(scope);\n const action = scope.split(\":\")[1] || scope;\n const isWrite =\n action.includes(\"write\") ||\n action.includes(\"create\") ||\n action.includes(\"delete\") ||\n action.includes(\"admin\");\n return (\n <button\n key={scope}\n type=\"button\"\n onClick={() => toggleScope(scope)}\n className={cn(\n \"rounded-md px-2.5 py-1 text-xs font-medium transition-colors\",\n isSelected\n ? isWrite\n ? \"bg-status-warning-bg text-white\"\n : \"bg-status-success-bg text-white\"\n : \"border bg-bg-sunken text-text-secondary hover:bg-bg-sunken/80\",\n )}\n >\n {action}\n </button>\n );\n })}\n </div>\n </div>\n ))}\n </div>\n )}\n </div>\n\n {/* Workspace Selection (multi-select picker) */}\n <div>\n <label className=\"mb-2 block text-sm font-medium\">\n {tr(\"devportal.workspaceAccess\", \"Workspace Access\")}\n </label>\n\n {/* All workspaces toggle */}\n <button\n type=\"button\"\n onClick={handleAllWorkspacesToggle}\n disabled={loading}\n className={cn(\n \"mb-2 flex w-full items-center gap-2 rounded-md border px-3 py-2 text-sm transition-colors\",\n allWorkspaces\n ? \"border-status-success-bg bg-status-success-bg-subtle text-status-success-text\"\n : \"hover:bg-bg-sunken\",\n )}\n >\n {allWorkspaces && (\n <Check className=\"h-4 w-4 text-status-success-text\" />\n )}\n <span>\n {tr(\n \"devportal.allWorkspacesUnrestricted\",\n \"All workspaces (unrestricted)\",\n )}\n </span>\n </button>\n\n {!allWorkspaces && (\n <>\n {/* Search */}\n <div className=\"relative mb-2\">\n <Search className=\"absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-text-secondary\" />\n <input\n type=\"text\"\n value={workspaceSearch}\n onChange={(e) => setWorkspaceSearch(e.target.value)}\n placeholder={tr(\n \"devportal.searchWorkspaces\",\n \"Search workspaces...\",\n )}\n disabled={loading}\n className=\"block w-full rounded-md border py-2 pl-9 pr-3 text-sm placeholder:text-text-secondary focus:outline-none focus:ring-2 focus:ring-status-success-bg/20 focus:border-status-success-bg transition-colors disabled:opacity-50\"\n />\n </div>\n\n {/* Workspace list */}\n <div className=\"max-h-40 overflow-y-auto rounded-md border\">\n {loadingWorkspaces ? (\n <div className=\"flex items-center justify-center p-4\">\n <Loader2 className=\"h-4 w-4 animate-spin text-text-secondary\" />\n <span className=\"ml-2 text-sm text-text-secondary\">\n {tr(\n \"devportal.loadingWorkspaces\",\n \"Loading workspaces...\",\n )}\n </span>\n </div>\n ) : filteredWorkspaces.length === 0 ? (\n <p className=\"p-4 text-center text-sm text-text-secondary\">\n {workspaces.length === 0\n ? tr(\n \"devportal.noWorkspacesFound\",\n \"No workspaces found\",\n )\n : tr(\n \"devportal.noMatchingWorkspaces\",\n \"No matching workspaces\",\n )}\n </p>\n ) : (\n filteredWorkspaces.map((ws) => {\n const isSelected = selectedWorkspaceIds.includes(\n ws.id,\n );\n return (\n <button\n key={ws.id}\n type=\"button\"\n onClick={() => toggleWorkspace(ws.id)}\n disabled={loading}\n className={cn(\n \"flex w-full items-center gap-2 border-b px-3 py-2 text-left text-sm transition-colors last:border-b-0\",\n isSelected\n ? \"bg-status-success-bg-subtle text-status-success-text\"\n : \"hover:bg-bg-sunken\",\n )}\n >\n <div\n className={cn(\n \"flex h-4 w-4 shrink-0 items-center justify-center rounded border\",\n isSelected\n ? \"border-status-success-bg bg-status-success-bg\"\n : \"border-muted-foreground/30\",\n )}\n >\n {isSelected && (\n <Check className=\"h-3 w-3 text-white\" />\n )}\n </div>\n <span className=\"font-medium\">{ws.name}</span>\n </button>\n );\n })\n )}\n </div>\n\n {/* Selected count */}\n {selectedWorkspaceIds.length > 0 && (\n <p className=\"mt-1.5 text-xs text-text-secondary\">\n {selectedWorkspaceIds.length} workspace\n {selectedWorkspaceIds.length !== 1 ? \"s\" : \"\"} selected\n </p>\n )}\n </>\n )}\n </div>\n\n {/* Expiration */}\n <div>\n <label className=\"mb-2 block text-sm font-medium\">\n {tr(\"devportal.expiration\", \"Expiration\")}\n <span className=\"ml-1.5 text-xs font-normal text-text-secondary\">\n {tr(\"devportal.optional\", \"(optional)\")}\n </span>\n </label>\n <input\n type=\"datetime-local\"\n value={expiresAt}\n onChange={(e) => setExpiresAt(e.target.value)}\n disabled={loading}\n min={new Date().toISOString().slice(0, 16)}\n className=\"block w-full rounded-md border px-3 py-2 text-sm placeholder:text-text-secondary focus:outline-none focus:ring-2 focus:ring-status-success-bg/20 focus:border-status-success-bg transition-colors disabled:opacity-50\"\n />\n <p className=\"mt-1.5 text-xs text-text-secondary\">\n {tr(\n \"devportal.leaveEmptyForNonExpiring\",\n \"Leave empty for a delegation that does not expire.\",\n )}\n </p>\n </div>\n\n {/* Error */}\n {error && (\n <div className=\"flex items-start gap-3 rounded-md border border-border-subtle bg-status-error-bg-subtle p-4\">\n <AlertCircle className=\"h-5 w-5 shrink-0 text-status-error-text\" />\n <p className=\"text-sm text-status-error-text\">{error}</p>\n </div>\n )}\n </>\n )}\n </div>\n\n {/* Footer */}\n {!successMessage && (\n <div className=\"flex justify-end gap-3 border-t p-6\">\n <button\n type=\"button\"\n onClick={handleClose}\n disabled={loading}\n className=\"rounded-md border px-4 py-2 text-sm font-medium hover:bg-bg-sunken transition-colors disabled:opacity-50\"\n >\n {tr(\"devportal.cancel\", \"Cancel\")}\n </button>\n <button\n type=\"button\"\n onClick={handleGrant}\n disabled={loading || selectedScopes.length === 0}\n className={cn(\n \"flex items-center gap-2 rounded-md px-4 py-2 text-sm font-medium text-white transition-colors disabled:opacity-50 disabled:cursor-not-allowed\",\n \"bg-status-success-bg hover:bg-status-success-bg/90\",\n )}\n >\n {loading ? (\n <>\n <Loader2 className=\"h-4 w-4 animate-spin\" />\n {tr(\"devportal.granting\", \"Granting...\")}\n </>\n ) : (\n tr(\"devportal.grant\", \"Grant\")\n )}\n </button>\n </div>\n )}\n </div>\n </div>\n );\n};\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"LoadingState.js","names":[],"sources":["../../../src/devportal/components/LoadingState.tsx"],"sourcesContent":["import type { FC } from \"react\";\nimport { Loader2 } from \"lucide-react\";\nimport { useI18n } from \"@burdenoff/fe-libs/shared/providers/shell/I18nProvider\";\nimport { cn } from \"../utils/cn\";\n\ninterface LoadingStateProps {\n message?: string;\n className?: string;\n size?: \"sm\" | \"md\" | \"lg\";\n}\n\n/**\n * Loading state component with spinner and optional message\n */\nexport const LoadingState: FC<LoadingStateProps> = ({\n message,\n className,\n size = \"md\",\n}) => {\n const { t } = useI18n();\n const tr = (key: string, fallback: string): string => {\n const value = t(key);\n return value === key ? fallback : value;\n };\n const resolvedMessage = message ?? tr(\"devportal.loadingDots\", \"Loading...\");\n const sizeClasses = {\n sm: \"h-4 w-4\",\n md: \"h-8 w-8\",\n lg: \"h-12 w-12\",\n };\n\n return (\n <div\n className={cn(\n \"flex flex-col items-center justify-center gap-4 p-8\",\n className,\n )}\n >\n <Loader2 className={cn(\"animate-spin text-primary\", sizeClasses[size])} />\n {resolvedMessage && (\n <p className=\"text-sm text-text-secondary\">{resolvedMessage}</p>\n )}\n </div>\n );\n};\n\n/**\n * Full page loading state\n */\nexport const PageLoadingState: FC<{ message?: string }> = ({ message }) => (\n <div className=\"flex min-h-[400px] w-full items-center justify-center\">\n <LoadingState message={message} size=\"lg\" />\n </div>\n);\n\n/**\n * Card loading skeleton\n */\nexport const CardSkeleton: FC<{ className?: string }> = ({ className }) => (\n <div
|
|
1
|
+
{"version":3,"file":"LoadingState.js","names":[],"sources":["../../../src/devportal/components/LoadingState.tsx"],"sourcesContent":["import type { FC } from \"react\";\nimport { Loader2 } from \"lucide-react\";\nimport { useI18n } from \"@burdenoff/fe-libs/shared/providers/shell/I18nProvider\";\nimport { cn } from \"../utils/cn\";\n\ninterface LoadingStateProps {\n message?: string;\n className?: string;\n size?: \"sm\" | \"md\" | \"lg\";\n}\n\n/**\n * Loading state component with spinner and optional message\n */\nexport const LoadingState: FC<LoadingStateProps> = ({\n message,\n className,\n size = \"md\",\n}) => {\n const { t } = useI18n();\n const tr = (key: string, fallback: string): string => {\n const value = t(key);\n return value === key ? fallback : value;\n };\n const resolvedMessage = message ?? tr(\"devportal.loadingDots\", \"Loading...\");\n const sizeClasses = {\n sm: \"h-4 w-4\",\n md: \"h-8 w-8\",\n lg: \"h-12 w-12\",\n };\n\n return (\n <div\n className={cn(\n \"flex flex-col items-center justify-center gap-4 p-8\",\n className,\n )}\n >\n <Loader2 className={cn(\"animate-spin text-primary\", sizeClasses[size])} />\n {resolvedMessage && (\n <p className=\"text-sm text-text-secondary\">{resolvedMessage}</p>\n )}\n </div>\n );\n};\n\n/**\n * Full page loading state\n */\nexport const PageLoadingState: FC<{ message?: string }> = ({ message }) => (\n <div className=\"flex min-h-[400px] w-full items-center justify-center\">\n <LoadingState message={message} size=\"lg\" />\n </div>\n);\n\n/**\n * Card loading skeleton\n */\nexport const CardSkeleton: FC<{ className?: string }> = ({ className }) => (\n <div\n className={cn(\n \"rounded-card border border-border-seam bg-bg-surface p-6\",\n className,\n )}\n >\n <div className=\"h-4 w-3/4 overflow-hidden rounded bg-bg-sunken animate-vc-shimmer\" />\n <div className=\"mt-4 h-3 w-1/2 overflow-hidden rounded bg-bg-sunken animate-vc-shimmer\" />\n <div className=\"mt-2 h-3 w-2/3 overflow-hidden rounded bg-bg-sunken animate-vc-shimmer\" />\n </div>\n);\n\n/**\n * Table row skeleton\n */\nexport const TableRowSkeleton: FC<{ columns?: number }> = ({ columns = 4 }) => (\n <tr>\n {Array.from({ length: columns }).map((_, i) => (\n <td key={i} className=\"px-4 py-3\">\n <div className=\"h-4 overflow-hidden rounded bg-bg-sunken animate-vc-shimmer\" />\n </td>\n ))}\n </tr>\n);\n"],"mappings":";;;;;AAcA,IAAa,KAAuC,EAClD,YACA,cACA,UAAO,WACH;CACJ,IAAM,EAAE,SAAM,GAAS,EAKjB,IAAkB,OAJZ,GAAa,MAA6B;EACpD,IAAM,IAAQ,EAAE,EAAI;AACpB,SAAO,MAAU,IAAM,IAAW;IAEE,yBAAyB,aAAa;AAO5E,QACE,kBAAC,OAAD;EACE,WAAW,EACT,uDACA,EACD;YAJH,CAME,kBAAC,GAAD,EAAS,WAAW,EAAG,6BAbP;GAClB,IAAI;GACJ,IAAI;GACJ,IAAI;GACL,CASmE,GAAM,EAAI,CAAA,EACzE,KACC,kBAAC,KAAD;GAAG,WAAU;aAA+B;GAAoB,CAAA,CAE9D;;GAOG,KAA8C,EAAE,iBAC3D,kBAAC,OAAD;CAAK,WAAU;WACb,kBAAC,GAAD;EAAuB;EAAS,MAAK;EAAO,CAAA;CACxC,CAAA"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"PageLayout.js","names":[],"sources":["../../../src/devportal/components/PageLayout.tsx"],"sourcesContent":["import type { FC, HTMLAttributes, ReactNode } from \"react\";\nimport { ArrowLeft } from \"lucide-react\";\nimport { useI18n } from \"@burdenoff/fe-libs/shared/providers/shell/I18nProvider\";\nimport { AuroraBackground, PagePurpose } from \"@burdenoff/fe-libs/ui\";\nimport { cn } from \"../utils/cn\";\nimport { useDevportal } from \"../providers/DevportalProvider\";\n\ninterface PageLayoutProps {\n title: ReactNode;\n description?: string;\n backLink?: string;\n backLabel?: string;\n actions?: ReactNode;\n children: ReactNode;\n className?: string;\n fullWidth?: boolean;\n /**\n * Plain-language \"story\" for the page — WHAT it is for and the real use-case\n * — rendered as a calm {@link PagePurpose} under the header so every page is\n * self-explanatory for a new user. Keep it to one or two sentences.\n */\n purpose?: ReactNode;\n /** Optional \"Learn more\" link rendered at the end of the purpose line. */\n purposeLearnMoreHref?: string;\n purposeLearnMoreLabel?: string;\n /**\n * When true the page header sits over a subtle living-glass aurora backdrop\n * (Boff UI 2.0). Reserve this for the MFE's primary/overview page — one\n * aurora per MFE. The backdrop is decorative (-z-10) and never moves content.\n */\n heroBackdrop?: boolean;\n}\n\n/**\n * Standard page layout with header, optional back link, and actions\n */\nexport const PageLayout: FC<PageLayoutProps> = ({\n title,\n description,\n backLink,\n backLabel,\n actions,\n children,\n className,\n fullWidth = false,\n purpose,\n purposeLearnMoreHref,\n purposeLearnMoreLabel,\n heroBackdrop = false,\n}) => {\n const { navigate, basePath } = useDevportal();\n const { t } = useI18n();\n const tr = (key: string, fallback: string): string => {\n const value = t(key);\n return value === key ? fallback : value;\n };\n const resolvedBackLabel = backLabel ?? tr(\"devportal.back\", \"Back\");\n\n const handleBack = () => {\n if (!navigate || !backLink) return;\n\n const normalizedBasePath = basePath?.endsWith(\"/\")\n ? basePath.slice(0, -1)\n : basePath || \"\";\n const normalizedBackLink = backLink.startsWith(\"/\")\n ? backLink\n : `/${backLink}`;\n const alreadyIncludesBasePath =\n normalizedBasePath.length > 0 &&\n (normalizedBackLink === normalizedBasePath ||\n normalizedBackLink.startsWith(`${normalizedBasePath}/`));\n\n navigate(\n alreadyIncludesBasePath\n ? normalizedBackLink\n : `${normalizedBasePath}${normalizedBackLink}`,\n );\n };\n\n return (\n <div className={cn(\"flex flex-col gap-6 p-6\", className)}>\n {/* Header — optionally over a subtle living-glass aurora backdrop */}\n <div\n className={cn(\n \"flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between\",\n heroBackdrop &&\n \"relative overflow-hidden rounded-card border border-border-seam/60 px-5 py-5 sm:px-6 sm:py-6\",\n )}\n >\n {heroBackdrop && <AuroraBackground intensity=\"subtle\" />}\n <div className=\"flex flex-col gap-1\">\n {backLink && (\n <button\n onClick={handleBack}\n className=\"mb-2 flex items-center gap-1 text-sm text-text-secondary hover:text-text-primary transition-colors\"\n >\n <ArrowLeft size={16} />\n {resolvedBackLabel}\n </button>\n )}\n <h1 className=\"text-2xl font-bold tracking-tight\">{title}</h1>\n {description &&
|
|
1
|
+
{"version":3,"file":"PageLayout.js","names":[],"sources":["../../../src/devportal/components/PageLayout.tsx"],"sourcesContent":["import type { FC, HTMLAttributes, ReactNode } from \"react\";\nimport { ArrowLeft } from \"lucide-react\";\nimport { useI18n } from \"@burdenoff/fe-libs/shared/providers/shell/I18nProvider\";\nimport { AuroraBackground, PagePurpose } from \"@burdenoff/fe-libs/ui\";\nimport { cn } from \"../utils/cn\";\nimport { useDevportal } from \"../providers/DevportalProvider\";\n\ninterface PageLayoutProps {\n title: ReactNode;\n description?: string;\n backLink?: string;\n backLabel?: string;\n actions?: ReactNode;\n children: ReactNode;\n className?: string;\n fullWidth?: boolean;\n /**\n * Plain-language \"story\" for the page — WHAT it is for and the real use-case\n * — rendered as a calm {@link PagePurpose} under the header so every page is\n * self-explanatory for a new user. Keep it to one or two sentences.\n */\n purpose?: ReactNode;\n /** Optional \"Learn more\" link rendered at the end of the purpose line. */\n purposeLearnMoreHref?: string;\n purposeLearnMoreLabel?: string;\n /**\n * When true the page header sits over a subtle living-glass aurora backdrop\n * (Boff UI 2.0). Reserve this for the MFE's primary/overview page — one\n * aurora per MFE. The backdrop is decorative (-z-10) and never moves content.\n */\n heroBackdrop?: boolean;\n}\n\n/**\n * Standard page layout with header, optional back link, and actions\n */\nexport const PageLayout: FC<PageLayoutProps> = ({\n title,\n description,\n backLink,\n backLabel,\n actions,\n children,\n className,\n fullWidth = false,\n purpose,\n purposeLearnMoreHref,\n purposeLearnMoreLabel,\n heroBackdrop = false,\n}) => {\n const { navigate, basePath } = useDevportal();\n const { t } = useI18n();\n const tr = (key: string, fallback: string): string => {\n const value = t(key);\n return value === key ? fallback : value;\n };\n const resolvedBackLabel = backLabel ?? tr(\"devportal.back\", \"Back\");\n\n const handleBack = () => {\n if (!navigate || !backLink) return;\n\n const normalizedBasePath = basePath?.endsWith(\"/\")\n ? basePath.slice(0, -1)\n : basePath || \"\";\n const normalizedBackLink = backLink.startsWith(\"/\")\n ? backLink\n : `/${backLink}`;\n const alreadyIncludesBasePath =\n normalizedBasePath.length > 0 &&\n (normalizedBackLink === normalizedBasePath ||\n normalizedBackLink.startsWith(`${normalizedBasePath}/`));\n\n navigate(\n alreadyIncludesBasePath\n ? normalizedBackLink\n : `${normalizedBasePath}${normalizedBackLink}`,\n );\n };\n\n return (\n <div className={cn(\"flex flex-col gap-6 p-6\", className)}>\n {/* Header — optionally over a subtle living-glass aurora backdrop */}\n <div\n className={cn(\n \"flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between\",\n heroBackdrop &&\n \"relative overflow-hidden rounded-card border border-border-seam/60 px-5 py-5 sm:px-6 sm:py-6\",\n )}\n >\n {heroBackdrop && <AuroraBackground intensity=\"subtle\" />}\n <div className=\"flex flex-col gap-1\">\n {backLink && (\n <button\n onClick={handleBack}\n className=\"mb-2 flex items-center gap-1 text-sm text-text-secondary hover:text-text-primary transition-colors\"\n >\n <ArrowLeft size={16} />\n {resolvedBackLabel}\n </button>\n )}\n <h1 className=\"text-2xl font-bold tracking-tight\">{title}</h1>\n {description && <p className=\"text-text-secondary\">{description}</p>}\n {purpose && (\n <PagePurpose\n className=\"mt-1 max-w-2xl\"\n learnMoreHref={purposeLearnMoreHref}\n learnMoreLabel={purposeLearnMoreLabel}\n >\n {purpose}\n </PagePurpose>\n )}\n </div>\n {actions && <div className=\"flex items-center gap-2\">{actions}</div>}\n </div>\n\n {/* Content */}\n <div className={cn(!fullWidth && \"max-w-7xl\")}>{children}</div>\n </div>\n );\n};\n\n/**\n * Section within a page\n */\nexport const PageSection: FC<\n {\n title?: string;\n description?: string;\n actions?: ReactNode;\n children: ReactNode;\n className?: string;\n } & HTMLAttributes<HTMLElement>\n> = ({ title, description, actions, children, className, ...rest }) => (\n <section className={cn(\"space-y-4\", className)} {...rest}>\n {(title || actions) && (\n <div className=\"flex items-center justify-between\">\n <div>\n {title && <h2 className=\"text-lg font-semibold\">{title}</h2>}\n {description && (\n <p className=\"text-sm text-text-secondary\">{description}</p>\n )}\n </div>\n {actions && <div className=\"flex items-center gap-2\">{actions}</div>}\n </div>\n )}\n {children}\n </section>\n);\n"],"mappings":";;;;;;;AAoCA,IAAa,KAAmC,EAC9C,UACA,gBACA,aACA,cACA,YACA,aACA,cACA,eAAY,IACZ,YACA,yBACA,0BACA,kBAAe,SACX;CACJ,IAAM,EAAE,aAAU,gBAAa,GAAc,EACvC,EAAE,SAAM,GAAS,EAKjB,IAAoB,OAJd,GAAa,MAA6B;EACpD,IAAM,IAAQ,EAAE,EAAI;AACpB,SAAO,MAAU,IAAM,IAAW;IAEM,kBAAkB,OAAO;AAuBnE,QACE,kBAAC,OAAD;EAAK,WAAW,EAAG,2BAA2B,EAAU;YAAxD,CAEE,kBAAC,OAAD;GACE,WAAW,EACT,sEACA,KACE,+FACH;aALH;IAOG,KAAgB,kBAAC,GAAD,EAAkB,WAAU,UAAW,CAAA;IACxD,kBAAC,OAAD;KAAK,WAAU;eAAf;MACG,KACC,kBAAC,UAAD;OACE,eAnCa;AACvB,YAAI,CAAC,KAAY,CAAC,EAAU;QAE5B,IAAM,IAAqB,GAAU,SAAS,IAAI,GAC9C,EAAS,MAAM,GAAG,GAAG,GACrB,KAAY,IACV,IAAqB,EAAS,WAAW,IAAI,GAC/C,IACA,IAAI;AAMR,UAJE,EAAmB,SAAS,MAC3B,MAAuB,KACtB,EAAmB,WAAW,GAAG,EAAmB,GAAG,IAIrD,IACA,GAAG,IAAqB,IAC7B;;OAkBS,WAAU;iBAFZ,CAIE,kBAAC,GAAD,EAAW,MAAM,IAAM,CAAA,EACtB,EACM;;MAEX,kBAAC,MAAD;OAAI,WAAU;iBAAqC;OAAW,CAAA;MAC7D,KAAe,kBAAC,KAAD;OAAG,WAAU;iBAAuB;OAAgB,CAAA;MACnE,KACC,kBAAC,GAAD;OACE,WAAU;OACV,eAAe;OACf,gBAAgB;iBAEf;OACW,CAAA;MAEZ;;IACL,KAAW,kBAAC,OAAD;KAAK,WAAU;eAA2B;KAAc,CAAA;IAChE;MAGN,kBAAC,OAAD;GAAK,WAAW,EAAG,CAAC,KAAa,YAAY;GAAG;GAAe,CAAA,CAC3D;;GAOG,KAQR,EAAE,UAAO,gBAAa,YAAS,aAAU,cAAW,GAAG,QAC1D,kBAAC,WAAD;CAAS,WAAW,EAAG,aAAa,EAAU;CAAE,GAAI;WAApD,EACI,KAAS,MACT,kBAAC,OAAD;EAAK,WAAU;YAAf,CACE,kBAAC,OAAD,EAAA,UAAA,CACG,KAAS,kBAAC,MAAD;GAAI,WAAU;aAAyB;GAAW,CAAA,EAC3D,KACC,kBAAC,KAAD;GAAG,WAAU;aAA+B;GAAgB,CAAA,CAE1D,EAAA,CAAA,EACL,KAAW,kBAAC,OAAD;GAAK,WAAU;aAA2B;GAAc,CAAA,CAChE;KAEP,EACO"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"SelectionCheckbox.js","names":[],"sources":["../../../src/devportal/components/SelectionCheckbox.tsx"],"sourcesContent":["import { useEffect, useRef } from \"react\";\nimport { Check } from \"lucide-react\";\nimport { cn } from \"../utils/cn\";\n\ninterface SelectionCheckboxProps {\n checked: boolean;\n indeterminate?: boolean;\n disabled?: boolean;\n onChange: (checked: boolean) => void;\n ariaLabel: string;\n className?: string;\n}\n\nexport function SelectionCheckbox({\n checked,\n indeterminate = false,\n disabled = false,\n onChange,\n ariaLabel,\n className,\n}: SelectionCheckboxProps) {\n const inputRef = useRef<HTMLInputElement>(null);\n\n useEffect(() => {\n if (inputRef.current) {\n inputRef.current.indeterminate = indeterminate && !checked;\n }\n }, [checked, indeterminate]);\n\n return (\n <label\n className={cn(\n \"inline-flex h-5 w-5 cursor-pointer items-center justify-center rounded border border-border-primary bg-bg-surface transition-colors\",\n checked
|
|
1
|
+
{"version":3,"file":"SelectionCheckbox.js","names":[],"sources":["../../../src/devportal/components/SelectionCheckbox.tsx"],"sourcesContent":["import { useEffect, useRef } from \"react\";\nimport { Check } from \"lucide-react\";\nimport { cn } from \"../utils/cn\";\n\ninterface SelectionCheckboxProps {\n checked: boolean;\n indeterminate?: boolean;\n disabled?: boolean;\n onChange: (checked: boolean) => void;\n ariaLabel: string;\n className?: string;\n}\n\nexport function SelectionCheckbox({\n checked,\n indeterminate = false,\n disabled = false,\n onChange,\n ariaLabel,\n className,\n}: SelectionCheckboxProps) {\n const inputRef = useRef<HTMLInputElement>(null);\n\n useEffect(() => {\n if (inputRef.current) {\n inputRef.current.indeterminate = indeterminate && !checked;\n }\n }, [checked, indeterminate]);\n\n return (\n <label\n className={cn(\n \"inline-flex h-5 w-5 cursor-pointer items-center justify-center rounded border border-border-primary bg-bg-surface transition-colors\",\n checked &&\n \"border-primary bg-action-primary-bg text-action-primary-text\",\n disabled && \"cursor-not-allowed opacity-50\",\n className,\n )}\n >\n <input\n ref={inputRef}\n type=\"checkbox\"\n checked={checked}\n disabled={disabled}\n aria-label={ariaLabel}\n onChange={(event) => onChange(event.target.checked)}\n className=\"sr-only\"\n />\n {(checked || indeterminate) && (\n <Check className={cn(\"h-3.5 w-3.5\", indeterminate && \"opacity-70\")} />\n )}\n </label>\n );\n}\n"],"mappings":";;;;;AAaA,SAAgB,EAAkB,EAChC,YACA,mBAAgB,IAChB,cAAW,IACX,aACA,cACA,gBACyB;CACzB,IAAM,IAAW,EAAyB,KAAK;AAQ/C,QANA,QAAgB;AACd,EAAI,EAAS,YACX,EAAS,QAAQ,gBAAgB,KAAiB,CAAC;IAEpD,CAAC,GAAS,EAAc,CAAC,EAG1B,kBAAC,SAAD;EACE,WAAW,EACT,uIACA,KACE,gEACF,KAAY,iCACZ,EACD;YAPH,CASE,kBAAC,SAAD;GACE,KAAK;GACL,MAAK;GACI;GACC;GACV,cAAY;GACZ,WAAW,MAAU,EAAS,EAAM,OAAO,QAAQ;GACnD,WAAU;GACV,CAAA,GACA,KAAW,MACX,kBAAC,GAAD,EAAO,WAAW,EAAG,eAAe,KAAiB,aAAa,EAAI,CAAA,CAElE"}
|
|
@@ -2,9 +2,9 @@ import { usePermissions as e } from "@burdenoff/fe-libs/shared/providers/shell";
|
|
|
2
2
|
//#region src/devportal/hooks/usePermission.ts
|
|
3
3
|
var t = (t) => {
|
|
4
4
|
try {
|
|
5
|
-
let n = e(), r = n.isLoading, i = n.permissions?.permissions?.some((e) => e.resourceType === "rbac" && e.level === "ADMIN") ?? !1, a = n.hasPermission;
|
|
5
|
+
let n = e(), r = n.isLoading, i = n.permissions?.permissions?.some((e) => e.resourceType === "rbac" && e.level === "ADMIN") ?? !1, a = t.split(":")[0], o = n.permissions?.permissions?.some((e) => e.resourceType === a && e.level === "ADMIN") ?? !1, s = n.hasPermission, c = n.permissions?.permissions?.some((e) => e.resourceType.startsWith("devportal-")) ?? !1, l = !r && !!n.permissions && !c;
|
|
6
6
|
return {
|
|
7
|
-
allowed: i || (
|
|
7
|
+
allowed: i || o || (s ? s(t) : !0) || l,
|
|
8
8
|
loading: r
|
|
9
9
|
};
|
|
10
10
|
} catch {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"usePermission.js","names":[],"sources":["../../../src/devportal/hooks/usePermission.ts"],"sourcesContent":["import { usePermissions } from \"@burdenoff/fe-libs/shared/providers/shell\";\nimport type { DevportalPermission } from \"../constants/permissions\";\n\ninterface PermissionResult {\n /** True when the actor has the permission (or is RBAC admin / standalone). */\n allowed: boolean;\n /** True while the permissions request is still in flight. */\n loading: boolean;\n}\n\n/**\n * Button-level permission gate for devportal mutations.\n *\n * Wraps the shell `usePermissions` hook with the same standalone /\n * admin-bypass semantics as `PermissionGuard`. Use to gate individual\n * mutation buttons (Create / Edit / Delete / Publish / Submit / Approve /\n * Reject / Generate) so disabled state is server-driven, not just\n * page-level.\n *\n * Standalone mode (no shell provider) -> `allowed: true` so dev mode\n * stays usable. The page-level `PermissionGuard` is the coarse-grained\n * gate; this hook is the fine-grained one.\n */\nexport const useDevportalPermission = (\n permission: DevportalPermission,\n): PermissionResult => {\n try {\n const ctx = usePermissions();\n const isLoading = ctx.isLoading;\n const isRbacAdmin =\n ctx.permissions?.permissions?.some(\n (entry) => entry.resourceType === \"rbac\" && entry.level === \"ADMIN\",\n ) ?? false;\n const has = ctx.hasPermission as (p: DevportalPermission) => boolean;\n return {\n allowed
|
|
1
|
+
{"version":3,"file":"usePermission.js","names":[],"sources":["../../../src/devportal/hooks/usePermission.ts"],"sourcesContent":["import { usePermissions } from \"@burdenoff/fe-libs/shared/providers/shell\";\nimport type { DevportalPermission } from \"../constants/permissions\";\n\ninterface PermissionResult {\n /** True when the actor has the permission (or is RBAC admin / standalone). */\n allowed: boolean;\n /** True while the permissions request is still in flight. */\n loading: boolean;\n}\n\n/**\n * Button-level permission gate for devportal mutations.\n *\n * Wraps the shell `usePermissions` hook with the same standalone /\n * admin-bypass semantics as `PermissionGuard`. Use to gate individual\n * mutation buttons (Create / Edit / Delete / Publish / Submit / Approve /\n * Reject / Generate) so disabled state is server-driven, not just\n * page-level.\n *\n * Standalone mode (no shell provider) -> `allowed: true` so dev mode\n * stays usable. The page-level `PermissionGuard` is the coarse-grained\n * gate; this hook is the fine-grained one.\n */\nexport const useDevportalPermission = (\n permission: DevportalPermission,\n): PermissionResult => {\n try {\n const ctx = usePermissions();\n const isLoading = ctx.isLoading;\n const isRbacAdmin =\n ctx.permissions?.permissions?.some(\n (entry) => entry.resourceType === \"rbac\" && entry.level === \"ADMIN\",\n ) ?? false;\n\n // Extract the resource type from the permission string (e.g. \"devportal-app:submit\" → \"devportal-app\")\n const resourceType = permission.split(\":\")[0];\n const hasResourceAdmin =\n ctx.permissions?.permissions?.some(\n (entry) =>\n entry.resourceType === resourceType && entry.level === \"ADMIN\",\n ) ?? false;\n\n const has = ctx.hasPermission as (p: DevportalPermission) => boolean;\n\n // If the user is authenticated but the permissions API returned nothing about devportal\n // resources at all, fall back to true — the backend enforces real auth.\n const hasAnyDevportalPermission =\n ctx.permissions?.permissions?.some((entry) =>\n entry.resourceType.startsWith(\"devportal-\"),\n ) ?? false;\n const isAuthenticated = !isLoading && !!ctx.permissions;\n const noDevportalInfoYet = isAuthenticated && !hasAnyDevportalPermission;\n\n return {\n allowed:\n isRbacAdmin ||\n hasResourceAdmin ||\n (has ? has(permission) : true) ||\n noDevportalInfoYet,\n loading: isLoading,\n };\n } catch {\n // Standalone / dev mode: no shell provider mounted.\n return { allowed: true, loading: false };\n }\n};\n"],"mappings":";;AAuBA,IAAa,KACX,MACqB;AACrB,KAAI;EACF,IAAM,IAAM,GAAgB,EACtB,IAAY,EAAI,WAChB,IACJ,EAAI,aAAa,aAAa,MAC3B,MAAU,EAAM,iBAAiB,UAAU,EAAM,UAAU,QAC7D,IAAI,IAGD,IAAe,EAAW,MAAM,IAAI,CAAC,IACrC,IACJ,EAAI,aAAa,aAAa,MAC3B,MACC,EAAM,iBAAiB,KAAgB,EAAM,UAAU,QAC1D,IAAI,IAED,IAAM,EAAI,eAIV,IACJ,EAAI,aAAa,aAAa,MAAM,MAClC,EAAM,aAAa,WAAW,aAAa,CAC5C,IAAI,IAED,IADkB,CAAC,KAAa,CAAC,CAAC,EAAI,eACE,CAAC;AAE/C,SAAO;GACL,SACE,KACA,MACC,IAAM,EAAI,EAAW,GAAG,OACzB;GACF,SAAS;GACV;SACK;AAEN,SAAO;GAAE,SAAS;GAAM,SAAS;GAAO"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AnalyticsDashboardPage.js","names":[],"sources":["../../../src/devportal/pages/AnalyticsDashboardPage.tsx"],"sourcesContent":["import type { FC } from \"react\";\nimport { useEffect, useMemo, useState } from \"react\";\nimport { useParams } from \"react-router-dom\";\nimport {\n Activity,\n AlertCircle,\n AlertTriangle,\n BarChart3,\n Clock,\n Loader2,\n Users,\n} from \"lucide-react\";\nimport {\n Bar,\n BarChart,\n CartesianGrid,\n Line,\n LineChart,\n ResponsiveContainer,\n Tooltip,\n XAxis,\n YAxis,\n} from \"recharts\";\nimport { EmphasisPanel, GlassCard } from \"@burdenoff/fe-libs/ui\";\nimport { PageLayout, PageSection } from \"../components/PageLayout\";\nimport { DataTable } from \"../components/DataTable\";\nimport { EmptyState } from \"../components/EmptyState\";\nimport { StatCard, StatCardGrid } from \"../components/StatCard\";\nimport { AppListItem } from \"../components/AppCard\";\nimport type { Application } from \"../types\";\nimport { useDevportal } from \"../providers/DevportalProvider\";\nimport {\n type GetAnalyticsDashboardOptionsQuery,\n getErrorMessage,\n useGetAnalyticsDashboardOptionsQuery,\n useGetApplicationAnalyticsDashboardQuery,\n} from \"../hooks\";\nimport { useTr } from \"../utils/i18n\";\n\ntype TimeRange = \"7d\" | \"30d\" | \"90d\";\n\nconst daysByRange: Record<TimeRange, number> = {\n \"7d\": 7,\n \"30d\": 30,\n \"90d\": 90,\n};\n\nconst chartColors = {\n primary: \"var(--color-chart-1)\",\n secondary: \"var(--color-chart-2)\",\n tertiary: \"var(--color-chart-4)\",\n negative: \"var(--color-chart-negative)\",\n};\n\nexport const AnalyticsDashboardPage: FC = () => {\n const { appId } = useParams<{ appId: string }>();\n const { navigate, basePath, activeOrgId } = useDevportal();\n const tr = useTr();\n const [timeRange, setTimeRange] = useState<TimeRange>(\"30d\");\n const [selectedApplicationId, setSelectedApplicationId] = useState(\n appId || \"\",\n );\n\n const now = useMemo(() => new Date(), []);\n const startDate = useMemo(() => {\n const nextDate = new Date(now);\n nextDate.setDate(nextDate.getDate() - daysByRange[timeRange]);\n return nextDate.toISOString();\n }, [now, timeRange]);\n\n const {\n data: optionsData,\n loading: optionsLoading,\n error: optionsError,\n } = useGetAnalyticsDashboardOptionsQuery({\n skip: !!appId,\n fetchPolicy: \"cache-and-network\",\n });\n\n const { data, loading, error } = useGetApplicationAnalyticsDashboardQuery({\n variables: {\n applicationId: selectedApplicationId || appId || \"\",\n period: \"daily\",\n startDate,\n endDate: now.toISOString(),\n },\n skip: !(selectedApplicationId || appId),\n fetchPolicy: \"cache-and-network\",\n });\n\n const allApplications: Application[] =\n optionsData?.applications?.edges?.map(\n (\n edge: GetAnalyticsDashboardOptionsQuery[\"applications\"][\"edges\"][number],\n ) => edge.node as Application,\n ) ?? [];\n // Scope the app picker to the active org when one is selected\n const applications = activeOrgId\n ? allApplications.filter((a) => a.developerOrganizationId === activeOrgId)\n : allApplications;\n const selectedApplication = data?.application;\n\n useEffect(() => {\n const name = selectedApplication?.name;\n const id = appId || selectedApplicationId;\n if (!id || !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(id, name);\n return () => reg?.unregister(id);\n }, [appId, selectedApplicationId, selectedApplication?.name]);\n\n const analytics = [...(data?.applicationAnalytics ?? [])].sort(\n (left, right) =>\n new Date(left.date).getTime() - new Date(right.date).getTime(),\n );\n\n const latest = analytics[analytics.length - 1];\n const previous = analytics[analytics.length - 2];\n const chartData = analytics.map((entry) => ({\n date: new Date(entry.date).toLocaleDateString(undefined, {\n month: \"short\",\n day: \"numeric\",\n }),\n apiCalls: entry.apiCalls,\n errors: entry.errors,\n activeUsers: entry.activeUsers,\n newUsers: entry.newUsers,\n sessions: entry.sessions,\n avgResponseTime: entry.avgResponseTime ?? 0,\n }));\n\n const errorRate =\n latest && latest.apiCalls > 0 ? (latest.errors / latest.apiCalls) * 100 : 0;\n const previousErrorRate =\n previous && previous.apiCalls > 0\n ? (previous.errors / previous.apiCalls) * 100\n : 0;\n const responseTrend =\n latest?.avgResponseTime && previous?.avgResponseTime\n ? previous.avgResponseTime - latest.avgResponseTime\n : undefined;\n\n const handleNavigate = (path: string) => {\n navigate?.(`${basePath}${path}`);\n };\n\n if (!appId && optionsLoading && !optionsData) {\n return (\n <PageLayout\n title={tr(\"devportal.analytics.title\", \"Analytics\")}\n description={tr(\n \"devportal.analytics.description\",\n \"Explore live application usage and performance metrics\",\n )}\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 (!appId && optionsError) {\n return (\n <PageLayout\n title={tr(\"devportal.analytics.title\", \"Analytics\")}\n description={tr(\n \"devportal.analytics.description\",\n \"Explore live application usage and performance metrics\",\n )}\n >\n <EmptyState\n icon={<AlertCircle size={32} className=\"text-status-error-text\" />}\n title={tr(\n \"devportal.analytics.errorLoadApps\",\n \"Unable to load applications\",\n )}\n description={getErrorMessage(optionsError)}\n />\n </PageLayout>\n );\n }\n\n if (!appId && !selectedApplicationId) {\n return (\n <PageLayout\n title={tr(\"devportal.analytics.title\", \"Analytics\")}\n description={tr(\n \"devportal.analytics.selectAppDescription\",\n \"Select an application to review live usage metrics from the public gateway\",\n )}\n >\n {applications.length > 0 ? (\n <div className=\"space-y-2\">\n {applications.map((application) => (\n <AppListItem\n key={application.id}\n app={application}\n onClick={() => setSelectedApplicationId(application.id)}\n />\n ))}\n </div>\n ) : (\n <EmptyState\n illustration=\"empty-data\"\n icon={<BarChart3 size={32} />}\n title={tr(\n \"devportal.analytics.noAppsTitle\",\n \"No applications available\",\n )}\n description={tr(\n \"devportal.analytics.noAppsDescription\",\n \"Create an application before viewing analytics.\",\n )}\n />\n )}\n </PageLayout>\n );\n }\n\n if (error) {\n return (\n <PageLayout\n title={tr(\"devportal.analytics.title\", \"Analytics\")}\n description={tr(\n \"devportal.analytics.description\",\n \"Explore live application usage and performance metrics\",\n )}\n backLink={\n selectedApplicationId ? `/apps/${selectedApplicationId}` : undefined\n }\n backLabel={tr(\n \"devportal.analytics.backToApplication\",\n \"Back to Application\",\n )}\n >\n <EmptyState\n icon={<AlertCircle size={32} className=\"text-status-error-text\" />}\n title={tr(\n \"devportal.analytics.errorLoadAnalytics\",\n \"Unable to load analytics\",\n )}\n description={getErrorMessage(error)}\n />\n </PageLayout>\n );\n }\n\n return (\n <PageLayout\n title={\n appId\n ? tr(\"devportal.analytics.appTitle\", \"Application Analytics\")\n : tr(\"devportal.analytics.title\", \"Analytics\")\n }\n description={\n selectedApplication\n ? `${tr(\"devportal.analytics.liveUsageFor\", \"Live usage and performance for\")} ${selectedApplication.name}`\n : tr(\n \"devportal.analytics.description\",\n \"Explore live application usage and performance metrics\",\n )\n }\n purpose={tr(\n \"devportal.analytics.purpose\",\n \"Analytics shows how your apps are actually used — API call volume, active installs, latency, and error rates over time — so you can spot regressions, plan capacity, and prove value to your users. Pick an app and a time range to drill in.\",\n )}\n backLink={\n appId && selectedApplication\n ? `/apps/${selectedApplication.id}`\n : undefined\n }\n backLabel={tr(\n \"devportal.analytics.backToApplication\",\n \"Back to Application\",\n )}\n actions={\n <div className=\"flex flex-wrap gap-2\">\n {!appId && applications.length > 0 && (\n <select\n value={selectedApplicationId}\n onChange={(event) => setSelectedApplicationId(event.target.value)}\n aria-label={tr(\n \"devportal.analytics.selectAppAria\",\n \"Select application\",\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 {applications.map((application) => (\n <option key={application.id} value={application.id}>\n {application.name}\n </option>\n ))}\n </select>\n )}\n <select\n value={timeRange}\n onChange={(event) => setTimeRange(event.target.value as TimeRange)}\n aria-label={tr(\n \"devportal.analytics.selectRangeAria\",\n \"Select time range\",\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=\"7d\">\n {tr(\"devportal.analytics.range.7d\", \"Last 7 days\")}\n </option>\n <option value=\"30d\">\n {tr(\"devportal.analytics.range.30d\", \"Last 30 days\")}\n </option>\n <option value=\"90d\">\n {tr(\"devportal.analytics.range.90d\", \"Last 90 days\")}\n </option>\n </select>\n </div>\n }\n >\n {loading && !analytics.length ? (\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 ) : analytics.length > 0 ? (\n <>\n <EmphasisPanel className=\"mb-6\">\n <StatCardGrid columns={4}>\n <StatCard\n title={tr(\"devportal.analytics.apiCalls\", \"API Calls\")}\n value={(latest?.apiCalls ?? 0).toLocaleString()}\n icon={<Activity size={20} />}\n />\n <StatCard\n title={tr(\"devportal.analytics.activeUsers\", \"Active Users\")}\n value={(latest?.activeUsers ?? 0).toLocaleString()}\n icon={<Users size={20} />}\n />\n <StatCard\n title={tr(\"devportal.analytics.errorRate\", \"Error Rate\")}\n value={`${errorRate.toFixed(2)}%`}\n icon={<AlertTriangle size={20} />}\n trend={\n previous\n ? {\n value: previousErrorRate - errorRate,\n label: tr(\n \"devportal.analytics.vsPrevious\",\n \"vs previous period\",\n ),\n }\n : undefined\n }\n />\n <StatCard\n title={tr(\n \"devportal.analytics.avgResponseTime\",\n \"Avg Response Time\",\n )}\n value={\n latest?.avgResponseTime\n ? `${latest.avgResponseTime}ms`\n : tr(\"devportal.common.na\", \"N/A\")\n }\n icon={<Clock size={20} />}\n trend={\n typeof responseTrend === \"number\"\n ? {\n value: responseTrend,\n label: tr(\n \"devportal.analytics.fasterThanPrevious\",\n \"faster than previous\",\n ),\n }\n : undefined\n }\n />\n </StatCardGrid>\n </EmphasisPanel>\n\n <div className=\"grid gap-6 xl:grid-cols-2\">\n <PageSection\n title={tr(\"devportal.analytics.trafficTrend\", \"Traffic trend\")}\n >\n <GlassCard treatment=\"insight\" className=\"p-4\">\n <ResponsiveContainer width=\"100%\" height={300}>\n <LineChart data={chartData}>\n <CartesianGrid\n strokeDasharray=\"3 3\"\n stroke=\"var(--color-chart-grid)\"\n />\n <XAxis\n dataKey=\"date\"\n stroke=\"var(--color-chart-axis)\"\n fontSize={12}\n />\n <YAxis stroke=\"var(--color-chart-axis)\" fontSize={12} />\n <Tooltip\n contentStyle={{\n backgroundColor: \"var(--color-card)\",\n border: \"1px solid var(--color-border-default)\",\n borderRadius: 12,\n }}\n />\n <Line\n type=\"monotone\"\n dataKey=\"apiCalls\"\n stroke={chartColors.primary}\n strokeWidth={2}\n dot={false}\n />\n <Line\n type=\"monotone\"\n dataKey=\"errors\"\n stroke={chartColors.negative}\n strokeWidth={2}\n dot={false}\n />\n </LineChart>\n </ResponsiveContainer>\n </GlassCard>\n </PageSection>\n\n <PageSection\n title={tr(\"devportal.analytics.userActivity\", \"User activity\")}\n >\n <GlassCard treatment=\"insight\" className=\"p-4\">\n <ResponsiveContainer width=\"100%\" height={300}>\n <BarChart data={chartData}>\n <CartesianGrid\n strokeDasharray=\"3 3\"\n stroke=\"var(--color-chart-grid)\"\n />\n <XAxis\n dataKey=\"date\"\n stroke=\"var(--color-chart-axis)\"\n fontSize={12}\n />\n <YAxis stroke=\"var(--color-chart-axis)\" fontSize={12} />\n <Tooltip\n contentStyle={{\n backgroundColor: \"var(--color-card)\",\n border: \"1px solid var(--color-border-default)\",\n borderRadius: 12,\n }}\n />\n <Bar\n dataKey=\"activeUsers\"\n fill={chartColors.primary}\n radius={[6, 6, 0, 0]}\n />\n <Bar\n dataKey=\"newUsers\"\n fill={chartColors.secondary}\n radius={[6, 6, 0, 0]}\n />\n </BarChart>\n </ResponsiveContainer>\n </GlassCard>\n </PageSection>\n </div>\n\n <PageSection\n title={tr(\"devportal.analytics.dailyBreakdown\", \"Daily breakdown\")}\n >\n <DataTable\n data={analytics}\n keyField=\"id\"\n columns={[\n {\n key: \"date\",\n header: tr(\"devportal.analytics.date\", \"Date\"),\n render: (entry) =>\n new Date(entry.date).toLocaleDateString(undefined, {\n month: \"short\",\n day: \"numeric\",\n year: \"numeric\",\n }),\n },\n {\n key: \"apiCalls\",\n header: tr(\"devportal.analytics.apiCalls\", \"API Calls\"),\n render: (entry) => entry.apiCalls.toLocaleString(),\n },\n {\n key: \"errors\",\n header: tr(\"devportal.analytics.errors\", \"Errors\"),\n render: (entry) => entry.errors.toLocaleString(),\n },\n {\n key: \"activeUsers\",\n header: tr(\"devportal.analytics.activeUsers\", \"Active Users\"),\n render: (entry) => entry.activeUsers.toLocaleString(),\n },\n {\n key: \"newUsers\",\n header: tr(\"devportal.analytics.newUsers\", \"New Users\"),\n render: (entry) => entry.newUsers.toLocaleString(),\n },\n {\n key: \"avgResponseTime\",\n header: tr(\"devportal.analytics.avgResponse\", \"Avg Response\"),\n render: (entry) =>\n entry.avgResponseTime\n ? `${entry.avgResponseTime}ms`\n : tr(\"devportal.common.na\", \"N/A\"),\n },\n {\n key: \"sessions\",\n header: tr(\"devportal.analytics.sessions\", \"Sessions\"),\n render: (entry) => entry.sessions.toLocaleString(),\n },\n ]}\n />\n </PageSection>\n </>\n ) : (\n <EmptyState\n illustration=\"empty-data\"\n icon={<BarChart3 size={32} />}\n title={tr(\n \"devportal.analytics.noAnalyticsTitle\",\n \"No analytics available yet\",\n )}\n description={tr(\n \"devportal.analytics.noAnalyticsDescription\",\n \"Analytics will appear after this application starts receiving traffic through the public gateway.\",\n )}\n action={\n !appId ? (\n <button\n type=\"button\"\n onClick={() => handleNavigate(\"/apps\")}\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 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary\"\n >\n {tr(\n \"devportal.analytics.viewApplications\",\n \"View applications\",\n )}\n </button>\n ) : undefined\n }\n />\n )}\n </PageLayout>\n );\n};\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"AnalyticsDashboardPage.js","names":[],"sources":["../../../src/devportal/pages/AnalyticsDashboardPage.tsx"],"sourcesContent":["import type { FC } from \"react\";\nimport { useEffect, useMemo, useState } from \"react\";\nimport { useParams } from \"react-router-dom\";\nimport {\n Activity,\n AlertCircle,\n AlertTriangle,\n BarChart3,\n Clock,\n Loader2,\n Users,\n} from \"lucide-react\";\nimport {\n Bar,\n BarChart,\n CartesianGrid,\n Line,\n LineChart,\n ResponsiveContainer,\n Tooltip,\n XAxis,\n YAxis,\n} from \"recharts\";\nimport { EmphasisPanel, GlassCard } from \"@burdenoff/fe-libs/ui\";\nimport { PageLayout, PageSection } from \"../components/PageLayout\";\nimport { DataTable } from \"../components/DataTable\";\nimport { EmptyState } from \"../components/EmptyState\";\nimport { StatCard, StatCardGrid } from \"../components/StatCard\";\nimport { AppListItem } from \"../components/AppCard\";\nimport type { Application } from \"../types\";\nimport { useDevportal } from \"../providers/DevportalProvider\";\nimport {\n type GetAnalyticsDashboardOptionsQuery,\n getErrorMessage,\n useGetAnalyticsDashboardOptionsQuery,\n useGetApplicationAnalyticsDashboardQuery,\n} from \"../hooks\";\nimport { useTr } from \"../utils/i18n\";\n\ntype TimeRange = \"7d\" | \"30d\" | \"90d\";\n\nconst daysByRange: Record<TimeRange, number> = {\n \"7d\": 7,\n \"30d\": 30,\n \"90d\": 90,\n};\n\nconst chartColors = {\n primary: \"var(--color-chart-1)\",\n secondary: \"var(--color-chart-2)\",\n tertiary: \"var(--color-chart-4)\",\n negative: \"var(--color-chart-negative)\",\n};\n\nexport const AnalyticsDashboardPage: FC = () => {\n const { appId } = useParams<{ appId: string }>();\n const { navigate, basePath, activeOrgId } = useDevportal();\n const tr = useTr();\n const [timeRange, setTimeRange] = useState<TimeRange>(\"30d\");\n const [selectedApplicationId, setSelectedApplicationId] = useState(\n appId || \"\",\n );\n\n const now = useMemo(() => new Date(), []);\n const startDate = useMemo(() => {\n const nextDate = new Date(now);\n nextDate.setDate(nextDate.getDate() - daysByRange[timeRange]);\n return nextDate.toISOString();\n }, [now, timeRange]);\n\n const {\n data: optionsData,\n loading: optionsLoading,\n error: optionsError,\n } = useGetAnalyticsDashboardOptionsQuery({\n skip: !!appId,\n fetchPolicy: \"cache-and-network\",\n });\n\n const { data, loading, error } = useGetApplicationAnalyticsDashboardQuery({\n variables: {\n applicationId: selectedApplicationId || appId || \"\",\n period: \"daily\",\n startDate,\n endDate: now.toISOString(),\n },\n skip: !(selectedApplicationId || appId),\n fetchPolicy: \"cache-and-network\",\n });\n\n const allApplications: Application[] =\n optionsData?.applications?.edges?.map(\n (\n edge: GetAnalyticsDashboardOptionsQuery[\"applications\"][\"edges\"][number],\n ) => edge.node as Application,\n ) ?? [];\n // Scope the app picker to the active org when one is selected\n const applications = activeOrgId\n ? allApplications.filter((a) => a.developerOrganizationId === activeOrgId)\n : allApplications;\n const selectedApplication = data?.application;\n\n useEffect(() => {\n const name = selectedApplication?.name;\n const id = appId || selectedApplicationId;\n if (!id || !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(id, name);\n return () => reg?.unregister(id);\n }, [appId, selectedApplicationId, selectedApplication?.name]);\n\n const analytics = [...(data?.applicationAnalytics ?? [])].sort(\n (left, right) =>\n new Date(left.date).getTime() - new Date(right.date).getTime(),\n );\n\n const latest = analytics[analytics.length - 1];\n const previous = analytics[analytics.length - 2];\n const chartData = analytics.map((entry) => ({\n date: new Date(entry.date).toLocaleDateString(undefined, {\n month: \"short\",\n day: \"numeric\",\n }),\n apiCalls: entry.apiCalls,\n errors: entry.errors,\n activeUsers: entry.activeUsers,\n newUsers: entry.newUsers,\n sessions: entry.sessions,\n avgResponseTime: entry.avgResponseTime ?? 0,\n }));\n\n const errorRate =\n latest && latest.apiCalls > 0 ? (latest.errors / latest.apiCalls) * 100 : 0;\n const previousErrorRate =\n previous && previous.apiCalls > 0\n ? (previous.errors / previous.apiCalls) * 100\n : 0;\n const responseTrend =\n latest?.avgResponseTime && previous?.avgResponseTime\n ? previous.avgResponseTime - latest.avgResponseTime\n : undefined;\n\n const handleNavigate = (path: string) => {\n navigate?.(`${basePath}${path}`);\n };\n\n if (!appId && optionsLoading && !optionsData) {\n return (\n <PageLayout\n title={tr(\"devportal.analytics.title\", \"Analytics\")}\n description={tr(\n \"devportal.analytics.description\",\n \"Explore live application usage and performance metrics\",\n )}\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 (!appId && optionsError) {\n return (\n <PageLayout\n title={tr(\"devportal.analytics.title\", \"Analytics\")}\n description={tr(\n \"devportal.analytics.description\",\n \"Explore live application usage and performance metrics\",\n )}\n >\n <EmptyState\n icon={<AlertCircle size={32} className=\"text-status-error-text\" />}\n title={tr(\n \"devportal.analytics.errorLoadApps\",\n \"Unable to load applications\",\n )}\n description={getErrorMessage(optionsError)}\n />\n </PageLayout>\n );\n }\n\n if (!appId && !selectedApplicationId) {\n return (\n <PageLayout\n title={tr(\"devportal.analytics.title\", \"Analytics\")}\n description={tr(\n \"devportal.analytics.selectAppDescription\",\n \"Select an application to review live usage metrics from the public gateway\",\n )}\n >\n {applications.length > 0 ? (\n <div className=\"space-y-2\">\n {applications.map((application) => (\n <AppListItem\n key={application.id}\n app={application}\n onClick={() => setSelectedApplicationId(application.id)}\n />\n ))}\n </div>\n ) : (\n <EmptyState\n illustration=\"empty-data\"\n icon={<BarChart3 size={32} />}\n title={tr(\n \"devportal.analytics.noAppsTitle\",\n \"No applications available\",\n )}\n description={tr(\n \"devportal.analytics.noAppsDescription\",\n \"Create an application before viewing analytics.\",\n )}\n />\n )}\n </PageLayout>\n );\n }\n\n if (error) {\n return (\n <PageLayout\n title={tr(\"devportal.analytics.title\", \"Analytics\")}\n description={tr(\n \"devportal.analytics.description\",\n \"Explore live application usage and performance metrics\",\n )}\n backLink={\n selectedApplicationId ? `/apps/${selectedApplicationId}` : undefined\n }\n backLabel={tr(\n \"devportal.analytics.backToApplication\",\n \"Back to Application\",\n )}\n >\n <EmptyState\n icon={<AlertCircle size={32} className=\"text-status-error-text\" />}\n title={tr(\n \"devportal.analytics.errorLoadAnalytics\",\n \"Unable to load analytics\",\n )}\n description={getErrorMessage(error)}\n />\n </PageLayout>\n );\n }\n\n return (\n <PageLayout\n title={\n appId\n ? tr(\"devportal.analytics.appTitle\", \"Application Analytics\")\n : tr(\"devportal.analytics.title\", \"Analytics\")\n }\n description={\n selectedApplication\n ? `${tr(\"devportal.analytics.liveUsageFor\", \"Live usage and performance for\")} ${selectedApplication.name}`\n : tr(\n \"devportal.analytics.description\",\n \"Explore live application usage and performance metrics\",\n )\n }\n purpose={tr(\n \"devportal.analytics.purpose\",\n \"Analytics shows how your apps are actually used — API call volume, active installs, latency, and error rates over time — so you can spot regressions, plan capacity, and prove value to your users. Pick an app and a time range to drill in.\",\n )}\n backLink={\n appId && selectedApplication\n ? `/apps/${selectedApplication.id}`\n : undefined\n }\n backLabel={tr(\n \"devportal.analytics.backToApplication\",\n \"Back to Application\",\n )}\n actions={\n <div className=\"flex flex-wrap gap-2\">\n {!appId && applications.length > 0 && (\n <select\n value={selectedApplicationId}\n onChange={(event) => setSelectedApplicationId(event.target.value)}\n aria-label={tr(\n \"devportal.analytics.selectAppAria\",\n \"Select application\",\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 {applications.map((application) => (\n <option key={application.id} value={application.id}>\n {application.name}\n </option>\n ))}\n </select>\n )}\n <select\n value={timeRange}\n onChange={(event) => setTimeRange(event.target.value as TimeRange)}\n aria-label={tr(\n \"devportal.analytics.selectRangeAria\",\n \"Select time range\",\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=\"7d\">\n {tr(\"devportal.analytics.range.7d\", \"Last 7 days\")}\n </option>\n <option value=\"30d\">\n {tr(\"devportal.analytics.range.30d\", \"Last 30 days\")}\n </option>\n <option value=\"90d\">\n {tr(\"devportal.analytics.range.90d\", \"Last 90 days\")}\n </option>\n </select>\n </div>\n }\n >\n {loading && !analytics.length ? (\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 ) : analytics.length > 0 ? (\n <>\n <EmphasisPanel className=\"mb-6\">\n <StatCardGrid columns={4}>\n <StatCard\n title={tr(\"devportal.analytics.apiCalls\", \"API Calls\")}\n value={(latest?.apiCalls ?? 0).toLocaleString()}\n icon={<Activity size={20} />}\n />\n <StatCard\n title={tr(\"devportal.analytics.activeUsers\", \"Active Users\")}\n value={(latest?.activeUsers ?? 0).toLocaleString()}\n icon={<Users size={20} />}\n />\n <StatCard\n title={tr(\"devportal.analytics.errorRate\", \"Error Rate\")}\n value={`${errorRate.toFixed(2)}%`}\n icon={<AlertTriangle size={20} />}\n trend={\n previous\n ? {\n value: previousErrorRate - errorRate,\n label: tr(\n \"devportal.analytics.vsPrevious\",\n \"vs previous period\",\n ),\n }\n : undefined\n }\n />\n <StatCard\n title={tr(\n \"devportal.analytics.avgResponseTime\",\n \"Avg Response Time\",\n )}\n value={\n latest?.avgResponseTime\n ? `${latest.avgResponseTime}ms`\n : tr(\"devportal.common.na\", \"N/A\")\n }\n icon={<Clock size={20} />}\n trend={\n typeof responseTrend === \"number\"\n ? {\n value: responseTrend,\n label: tr(\n \"devportal.analytics.fasterThanPrevious\",\n \"faster than previous\",\n ),\n }\n : undefined\n }\n />\n </StatCardGrid>\n </EmphasisPanel>\n\n <div className=\"grid gap-6 xl:grid-cols-2\">\n <PageSection\n title={tr(\"devportal.analytics.trafficTrend\", \"Traffic trend\")}\n >\n <GlassCard treatment=\"insight\" className=\"p-4\">\n <ResponsiveContainer width=\"100%\" height={300}>\n <LineChart data={chartData}>\n <CartesianGrid\n strokeDasharray=\"3 3\"\n stroke=\"var(--color-chart-grid)\"\n />\n <XAxis\n dataKey=\"date\"\n stroke=\"var(--color-chart-axis)\"\n fontSize={12}\n />\n <YAxis stroke=\"var(--color-chart-axis)\" fontSize={12} />\n <Tooltip\n contentStyle={{\n backgroundColor: \"var(--color-card)\",\n border: \"1px solid var(--color-border-default)\",\n borderRadius: 12,\n }}\n />\n <Line\n type=\"monotone\"\n dataKey=\"apiCalls\"\n stroke={chartColors.primary}\n strokeWidth={2}\n dot={false}\n />\n <Line\n type=\"monotone\"\n dataKey=\"errors\"\n stroke={chartColors.negative}\n strokeWidth={2}\n dot={false}\n />\n </LineChart>\n </ResponsiveContainer>\n </GlassCard>\n </PageSection>\n\n <PageSection\n title={tr(\"devportal.analytics.userActivity\", \"User activity\")}\n >\n <GlassCard treatment=\"insight\" className=\"p-4\">\n <ResponsiveContainer width=\"100%\" height={300}>\n <BarChart data={chartData}>\n <CartesianGrid\n strokeDasharray=\"3 3\"\n stroke=\"var(--color-chart-grid)\"\n />\n <XAxis\n dataKey=\"date\"\n stroke=\"var(--color-chart-axis)\"\n fontSize={12}\n />\n <YAxis stroke=\"var(--color-chart-axis)\" fontSize={12} />\n <Tooltip\n contentStyle={{\n backgroundColor: \"var(--color-card)\",\n border: \"1px solid var(--color-border-default)\",\n borderRadius: 12,\n }}\n />\n <Bar\n dataKey=\"activeUsers\"\n fill={chartColors.primary}\n radius={[6, 6, 0, 0]}\n />\n <Bar\n dataKey=\"newUsers\"\n fill={chartColors.secondary}\n radius={[6, 6, 0, 0]}\n />\n </BarChart>\n </ResponsiveContainer>\n </GlassCard>\n </PageSection>\n </div>\n\n <PageSection\n title={tr(\"devportal.analytics.dailyBreakdown\", \"Daily breakdown\")}\n >\n <DataTable\n data={analytics}\n keyField=\"id\"\n columns={[\n {\n key: \"date\",\n header: tr(\"devportal.analytics.date\", \"Date\"),\n render: (entry) =>\n new Date(entry.date).toLocaleDateString(undefined, {\n month: \"short\",\n day: \"numeric\",\n year: \"numeric\",\n }),\n },\n {\n key: \"apiCalls\",\n header: tr(\"devportal.analytics.apiCalls\", \"API Calls\"),\n render: (entry) => entry.apiCalls.toLocaleString(),\n },\n {\n key: \"errors\",\n header: tr(\"devportal.analytics.errors\", \"Errors\"),\n render: (entry) => entry.errors.toLocaleString(),\n },\n {\n key: \"activeUsers\",\n header: tr(\"devportal.analytics.activeUsers\", \"Active Users\"),\n render: (entry) => entry.activeUsers.toLocaleString(),\n },\n {\n key: \"newUsers\",\n header: tr(\"devportal.analytics.newUsers\", \"New Users\"),\n render: (entry) => entry.newUsers.toLocaleString(),\n },\n {\n key: \"avgResponseTime\",\n header: tr(\"devportal.analytics.avgResponse\", \"Avg Response\"),\n render: (entry) =>\n entry.avgResponseTime\n ? `${entry.avgResponseTime}ms`\n : tr(\"devportal.common.na\", \"N/A\"),\n },\n {\n key: \"sessions\",\n header: tr(\"devportal.analytics.sessions\", \"Sessions\"),\n render: (entry) => entry.sessions.toLocaleString(),\n },\n ]}\n />\n </PageSection>\n </>\n ) : (\n <EmptyState\n illustration=\"empty-data\"\n icon={<BarChart3 size={32} />}\n title={tr(\n \"devportal.analytics.noAnalyticsTitle\",\n \"No analytics available yet\",\n )}\n description={tr(\n \"devportal.analytics.noAnalyticsDescription\",\n \"Analytics will appear after this application starts receiving traffic through the public gateway.\",\n )}\n action={\n !appId ? (\n <button\n type=\"button\"\n onClick={() => handleNavigate(\"/apps\")}\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 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary\"\n >\n {tr(\n \"devportal.analytics.viewApplications\",\n \"View applications\",\n )}\n </button>\n ) : undefined\n }\n />\n )}\n </PageLayout>\n );\n};\n"],"mappings":"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"}
|