From 79f095fa3aad82569c5de215cec7d4420714fb23 Mon Sep 17 00:00:00 2001 From: Manish Kumar Date: Tue, 11 Aug 2026 23:00:12 -0500 Subject: [PATCH] feat(analytics): PR-D -- Usage Analytics v1 frontend dashboard Fourth and final sub-PR. Frontend for the /analytics/* API (#39 PR-C). - analytics.ts: data layer, mirroring interactions.ts's own shape -- typed fetch wrappers for all 7 GET endpoints plus exportAnalyticsCsv (fetched, not a bare , so Authorization can be attached; blob download). /analytics/performance's response type pins org_id/team_id to `null` and scope to 'platform' at the type level, matching the backend's own hard requirement that those numbers are never presented as tenant-scoped. - pages/AnalyticsDashboard.tsx: reuses the existing DashboardGrid/ MetricCard widget family (PR10) and recharts (already a dependency, already used by BillingPage.tsx/EcosystemPage.tsx -- same chart colors, no new visual system). Overview cards, a queries/active-users trend line chart, DAU/WAU/MAU, a service breakdown table, performance cards (explicitly labeled platform-wide), 7d/30d/90d date presets, CSV export, and an organization filter shown only to a platform_admin (an org_admin/team_admin's scope is already fixed server-side to their own org/team). Null values render as "--", never a fabricated number, same convention DashboardPage.tsx already established. - auth.ts: canSeeAnalytics(), a UX-only mirror of the backend's require_analytics_scope matrix (platform_admin/org_admin/team_admin allowed, regular member denied) built from the SessionUser fields Phase 1 PR3/Team Management v0.8.0 already put there (orgRoles, teamRole) -- no new claim, no new auth mechanism. - navigation.ts / AdminApp.tsx: wired in exactly like 'interactions' was -- one NAV entry (Business section, next to Billing) gated by canSeeAnalytics, one switch case rendering AnalyticsDashboard. Test plan: `npm test` -- 474 passed (38 files), no regressions. AnalyticsDashboard.test.tsx (10 tests): loads and renders live data, date-range re-fetch, API failure + retry, 403 permission-denied state, empty-data state, null-value "--" rendering, CSV export (success + failure), organization filter shown only for a platform_admin. `tsc -b` and `vite build --mode admin` both clean. --- frontend/cc-ui/src/analytics.ts | 174 +++++++++++ frontend/cc-ui/src/apps/AdminApp.tsx | 21 +- frontend/cc-ui/src/auth.ts | 17 ++ frontend/cc-ui/src/navigation.ts | 16 +- .../src/pages/AnalyticsDashboard.test.tsx | 206 +++++++++++++ .../cc-ui/src/pages/AnalyticsDashboard.tsx | 287 ++++++++++++++++++ frontend/cc-ui/tsconfig.app.tsbuildinfo | 2 +- 7 files changed, 719 insertions(+), 4 deletions(-) create mode 100644 frontend/cc-ui/src/analytics.ts create mode 100644 frontend/cc-ui/src/pages/AnalyticsDashboard.test.tsx create mode 100644 frontend/cc-ui/src/pages/AnalyticsDashboard.tsx diff --git a/frontend/cc-ui/src/analytics.ts b/frontend/cc-ui/src/analytics.ts new file mode 100644 index 0000000..621766e --- /dev/null +++ b/frontend/cc-ui/src/analytics.ts @@ -0,0 +1,174 @@ +// Usage Analytics v1 (PR-D). Data layer, mirroring interactions.ts's own +// shape exactly -- every call hits this service's own backend at a +// relative path (control_center.analytics.router, mounted at /analytics +// in main.py); no function here makes an authorization decision, that's +// entirely require_analytics_scope's job server-side (401/403 on a +// missing/insufficient token, re-checked on every single call below -- +// the org_id/team_id this file sends are a UX convenience, never trusted +// client-side). +import { authHeaders, reportUnauthorized } from './auth' + +async function apiFetch(path: string, init: RequestInit = {}): Promise { + const r = await fetch(path, { + ...init, + headers: { ...authHeaders(), ...(init.headers ?? {}) }, + }) + if (r.status === 401) { + reportUnauthorized() + } + return r +} + +export interface AnalyticsFilters { + fromDate?: string + toDate?: string + orgId?: number + teamId?: number +} + +function buildQuery(filters: AnalyticsFilters): URLSearchParams { + const qs = new URLSearchParams() + if (filters.fromDate) qs.set('from_date', filters.fromDate) + if (filters.toDate) qs.set('to_date', filters.toDate) + if (filters.orgId != null) qs.set('org_id', String(filters.orgId)) + if (filters.teamId != null) qs.set('team_id', String(filters.teamId)) + return qs +} + +// Mirrors control_center.analytics.service.get_overview's response +// exactly. `total_queries`/`active_users`/`workflows_run` are `null`, +// not fabricated, when the backend couldn't answer for this scope (a +// team-roster lookup failure, or a platform_admin asking about +// workflows -- see that module's own docstring for the full list) -- +// this page renders those the same "--" way DashboardPage.tsx already +// established for a null MetricCard value. +export interface AnalyticsOverview { + total_queries: number | null + active_users: number | null + workflows_run: number | null + error_rate: number + from_date: string + to_date: string + org_id: number | null + team_id: number | null + team_scope_available?: boolean +} + +export interface DailyCount { + date: string + count: number | null +} + +export interface AnalyticsUsers { + daily: DailyCount[] + dau: number | null + wau: number | null + mau: number | null + org_id: number | null + team_id: number | null + team_scope_available?: boolean +} + +export interface AnalyticsQueries { + daily: DailyCount[] + total_queries: number | null + from_date: string + to_date: string + org_id: number | null + team_id: number | null + team_scope_available?: boolean +} + +export interface ServiceBreakdownRow { + service: string + total_calls: number + errors: number + error_rate: number + avg_latency_ms: number | null +} + +export interface AnalyticsServices { + services: ServiceBreakdownRow[] + org_id: number | null + team_id: number | null + note?: string +} + +// PLATFORM-WIDE ONLY -- org_id/team_id are always null here regardless +// of the caller's own filter selection (audit:events, this response's +// real source, carries no organization_id at all -- see aggregator.py's +// own module docstring). AnalyticsDashboard.tsx must never attribute +// these numbers to whatever org/team filter happens to be selected. +export interface AnalyticsPerformance { + scope: 'platform' + org_id: null + team_id: null + p50_latency_ms: number | null + p95_latency_ms: number | null + p99_latency_ms: number | null + error_rate: number + throughput_per_day: number + latency_source: 'prometheus' | 'events' + from_date: string + to_date: string +} + +export interface AnalyticsWorkflows { + workflows_run: number | null + daily: DailyCount[] | null + success_rate: number | null + org_id: number | null + team_id: number | null + from_date: string + to_date: string + note?: string +} + +export interface AnalyticsUsage { + org_id: number | null + team_id: number | null + billing_available: boolean + usage: Record | null + limits: Record | null + note?: string +} + +async function getJson(path: string, filters: AnalyticsFilters): Promise { + const qs = buildQuery(filters) + const r = await apiFetch(`${path}?${qs.toString()}`) + if (!r.ok) throw new Error(`${path} ${r.status}`) + return r.json() +} + +export const fetchAnalyticsOverview = (filters: AnalyticsFilters = {}) => getJson('/analytics/overview', filters) +export const fetchAnalyticsQueries = (filters: AnalyticsFilters = {}) => getJson('/analytics/queries', filters) +export const fetchAnalyticsUsers = (filters: AnalyticsFilters = {}) => getJson('/analytics/users', filters) +export const fetchAnalyticsServices = (filters: AnalyticsFilters = {}) => getJson('/analytics/services', filters) +export const fetchAnalyticsWorkflows = (filters: AnalyticsFilters = {}) => getJson('/analytics/workflows', filters) +export const fetchAnalyticsPerformance = (filters: AnalyticsFilters = {}) => getJson('/analytics/performance', filters) +export const fetchAnalyticsUsage = (filters: AnalyticsFilters = {}) => getJson('/analytics/usage', filters) + +export type ExportType = 'overview' | 'queries' | 'users' | 'services' | 'workflows' | 'performance' + +// Triggers a browser download of the CSV the backend streams back +// (StreamingResponse, media_type text/csv) -- fetched (not a bare ) specifically so the Authorization header can be attached; a +// plain link can't carry it. Throws the same `${path} ${status}` shape +// every other function in this file does, including a 403 -- an +// unauthorized export attempt surfaces the same way an unauthorized +// dashboard load already does, not a silently-empty file. +export async function exportAnalyticsCsv(type: ExportType, filters: AnalyticsFilters = {}): Promise { + const qs = buildQuery(filters) + qs.set('type', type) + const r = await apiFetch(`/analytics/export?${qs.toString()}`) + if (!r.ok) throw new Error(`/analytics/export ${r.status}`) + const blob = await r.blob() + const url = URL.createObjectURL(blob) + const a = document.createElement('a') + a.href = url + a.download = `analytics_${type}.csv` + document.body.appendChild(a) + a.click() + a.remove() + URL.revokeObjectURL(url) +} diff --git a/frontend/cc-ui/src/apps/AdminApp.tsx b/frontend/cc-ui/src/apps/AdminApp.tsx index 6dc7f3e..6c62522 100644 --- a/frontend/cc-ui/src/apps/AdminApp.tsx +++ b/frontend/cc-ui/src/apps/AdminApp.tsx @@ -1,7 +1,7 @@ import { useState, useEffect, useCallback } from 'react' import { fetchSummary, fetchReportStatus, triggerGenerate } from '../api' import { - getSessionUser, hasAdminAccess, hasOrganizationsAccess, hasPlatformAdminAccess, + canSeeAnalytics as canSeeAnalyticsAccess, getSessionUser, hasAdminAccess, hasOrganizationsAccess, hasPlatformAdminAccess, UNAUTHORIZED_EVENT, } from '../auth' import { findNavItem } from '../navigation' @@ -33,6 +33,7 @@ import BillingPage from '../pages/billing/BillingPage' import AuditLogsPage from '../pages/audit/AuditLogsPage' import SessionsPage from '../pages/security/SessionsPage' import InteractionsPage from '../pages/InteractionsPage' +import AnalyticsDashboard from '../pages/AnalyticsDashboard' import SecurityDashboardPage from '../pages/security/SecurityDashboardPage' import OrganizationMFAPolicyPage from '../pages/security/OrganizationMFAPolicyPage' import AuthGate from './AuthGate' @@ -128,6 +129,12 @@ function AdminDashboard() { // PR-B5-B: same reasoning as canSeeAuditLogs -- GET /platform/ // interactions is manage_all_orgs-gated, not org-scoped. const canSeeInteractions = hasPlatformAdminAccess() + // Usage Analytics v1 (PR-D): narrower than canSeeOrganizations -- + // mirrors require_analytics_scope's own platform_admin/org_admin/ + // team_admin/deny matrix exactly, not the broader "any org member" + // gate 'billing'/'iam' use, since a regular org member is denied + // analytics access entirely by the backend. + const canSeeAnalytics = canSeeAnalyticsAccess() // PR11.5.6: same reasoning as canSeeAuditLogs -- GET /platform/users, // GET /platform/orgs, GET /platform/audit-events (everything the // Security Dashboard reads) are all manage_all_orgs-gated. @@ -325,7 +332,7 @@ function AdminDashboard() { ) : undefined} > {renderPage(active, { - canSeeOps, canSeeOrganizations, canSeeUsers, canSeeAuditLogs, canSeeInteractions, canSeeSecurityOverview, refreshKey, + canSeeOps, canSeeOrganizations, canSeeUsers, canSeeAuditLogs, canSeeInteractions, canSeeAnalytics, canSeeSecurityOverview, refreshKey, selectedOrgId, setSelectedOrgId, selectedUserId, setSelectedUserId, teamsOrgHint, rolesOrgHint, onViewTeams: handleViewTeams, onViewRoles: handleViewRoles, selectedSsoOrgId, setSelectedSsoOrgId, navigateToSsoSettings, @@ -344,6 +351,7 @@ interface RenderCtx { canSeeUsers: boolean canSeeAuditLogs: boolean canSeeInteractions: boolean + canSeeAnalytics: boolean canSeeSecurityOverview: boolean refreshKey: number selectedOrgId: number | null @@ -532,6 +540,15 @@ function renderPage(active: PageKey, ctx: RenderCtx) { ? ctx.setSelectedBillingOrgId(null)} /> : + // Usage Analytics v1 (PR-D): flat, no org-picker/deep-link -- unlike + // 'billing' immediately above, the page itself resolves/filters + // scope in-page (an in-page organization setOrgFilter(e.target.value ? Number(e.target.value) : undefined)} + > + + {orgOptions?.map(o => )} + + + )} + + + + {exportError && ( +
+ setExportError(null)} /> +
+ )} + + {loading && } + {!loading && error && } + + {!loading && !error && data && ( + <> + + + handleExport('services')} /> + + + )} + + )} + + ) +} + +function OverviewSection({ overview }: { overview: AnalyticsOverview }) { + return ( + + + + + + + ) +} + +function TrendChart({ data: chartData }: { data: { date: string; count: number | null }[] }) { + if (chartData.every(d => d.count == null)) { + return + } + return ( +
+ + + + + + + + + +
+ ) +} + +function UsersSection({ users }: { users: AnalyticsUsers }) { + return ( + + + + +
+ + {users.team_scope_available === false && ( +
+ Team-scoped data is temporarily unavailable (the team roster couldn't be resolved) -- showing no data rather than an incorrect number. +
+ )} + +
+
+
+ ) +} + +function ServicesSection({ services, onExport }: { services: AnalyticsServices; onExport: () => void }) { + return ( + + {services.services.length === 0 ? ( +
+ +
+ ) : ( +
+
+ +
+ r.service} + rows={services.services} + emptyLabel="No service activity" + columns={[ + { key: 'service', header: 'Service', render: r => r.service }, + { key: 'total_calls', header: 'Calls', render: r => r.total_calls }, + { key: 'errors', header: 'Errors', render: r => r.errors }, + { key: 'error_rate', header: 'Error Rate', render: r => formatPct(r.error_rate) }, + { key: 'avg_latency_ms', header: 'Avg Latency', render: r => r.avg_latency_ms != null ? `${r.avg_latency_ms.toFixed(0)} ms` : '—' }, + ]} + /> +
+ )} +
+ ) +} + +function PerformanceSection({ performance }: { performance: AnalyticsPerformance }) { + return ( + + + + + + + ) +} diff --git a/frontend/cc-ui/tsconfig.app.tsbuildinfo b/frontend/cc-ui/tsconfig.app.tsbuildinfo index 82518a1..0d969b4 100644 --- a/frontend/cc-ui/tsconfig.app.tsbuildinfo +++ b/frontend/cc-ui/tsconfig.app.tsbuildinfo @@ -1 +1 @@ -{"root":["./src/api.ts","./src/audit.ts","./src/auth.test.ts","./src/auth.ts","./src/billing.ts","./src/dashboard.ts","./src/format.ts","./src/integrations.ts","./src/interactions.ts","./src/main.tsx","./src/model_registry.ts","./src/navigation.test.ts","./src/navigation.ts","./src/organizations.ts","./src/platform_config.ts","./src/rag.ts","./src/roles.ts","./src/security.ts","./src/serviceAccounts.ts","./src/sessions.ts","./src/sso.ts","./src/teams.ts","./src/tes.ts","./src/test-setup.ts","./src/users.ts","./src/workflows.ts","./src/apps/AdminApp.test.tsx","./src/apps/AdminApp.tsx","./src/apps/AuthGate.tsx","./src/apps/ControlApp.test.tsx","./src/apps/ControlApp.tsx","./src/apps/UnknownModeNotice.test.tsx","./src/apps/UnknownModeNotice.tsx","./src/components/AccessDenied.test.tsx","./src/components/AccessDenied.tsx","./src/components/AdminLogo.tsx","./src/components/Header.tsx","./src/components/LoginScreen.test.tsx","./src/components/LoginScreen.tsx","./src/components/OAuthButtons.tsx","./src/components/StatusBadge.tsx","./src/components/dashboard/AlertCard.tsx","./src/components/dashboard/DashboardCard.tsx","./src/components/dashboard/DashboardGrid.tsx","./src/components/dashboard/HealthCard.tsx","./src/components/dashboard/MetricCard.tsx","./src/components/dashboard/StatusCard.tsx","./src/components/dashboard/TrendCard.tsx","./src/components/dashboard/dashboard-widgets.test.tsx","./src/components/dashboard/index.ts","./src/components/organizations/OrganizationStatusBadge.tsx","./src/components/organizations/OrganizationSummaryCard.tsx","./src/components/organizations/OrganizationTable.tsx","./src/components/organizations/SecuritySummaryCard.tsx","./src/components/roles/PermissionSelector.test.tsx","./src/components/roles/PermissionSelector.tsx","./src/components/roles/RoleAssignmentList.tsx","./src/components/roles/RoleBadge.tsx","./src/components/roles/RoleSelector.test.tsx","./src/components/roles/RoleSelector.tsx","./src/components/shell/AppShell.tsx","./src/components/shell/Breadcrumb.tsx","./src/components/shell/Footer.tsx","./src/components/shell/GlobalSearch.tsx","./src/components/shell/NotificationsMenu.tsx","./src/components/shell/OrgSelector.tsx","./src/components/shell/ProfileMenu.tsx","./src/components/shell/SidebarNav.tsx","./src/components/shell/TeamSwitcher.test.tsx","./src/components/shell/TeamSwitcher.tsx","./src/components/shell/ThemeToggle.tsx","./src/components/shell/TopAppBar.tsx","./src/components/shell/index.ts","./src/components/teams/TeamMembersPanel.test.tsx","./src/components/teams/TeamMembersPanel.tsx","./src/components/teams/TeamRow.test.tsx","./src/components/teams/TeamRow.tsx","./src/components/teams/TeamsCard.test.tsx","./src/components/teams/TeamsCard.tsx","./src/components/ui/ActionToolbar.tsx","./src/components/ui/BackLink.tsx","./src/components/ui/Button.tsx","./src/components/ui/Card.tsx","./src/components/ui/ComingSoon.tsx","./src/components/ui/DataTable.tsx","./src/components/ui/EmptyState.tsx","./src/components/ui/ErrorState.tsx","./src/components/ui/LoadingState.tsx","./src/components/ui/PageContainer.tsx","./src/components/ui/Pagination.tsx","./src/components/ui/SectionHeader.tsx","./src/components/ui/SessionExpiredState.tsx","./src/components/ui/StatCard.tsx","./src/components/ui/icon-type.ts","./src/components/ui/index.ts","./src/components/users/UserMFASecurityCard.test.tsx","./src/components/users/UserMFASecurityCard.tsx","./src/components/users/UserOrgMembershipList.tsx","./src/components/users/UserStatusAction.tsx","./src/pages/CloudPage.tsx","./src/pages/ConfigPage.tsx","./src/pages/DashboardPage.test.tsx","./src/pages/DashboardPage.tsx","./src/pages/DockerPage.tsx","./src/pages/EcosystemPage.tsx","./src/pages/HealthPage.tsx","./src/pages/IntegrationsPage.test.tsx","./src/pages/IntegrationsPage.tsx","./src/pages/InteractionsPage.test.tsx","./src/pages/InteractionsPage.tsx","./src/pages/LlmPage.tsx","./src/pages/OrganizationDetailPage.test.tsx","./src/pages/OrganizationDetailPage.tsx","./src/pages/OrganizationsPage.test.tsx","./src/pages/OrganizationsPage.tsx","./src/pages/PlatformSettingsPage.test.tsx","./src/pages/PlatformSettingsPage.tsx","./src/pages/UserDetailPage.test.tsx","./src/pages/UserDetailPage.tsx","./src/pages/UsersPage.test.tsx","./src/pages/UsersPage.tsx","./src/pages/audit/AuditLogsPage.test.tsx","./src/pages/audit/AuditLogsPage.tsx","./src/pages/billing/BillingPage.test.tsx","./src/pages/billing/BillingPage.tsx","./src/pages/billing/SubscriptionPage.test.tsx","./src/pages/billing/SubscriptionPage.tsx","./src/pages/identity/RolesPage.test.tsx","./src/pages/identity/RolesPage.tsx","./src/pages/identity/SSOSettingsPage.test.tsx","./src/pages/identity/SSOSettingsPage.tsx","./src/pages/identity/ServiceAccountsPage.test.tsx","./src/pages/identity/ServiceAccountsPage.tsx","./src/pages/identity/TeamsPage.test.tsx","./src/pages/identity/TeamsPage.tsx","./src/pages/operations/AIModelsPage.test.tsx","./src/pages/operations/AIModelsPage.tsx","./src/pages/operations/RAGPage.test.tsx","./src/pages/operations/RAGPage.tsx","./src/pages/operations/ToolExecutionPage.test.tsx","./src/pages/operations/ToolExecutionPage.tsx","./src/pages/operations/WorkflowsPage.test.tsx","./src/pages/operations/WorkflowsPage.tsx","./src/pages/security/OrganizationMFAPolicyPage.test.tsx","./src/pages/security/OrganizationMFAPolicyPage.tsx","./src/pages/security/SecurityDashboardPage.test.tsx","./src/pages/security/SecurityDashboardPage.tsx","./src/pages/security/SessionsPage.test.tsx","./src/pages/security/SessionsPage.tsx"],"version":"5.9.3"} \ No newline at end of file +{"root":["./src/analytics.ts","./src/api.ts","./src/audit.ts","./src/auth.test.ts","./src/auth.ts","./src/billing.ts","./src/dashboard.ts","./src/format.ts","./src/integrations.ts","./src/interactions.ts","./src/main.tsx","./src/model_registry.ts","./src/navigation.test.ts","./src/navigation.ts","./src/organizations.ts","./src/platform_config.ts","./src/rag.ts","./src/roles.ts","./src/security.ts","./src/serviceAccounts.ts","./src/sessions.ts","./src/sso.ts","./src/teams.ts","./src/tes.ts","./src/test-setup.ts","./src/users.ts","./src/workflows.ts","./src/apps/AdminApp.test.tsx","./src/apps/AdminApp.tsx","./src/apps/AuthGate.tsx","./src/apps/ControlApp.test.tsx","./src/apps/ControlApp.tsx","./src/apps/UnknownModeNotice.test.tsx","./src/apps/UnknownModeNotice.tsx","./src/components/AccessDenied.test.tsx","./src/components/AccessDenied.tsx","./src/components/AdminLogo.tsx","./src/components/Header.tsx","./src/components/LoginScreen.test.tsx","./src/components/LoginScreen.tsx","./src/components/OAuthButtons.tsx","./src/components/StatusBadge.tsx","./src/components/dashboard/AlertCard.tsx","./src/components/dashboard/DashboardCard.tsx","./src/components/dashboard/DashboardGrid.tsx","./src/components/dashboard/HealthCard.tsx","./src/components/dashboard/MetricCard.tsx","./src/components/dashboard/StatusCard.tsx","./src/components/dashboard/TrendCard.tsx","./src/components/dashboard/dashboard-widgets.test.tsx","./src/components/dashboard/index.ts","./src/components/organizations/OrganizationStatusBadge.tsx","./src/components/organizations/OrganizationSummaryCard.tsx","./src/components/organizations/OrganizationTable.tsx","./src/components/organizations/SecuritySummaryCard.tsx","./src/components/roles/PermissionSelector.test.tsx","./src/components/roles/PermissionSelector.tsx","./src/components/roles/RoleAssignmentList.tsx","./src/components/roles/RoleBadge.tsx","./src/components/roles/RoleSelector.test.tsx","./src/components/roles/RoleSelector.tsx","./src/components/shell/AppShell.tsx","./src/components/shell/Breadcrumb.tsx","./src/components/shell/Footer.tsx","./src/components/shell/GlobalSearch.tsx","./src/components/shell/NotificationsMenu.tsx","./src/components/shell/OrgSelector.tsx","./src/components/shell/ProfileMenu.tsx","./src/components/shell/SidebarNav.tsx","./src/components/shell/TeamSwitcher.test.tsx","./src/components/shell/TeamSwitcher.tsx","./src/components/shell/ThemeToggle.tsx","./src/components/shell/TopAppBar.tsx","./src/components/shell/index.ts","./src/components/teams/TeamMembersPanel.test.tsx","./src/components/teams/TeamMembersPanel.tsx","./src/components/teams/TeamRow.test.tsx","./src/components/teams/TeamRow.tsx","./src/components/teams/TeamsCard.test.tsx","./src/components/teams/TeamsCard.tsx","./src/components/ui/ActionToolbar.tsx","./src/components/ui/BackLink.tsx","./src/components/ui/Button.tsx","./src/components/ui/Card.tsx","./src/components/ui/ComingSoon.tsx","./src/components/ui/DataTable.tsx","./src/components/ui/EmptyState.tsx","./src/components/ui/ErrorState.tsx","./src/components/ui/LoadingState.tsx","./src/components/ui/PageContainer.tsx","./src/components/ui/Pagination.tsx","./src/components/ui/SectionHeader.tsx","./src/components/ui/SessionExpiredState.tsx","./src/components/ui/StatCard.tsx","./src/components/ui/icon-type.ts","./src/components/ui/index.ts","./src/components/users/UserMFASecurityCard.test.tsx","./src/components/users/UserMFASecurityCard.tsx","./src/components/users/UserOrgMembershipList.tsx","./src/components/users/UserStatusAction.tsx","./src/pages/AnalyticsDashboard.test.tsx","./src/pages/AnalyticsDashboard.tsx","./src/pages/CloudPage.tsx","./src/pages/ConfigPage.tsx","./src/pages/DashboardPage.test.tsx","./src/pages/DashboardPage.tsx","./src/pages/DockerPage.tsx","./src/pages/EcosystemPage.tsx","./src/pages/HealthPage.tsx","./src/pages/IntegrationsPage.test.tsx","./src/pages/IntegrationsPage.tsx","./src/pages/InteractionsPage.test.tsx","./src/pages/InteractionsPage.tsx","./src/pages/LlmPage.tsx","./src/pages/OrganizationDetailPage.test.tsx","./src/pages/OrganizationDetailPage.tsx","./src/pages/OrganizationsPage.test.tsx","./src/pages/OrganizationsPage.tsx","./src/pages/PlatformSettingsPage.test.tsx","./src/pages/PlatformSettingsPage.tsx","./src/pages/UserDetailPage.test.tsx","./src/pages/UserDetailPage.tsx","./src/pages/UsersPage.test.tsx","./src/pages/UsersPage.tsx","./src/pages/audit/AuditLogsPage.test.tsx","./src/pages/audit/AuditLogsPage.tsx","./src/pages/billing/BillingPage.test.tsx","./src/pages/billing/BillingPage.tsx","./src/pages/billing/SubscriptionPage.test.tsx","./src/pages/billing/SubscriptionPage.tsx","./src/pages/identity/RolesPage.test.tsx","./src/pages/identity/RolesPage.tsx","./src/pages/identity/SSOSettingsPage.test.tsx","./src/pages/identity/SSOSettingsPage.tsx","./src/pages/identity/ServiceAccountsPage.test.tsx","./src/pages/identity/ServiceAccountsPage.tsx","./src/pages/identity/TeamsPage.test.tsx","./src/pages/identity/TeamsPage.tsx","./src/pages/operations/AIModelsPage.test.tsx","./src/pages/operations/AIModelsPage.tsx","./src/pages/operations/RAGPage.test.tsx","./src/pages/operations/RAGPage.tsx","./src/pages/operations/ToolExecutionPage.test.tsx","./src/pages/operations/ToolExecutionPage.tsx","./src/pages/operations/WorkflowsPage.test.tsx","./src/pages/operations/WorkflowsPage.tsx","./src/pages/security/OrganizationMFAPolicyPage.test.tsx","./src/pages/security/OrganizationMFAPolicyPage.tsx","./src/pages/security/SecurityDashboardPage.test.tsx","./src/pages/security/SecurityDashboardPage.tsx","./src/pages/security/SessionsPage.test.tsx","./src/pages/security/SessionsPage.tsx"],"version":"5.9.3"} \ No newline at end of file