@burdenoff/microfe-adaptercloud 2026.628.1 → 2026.629.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/adaptercloud/context/AdapterCloudContext.js.map +1 -1
- package/dist/adaptercloud/pages/OverviewPage.js +1 -1
- package/dist/adaptercloud/pages/OverviewPage.js.map +1 -1
- package/dist/adaptercloud/pages/adapters/AdapterDetailPage.js.map +1 -1
- package/dist/adaptercloud/pages/adapters/AdapterFormPage.js.map +1 -1
- package/dist/adaptercloud/pages/connections/ConnectionDetailPage.js.map +1 -1
- package/dist/adaptercloud/pages/costs/CostsPage.js.map +1 -1
- package/dist/adaptercloud/pages/policies/PoliciesListPage.js.map +1 -1
- package/dist/adaptercloud/pages/policies/PolicyDetailPage.js.map +1 -1
- package/dist/adaptercloud/pages/policies/PolicyFormPage.js.map +1 -1
- package/dist/adaptercloud/pages/violations/ViolationsListPage.js.map +1 -1
- package/package.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AdapterCloudContext.js","names":[],"sources":["../../../src/adaptercloud/context/AdapterCloudContext.tsx"],"sourcesContent":["/**\n * AdapterCloud Context Provider\n * @module adaptercloud/context\n *\n * Provides the host-passed integration props (basePath / currentUser /\n * apiGatewayUrl / tenantId / workspaceId / authToken) plus navigation helpers to\n * the AdapterCloud pages. Standalone mode wraps a MemoryRouter; integrated mode\n * uses the shell-provided `navigate`.\n */\n\nimport type { FC, PropsWithChildren } from 'react';\nimport { createContext, useContext, useMemo, useCallback } from 'react';\nimport { MemoryRouter, useNavigate as useRouterNavigate } from 'react-router-dom';\nimport type { AdapterCloudRootProps } from '../types';\nimport { joinPath } from '../utils/navigation';\n\n// ============================================================================\n// Context\n// ============================================================================\n\ninterface AdapterCloudContextValue
|
|
1
|
+
{"version":3,"file":"AdapterCloudContext.js","names":[],"sources":["../../../src/adaptercloud/context/AdapterCloudContext.tsx"],"sourcesContent":["/**\n * AdapterCloud Context Provider\n * @module adaptercloud/context\n *\n * Provides the host-passed integration props (basePath / currentUser /\n * apiGatewayUrl / tenantId / workspaceId / authToken) plus navigation helpers to\n * the AdapterCloud pages. Standalone mode wraps a MemoryRouter; integrated mode\n * uses the shell-provided `navigate`.\n */\n\nimport type { FC, PropsWithChildren } from 'react';\nimport { createContext, useContext, useMemo, useCallback } from 'react';\nimport { MemoryRouter, useNavigate as useRouterNavigate } from 'react-router-dom';\nimport type { AdapterCloudRootProps } from '../types';\nimport { joinPath } from '../utils/navigation';\n\n// ============================================================================\n// Context\n// ============================================================================\n\ninterface AdapterCloudContextValue extends Omit<\n AdapterCloudRootProps,\n 'registerNavItems' | 'registerFooterItems' | 'navigate'\n> {\n isStandalone: boolean;\n /** Shell's navigate function (only available in integrated mode) */\n navigate?: (path: string) => void;\n /** Navigate to a path relative to basePath */\n navigateTo: (relativePath: string) => void;\n /** Get the full path for a relative path */\n getPath: (relativePath: string) => string;\n}\n\nconst AdapterCloudContext = createContext<AdapterCloudContextValue | null>(null);\n\n// ============================================================================\n// Hook\n// ============================================================================\n\nexport function useAdapterCloud(): AdapterCloudContextValue {\n const context = useContext(AdapterCloudContext);\n if (!context) {\n throw new Error('useAdapterCloud must be used within a AdapterCloudProvider');\n }\n return context;\n}\n\n// ============================================================================\n// Provider Component\n// ============================================================================\n\ntype ProviderProps = Omit<AdapterCloudRootProps, 'registerNavItems' | 'registerFooterItems'> & {\n isStandalone: boolean;\n};\n\n/** Provider for standalone mode (has access to MemoryRouter) */\nconst StandaloneProviderInner: FC<PropsWithChildren<ProviderProps>> = ({\n children,\n basePath,\n currentUser,\n tenantId,\n workspaceId,\n apiGatewayUrl,\n authToken,\n}) => {\n const routerNavigate = useRouterNavigate();\n const effectiveBasePath = basePath || '/';\n\n const getPath = useCallback(\n (relativePath: string) => joinPath(effectiveBasePath, relativePath),\n [effectiveBasePath]\n );\n\n const navigateTo = useCallback(\n (relativePath: string) => {\n const fullPath = joinPath(effectiveBasePath, relativePath);\n routerNavigate(fullPath);\n },\n [effectiveBasePath, routerNavigate]\n );\n\n const contextValue = useMemo<AdapterCloudContextValue>(\n () => ({\n basePath: effectiveBasePath,\n navigate: undefined,\n currentUser,\n tenantId,\n workspaceId,\n apiGatewayUrl,\n authToken,\n isStandalone: true,\n navigateTo,\n getPath,\n }),\n [\n effectiveBasePath,\n currentUser,\n tenantId,\n workspaceId,\n apiGatewayUrl,\n authToken,\n navigateTo,\n getPath,\n ]\n );\n\n return (\n <AdapterCloudContext.Provider value={contextValue}>{children}</AdapterCloudContext.Provider>\n );\n};\n\n/** Provider for integrated mode (shell provides navigate) */\nconst IntegratedProviderInner: FC<\n PropsWithChildren<ProviderProps & { navigate: (path: string) => void }>\n> = ({\n children,\n basePath,\n navigate,\n currentUser,\n tenantId,\n workspaceId,\n apiGatewayUrl,\n authToken,\n}) => {\n const effectiveBasePath = basePath || '/';\n\n const getPath = useCallback(\n (relativePath: string) => joinPath(effectiveBasePath, relativePath),\n [effectiveBasePath]\n );\n\n const navigateTo = useCallback(\n (relativePath: string) => {\n const fullPath = joinPath(effectiveBasePath, relativePath);\n navigate(fullPath);\n },\n [effectiveBasePath, navigate]\n );\n\n const contextValue = useMemo<AdapterCloudContextValue>(\n () => ({\n basePath: effectiveBasePath,\n navigate,\n currentUser,\n tenantId,\n workspaceId,\n apiGatewayUrl,\n authToken,\n isStandalone: false,\n navigateTo,\n getPath,\n }),\n [\n effectiveBasePath,\n navigate,\n currentUser,\n tenantId,\n workspaceId,\n apiGatewayUrl,\n authToken,\n navigateTo,\n getPath,\n ]\n );\n\n return (\n <AdapterCloudContext.Provider value={contextValue}>{children}</AdapterCloudContext.Provider>\n );\n};\n\nexport const AdapterCloudProvider: FC<PropsWithChildren<AdapterCloudRootProps>> = (props) => {\n const { children, navigate } = props;\n // Standalone mode: no navigate function provided (MFE manages its own router)\n // Integrated mode: navigate function provided by shell (shell provides router)\n const isStandalone = !navigate;\n\n // Standalone mode: wrap in MemoryRouter\n if (isStandalone) {\n return (\n <MemoryRouter>\n <StandaloneProviderInner {...props} isStandalone={true}>\n {children}\n </StandaloneProviderInner>\n </MemoryRouter>\n );\n }\n\n // Integrated mode: shell provides router\n return (\n <IntegratedProviderInner {...props} navigate={navigate} isStandalone={false}>\n {children}\n </IntegratedProviderInner>\n );\n};\n"],"mappings":";;;;;AAiCA,IAAM,IAAsB,EAA+C,KAAK;AAMhF,SAAgB,IAA4C;CAC1D,IAAM,IAAU,EAAW,EAAoB;AAC/C,KAAI,CAAC,EACH,OAAU,MAAM,6DAA6D;AAE/E,QAAO;;AAYT,IAAM,KAAiE,EACrE,aACA,aACA,gBACA,aACA,gBACA,kBACA,mBACI;CACJ,IAAM,IAAiB,GAAmB,EACpC,IAAoB,KAAY,KAEhC,IAAU,GACb,MAAyB,EAAS,GAAmB,EAAa,EACnE,CAAC,EAAkB,CACpB,EAEK,IAAa,GAChB,MAAyB;AAExB,IADiB,EAAS,GAAmB,EAAa,CAClC;IAE1B,CAAC,GAAmB,EAAe,CACpC,EAEK,IAAe,SACZ;EACL,UAAU;EACV,UAAU,KAAA;EACV;EACA;EACA;EACA;EACA;EACA,cAAc;EACd;EACA;EACD,GACD;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACD,CACF;AAED,QACE,kBAAC,EAAoB,UAArB;EAA8B,OAAO;EAAe;EAAwC,CAAA;GAK1F,KAED,EACH,aACA,aACA,aACA,gBACA,aACA,gBACA,kBACA,mBACI;CACJ,IAAM,IAAoB,KAAY,KAEhC,IAAU,GACb,MAAyB,EAAS,GAAmB,EAAa,EACnE,CAAC,EAAkB,CACpB,EAEK,IAAa,GAChB,MAAyB;AAExB,IADiB,EAAS,GAAmB,EAAa,CACxC;IAEpB,CAAC,GAAmB,EAAS,CAC9B,EAEK,IAAe,SACZ;EACL,UAAU;EACV;EACA;EACA;EACA;EACA;EACA;EACA,cAAc;EACd;EACA;EACD,GACD;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACD,CACF;AAED,QACE,kBAAC,EAAoB,UAArB;EAA8B,OAAO;EAAe;EAAwC,CAAA;GAInF,KAAsE,MAAU;CAC3F,IAAM,EAAE,aAAU,gBAAa;AAiB/B,QAdsB,IAepB,kBAAC,GAAD;EAAyB,GAAI;EAAiB;EAAU,cAAc;EACnE;EACuB,CAAA,GAZxB,kBAAC,GAAD,EAAA,UACE,kBAAC,GAAD;EAAyB,GAAI;EAAO,cAAc;EAC/C;EACuB,CAAA,EACb,CAAA"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"OverviewPage.js","names":[],"sources":["../../../src/adaptercloud/pages/OverviewPage.tsx"],"sourcesContent":["/**\n * Overview / Dashboard Page\n * @module adaptercloud/pages\n *\n * The AdapterCloud landing page: a single-pane view of the estate — fabric\n * counts (adapters, connections, resources), governance posture (open\n * violations, unremediated drift), operations (recent sync runs) and FinOps\n * (workspace cost summary by service). All data is live from the deployed\n * `wspace-adaptercloud-svc` subgraph.\n */\n\nimport { useMemo, useCallback } from 'react';\nimport { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';\nimport {\n Boxes,\n Cable,\n Server,\n ShieldAlert,\n GitCompareArrows,\n RefreshCw,\n DollarSign,\n ArrowRight,\n Activity,\n AlertCircle,\n Plug,\n} from 'lucide-react';\nimport { EmphasisPanel, PagePurpose, GlassCard } from '@burdenoff/fe-libs/ui';\nimport { PageLayout } from '../components/PageLayout';\nimport { MetricsCard } from '../components/MetricsCard';\nimport { StatusBadge } from '../components/StatusBadge';\nimport { SeverityBadge } from '../components/SeverityBadge';\nimport { DataState } from '../components/DataState';\nimport { useAdapterCloud } from '../context/AdapterCloudContext';\nimport {\n useAdapters,\n useConnections,\n useResources,\n useViolations,\n useDriftRecords,\n useSyncRuns,\n useCostSummary,\n} from '../hooks/useAdapterCloudApi';\nimport { formatRelativeTime } from '../utils/formatters';\nimport { formatMoney, severityRank, syncKindLabels } from '../utils/domain';\nimport type { AdaSeverity } from '../types';\n\nexport function OverviewPage() {\n const { navigateTo, currentUser } = useAdapterCloud();\n const { t: translate } = useI18n();\n const t = useCallback(\n (key: string, fallback: string): string => {\n const value = translate(key);\n return value === key ? fallback : value;\n },\n [translate]\n );\n\n const adapters = useAdapters({ pagination: { limit: 1 } });\n const connections = useConnections({ pagination: { limit: 100 } });\n const resources = useResources({ pagination: { limit: 1 } });\n const openViolations = useViolations({ status: 'OPEN', pagination: { limit: 50 } });\n const drift = useDriftRecords({ status: 'DETECTED' });\n const syncRuns = useSyncRuns({ pagination: { limit: 5 } });\n const costSummary = useCostSummary();\n\n const firstName = currentUser?.firstName ?? currentUser?.name ?? 'there';\n\n // Connection health breakdown\n const connectionHealth = useMemo(() => {\n const items = connections.data?.items ?? [];\n const counts = { CONNECTED: 0, DEGRADED: 0, ERROR: 0, PENDING: 0, DISABLED: 0 };\n for (const c of items) counts[c.status] += 1;\n return counts;\n }, [connections.data]);\n\n const topViolations = useMemo(() => {\n return [...(openViolations.data?.items ?? [])]\n .sort((a, b) => severityRank[b.severity] - severityRank[a.severity])\n .slice(0, 5);\n }, [openViolations.data]);\n\n const recentRuns = syncRuns.data?.items ?? [];\n\n const topCostLines = useMemo(() => {\n return [...(costSummary.data?.lines ?? [])].sort((a, b) => b.total - a.total).slice(0, 6);\n }, [costSummary.data]);\n\n const isLoading =\n adapters.isLoading ||\n connections.isLoading ||\n resources.isLoading ||\n openViolations.isLoading ||\n drift.isLoading ||\n costSummary.isLoading;\n\n const isError =\n adapters.isError ||\n connections.isError ||\n resources.isError ||\n openViolations.isError ||\n drift.isError ||\n costSummary.isError;\n\n const refetchAll = useCallback(() => {\n void adapters.refetch();\n void connections.refetch();\n void resources.refetch();\n void openViolations.refetch();\n void drift.refetch();\n void syncRuns.refetch();\n void costSummary.refetch();\n }, [adapters, connections, resources, openViolations, drift, syncRuns, costSummary]);\n\n const stats = [\n {\n icon: <Boxes className=\"h-5 w-5\" />,\n value: adapters.data?.items.length\n ? String(adapters.data.items.length)\n : adapters.data\n ? '0'\n : '—',\n label: t('adaptercloud.overview.adapters', 'Adapters'),\n onClick: () => navigateTo('/adapters'),\n },\n {\n icon: <Cable className=\"h-5 w-5\" />,\n value: connections.data ? String(connections.data.items.length) : '—',\n label: t('adaptercloud.overview.connections', 'Connections'),\n onClick: () => navigateTo('/connections'),\n },\n {\n icon: <Server className=\"h-5 w-5\" />,\n value: resources.data ? String(resources.data.items.length) : '—',\n label: t('adaptercloud.overview.resources', 'Resources'),\n onClick: () => navigateTo('/resources'),\n },\n {\n icon: <ShieldAlert className=\"h-5 w-5\" />,\n value: openViolations.data ? String(openViolations.data.items.length) : '—',\n label: t('adaptercloud.overview.openViolations', 'Open Violations'),\n onClick: () => navigateTo('/violations'),\n },\n ];\n\n return (\n <PageLayout\n title={`${t('adaptercloud.overview.hello', 'Welcome back')}, ${firstName}`}\n description={t(\n 'adaptercloud.overview.subtitle',\n 'Your infrastructure continuum at a glance — fabric, governance, operations and cost.'\n )}\n actions={\n <button\n type=\"button\"\n onClick={refetchAll}\n className=\"inline-flex items-center gap-2 rounded-lg border border-border-subtle bg-bg-surface px-3 py-2 text-sm font-medium text-text-primary transition-colors hover:bg-accent\"\n >\n <RefreshCw className=\"h-4 w-4\" />\n {t('adaptercloud.common.refresh', 'Refresh')}\n </button>\n }\n >\n <PagePurpose className=\"mb-6\">\n {t(\n 'adaptercloud.overview.purpose',\n 'AdapterCloud connects your fragmented infrastructure — multi-cloud, on-prem, edge and SaaS — into one governable fabric. This overview is your control plane: see what you run, how connected it is, what is out of policy, and what it costs.'\n )}\n </PagePurpose>\n\n <DataState isLoading={isLoading} isError={isError} error={adapters.error} onRetry={refetchAll}>\n {/* Top metrics — the one emphasis zone */}\n <EmphasisPanel className=\"mb-8\">\n <h2 className=\"mb-4 text-lg font-semibold text-text-primary\">\n {t('adaptercloud.overview.estate', 'Estate Summary')}\n </h2>\n <div className=\"grid grid-cols-2 gap-4 lg:grid-cols-4\">\n {stats.map((stat) => (\n <button key={stat.label} type=\"button\" onClick={stat.onClick} className=\"text-left\">\n <MetricsCard icon={stat.icon} value={stat.value} label={stat.label} />\n </button>\n ))}\n </div>\n </EmphasisPanel>\n\n {/* Connection health + Cost summary */}\n <div className=\"mb-8 grid grid-cols-1 gap-6 lg:grid-cols-2\">\n {/* Connection health */}\n <section>\n <div className=\"mb-4 flex items-center justify-between\">\n <h2 className=\"text-lg font-semibold text-text-primary\">\n {t('adaptercloud.overview.connectionHealth', 'Connection Health')}\n </h2>\n <button\n type=\"button\"\n onClick={() => navigateTo('/connections')}\n className=\"flex items-center gap-1 text-sm font-medium text-primary hover:underline\"\n >\n {t('adaptercloud.common.viewAll', 'View all')}{' '}\n <ArrowRight className=\"h-3.5 w-3.5\" />\n </button>\n </div>\n <GlassCard className=\"p-5\">\n {connections.data && connections.data.items.length > 0 ? (\n <ul className=\"space-y-3\">\n {(\n [\n ['CONNECTED', connectionHealth.CONNECTED],\n ['DEGRADED', connectionHealth.DEGRADED],\n ['ERROR', connectionHealth.ERROR],\n ['PENDING', connectionHealth.PENDING],\n ['DISABLED', connectionHealth.DISABLED],\n ] as const\n ).map(([status, count]) => (\n <li key={status} className=\"flex items-center justify-between\">\n <StatusBadge status={status} />\n <span className=\"text-sm font-semibold tabular-nums text-text-primary\">\n {count}\n </span>\n </li>\n ))}\n </ul>\n ) : (\n <div className=\"flex flex-col items-center gap-3 py-6 text-center\">\n <Plug className=\"h-8 w-8 text-text-secondary\" />\n <p className=\"text-sm text-text-secondary\">\n {t(\n 'adaptercloud.overview.noConnections',\n 'No connections yet. Connect a target to start discovering resources.'\n )}\n </p>\n <button\n type=\"button\"\n onClick={() => navigateTo('/connections/new')}\n className=\"rounded-lg bg-action-primary-bg px-4 py-2 text-sm font-medium text-action-primary-text transition-colors hover:opacity-90\"\n >\n {t('adaptercloud.overview.addConnection', 'Add connection')}\n </button>\n </div>\n )}\n </GlassCard>\n </section>\n\n {/* Cost summary */}\n <section>\n <div className=\"mb-4 flex items-center justify-between\">\n <h2 className=\"text-lg font-semibold text-text-primary\">\n {t('adaptercloud.overview.costByService', 'Cost by Service')}\n </h2>\n <button\n type=\"button\"\n onClick={() => navigateTo('/costs')}\n className=\"flex items-center gap-1 text-sm font-medium text-primary hover:underline\"\n >\n {t('adaptercloud.overview.finops', 'FinOps')} <ArrowRight className=\"h-3.5 w-3.5\" />\n </button>\n </div>\n <GlassCard className=\"p-5\">\n <div className=\"mb-4 flex items-baseline gap-2\">\n <DollarSign className=\"h-5 w-5 text-text-secondary\" />\n <span className=\"text-2xl font-bold tabular-nums text-text-primary\">\n {costSummary.data\n ? formatMoney(costSummary.data.total, costSummary.data.currency)\n : '—'}\n </span>\n <span className=\"text-sm text-text-secondary\">\n {t('adaptercloud.overview.totalSpend', 'total spend')}\n </span>\n </div>\n {topCostLines.length > 0 ? (\n <ul className=\"space-y-2\">\n {topCostLines.map((line) => (\n <li key={line.service} className=\"flex items-center justify-between text-sm\">\n <span className=\"truncate text-text-primary\">{line.service}</span>\n <span className=\"tabular-nums text-text-secondary\">\n {formatMoney(line.total, costSummary.data?.currency ?? 'USD')}\n </span>\n </li>\n ))}\n </ul>\n ) : (\n <p className=\"py-4 text-center text-sm text-text-secondary\">\n {t(\n 'adaptercloud.overview.noCost',\n 'No cost records ingested yet. Run a cost-ingest sync to populate FinOps.'\n )}\n </p>\n )}\n </GlassCard>\n </section>\n </div>\n\n {/* Governance + Operations */}\n <div className=\"grid grid-cols-1 gap-6 lg:grid-cols-2\">\n {/* Top open violations */}\n <section>\n <div className=\"mb-4 flex items-center justify-between\">\n <h2 className=\"text-lg font-semibold text-text-primary\">\n {t('adaptercloud.overview.priorityViolations', 'Priority Violations')}\n </h2>\n <button\n type=\"button\"\n onClick={() => navigateTo('/violations')}\n className=\"flex items-center gap-1 text-sm font-medium text-primary hover:underline\"\n >\n {t('adaptercloud.overview.queue', 'Queue')} <ArrowRight className=\"h-3.5 w-3.5\" />\n </button>\n </div>\n <div className=\"divide-y divide-border rounded-xl border border-border-subtle bg-bg-surface\">\n {topViolations.length > 0 ? (\n topViolations.map((v) => (\n <button\n key={v.id}\n type=\"button\"\n onClick={() => navigateTo('/violations')}\n className=\"flex w-full items-start gap-3 px-4 py-3 text-left transition-colors hover:bg-accent/50\"\n >\n <AlertCircle className=\"mt-0.5 h-4 w-4 shrink-0 text-status-error-text\" />\n <div className=\"min-w-0 flex-1\">\n <p className=\"truncate text-sm font-medium text-text-primary\">{v.message}</p>\n <p className=\"mt-0.5 text-xs text-text-secondary\">\n {v.policy?.name ?? t('adaptercloud.overview.policy', 'Policy')} ·{' '}\n {formatRelativeTime(v.detectedAt)}\n </p>\n </div>\n <SeverityBadge severity={v.severity as AdaSeverity} />\n </button>\n ))\n ) : (\n <div className=\"flex flex-col items-center gap-2 px-4 py-8 text-center\">\n <ShieldAlert className=\"h-8 w-8 text-status-success-text\" />\n <p className=\"text-sm text-text-secondary\">\n {t('adaptercloud.overview.noViolations', 'No open violations. Estate is clean.')}\n </p>\n </div>\n )}\n </div>\n {drift.data && drift.data.length > 0 && (\n <button\n type=\"button\"\n onClick={() => navigateTo('/drift')}\n className=\"mt-3 flex w-full items-center gap-2 rounded-lg border border-border-subtle bg-bg-surface px-4 py-3 text-left text-sm transition-colors hover:bg-accent/50\"\n >\n <GitCompareArrows className=\"h-4 w-4 text-status-warning-text\" />\n <span className=\"text-text-primary\">\n {drift.data.length} {t('adaptercloud.overview.driftDetected', 'drift records detected')}\n </span>\n <ArrowRight className=\"ml-auto h-3.5 w-3.5 text-text-secondary\" />\n </button>\n )}\n </section>\n\n {/* Recent sync runs */}\n <section>\n <div className=\"mb-4 flex items-center justify-between\">\n <h2 className=\"text-lg font-semibold text-text-primary\">\n {t('adaptercloud.overview.recentRuns', 'Recent Sync Runs')}\n </h2>\n <button\n type=\"button\"\n onClick={() => navigateTo('/sync-runs')}\n className=\"flex items-center gap-1 text-sm font-medium text-primary hover:underline\"\n >\n {t('adaptercloud.common.viewAll', 'View all')}{' '}\n <ArrowRight className=\"h-3.5 w-3.5\" />\n </button>\n </div>\n <div className=\"divide-y divide-border rounded-xl border border-border-subtle bg-bg-surface\">\n {recentRuns.length > 0 ? (\n recentRuns.map((run) => (\n <button\n key={run.id}\n type=\"button\"\n onClick={() => navigateTo('/sync-runs')}\n className=\"flex w-full items-center gap-3 px-4 py-3 text-left transition-colors hover:bg-accent/50\"\n >\n <div className=\"flex h-8 w-8 shrink-0 items-center justify-center rounded-full bg-bg-sunken\">\n <Activity className=\"h-4 w-4 text-text-secondary\" />\n </div>\n <div className=\"min-w-0 flex-1\">\n <p className=\"text-sm font-medium text-text-primary\">\n {syncKindLabels[run.kind]}\n </p>\n <p className=\"mt-0.5 text-xs text-text-secondary\">\n {run.discoveredCount} {t('adaptercloud.overview.discovered', 'discovered')} ·{' '}\n {formatRelativeTime(run.createdAt)}\n </p>\n </div>\n <StatusBadge status={run.status} />\n </button>\n ))\n ) : (\n <div className=\"flex flex-col items-center gap-2 px-4 py-8 text-center\">\n <RefreshCw className=\"h-8 w-8 text-text-secondary\" />\n <p className=\"text-sm text-text-secondary\">\n {t('adaptercloud.overview.noRuns', 'No sync runs yet.')}\n </p>\n </div>\n )}\n </div>\n </section>\n </div>\n </DataState>\n </PageLayout>\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"OverviewPage.js","names":[],"sources":["../../../src/adaptercloud/pages/OverviewPage.tsx"],"sourcesContent":["/**\n * Overview / Dashboard Page\n * @module adaptercloud/pages\n *\n * The AdapterCloud landing page: a single-pane view of the estate — fabric\n * counts (adapters, connections, resources), governance posture (open\n * violations, unremediated drift), operations (recent sync runs) and FinOps\n * (workspace cost summary by service). All data is live from the deployed\n * `wspace-adaptercloud-svc` subgraph.\n */\n\nimport { useMemo, useCallback } from 'react';\nimport { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';\nimport {\n Boxes,\n Cable,\n Server,\n ShieldAlert,\n GitCompareArrows,\n RefreshCw,\n DollarSign,\n ArrowRight,\n Activity,\n AlertCircle,\n Plug,\n} from 'lucide-react';\nimport { EmphasisPanel, PagePurpose, GlassCard } from '@burdenoff/fe-libs/ui';\nimport { PageLayout } from '../components/PageLayout';\nimport { MetricsCard } from '../components/MetricsCard';\nimport { StatusBadge } from '../components/StatusBadge';\nimport { SeverityBadge } from '../components/SeverityBadge';\nimport { DataState } from '../components/DataState';\nimport { useAdapterCloud } from '../context/AdapterCloudContext';\nimport {\n useAdapters,\n useConnections,\n useResources,\n useViolations,\n useDriftRecords,\n useSyncRuns,\n useCostSummary,\n} from '../hooks/useAdapterCloudApi';\nimport { formatRelativeTime } from '../utils/formatters';\nimport { formatMoney, severityRank, syncKindLabels } from '../utils/domain';\nimport type { AdaSeverity } from '../types';\n\nexport function OverviewPage() {\n const { navigateTo, currentUser } = useAdapterCloud();\n const { t: translate } = useI18n();\n const t = useCallback(\n (key: string, fallback: string): string => {\n const value = translate(key);\n return value === key ? fallback : value;\n },\n [translate]\n );\n\n const adapters = useAdapters({ pagination: { limit: 1 } });\n const connections = useConnections({ pagination: { limit: 100 } });\n const resources = useResources({ pagination: { limit: 1 } });\n const openViolations = useViolations({ status: 'OPEN', pagination: { limit: 50 } });\n const drift = useDriftRecords({ status: 'DETECTED' });\n const syncRuns = useSyncRuns({ pagination: { limit: 5 } });\n const costSummary = useCostSummary();\n\n const firstName = currentUser?.firstName ?? currentUser?.name ?? 'there';\n\n // Connection health breakdown\n const connectionHealth = useMemo(() => {\n const items = connections.data?.items ?? [];\n const counts = { CONNECTED: 0, DEGRADED: 0, ERROR: 0, PENDING: 0, DISABLED: 0 };\n for (const c of items) counts[c.status] += 1;\n return counts;\n }, [connections.data]);\n\n const topViolations = useMemo(() => {\n return [...(openViolations.data?.items ?? [])]\n .sort((a, b) => severityRank[b.severity] - severityRank[a.severity])\n .slice(0, 5);\n }, [openViolations.data]);\n\n const recentRuns = syncRuns.data?.items ?? [];\n\n const topCostLines = useMemo(() => {\n return [...(costSummary.data?.lines ?? [])].sort((a, b) => b.total - a.total).slice(0, 6);\n }, [costSummary.data]);\n\n const isLoading =\n adapters.isLoading ||\n connections.isLoading ||\n resources.isLoading ||\n openViolations.isLoading ||\n drift.isLoading ||\n costSummary.isLoading;\n\n const isError =\n adapters.isError ||\n connections.isError ||\n resources.isError ||\n openViolations.isError ||\n drift.isError ||\n costSummary.isError;\n\n const refetchAll = useCallback(() => {\n void adapters.refetch();\n void connections.refetch();\n void resources.refetch();\n void openViolations.refetch();\n void drift.refetch();\n void syncRuns.refetch();\n void costSummary.refetch();\n }, [adapters, connections, resources, openViolations, drift, syncRuns, costSummary]);\n\n const stats = [\n {\n icon: <Boxes className=\"h-5 w-5\" />,\n value: adapters.data?.items.length\n ? String(adapters.data.items.length)\n : adapters.data\n ? '0'\n : '—',\n label: t('adaptercloud.overview.adapters', 'Adapters'),\n onClick: () => navigateTo('/adapters'),\n },\n {\n icon: <Cable className=\"h-5 w-5\" />,\n value: connections.data ? String(connections.data.items.length) : '—',\n label: t('adaptercloud.overview.connections', 'Connections'),\n onClick: () => navigateTo('/connections'),\n },\n {\n icon: <Server className=\"h-5 w-5\" />,\n value: resources.data ? String(resources.data.items.length) : '—',\n label: t('adaptercloud.overview.resources', 'Resources'),\n onClick: () => navigateTo('/resources'),\n },\n {\n icon: <ShieldAlert className=\"h-5 w-5\" />,\n value: openViolations.data ? String(openViolations.data.items.length) : '—',\n label: t('adaptercloud.overview.openViolations', 'Open Violations'),\n onClick: () => navigateTo('/violations'),\n },\n ];\n\n return (\n <PageLayout\n title={`${t('adaptercloud.overview.hello', 'Welcome back')}, ${firstName}`}\n description={t(\n 'adaptercloud.overview.subtitle',\n 'Your infrastructure continuum at a glance — fabric, governance, operations and cost.'\n )}\n actions={\n <button\n type=\"button\"\n onClick={refetchAll}\n className=\"inline-flex items-center gap-2 rounded-lg border border-border-subtle bg-bg-surface px-3 py-2 text-sm font-medium text-text-primary transition-colors hover:bg-accent\"\n >\n <RefreshCw className=\"h-4 w-4\" />\n {t('adaptercloud.common.refresh', 'Refresh')}\n </button>\n }\n >\n <PagePurpose className=\"mb-6\">\n {t(\n 'adaptercloud.overview.purpose',\n 'AdapterCloud connects your fragmented infrastructure — multi-cloud, on-prem, edge and SaaS — into one governable fabric. This overview is your control plane: see what you run, how connected it is, what is out of policy, and what it costs.'\n )}\n </PagePurpose>\n\n <DataState\n isLoading={isLoading}\n isError={isError}\n error={adapters.error}\n onRetry={refetchAll}\n >\n {/* Top metrics — the one emphasis zone */}\n <EmphasisPanel className=\"mb-8\">\n <h2 className=\"mb-4 text-lg font-semibold text-text-primary\">\n {t('adaptercloud.overview.estate', 'Estate Summary')}\n </h2>\n <div className=\"grid grid-cols-2 gap-4 lg:grid-cols-4\">\n {stats.map((stat) => (\n <button key={stat.label} type=\"button\" onClick={stat.onClick} className=\"text-left\">\n <MetricsCard icon={stat.icon} value={stat.value} label={stat.label} />\n </button>\n ))}\n </div>\n </EmphasisPanel>\n\n {/* Connection health + Cost summary */}\n <div className=\"mb-8 grid grid-cols-1 gap-6 lg:grid-cols-2\">\n {/* Connection health */}\n <section>\n <div className=\"mb-4 flex items-center justify-between\">\n <h2 className=\"text-lg font-semibold text-text-primary\">\n {t('adaptercloud.overview.connectionHealth', 'Connection Health')}\n </h2>\n <button\n type=\"button\"\n onClick={() => navigateTo('/connections')}\n className=\"flex items-center gap-1 text-sm font-medium text-primary hover:underline\"\n >\n {t('adaptercloud.common.viewAll', 'View all')}{' '}\n <ArrowRight className=\"h-3.5 w-3.5\" />\n </button>\n </div>\n <GlassCard className=\"p-5\">\n {connections.data && connections.data.items.length > 0 ? (\n <ul className=\"space-y-3\">\n {(\n [\n ['CONNECTED', connectionHealth.CONNECTED],\n ['DEGRADED', connectionHealth.DEGRADED],\n ['ERROR', connectionHealth.ERROR],\n ['PENDING', connectionHealth.PENDING],\n ['DISABLED', connectionHealth.DISABLED],\n ] as const\n ).map(([status, count]) => (\n <li key={status} className=\"flex items-center justify-between\">\n <StatusBadge status={status} />\n <span className=\"text-sm font-semibold tabular-nums text-text-primary\">\n {count}\n </span>\n </li>\n ))}\n </ul>\n ) : (\n <div className=\"flex flex-col items-center gap-3 py-6 text-center\">\n <Plug className=\"h-8 w-8 text-text-secondary\" />\n <p className=\"text-sm text-text-secondary\">\n {t(\n 'adaptercloud.overview.noConnections',\n 'No connections yet. Connect a target to start discovering resources.'\n )}\n </p>\n <button\n type=\"button\"\n onClick={() => navigateTo('/connections/new')}\n className=\"rounded-lg bg-action-primary-bg px-4 py-2 text-sm font-medium text-action-primary-text transition-colors hover:opacity-90\"\n >\n {t('adaptercloud.overview.addConnection', 'Add connection')}\n </button>\n </div>\n )}\n </GlassCard>\n </section>\n\n {/* Cost summary */}\n <section>\n <div className=\"mb-4 flex items-center justify-between\">\n <h2 className=\"text-lg font-semibold text-text-primary\">\n {t('adaptercloud.overview.costByService', 'Cost by Service')}\n </h2>\n <button\n type=\"button\"\n onClick={() => navigateTo('/costs')}\n className=\"flex items-center gap-1 text-sm font-medium text-primary hover:underline\"\n >\n {t('adaptercloud.overview.finops', 'FinOps')} <ArrowRight className=\"h-3.5 w-3.5\" />\n </button>\n </div>\n <GlassCard className=\"p-5\">\n <div className=\"mb-4 flex items-baseline gap-2\">\n <DollarSign className=\"h-5 w-5 text-text-secondary\" />\n <span className=\"text-2xl font-bold tabular-nums text-text-primary\">\n {costSummary.data\n ? formatMoney(costSummary.data.total, costSummary.data.currency)\n : '—'}\n </span>\n <span className=\"text-sm text-text-secondary\">\n {t('adaptercloud.overview.totalSpend', 'total spend')}\n </span>\n </div>\n {topCostLines.length > 0 ? (\n <ul className=\"space-y-2\">\n {topCostLines.map((line) => (\n <li key={line.service} className=\"flex items-center justify-between text-sm\">\n <span className=\"truncate text-text-primary\">{line.service}</span>\n <span className=\"tabular-nums text-text-secondary\">\n {formatMoney(line.total, costSummary.data?.currency ?? 'USD')}\n </span>\n </li>\n ))}\n </ul>\n ) : (\n <p className=\"py-4 text-center text-sm text-text-secondary\">\n {t(\n 'adaptercloud.overview.noCost',\n 'No cost records ingested yet. Run a cost-ingest sync to populate FinOps.'\n )}\n </p>\n )}\n </GlassCard>\n </section>\n </div>\n\n {/* Governance + Operations */}\n <div className=\"grid grid-cols-1 gap-6 lg:grid-cols-2\">\n {/* Top open violations */}\n <section>\n <div className=\"mb-4 flex items-center justify-between\">\n <h2 className=\"text-lg font-semibold text-text-primary\">\n {t('adaptercloud.overview.priorityViolations', 'Priority Violations')}\n </h2>\n <button\n type=\"button\"\n onClick={() => navigateTo('/violations')}\n className=\"flex items-center gap-1 text-sm font-medium text-primary hover:underline\"\n >\n {t('adaptercloud.overview.queue', 'Queue')} <ArrowRight className=\"h-3.5 w-3.5\" />\n </button>\n </div>\n <div className=\"divide-y divide-border rounded-xl border border-border-subtle bg-bg-surface\">\n {topViolations.length > 0 ? (\n topViolations.map((v) => (\n <button\n key={v.id}\n type=\"button\"\n onClick={() => navigateTo('/violations')}\n className=\"flex w-full items-start gap-3 px-4 py-3 text-left transition-colors hover:bg-accent/50\"\n >\n <AlertCircle className=\"mt-0.5 h-4 w-4 shrink-0 text-status-error-text\" />\n <div className=\"min-w-0 flex-1\">\n <p className=\"truncate text-sm font-medium text-text-primary\">{v.message}</p>\n <p className=\"mt-0.5 text-xs text-text-secondary\">\n {v.policy?.name ?? t('adaptercloud.overview.policy', 'Policy')} ·{' '}\n {formatRelativeTime(v.detectedAt)}\n </p>\n </div>\n <SeverityBadge severity={v.severity as AdaSeverity} />\n </button>\n ))\n ) : (\n <div className=\"flex flex-col items-center gap-2 px-4 py-8 text-center\">\n <ShieldAlert className=\"h-8 w-8 text-status-success-text\" />\n <p className=\"text-sm text-text-secondary\">\n {t(\n 'adaptercloud.overview.noViolations',\n 'No open violations. Estate is clean.'\n )}\n </p>\n </div>\n )}\n </div>\n {drift.data && drift.data.length > 0 && (\n <button\n type=\"button\"\n onClick={() => navigateTo('/drift')}\n className=\"mt-3 flex w-full items-center gap-2 rounded-lg border border-border-subtle bg-bg-surface px-4 py-3 text-left text-sm transition-colors hover:bg-accent/50\"\n >\n <GitCompareArrows className=\"h-4 w-4 text-status-warning-text\" />\n <span className=\"text-text-primary\">\n {drift.data.length}{' '}\n {t('adaptercloud.overview.driftDetected', 'drift records detected')}\n </span>\n <ArrowRight className=\"ml-auto h-3.5 w-3.5 text-text-secondary\" />\n </button>\n )}\n </section>\n\n {/* Recent sync runs */}\n <section>\n <div className=\"mb-4 flex items-center justify-between\">\n <h2 className=\"text-lg font-semibold text-text-primary\">\n {t('adaptercloud.overview.recentRuns', 'Recent Sync Runs')}\n </h2>\n <button\n type=\"button\"\n onClick={() => navigateTo('/sync-runs')}\n className=\"flex items-center gap-1 text-sm font-medium text-primary hover:underline\"\n >\n {t('adaptercloud.common.viewAll', 'View all')}{' '}\n <ArrowRight className=\"h-3.5 w-3.5\" />\n </button>\n </div>\n <div className=\"divide-y divide-border rounded-xl border border-border-subtle bg-bg-surface\">\n {recentRuns.length > 0 ? (\n recentRuns.map((run) => (\n <button\n key={run.id}\n type=\"button\"\n onClick={() => navigateTo('/sync-runs')}\n className=\"flex w-full items-center gap-3 px-4 py-3 text-left transition-colors hover:bg-accent/50\"\n >\n <div className=\"flex h-8 w-8 shrink-0 items-center justify-center rounded-full bg-bg-sunken\">\n <Activity className=\"h-4 w-4 text-text-secondary\" />\n </div>\n <div className=\"min-w-0 flex-1\">\n <p className=\"text-sm font-medium text-text-primary\">\n {syncKindLabels[run.kind]}\n </p>\n <p className=\"mt-0.5 text-xs text-text-secondary\">\n {run.discoveredCount} {t('adaptercloud.overview.discovered', 'discovered')}{' '}\n · {formatRelativeTime(run.createdAt)}\n </p>\n </div>\n <StatusBadge status={run.status} />\n </button>\n ))\n ) : (\n <div className=\"flex flex-col items-center gap-2 px-4 py-8 text-center\">\n <RefreshCw className=\"h-8 w-8 text-text-secondary\" />\n <p className=\"text-sm text-text-secondary\">\n {t('adaptercloud.overview.noRuns', 'No sync runs yet.')}\n </p>\n </div>\n )}\n </div>\n </section>\n </div>\n </DataState>\n </PageLayout>\n );\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AdapterDetailPage.js","names":[],"sources":["../../../../src/adaptercloud/pages/adapters/AdapterDetailPage.tsx"],"sourcesContent":["/**\n * Adapter Detail Page\n * @module adaptercloud/pages/adapters\n *\n * Full view of one adapter (`adaAdapter`) — metadata, capabilities, config\n * schema and its connections. Supports archiving via `adaArchiveAdapter`.\n */\n\nimport { useCallback } from 'react';\nimport { useParams } from 'react-router-dom';\nimport { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';\nimport { ArrowLeft, Archive, Cable, Pencil } from 'lucide-react';\nimport { GlassCard } from '@burdenoff/fe-libs/ui';\nimport { PageLayout } from '../../components/PageLayout';\nimport { StatusBadge } from '../../components/StatusBadge';\nimport { DataState } from '../../components/DataState';\nimport { useAdapterCloud } from '../../context/AdapterCloudContext';\nimport { useAdapter, useArchiveAdapter } from '../../hooks/useAdapterCloudApi';\nimport { adapterKindLabels, authMethodLabels } from '../../utils/domain';\nimport { formatDate } from '../../utils/formatters';\n\nexport function AdapterDetailPage() {\n const { id } = useParams<{ id: string }>();\n const { navigateTo } = useAdapterCloud();\n const { t: translate } = useI18n();\n const t = useCallback(\n (key: string, fallback: string): string => {\n const value = translate(key);\n return value === key ? fallback : value;\n },\n [translate]\n );\n\n const query = useAdapter(id);\n const archive = useArchiveAdapter();\n const adapter = query.data;\n\n const handleArchive = useCallback(() => {\n if (!id) return;\n archive.mutate(id, {\n onSuccess: () => navigateTo('/adapters'),\n });\n }, [archive, id, navigateTo]);\n\n return (\n <PageLayout\n title={adapter?.name ?? t('adaptercloud.adapters.detailTitle', 'Adapter')}\n description={adapter?.slug}\n actions={\n <div className=\"flex items-center gap-2\">\n <button\n type=\"button\"\n onClick={() => navigateTo('/adapters')}\n className=\"inline-flex items-center gap-2 rounded-lg border border-border-subtle bg-bg-surface px-3 py-2 text-sm font-medium text-text-primary transition-colors hover:bg-accent\"\n >\n <ArrowLeft className=\"h-4 w-4\" />\n {t('adaptercloud.common.back', 'Back')}\n </button>\n {adapter && (\n <>\n <button\n type=\"button\"\n onClick={() => navigateTo(`/adapters/${adapter.id}/edit`)}\n className=\"inline-flex items-center gap-2 rounded-lg border border-border-subtle bg-bg-surface px-3 py-2 text-sm font-medium text-text-primary transition-colors hover:bg-accent\"\n >\n <Pencil className=\"h-4 w-4\" />\n {t('adaptercloud.common.edit', 'Edit')}\n </button>\n {adapter.status !== 'ARCHIVED' && (\n <button\n type=\"button\"\n onClick={handleArchive}\n disabled={archive.isPending}\n className=\"inline-flex items-center gap-2 rounded-lg border border-border-subtle bg-bg-surface px-3 py-2 text-sm font-medium text-status-error-text transition-colors hover:bg-status-error-bg-subtle disabled:opacity-50\"\n >\n <Archive className=\"h-4 w-4\" />\n {t('adaptercloud.common.archive', 'Archive')}\n </button>\n )}\n </>\n )}\n </div>\n }\n >\n <DataState\n isLoading={query.isLoading}\n isError={query.isError}\n isEmpty={!query.isLoading && !adapter}\n error={query.error}\n onRetry={() => void query.refetch()}\n emptyTitle={t('adaptercloud.adapters.notFound', 'Adapter not found')}\n emptyDescription={t(\n 'adaptercloud.adapters.notFoundDescription',\n 'This adapter does not exist or belongs to another workspace.'\n )}\n >\n {adapter && (\n <div className=\"space-y-6\">\n {/* Metadata */}\n <GlassCard className=\"p-6\">\n <div className=\"mb-4 flex items-center gap-3\">\n <StatusBadge status={adapter.status} />\n <span className=\"inline-flex items-center rounded-full bg-bg-sunken px-2.5 py-0.5 text-xs font-medium text-text-secondary\">\n {adapterKindLabels[adapter.kind]}\n </span>\n <span className=\"text-xs text-text-secondary\">v{adapter.version}</span>\n </div>\n\n {adapter.description && (\n <p className=\"mb-4 text-sm text-text-secondary\">{adapter.description}</p>\n )}\n\n <dl className=\"grid grid-cols-2 gap-4 text-sm sm:grid-cols-3\">\n <div>\n <dt className=\"text-text-secondary\">{t('adaptercloud.adapters.vendor', 'Vendor')}</dt>\n <dd className=\"mt-0.5 font-medium text-text-primary\">{adapter.vendor ?? '—'}</dd>\n </div>\n <div>\n <dt className=\"text-text-secondary\">\n {t('adaptercloud.common.created', 'Created')}\n </dt>\n <dd className=\"mt-0.5 font-medium text-text-primary\">\n {formatDate(adapter.createdAt)}\n </dd>\n </div>\n <div>\n <dt className=\"text-text-secondary\">\n {t('adaptercloud.common.updated', 'Updated')}\n </dt>\n <dd className=\"mt-0.5 font-medium text-text-primary\">\n {formatDate(adapter.updatedAt)}\n </dd>\n </div>\n </dl>\n\n {adapter.capabilities.length > 0 && (\n <div className=\"mt-5\">\n <h3 className=\"mb-2 text-sm font-semibold text-text-primary\">\n {t('adaptercloud.adapters.capabilities', 'Capabilities')}\n </h3>\n <div className=\"flex flex-wrap gap-1.5\">\n {adapter.capabilities.map((cap) => (\n <span\n key={cap}\n className=\"inline-flex items-center rounded-full bg-accent-blue-subtle px-2.5 py-0.5 text-xs text-accent-blue\"\n >\n {cap}\n </span>\n ))}\n </div>\n </div>\n )}\n\n {adapter.tags.length > 0 && (\n <div className=\"mt-4\">\n <h3 className=\"mb-2 text-sm font-semibold text-text-primary\">\n {t('adaptercloud.common.tags', 'Tags')}\n </h3>\n <div className=\"flex flex-wrap gap-1.5\">\n {adapter.tags.map((tag) => (\n <span\n key={tag}\n className=\"inline-flex items-center rounded-full bg-bg-sunken px-2.5 py-0.5 text-xs text-text-secondary\"\n >\n {tag}\n </span>\n ))}\n </div>\n </div>\n )}\n </GlassCard>\n\n {/* Config schema */}\n {adapter.configSchema != null && (\n <GlassCard className=\"p-6\">\n <h3 className=\"mb-3 text-sm font-semibold text-text-primary\">\n {t('adaptercloud.adapters.configSchema', 'Config Schema')}\n </h3>\n <pre className=\"overflow-auto rounded-lg bg-bg-sunken p-4 text-xs text-text-secondary\">\n {JSON.stringify(adapter.configSchema, null, 2)}\n </pre>\n </GlassCard>\n )}\n\n {/* Connections */}\n <section>\n <h3 className=\"mb-3 text-base font-semibold text-text-primary\">\n {t('adaptercloud.adapters.connections', 'Connections')}\n </h3>\n {adapter.connections && adapter.connections.length > 0 ? (\n <div className=\"divide-y divide-border rounded-xl border border-border-subtle bg-bg-surface\">\n {adapter.connections.map((conn) => (\n <button\n key={conn.id}\n type=\"button\"\n onClick={() => navigateTo(`/connections/${conn.id}`)}\n className=\"flex w-full items-center gap-3 px-4 py-3 text-left transition-colors hover:bg-accent/50\"\n >\n <Cable className=\"h-4 w-4 shrink-0 text-text-secondary\" />\n <div className=\"min-w-0 flex-1\">\n <p className=\"truncate text-sm font-medium text-text-primary\">\n {conn.name}\n </p>\n <p className=\"mt-0.5 text-xs text-text-secondary\">\n {authMethodLabels[conn.authMethod]} · {conn.resourceCount}{' '}\n {t('adaptercloud.adapters.resourcesLower', 'resources')}\n </p>\n </div>\n <StatusBadge status={conn.status} />\n </button>\n ))}\n </div>\n ) : (\n <p className=\"rounded-xl border border-border-subtle bg-bg-surface px-4 py-6 text-center text-sm text-text-secondary\">\n {t('adaptercloud.adapters.noConnections', 'No connections use this adapter yet.')}\n </p>\n )}\n </section>\n </div>\n )}\n </DataState>\n </PageLayout>\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"AdapterDetailPage.js","names":[],"sources":["../../../../src/adaptercloud/pages/adapters/AdapterDetailPage.tsx"],"sourcesContent":["/**\n * Adapter Detail Page\n * @module adaptercloud/pages/adapters\n *\n * Full view of one adapter (`adaAdapter`) — metadata, capabilities, config\n * schema and its connections. Supports archiving via `adaArchiveAdapter`.\n */\n\nimport { useCallback } from 'react';\nimport { useParams } from 'react-router-dom';\nimport { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';\nimport { ArrowLeft, Archive, Cable, Pencil } from 'lucide-react';\nimport { GlassCard } from '@burdenoff/fe-libs/ui';\nimport { PageLayout } from '../../components/PageLayout';\nimport { StatusBadge } from '../../components/StatusBadge';\nimport { DataState } from '../../components/DataState';\nimport { useAdapterCloud } from '../../context/AdapterCloudContext';\nimport { useAdapter, useArchiveAdapter } from '../../hooks/useAdapterCloudApi';\nimport { adapterKindLabels, authMethodLabels } from '../../utils/domain';\nimport { formatDate } from '../../utils/formatters';\n\nexport function AdapterDetailPage() {\n const { id } = useParams<{ id: string }>();\n const { navigateTo } = useAdapterCloud();\n const { t: translate } = useI18n();\n const t = useCallback(\n (key: string, fallback: string): string => {\n const value = translate(key);\n return value === key ? fallback : value;\n },\n [translate]\n );\n\n const query = useAdapter(id);\n const archive = useArchiveAdapter();\n const adapter = query.data;\n\n const handleArchive = useCallback(() => {\n if (!id) return;\n archive.mutate(id, {\n onSuccess: () => navigateTo('/adapters'),\n });\n }, [archive, id, navigateTo]);\n\n return (\n <PageLayout\n title={adapter?.name ?? t('adaptercloud.adapters.detailTitle', 'Adapter')}\n description={adapter?.slug}\n actions={\n <div className=\"flex items-center gap-2\">\n <button\n type=\"button\"\n onClick={() => navigateTo('/adapters')}\n className=\"inline-flex items-center gap-2 rounded-lg border border-border-subtle bg-bg-surface px-3 py-2 text-sm font-medium text-text-primary transition-colors hover:bg-accent\"\n >\n <ArrowLeft className=\"h-4 w-4\" />\n {t('adaptercloud.common.back', 'Back')}\n </button>\n {adapter && (\n <>\n <button\n type=\"button\"\n onClick={() => navigateTo(`/adapters/${adapter.id}/edit`)}\n className=\"inline-flex items-center gap-2 rounded-lg border border-border-subtle bg-bg-surface px-3 py-2 text-sm font-medium text-text-primary transition-colors hover:bg-accent\"\n >\n <Pencil className=\"h-4 w-4\" />\n {t('adaptercloud.common.edit', 'Edit')}\n </button>\n {adapter.status !== 'ARCHIVED' && (\n <button\n type=\"button\"\n onClick={handleArchive}\n disabled={archive.isPending}\n className=\"inline-flex items-center gap-2 rounded-lg border border-border-subtle bg-bg-surface px-3 py-2 text-sm font-medium text-status-error-text transition-colors hover:bg-status-error-bg-subtle disabled:opacity-50\"\n >\n <Archive className=\"h-4 w-4\" />\n {t('adaptercloud.common.archive', 'Archive')}\n </button>\n )}\n </>\n )}\n </div>\n }\n >\n <DataState\n isLoading={query.isLoading}\n isError={query.isError}\n isEmpty={!query.isLoading && !adapter}\n error={query.error}\n onRetry={() => void query.refetch()}\n emptyTitle={t('adaptercloud.adapters.notFound', 'Adapter not found')}\n emptyDescription={t(\n 'adaptercloud.adapters.notFoundDescription',\n 'This adapter does not exist or belongs to another workspace.'\n )}\n >\n {adapter && (\n <div className=\"space-y-6\">\n {/* Metadata */}\n <GlassCard className=\"p-6\">\n <div className=\"mb-4 flex items-center gap-3\">\n <StatusBadge status={adapter.status} />\n <span className=\"inline-flex items-center rounded-full bg-bg-sunken px-2.5 py-0.5 text-xs font-medium text-text-secondary\">\n {adapterKindLabels[adapter.kind]}\n </span>\n <span className=\"text-xs text-text-secondary\">v{adapter.version}</span>\n </div>\n\n {adapter.description && (\n <p className=\"mb-4 text-sm text-text-secondary\">{adapter.description}</p>\n )}\n\n <dl className=\"grid grid-cols-2 gap-4 text-sm sm:grid-cols-3\">\n <div>\n <dt className=\"text-text-secondary\">\n {t('adaptercloud.adapters.vendor', 'Vendor')}\n </dt>\n <dd className=\"mt-0.5 font-medium text-text-primary\">{adapter.vendor ?? '—'}</dd>\n </div>\n <div>\n <dt className=\"text-text-secondary\">\n {t('adaptercloud.common.created', 'Created')}\n </dt>\n <dd className=\"mt-0.5 font-medium text-text-primary\">\n {formatDate(adapter.createdAt)}\n </dd>\n </div>\n <div>\n <dt className=\"text-text-secondary\">\n {t('adaptercloud.common.updated', 'Updated')}\n </dt>\n <dd className=\"mt-0.5 font-medium text-text-primary\">\n {formatDate(adapter.updatedAt)}\n </dd>\n </div>\n </dl>\n\n {adapter.capabilities.length > 0 && (\n <div className=\"mt-5\">\n <h3 className=\"mb-2 text-sm font-semibold text-text-primary\">\n {t('adaptercloud.adapters.capabilities', 'Capabilities')}\n </h3>\n <div className=\"flex flex-wrap gap-1.5\">\n {adapter.capabilities.map((cap) => (\n <span\n key={cap}\n className=\"inline-flex items-center rounded-full bg-accent-blue-subtle px-2.5 py-0.5 text-xs text-accent-blue\"\n >\n {cap}\n </span>\n ))}\n </div>\n </div>\n )}\n\n {adapter.tags.length > 0 && (\n <div className=\"mt-4\">\n <h3 className=\"mb-2 text-sm font-semibold text-text-primary\">\n {t('adaptercloud.common.tags', 'Tags')}\n </h3>\n <div className=\"flex flex-wrap gap-1.5\">\n {adapter.tags.map((tag) => (\n <span\n key={tag}\n className=\"inline-flex items-center rounded-full bg-bg-sunken px-2.5 py-0.5 text-xs text-text-secondary\"\n >\n {tag}\n </span>\n ))}\n </div>\n </div>\n )}\n </GlassCard>\n\n {/* Config schema */}\n {adapter.configSchema != null && (\n <GlassCard className=\"p-6\">\n <h3 className=\"mb-3 text-sm font-semibold text-text-primary\">\n {t('adaptercloud.adapters.configSchema', 'Config Schema')}\n </h3>\n <pre className=\"overflow-auto rounded-lg bg-bg-sunken p-4 text-xs text-text-secondary\">\n {JSON.stringify(adapter.configSchema, null, 2)}\n </pre>\n </GlassCard>\n )}\n\n {/* Connections */}\n <section>\n <h3 className=\"mb-3 text-base font-semibold text-text-primary\">\n {t('adaptercloud.adapters.connections', 'Connections')}\n </h3>\n {adapter.connections && adapter.connections.length > 0 ? (\n <div className=\"divide-y divide-border rounded-xl border border-border-subtle bg-bg-surface\">\n {adapter.connections.map((conn) => (\n <button\n key={conn.id}\n type=\"button\"\n onClick={() => navigateTo(`/connections/${conn.id}`)}\n className=\"flex w-full items-center gap-3 px-4 py-3 text-left transition-colors hover:bg-accent/50\"\n >\n <Cable className=\"h-4 w-4 shrink-0 text-text-secondary\" />\n <div className=\"min-w-0 flex-1\">\n <p className=\"truncate text-sm font-medium text-text-primary\">\n {conn.name}\n </p>\n <p className=\"mt-0.5 text-xs text-text-secondary\">\n {authMethodLabels[conn.authMethod]} · {conn.resourceCount}{' '}\n {t('adaptercloud.adapters.resourcesLower', 'resources')}\n </p>\n </div>\n <StatusBadge status={conn.status} />\n </button>\n ))}\n </div>\n ) : (\n <p className=\"rounded-xl border border-border-subtle bg-bg-surface px-4 py-6 text-center text-sm text-text-secondary\">\n {t('adaptercloud.adapters.noConnections', 'No connections use this adapter yet.')}\n </p>\n )}\n </section>\n </div>\n )}\n </DataState>\n </PageLayout>\n );\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AdapterFormPage.js","names":[],"sources":["../../../../src/adaptercloud/pages/adapters/AdapterFormPage.tsx"],"sourcesContent":["/**\n * Adapter Create / Edit Form Page\n * @module adaptercloud/pages/adapters\n *\n * Creates a new adapter (`adaCreateAdapter`) or edits an existing one\n * (`adaUpdateAdapter`). The presence of a route `:id` switches to edit mode and\n * pre-fills from `adaAdapter`.\n */\n\nimport { useCallback, useEffect, useState } from 'react';\nimport { useParams } from 'react-router-dom';\nimport { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';\nimport { ArrowLeft, Loader2, Save } from 'lucide-react';\nimport { GlassCard } from '@burdenoff/fe-libs/ui';\nimport { PageLayout } from '../../components/PageLayout';\nimport { DataState } from '../../components/DataState';\nimport { useAdapterCloud } from '../../context/AdapterCloudContext';\nimport {\n useAdapter,\n useCreateAdapter,\n useUpdateAdapter,\n} from '../../hooks/useAdapterCloudApi';\nimport { adapterKindLabels, adapterStatusLabels } from '../../utils/domain';\nimport type { AdaAdapterKind, AdaAdapterStatus } from '../../types';\n\nconst slugify = (value: string): string =>\n value\n .toLowerCase()\n .trim()\n .replace(/[^a-z0-9]+/g, '-')\n .replace(/^-+|-+$/g, '');\n\nconst parseList = (value: string): string[] =>\n value\n .split(',')\n .map((s) => s.trim())\n .filter(Boolean);\n\ninterface FormState {\n name: string;\n slug: string;\n kind: AdaAdapterKind;\n status: AdaAdapterStatus;\n vendor: string;\n version: string;\n description: string;\n capabilities: string;\n tags: string;\n}\n\nconst EMPTY: FormState = {\n name: '',\n slug: '',\n kind: 'CUSTOM',\n status: 'DRAFT',\n vendor: '',\n version: '1.0.0',\n description: '',\n capabilities: '',\n tags: '',\n};\n\nexport function AdapterFormPage() {\n const { id } = useParams<{ id: string }>();\n const isEdit = Boolean(id);\n const { navigateTo } = useAdapterCloud();\n const { t: translate } = useI18n();\n const t = useCallback(\n (key: string, fallback: string): string => {\n const value = translate(key);\n return value === key ? fallback : value;\n },\n [translate]\n );\n\n const existing = useAdapter(isEdit ? id : undefined);\n const create = useCreateAdapter();\n const update = useUpdateAdapter();\n\n const [form, setForm] = useState<FormState>(EMPTY);\n const [slugTouched, setSlugTouched] = useState(false);\n const [error, setError] = useState<string | null>(null);\n\n // Pre-fill in edit mode\n useEffect(() => {\n const a = existing.data;\n if (isEdit && a) {\n setForm({\n name: a.name,\n slug: a.slug,\n kind: a.kind,\n status: a.status,\n vendor: a.vendor ?? '',\n version: a.version,\n description: a.description ?? '',\n capabilities: a.capabilities.join(', '),\n tags: a.tags.join(', '),\n });\n setSlugTouched(true);\n }\n }, [isEdit, existing.data]);\n\n const setField = useCallback(\n <K extends keyof FormState>(key: K, value: FormState[K]) => {\n setForm((prev) => ({ ...prev, [key]: value }));\n },\n []\n );\n\n const onNameChange = useCallback(\n (value: string) => {\n setForm((prev) => ({\n ...prev,\n name: value,\n slug: slugTouched ? prev.slug : slugify(value),\n }));\n },\n [slugTouched]\n );\n\n const isPending = create.isPending || update.isPending;\n\n const handleSubmit = useCallback(\n (e: React.FormEvent) => {\n e.preventDefault();\n setError(null);\n\n if (!form.name.trim()) {\n setError(t('adaptercloud.adapters.form.nameRequired', 'Name is required.'));\n return;\n }\n if (!isEdit && !form.slug.trim()) {\n setError(t('adaptercloud.adapters.form.slugRequired', 'Slug is required.'));\n return;\n }\n\n const common = {\n name: form.name.trim(),\n kind: form.kind,\n status: form.status,\n vendor: form.vendor.trim() || undefined,\n version: form.version.trim() || undefined,\n description: form.description.trim() || undefined,\n capabilities: parseList(form.capabilities),\n tags: parseList(form.tags),\n };\n\n const onError = (err: unknown) =>\n setError(err instanceof Error ? err.message : 'Failed to save adapter.');\n\n if (isEdit && id) {\n update.mutate(\n { id, input: common },\n { onSuccess: (a) => navigateTo(`/adapters/${a.id}`), onError }\n );\n } else {\n create.mutate(\n { ...common, slug: form.slug.trim() },\n { onSuccess: (a) => navigateTo(`/adapters/${a.id}`), onError }\n );\n }\n },\n [create, update, form, id, isEdit, navigateTo, t]\n );\n\n const fieldClass =\n 'w-full rounded-lg border border-border-subtle bg-bg-surface px-3 py-2 text-sm text-text-primary placeholder:text-text-secondary focus:outline-none focus:ring-2 focus:ring-[var(--color-focus-ring)]';\n const labelClass = 'mb-1.5 block text-sm font-medium text-text-primary';\n\n return (\n <PageLayout\n title={\n isEdit\n ? t('adaptercloud.adapters.editTitle', 'Edit Adapter')\n : t('adaptercloud.adapters.newTitle', 'New Adapter')\n }\n description={t(\n 'adaptercloud.adapters.formSubtitle',\n 'Define how AdapterCloud connects to a class of system.'\n )}\n actions={\n <button\n type=\"button\"\n onClick={() => navigateTo(isEdit && id ? `/adapters/${id}` : '/adapters')}\n className=\"inline-flex items-center gap-2 rounded-lg border border-border-subtle bg-bg-surface px-3 py-2 text-sm font-medium text-text-primary transition-colors hover:bg-accent\"\n >\n <ArrowLeft className=\"h-4 w-4\" />\n {t('adaptercloud.common.cancel', 'Cancel')}\n </button>\n }\n >\n <DataState\n isLoading={isEdit && existing.isLoading}\n isError={isEdit && existing.isError}\n isEmpty={isEdit && !existing.isLoading && !existing.data}\n error={existing.error}\n onRetry={() => void existing.refetch()}\n emptyTitle={t('adaptercloud.adapters.notFound', 'Adapter not found')}\n >\n <form onSubmit={handleSubmit} className=\"max-w-2xl\">\n <GlassCard className=\"space-y-5 p-6\">\n <div className=\"grid grid-cols-1 gap-5 sm:grid-cols-2\">\n <div>\n <label htmlFor=\"ada-name\" className={labelClass}>\n {t('adaptercloud.adapters.form.name', 'Name')} *\n </label>\n <input\n id=\"ada-name\"\n type=\"text\"\n value={form.name}\n onChange={(e) => onNameChange(e.target.value)}\n className={fieldClass}\n placeholder=\"AWS Production\"\n />\n </div>\n <div>\n <label htmlFor=\"ada-slug\" className={labelClass}>\n {t('adaptercloud.adapters.form.slug', 'Slug')} {isEdit ? '' : '*'}\n </label>\n <input\n id=\"ada-slug\"\n type=\"text\"\n value={form.slug}\n disabled={isEdit}\n onChange={(e) => {\n setSlugTouched(true);\n setField('slug', slugify(e.target.value));\n }}\n className={`${fieldClass} disabled:cursor-not-allowed disabled:opacity-60`}\n placeholder=\"aws-production\"\n />\n </div>\n </div>\n\n <div className=\"grid grid-cols-1 gap-5 sm:grid-cols-2\">\n <div>\n <label htmlFor=\"ada-kind\" className={labelClass}>\n {t('adaptercloud.adapters.form.kind', 'Kind')}\n </label>\n <select\n id=\"ada-kind\"\n value={form.kind}\n onChange={(e) => setField('kind', e.target.value as AdaAdapterKind)}\n className={fieldClass}\n >\n {(Object.keys(adapterKindLabels) as AdaAdapterKind[]).map((k) => (\n <option key={k} value={k}>\n {adapterKindLabels[k]}\n </option>\n ))}\n </select>\n </div>\n <div>\n <label htmlFor=\"ada-status\" className={labelClass}>\n {t('adaptercloud.adapters.form.status', 'Status')}\n </label>\n <select\n id=\"ada-status\"\n value={form.status}\n onChange={(e) => setField('status', e.target.value as AdaAdapterStatus)}\n className={fieldClass}\n >\n {(Object.keys(adapterStatusLabels) as AdaAdapterStatus[]).map((s) => (\n <option key={s} value={s}>\n {adapterStatusLabels[s]}\n </option>\n ))}\n </select>\n </div>\n </div>\n\n <div className=\"grid grid-cols-1 gap-5 sm:grid-cols-2\">\n <div>\n <label htmlFor=\"ada-vendor\" className={labelClass}>\n {t('adaptercloud.adapters.form.vendor', 'Vendor')}\n </label>\n <input\n id=\"ada-vendor\"\n type=\"text\"\n value={form.vendor}\n onChange={(e) => setField('vendor', e.target.value)}\n className={fieldClass}\n placeholder=\"Amazon Web Services\"\n />\n </div>\n <div>\n <label htmlFor=\"ada-version\" className={labelClass}>\n {t('adaptercloud.adapters.form.version', 'Version')}\n </label>\n <input\n id=\"ada-version\"\n type=\"text\"\n value={form.version}\n onChange={(e) => setField('version', e.target.value)}\n className={fieldClass}\n placeholder=\"1.0.0\"\n />\n </div>\n </div>\n\n <div>\n <label htmlFor=\"ada-description\" className={labelClass}>\n {t('adaptercloud.adapters.form.description', 'Description')}\n </label>\n <textarea\n id=\"ada-description\"\n value={form.description}\n onChange={(e) => setField('description', e.target.value)}\n rows={3}\n className={fieldClass}\n placeholder={t(\n 'adaptercloud.adapters.form.descriptionPlaceholder',\n 'What this adapter connects to and how.'\n )}\n />\n </div>\n\n <div>\n <label htmlFor=\"ada-capabilities\" className={labelClass}>\n {t('adaptercloud.adapters.form.capabilities', 'Capabilities')}\n </label>\n <input\n id=\"ada-capabilities\"\n type=\"text\"\n value={form.capabilities}\n onChange={(e) => setField('capabilities', e.target.value)}\n className={fieldClass}\n placeholder=\"discovery, cost, reconcile\"\n />\n <p className=\"mt-1 text-xs text-text-secondary\">\n {t('adaptercloud.common.commaSeparated', 'Comma-separated.')}\n </p>\n </div>\n\n <div>\n <label htmlFor=\"ada-tags\" className={labelClass}>\n {t('adaptercloud.common.tags', 'Tags')}\n </label>\n <input\n id=\"ada-tags\"\n type=\"text\"\n value={form.tags}\n onChange={(e) => setField('tags', e.target.value)}\n className={fieldClass}\n placeholder=\"production, cloud\"\n />\n <p className=\"mt-1 text-xs text-text-secondary\">\n {t('adaptercloud.common.commaSeparated', 'Comma-separated.')}\n </p>\n </div>\n\n {error && (\n <p className=\"rounded-lg bg-status-error-bg-subtle px-3 py-2 text-sm text-status-error-text\">\n {error}\n </p>\n )}\n\n <div className=\"flex justify-end gap-3 pt-2\">\n <button\n type=\"submit\"\n disabled={isPending}\n className=\"inline-flex items-center gap-2 rounded-lg bg-action-primary-bg px-5 py-2 text-sm font-medium text-action-primary-text transition-colors hover:opacity-90 disabled:opacity-50\"\n >\n {isPending ? (\n <Loader2 className=\"h-4 w-4 animate-spin\" />\n ) : (\n <Save className=\"h-4 w-4\" />\n )}\n {isEdit\n ? t('adaptercloud.common.saveChanges', 'Save changes')\n : t('adaptercloud.adapters.create', 'Create adapter')}\n </button>\n </div>\n </GlassCard>\n </form>\n </DataState>\n </PageLayout>\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"AdapterFormPage.js","names":[],"sources":["../../../../src/adaptercloud/pages/adapters/AdapterFormPage.tsx"],"sourcesContent":["/**\n * Adapter Create / Edit Form Page\n * @module adaptercloud/pages/adapters\n *\n * Creates a new adapter (`adaCreateAdapter`) or edits an existing one\n * (`adaUpdateAdapter`). The presence of a route `:id` switches to edit mode and\n * pre-fills from `adaAdapter`.\n */\n\nimport { useCallback, useEffect, useState } from 'react';\nimport { useParams } from 'react-router-dom';\nimport { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';\nimport { ArrowLeft, Loader2, Save } from 'lucide-react';\nimport { GlassCard } from '@burdenoff/fe-libs/ui';\nimport { PageLayout } from '../../components/PageLayout';\nimport { DataState } from '../../components/DataState';\nimport { useAdapterCloud } from '../../context/AdapterCloudContext';\nimport { useAdapter, useCreateAdapter, useUpdateAdapter } from '../../hooks/useAdapterCloudApi';\nimport { adapterKindLabels, adapterStatusLabels } from '../../utils/domain';\nimport type { AdaAdapterKind, AdaAdapterStatus } from '../../types';\n\nconst slugify = (value: string): string =>\n value\n .toLowerCase()\n .trim()\n .replace(/[^a-z0-9]+/g, '-')\n .replace(/^-+|-+$/g, '');\n\nconst parseList = (value: string): string[] =>\n value\n .split(',')\n .map((s) => s.trim())\n .filter(Boolean);\n\ninterface FormState {\n name: string;\n slug: string;\n kind: AdaAdapterKind;\n status: AdaAdapterStatus;\n vendor: string;\n version: string;\n description: string;\n capabilities: string;\n tags: string;\n}\n\nconst EMPTY: FormState = {\n name: '',\n slug: '',\n kind: 'CUSTOM',\n status: 'DRAFT',\n vendor: '',\n version: '1.0.0',\n description: '',\n capabilities: '',\n tags: '',\n};\n\nexport function AdapterFormPage() {\n const { id } = useParams<{ id: string }>();\n const isEdit = Boolean(id);\n const { navigateTo } = useAdapterCloud();\n const { t: translate } = useI18n();\n const t = useCallback(\n (key: string, fallback: string): string => {\n const value = translate(key);\n return value === key ? fallback : value;\n },\n [translate]\n );\n\n const existing = useAdapter(isEdit ? id : undefined);\n const create = useCreateAdapter();\n const update = useUpdateAdapter();\n\n const [form, setForm] = useState<FormState>(EMPTY);\n const [slugTouched, setSlugTouched] = useState(false);\n const [error, setError] = useState<string | null>(null);\n\n // Pre-fill in edit mode\n useEffect(() => {\n const a = existing.data;\n if (isEdit && a) {\n setForm({\n name: a.name,\n slug: a.slug,\n kind: a.kind,\n status: a.status,\n vendor: a.vendor ?? '',\n version: a.version,\n description: a.description ?? '',\n capabilities: a.capabilities.join(', '),\n tags: a.tags.join(', '),\n });\n setSlugTouched(true);\n }\n }, [isEdit, existing.data]);\n\n const setField = useCallback(<K extends keyof FormState>(key: K, value: FormState[K]) => {\n setForm((prev) => ({ ...prev, [key]: value }));\n }, []);\n\n const onNameChange = useCallback(\n (value: string) => {\n setForm((prev) => ({\n ...prev,\n name: value,\n slug: slugTouched ? prev.slug : slugify(value),\n }));\n },\n [slugTouched]\n );\n\n const isPending = create.isPending || update.isPending;\n\n const handleSubmit = useCallback(\n (e: React.FormEvent) => {\n e.preventDefault();\n setError(null);\n\n if (!form.name.trim()) {\n setError(t('adaptercloud.adapters.form.nameRequired', 'Name is required.'));\n return;\n }\n if (!isEdit && !form.slug.trim()) {\n setError(t('adaptercloud.adapters.form.slugRequired', 'Slug is required.'));\n return;\n }\n\n const common = {\n name: form.name.trim(),\n kind: form.kind,\n status: form.status,\n vendor: form.vendor.trim() || undefined,\n version: form.version.trim() || undefined,\n description: form.description.trim() || undefined,\n capabilities: parseList(form.capabilities),\n tags: parseList(form.tags),\n };\n\n const onError = (err: unknown) =>\n setError(err instanceof Error ? err.message : 'Failed to save adapter.');\n\n if (isEdit && id) {\n update.mutate(\n { id, input: common },\n { onSuccess: (a) => navigateTo(`/adapters/${a.id}`), onError }\n );\n } else {\n create.mutate(\n { ...common, slug: form.slug.trim() },\n { onSuccess: (a) => navigateTo(`/adapters/${a.id}`), onError }\n );\n }\n },\n [create, update, form, id, isEdit, navigateTo, t]\n );\n\n const fieldClass =\n 'w-full rounded-lg border border-border-subtle bg-bg-surface px-3 py-2 text-sm text-text-primary placeholder:text-text-secondary focus:outline-none focus:ring-2 focus:ring-[var(--color-focus-ring)]';\n const labelClass = 'mb-1.5 block text-sm font-medium text-text-primary';\n\n return (\n <PageLayout\n title={\n isEdit\n ? t('adaptercloud.adapters.editTitle', 'Edit Adapter')\n : t('adaptercloud.adapters.newTitle', 'New Adapter')\n }\n description={t(\n 'adaptercloud.adapters.formSubtitle',\n 'Define how AdapterCloud connects to a class of system.'\n )}\n actions={\n <button\n type=\"button\"\n onClick={() => navigateTo(isEdit && id ? `/adapters/${id}` : '/adapters')}\n className=\"inline-flex items-center gap-2 rounded-lg border border-border-subtle bg-bg-surface px-3 py-2 text-sm font-medium text-text-primary transition-colors hover:bg-accent\"\n >\n <ArrowLeft className=\"h-4 w-4\" />\n {t('adaptercloud.common.cancel', 'Cancel')}\n </button>\n }\n >\n <DataState\n isLoading={isEdit && existing.isLoading}\n isError={isEdit && existing.isError}\n isEmpty={isEdit && !existing.isLoading && !existing.data}\n error={existing.error}\n onRetry={() => void existing.refetch()}\n emptyTitle={t('adaptercloud.adapters.notFound', 'Adapter not found')}\n >\n <form onSubmit={handleSubmit} className=\"max-w-2xl\">\n <GlassCard className=\"space-y-5 p-6\">\n <div className=\"grid grid-cols-1 gap-5 sm:grid-cols-2\">\n <div>\n <label htmlFor=\"ada-name\" className={labelClass}>\n {t('adaptercloud.adapters.form.name', 'Name')} *\n </label>\n <input\n id=\"ada-name\"\n type=\"text\"\n value={form.name}\n onChange={(e) => onNameChange(e.target.value)}\n className={fieldClass}\n placeholder=\"AWS Production\"\n />\n </div>\n <div>\n <label htmlFor=\"ada-slug\" className={labelClass}>\n {t('adaptercloud.adapters.form.slug', 'Slug')} {isEdit ? '' : '*'}\n </label>\n <input\n id=\"ada-slug\"\n type=\"text\"\n value={form.slug}\n disabled={isEdit}\n onChange={(e) => {\n setSlugTouched(true);\n setField('slug', slugify(e.target.value));\n }}\n className={`${fieldClass} disabled:cursor-not-allowed disabled:opacity-60`}\n placeholder=\"aws-production\"\n />\n </div>\n </div>\n\n <div className=\"grid grid-cols-1 gap-5 sm:grid-cols-2\">\n <div>\n <label htmlFor=\"ada-kind\" className={labelClass}>\n {t('adaptercloud.adapters.form.kind', 'Kind')}\n </label>\n <select\n id=\"ada-kind\"\n value={form.kind}\n onChange={(e) => setField('kind', e.target.value as AdaAdapterKind)}\n className={fieldClass}\n >\n {(Object.keys(adapterKindLabels) as AdaAdapterKind[]).map((k) => (\n <option key={k} value={k}>\n {adapterKindLabels[k]}\n </option>\n ))}\n </select>\n </div>\n <div>\n <label htmlFor=\"ada-status\" className={labelClass}>\n {t('adaptercloud.adapters.form.status', 'Status')}\n </label>\n <select\n id=\"ada-status\"\n value={form.status}\n onChange={(e) => setField('status', e.target.value as AdaAdapterStatus)}\n className={fieldClass}\n >\n {(Object.keys(adapterStatusLabels) as AdaAdapterStatus[]).map((s) => (\n <option key={s} value={s}>\n {adapterStatusLabels[s]}\n </option>\n ))}\n </select>\n </div>\n </div>\n\n <div className=\"grid grid-cols-1 gap-5 sm:grid-cols-2\">\n <div>\n <label htmlFor=\"ada-vendor\" className={labelClass}>\n {t('adaptercloud.adapters.form.vendor', 'Vendor')}\n </label>\n <input\n id=\"ada-vendor\"\n type=\"text\"\n value={form.vendor}\n onChange={(e) => setField('vendor', e.target.value)}\n className={fieldClass}\n placeholder=\"Amazon Web Services\"\n />\n </div>\n <div>\n <label htmlFor=\"ada-version\" className={labelClass}>\n {t('adaptercloud.adapters.form.version', 'Version')}\n </label>\n <input\n id=\"ada-version\"\n type=\"text\"\n value={form.version}\n onChange={(e) => setField('version', e.target.value)}\n className={fieldClass}\n placeholder=\"1.0.0\"\n />\n </div>\n </div>\n\n <div>\n <label htmlFor=\"ada-description\" className={labelClass}>\n {t('adaptercloud.adapters.form.description', 'Description')}\n </label>\n <textarea\n id=\"ada-description\"\n value={form.description}\n onChange={(e) => setField('description', e.target.value)}\n rows={3}\n className={fieldClass}\n placeholder={t(\n 'adaptercloud.adapters.form.descriptionPlaceholder',\n 'What this adapter connects to and how.'\n )}\n />\n </div>\n\n <div>\n <label htmlFor=\"ada-capabilities\" className={labelClass}>\n {t('adaptercloud.adapters.form.capabilities', 'Capabilities')}\n </label>\n <input\n id=\"ada-capabilities\"\n type=\"text\"\n value={form.capabilities}\n onChange={(e) => setField('capabilities', e.target.value)}\n className={fieldClass}\n placeholder=\"discovery, cost, reconcile\"\n />\n <p className=\"mt-1 text-xs text-text-secondary\">\n {t('adaptercloud.common.commaSeparated', 'Comma-separated.')}\n </p>\n </div>\n\n <div>\n <label htmlFor=\"ada-tags\" className={labelClass}>\n {t('adaptercloud.common.tags', 'Tags')}\n </label>\n <input\n id=\"ada-tags\"\n type=\"text\"\n value={form.tags}\n onChange={(e) => setField('tags', e.target.value)}\n className={fieldClass}\n placeholder=\"production, cloud\"\n />\n <p className=\"mt-1 text-xs text-text-secondary\">\n {t('adaptercloud.common.commaSeparated', 'Comma-separated.')}\n </p>\n </div>\n\n {error && (\n <p className=\"rounded-lg bg-status-error-bg-subtle px-3 py-2 text-sm text-status-error-text\">\n {error}\n </p>\n )}\n\n <div className=\"flex justify-end gap-3 pt-2\">\n <button\n type=\"submit\"\n disabled={isPending}\n className=\"inline-flex items-center gap-2 rounded-lg bg-action-primary-bg px-5 py-2 text-sm font-medium text-action-primary-text transition-colors hover:opacity-90 disabled:opacity-50\"\n >\n {isPending ? (\n <Loader2 className=\"h-4 w-4 animate-spin\" />\n ) : (\n <Save className=\"h-4 w-4\" />\n )}\n {isEdit\n ? t('adaptercloud.common.saveChanges', 'Save changes')\n : t('adaptercloud.adapters.create', 'Create adapter')}\n </button>\n </div>\n </GlassCard>\n </form>\n </DataState>\n </PageLayout>\n );\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ConnectionDetailPage.js","names":[],"sources":["../../../../src/adaptercloud/pages/connections/ConnectionDetailPage.tsx"],"sourcesContent":["/**\n * Connection Detail Page\n * @module adaptercloud/pages/connections\n *\n * One connection (`adaConnection`) — its adapter, health, config, and the\n * resources it has discovered. Supports the validated health transition\n * (`adaSetConnectionStatus`), disable (`adaDisableConnection`), and starting a\n * sync run (`adaStartSyncRun`) against it.\n */\n\nimport { useCallback, useState } from 'react';\nimport { useParams } from 'react-router-dom';\nimport { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';\nimport { ArrowLeft, Ban, Pencil, Play, RefreshCw, Server } from 'lucide-react';\nimport { GlassCard } from '@burdenoff/fe-libs/ui';\nimport { PageLayout } from '../../components/PageLayout';\nimport { StatusBadge } from '../../components/StatusBadge';\nimport { DataState } from '../../components/DataState';\nimport { useAdapterCloud } from '../../context/AdapterCloudContext';\nimport {\n useConnection,\n useDisableConnection,\n useSetConnectionStatus,\n useStartSyncRun,\n} from '../../hooks/useAdapterCloudApi';\nimport {\n authMethodLabels,\n connectionStatusLabels,\n resourceStateLabels,\n syncKindLabels,\n} from '../../utils/domain';\nimport { formatDate } from '../../utils/formatters';\nimport { formatMoney } from '../../utils/domain';\nimport type { AdaConnectionStatus, AdaSyncKind } from '../../types';\n\nconst TRANSITIONABLE: AdaConnectionStatus[] = ['CONNECTED', 'DEGRADED', 'ERROR', 'PENDING'];\nconst SYNC_KINDS: AdaSyncKind[] = ['DISCOVERY', 'RECONCILE', 'COST_INGEST', 'FULL'];\n\nexport function ConnectionDetailPage() {\n const { id } = useParams<{ id: string }>();\n const { navigateTo } = useAdapterCloud();\n const { t: translate } = useI18n();\n const t = useCallback(\n (key: string, fallback: string): string => {\n const value = translate(key);\n return value === key ? fallback : value;\n },\n [translate]\n );\n\n const query = useConnection(id);\n const setStatus = useSetConnectionStatus();\n const disable = useDisableConnection();\n const startRun = useStartSyncRun();\n const conn = query.data;\n\n const [syncKind, setSyncKind] = useState<AdaSyncKind>('DISCOVERY');\n const [actionError, setActionError] = useState<string | null>(null);\n\n const onError = useCallback(\n (err: unknown) =>\n setActionError(err instanceof Error ? err.message : 'Action failed. Please try again.'),\n []\n );\n\n const handleSetStatus = useCallback(\n (newStatus: AdaConnectionStatus) => {\n if (!id) return;\n setActionError(null);\n setStatus.mutate({ id, status: newStatus }, { onError });\n },\n [id, setStatus, onError]\n );\n\n const handleDisable = useCallback(() => {\n if (!id) return;\n setActionError(null);\n disable.mutate(id, { onError });\n }, [id, disable, onError]);\n\n const handleStartRun = useCallback(() => {\n if (!id) return;\n setActionError(null);\n startRun.mutate(\n { connectionId: id, kind: syncKind },\n { onSuccess: () => navigateTo('/sync-runs'), onError }\n );\n }, [id, syncKind, startRun, navigateTo, onError]);\n\n return (\n <PageLayout\n title={conn?.name ?? t('adaptercloud.connections.detailTitle', 'Connection')}\n description={conn?.adapter?.name}\n actions={\n <div className=\"flex items-center gap-2\">\n <button\n type=\"button\"\n onClick={() => navigateTo('/connections')}\n className=\"inline-flex items-center gap-2 rounded-lg border border-border-subtle bg-bg-surface px-3 py-2 text-sm font-medium text-text-primary transition-colors hover:bg-accent\"\n >\n <ArrowLeft className=\"h-4 w-4\" />\n {t('adaptercloud.common.back', 'Back')}\n </button>\n {conn && (\n <button\n type=\"button\"\n onClick={() => navigateTo(`/connections/${conn.id}/edit`)}\n className=\"inline-flex items-center gap-2 rounded-lg border border-border-subtle bg-bg-surface px-3 py-2 text-sm font-medium text-text-primary transition-colors hover:bg-accent\"\n >\n <Pencil className=\"h-4 w-4\" />\n {t('adaptercloud.common.edit', 'Edit')}\n </button>\n )}\n </div>\n }\n >\n <DataState\n isLoading={query.isLoading}\n isError={query.isError}\n isEmpty={!query.isLoading && !conn}\n error={query.error}\n onRetry={() => void query.refetch()}\n emptyTitle={t('adaptercloud.connections.notFound', 'Connection not found')}\n >\n {conn && (\n <div className=\"space-y-6\">\n {actionError && (\n <p className=\"rounded-lg bg-status-error-bg-subtle px-3 py-2 text-sm text-status-error-text\">\n {actionError}\n </p>\n )}\n\n {/* Health + actions */}\n <GlassCard className=\"p-6\">\n <div className=\"mb-4 flex flex-wrap items-center gap-3\">\n <StatusBadge status={conn.status} />\n <span className=\"inline-flex items-center rounded-full bg-bg-sunken px-2.5 py-0.5 text-xs font-medium text-text-secondary\">\n {authMethodLabels[conn.authMethod]}\n </span>\n </div>\n\n <dl className=\"grid grid-cols-2 gap-4 text-sm sm:grid-cols-4\">\n <div>\n <dt className=\"text-text-secondary\">\n {t('adaptercloud.connections.region', 'Region')}\n </dt>\n <dd className=\"mt-0.5 font-medium text-text-primary\">{conn.region ?? '—'}</dd>\n </div>\n <div>\n <dt className=\"text-text-secondary\">\n {t('adaptercloud.connections.environment', 'Environment')}\n </dt>\n <dd className=\"mt-0.5 font-medium text-text-primary\">{conn.environment ?? '—'}</dd>\n </div>\n <div>\n <dt className=\"text-text-secondary\">\n {t('adaptercloud.connections.resourceCount', 'Resources')}\n </dt>\n <dd className=\"mt-0.5 font-medium tabular-nums text-text-primary\">\n {conn.resourceCount}\n </dd>\n </div>\n <div>\n <dt className=\"text-text-secondary\">\n {t('adaptercloud.connections.lastSynced', 'Last synced')}\n </dt>\n <dd className=\"mt-0.5 font-medium text-text-primary\">\n {conn.lastSyncedAt ? formatDate(conn.lastSyncedAt) : '—'}\n </dd>\n </div>\n </dl>\n\n {conn.lastError && (\n <p className=\"mt-4 rounded-lg bg-status-error-bg-subtle px-3 py-2 text-sm text-status-error-text\">\n {conn.lastError}\n </p>\n )}\n\n {/* Health transitions */}\n <div className=\"mt-5 border-t border-border-subtle pt-5\">\n <h3 className=\"mb-3 text-sm font-semibold text-text-primary\">\n {t('adaptercloud.connections.setHealth', 'Set health')}\n </h3>\n <div className=\"flex flex-wrap gap-2\">\n {TRANSITIONABLE.map((s) => (\n <button\n key={s}\n type=\"button\"\n disabled={setStatus.isPending || conn.status === s}\n onClick={() => handleSetStatus(s)}\n className=\"rounded-lg border border-border-subtle px-3 py-1.5 text-xs font-medium text-text-primary transition-colors hover:bg-accent disabled:cursor-not-allowed disabled:opacity-40\"\n >\n {connectionStatusLabels[s]}\n </button>\n ))}\n {conn.status !== 'DISABLED' && (\n <button\n type=\"button\"\n disabled={disable.isPending}\n onClick={handleDisable}\n className=\"inline-flex items-center gap-1.5 rounded-lg border border-border-subtle px-3 py-1.5 text-xs font-medium text-status-error-text transition-colors hover:bg-status-error-bg-subtle disabled:opacity-40\"\n >\n <Ban className=\"h-3.5 w-3.5\" />\n {t('adaptercloud.connections.disable', 'Disable')}\n </button>\n )}\n </div>\n </div>\n\n {/* Start sync run */}\n <div className=\"mt-5 border-t border-border-subtle pt-5\">\n <h3 className=\"mb-3 flex items-center gap-2 text-sm font-semibold text-text-primary\">\n <RefreshCw className=\"h-4 w-4\" />\n {t('adaptercloud.connections.startSync', 'Start a sync run')}\n </h3>\n <div className=\"flex flex-wrap items-center gap-3\">\n <select\n value={syncKind}\n onChange={(e) => setSyncKind(e.target.value as AdaSyncKind)}\n className=\"rounded-lg border border-border-subtle bg-bg-surface px-3 py-2 text-sm text-text-primary focus:outline-none focus:ring-2 focus:ring-[var(--color-focus-ring)]\"\n >\n {SYNC_KINDS.map((k) => (\n <option key={k} value={k}>\n {syncKindLabels[k]}\n </option>\n ))}\n </select>\n <button\n type=\"button\"\n disabled={startRun.isPending}\n onClick={handleStartRun}\n className=\"inline-flex items-center gap-2 rounded-lg bg-action-primary-bg px-4 py-2 text-sm font-medium text-action-primary-text transition-colors hover:opacity-90 disabled:opacity-50\"\n >\n <Play className=\"h-4 w-4\" />\n {t('adaptercloud.connections.run', 'Run')}\n </button>\n </div>\n </div>\n </GlassCard>\n\n {/* Config */}\n {conn.config != null && (\n <GlassCard className=\"p-6\">\n <h3 className=\"mb-3 text-sm font-semibold text-text-primary\">\n {t('adaptercloud.connections.config', 'Configuration')}\n </h3>\n <pre className=\"overflow-auto rounded-lg bg-bg-sunken p-4 text-xs text-text-secondary\">\n {JSON.stringify(conn.config, null, 2)}\n </pre>\n </GlassCard>\n )}\n\n {/* Resources */}\n <section>\n <div className=\"mb-3 flex items-center justify-between\">\n <h3 className=\"text-base font-semibold text-text-primary\">\n {t('adaptercloud.connections.discoveredResources', 'Discovered Resources')}\n </h3>\n <button\n type=\"button\"\n onClick={() => navigateTo(`/resources?connectionId=${conn.id}`)}\n className=\"text-sm font-medium text-primary hover:underline\"\n >\n {t('adaptercloud.common.viewAll', 'View all')}\n </button>\n </div>\n {conn.resources && conn.resources.length > 0 ? (\n <div className=\"divide-y divide-border rounded-xl border border-border-subtle bg-bg-surface\">\n {conn.resources.slice(0, 10).map((r) => (\n <button\n key={r.id}\n type=\"button\"\n onClick={() => navigateTo(`/resources/${r.id}`)}\n className=\"flex w-full items-center gap-3 px-4 py-3 text-left transition-colors hover:bg-accent/50\"\n >\n <Server className=\"h-4 w-4 shrink-0 text-text-secondary\" />\n <div className=\"min-w-0 flex-1\">\n <p className=\"truncate text-sm font-medium text-text-primary\">{r.name}</p>\n <p className=\"mt-0.5 text-xs text-text-secondary\">\n {r.resourceType}\n {r.region ? ` · ${r.region}` : ''}\n </p>\n </div>\n <span className=\"hidden text-xs text-text-secondary sm:inline\">\n {formatMoney(r.monthlyCost)}\n </span>\n <StatusBadge status={r.state} label={resourceStateLabels[r.state]} />\n </button>\n ))}\n </div>\n ) : (\n <p className=\"rounded-xl border border-border-subtle bg-bg-surface px-4 py-6 text-center text-sm text-text-secondary\">\n {t(\n 'adaptercloud.connections.noResources',\n 'No resources discovered yet. Start a discovery sync run.'\n )}\n </p>\n )}\n </section>\n </div>\n )}\n </DataState>\n </PageLayout>\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"ConnectionDetailPage.js","names":[],"sources":["../../../../src/adaptercloud/pages/connections/ConnectionDetailPage.tsx"],"sourcesContent":["/**\n * Connection Detail Page\n * @module adaptercloud/pages/connections\n *\n * One connection (`adaConnection`) — its adapter, health, config, and the\n * resources it has discovered. Supports the validated health transition\n * (`adaSetConnectionStatus`), disable (`adaDisableConnection`), and starting a\n * sync run (`adaStartSyncRun`) against it.\n */\n\nimport { useCallback, useState } from 'react';\nimport { useParams } from 'react-router-dom';\nimport { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';\nimport { ArrowLeft, Ban, Pencil, Play, RefreshCw, Server } from 'lucide-react';\nimport { GlassCard } from '@burdenoff/fe-libs/ui';\nimport { PageLayout } from '../../components/PageLayout';\nimport { StatusBadge } from '../../components/StatusBadge';\nimport { DataState } from '../../components/DataState';\nimport { useAdapterCloud } from '../../context/AdapterCloudContext';\nimport {\n useConnection,\n useDisableConnection,\n useSetConnectionStatus,\n useStartSyncRun,\n} from '../../hooks/useAdapterCloudApi';\nimport {\n authMethodLabels,\n connectionStatusLabels,\n resourceStateLabels,\n syncKindLabels,\n} from '../../utils/domain';\nimport { formatDate } from '../../utils/formatters';\nimport { formatMoney } from '../../utils/domain';\nimport type { AdaConnectionStatus, AdaSyncKind } from '../../types';\n\nconst TRANSITIONABLE: AdaConnectionStatus[] = ['CONNECTED', 'DEGRADED', 'ERROR', 'PENDING'];\nconst SYNC_KINDS: AdaSyncKind[] = ['DISCOVERY', 'RECONCILE', 'COST_INGEST', 'FULL'];\n\nexport function ConnectionDetailPage() {\n const { id } = useParams<{ id: string }>();\n const { navigateTo } = useAdapterCloud();\n const { t: translate } = useI18n();\n const t = useCallback(\n (key: string, fallback: string): string => {\n const value = translate(key);\n return value === key ? fallback : value;\n },\n [translate]\n );\n\n const query = useConnection(id);\n const setStatus = useSetConnectionStatus();\n const disable = useDisableConnection();\n const startRun = useStartSyncRun();\n const conn = query.data;\n\n const [syncKind, setSyncKind] = useState<AdaSyncKind>('DISCOVERY');\n const [actionError, setActionError] = useState<string | null>(null);\n\n const onError = useCallback(\n (err: unknown) =>\n setActionError(err instanceof Error ? err.message : 'Action failed. Please try again.'),\n []\n );\n\n const handleSetStatus = useCallback(\n (newStatus: AdaConnectionStatus) => {\n if (!id) return;\n setActionError(null);\n setStatus.mutate({ id, status: newStatus }, { onError });\n },\n [id, setStatus, onError]\n );\n\n const handleDisable = useCallback(() => {\n if (!id) return;\n setActionError(null);\n disable.mutate(id, { onError });\n }, [id, disable, onError]);\n\n const handleStartRun = useCallback(() => {\n if (!id) return;\n setActionError(null);\n startRun.mutate(\n { connectionId: id, kind: syncKind },\n { onSuccess: () => navigateTo('/sync-runs'), onError }\n );\n }, [id, syncKind, startRun, navigateTo, onError]);\n\n return (\n <PageLayout\n title={conn?.name ?? t('adaptercloud.connections.detailTitle', 'Connection')}\n description={conn?.adapter?.name}\n actions={\n <div className=\"flex items-center gap-2\">\n <button\n type=\"button\"\n onClick={() => navigateTo('/connections')}\n className=\"inline-flex items-center gap-2 rounded-lg border border-border-subtle bg-bg-surface px-3 py-2 text-sm font-medium text-text-primary transition-colors hover:bg-accent\"\n >\n <ArrowLeft className=\"h-4 w-4\" />\n {t('adaptercloud.common.back', 'Back')}\n </button>\n {conn && (\n <button\n type=\"button\"\n onClick={() => navigateTo(`/connections/${conn.id}/edit`)}\n className=\"inline-flex items-center gap-2 rounded-lg border border-border-subtle bg-bg-surface px-3 py-2 text-sm font-medium text-text-primary transition-colors hover:bg-accent\"\n >\n <Pencil className=\"h-4 w-4\" />\n {t('adaptercloud.common.edit', 'Edit')}\n </button>\n )}\n </div>\n }\n >\n <DataState\n isLoading={query.isLoading}\n isError={query.isError}\n isEmpty={!query.isLoading && !conn}\n error={query.error}\n onRetry={() => void query.refetch()}\n emptyTitle={t('adaptercloud.connections.notFound', 'Connection not found')}\n >\n {conn && (\n <div className=\"space-y-6\">\n {actionError && (\n <p className=\"rounded-lg bg-status-error-bg-subtle px-3 py-2 text-sm text-status-error-text\">\n {actionError}\n </p>\n )}\n\n {/* Health + actions */}\n <GlassCard className=\"p-6\">\n <div className=\"mb-4 flex flex-wrap items-center gap-3\">\n <StatusBadge status={conn.status} />\n <span className=\"inline-flex items-center rounded-full bg-bg-sunken px-2.5 py-0.5 text-xs font-medium text-text-secondary\">\n {authMethodLabels[conn.authMethod]}\n </span>\n </div>\n\n <dl className=\"grid grid-cols-2 gap-4 text-sm sm:grid-cols-4\">\n <div>\n <dt className=\"text-text-secondary\">\n {t('adaptercloud.connections.region', 'Region')}\n </dt>\n <dd className=\"mt-0.5 font-medium text-text-primary\">{conn.region ?? '—'}</dd>\n </div>\n <div>\n <dt className=\"text-text-secondary\">\n {t('adaptercloud.connections.environment', 'Environment')}\n </dt>\n <dd className=\"mt-0.5 font-medium text-text-primary\">\n {conn.environment ?? '—'}\n </dd>\n </div>\n <div>\n <dt className=\"text-text-secondary\">\n {t('adaptercloud.connections.resourceCount', 'Resources')}\n </dt>\n <dd className=\"mt-0.5 font-medium tabular-nums text-text-primary\">\n {conn.resourceCount}\n </dd>\n </div>\n <div>\n <dt className=\"text-text-secondary\">\n {t('adaptercloud.connections.lastSynced', 'Last synced')}\n </dt>\n <dd className=\"mt-0.5 font-medium text-text-primary\">\n {conn.lastSyncedAt ? formatDate(conn.lastSyncedAt) : '—'}\n </dd>\n </div>\n </dl>\n\n {conn.lastError && (\n <p className=\"mt-4 rounded-lg bg-status-error-bg-subtle px-3 py-2 text-sm text-status-error-text\">\n {conn.lastError}\n </p>\n )}\n\n {/* Health transitions */}\n <div className=\"mt-5 border-t border-border-subtle pt-5\">\n <h3 className=\"mb-3 text-sm font-semibold text-text-primary\">\n {t('adaptercloud.connections.setHealth', 'Set health')}\n </h3>\n <div className=\"flex flex-wrap gap-2\">\n {TRANSITIONABLE.map((s) => (\n <button\n key={s}\n type=\"button\"\n disabled={setStatus.isPending || conn.status === s}\n onClick={() => handleSetStatus(s)}\n className=\"rounded-lg border border-border-subtle px-3 py-1.5 text-xs font-medium text-text-primary transition-colors hover:bg-accent disabled:cursor-not-allowed disabled:opacity-40\"\n >\n {connectionStatusLabels[s]}\n </button>\n ))}\n {conn.status !== 'DISABLED' && (\n <button\n type=\"button\"\n disabled={disable.isPending}\n onClick={handleDisable}\n className=\"inline-flex items-center gap-1.5 rounded-lg border border-border-subtle px-3 py-1.5 text-xs font-medium text-status-error-text transition-colors hover:bg-status-error-bg-subtle disabled:opacity-40\"\n >\n <Ban className=\"h-3.5 w-3.5\" />\n {t('adaptercloud.connections.disable', 'Disable')}\n </button>\n )}\n </div>\n </div>\n\n {/* Start sync run */}\n <div className=\"mt-5 border-t border-border-subtle pt-5\">\n <h3 className=\"mb-3 flex items-center gap-2 text-sm font-semibold text-text-primary\">\n <RefreshCw className=\"h-4 w-4\" />\n {t('adaptercloud.connections.startSync', 'Start a sync run')}\n </h3>\n <div className=\"flex flex-wrap items-center gap-3\">\n <select\n value={syncKind}\n onChange={(e) => setSyncKind(e.target.value as AdaSyncKind)}\n className=\"rounded-lg border border-border-subtle bg-bg-surface px-3 py-2 text-sm text-text-primary focus:outline-none focus:ring-2 focus:ring-[var(--color-focus-ring)]\"\n >\n {SYNC_KINDS.map((k) => (\n <option key={k} value={k}>\n {syncKindLabels[k]}\n </option>\n ))}\n </select>\n <button\n type=\"button\"\n disabled={startRun.isPending}\n onClick={handleStartRun}\n className=\"inline-flex items-center gap-2 rounded-lg bg-action-primary-bg px-4 py-2 text-sm font-medium text-action-primary-text transition-colors hover:opacity-90 disabled:opacity-50\"\n >\n <Play className=\"h-4 w-4\" />\n {t('adaptercloud.connections.run', 'Run')}\n </button>\n </div>\n </div>\n </GlassCard>\n\n {/* Config */}\n {conn.config != null && (\n <GlassCard className=\"p-6\">\n <h3 className=\"mb-3 text-sm font-semibold text-text-primary\">\n {t('adaptercloud.connections.config', 'Configuration')}\n </h3>\n <pre className=\"overflow-auto rounded-lg bg-bg-sunken p-4 text-xs text-text-secondary\">\n {JSON.stringify(conn.config, null, 2)}\n </pre>\n </GlassCard>\n )}\n\n {/* Resources */}\n <section>\n <div className=\"mb-3 flex items-center justify-between\">\n <h3 className=\"text-base font-semibold text-text-primary\">\n {t('adaptercloud.connections.discoveredResources', 'Discovered Resources')}\n </h3>\n <button\n type=\"button\"\n onClick={() => navigateTo(`/resources?connectionId=${conn.id}`)}\n className=\"text-sm font-medium text-primary hover:underline\"\n >\n {t('adaptercloud.common.viewAll', 'View all')}\n </button>\n </div>\n {conn.resources && conn.resources.length > 0 ? (\n <div className=\"divide-y divide-border rounded-xl border border-border-subtle bg-bg-surface\">\n {conn.resources.slice(0, 10).map((r) => (\n <button\n key={r.id}\n type=\"button\"\n onClick={() => navigateTo(`/resources/${r.id}`)}\n className=\"flex w-full items-center gap-3 px-4 py-3 text-left transition-colors hover:bg-accent/50\"\n >\n <Server className=\"h-4 w-4 shrink-0 text-text-secondary\" />\n <div className=\"min-w-0 flex-1\">\n <p className=\"truncate text-sm font-medium text-text-primary\">{r.name}</p>\n <p className=\"mt-0.5 text-xs text-text-secondary\">\n {r.resourceType}\n {r.region ? ` · ${r.region}` : ''}\n </p>\n </div>\n <span className=\"hidden text-xs text-text-secondary sm:inline\">\n {formatMoney(r.monthlyCost)}\n </span>\n <StatusBadge status={r.state} label={resourceStateLabels[r.state]} />\n </button>\n ))}\n </div>\n ) : (\n <p className=\"rounded-xl border border-border-subtle bg-bg-surface px-4 py-6 text-center text-sm text-text-secondary\">\n {t(\n 'adaptercloud.connections.noResources',\n 'No resources discovered yet. Start a discovery sync run.'\n )}\n </p>\n )}\n </section>\n </div>\n )}\n </DataState>\n </PageLayout>\n );\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"CostsPage.js","names":[],"sources":["../../../../src/adaptercloud/pages/costs/CostsPage.tsx"],"sourcesContent":["/**\n * Costs (FinOps) Page\n * @module adaptercloud/pages/costs\n *\n * Workspace FinOps: the cost summary by service (`adaCostSummary`) plus the raw\n * cost-record ledger (`adaListCostRecords`), optionally filtered by service.\n */\n\nimport { useState, useCallback, useMemo } from 'react';\nimport { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';\nimport { DollarSign, Search, Receipt } from 'lucide-react';\nimport {\n EmphasisPanel,\n GlassCard,\n ResponsiveTable,\n type ResponsiveTableColumn,\n} from '@burdenoff/fe-libs/ui';\nimport { PageLayout } from '../../components/PageLayout';\nimport { MetricsCard } from '../../components/MetricsCard';\nimport { DataState } from '../../components/DataState';\nimport { useCostSummary, useCostRecords } from '../../hooks/useAdapterCloudApi';\nimport { formatMoney } from '../../utils/domain';\nimport { formatDate } from '../../utils/formatters';\nimport type { AdaCostRecord } from '../../types';\n\nexport function CostsPage() {\n const { t: translate } = useI18n();\n const t = useCallback(\n (key: string, fallback: string): string => {\n const value = translate(key);\n return value === key ? fallback : value;\n },\n [translate]\n );\n\n const [serviceInput, setServiceInput] = useState('');\n const [appliedService, setAppliedService] = useState('');\n\n const summary = useCostSummary();\n const records = useCostRecords({ service: appliedService || undefined });\n\n const currency = summary.data?.currency ?? 'USD';\n\n const sortedLines = useMemo(\n () => [...(summary.data?.lines ?? [])].sort((a, b) => b.total - a.total),\n [summary.data]\n );\n\n const onSearch = useCallback(\n (e: React.FormEvent) => {\n e.preventDefault();\n setAppliedService(serviceInput.trim());\n },\n [serviceInput]\n );\n\n const recordColumns: ResponsiveTableColumn<AdaCostRecord>[] = [\n {\n key: 'service',\n header: t('adaptercloud.costs.service', 'Service'),\n priority: 'primary',\n cell: (r) => <span className=\"font-medium text-text-primary\">{r.service}</span>,\n },\n {\n key: 'amount',\n header: t('adaptercloud.costs.amount', 'Amount'),\n priority: 'primary',\n cell: (r) => (\n <span className=\"tabular-nums text-text-primary\">{formatMoney(r.amount, r.currency)}</span>\n ),\n },\n {\n key: 'period',\n header: t('adaptercloud.costs.period', 'Period'),\n priority: 'secondary',\n cell: (r) => (\n <span className=\"text-sm text-text-secondary\">\n {formatDate(r.periodStart, { hour: undefined, minute: undefined })} –{' '}\n {formatDate(r.periodEnd, { hour: undefined, minute: undefined })}\n </span>\n ),\n },\n {\n key: 'scope',\n header: t('adaptercloud.costs.scope', 'Scope'),\n priority: 'hidden-on-mobile',\n cell: (r) => (\n <span className=\"text-xs text-text-secondary\">\n {r.resourceId\n ? t('adaptercloud.costs.resourceScope', 'Resource')\n : r.connectionId\n ? t('adaptercloud.costs.connectionScope', 'Connection')\n : t('adaptercloud.costs.workspaceScope', 'Workspace')}\n </span>\n ),\n },\n ];\n\n return (\n <PageLayout\n title={t('adaptercloud.costs.title', 'FinOps')}\n description={t(\n 'adaptercloud.costs.subtitle',\n 'Spend attributed to the same resources you operate — cost and topology share one model.'\n )}\n >\n <DataState\n isLoading={summary.isLoading}\n isError={summary.isError}\n error={summary.error}\n onRetry={() => void summary.refetch()}\n >\n {/* Total + by-service */}\n <EmphasisPanel className=\"mb-8\">\n <div className=\"mb-4 flex items-center justify-between\">\n <h2 className=\"text-lg font-semibold text-text-primary\">\n {t('adaptercloud.costs.summary', 'Cost Summary')}\n </h2>\n </div>\n <div className=\"grid grid-cols-2 gap-4 lg:grid-cols-4\">\n <MetricsCard\n featured\n icon={<DollarSign className=\"h-5 w-5\" />}\n value={summary.data ? formatMoney(summary.data.total, currency) : '—'}\n label={t('adaptercloud.costs.totalSpend', 'Total Spend')}\n />\n {sortedLines.slice(0, 3).map((line) => (\n <MetricsCard\n key={line.service}\n icon={<Receipt className=\"h-5 w-5\" />}\n value={formatMoney(line.total, currency)}\n label={line.service}\n />\n ))}\n </div>\n </EmphasisPanel>\n\n {/* Full by-service breakdown */}\n {sortedLines.length > 0 && (\n <section className=\"mb-8\">\n <h2 className=\"mb-4 text-lg font-semibold text-text-primary\">\n {t('adaptercloud.costs.byService', 'Spend by Service')}\n </h2>\n <GlassCard className=\"p-5\">\n <ul className=\"space-y-3\">\n {sortedLines.map((line) => {\n const pct = summary.data?.total\n ? Math.round((line.total / summary.data.total) * 100)\n : 0;\n return (\n <li key={line.service}>\n <div className=\"mb-1 flex items-center justify-between text-sm\">\n <span className=\"truncate text-text-primary\">{line.service}</span>\n <span className=\"tabular-nums text-text-secondary\">\n {formatMoney(line.total, currency)} · {pct}%\n </span>\n </div>\n <div className=\"h-1.5 w-full overflow-hidden rounded-full bg-bg-sunken\">\n <div\n className=\"h-full rounded-full bg-action-primary-bg\"\n style={{ width: `${Math.max(pct, 2)}%` }}\n />\n </div>\n </li>\n );\n })}\n </ul>\n </GlassCard>\n </section>\n )}\n </DataState>\n\n {/* Cost records ledger */}\n <section>\n <div className=\"mb-4 flex flex-wrap items-center justify-between gap-3\">\n <h2 className=\"text-lg font-semibold text-text-primary\">\n {t('adaptercloud.costs.records', 'Cost Records')}\n </h2>\n <form onSubmit={onSearch} className=\"relative\">\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={serviceInput}\n onChange={(e) => setServiceInput(e.target.value)}\n placeholder={t('adaptercloud.costs.filterService', 'Filter by service…')}\n className=\"w-56 rounded-lg border border-border-subtle bg-bg-surface py-2 pl-9 pr-3 text-sm text-text-primary placeholder:text-text-secondary focus:outline-none focus:ring-2 focus:ring-[var(--color-focus-ring)]\"\n />\n </form>\n </div>\n\n <DataState\n isLoading={records.isLoading}\n isError={records.isError}\n isEmpty={(records.data ?? []).length === 0}\n error={records.error}\n onRetry={() => void records.refetch()}\n emptyTitle={t('adaptercloud.costs.emptyTitle', 'No cost records')}\n emptyDescription={t(\n 'adaptercloud.costs.emptyDescription',\n 'Run a cost-ingest sync against a connection to populate the ledger.'\n )}\n emptyIcon={<Receipt className=\"h-10 w-10\" />}\n >\n <ResponsiveTable\n data={records.data ?? []}\n columns={recordColumns}\n rowKey={(r) => r.id}\n />\n </DataState>\n </section>\n </PageLayout>\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"CostsPage.js","names":[],"sources":["../../../../src/adaptercloud/pages/costs/CostsPage.tsx"],"sourcesContent":["/**\n * Costs (FinOps) Page\n * @module adaptercloud/pages/costs\n *\n * Workspace FinOps: the cost summary by service (`adaCostSummary`) plus the raw\n * cost-record ledger (`adaListCostRecords`), optionally filtered by service.\n */\n\nimport { useState, useCallback, useMemo } from 'react';\nimport { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';\nimport { DollarSign, Search, Receipt } from 'lucide-react';\nimport {\n EmphasisPanel,\n GlassCard,\n ResponsiveTable,\n type ResponsiveTableColumn,\n} from '@burdenoff/fe-libs/ui';\nimport { PageLayout } from '../../components/PageLayout';\nimport { MetricsCard } from '../../components/MetricsCard';\nimport { DataState } from '../../components/DataState';\nimport { useCostSummary, useCostRecords } from '../../hooks/useAdapterCloudApi';\nimport { formatMoney } from '../../utils/domain';\nimport { formatDate } from '../../utils/formatters';\nimport type { AdaCostRecord } from '../../types';\n\nexport function CostsPage() {\n const { t: translate } = useI18n();\n const t = useCallback(\n (key: string, fallback: string): string => {\n const value = translate(key);\n return value === key ? fallback : value;\n },\n [translate]\n );\n\n const [serviceInput, setServiceInput] = useState('');\n const [appliedService, setAppliedService] = useState('');\n\n const summary = useCostSummary();\n const records = useCostRecords({ service: appliedService || undefined });\n\n const currency = summary.data?.currency ?? 'USD';\n\n const sortedLines = useMemo(\n () => [...(summary.data?.lines ?? [])].sort((a, b) => b.total - a.total),\n [summary.data]\n );\n\n const onSearch = useCallback(\n (e: React.FormEvent) => {\n e.preventDefault();\n setAppliedService(serviceInput.trim());\n },\n [serviceInput]\n );\n\n const recordColumns: ResponsiveTableColumn<AdaCostRecord>[] = [\n {\n key: 'service',\n header: t('adaptercloud.costs.service', 'Service'),\n priority: 'primary',\n cell: (r) => <span className=\"font-medium text-text-primary\">{r.service}</span>,\n },\n {\n key: 'amount',\n header: t('adaptercloud.costs.amount', 'Amount'),\n priority: 'primary',\n cell: (r) => (\n <span className=\"tabular-nums text-text-primary\">{formatMoney(r.amount, r.currency)}</span>\n ),\n },\n {\n key: 'period',\n header: t('adaptercloud.costs.period', 'Period'),\n priority: 'secondary',\n cell: (r) => (\n <span className=\"text-sm text-text-secondary\">\n {formatDate(r.periodStart, { hour: undefined, minute: undefined })} –{' '}\n {formatDate(r.periodEnd, { hour: undefined, minute: undefined })}\n </span>\n ),\n },\n {\n key: 'scope',\n header: t('adaptercloud.costs.scope', 'Scope'),\n priority: 'hidden-on-mobile',\n cell: (r) => (\n <span className=\"text-xs text-text-secondary\">\n {r.resourceId\n ? t('adaptercloud.costs.resourceScope', 'Resource')\n : r.connectionId\n ? t('adaptercloud.costs.connectionScope', 'Connection')\n : t('adaptercloud.costs.workspaceScope', 'Workspace')}\n </span>\n ),\n },\n ];\n\n return (\n <PageLayout\n title={t('adaptercloud.costs.title', 'FinOps')}\n description={t(\n 'adaptercloud.costs.subtitle',\n 'Spend attributed to the same resources you operate — cost and topology share one model.'\n )}\n >\n <DataState\n isLoading={summary.isLoading}\n isError={summary.isError}\n error={summary.error}\n onRetry={() => void summary.refetch()}\n >\n {/* Total + by-service */}\n <EmphasisPanel className=\"mb-8\">\n <div className=\"mb-4 flex items-center justify-between\">\n <h2 className=\"text-lg font-semibold text-text-primary\">\n {t('adaptercloud.costs.summary', 'Cost Summary')}\n </h2>\n </div>\n <div className=\"grid grid-cols-2 gap-4 lg:grid-cols-4\">\n <MetricsCard\n featured\n icon={<DollarSign className=\"h-5 w-5\" />}\n value={summary.data ? formatMoney(summary.data.total, currency) : '—'}\n label={t('adaptercloud.costs.totalSpend', 'Total Spend')}\n />\n {sortedLines.slice(0, 3).map((line) => (\n <MetricsCard\n key={line.service}\n icon={<Receipt className=\"h-5 w-5\" />}\n value={formatMoney(line.total, currency)}\n label={line.service}\n />\n ))}\n </div>\n </EmphasisPanel>\n\n {/* Full by-service breakdown */}\n {sortedLines.length > 0 && (\n <section className=\"mb-8\">\n <h2 className=\"mb-4 text-lg font-semibold text-text-primary\">\n {t('adaptercloud.costs.byService', 'Spend by Service')}\n </h2>\n <GlassCard className=\"p-5\">\n <ul className=\"space-y-3\">\n {sortedLines.map((line) => {\n const pct = summary.data?.total\n ? Math.round((line.total / summary.data.total) * 100)\n : 0;\n return (\n <li key={line.service}>\n <div className=\"mb-1 flex items-center justify-between text-sm\">\n <span className=\"truncate text-text-primary\">{line.service}</span>\n <span className=\"tabular-nums text-text-secondary\">\n {formatMoney(line.total, currency)} · {pct}%\n </span>\n </div>\n <div className=\"h-1.5 w-full overflow-hidden rounded-full bg-bg-sunken\">\n <div\n className=\"h-full rounded-full bg-action-primary-bg\"\n style={{ width: `${Math.max(pct, 2)}%` }}\n />\n </div>\n </li>\n );\n })}\n </ul>\n </GlassCard>\n </section>\n )}\n </DataState>\n\n {/* Cost records ledger */}\n <section>\n <div className=\"mb-4 flex flex-wrap items-center justify-between gap-3\">\n <h2 className=\"text-lg font-semibold text-text-primary\">\n {t('adaptercloud.costs.records', 'Cost Records')}\n </h2>\n <form onSubmit={onSearch} className=\"relative\">\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={serviceInput}\n onChange={(e) => setServiceInput(e.target.value)}\n placeholder={t('adaptercloud.costs.filterService', 'Filter by service…')}\n className=\"w-56 rounded-lg border border-border-subtle bg-bg-surface py-2 pl-9 pr-3 text-sm text-text-primary placeholder:text-text-secondary focus:outline-none focus:ring-2 focus:ring-[var(--color-focus-ring)]\"\n />\n </form>\n </div>\n\n <DataState\n isLoading={records.isLoading}\n isError={records.isError}\n isEmpty={(records.data ?? []).length === 0}\n error={records.error}\n onRetry={() => void records.refetch()}\n emptyTitle={t('adaptercloud.costs.emptyTitle', 'No cost records')}\n emptyDescription={t(\n 'adaptercloud.costs.emptyDescription',\n 'Run a cost-ingest sync against a connection to populate the ledger.'\n )}\n emptyIcon={<Receipt className=\"h-10 w-10\" />}\n >\n <ResponsiveTable data={records.data ?? []} columns={recordColumns} rowKey={(r) => r.id} />\n </DataState>\n </section>\n </PageLayout>\n );\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"PoliciesListPage.js","names":[],"sources":["../../../../src/adaptercloud/pages/policies/PoliciesListPage.tsx"],"sourcesContent":["/**\n * Policies List Page\n * @module adaptercloud/pages/policies\n *\n * Governance rules evaluated against the inventory (`adaListPolicies`). Filters\n * by kind and active flag; offset pagination.\n */\n\nimport { useState, useCallback } from 'react';\nimport { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';\nimport { ScrollText, Plus, ChevronLeft, ChevronRight } from 'lucide-react';\nimport { GlassCard } from '@burdenoff/fe-libs/ui';\nimport { PageLayout } from '../../components/PageLayout';\nimport { SeverityBadge } from '../../components/SeverityBadge';\nimport { DataState } from '../../components/DataState';\nimport { useAdapterCloud } from '../../context/AdapterCloudContext';\nimport { usePolicies } from '../../hooks/useAdapterCloudApi';\nimport { policyKindLabels, policyEnforcementLabels } from '../../utils/domain';\nimport { formatRelativeTime } from '../../utils/formatters';\nimport { cn } from '../../utils/cn';\nimport type { AdaPolicyKind } from '../../types';\n\nconst PAGE_SIZE = 20;\n\ntype ActiveFilter = 'all' | 'active' | 'inactive';\n\nexport function PoliciesListPage() {\n const { navigateTo } = useAdapterCloud();\n const { t: translate } = useI18n();\n const t = useCallback(\n (key: string, fallback: string): string => {\n const value = translate(key);\n return value === key ? fallback : value;\n },\n [translate]\n );\n\n const [kind, setKind] = useState<AdaPolicyKind | ''>('');\n const [active, setActive] = useState<ActiveFilter>('all');\n const [offset, setOffset] = useState(0);\n\n const query = usePolicies({\n kind: kind || undefined,\n isActive: active === 'all' ? undefined : active === 'active',\n pagination: { limit: PAGE_SIZE, offset },\n });\n\n const items = query.data?.items ?? [];\n const hasMore = query.data?.pageInfo.hasMore ?? false;\n\n return (\n <PageLayout\n title={t('adaptercloud.policies.title', 'Policies')}\n description={t(\n 'adaptercloud.policies.subtitle',\n 'Governance rules that evaluate the inventory — guardrail, tagging, security, cost, compliance and naming.'\n )}\n actions={\n <button\n type=\"button\"\n onClick={() => navigateTo('/policies/new')}\n className=\"inline-flex items-center gap-2 rounded-lg bg-action-primary-bg px-4 py-2 text-sm font-medium text-action-primary-text transition-colors hover:opacity-90\"\n >\n <Plus className=\"h-4 w-4\" />\n {t('adaptercloud.policies.new', 'New policy')}\n </button>\n }\n >\n <div className=\"mb-6 flex flex-wrap gap-3\">\n <select\n value={kind}\n onChange={(e) => {\n setKind(e.target.value as AdaPolicyKind | '');\n setOffset(0);\n }}\n className=\"rounded-lg border border-border-subtle bg-bg-surface px-3 py-2 text-sm text-text-primary focus:outline-none focus:ring-2 focus:ring-[var(--color-focus-ring)]\"\n >\n <option value=\"\">{t('adaptercloud.policies.allKinds', 'All kinds')}</option>\n {(Object.keys(policyKindLabels) as AdaPolicyKind[]).map((k) => (\n <option key={k} value={k}>\n {policyKindLabels[k]}\n </option>\n ))}\n </select>\n <select\n value={active}\n onChange={(e) => {\n setActive(e.target.value as ActiveFilter);\n setOffset(0);\n }}\n className=\"rounded-lg border border-border-subtle bg-bg-surface px-3 py-2 text-sm text-text-primary focus:outline-none focus:ring-2 focus:ring-[var(--color-focus-ring)]\"\n >\n <option value=\"all\">{t('adaptercloud.policies.allStates', 'All')}</option>\n <option value=\"active\">{t('adaptercloud.policies.active', 'Active')}</option>\n <option value=\"inactive\">{t('adaptercloud.policies.inactive', 'Inactive')}</option>\n </select>\n </div>\n\n <DataState\n isLoading={query.isLoading}\n isError={query.isError}\n isEmpty={items.length === 0}\n error={query.error}\n onRetry={() => void query.refetch()}\n emptyTitle={t('adaptercloud.policies.emptyTitle', 'No policies yet')}\n emptyDescription={t(\n 'adaptercloud.policies.emptyDescription',\n 'Author your first governance policy to put compliance into the system.'\n )}\n emptyIcon={<ScrollText className=\"h-10 w-10\" />}\n emptyAction={\n <button\n type=\"button\"\n onClick={() => navigateTo('/policies/new')}\n className=\"rounded-lg bg-action-primary-bg px-4 py-2 text-sm font-medium text-action-primary-text transition-colors hover:opacity-90\"\n >\n {t('adaptercloud.policies.new', 'New policy')}\n </button>\n }\n >\n <div className=\"grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3\">\n {items.map((policy) => (\n <GlassCard\n key={policy.id}\n role=\"button\"\n tabIndex={0}\n onClick={() => navigateTo(`/policies/${policy.id}`)}\n onKeyDown={(e) => {\n if (e.key === 'Enter' || e.key === ' ') {\n e.preventDefault();\n navigateTo(`/policies/${policy.id}`);\n }\n }}\n className=\"cursor-pointer p-5 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--color-focus-ring)]\"\n >\n <div className=\"flex items-start justify-between gap-2\">\n <div className=\"min-w-0\">\n <h3 className=\"truncate text-base font-semibold text-text-primary\">\n {policy.name}\n </h3>\n <p className=\"mt-0.5 text-xs text-text-secondary\">\n {policyKindLabels[policy.kind]}\n </p>\n </div>\n <SeverityBadge severity={policy.severity} />\n </div>\n\n {policy.description && (\n <p className=\"mt-3 line-clamp-2 text-sm text-text-secondary\">{policy.description}</p>\n )}\n\n <div className=\"mt-4 flex items-center gap-2\">\n <span className=\"inline-flex items-center rounded-full bg-bg-sunken px-2 py-0.5 text-xs text-text-secondary\">\n {policyEnforcementLabels[policy.enforcement]}\n </span>\n <span\n className={cn(\n 'inline-flex items-center gap-1 rounded-full px-2 py-0.5 text-xs font-medium',\n policy.isActive\n ? 'bg-status-success-bg-subtle text-status-success-text'\n : 'bg-bg-sunken text-text-secondary'\n )}\n >\n <span\n className={cn(\n 'h-1.5 w-1.5 rounded-full',\n policy.isActive ? 'bg-status-success-text' : 'bg-text-secondary'\n )}\n />\n {policy.isActive\n ? t('adaptercloud.policies.active', 'Active')\n : t('adaptercloud.policies.inactive', 'Inactive')}\n </span>\n </div>\n\n <p className=\"mt-4 text-xs text-text-secondary\">\n {t('adaptercloud.common.updated', 'Updated')} {formatRelativeTime(policy.updatedAt)}\n </p>\n </GlassCard>\n ))}\n </div>\n\n {(offset > 0 || hasMore) && (\n <div className=\"mt-6 flex items-center justify-between\">\n <button\n type=\"button\"\n disabled={offset === 0}\n onClick={() => setOffset((o) => Math.max(0, o - PAGE_SIZE))}\n className=\"inline-flex items-center gap-1 rounded-lg border border-border-subtle px-3 py-2 text-sm font-medium text-text-primary transition-colors hover:bg-accent disabled:cursor-not-allowed disabled:opacity-50\"\n >\n <ChevronLeft className=\"h-4 w-4\" />\n {t('adaptercloud.common.prev', 'Previous')}\n </button>\n <span className=\"text-sm text-text-secondary\">\n {t('adaptercloud.common.showing', 'Showing')} {offset + 1}–{offset + items.length}\n </span>\n <button\n type=\"button\"\n disabled={!hasMore}\n onClick={() => setOffset((o) => o + PAGE_SIZE)}\n className=\"inline-flex items-center gap-1 rounded-lg border border-border-subtle px-3 py-2 text-sm font-medium text-text-primary transition-colors hover:bg-accent disabled:cursor-not-allowed disabled:opacity-50\"\n >\n {t('adaptercloud.common.next', 'Next')}\n <ChevronRight className=\"h-4 w-4\" />\n </button>\n </div>\n )}\n </DataState>\n </PageLayout>\n );\n}\n"],"mappings":";;;;;;;;;;;;;;AAsBA,IAAM,IAAY;AAIlB,SAAgB,IAAmB;CACjC,IAAM,EAAE,kBAAe,GAAiB,EAClC,EAAE,GAAG,MAAc,GAAS,EAC5B,IAAI,GACP,GAAa,MAA6B;EACzC,IAAM,IAAQ,EAAU,EAAI;AAC5B,SAAO,MAAU,IAAM,IAAW;IAEpC,CAAC,EAAU,CACZ,EAEK,CAAC,GAAM,KAAW,EAA6B,GAAG,EAClD,CAAC,GAAQ,KAAa,EAAuB,MAAM,EACnD,CAAC,GAAQ,KAAa,EAAS,EAAE,EAEjC,IAAQ,EAAY;EACxB,MAAM,KAAQ,KAAA;EACd,UAAU,MAAW,QAAQ,KAAA,IAAY,MAAW;EACpD,YAAY;GAAE,OAAO;GAAW;GAAQ;EACzC,CAAC,EAEI,IAAQ,EAAM,MAAM,SAAS,EAAE,EAC/B,IAAU,EAAM,MAAM,SAAS,WAAW;AAEhD,QACE,kBAAC,GAAD;EACE,OAAO,EAAE,+BAA+B,WAAW;EACnD,aAAa,EACX,kCACA,4GACD;EACD,SACE,kBAAC,UAAD;GACE,MAAK;GACL,eAAe,EAAW,gBAAgB;GAC1C,WAAU;aAHZ,CAKE,kBAAC,GAAD,EAAM,WAAU,WAAY,CAAA,EAC3B,EAAE,6BAA6B,aAAa,CACtC;;YAdb,CAiBE,kBAAC,OAAD;GAAK,WAAU;aAAf,CACE,kBAAC,UAAD;IACE,OAAO;IACP,WAAW,MAAM;AAEf,KADA,EAAQ,EAAE,OAAO,MAA4B,EAC7C,EAAU,EAAE;;IAEd,WAAU;cANZ,CAQE,kBAAC,UAAD;KAAQ,OAAM;eAAI,EAAE,kCAAkC,YAAY;KAAU,CAAA,EAC1E,OAAO,KAAK,EAAiB,CAAqB,KAAK,MACvD,kBAAC,UAAD;KAAgB,OAAO;eACpB,EAAiB;KACX,EAFI,EAEJ,CACT,CACK;OACT,kBAAC,UAAD;IACE,OAAO;IACP,WAAW,MAAM;AAEf,KADA,EAAU,EAAE,OAAO,MAAsB,EACzC,EAAU,EAAE;;IAEd,WAAU;cANZ;KAQE,kBAAC,UAAD;MAAQ,OAAM;gBAAO,EAAE,mCAAmC,MAAM;MAAU,CAAA;KAC1E,kBAAC,UAAD;MAAQ,OAAM;gBAAU,EAAE,gCAAgC,SAAS;MAAU,CAAA;KAC7E,kBAAC,UAAD;MAAQ,OAAM;gBAAY,EAAE,kCAAkC,WAAW;MAAU,CAAA;KAC5E;MACL;MAEN,kBAAC,GAAD;GACE,WAAW,EAAM;GACjB,SAAS,EAAM;GACf,SAAS,EAAM,WAAW;GAC1B,OAAO,EAAM;GACb,eAAe,KAAK,EAAM,SAAS;GACnC,YAAY,EAAE,oCAAoC,kBAAkB;GACpE,kBAAkB,EAChB,0CACA,yEACD;GACD,WAAW,kBAAC,GAAD,EAAY,WAAU,aAAc,CAAA;GAC/C,aACE,kBAAC,UAAD;IACE,MAAK;IACL,eAAe,EAAW,gBAAgB;IAC1C,WAAU;cAET,EAAE,6BAA6B,aAAa;IACtC,CAAA;aAnBb,CAsBE,kBAAC,OAAD;IAAK,WAAU;cACZ,EAAM,KAAK,MACV,kBAAC,GAAD;KAEE,MAAK;KACL,UAAU;KACV,eAAe,EAAW,aAAa,EAAO,KAAK;KACnD,YAAY,MAAM;AAChB,OAAI,EAAE,QAAQ,WAAW,EAAE,QAAQ,SACjC,EAAE,gBAAgB,EAClB,EAAW,aAAa,EAAO,KAAK;;KAGxC,WAAU;eAXZ;MAaE,kBAAC,OAAD;OAAK,WAAU;iBAAf,CACE,kBAAC,OAAD;QAAK,WAAU;kBAAf,CACE,kBAAC,MAAD;SAAI,WAAU;mBACX,EAAO;SACL,CAAA,EACL,kBAAC,KAAD;SAAG,WAAU;mBACV,EAAiB,EAAO;SACvB,CAAA,CACA;WACN,kBAAC,GAAD,EAAe,UAAU,EAAO,UAAY,CAAA,CACxC;;MAEL,EAAO,eACN,kBAAC,KAAD;OAAG,WAAU;iBAAiD,EAAO;OAAgB,CAAA;MAGvF,kBAAC,OAAD;OAAK,WAAU;iBAAf,CACE,kBAAC,QAAD;QAAM,WAAU;kBACb,EAAwB,EAAO;QAC3B,CAAA,EACP,kBAAC,QAAD;QACE,WAAW,EACT,+EACA,EAAO,WACH,yDACA,mCACL;kBANH,CAQE,kBAAC,QAAD,EACE,WAAW,EACT,4BACA,EAAO,WAAW,2BAA2B,oBAC9C,EACD,CAAA,EACD,EAAO,WACJ,EAAE,gCAAgC,SAAS,GAC3C,EAAE,kCAAkC,WAAW,CAC9C;UACH;;MAEN,kBAAC,KAAD;OAAG,WAAU;iBAAb;QACG,EAAE,+BAA+B,UAAU;QAAC;QAAE,EAAmB,EAAO,UAAU;QACjF;;MACM;OAvDL,EAAO,GAuDF,CACZ;IACE,CAAA,GAEJ,IAAS,KAAK,MACd,kBAAC,OAAD;IAAK,WAAU;cAAf;KACE,kBAAC,UAAD;MACE,MAAK;MACL,UAAU,MAAW;MACrB,eAAe,GAAW,MAAM,KAAK,IAAI,GAAG,IAAI,EAAU,CAAC;MAC3D,WAAU;gBAJZ,CAME,kBAAC,GAAD,EAAa,WAAU,WAAY,CAAA,EAClC,EAAE,4BAA4B,WAAW,CACnC;;KACT,kBAAC,QAAD;MAAM,WAAU;gBAAhB;OACG,EAAE,+BAA+B,UAAU;OAAC;OAAE,IAAS;OAAE;OAAE,IAAS,EAAM;OACtE;;KACP,kBAAC,UAAD;MACE,MAAK;MACL,UAAU,CAAC;MACX,eAAe,GAAW,MAAM,IAAI,EAAU;MAC9C,WAAU;gBAJZ,CAMG,EAAE,4BAA4B,OAAO,EACtC,kBAAC,GAAD,EAAc,WAAU,WAAY,CAAA,CAC7B;;KACL;MAEE;KACD"}
|
|
1
|
+
{"version":3,"file":"PoliciesListPage.js","names":[],"sources":["../../../../src/adaptercloud/pages/policies/PoliciesListPage.tsx"],"sourcesContent":["/**\n * Policies List Page\n * @module adaptercloud/pages/policies\n *\n * Governance rules evaluated against the inventory (`adaListPolicies`). Filters\n * by kind and active flag; offset pagination.\n */\n\nimport { useState, useCallback } from 'react';\nimport { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';\nimport { ScrollText, Plus, ChevronLeft, ChevronRight } from 'lucide-react';\nimport { GlassCard } from '@burdenoff/fe-libs/ui';\nimport { PageLayout } from '../../components/PageLayout';\nimport { SeverityBadge } from '../../components/SeverityBadge';\nimport { DataState } from '../../components/DataState';\nimport { useAdapterCloud } from '../../context/AdapterCloudContext';\nimport { usePolicies } from '../../hooks/useAdapterCloudApi';\nimport { policyKindLabels, policyEnforcementLabels } from '../../utils/domain';\nimport { formatRelativeTime } from '../../utils/formatters';\nimport { cn } from '../../utils/cn';\nimport type { AdaPolicyKind } from '../../types';\n\nconst PAGE_SIZE = 20;\n\ntype ActiveFilter = 'all' | 'active' | 'inactive';\n\nexport function PoliciesListPage() {\n const { navigateTo } = useAdapterCloud();\n const { t: translate } = useI18n();\n const t = useCallback(\n (key: string, fallback: string): string => {\n const value = translate(key);\n return value === key ? fallback : value;\n },\n [translate]\n );\n\n const [kind, setKind] = useState<AdaPolicyKind | ''>('');\n const [active, setActive] = useState<ActiveFilter>('all');\n const [offset, setOffset] = useState(0);\n\n const query = usePolicies({\n kind: kind || undefined,\n isActive: active === 'all' ? undefined : active === 'active',\n pagination: { limit: PAGE_SIZE, offset },\n });\n\n const items = query.data?.items ?? [];\n const hasMore = query.data?.pageInfo.hasMore ?? false;\n\n return (\n <PageLayout\n title={t('adaptercloud.policies.title', 'Policies')}\n description={t(\n 'adaptercloud.policies.subtitle',\n 'Governance rules that evaluate the inventory — guardrail, tagging, security, cost, compliance and naming.'\n )}\n actions={\n <button\n type=\"button\"\n onClick={() => navigateTo('/policies/new')}\n className=\"inline-flex items-center gap-2 rounded-lg bg-action-primary-bg px-4 py-2 text-sm font-medium text-action-primary-text transition-colors hover:opacity-90\"\n >\n <Plus className=\"h-4 w-4\" />\n {t('adaptercloud.policies.new', 'New policy')}\n </button>\n }\n >\n <div className=\"mb-6 flex flex-wrap gap-3\">\n <select\n value={kind}\n onChange={(e) => {\n setKind(e.target.value as AdaPolicyKind | '');\n setOffset(0);\n }}\n className=\"rounded-lg border border-border-subtle bg-bg-surface px-3 py-2 text-sm text-text-primary focus:outline-none focus:ring-2 focus:ring-[var(--color-focus-ring)]\"\n >\n <option value=\"\">{t('adaptercloud.policies.allKinds', 'All kinds')}</option>\n {(Object.keys(policyKindLabels) as AdaPolicyKind[]).map((k) => (\n <option key={k} value={k}>\n {policyKindLabels[k]}\n </option>\n ))}\n </select>\n <select\n value={active}\n onChange={(e) => {\n setActive(e.target.value as ActiveFilter);\n setOffset(0);\n }}\n className=\"rounded-lg border border-border-subtle bg-bg-surface px-3 py-2 text-sm text-text-primary focus:outline-none focus:ring-2 focus:ring-[var(--color-focus-ring)]\"\n >\n <option value=\"all\">{t('adaptercloud.policies.allStates', 'All')}</option>\n <option value=\"active\">{t('adaptercloud.policies.active', 'Active')}</option>\n <option value=\"inactive\">{t('adaptercloud.policies.inactive', 'Inactive')}</option>\n </select>\n </div>\n\n <DataState\n isLoading={query.isLoading}\n isError={query.isError}\n isEmpty={items.length === 0}\n error={query.error}\n onRetry={() => void query.refetch()}\n emptyTitle={t('adaptercloud.policies.emptyTitle', 'No policies yet')}\n emptyDescription={t(\n 'adaptercloud.policies.emptyDescription',\n 'Author your first governance policy to put compliance into the system.'\n )}\n emptyIcon={<ScrollText className=\"h-10 w-10\" />}\n emptyAction={\n <button\n type=\"button\"\n onClick={() => navigateTo('/policies/new')}\n className=\"rounded-lg bg-action-primary-bg px-4 py-2 text-sm font-medium text-action-primary-text transition-colors hover:opacity-90\"\n >\n {t('adaptercloud.policies.new', 'New policy')}\n </button>\n }\n >\n <div className=\"grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3\">\n {items.map((policy) => (\n <GlassCard\n key={policy.id}\n role=\"button\"\n tabIndex={0}\n onClick={() => navigateTo(`/policies/${policy.id}`)}\n onKeyDown={(e) => {\n if (e.key === 'Enter' || e.key === ' ') {\n e.preventDefault();\n navigateTo(`/policies/${policy.id}`);\n }\n }}\n className=\"cursor-pointer p-5 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--color-focus-ring)]\"\n >\n <div className=\"flex items-start justify-between gap-2\">\n <div className=\"min-w-0\">\n <h3 className=\"truncate text-base font-semibold text-text-primary\">\n {policy.name}\n </h3>\n <p className=\"mt-0.5 text-xs text-text-secondary\">\n {policyKindLabels[policy.kind]}\n </p>\n </div>\n <SeverityBadge severity={policy.severity} />\n </div>\n\n {policy.description && (\n <p className=\"mt-3 line-clamp-2 text-sm text-text-secondary\">\n {policy.description}\n </p>\n )}\n\n <div className=\"mt-4 flex items-center gap-2\">\n <span className=\"inline-flex items-center rounded-full bg-bg-sunken px-2 py-0.5 text-xs text-text-secondary\">\n {policyEnforcementLabels[policy.enforcement]}\n </span>\n <span\n className={cn(\n 'inline-flex items-center gap-1 rounded-full px-2 py-0.5 text-xs font-medium',\n policy.isActive\n ? 'bg-status-success-bg-subtle text-status-success-text'\n : 'bg-bg-sunken text-text-secondary'\n )}\n >\n <span\n className={cn(\n 'h-1.5 w-1.5 rounded-full',\n policy.isActive ? 'bg-status-success-text' : 'bg-text-secondary'\n )}\n />\n {policy.isActive\n ? t('adaptercloud.policies.active', 'Active')\n : t('adaptercloud.policies.inactive', 'Inactive')}\n </span>\n </div>\n\n <p className=\"mt-4 text-xs text-text-secondary\">\n {t('adaptercloud.common.updated', 'Updated')} {formatRelativeTime(policy.updatedAt)}\n </p>\n </GlassCard>\n ))}\n </div>\n\n {(offset > 0 || hasMore) && (\n <div className=\"mt-6 flex items-center justify-between\">\n <button\n type=\"button\"\n disabled={offset === 0}\n onClick={() => setOffset((o) => Math.max(0, o - PAGE_SIZE))}\n className=\"inline-flex items-center gap-1 rounded-lg border border-border-subtle px-3 py-2 text-sm font-medium text-text-primary transition-colors hover:bg-accent disabled:cursor-not-allowed disabled:opacity-50\"\n >\n <ChevronLeft className=\"h-4 w-4\" />\n {t('adaptercloud.common.prev', 'Previous')}\n </button>\n <span className=\"text-sm text-text-secondary\">\n {t('adaptercloud.common.showing', 'Showing')} {offset + 1}–{offset + items.length}\n </span>\n <button\n type=\"button\"\n disabled={!hasMore}\n onClick={() => setOffset((o) => o + PAGE_SIZE)}\n className=\"inline-flex items-center gap-1 rounded-lg border border-border-subtle px-3 py-2 text-sm font-medium text-text-primary transition-colors hover:bg-accent disabled:cursor-not-allowed disabled:opacity-50\"\n >\n {t('adaptercloud.common.next', 'Next')}\n <ChevronRight className=\"h-4 w-4\" />\n </button>\n </div>\n )}\n </DataState>\n </PageLayout>\n );\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"PolicyDetailPage.js","names":[],"sources":["../../../../src/adaptercloud/pages/policies/PolicyDetailPage.tsx"],"sourcesContent":["/**\n * Policy Detail Page\n * @module adaptercloud/pages/policies\n *\n * One governance policy (`adaPolicy`) — its kind, enforcement, severity, JSON\n * expression, applies-to selector, and the violations it has produced. Supports\n * archiving via `adaArchivePolicy`.\n */\n\nimport { useCallback } from 'react';\nimport { useParams } from 'react-router-dom';\nimport { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';\nimport { ArrowLeft, Archive, AlertCircle } from 'lucide-react';\nimport { GlassCard } from '@burdenoff/fe-libs/ui';\nimport { PageLayout } from '../../components/PageLayout';\nimport { StatusBadge } from '../../components/StatusBadge';\nimport { SeverityBadge } from '../../components/SeverityBadge';\nimport { DataState } from '../../components/DataState';\nimport { useAdapterCloud } from '../../context/AdapterCloudContext';\nimport { usePolicy, useArchivePolicy } from '../../hooks/useAdapterCloudApi';\nimport { policyKindLabels, policyEnforcementLabels } from '../../utils/domain';\nimport { formatDate, formatRelativeTime } from '../../utils/formatters';\nimport { cn } from '../../utils/cn';\n\nexport function PolicyDetailPage() {\n const { id } = useParams<{ id: string }>();\n const { navigateTo } = useAdapterCloud();\n const { t: translate } = useI18n();\n const t = useCallback(\n (key: string, fallback: string): string => {\n const value = translate(key);\n return value === key ? fallback : value;\n },\n [translate]\n );\n\n const query = usePolicy(id);\n const archive = useArchivePolicy();\n const policy = query.data;\n\n const handleArchive = useCallback(() => {\n if (!id) return;\n archive.mutate(id, { onSuccess: () => navigateTo('/policies') });\n }, [archive, id, navigateTo]);\n\n return (\n <PageLayout\n title={policy?.name ?? t('adaptercloud.policies.detailTitle', 'Policy')}\n description={policy ? policyKindLabels[policy.kind] : undefined}\n actions={\n <div className=\"flex items-center gap-2\">\n <button\n type=\"button\"\n onClick={() => navigateTo('/policies')}\n className=\"inline-flex items-center gap-2 rounded-lg border border-border-subtle bg-bg-surface px-3 py-2 text-sm font-medium text-text-primary transition-colors hover:bg-accent\"\n >\n <ArrowLeft className=\"h-4 w-4\" />\n {t('adaptercloud.common.back', 'Back')}\n </button>\n {policy && (\n <button\n type=\"button\"\n onClick={handleArchive}\n disabled={archive.isPending}\n className=\"inline-flex items-center gap-2 rounded-lg border border-border-subtle bg-bg-surface px-3 py-2 text-sm font-medium text-status-error-text transition-colors hover:bg-status-error-bg-subtle disabled:opacity-50\"\n >\n <Archive className=\"h-4 w-4\" />\n {t('adaptercloud.common.archive', 'Archive')}\n </button>\n )}\n </div>\n }\n >\n <DataState\n isLoading={query.isLoading}\n isError={query.isError}\n isEmpty={!query.isLoading && !policy}\n error={query.error}\n onRetry={() => void query.refetch()}\n emptyTitle={t('adaptercloud.policies.notFound', 'Policy not found')}\n >\n {policy && (\n <div className=\"space-y-6\">\n <GlassCard className=\"p-6\">\n <div className=\"mb-4 flex flex-wrap items-center gap-3\">\n <SeverityBadge severity={policy.severity} />\n <span className=\"inline-flex items-center rounded-full bg-bg-sunken px-2.5 py-0.5 text-xs font-medium text-text-secondary\">\n {policyEnforcementLabels[policy.enforcement]}\n </span>\n <span\n className={cn(\n 'inline-flex items-center gap-1 rounded-full px-2.5 py-0.5 text-xs font-medium',\n policy.isActive\n ? 'bg-status-success-bg-subtle text-status-success-text'\n : 'bg-bg-sunken text-text-secondary'\n )}\n >\n {policy.isActive\n ? t('adaptercloud.policies.active', 'Active')\n : t('adaptercloud.policies.inactive', 'Inactive')}\n </span>\n </div>\n\n {policy.description && (\n <p className=\"mb-4 text-sm text-text-secondary\">{policy.description}</p>\n )}\n\n <dl className=\"grid grid-cols-2 gap-4 text-sm sm:grid-cols-3\">\n <div>\n <dt className=\"text-text-secondary\">{t('adaptercloud.policies.kind', 'Kind')}</dt>\n <dd className=\"mt-0.5 font-medium text-text-primary\">\n {policyKindLabels[policy.kind]}\n </dd>\n </div>\n <div>\n <dt className=\"text-text-secondary\">\n {t('adaptercloud.common.created', 'Created')}\n </dt>\n <dd className=\"mt-0.5 font-medium text-text-primary\">\n {formatDate(policy.createdAt)}\n </dd>\n </div>\n <div>\n <dt className=\"text-text-secondary\">\n {t('adaptercloud.common.updated', 'Updated')}\n </dt>\n <dd className=\"mt-0.5 font-medium text-text-primary\">\n {formatDate(policy.updatedAt)}\n </dd>\n </div>\n </dl>\n\n {policy.appliesTo.length > 0 && (\n <div className=\"mt-5\">\n <h3 className=\"mb-2 text-sm font-semibold text-text-primary\">\n {t('adaptercloud.policies.appliesTo', 'Applies To')}\n </h3>\n <div className=\"flex flex-wrap gap-1.5\">\n {policy.appliesTo.map((sel) => (\n <span\n key={sel}\n className=\"inline-flex items-center rounded-full bg-accent-blue-subtle px-2.5 py-0.5 font-mono text-xs text-accent-blue\"\n >\n {sel}\n </span>\n ))}\n </div>\n </div>\n )}\n\n {policy.tags.length > 0 && (\n <div className=\"mt-4\">\n <h3 className=\"mb-2 text-sm font-semibold text-text-primary\">\n {t('adaptercloud.common.tags', 'Tags')}\n </h3>\n <div className=\"flex flex-wrap gap-1.5\">\n {policy.tags.map((tag) => (\n <span\n key={tag}\n className=\"inline-flex items-center rounded-full bg-bg-sunken px-2.5 py-0.5 text-xs text-text-secondary\"\n >\n {tag}\n </span>\n ))}\n </div>\n </div>\n )}\n </GlassCard>\n\n {/* Expression */}\n <GlassCard className=\"p-6\">\n <h3 className=\"mb-3 text-sm font-semibold text-text-primary\">\n {t('adaptercloud.policies.expression', 'Expression')}\n </h3>\n <pre className=\"overflow-auto rounded-lg bg-bg-sunken p-4 text-xs text-text-secondary\">\n {JSON.stringify(policy.expression, null, 2)}\n </pre>\n </GlassCard>\n\n {/* Violations */}\n <section>\n <div className=\"mb-3 flex items-center justify-between\">\n <h3 className=\"text-base font-semibold text-text-primary\">\n {t('adaptercloud.policies.violations', 'Violations')}\n </h3>\n <button\n type=\"button\"\n onClick={() => navigateTo(`/violations?policyId=${policy.id}`)}\n className=\"text-sm font-medium text-primary hover:underline\"\n >\n {t('adaptercloud.common.viewAll', 'View all')}\n </button>\n </div>\n {policy.violations && policy.violations.length > 0 ? (\n <div className=\"divide-y divide-border rounded-xl border border-border-subtle bg-bg-surface\">\n {policy.violations.slice(0, 10).map((v) => (\n <button\n key={v.id}\n type=\"button\"\n onClick={() => navigateTo(`/violations?policyId=${policy.id}`)}\n className=\"flex w-full items-start gap-3 px-4 py-3 text-left transition-colors hover:bg-accent/50\"\n >\n <AlertCircle className=\"mt-0.5 h-4 w-4 shrink-0 text-status-error-text\" />\n <div className=\"min-w-0 flex-1\">\n <p className=\"truncate text-sm font-medium text-text-primary\">\n {v.message}\n </p>\n <p className=\"mt-0.5 text-xs text-text-secondary\">\n {formatRelativeTime(v.detectedAt)}\n </p>\n </div>\n <StatusBadge status={v.status} />\n </button>\n ))}\n </div>\n ) : (\n <p className=\"rounded-xl border border-border-subtle bg-bg-surface px-4 py-6 text-center text-sm text-text-secondary\">\n {t('adaptercloud.policies.noViolations', 'No violations recorded for this policy.')}\n </p>\n )}\n </section>\n </div>\n )}\n </DataState>\n </PageLayout>\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"PolicyDetailPage.js","names":[],"sources":["../../../../src/adaptercloud/pages/policies/PolicyDetailPage.tsx"],"sourcesContent":["/**\n * Policy Detail Page\n * @module adaptercloud/pages/policies\n *\n * One governance policy (`adaPolicy`) — its kind, enforcement, severity, JSON\n * expression, applies-to selector, and the violations it has produced. Supports\n * archiving via `adaArchivePolicy`.\n */\n\nimport { useCallback } from 'react';\nimport { useParams } from 'react-router-dom';\nimport { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';\nimport { ArrowLeft, Archive, AlertCircle } from 'lucide-react';\nimport { GlassCard } from '@burdenoff/fe-libs/ui';\nimport { PageLayout } from '../../components/PageLayout';\nimport { StatusBadge } from '../../components/StatusBadge';\nimport { SeverityBadge } from '../../components/SeverityBadge';\nimport { DataState } from '../../components/DataState';\nimport { useAdapterCloud } from '../../context/AdapterCloudContext';\nimport { usePolicy, useArchivePolicy } from '../../hooks/useAdapterCloudApi';\nimport { policyKindLabels, policyEnforcementLabels } from '../../utils/domain';\nimport { formatDate, formatRelativeTime } from '../../utils/formatters';\nimport { cn } from '../../utils/cn';\n\nexport function PolicyDetailPage() {\n const { id } = useParams<{ id: string }>();\n const { navigateTo } = useAdapterCloud();\n const { t: translate } = useI18n();\n const t = useCallback(\n (key: string, fallback: string): string => {\n const value = translate(key);\n return value === key ? fallback : value;\n },\n [translate]\n );\n\n const query = usePolicy(id);\n const archive = useArchivePolicy();\n const policy = query.data;\n\n const handleArchive = useCallback(() => {\n if (!id) return;\n archive.mutate(id, { onSuccess: () => navigateTo('/policies') });\n }, [archive, id, navigateTo]);\n\n return (\n <PageLayout\n title={policy?.name ?? t('adaptercloud.policies.detailTitle', 'Policy')}\n description={policy ? policyKindLabels[policy.kind] : undefined}\n actions={\n <div className=\"flex items-center gap-2\">\n <button\n type=\"button\"\n onClick={() => navigateTo('/policies')}\n className=\"inline-flex items-center gap-2 rounded-lg border border-border-subtle bg-bg-surface px-3 py-2 text-sm font-medium text-text-primary transition-colors hover:bg-accent\"\n >\n <ArrowLeft className=\"h-4 w-4\" />\n {t('adaptercloud.common.back', 'Back')}\n </button>\n {policy && (\n <button\n type=\"button\"\n onClick={handleArchive}\n disabled={archive.isPending}\n className=\"inline-flex items-center gap-2 rounded-lg border border-border-subtle bg-bg-surface px-3 py-2 text-sm font-medium text-status-error-text transition-colors hover:bg-status-error-bg-subtle disabled:opacity-50\"\n >\n <Archive className=\"h-4 w-4\" />\n {t('adaptercloud.common.archive', 'Archive')}\n </button>\n )}\n </div>\n }\n >\n <DataState\n isLoading={query.isLoading}\n isError={query.isError}\n isEmpty={!query.isLoading && !policy}\n error={query.error}\n onRetry={() => void query.refetch()}\n emptyTitle={t('adaptercloud.policies.notFound', 'Policy not found')}\n >\n {policy && (\n <div className=\"space-y-6\">\n <GlassCard className=\"p-6\">\n <div className=\"mb-4 flex flex-wrap items-center gap-3\">\n <SeverityBadge severity={policy.severity} />\n <span className=\"inline-flex items-center rounded-full bg-bg-sunken px-2.5 py-0.5 text-xs font-medium text-text-secondary\">\n {policyEnforcementLabels[policy.enforcement]}\n </span>\n <span\n className={cn(\n 'inline-flex items-center gap-1 rounded-full px-2.5 py-0.5 text-xs font-medium',\n policy.isActive\n ? 'bg-status-success-bg-subtle text-status-success-text'\n : 'bg-bg-sunken text-text-secondary'\n )}\n >\n {policy.isActive\n ? t('adaptercloud.policies.active', 'Active')\n : t('adaptercloud.policies.inactive', 'Inactive')}\n </span>\n </div>\n\n {policy.description && (\n <p className=\"mb-4 text-sm text-text-secondary\">{policy.description}</p>\n )}\n\n <dl className=\"grid grid-cols-2 gap-4 text-sm sm:grid-cols-3\">\n <div>\n <dt className=\"text-text-secondary\">{t('adaptercloud.policies.kind', 'Kind')}</dt>\n <dd className=\"mt-0.5 font-medium text-text-primary\">\n {policyKindLabels[policy.kind]}\n </dd>\n </div>\n <div>\n <dt className=\"text-text-secondary\">\n {t('adaptercloud.common.created', 'Created')}\n </dt>\n <dd className=\"mt-0.5 font-medium text-text-primary\">\n {formatDate(policy.createdAt)}\n </dd>\n </div>\n <div>\n <dt className=\"text-text-secondary\">\n {t('adaptercloud.common.updated', 'Updated')}\n </dt>\n <dd className=\"mt-0.5 font-medium text-text-primary\">\n {formatDate(policy.updatedAt)}\n </dd>\n </div>\n </dl>\n\n {policy.appliesTo.length > 0 && (\n <div className=\"mt-5\">\n <h3 className=\"mb-2 text-sm font-semibold text-text-primary\">\n {t('adaptercloud.policies.appliesTo', 'Applies To')}\n </h3>\n <div className=\"flex flex-wrap gap-1.5\">\n {policy.appliesTo.map((sel) => (\n <span\n key={sel}\n className=\"inline-flex items-center rounded-full bg-accent-blue-subtle px-2.5 py-0.5 font-mono text-xs text-accent-blue\"\n >\n {sel}\n </span>\n ))}\n </div>\n </div>\n )}\n\n {policy.tags.length > 0 && (\n <div className=\"mt-4\">\n <h3 className=\"mb-2 text-sm font-semibold text-text-primary\">\n {t('adaptercloud.common.tags', 'Tags')}\n </h3>\n <div className=\"flex flex-wrap gap-1.5\">\n {policy.tags.map((tag) => (\n <span\n key={tag}\n className=\"inline-flex items-center rounded-full bg-bg-sunken px-2.5 py-0.5 text-xs text-text-secondary\"\n >\n {tag}\n </span>\n ))}\n </div>\n </div>\n )}\n </GlassCard>\n\n {/* Expression */}\n <GlassCard className=\"p-6\">\n <h3 className=\"mb-3 text-sm font-semibold text-text-primary\">\n {t('adaptercloud.policies.expression', 'Expression')}\n </h3>\n <pre className=\"overflow-auto rounded-lg bg-bg-sunken p-4 text-xs text-text-secondary\">\n {JSON.stringify(policy.expression, null, 2)}\n </pre>\n </GlassCard>\n\n {/* Violations */}\n <section>\n <div className=\"mb-3 flex items-center justify-between\">\n <h3 className=\"text-base font-semibold text-text-primary\">\n {t('adaptercloud.policies.violations', 'Violations')}\n </h3>\n <button\n type=\"button\"\n onClick={() => navigateTo(`/violations?policyId=${policy.id}`)}\n className=\"text-sm font-medium text-primary hover:underline\"\n >\n {t('adaptercloud.common.viewAll', 'View all')}\n </button>\n </div>\n {policy.violations && policy.violations.length > 0 ? (\n <div className=\"divide-y divide-border rounded-xl border border-border-subtle bg-bg-surface\">\n {policy.violations.slice(0, 10).map((v) => (\n <button\n key={v.id}\n type=\"button\"\n onClick={() => navigateTo(`/violations?policyId=${policy.id}`)}\n className=\"flex w-full items-start gap-3 px-4 py-3 text-left transition-colors hover:bg-accent/50\"\n >\n <AlertCircle className=\"mt-0.5 h-4 w-4 shrink-0 text-status-error-text\" />\n <div className=\"min-w-0 flex-1\">\n <p className=\"truncate text-sm font-medium text-text-primary\">\n {v.message}\n </p>\n <p className=\"mt-0.5 text-xs text-text-secondary\">\n {formatRelativeTime(v.detectedAt)}\n </p>\n </div>\n <StatusBadge status={v.status} />\n </button>\n ))}\n </div>\n ) : (\n <p className=\"rounded-xl border border-border-subtle bg-bg-surface px-4 py-6 text-center text-sm text-text-secondary\">\n {t(\n 'adaptercloud.policies.noViolations',\n 'No violations recorded for this policy.'\n )}\n </p>\n )}\n </section>\n </div>\n )}\n </DataState>\n </PageLayout>\n );\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"PolicyFormPage.js","names":[],"sources":["../../../../src/adaptercloud/pages/policies/PolicyFormPage.tsx"],"sourcesContent":["/**\n * Policy Create Form Page\n * @module adaptercloud/pages/policies\n *\n * Authors a new governance policy (`adaCreatePolicy`). The expression is a JSON\n * document validated client-side before submit.\n */\n\nimport { useCallback, useState } from 'react';\nimport { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';\nimport { ArrowLeft, Loader2, Save } from 'lucide-react';\nimport { GlassCard } from '@burdenoff/fe-libs/ui';\nimport { PageLayout } from '../../components/PageLayout';\nimport { useAdapterCloud } from '../../context/AdapterCloudContext';\nimport { useCreatePolicy } from '../../hooks/useAdapterCloudApi';\nimport {\n policyKindLabels,\n policyEnforcementLabels,\n severityLabels,\n} from '../../utils/domain';\nimport type { AdaPolicyEnforcement, AdaPolicyKind, AdaSeverity } from '../../types';\n\nconst parseList = (value: string): string[] =>\n value\n .split(',')\n .map((s) => s.trim())\n .filter(Boolean);\n\nconst DEFAULT_EXPRESSION = `{\n \"match\": { \"resourceType\": \"*\" },\n \"require\": { \"tags\": [\"owner\", \"env\"] }\n}`;\n\nexport function PolicyFormPage() {\n const { navigateTo } = useAdapterCloud();\n const { t: translate } = useI18n();\n const t = useCallback(\n (key: string, fallback: string): string => {\n const value = translate(key);\n return value === key ? fallback : value;\n },\n [translate]\n );\n\n const create = useCreatePolicy();\n\n const [name, setName] = useState('');\n const [description, setDescription] = useState('');\n const [kind, setKind] = useState<AdaPolicyKind>('GUARDRAIL');\n const [enforcement, setEnforcement] = useState<AdaPolicyEnforcement>('WARN');\n const [severity, setSeverity] = useState<AdaSeverity>('MEDIUM');\n const [appliesTo, setAppliesTo] = useState('');\n const [tags, setTags] = useState('');\n const [isActive, setIsActive] = useState(true);\n const [expression, setExpression] = useState(DEFAULT_EXPRESSION);\n const [error, setError] = useState<string | null>(null);\n\n const handleSubmit = useCallback(\n (e: React.FormEvent) => {\n e.preventDefault();\n setError(null);\n\n if (!name.trim()) {\n setError(t('adaptercloud.policies.form.nameRequired', 'Name is required.'));\n return;\n }\n\n let parsedExpression: unknown;\n try {\n parsedExpression = JSON.parse(expression);\n } catch {\n setError(\n t('adaptercloud.policies.form.invalidExpression', 'Expression must be valid JSON.')\n );\n return;\n }\n\n create.mutate(\n {\n name: name.trim(),\n description: description.trim() || undefined,\n kind,\n enforcement,\n severity,\n isActive,\n appliesTo: parseList(appliesTo),\n tags: parseList(tags),\n expression: parsedExpression,\n },\n {\n onSuccess: (p) => navigateTo(`/policies/${p.id}`),\n onError: (err) =>\n setError(err instanceof Error ? err.message : 'Failed to create policy.'),\n }\n );\n },\n [\n create,\n name,\n description,\n kind,\n enforcement,\n severity,\n isActive,\n appliesTo,\n tags,\n expression,\n navigateTo,\n t,\n ]\n );\n\n const fieldClass =\n 'w-full rounded-lg border border-border-subtle bg-bg-surface px-3 py-2 text-sm text-text-primary placeholder:text-text-secondary focus:outline-none focus:ring-2 focus:ring-[var(--color-focus-ring)]';\n const labelClass = 'mb-1.5 block text-sm font-medium text-text-primary';\n\n return (\n <PageLayout\n title={t('adaptercloud.policies.newTitle', 'New Policy')}\n description={t(\n 'adaptercloud.policies.formSubtitle',\n 'Put a governance rule into the system instead of a wiki.'\n )}\n actions={\n <button\n type=\"button\"\n onClick={() => navigateTo('/policies')}\n className=\"inline-flex items-center gap-2 rounded-lg border border-border-subtle bg-bg-surface px-3 py-2 text-sm font-medium text-text-primary transition-colors hover:bg-accent\"\n >\n <ArrowLeft className=\"h-4 w-4\" />\n {t('adaptercloud.common.cancel', 'Cancel')}\n </button>\n }\n >\n <form onSubmit={handleSubmit} className=\"max-w-2xl\">\n <GlassCard className=\"space-y-5 p-6\">\n <div>\n <label htmlFor=\"ada-pol-name\" className={labelClass}>\n {t('adaptercloud.policies.form.name', 'Name')} *\n </label>\n <input\n id=\"ada-pol-name\"\n type=\"text\"\n value={name}\n onChange={(e) => setName(e.target.value)}\n className={fieldClass}\n placeholder=\"Require owner + env tags\"\n />\n </div>\n\n <div>\n <label htmlFor=\"ada-pol-desc\" className={labelClass}>\n {t('adaptercloud.policies.form.description', 'Description')}\n </label>\n <textarea\n id=\"ada-pol-desc\"\n value={description}\n onChange={(e) => setDescription(e.target.value)}\n rows={2}\n className={fieldClass}\n placeholder={t(\n 'adaptercloud.policies.form.descriptionPlaceholder',\n 'What this policy enforces and why.'\n )}\n />\n </div>\n\n <div className=\"grid grid-cols-1 gap-5 sm:grid-cols-3\">\n <div>\n <label htmlFor=\"ada-pol-kind\" className={labelClass}>\n {t('adaptercloud.policies.form.kind', 'Kind')}\n </label>\n <select\n id=\"ada-pol-kind\"\n value={kind}\n onChange={(e) => setKind(e.target.value as AdaPolicyKind)}\n className={fieldClass}\n >\n {(Object.keys(policyKindLabels) as AdaPolicyKind[]).map((k) => (\n <option key={k} value={k}>\n {policyKindLabels[k]}\n </option>\n ))}\n </select>\n </div>\n <div>\n <label htmlFor=\"ada-pol-enf\" className={labelClass}>\n {t('adaptercloud.policies.form.enforcement', 'Enforcement')}\n </label>\n <select\n id=\"ada-pol-enf\"\n value={enforcement}\n onChange={(e) => setEnforcement(e.target.value as AdaPolicyEnforcement)}\n className={fieldClass}\n >\n {(Object.keys(policyEnforcementLabels) as AdaPolicyEnforcement[]).map((en) => (\n <option key={en} value={en}>\n {policyEnforcementLabels[en]}\n </option>\n ))}\n </select>\n </div>\n <div>\n <label htmlFor=\"ada-pol-sev\" className={labelClass}>\n {t('adaptercloud.policies.form.severity', 'Severity')}\n </label>\n <select\n id=\"ada-pol-sev\"\n value={severity}\n onChange={(e) => setSeverity(e.target.value as AdaSeverity)}\n className={fieldClass}\n >\n {(Object.keys(severityLabels) as AdaSeverity[]).map((s) => (\n <option key={s} value={s}>\n {severityLabels[s]}\n </option>\n ))}\n </select>\n </div>\n </div>\n\n <div>\n <label htmlFor=\"ada-pol-applies\" className={labelClass}>\n {t('adaptercloud.policies.form.appliesTo', 'Applies To')}\n </label>\n <input\n id=\"ada-pol-applies\"\n type=\"text\"\n value={appliesTo}\n onChange={(e) => setAppliesTo(e.target.value)}\n className={fieldClass}\n placeholder=\"aws:ec2:instance, aws:s3:bucket\"\n />\n <p className=\"mt-1 text-xs text-text-secondary\">\n {t('adaptercloud.common.commaSeparated', 'Comma-separated.')}\n </p>\n </div>\n\n <div>\n <label htmlFor=\"ada-pol-expr\" className={labelClass}>\n {t('adaptercloud.policies.form.expression', 'Expression (JSON)')} *\n </label>\n <textarea\n id=\"ada-pol-expr\"\n value={expression}\n onChange={(e) => setExpression(e.target.value)}\n rows={8}\n spellCheck={false}\n className={`${fieldClass} font-mono`}\n />\n </div>\n\n <div>\n <label htmlFor=\"ada-pol-tags\" className={labelClass}>\n {t('adaptercloud.common.tags', 'Tags')}\n </label>\n <input\n id=\"ada-pol-tags\"\n type=\"text\"\n value={tags}\n onChange={(e) => setTags(e.target.value)}\n className={fieldClass}\n placeholder=\"tagging, governance\"\n />\n </div>\n\n <label className=\"flex items-center gap-2 text-sm text-text-primary\">\n <input\n type=\"checkbox\"\n checked={isActive}\n onChange={(e) => setIsActive(e.target.checked)}\n className=\"h-4 w-4 rounded border-border-subtle\"\n />\n {t('adaptercloud.policies.form.activeImmediately', 'Active immediately')}\n </label>\n\n {error && (\n <p className=\"rounded-lg bg-status-error-bg-subtle px-3 py-2 text-sm text-status-error-text\">\n {error}\n </p>\n )}\n\n <div className=\"flex justify-end gap-3 pt-2\">\n <button\n type=\"submit\"\n disabled={create.isPending}\n className=\"inline-flex items-center gap-2 rounded-lg bg-action-primary-bg px-5 py-2 text-sm font-medium text-action-primary-text transition-colors hover:opacity-90 disabled:opacity-50\"\n >\n {create.isPending ? (\n <Loader2 className=\"h-4 w-4 animate-spin\" />\n ) : (\n <Save className=\"h-4 w-4\" />\n )}\n {t('adaptercloud.policies.create', 'Create policy')}\n </button>\n </div>\n </GlassCard>\n </form>\n </PageLayout>\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"PolicyFormPage.js","names":[],"sources":["../../../../src/adaptercloud/pages/policies/PolicyFormPage.tsx"],"sourcesContent":["/**\n * Policy Create Form Page\n * @module adaptercloud/pages/policies\n *\n * Authors a new governance policy (`adaCreatePolicy`). The expression is a JSON\n * document validated client-side before submit.\n */\n\nimport { useCallback, useState } from 'react';\nimport { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';\nimport { ArrowLeft, Loader2, Save } from 'lucide-react';\nimport { GlassCard } from '@burdenoff/fe-libs/ui';\nimport { PageLayout } from '../../components/PageLayout';\nimport { useAdapterCloud } from '../../context/AdapterCloudContext';\nimport { useCreatePolicy } from '../../hooks/useAdapterCloudApi';\nimport { policyKindLabels, policyEnforcementLabels, severityLabels } from '../../utils/domain';\nimport type { AdaPolicyEnforcement, AdaPolicyKind, AdaSeverity } from '../../types';\n\nconst parseList = (value: string): string[] =>\n value\n .split(',')\n .map((s) => s.trim())\n .filter(Boolean);\n\nconst DEFAULT_EXPRESSION = `{\n \"match\": { \"resourceType\": \"*\" },\n \"require\": { \"tags\": [\"owner\", \"env\"] }\n}`;\n\nexport function PolicyFormPage() {\n const { navigateTo } = useAdapterCloud();\n const { t: translate } = useI18n();\n const t = useCallback(\n (key: string, fallback: string): string => {\n const value = translate(key);\n return value === key ? fallback : value;\n },\n [translate]\n );\n\n const create = useCreatePolicy();\n\n const [name, setName] = useState('');\n const [description, setDescription] = useState('');\n const [kind, setKind] = useState<AdaPolicyKind>('GUARDRAIL');\n const [enforcement, setEnforcement] = useState<AdaPolicyEnforcement>('WARN');\n const [severity, setSeverity] = useState<AdaSeverity>('MEDIUM');\n const [appliesTo, setAppliesTo] = useState('');\n const [tags, setTags] = useState('');\n const [isActive, setIsActive] = useState(true);\n const [expression, setExpression] = useState(DEFAULT_EXPRESSION);\n const [error, setError] = useState<string | null>(null);\n\n const handleSubmit = useCallback(\n (e: React.FormEvent) => {\n e.preventDefault();\n setError(null);\n\n if (!name.trim()) {\n setError(t('adaptercloud.policies.form.nameRequired', 'Name is required.'));\n return;\n }\n\n let parsedExpression: unknown;\n try {\n parsedExpression = JSON.parse(expression);\n } catch {\n setError(\n t('adaptercloud.policies.form.invalidExpression', 'Expression must be valid JSON.')\n );\n return;\n }\n\n create.mutate(\n {\n name: name.trim(),\n description: description.trim() || undefined,\n kind,\n enforcement,\n severity,\n isActive,\n appliesTo: parseList(appliesTo),\n tags: parseList(tags),\n expression: parsedExpression,\n },\n {\n onSuccess: (p) => navigateTo(`/policies/${p.id}`),\n onError: (err) =>\n setError(err instanceof Error ? err.message : 'Failed to create policy.'),\n }\n );\n },\n [\n create,\n name,\n description,\n kind,\n enforcement,\n severity,\n isActive,\n appliesTo,\n tags,\n expression,\n navigateTo,\n t,\n ]\n );\n\n const fieldClass =\n 'w-full rounded-lg border border-border-subtle bg-bg-surface px-3 py-2 text-sm text-text-primary placeholder:text-text-secondary focus:outline-none focus:ring-2 focus:ring-[var(--color-focus-ring)]';\n const labelClass = 'mb-1.5 block text-sm font-medium text-text-primary';\n\n return (\n <PageLayout\n title={t('adaptercloud.policies.newTitle', 'New Policy')}\n description={t(\n 'adaptercloud.policies.formSubtitle',\n 'Put a governance rule into the system instead of a wiki.'\n )}\n actions={\n <button\n type=\"button\"\n onClick={() => navigateTo('/policies')}\n className=\"inline-flex items-center gap-2 rounded-lg border border-border-subtle bg-bg-surface px-3 py-2 text-sm font-medium text-text-primary transition-colors hover:bg-accent\"\n >\n <ArrowLeft className=\"h-4 w-4\" />\n {t('adaptercloud.common.cancel', 'Cancel')}\n </button>\n }\n >\n <form onSubmit={handleSubmit} className=\"max-w-2xl\">\n <GlassCard className=\"space-y-5 p-6\">\n <div>\n <label htmlFor=\"ada-pol-name\" className={labelClass}>\n {t('adaptercloud.policies.form.name', 'Name')} *\n </label>\n <input\n id=\"ada-pol-name\"\n type=\"text\"\n value={name}\n onChange={(e) => setName(e.target.value)}\n className={fieldClass}\n placeholder=\"Require owner + env tags\"\n />\n </div>\n\n <div>\n <label htmlFor=\"ada-pol-desc\" className={labelClass}>\n {t('adaptercloud.policies.form.description', 'Description')}\n </label>\n <textarea\n id=\"ada-pol-desc\"\n value={description}\n onChange={(e) => setDescription(e.target.value)}\n rows={2}\n className={fieldClass}\n placeholder={t(\n 'adaptercloud.policies.form.descriptionPlaceholder',\n 'What this policy enforces and why.'\n )}\n />\n </div>\n\n <div className=\"grid grid-cols-1 gap-5 sm:grid-cols-3\">\n <div>\n <label htmlFor=\"ada-pol-kind\" className={labelClass}>\n {t('adaptercloud.policies.form.kind', 'Kind')}\n </label>\n <select\n id=\"ada-pol-kind\"\n value={kind}\n onChange={(e) => setKind(e.target.value as AdaPolicyKind)}\n className={fieldClass}\n >\n {(Object.keys(policyKindLabels) as AdaPolicyKind[]).map((k) => (\n <option key={k} value={k}>\n {policyKindLabels[k]}\n </option>\n ))}\n </select>\n </div>\n <div>\n <label htmlFor=\"ada-pol-enf\" className={labelClass}>\n {t('adaptercloud.policies.form.enforcement', 'Enforcement')}\n </label>\n <select\n id=\"ada-pol-enf\"\n value={enforcement}\n onChange={(e) => setEnforcement(e.target.value as AdaPolicyEnforcement)}\n className={fieldClass}\n >\n {(Object.keys(policyEnforcementLabels) as AdaPolicyEnforcement[]).map((en) => (\n <option key={en} value={en}>\n {policyEnforcementLabels[en]}\n </option>\n ))}\n </select>\n </div>\n <div>\n <label htmlFor=\"ada-pol-sev\" className={labelClass}>\n {t('adaptercloud.policies.form.severity', 'Severity')}\n </label>\n <select\n id=\"ada-pol-sev\"\n value={severity}\n onChange={(e) => setSeverity(e.target.value as AdaSeverity)}\n className={fieldClass}\n >\n {(Object.keys(severityLabels) as AdaSeverity[]).map((s) => (\n <option key={s} value={s}>\n {severityLabels[s]}\n </option>\n ))}\n </select>\n </div>\n </div>\n\n <div>\n <label htmlFor=\"ada-pol-applies\" className={labelClass}>\n {t('adaptercloud.policies.form.appliesTo', 'Applies To')}\n </label>\n <input\n id=\"ada-pol-applies\"\n type=\"text\"\n value={appliesTo}\n onChange={(e) => setAppliesTo(e.target.value)}\n className={fieldClass}\n placeholder=\"aws:ec2:instance, aws:s3:bucket\"\n />\n <p className=\"mt-1 text-xs text-text-secondary\">\n {t('adaptercloud.common.commaSeparated', 'Comma-separated.')}\n </p>\n </div>\n\n <div>\n <label htmlFor=\"ada-pol-expr\" className={labelClass}>\n {t('adaptercloud.policies.form.expression', 'Expression (JSON)')} *\n </label>\n <textarea\n id=\"ada-pol-expr\"\n value={expression}\n onChange={(e) => setExpression(e.target.value)}\n rows={8}\n spellCheck={false}\n className={`${fieldClass} font-mono`}\n />\n </div>\n\n <div>\n <label htmlFor=\"ada-pol-tags\" className={labelClass}>\n {t('adaptercloud.common.tags', 'Tags')}\n </label>\n <input\n id=\"ada-pol-tags\"\n type=\"text\"\n value={tags}\n onChange={(e) => setTags(e.target.value)}\n className={fieldClass}\n placeholder=\"tagging, governance\"\n />\n </div>\n\n <label className=\"flex items-center gap-2 text-sm text-text-primary\">\n <input\n type=\"checkbox\"\n checked={isActive}\n onChange={(e) => setIsActive(e.target.checked)}\n className=\"h-4 w-4 rounded border-border-subtle\"\n />\n {t('adaptercloud.policies.form.activeImmediately', 'Active immediately')}\n </label>\n\n {error && (\n <p className=\"rounded-lg bg-status-error-bg-subtle px-3 py-2 text-sm text-status-error-text\">\n {error}\n </p>\n )}\n\n <div className=\"flex justify-end gap-3 pt-2\">\n <button\n type=\"submit\"\n disabled={create.isPending}\n className=\"inline-flex items-center gap-2 rounded-lg bg-action-primary-bg px-5 py-2 text-sm font-medium text-action-primary-text transition-colors hover:opacity-90 disabled:opacity-50\"\n >\n {create.isPending ? (\n <Loader2 className=\"h-4 w-4 animate-spin\" />\n ) : (\n <Save className=\"h-4 w-4\" />\n )}\n {t('adaptercloud.policies.create', 'Create policy')}\n </button>\n </div>\n </GlassCard>\n </form>\n </PageLayout>\n );\n}\n"],"mappings":";;;;;;;;;;AAkBA,IAAM,KAAa,MACjB,EACG,MAAM,IAAI,CACV,KAAK,MAAM,EAAE,MAAM,CAAC,CACpB,OAAO,QAAQ,EAEd,IAAqB;AAK3B,SAAgB,IAAiB;CAC/B,IAAM,EAAE,kBAAe,GAAiB,EAClC,EAAE,GAAG,MAAc,GAAS,EAC5B,IAAI,GACP,GAAa,MAA6B;EACzC,IAAM,IAAQ,EAAU,EAAI;AAC5B,SAAO,MAAU,IAAM,IAAW;IAEpC,CAAC,EAAU,CACZ,EAEK,IAAS,GAAiB,EAE1B,CAAC,GAAM,KAAW,EAAS,GAAG,EAC9B,CAAC,GAAa,KAAkB,EAAS,GAAG,EAC5C,CAAC,GAAM,KAAW,EAAwB,YAAY,EACtD,CAAC,GAAa,KAAkB,EAA+B,OAAO,EACtE,CAAC,GAAU,KAAe,EAAsB,SAAS,EACzD,CAAC,GAAW,KAAgB,EAAS,GAAG,EACxC,CAAC,GAAM,KAAW,EAAS,GAAG,EAC9B,CAAC,GAAU,KAAe,EAAS,GAAK,EACxC,CAAC,GAAY,KAAiB,EAAS,EAAmB,EAC1D,CAAC,GAAO,KAAY,EAAwB,KAAK,EAEjD,IAAe,GAClB,MAAuB;AAItB,MAHA,EAAE,gBAAgB,EAClB,EAAS,KAAK,EAEV,CAAC,EAAK,MAAM,EAAE;AAChB,KAAS,EAAE,2CAA2C,oBAAoB,CAAC;AAC3E;;EAGF,IAAI;AACJ,MAAI;AACF,OAAmB,KAAK,MAAM,EAAW;UACnC;AACN,KACE,EAAE,gDAAgD,iCAAiC,CACpF;AACD;;AAGF,IAAO,OACL;GACE,MAAM,EAAK,MAAM;GACjB,aAAa,EAAY,MAAM,IAAI,KAAA;GACnC;GACA;GACA;GACA;GACA,WAAW,EAAU,EAAU;GAC/B,MAAM,EAAU,EAAK;GACrB,YAAY;GACb,EACD;GACE,YAAY,MAAM,EAAW,aAAa,EAAE,KAAK;GACjD,UAAU,MACR,EAAS,aAAe,QAAQ,EAAI,UAAU,2BAA2B;GAC5E,CACF;IAEH;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACD,CACF,EAEK,IACJ,wMACI,IAAa;AAEnB,QACE,kBAAC,GAAD;EACE,OAAO,EAAE,kCAAkC,aAAa;EACxD,aAAa,EACX,sCACA,2DACD;EACD,SACE,kBAAC,UAAD;GACE,MAAK;GACL,eAAe,EAAW,YAAY;GACtC,WAAU;aAHZ,CAKE,kBAAC,GAAD,EAAW,WAAU,WAAY,CAAA,EAChC,EAAE,8BAA8B,SAAS,CACnC;;YAGX,kBAAC,QAAD;GAAM,UAAU;GAAc,WAAU;aACtC,kBAAC,GAAD;IAAW,WAAU;cAArB;KACE,kBAAC,OAAD,EAAA,UAAA,CACE,kBAAC,SAAD;MAAO,SAAQ;MAAe,WAAW;gBAAzC,CACG,EAAE,mCAAmC,OAAO,EAAC,KACxC;SACR,kBAAC,SAAD;MACE,IAAG;MACH,MAAK;MACL,OAAO;MACP,WAAW,MAAM,EAAQ,EAAE,OAAO,MAAM;MACxC,WAAW;MACX,aAAY;MACZ,CAAA,CACE,EAAA,CAAA;KAEN,kBAAC,OAAD,EAAA,UAAA,CACE,kBAAC,SAAD;MAAO,SAAQ;MAAe,WAAW;gBACtC,EAAE,0CAA0C,cAAc;MACrD,CAAA,EACR,kBAAC,YAAD;MACE,IAAG;MACH,OAAO;MACP,WAAW,MAAM,EAAe,EAAE,OAAO,MAAM;MAC/C,MAAM;MACN,WAAW;MACX,aAAa,EACX,qDACA,qCACD;MACD,CAAA,CACE,EAAA,CAAA;KAEN,kBAAC,OAAD;MAAK,WAAU;gBAAf;OACE,kBAAC,OAAD,EAAA,UAAA,CACE,kBAAC,SAAD;QAAO,SAAQ;QAAe,WAAW;kBACtC,EAAE,mCAAmC,OAAO;QACvC,CAAA,EACR,kBAAC,UAAD;QACE,IAAG;QACH,OAAO;QACP,WAAW,MAAM,EAAQ,EAAE,OAAO,MAAuB;QACzD,WAAW;kBAET,OAAO,KAAK,EAAiB,CAAqB,KAAK,MACvD,kBAAC,UAAD;SAAgB,OAAO;mBACpB,EAAiB;SACX,EAFI,EAEJ,CACT;QACK,CAAA,CACL,EAAA,CAAA;OACN,kBAAC,OAAD,EAAA,UAAA,CACE,kBAAC,SAAD;QAAO,SAAQ;QAAc,WAAW;kBACrC,EAAE,0CAA0C,cAAc;QACrD,CAAA,EACR,kBAAC,UAAD;QACE,IAAG;QACH,OAAO;QACP,WAAW,MAAM,EAAe,EAAE,OAAO,MAA8B;QACvE,WAAW;kBAET,OAAO,KAAK,EAAwB,CAA4B,KAAK,MACrE,kBAAC,UAAD;SAAiB,OAAO;mBACrB,EAAwB;SAClB,EAFI,EAEJ,CACT;QACK,CAAA,CACL,EAAA,CAAA;OACN,kBAAC,OAAD,EAAA,UAAA,CACE,kBAAC,SAAD;QAAO,SAAQ;QAAc,WAAW;kBACrC,EAAE,uCAAuC,WAAW;QAC/C,CAAA,EACR,kBAAC,UAAD;QACE,IAAG;QACH,OAAO;QACP,WAAW,MAAM,EAAY,EAAE,OAAO,MAAqB;QAC3D,WAAW;kBAET,OAAO,KAAK,EAAe,CAAmB,KAAK,MACnD,kBAAC,UAAD;SAAgB,OAAO;mBACpB,EAAe;SACT,EAFI,EAEJ,CACT;QACK,CAAA,CACL,EAAA,CAAA;OACF;;KAEN,kBAAC,OAAD,EAAA,UAAA;MACE,kBAAC,SAAD;OAAO,SAAQ;OAAkB,WAAW;iBACzC,EAAE,wCAAwC,aAAa;OAClD,CAAA;MACR,kBAAC,SAAD;OACE,IAAG;OACH,MAAK;OACL,OAAO;OACP,WAAW,MAAM,EAAa,EAAE,OAAO,MAAM;OAC7C,WAAW;OACX,aAAY;OACZ,CAAA;MACF,kBAAC,KAAD;OAAG,WAAU;iBACV,EAAE,sCAAsC,mBAAmB;OAC1D,CAAA;MACA,EAAA,CAAA;KAEN,kBAAC,OAAD,EAAA,UAAA,CACE,kBAAC,SAAD;MAAO,SAAQ;MAAe,WAAW;gBAAzC,CACG,EAAE,yCAAyC,oBAAoB,EAAC,KAC3D;SACR,kBAAC,YAAD;MACE,IAAG;MACH,OAAO;MACP,WAAW,MAAM,EAAc,EAAE,OAAO,MAAM;MAC9C,MAAM;MACN,YAAY;MACZ,WAAW,GAAG,EAAW;MACzB,CAAA,CACE,EAAA,CAAA;KAEN,kBAAC,OAAD,EAAA,UAAA,CACE,kBAAC,SAAD;MAAO,SAAQ;MAAe,WAAW;gBACtC,EAAE,4BAA4B,OAAO;MAChC,CAAA,EACR,kBAAC,SAAD;MACE,IAAG;MACH,MAAK;MACL,OAAO;MACP,WAAW,MAAM,EAAQ,EAAE,OAAO,MAAM;MACxC,WAAW;MACX,aAAY;MACZ,CAAA,CACE,EAAA,CAAA;KAEN,kBAAC,SAAD;MAAO,WAAU;gBAAjB,CACE,kBAAC,SAAD;OACE,MAAK;OACL,SAAS;OACT,WAAW,MAAM,EAAY,EAAE,OAAO,QAAQ;OAC9C,WAAU;OACV,CAAA,EACD,EAAE,gDAAgD,qBAAqB,CAClE;;KAEP,KACC,kBAAC,KAAD;MAAG,WAAU;gBACV;MACC,CAAA;KAGN,kBAAC,OAAD;MAAK,WAAU;gBACb,kBAAC,UAAD;OACE,MAAK;OACL,UAAU,EAAO;OACjB,WAAU;iBAHZ,CAKG,EAAO,YACN,kBAAC,GAAD,EAAS,WAAU,wBAAyB,CAAA,GAE5C,kBAAC,GAAD,EAAM,WAAU,WAAY,CAAA,EAE7B,EAAE,gCAAgC,gBAAgB,CAC5C;;MACL,CAAA;KACI;;GACP,CAAA;EACI,CAAA"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ViolationsListPage.js","names":[],"sources":["../../../../src/adaptercloud/pages/violations/ViolationsListPage.tsx"],"sourcesContent":["/**\n * Violations Queue Page\n * @module adaptercloud/pages/violations\n *\n * The governance work queue (`adaListViolations`). Filters by status (and by\n * policy/resource via URL params). Each open/acknowledged violation can be\n * transitioned via `adaResolveViolation` — acknowledge, resolve, or waive (a\n * waiver requires a reason, enforced both client-side and at the service).\n */\n\nimport { useState, useCallback } from 'react';\nimport { useSearchParams } from 'react-router-dom';\nimport { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';\nimport {\n ShieldAlert,\n ChevronLeft,\n ChevronRight,\n Check,\n CheckCheck,\n FileText,\n Loader2,\n} from 'lucide-react';\nimport { GlassCard, ResponsiveDialog, ResponsiveDialogContent, ResponsiveDialogHeader, ResponsiveDialogTitle, ResponsiveDialogFooter } from '@burdenoff/fe-libs/ui';\nimport { PageLayout } from '../../components/PageLayout';\nimport { StatusBadge } from '../../components/StatusBadge';\nimport { SeverityBadge } from '../../components/SeverityBadge';\nimport { DataState } from '../../components/DataState';\nimport { useAdapterCloud } from '../../context/AdapterCloudContext';\nimport { useViolations, useResolveViolation } from '../../hooks/useAdapterCloudApi';\nimport { violationStatusLabels, severityRank } from '../../utils/domain';\nimport { formatDate } from '../../utils/formatters';\nimport type { AdaPolicyViolation, AdaViolationStatus } from '../../types';\n\nconst PAGE_SIZE = 20;\n\nexport function ViolationsListPage() {\n const { navigateTo } = useAdapterCloud();\n const [searchParams] = useSearchParams();\n const policyId = searchParams.get('policyId') ?? undefined;\n const resourceId = searchParams.get('resourceId') ?? undefined;\n\n const { t: translate } = useI18n();\n const t = useCallback(\n (key: string, fallback: string): string => {\n const value = translate(key);\n return value === key ? fallback : value;\n },\n [translate]\n );\n\n const [status, setStatus] = useState<AdaViolationStatus | ''>('OPEN');\n const [offset, setOffset] = useState(0);\n const [waiveTarget, setWaiveTarget] = useState<AdaPolicyViolation | null>(null);\n const [waiverReason, setWaiverReason] = useState('');\n const [actionError, setActionError] = useState<string | null>(null);\n\n const query = useViolations({\n policyId,\n resourceId,\n status: status || undefined,\n pagination: { limit: PAGE_SIZE, offset },\n });\n const resolve = useResolveViolation();\n\n const items = [...(query.data?.items ?? [])].sort(\n (a, b) => severityRank[b.severity] - severityRank[a.severity]\n );\n const hasMore = query.data?.pageInfo.hasMore ?? false;\n\n const onError = useCallback(\n (err: unknown) =>\n setActionError(err instanceof Error ? err.message : 'Action failed. Please try again.'),\n []\n );\n\n const transition = useCallback(\n (id: string, newStatus: AdaViolationStatus) => {\n setActionError(null);\n resolve.mutate({ id, status: newStatus }, { onError });\n },\n [resolve, onError]\n );\n\n const confirmWaive = useCallback(() => {\n if (!waiveTarget) return;\n if (!waiverReason.trim()) {\n setActionError(t('adaptercloud.violations.reasonRequired', 'A waiver requires a reason.'));\n return;\n }\n setActionError(null);\n resolve.mutate(\n { id: waiveTarget.id, status: 'WAIVED', waiverReason: waiverReason.trim() },\n {\n onSuccess: () => {\n setWaiveTarget(null);\n setWaiverReason('');\n },\n onError,\n }\n );\n }, [waiveTarget, waiverReason, resolve, onError, t]);\n\n return (\n <PageLayout\n title={t('adaptercloud.violations.title', 'Violations')}\n description={t(\n 'adaptercloud.violations.subtitle',\n 'Work the governance queue — acknowledge, resolve, or waive policy breaches.'\n )}\n >\n <div className=\"mb-6 flex flex-wrap items-center gap-3\">\n <select\n value={status}\n onChange={(e) => {\n setStatus(e.target.value as AdaViolationStatus | '');\n setOffset(0);\n }}\n className=\"rounded-lg border border-border-subtle bg-bg-surface px-3 py-2 text-sm text-text-primary focus:outline-none focus:ring-2 focus:ring-[var(--color-focus-ring)]\"\n >\n <option value=\"\">{t('adaptercloud.violations.allStatuses', 'All statuses')}</option>\n {(Object.keys(violationStatusLabels) as AdaViolationStatus[]).map((s) => (\n <option key={s} value={s}>\n {violationStatusLabels[s]}\n </option>\n ))}\n </select>\n {(policyId || resourceId) && (\n <button\n type=\"button\"\n onClick={() => navigateTo('/violations')}\n className=\"inline-flex items-center gap-1.5 rounded-full bg-accent-blue-subtle px-2.5 py-1 text-xs text-accent-blue hover:underline\"\n >\n {t('adaptercloud.violations.clearScope', 'Clear scope ×')}\n </button>\n )}\n </div>\n\n {actionError && (\n <p className=\"mb-4 rounded-lg bg-status-error-bg-subtle px-3 py-2 text-sm text-status-error-text\">\n {actionError}\n </p>\n )}\n\n <DataState\n isLoading={query.isLoading}\n isError={query.isError}\n isEmpty={items.length === 0}\n error={query.error}\n onRetry={() => void query.refetch()}\n emptyTitle={t('adaptercloud.violations.emptyTitle', 'No violations')}\n emptyDescription={t(\n 'adaptercloud.violations.emptyDescription',\n 'Nothing matches this filter — your estate is clean here.'\n )}\n emptyIcon={<ShieldAlert className=\"h-10 w-10\" />}\n >\n <div className=\"space-y-3\">\n {items.map((v) => {\n const isOpen = v.status === 'OPEN' || v.status === 'ACKNOWLEDGED';\n return (\n <GlassCard key={v.id} className=\"p-5\">\n <div className=\"flex flex-wrap items-start justify-between gap-3\">\n <div className=\"min-w-0 flex-1\">\n <div className=\"mb-1.5 flex flex-wrap items-center gap-2\">\n <SeverityBadge severity={v.severity} />\n <StatusBadge status={v.status} />\n {v.policy && (\n <button\n type=\"button\"\n onClick={() => navigateTo(`/policies/${v.policyId}`)}\n className=\"text-xs font-medium text-primary hover:underline\"\n >\n {v.policy.name}\n </button>\n )}\n </div>\n <p className=\"text-sm font-medium text-text-primary\">{v.message}</p>\n <p className=\"mt-1 text-xs text-text-secondary\">\n {t('adaptercloud.violations.detected', 'Detected')} {formatDate(v.detectedAt)}\n {' · '}\n <button\n type=\"button\"\n onClick={() => navigateTo(`/resources/${v.resourceId}`)}\n className=\"hover:underline\"\n >\n {t('adaptercloud.violations.viewResource', 'View resource')}\n </button>\n </p>\n {v.waiverReason && (\n <p className=\"mt-2 rounded-lg bg-bg-sunken px-3 py-1.5 text-xs text-text-secondary\">\n {t('adaptercloud.violations.waiver', 'Waiver')}: {v.waiverReason}\n </p>\n )}\n </div>\n\n {isOpen && (\n <div className=\"flex shrink-0 flex-wrap gap-2\">\n {v.status === 'OPEN' && (\n <button\n type=\"button\"\n disabled={resolve.isPending}\n onClick={() => transition(v.id, 'ACKNOWLEDGED')}\n className=\"inline-flex items-center gap-1.5 rounded-lg border border-border-subtle px-3 py-1.5 text-xs font-medium text-text-primary transition-colors hover:bg-accent disabled:opacity-50\"\n >\n <Check className=\"h-3.5 w-3.5\" />\n {t('adaptercloud.violations.acknowledge', 'Acknowledge')}\n </button>\n )}\n <button\n type=\"button\"\n disabled={resolve.isPending}\n onClick={() => transition(v.id, 'RESOLVED')}\n className=\"inline-flex items-center gap-1.5 rounded-lg border border-border-subtle px-3 py-1.5 text-xs font-medium text-status-success-text transition-colors hover:bg-status-success-bg-subtle disabled:opacity-50\"\n >\n <CheckCheck className=\"h-3.5 w-3.5\" />\n {t('adaptercloud.violations.resolve', 'Resolve')}\n </button>\n <button\n type=\"button\"\n disabled={resolve.isPending}\n onClick={() => {\n setActionError(null);\n setWaiverReason('');\n setWaiveTarget(v);\n }}\n className=\"inline-flex items-center gap-1.5 rounded-lg border border-border-subtle px-3 py-1.5 text-xs font-medium text-text-secondary transition-colors hover:bg-accent disabled:opacity-50\"\n >\n <FileText className=\"h-3.5 w-3.5\" />\n {t('adaptercloud.violations.waive', 'Waive')}\n </button>\n </div>\n )}\n </div>\n </GlassCard>\n );\n })}\n </div>\n\n {(offset > 0 || hasMore) && (\n <div className=\"mt-6 flex items-center justify-between\">\n <button\n type=\"button\"\n disabled={offset === 0}\n onClick={() => setOffset((o) => Math.max(0, o - PAGE_SIZE))}\n className=\"inline-flex items-center gap-1 rounded-lg border border-border-subtle px-3 py-2 text-sm font-medium text-text-primary transition-colors hover:bg-accent disabled:cursor-not-allowed disabled:opacity-50\"\n >\n <ChevronLeft className=\"h-4 w-4\" />\n {t('adaptercloud.common.prev', 'Previous')}\n </button>\n <span className=\"text-sm text-text-secondary\">\n {t('adaptercloud.common.showing', 'Showing')} {offset + 1}–{offset + items.length}\n </span>\n <button\n type=\"button\"\n disabled={!hasMore}\n onClick={() => setOffset((o) => o + PAGE_SIZE)}\n className=\"inline-flex items-center gap-1 rounded-lg border border-border-subtle px-3 py-2 text-sm font-medium text-text-primary transition-colors hover:bg-accent disabled:cursor-not-allowed disabled:opacity-50\"\n >\n {t('adaptercloud.common.next', 'Next')}\n <ChevronRight className=\"h-4 w-4\" />\n </button>\n </div>\n )}\n </DataState>\n\n {/* Waive dialog */}\n <ResponsiveDialog\n open={Boolean(waiveTarget)}\n onOpenChange={(open) => {\n if (!open) setWaiveTarget(null);\n }}\n >\n <ResponsiveDialogContent>\n <ResponsiveDialogHeader>\n <ResponsiveDialogTitle>\n {t('adaptercloud.violations.waiveTitle', 'Waive violation')}\n </ResponsiveDialogTitle>\n </ResponsiveDialogHeader>\n <div className=\"px-1 py-2\">\n <p className=\"mb-3 text-sm text-text-secondary\">\n {t(\n 'adaptercloud.violations.waivePrompt',\n 'A waiver requires a documented reason and is recorded on the violation.'\n )}\n </p>\n <textarea\n value={waiverReason}\n onChange={(e) => setWaiverReason(e.target.value)}\n rows={4}\n autoFocus\n placeholder={t('adaptercloud.violations.waiveReasonPlaceholder', 'Why is this acceptable?')}\n className=\"w-full rounded-lg border border-border-subtle bg-bg-surface px-3 py-2 text-sm text-text-primary placeholder:text-text-secondary focus:outline-none focus:ring-2 focus:ring-[var(--color-focus-ring)]\"\n />\n {actionError && (\n <p className=\"mt-2 text-sm text-status-error-text\">{actionError}</p>\n )}\n </div>\n <ResponsiveDialogFooter>\n <button\n type=\"button\"\n onClick={() => setWaiveTarget(null)}\n className=\"rounded-lg border border-border-subtle px-4 py-2 text-sm font-medium text-text-primary transition-colors hover:bg-accent\"\n >\n {t('adaptercloud.common.cancel', 'Cancel')}\n </button>\n <button\n type=\"button\"\n disabled={resolve.isPending}\n onClick={confirmWaive}\n className=\"inline-flex items-center gap-2 rounded-lg bg-action-primary-bg px-4 py-2 text-sm font-medium text-action-primary-text transition-colors hover:opacity-90 disabled:opacity-50\"\n >\n {resolve.isPending && <Loader2 className=\"h-4 w-4 animate-spin\" />}\n {t('adaptercloud.violations.confirmWaive', 'Waive')}\n </button>\n </ResponsiveDialogFooter>\n </ResponsiveDialogContent>\n </ResponsiveDialog>\n </PageLayout>\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"ViolationsListPage.js","names":[],"sources":["../../../../src/adaptercloud/pages/violations/ViolationsListPage.tsx"],"sourcesContent":["/**\n * Violations Queue Page\n * @module adaptercloud/pages/violations\n *\n * The governance work queue (`adaListViolations`). Filters by status (and by\n * policy/resource via URL params). Each open/acknowledged violation can be\n * transitioned via `adaResolveViolation` — acknowledge, resolve, or waive (a\n * waiver requires a reason, enforced both client-side and at the service).\n */\n\nimport { useState, useCallback } from 'react';\nimport { useSearchParams } from 'react-router-dom';\nimport { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';\nimport {\n ShieldAlert,\n ChevronLeft,\n ChevronRight,\n Check,\n CheckCheck,\n FileText,\n Loader2,\n} from 'lucide-react';\nimport {\n GlassCard,\n ResponsiveDialog,\n ResponsiveDialogContent,\n ResponsiveDialogHeader,\n ResponsiveDialogTitle,\n ResponsiveDialogFooter,\n} from '@burdenoff/fe-libs/ui';\nimport { PageLayout } from '../../components/PageLayout';\nimport { StatusBadge } from '../../components/StatusBadge';\nimport { SeverityBadge } from '../../components/SeverityBadge';\nimport { DataState } from '../../components/DataState';\nimport { useAdapterCloud } from '../../context/AdapterCloudContext';\nimport { useViolations, useResolveViolation } from '../../hooks/useAdapterCloudApi';\nimport { violationStatusLabels, severityRank } from '../../utils/domain';\nimport { formatDate } from '../../utils/formatters';\nimport type { AdaPolicyViolation, AdaViolationStatus } from '../../types';\n\nconst PAGE_SIZE = 20;\n\nexport function ViolationsListPage() {\n const { navigateTo } = useAdapterCloud();\n const [searchParams] = useSearchParams();\n const policyId = searchParams.get('policyId') ?? undefined;\n const resourceId = searchParams.get('resourceId') ?? undefined;\n\n const { t: translate } = useI18n();\n const t = useCallback(\n (key: string, fallback: string): string => {\n const value = translate(key);\n return value === key ? fallback : value;\n },\n [translate]\n );\n\n const [status, setStatus] = useState<AdaViolationStatus | ''>('OPEN');\n const [offset, setOffset] = useState(0);\n const [waiveTarget, setWaiveTarget] = useState<AdaPolicyViolation | null>(null);\n const [waiverReason, setWaiverReason] = useState('');\n const [actionError, setActionError] = useState<string | null>(null);\n\n const query = useViolations({\n policyId,\n resourceId,\n status: status || undefined,\n pagination: { limit: PAGE_SIZE, offset },\n });\n const resolve = useResolveViolation();\n\n const items = [...(query.data?.items ?? [])].sort(\n (a, b) => severityRank[b.severity] - severityRank[a.severity]\n );\n const hasMore = query.data?.pageInfo.hasMore ?? false;\n\n const onError = useCallback(\n (err: unknown) =>\n setActionError(err instanceof Error ? err.message : 'Action failed. Please try again.'),\n []\n );\n\n const transition = useCallback(\n (id: string, newStatus: AdaViolationStatus) => {\n setActionError(null);\n resolve.mutate({ id, status: newStatus }, { onError });\n },\n [resolve, onError]\n );\n\n const confirmWaive = useCallback(() => {\n if (!waiveTarget) return;\n if (!waiverReason.trim()) {\n setActionError(t('adaptercloud.violations.reasonRequired', 'A waiver requires a reason.'));\n return;\n }\n setActionError(null);\n resolve.mutate(\n { id: waiveTarget.id, status: 'WAIVED', waiverReason: waiverReason.trim() },\n {\n onSuccess: () => {\n setWaiveTarget(null);\n setWaiverReason('');\n },\n onError,\n }\n );\n }, [waiveTarget, waiverReason, resolve, onError, t]);\n\n return (\n <PageLayout\n title={t('adaptercloud.violations.title', 'Violations')}\n description={t(\n 'adaptercloud.violations.subtitle',\n 'Work the governance queue — acknowledge, resolve, or waive policy breaches.'\n )}\n >\n <div className=\"mb-6 flex flex-wrap items-center gap-3\">\n <select\n value={status}\n onChange={(e) => {\n setStatus(e.target.value as AdaViolationStatus | '');\n setOffset(0);\n }}\n className=\"rounded-lg border border-border-subtle bg-bg-surface px-3 py-2 text-sm text-text-primary focus:outline-none focus:ring-2 focus:ring-[var(--color-focus-ring)]\"\n >\n <option value=\"\">{t('adaptercloud.violations.allStatuses', 'All statuses')}</option>\n {(Object.keys(violationStatusLabels) as AdaViolationStatus[]).map((s) => (\n <option key={s} value={s}>\n {violationStatusLabels[s]}\n </option>\n ))}\n </select>\n {(policyId || resourceId) && (\n <button\n type=\"button\"\n onClick={() => navigateTo('/violations')}\n className=\"inline-flex items-center gap-1.5 rounded-full bg-accent-blue-subtle px-2.5 py-1 text-xs text-accent-blue hover:underline\"\n >\n {t('adaptercloud.violations.clearScope', 'Clear scope ×')}\n </button>\n )}\n </div>\n\n {actionError && (\n <p className=\"mb-4 rounded-lg bg-status-error-bg-subtle px-3 py-2 text-sm text-status-error-text\">\n {actionError}\n </p>\n )}\n\n <DataState\n isLoading={query.isLoading}\n isError={query.isError}\n isEmpty={items.length === 0}\n error={query.error}\n onRetry={() => void query.refetch()}\n emptyTitle={t('adaptercloud.violations.emptyTitle', 'No violations')}\n emptyDescription={t(\n 'adaptercloud.violations.emptyDescription',\n 'Nothing matches this filter — your estate is clean here.'\n )}\n emptyIcon={<ShieldAlert className=\"h-10 w-10\" />}\n >\n <div className=\"space-y-3\">\n {items.map((v) => {\n const isOpen = v.status === 'OPEN' || v.status === 'ACKNOWLEDGED';\n return (\n <GlassCard key={v.id} className=\"p-5\">\n <div className=\"flex flex-wrap items-start justify-between gap-3\">\n <div className=\"min-w-0 flex-1\">\n <div className=\"mb-1.5 flex flex-wrap items-center gap-2\">\n <SeverityBadge severity={v.severity} />\n <StatusBadge status={v.status} />\n {v.policy && (\n <button\n type=\"button\"\n onClick={() => navigateTo(`/policies/${v.policyId}`)}\n className=\"text-xs font-medium text-primary hover:underline\"\n >\n {v.policy.name}\n </button>\n )}\n </div>\n <p className=\"text-sm font-medium text-text-primary\">{v.message}</p>\n <p className=\"mt-1 text-xs text-text-secondary\">\n {t('adaptercloud.violations.detected', 'Detected')} {formatDate(v.detectedAt)}\n {' · '}\n <button\n type=\"button\"\n onClick={() => navigateTo(`/resources/${v.resourceId}`)}\n className=\"hover:underline\"\n >\n {t('adaptercloud.violations.viewResource', 'View resource')}\n </button>\n </p>\n {v.waiverReason && (\n <p className=\"mt-2 rounded-lg bg-bg-sunken px-3 py-1.5 text-xs text-text-secondary\">\n {t('adaptercloud.violations.waiver', 'Waiver')}: {v.waiverReason}\n </p>\n )}\n </div>\n\n {isOpen && (\n <div className=\"flex shrink-0 flex-wrap gap-2\">\n {v.status === 'OPEN' && (\n <button\n type=\"button\"\n disabled={resolve.isPending}\n onClick={() => transition(v.id, 'ACKNOWLEDGED')}\n className=\"inline-flex items-center gap-1.5 rounded-lg border border-border-subtle px-3 py-1.5 text-xs font-medium text-text-primary transition-colors hover:bg-accent disabled:opacity-50\"\n >\n <Check className=\"h-3.5 w-3.5\" />\n {t('adaptercloud.violations.acknowledge', 'Acknowledge')}\n </button>\n )}\n <button\n type=\"button\"\n disabled={resolve.isPending}\n onClick={() => transition(v.id, 'RESOLVED')}\n className=\"inline-flex items-center gap-1.5 rounded-lg border border-border-subtle px-3 py-1.5 text-xs font-medium text-status-success-text transition-colors hover:bg-status-success-bg-subtle disabled:opacity-50\"\n >\n <CheckCheck className=\"h-3.5 w-3.5\" />\n {t('adaptercloud.violations.resolve', 'Resolve')}\n </button>\n <button\n type=\"button\"\n disabled={resolve.isPending}\n onClick={() => {\n setActionError(null);\n setWaiverReason('');\n setWaiveTarget(v);\n }}\n className=\"inline-flex items-center gap-1.5 rounded-lg border border-border-subtle px-3 py-1.5 text-xs font-medium text-text-secondary transition-colors hover:bg-accent disabled:opacity-50\"\n >\n <FileText className=\"h-3.5 w-3.5\" />\n {t('adaptercloud.violations.waive', 'Waive')}\n </button>\n </div>\n )}\n </div>\n </GlassCard>\n );\n })}\n </div>\n\n {(offset > 0 || hasMore) && (\n <div className=\"mt-6 flex items-center justify-between\">\n <button\n type=\"button\"\n disabled={offset === 0}\n onClick={() => setOffset((o) => Math.max(0, o - PAGE_SIZE))}\n className=\"inline-flex items-center gap-1 rounded-lg border border-border-subtle px-3 py-2 text-sm font-medium text-text-primary transition-colors hover:bg-accent disabled:cursor-not-allowed disabled:opacity-50\"\n >\n <ChevronLeft className=\"h-4 w-4\" />\n {t('adaptercloud.common.prev', 'Previous')}\n </button>\n <span className=\"text-sm text-text-secondary\">\n {t('adaptercloud.common.showing', 'Showing')} {offset + 1}–{offset + items.length}\n </span>\n <button\n type=\"button\"\n disabled={!hasMore}\n onClick={() => setOffset((o) => o + PAGE_SIZE)}\n className=\"inline-flex items-center gap-1 rounded-lg border border-border-subtle px-3 py-2 text-sm font-medium text-text-primary transition-colors hover:bg-accent disabled:cursor-not-allowed disabled:opacity-50\"\n >\n {t('adaptercloud.common.next', 'Next')}\n <ChevronRight className=\"h-4 w-4\" />\n </button>\n </div>\n )}\n </DataState>\n\n {/* Waive dialog */}\n <ResponsiveDialog\n open={Boolean(waiveTarget)}\n onOpenChange={(open) => {\n if (!open) setWaiveTarget(null);\n }}\n >\n <ResponsiveDialogContent>\n <ResponsiveDialogHeader>\n <ResponsiveDialogTitle>\n {t('adaptercloud.violations.waiveTitle', 'Waive violation')}\n </ResponsiveDialogTitle>\n </ResponsiveDialogHeader>\n <div className=\"px-1 py-2\">\n <p className=\"mb-3 text-sm text-text-secondary\">\n {t(\n 'adaptercloud.violations.waivePrompt',\n 'A waiver requires a documented reason and is recorded on the violation.'\n )}\n </p>\n <textarea\n value={waiverReason}\n onChange={(e) => setWaiverReason(e.target.value)}\n rows={4}\n autoFocus\n placeholder={t(\n 'adaptercloud.violations.waiveReasonPlaceholder',\n 'Why is this acceptable?'\n )}\n className=\"w-full rounded-lg border border-border-subtle bg-bg-surface px-3 py-2 text-sm text-text-primary placeholder:text-text-secondary focus:outline-none focus:ring-2 focus:ring-[var(--color-focus-ring)]\"\n />\n {actionError && <p className=\"mt-2 text-sm text-status-error-text\">{actionError}</p>}\n </div>\n <ResponsiveDialogFooter>\n <button\n type=\"button\"\n onClick={() => setWaiveTarget(null)}\n className=\"rounded-lg border border-border-subtle px-4 py-2 text-sm font-medium text-text-primary transition-colors hover:bg-accent\"\n >\n {t('adaptercloud.common.cancel', 'Cancel')}\n </button>\n <button\n type=\"button\"\n disabled={resolve.isPending}\n onClick={confirmWaive}\n className=\"inline-flex items-center gap-2 rounded-lg bg-action-primary-bg px-4 py-2 text-sm font-medium text-action-primary-text transition-colors hover:opacity-90 disabled:opacity-50\"\n >\n {resolve.isPending && <Loader2 className=\"h-4 w-4 animate-spin\" />}\n {t('adaptercloud.violations.confirmWaive', 'Waive')}\n </button>\n </ResponsiveDialogFooter>\n </ResponsiveDialogContent>\n </ResponsiveDialog>\n </PageLayout>\n );\n}\n"],"mappings":"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"}
|