@burdenoff/microfe-adaptercloud 2026.706.3 → 2026.707.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/components/viz/TopologyGraph.js.map +1 -1
- package/dist/adaptercloud/pages/OverviewPage.js.map +1 -1
- package/dist/adaptercloud/pages/adapters/AdaptersListPage.js +105 -85
- package/dist/adaptercloud/pages/adapters/AdaptersListPage.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 +21 -8
- package/dist/adaptercloud/pages/policies/PolicyDetailPage.js.map +1 -1
- package/dist/adaptercloud/pages/topology/TopologyPage.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":"TopologyGraph.js","names":[],"sources":["../../../../src/adaptercloud/components/viz/TopologyGraph.tsx"],"sourcesContent":["/**\n * TopologyGraph — a tokenized, dependency graph of the estate. Deterministic\n * layered layout: nodes are grouped into columns by `group` (class of system),\n * stacked within the column; edges are drawn as SVG bezier curves between node\n * centres. Clicking a node highlights its incident edges and selects it (the\n * parent renders an inspector). No external graph lib — pure SVG + tokens.\n *\n * Data is REAL: the parent feeds nodes from `adaListResources` and edges from\n * `adaListTopologyEdges`.\n */\nimport { useMemo, type FC } from 'react';\n\nexport interface TopologyNode {\n id: string;\n label: string;\n /** Column grouping label, e.g. \"Public Cloud\", \"Kubernetes\", \"Edge / IoT\". */\n group: string;\n type?: string;\n /** Resource state — drives the node accent (DRIFTED → warning, ORPHANED → error, else default). */\n state?: string;\n}\n\nexport interface TopologyEdge {\n id: string;\n from: string;\n to: string;\n kind?: string;\n /** Optional: mark an edge as drifted to render it amber. */\n drifted?: boolean;\n}\n\nconst NODE_W = 168;\nconst NODE_H = 44;\nconst COL_GAP = 120;\nconst ROW_GAP = 26;\nconst PAD = 28;\n\ninterface TopologyGraphProps {\n nodes: TopologyNode[];\n edges: TopologyEdge[];\n selectedId?: string | null;\n onSelect?: (id: string) => void;\n}\n\nexport const TopologyGraph: FC<TopologyGraphProps> = ({ nodes, edges, selectedId, onSelect }) => {\n const { positioned, width, height, edgePaths } = useMemo(() => {\n // group → ordered nodes\n const groups = new Map<string, TopologyNode[]>();\n for (const n of nodes) {\n const g = groups.get(n.group) ?? [];\n g.push(n);\n groups.set(n.group, g);\n }\n const groupNames = [...groups.keys()];\n const maxCol = Math.max(1, ...[...groups.values()].map((g) => g.length));\n\n const pos = new Map<string, { x: number; y: number; node: TopologyNode }>();\n groupNames.forEach((name, ci) => {\n const colNodes = groups.get(name)!;\n const colHeight = colNodes.length * NODE_H + (colNodes.length - 1) * ROW_GAP;\n const fullHeight = maxCol * NODE_H + (maxCol - 1) * ROW_GAP;\n const yStart = PAD + (fullHeight - colHeight) / 2;\n colNodes.forEach((node, ri) => {\n pos.set(node.id, {\n x: PAD + ci * (NODE_W + COL_GAP),\n y: yStart + ri * (NODE_H + ROW_GAP),\n node,\n });\n });\n });\n\n const w = PAD * 2 + groupNames.length * NODE_W + Math.max(0, groupNames.length - 1) * COL_GAP;\n const h = PAD * 2 + maxCol * NODE_H + Math.max(0, maxCol - 1) * ROW_GAP;\n\n const paths = edges\n .map((e) => {\n const a = pos.get(e.from);\n const b = pos.get(e.to);\n if (!a || !b) return null;\n const x1 = a.x + NODE_W;\n const y1 = a.y + NODE_H / 2;\n const x2 = b.x;\n const y2 = b.y + NODE_H / 2;\n const mx = (x1 + x2) / 2;\n const d = `M${x1},${y1} C${mx},${y1} ${mx},${y2} ${x2},${y2}`;\n return { ...e, d };\n })\n .filter(Boolean) as (TopologyEdge & { d: string })[];\n\n return { positioned: [...pos.values()], width: w, height: h, edgePaths: paths };\n }, [nodes, edges]);\n\n const incident = useMemo(() => {\n if (!selectedId) return new Set<string>();\n return new Set(edgePaths.filter((e) => e.from === selectedId || e.to === selectedId).map((e) => e.id));\n }, [selectedId, edgePaths]);\n\n const stateAccent = (state?: string) =>\n state === 'DRIFTED'\n ? 'border-status-warning-text/60'\n : state === 'ORPHANED' || state === 'DELETED'\n ? 'border-status-error-text/60'\n : 'border-border-strong';\n\n return (\n <div className=\"relative w-full overflow-auto rounded-xl border border-border-subtle bg-bg-sunken/40\">\n <div className=\"relative\" style={{ width, height, minWidth: '100%' }}>\n <svg className=\"absolute inset-0 h-full w-full\" width={width} height={height} aria-hidden>\n {edgePaths.map((e) => {\n const active = incident.has(e.id);\n const cls = e.drifted\n ? 'stroke-status-warning-text'\n : active\n ? 'stroke-accent-blue'\n : 'stroke-border-strong';\n return (\n <path\n key={e.id}\n d={e.d}\n fill=\"none\"\n className={cls}\n strokeWidth={active ? 2 : 1.25}\n strokeOpacity={selectedId && !active && !e.drifted ? 0.35 : 0.9}\n />\n );\n })}\n </svg>\n\n {positioned.map(({ x, y, node }) => {\n const selected = node.id === selectedId;\n return (\n <button\n key={node.id}\n type=\"button\"\n onClick={() => onSelect?.(node.id)}\n style={{ left: x, top: y, width: NODE_W, height: NODE_H }}\n className={`absolute flex items-center gap-2 rounded-lg border bg-bg-surface/90 px-3 text-left backdrop-blur transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--color-focus-ring)] ${\n selected ? 'border-accent-blue ring-2 ring-accent-blue/40' : stateAccent(node.state)\n }`}\n title={node.type ? `${node.label} · ${node.type}` : node.label}\n >\n <span\n className={`h-2 w-2 shrink-0 rounded-full ${\n node.state === 'DRIFTED'\n ? 'bg-status-warning-text'\n : node.state === 'ORPHANED' || node.state === 'DELETED'\n ? 'bg-status-error-text'\n : 'bg-status-success-text'\n }`}\n aria-hidden\n />\n <span className=\"min-w-0 flex-1\">\n <span className=\"block truncate text-sm font-medium text-text-primary\"
|
|
1
|
+
{"version":3,"file":"TopologyGraph.js","names":[],"sources":["../../../../src/adaptercloud/components/viz/TopologyGraph.tsx"],"sourcesContent":["/**\n * TopologyGraph — a tokenized, dependency graph of the estate. Deterministic\n * layered layout: nodes are grouped into columns by `group` (class of system),\n * stacked within the column; edges are drawn as SVG bezier curves between node\n * centres. Clicking a node highlights its incident edges and selects it (the\n * parent renders an inspector). No external graph lib — pure SVG + tokens.\n *\n * Data is REAL: the parent feeds nodes from `adaListResources` and edges from\n * `adaListTopologyEdges`.\n */\nimport { useMemo, type FC } from 'react';\n\nexport interface TopologyNode {\n id: string;\n label: string;\n /** Column grouping label, e.g. \"Public Cloud\", \"Kubernetes\", \"Edge / IoT\". */\n group: string;\n type?: string;\n /** Resource state — drives the node accent (DRIFTED → warning, ORPHANED → error, else default). */\n state?: string;\n}\n\nexport interface TopologyEdge {\n id: string;\n from: string;\n to: string;\n kind?: string;\n /** Optional: mark an edge as drifted to render it amber. */\n drifted?: boolean;\n}\n\nconst NODE_W = 168;\nconst NODE_H = 44;\nconst COL_GAP = 120;\nconst ROW_GAP = 26;\nconst PAD = 28;\n\ninterface TopologyGraphProps {\n nodes: TopologyNode[];\n edges: TopologyEdge[];\n selectedId?: string | null;\n onSelect?: (id: string) => void;\n}\n\nexport const TopologyGraph: FC<TopologyGraphProps> = ({ nodes, edges, selectedId, onSelect }) => {\n const { positioned, width, height, edgePaths } = useMemo(() => {\n // group → ordered nodes\n const groups = new Map<string, TopologyNode[]>();\n for (const n of nodes) {\n const g = groups.get(n.group) ?? [];\n g.push(n);\n groups.set(n.group, g);\n }\n const groupNames = [...groups.keys()];\n const maxCol = Math.max(1, ...[...groups.values()].map((g) => g.length));\n\n const pos = new Map<string, { x: number; y: number; node: TopologyNode }>();\n groupNames.forEach((name, ci) => {\n const colNodes = groups.get(name)!;\n const colHeight = colNodes.length * NODE_H + (colNodes.length - 1) * ROW_GAP;\n const fullHeight = maxCol * NODE_H + (maxCol - 1) * ROW_GAP;\n const yStart = PAD + (fullHeight - colHeight) / 2;\n colNodes.forEach((node, ri) => {\n pos.set(node.id, {\n x: PAD + ci * (NODE_W + COL_GAP),\n y: yStart + ri * (NODE_H + ROW_GAP),\n node,\n });\n });\n });\n\n const w = PAD * 2 + groupNames.length * NODE_W + Math.max(0, groupNames.length - 1) * COL_GAP;\n const h = PAD * 2 + maxCol * NODE_H + Math.max(0, maxCol - 1) * ROW_GAP;\n\n const paths = edges\n .map((e) => {\n const a = pos.get(e.from);\n const b = pos.get(e.to);\n if (!a || !b) return null;\n const x1 = a.x + NODE_W;\n const y1 = a.y + NODE_H / 2;\n const x2 = b.x;\n const y2 = b.y + NODE_H / 2;\n const mx = (x1 + x2) / 2;\n const d = `M${x1},${y1} C${mx},${y1} ${mx},${y2} ${x2},${y2}`;\n return { ...e, d };\n })\n .filter(Boolean) as (TopologyEdge & { d: string })[];\n\n return { positioned: [...pos.values()], width: w, height: h, edgePaths: paths };\n }, [nodes, edges]);\n\n const incident = useMemo(() => {\n if (!selectedId) return new Set<string>();\n return new Set(\n edgePaths.filter((e) => e.from === selectedId || e.to === selectedId).map((e) => e.id)\n );\n }, [selectedId, edgePaths]);\n\n const stateAccent = (state?: string) =>\n state === 'DRIFTED'\n ? 'border-status-warning-text/60'\n : state === 'ORPHANED' || state === 'DELETED'\n ? 'border-status-error-text/60'\n : 'border-border-strong';\n\n return (\n <div className=\"relative w-full overflow-auto rounded-xl border border-border-subtle bg-bg-sunken/40\">\n <div className=\"relative\" style={{ width, height, minWidth: '100%' }}>\n <svg className=\"absolute inset-0 h-full w-full\" width={width} height={height} aria-hidden>\n {edgePaths.map((e) => {\n const active = incident.has(e.id);\n const cls = e.drifted\n ? 'stroke-status-warning-text'\n : active\n ? 'stroke-accent-blue'\n : 'stroke-border-strong';\n return (\n <path\n key={e.id}\n d={e.d}\n fill=\"none\"\n className={cls}\n strokeWidth={active ? 2 : 1.25}\n strokeOpacity={selectedId && !active && !e.drifted ? 0.35 : 0.9}\n />\n );\n })}\n </svg>\n\n {positioned.map(({ x, y, node }) => {\n const selected = node.id === selectedId;\n return (\n <button\n key={node.id}\n type=\"button\"\n onClick={() => onSelect?.(node.id)}\n style={{ left: x, top: y, width: NODE_W, height: NODE_H }}\n className={`absolute flex items-center gap-2 rounded-lg border bg-bg-surface/90 px-3 text-left backdrop-blur transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--color-focus-ring)] ${\n selected ? 'border-accent-blue ring-2 ring-accent-blue/40' : stateAccent(node.state)\n }`}\n title={node.type ? `${node.label} · ${node.type}` : node.label}\n >\n <span\n className={`h-2 w-2 shrink-0 rounded-full ${\n node.state === 'DRIFTED'\n ? 'bg-status-warning-text'\n : node.state === 'ORPHANED' || node.state === 'DELETED'\n ? 'bg-status-error-text'\n : 'bg-status-success-text'\n }`}\n aria-hidden\n />\n <span className=\"min-w-0 flex-1\">\n <span className=\"block truncate text-sm font-medium text-text-primary\">\n {node.label}\n </span>\n {node.type && (\n <span className=\"block truncate font-mono text-[10px] text-text-secondary\">\n {node.type}\n </span>\n )}\n </span>\n </button>\n );\n })}\n </div>\n </div>\n );\n};\n"],"mappings":"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"}
|
|
@@ -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 Clock,\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 { StackedBar, BarChart, Sparkline } from '../components/viz';\nimport { useAdapterCloud } from '../context/AdapterCloudContext';\nimport {\n useAdapters,\n useConnections,\n useResources,\n useViolations,\n useDriftRecords,\n useSyncRuns,\n useCostSummary,\n useCostRecords,\n} from '../hooks/useAdapterCloudApi';\nimport { formatRelativeTime } from '../utils/formatters';\nimport { formatMoney, severityRank, syncKindLabels } from '../utils/domain';\nimport type { AdaPageInfo, AdaSeverity } from '../types';\n\n/**\n * Estate count for a tile: prefer the estate-wide `pageInfo.totalCount`, fall\n * back to the loaded page length when the field is absent (null/undefined), and\n * render an em dash before the first page has loaded.\n */\nfunction countLabel(page: { items: unknown[]; pageInfo: AdaPageInfo } | undefined): string {\n if (!page) return '—';\n return String(page.pageInfo.totalCount ?? page.items.length);\n}\n\n/**\n * Bucket cost records into an ascending monthly series (sum of `amount` per\n * calendar month keyed by `periodStart`). Feeds the cost-trend sparkline.\n */\nfunction monthlyCostSeries(records: { periodStart: string; amount: number }[]): number[] {\n const buckets = new Map<string, number>();\n for (const r of records) {\n const d = new Date(r.periodStart);\n if (Number.isNaN(d.getTime())) continue;\n const key = `${d.getUTCFullYear()}-${String(d.getUTCMonth() + 1).padStart(2, '0')}`;\n buckets.set(key, (buckets.get(key) ?? 0) + r.amount);\n }\n return [...buckets.entries()].sort(([a], [b]) => a.localeCompare(b)).map(([, total]) => total);\n}\n\n/**\n * Human-readable run duration (\"3m 12s\", \"1h 05m\") from a run's start/finish\n * timestamps. Returns null when either bound is missing or invalid (e.g. a\n * QUEUED/RUNNING run that has not finished).\n */\nfunction formatRunDuration(startedAt: string | null, finishedAt: string | null): string | null {\n if (!startedAt || !finishedAt) return null;\n const start = new Date(startedAt).getTime();\n const end = new Date(finishedAt).getTime();\n if (Number.isNaN(start) || Number.isNaN(end) || end < start) return null;\n const totalSeconds = Math.round((end - start) / 1000);\n const hours = Math.floor(totalSeconds / 3600);\n const minutes = Math.floor((totalSeconds % 3600) / 60);\n const seconds = totalSeconds % 60;\n if (hours > 0) return `${hours}h ${String(minutes).padStart(2, '0')}m`;\n if (minutes > 0) return `${minutes}m ${String(seconds).padStart(2, '0')}s`;\n return `${seconds}s`;\n}\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 // Estate count tiles prefer the estate-wide `pageInfo.totalCount` (svc-added)\n // and fall back to the loaded page length when it is absent, so the request\n // limit still bounds the fallback (limit:1 would cap the fallback at 1).\n const adapters = useAdapters({ pagination: { limit: 100 } });\n const connections = useConnections({ pagination: { limit: 100 } });\n const resources = useResources({ pagination: { limit: 100 } });\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: countLabel(adapters.data),\n label: t('adaptercloud.overview.adapters', 'Adapters'),\n onClick: () => navigateTo('/adapters'),\n },\n {\n icon: <Cable className=\"h-5 w-5\" />,\n value: countLabel(connections.data),\n label: t('adaptercloud.overview.connections', 'Connections'),\n onClick: () => navigateTo('/connections'),\n },\n {\n icon: <Server className=\"h-5 w-5\" />,\n value: countLabel(resources.data),\n label: t('adaptercloud.overview.resources', 'Resources'),\n onClick: () => navigateTo('/resources'),\n },\n {\n icon: <ShieldAlert className=\"h-5 w-5\" />,\n value: countLabel(openViolations.data),\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-accent-blue 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-accent-blue 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-accent-blue 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-accent-blue 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 { AdaPageInfo, AdaSeverity } from '../types';\n\n/**\n * Estate count for a tile: prefer the estate-wide `pageInfo.totalCount`, fall\n * back to the loaded page length when the field is absent (null/undefined), and\n * render an em dash before the first page has loaded.\n */\nfunction countLabel(page: { items: unknown[]; pageInfo: AdaPageInfo } | undefined): string {\n if (!page) return '—';\n return String(page.pageInfo.totalCount ?? page.items.length);\n}\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 // Estate count tiles prefer the estate-wide `pageInfo.totalCount` (svc-added)\n // and fall back to the loaded page length when it is absent, so the request\n // limit still bounds the fallback (limit:1 would cap the fallback at 1).\n const adapters = useAdapters({ pagination: { limit: 100 } });\n const connections = useConnections({ pagination: { limit: 100 } });\n const resources = useResources({ pagination: { limit: 100 } });\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: countLabel(adapters.data),\n label: t('adaptercloud.overview.adapters', 'Adapters'),\n onClick: () => navigateTo('/adapters'),\n },\n {\n icon: <Cable className=\"h-5 w-5\" />,\n value: countLabel(connections.data),\n label: t('adaptercloud.overview.connections', 'Connections'),\n onClick: () => navigateTo('/connections'),\n },\n {\n icon: <Server className=\"h-5 w-5\" />,\n value: countLabel(resources.data),\n label: t('adaptercloud.overview.resources', 'Resources'),\n onClick: () => navigateTo('/resources'),\n },\n {\n icon: <ShieldAlert className=\"h-5 w-5\" />,\n value: countLabel(openViolations.data),\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-accent-blue 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-accent-blue 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-accent-blue 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-accent-blue 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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"}
|
|
@@ -7,161 +7,181 @@ import { useAdapters as o } from "../../hooks/useAdapterCloudApi.js";
|
|
|
7
7
|
import { formatRelativeTime as s } from "../../utils/formatters.js";
|
|
8
8
|
import { useCallback as c, useState as l } from "react";
|
|
9
9
|
import { useI18n as u } from "@burdenoff/fe-libs/shared/providers/shell/I18nProvider";
|
|
10
|
-
import { Boxes as d, ChevronLeft as f, ChevronRight as p,
|
|
11
|
-
import { jsx as
|
|
12
|
-
import { GlassCard as
|
|
10
|
+
import { Boxes as d, ChevronLeft as f, ChevronRight as p, Cloud as m, Cpu as h, Database as g, HardDrive as _, Network as v, Plus as y, Puzzle as b, Radio as x, ServerCog as S } from "lucide-react";
|
|
11
|
+
import { jsx as C, jsxs as w } from "react/jsx-runtime";
|
|
12
|
+
import { GlassCard as T } from "@burdenoff/fe-libs/ui";
|
|
13
13
|
//#region src/adaptercloud/pages/adapters/AdaptersListPage.tsx
|
|
14
|
-
var
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
14
|
+
var E = 20, D = {
|
|
15
|
+
PUBLIC_CLOUD: m,
|
|
16
|
+
SAAS: m,
|
|
17
|
+
PRIVATE_CLOUD: S,
|
|
18
|
+
ON_PREM: _,
|
|
19
|
+
EDGE: x,
|
|
20
|
+
IOT: h,
|
|
21
|
+
KUBERNETES: d,
|
|
22
|
+
NETWORK: v,
|
|
23
|
+
DATA_SYSTEM: g,
|
|
24
|
+
CUSTOM: b
|
|
25
|
+
};
|
|
26
|
+
function O() {
|
|
27
|
+
let { navigateTo: m } = e(), { t: h } = u(), g = c((e, t) => {
|
|
28
|
+
let n = h(e);
|
|
18
29
|
return n === e ? t : n;
|
|
19
|
-
}, [
|
|
20
|
-
kind:
|
|
21
|
-
status:
|
|
30
|
+
}, [h]), [_, v] = l(""), [b, x] = l(""), [S, O] = l(0), k = o({
|
|
31
|
+
kind: _ || void 0,
|
|
32
|
+
status: b || void 0,
|
|
22
33
|
pagination: {
|
|
23
|
-
limit:
|
|
24
|
-
offset:
|
|
34
|
+
limit: E,
|
|
35
|
+
offset: S
|
|
25
36
|
}
|
|
26
|
-
}),
|
|
27
|
-
e(t),
|
|
37
|
+
}), A = k.data?.items ?? [], j = k.data?.pageInfo.hasMore ?? !1, M = c((e) => (t) => {
|
|
38
|
+
e(t), O(0);
|
|
28
39
|
}, []);
|
|
29
|
-
return /* @__PURE__ */
|
|
30
|
-
title:
|
|
31
|
-
description:
|
|
32
|
-
actions: /* @__PURE__ */
|
|
40
|
+
return /* @__PURE__ */ w(t, {
|
|
41
|
+
title: g("adaptercloud.adapters.title", "Adapters"),
|
|
42
|
+
description: g("adaptercloud.adapters.subtitle", "The catalog of connectors that bridge each class of system into the fabric."),
|
|
43
|
+
actions: /* @__PURE__ */ w("button", {
|
|
33
44
|
type: "button",
|
|
34
|
-
onClick: () =>
|
|
45
|
+
onClick: () => m("/adapters/new"),
|
|
35
46
|
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",
|
|
36
|
-
children: [/* @__PURE__ */
|
|
47
|
+
children: [/* @__PURE__ */ C(y, { className: "h-4 w-4" }), g("adaptercloud.adapters.new", "New adapter")]
|
|
37
48
|
}),
|
|
38
|
-
children: [/* @__PURE__ */
|
|
49
|
+
children: [/* @__PURE__ */ w("div", {
|
|
39
50
|
className: "mb-6 flex flex-wrap gap-3",
|
|
40
|
-
children: [/* @__PURE__ */
|
|
41
|
-
value:
|
|
42
|
-
onChange: (e) =>
|
|
43
|
-
"aria-label":
|
|
51
|
+
children: [/* @__PURE__ */ w("select", {
|
|
52
|
+
value: _,
|
|
53
|
+
onChange: (e) => M(v)(e.target.value),
|
|
54
|
+
"aria-label": g("adaptercloud.adapters.filterByKind", "Filter by kind"),
|
|
44
55
|
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)]",
|
|
45
|
-
children: [/* @__PURE__ */
|
|
56
|
+
children: [/* @__PURE__ */ C("option", {
|
|
46
57
|
value: "",
|
|
47
|
-
children:
|
|
48
|
-
}), Object.keys(n).map((e) => /* @__PURE__ */
|
|
58
|
+
children: g("adaptercloud.adapters.allKinds", "All kinds")
|
|
59
|
+
}), Object.keys(n).map((e) => /* @__PURE__ */ C("option", {
|
|
49
60
|
value: e,
|
|
50
61
|
children: n[e]
|
|
51
62
|
}, e))]
|
|
52
|
-
}), /* @__PURE__ */
|
|
53
|
-
value:
|
|
54
|
-
onChange: (e) =>
|
|
55
|
-
"aria-label":
|
|
63
|
+
}), /* @__PURE__ */ w("select", {
|
|
64
|
+
value: b,
|
|
65
|
+
onChange: (e) => M(x)(e.target.value),
|
|
66
|
+
"aria-label": g("adaptercloud.adapters.filterByStatus", "Filter by status"),
|
|
56
67
|
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)]",
|
|
57
|
-
children: [/* @__PURE__ */
|
|
68
|
+
children: [/* @__PURE__ */ C("option", {
|
|
58
69
|
value: "",
|
|
59
|
-
children:
|
|
60
|
-
}), Object.keys(r).map((e) => /* @__PURE__ */
|
|
70
|
+
children: g("adaptercloud.adapters.allStatuses", "All statuses")
|
|
71
|
+
}), Object.keys(r).map((e) => /* @__PURE__ */ C("option", {
|
|
61
72
|
value: e,
|
|
62
73
|
children: r[e]
|
|
63
74
|
}, e))]
|
|
64
75
|
})]
|
|
65
|
-
}), /* @__PURE__ */
|
|
66
|
-
isLoading:
|
|
67
|
-
isError:
|
|
68
|
-
isEmpty:
|
|
69
|
-
error:
|
|
70
|
-
onRetry: () => void
|
|
71
|
-
emptyTitle:
|
|
72
|
-
emptyDescription:
|
|
73
|
-
emptyIcon: /* @__PURE__ */
|
|
74
|
-
emptyAction: /* @__PURE__ */
|
|
76
|
+
}), /* @__PURE__ */ w(a, {
|
|
77
|
+
isLoading: k.isLoading,
|
|
78
|
+
isError: k.isError,
|
|
79
|
+
isEmpty: A.length === 0,
|
|
80
|
+
error: k.error,
|
|
81
|
+
onRetry: () => void k.refetch(),
|
|
82
|
+
emptyTitle: g("adaptercloud.adapters.emptyTitle", "No adapters yet"),
|
|
83
|
+
emptyDescription: g("adaptercloud.adapters.emptyDescription", "Define your first adapter to start bridging a class of system into the fabric."),
|
|
84
|
+
emptyIcon: /* @__PURE__ */ C(d, { className: "h-10 w-10" }),
|
|
85
|
+
emptyAction: /* @__PURE__ */ C("button", {
|
|
75
86
|
type: "button",
|
|
76
|
-
onClick: () =>
|
|
87
|
+
onClick: () => m("/adapters/new"),
|
|
77
88
|
className: "rounded-lg bg-action-primary-bg px-4 py-2 text-sm font-medium text-action-primary-text transition-colors hover:opacity-90",
|
|
78
|
-
children:
|
|
89
|
+
children: g("adaptercloud.adapters.new", "New adapter")
|
|
79
90
|
}),
|
|
80
|
-
children: [/* @__PURE__ */
|
|
91
|
+
children: [/* @__PURE__ */ C("div", {
|
|
81
92
|
className: "grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3",
|
|
82
|
-
children:
|
|
93
|
+
children: A.map((e) => /* @__PURE__ */ w(T, {
|
|
83
94
|
role: "button",
|
|
84
95
|
tabIndex: 0,
|
|
85
|
-
onClick: () =>
|
|
96
|
+
onClick: () => m(`/adapters/${e.id}`),
|
|
86
97
|
onKeyDown: (t) => {
|
|
87
|
-
(t.key === "Enter" || t.key === " ") && (t.preventDefault(),
|
|
98
|
+
(t.key === "Enter" || t.key === " ") && (t.preventDefault(), m(`/adapters/${e.id}`));
|
|
88
99
|
},
|
|
89
100
|
className: "cursor-pointer p-5 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--color-focus-ring)]",
|
|
90
101
|
children: [
|
|
91
|
-
/* @__PURE__ */
|
|
102
|
+
/* @__PURE__ */ w("div", {
|
|
92
103
|
className: "flex items-start justify-between gap-2",
|
|
93
|
-
children: [/* @__PURE__ */
|
|
94
|
-
className: "min-w-0",
|
|
95
|
-
children: [/* @__PURE__ */
|
|
96
|
-
className: "
|
|
97
|
-
children:
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
104
|
+
children: [/* @__PURE__ */ w("div", {
|
|
105
|
+
className: "flex min-w-0 items-center gap-3",
|
|
106
|
+
children: [/* @__PURE__ */ C("span", {
|
|
107
|
+
className: "flex h-10 w-10 shrink-0 items-center justify-center rounded-lg bg-accent-blue-subtle text-accent-blue",
|
|
108
|
+
children: (() => {
|
|
109
|
+
let t = D[e.kind];
|
|
110
|
+
return /* @__PURE__ */ C(t, { className: "h-5 w-5" });
|
|
111
|
+
})()
|
|
112
|
+
}), /* @__PURE__ */ w("div", {
|
|
113
|
+
className: "min-w-0",
|
|
114
|
+
children: [/* @__PURE__ */ C("h3", {
|
|
115
|
+
className: "truncate text-base font-semibold text-text-primary",
|
|
116
|
+
children: e.name
|
|
117
|
+
}), /* @__PURE__ */ C("p", {
|
|
118
|
+
className: "mt-0.5 truncate text-xs text-text-secondary",
|
|
119
|
+
children: e.slug
|
|
120
|
+
})]
|
|
101
121
|
})]
|
|
102
|
-
}), /* @__PURE__ */
|
|
122
|
+
}), /* @__PURE__ */ C(i, { status: e.status })]
|
|
103
123
|
}),
|
|
104
|
-
/* @__PURE__ */
|
|
124
|
+
/* @__PURE__ */ w("div", {
|
|
105
125
|
className: "mt-3 flex flex-wrap items-center gap-2 text-xs text-text-secondary",
|
|
106
126
|
children: [
|
|
107
|
-
/* @__PURE__ */
|
|
127
|
+
/* @__PURE__ */ C("span", {
|
|
108
128
|
className: "inline-flex items-center rounded-full bg-bg-sunken px-2 py-0.5",
|
|
109
129
|
children: n[e.kind]
|
|
110
130
|
}),
|
|
111
|
-
/* @__PURE__ */
|
|
112
|
-
e.vendor && /* @__PURE__ */
|
|
131
|
+
/* @__PURE__ */ w("span", { children: ["v", e.version] }),
|
|
132
|
+
e.vendor && /* @__PURE__ */ w("span", { children: ["· ", e.vendor] })
|
|
113
133
|
]
|
|
114
134
|
}),
|
|
115
|
-
e.description && /* @__PURE__ */
|
|
135
|
+
e.description && /* @__PURE__ */ C("p", {
|
|
116
136
|
className: "mt-3 line-clamp-2 text-sm text-text-secondary",
|
|
117
137
|
children: e.description
|
|
118
138
|
}),
|
|
119
|
-
e.capabilities.length > 0 && /* @__PURE__ */
|
|
139
|
+
e.capabilities.length > 0 && /* @__PURE__ */ w("div", {
|
|
120
140
|
className: "mt-3 flex flex-wrap gap-1.5",
|
|
121
|
-
children: [e.capabilities.slice(0, 4).map((e) => /* @__PURE__ */
|
|
141
|
+
children: [e.capabilities.slice(0, 4).map((e) => /* @__PURE__ */ C("span", {
|
|
122
142
|
className: "inline-flex items-center rounded-full bg-accent-blue-subtle px-2 py-0.5 text-xs text-accent-blue",
|
|
123
143
|
children: e
|
|
124
|
-
}, e)), e.capabilities.length > 4 && /* @__PURE__ */
|
|
144
|
+
}, e)), e.capabilities.length > 4 && /* @__PURE__ */ w("span", {
|
|
125
145
|
className: "text-xs text-text-secondary",
|
|
126
146
|
children: ["+", e.capabilities.length - 4]
|
|
127
147
|
})]
|
|
128
148
|
}),
|
|
129
|
-
/* @__PURE__ */
|
|
149
|
+
/* @__PURE__ */ w("p", {
|
|
130
150
|
className: "mt-4 text-xs text-text-secondary",
|
|
131
151
|
children: [
|
|
132
|
-
|
|
152
|
+
g("adaptercloud.common.updated", "Updated"),
|
|
133
153
|
" ",
|
|
134
154
|
s(e.updatedAt)
|
|
135
155
|
]
|
|
136
156
|
})
|
|
137
157
|
]
|
|
138
158
|
}, e.id))
|
|
139
|
-
}), (
|
|
159
|
+
}), (S > 0 || j) && /* @__PURE__ */ w("div", {
|
|
140
160
|
className: "mt-6 flex items-center justify-between",
|
|
141
161
|
children: [
|
|
142
|
-
/* @__PURE__ */
|
|
162
|
+
/* @__PURE__ */ w("button", {
|
|
143
163
|
type: "button",
|
|
144
|
-
disabled:
|
|
145
|
-
onClick: () =>
|
|
164
|
+
disabled: S === 0,
|
|
165
|
+
onClick: () => O((e) => Math.max(0, e - E)),
|
|
146
166
|
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",
|
|
147
|
-
children: [/* @__PURE__ */
|
|
167
|
+
children: [/* @__PURE__ */ C(f, { className: "h-4 w-4" }), g("adaptercloud.common.prev", "Previous")]
|
|
148
168
|
}),
|
|
149
|
-
/* @__PURE__ */
|
|
169
|
+
/* @__PURE__ */ w("span", {
|
|
150
170
|
className: "text-sm text-text-secondary",
|
|
151
171
|
children: [
|
|
152
|
-
|
|
172
|
+
g("adaptercloud.common.showing", "Showing"),
|
|
153
173
|
" ",
|
|
154
|
-
|
|
174
|
+
S + 1,
|
|
155
175
|
"–",
|
|
156
|
-
|
|
176
|
+
S + A.length
|
|
157
177
|
]
|
|
158
178
|
}),
|
|
159
|
-
/* @__PURE__ */
|
|
179
|
+
/* @__PURE__ */ w("button", {
|
|
160
180
|
type: "button",
|
|
161
|
-
disabled: !
|
|
162
|
-
onClick: () =>
|
|
181
|
+
disabled: !j,
|
|
182
|
+
onClick: () => O((e) => e + E),
|
|
163
183
|
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",
|
|
164
|
-
children: [
|
|
184
|
+
children: [g("adaptercloud.common.next", "Next"), /* @__PURE__ */ C(p, { className: "h-4 w-4" })]
|
|
165
185
|
})
|
|
166
186
|
]
|
|
167
187
|
})]
|
|
@@ -169,6 +189,6 @@ function y() {
|
|
|
169
189
|
});
|
|
170
190
|
}
|
|
171
191
|
//#endregion
|
|
172
|
-
export {
|
|
192
|
+
export { O as AdaptersListPage };
|
|
173
193
|
|
|
174
194
|
//# sourceMappingURL=AdaptersListPage.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AdaptersListPage.js","names":[],"sources":["../../../../src/adaptercloud/pages/adapters/AdaptersListPage.tsx"],"sourcesContent":["/**\n * Adapters List Page\n * @module adaptercloud/pages/adapters\n *\n * The adapter catalog — every reusable connector definition in the workspace.\n * Lists from `adaListAdapters` with kind/status filters and offset pagination\n * (driven by `AdaPageInfo.hasMore`).\n */\n\nimport { useState, useCallback, useMemo } from 'react';\nimport { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';\nimport {\n Boxes,\n Cloud,\n Cpu,\n Database,\n HardDrive,\n Network,\n Plus,\n Puzzle,\n Radio,\n Search,\n ServerCog,\n X,\n ChevronLeft,\n ChevronRight,\n type LucideIcon,\n} 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 { useAdapters } from '../../hooks/useAdapterCloudApi';\nimport { adapterKindLabels, adapterStatusLabels } from '../../utils/domain';\nimport { formatRelativeTime } from '../../utils/formatters';\nimport type { AdaAdapterKind, AdaAdapterStatus } from '../../types';\n\nconst PAGE_SIZE = 20;\n\n/** Per-kind glyph — mirrors the mock's vendor logo tile on each catalog card. */\nconst kindIcons: Record<AdaAdapterKind, LucideIcon> = {\n PUBLIC_CLOUD: Cloud,\n SAAS: Cloud,\n PRIVATE_CLOUD: ServerCog,\n ON_PREM: HardDrive,\n EDGE: Radio,\n IOT: Cpu,\n KUBERNETES: Boxes,\n NETWORK: Network,\n DATA_SYSTEM: Database,\n CUSTOM: Puzzle,\n};\n\nexport function AdaptersListPage() {\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<AdaAdapterKind | ''>('');\n const [status, setStatus] = useState<AdaAdapterStatus | ''>('');\n const [offset, setOffset] = useState(0);\n\n const query = useAdapters({\n kind: kind || undefined,\n status: status || undefined,\n pagination: { limit: PAGE_SIZE, offset },\n });\n\n const items = query.data?.items ?? [];\n const hasMore = query.data?.pageInfo.hasMore ?? false;\n\n const resetTo = useCallback(<T,>(setter: (v: T) => void) => {\n return (v: T) => {\n setter(v);\n setOffset(0);\n };\n }, []);\n\n return (\n <PageLayout\n title={t('adaptercloud.adapters.title', 'Adapters')}\n description={t(\n 'adaptercloud.adapters.subtitle',\n 'The catalog of connectors that bridge each class of system into the fabric.'\n )}\n actions={\n <button\n type=\"button\"\n onClick={() => navigateTo('/adapters/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.adapters.new', 'New adapter')}\n </button>\n }\n >\n {/* Filters */}\n <div className=\"mb-6 flex flex-wrap gap-3\">\n <select\n value={kind}\n onChange={(e) => resetTo(setKind)(e.target.value as AdaAdapterKind | '')}\n aria-label={t('adaptercloud.adapters.filterByKind', 'Filter by kind')}\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.adapters.allKinds', 'All kinds')}</option>\n {(Object.keys(adapterKindLabels) as AdaAdapterKind[]).map((k) => (\n <option key={k} value={k}>\n {adapterKindLabels[k]}\n </option>\n ))}\n </select>\n <select\n value={status}\n onChange={(e) => resetTo(setStatus)(e.target.value as AdaAdapterStatus | '')}\n aria-label={t('adaptercloud.adapters.filterByStatus', 'Filter by status')}\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.adapters.allStatuses', 'All statuses')}</option>\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\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.adapters.emptyTitle', 'No adapters yet')}\n emptyDescription={t(\n 'adaptercloud.adapters.emptyDescription',\n 'Define your first adapter to start bridging a class of system into the fabric.'\n )}\n emptyIcon={<Boxes className=\"h-10 w-10\" />}\n emptyAction={\n <button\n type=\"button\"\n onClick={() => navigateTo('/adapters/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.adapters.new', 'New adapter')}\n </button>\n }\n >\n <div className=\"grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3\">\n {items.map((adapter) => (\n <GlassCard\n key={adapter.id}\n role=\"button\"\n tabIndex={0}\n onClick={() => navigateTo(`/adapters/${adapter.id}`)}\n onKeyDown={(e) => {\n if (e.key === 'Enter' || e.key === ' ') {\n e.preventDefault();\n navigateTo(`/adapters/${adapter.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 {adapter.name}\n </h3>\n <p className=\"mt-0.5 truncate text-xs text-text-secondary\">{adapter.slug}</p>\n </div>\n <StatusBadge status={adapter.status} />\n </div>\n\n <div className=\"mt-3 flex flex-wrap items-center gap-2 text-xs text-text-secondary\">\n <span className=\"inline-flex items-center rounded-full bg-bg-sunken px-2 py-0.5\">\n {adapterKindLabels[adapter.kind]}\n </span>\n <span>v{adapter.version}</span>\n {adapter.vendor && <span>· {adapter.vendor}</span>}\n </div>\n\n {adapter.description && (\n <p className=\"mt-3 line-clamp-2 text-sm text-text-secondary\">\n {adapter.description}\n </p>\n )}\n\n {adapter.capabilities.length > 0 && (\n <div className=\"mt-3 flex flex-wrap gap-1.5\">\n {adapter.capabilities.slice(0, 4).map((cap) => (\n <span\n key={cap}\n className=\"inline-flex items-center rounded-full bg-accent-blue-subtle px-2 py-0.5 text-xs text-accent-blue\"\n >\n {cap}\n </span>\n ))}\n {adapter.capabilities.length > 4 && (\n <span className=\"text-xs text-text-secondary\">\n +{adapter.capabilities.length - 4}\n </span>\n )}\n </div>\n )}\n\n <p className=\"mt-4 text-xs text-text-secondary\">\n {t('adaptercloud.common.updated', 'Updated')}{' '}\n {formatRelativeTime(adapter.updatedAt)}\n </p>\n </GlassCard>\n ))}\n </div>\n\n {/* Pagination */}\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
|
+
{"version":3,"file":"AdaptersListPage.js","names":[],"sources":["../../../../src/adaptercloud/pages/adapters/AdaptersListPage.tsx"],"sourcesContent":["/**\n * Adapters List Page\n * @module adaptercloud/pages/adapters\n *\n * The adapter catalog — every reusable connector definition in the workspace.\n * Lists from `adaListAdapters` with kind/status filters and offset pagination\n * (driven by `AdaPageInfo.hasMore`).\n */\n\nimport { useState, useCallback } from 'react';\nimport { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';\nimport {\n Boxes,\n Cloud,\n Cpu,\n Database,\n HardDrive,\n Network,\n Plus,\n Puzzle,\n Radio,\n ServerCog,\n ChevronLeft,\n ChevronRight,\n type LucideIcon,\n} 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 { useAdapters } from '../../hooks/useAdapterCloudApi';\nimport { adapterKindLabels, adapterStatusLabels } from '../../utils/domain';\nimport { formatRelativeTime } from '../../utils/formatters';\nimport type { AdaAdapterKind, AdaAdapterStatus } from '../../types';\n\nconst PAGE_SIZE = 20;\n\n/** Per-kind glyph — mirrors the mock's vendor logo tile on each catalog card. */\nconst kindIcons: Record<AdaAdapterKind, LucideIcon> = {\n PUBLIC_CLOUD: Cloud,\n SAAS: Cloud,\n PRIVATE_CLOUD: ServerCog,\n ON_PREM: HardDrive,\n EDGE: Radio,\n IOT: Cpu,\n KUBERNETES: Boxes,\n NETWORK: Network,\n DATA_SYSTEM: Database,\n CUSTOM: Puzzle,\n};\n\nexport function AdaptersListPage() {\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<AdaAdapterKind | ''>('');\n const [status, setStatus] = useState<AdaAdapterStatus | ''>('');\n const [offset, setOffset] = useState(0);\n\n const query = useAdapters({\n kind: kind || undefined,\n status: status || undefined,\n pagination: { limit: PAGE_SIZE, offset },\n });\n\n const items = query.data?.items ?? [];\n const hasMore = query.data?.pageInfo.hasMore ?? false;\n\n const resetTo = useCallback(<T,>(setter: (v: T) => void) => {\n return (v: T) => {\n setter(v);\n setOffset(0);\n };\n }, []);\n\n return (\n <PageLayout\n title={t('adaptercloud.adapters.title', 'Adapters')}\n description={t(\n 'adaptercloud.adapters.subtitle',\n 'The catalog of connectors that bridge each class of system into the fabric.'\n )}\n actions={\n <button\n type=\"button\"\n onClick={() => navigateTo('/adapters/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.adapters.new', 'New adapter')}\n </button>\n }\n >\n {/* Filters */}\n <div className=\"mb-6 flex flex-wrap gap-3\">\n <select\n value={kind}\n onChange={(e) => resetTo(setKind)(e.target.value as AdaAdapterKind | '')}\n aria-label={t('adaptercloud.adapters.filterByKind', 'Filter by kind')}\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.adapters.allKinds', 'All kinds')}</option>\n {(Object.keys(adapterKindLabels) as AdaAdapterKind[]).map((k) => (\n <option key={k} value={k}>\n {adapterKindLabels[k]}\n </option>\n ))}\n </select>\n <select\n value={status}\n onChange={(e) => resetTo(setStatus)(e.target.value as AdaAdapterStatus | '')}\n aria-label={t('adaptercloud.adapters.filterByStatus', 'Filter by status')}\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.adapters.allStatuses', 'All statuses')}</option>\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\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.adapters.emptyTitle', 'No adapters yet')}\n emptyDescription={t(\n 'adaptercloud.adapters.emptyDescription',\n 'Define your first adapter to start bridging a class of system into the fabric.'\n )}\n emptyIcon={<Boxes className=\"h-10 w-10\" />}\n emptyAction={\n <button\n type=\"button\"\n onClick={() => navigateTo('/adapters/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.adapters.new', 'New adapter')}\n </button>\n }\n >\n <div className=\"grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3\">\n {items.map((adapter) => (\n <GlassCard\n key={adapter.id}\n role=\"button\"\n tabIndex={0}\n onClick={() => navigateTo(`/adapters/${adapter.id}`)}\n onKeyDown={(e) => {\n if (e.key === 'Enter' || e.key === ' ') {\n e.preventDefault();\n navigateTo(`/adapters/${adapter.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=\"flex min-w-0 items-center gap-3\">\n <span className=\"flex h-10 w-10 shrink-0 items-center justify-center rounded-lg bg-accent-blue-subtle text-accent-blue\">\n {(() => {\n const Icon = kindIcons[adapter.kind];\n return <Icon className=\"h-5 w-5\" />;\n })()}\n </span>\n <div className=\"min-w-0\">\n <h3 className=\"truncate text-base font-semibold text-text-primary\">\n {adapter.name}\n </h3>\n <p className=\"mt-0.5 truncate text-xs text-text-secondary\">{adapter.slug}</p>\n </div>\n </div>\n <StatusBadge status={adapter.status} />\n </div>\n\n <div className=\"mt-3 flex flex-wrap items-center gap-2 text-xs text-text-secondary\">\n <span className=\"inline-flex items-center rounded-full bg-bg-sunken px-2 py-0.5\">\n {adapterKindLabels[adapter.kind]}\n </span>\n <span>v{adapter.version}</span>\n {adapter.vendor && <span>· {adapter.vendor}</span>}\n </div>\n\n {adapter.description && (\n <p className=\"mt-3 line-clamp-2 text-sm text-text-secondary\">\n {adapter.description}\n </p>\n )}\n\n {adapter.capabilities.length > 0 && (\n <div className=\"mt-3 flex flex-wrap gap-1.5\">\n {adapter.capabilities.slice(0, 4).map((cap) => (\n <span\n key={cap}\n className=\"inline-flex items-center rounded-full bg-accent-blue-subtle px-2 py-0.5 text-xs text-accent-blue\"\n >\n {cap}\n </span>\n ))}\n {adapter.capabilities.length > 4 && (\n <span className=\"text-xs text-text-secondary\">\n +{adapter.capabilities.length - 4}\n </span>\n )}\n </div>\n )}\n\n <p className=\"mt-4 text-xs text-text-secondary\">\n {t('adaptercloud.common.updated', 'Updated')}{' '}\n {formatRelativeTime(adapter.updatedAt)}\n </p>\n </GlassCard>\n ))}\n </div>\n\n {/* Pagination */}\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":"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, X } 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 { BarChart, Sparkline } from '../../components/viz';\nimport { useAdapterCloud } from '../../context/AdapterCloudContext';\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 { 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 [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 // Cost-by-service bars for the BarChart (service · magnitude · $).\n const serviceBars = useMemo(\n () =>\n sortedLines.map((line) => ({\n key: line.service,\n label: line.service,\n value: line.total,\n display: formatMoney(line.total, currency),\n })),\n [sortedLines, currency]\n );\n\n // Monthly cost trend, derived from the raw ledger (periodStart → YYYY-MM buckets).\n const trend = useMemo(() => {\n const buckets = new Map<string, number>();\n for (const r of records.data ?? []) {\n const d = new Date(r.periodStart);\n if (Number.isNaN(d.getTime())) continue;\n const key = `${d.getUTCFullYear()}-${String(d.getUTCMonth() + 1).padStart(2, '0')}`;\n buckets.set(key, (buckets.get(key) ?? 0) + r.amount);\n }\n const series = [...buckets.entries()].sort(([a], [b]) => a.localeCompare(b));\n const points = series.map(([, v]) => v);\n const last = points[points.length - 1] ?? 0;\n const prev = points[points.length - 2] ?? 0;\n const deltaPct = prev > 0 ? Math.round(((last - prev) / prev) * 100) : null;\n return { points, deltaPct };\n }, [records.data]);\n\n const onSearch = useCallback(\n (e: React.FormEvent) => {\n e.preventDefault();\n setAppliedService(serviceInput.trim());\n },\n [serviceInput]\n );\n\n const clearService = useCallback(() => {\n setServiceInput('');\n setAppliedService('');\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: 'resource',\n header: t('adaptercloud.costs.resource', 'Resource'),\n priority: 'secondary',\n cell: (r) =>\n r.resource?.name ? (\n <button\n type=\"button\"\n onClick={() => r.resourceId && navigateTo(`/resources/${r.resourceId}`)}\n className=\"max-w-[12rem] truncate text-left text-sm text-accent-blue hover:underline\"\n >\n {r.resource.name}\n </button>\n ) : r.resourceId ? (\n <span className=\"font-mono text-xs text-text-secondary\">{r.resourceId.slice(0, 10)}…</span>\n ) : (\n <span className=\"text-text-tertiary\">—</span>\n ),\n },\n {\n key: 'connection',\n header: t('adaptercloud.costs.connection', 'Connection'),\n priority: 'hidden-on-mobile',\n cell: (r) =>\n r.connection?.name ? (\n <span className=\"max-w-[12rem] truncate text-sm text-text-primary\">{r.connection.name}</span>\n ) : (\n <span className=\"text-text-tertiary\">—</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 + monthly trend */}\n {sortedLines.length > 0 && (\n <div className=\"mb-8 grid grid-cols-1 gap-6 lg:grid-cols-3\">\n <section className=\"lg:col-span-2\">\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 <BarChart data={serviceBars} labelWidthClassName=\"w-24 sm:w-32\" />\n </GlassCard>\n </section>\n\n <section>\n <h2 className=\"mb-4 text-lg font-semibold text-text-primary\">\n {t('adaptercloud.costs.trend', 'Monthly Trend')}\n </h2>\n <GlassCard className=\"flex h-[calc(100%-2.75rem)] flex-col justify-between p-5\">\n {trend.points.length >= 2 ? (\n <>\n <div className=\"flex items-baseline gap-2\">\n <span className=\"text-2xl font-bold tabular-nums text-text-primary\">\n {formatMoney(trend.points[trend.points.length - 1] ?? 0, currency)}\n </span>\n {trend.deltaPct !== null && (\n <span\n className={`text-xs font-semibold tabular-nums ${\n trend.deltaPct > 0 ? 'text-status-error-text' : 'text-status-success-text'\n }`}\n >\n {trend.deltaPct > 0 ? '▲' : '▼'} {Math.abs(trend.deltaPct)}%{' '}\n {t('adaptercloud.costs.vsPrev', 'vs prev')}\n </span>\n )}\n </div>\n <Sparkline points={trend.points} className=\"mt-4 h-16 w-full text-accent-blue\" />\n </>\n ) : (\n <p className=\"py-6 text-center text-sm text-text-secondary\">\n {t('adaptercloud.costs.trendEmpty', 'Ingest at least two months of cost to see a trend.')}\n </p>\n )}\n </GlassCard>\n </section>\n </div>\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\n onSubmit={onSearch}\n role=\"search\"\n className=\"flex w-full items-center gap-2 sm:w-auto\"\n >\n <div className=\"relative w-full sm:w-56\">\n <input\n type=\"text\"\n value={serviceInput}\n onChange={(e) => setServiceInput(e.target.value)}\n aria-label={t('adaptercloud.costs.filterService', 'Filter by service')}\n placeholder={t('adaptercloud.costs.filterServicePlaceholder', 'Filter by service…')}\n className=\"w-full rounded-lg border border-border-subtle bg-bg-surface py-2 pl-3 pr-9 text-sm text-text-primary placeholder:text-text-secondary focus:outline-none focus:ring-2 focus:ring-[var(--color-focus-ring)]\"\n />\n {appliedService && (\n <button\n type=\"button\"\n onClick={clearService}\n aria-label={t('adaptercloud.costs.clearServiceFilter', 'Clear service filter')}\n className=\"absolute right-2 top-1/2 -translate-y-1/2 rounded p-0.5 text-text-secondary transition-colors hover:text-text-primary focus:outline-none focus:ring-2 focus:ring-[var(--color-focus-ring)]\"\n >\n <X className=\"h-4 w-4\" />\n </button>\n )}\n </div>\n <button\n type=\"submit\"\n aria-label={t('adaptercloud.costs.applyServiceFilter', 'Apply service filter')}\n className=\"inline-flex shrink-0 items-center justify-center rounded-lg border border-border-subtle bg-bg-surface px-3 py-2 text-text-primary transition-colors hover:bg-accent focus:outline-none focus:ring-2 focus:ring-[var(--color-focus-ring)]\"\n >\n <Search className=\"h-4 w-4\" />\n </button>\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 emptyAction={\n <button\n type=\"button\"\n onClick={() => navigateTo('/connections')}\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.costs.goToConnections', 'Go to connections')}\n </button>\n }\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
|
+
{"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, X } 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 { BarChart, Sparkline } from '../../components/viz';\nimport { useAdapterCloud } from '../../context/AdapterCloudContext';\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 { 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 [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 // Cost-by-service bars for the BarChart (service · magnitude · $).\n const serviceBars = useMemo(\n () =>\n sortedLines.map((line) => ({\n key: line.service,\n label: line.service,\n value: line.total,\n display: formatMoney(line.total, currency),\n })),\n [sortedLines, currency]\n );\n\n // Monthly cost trend, derived from the raw ledger (periodStart → YYYY-MM buckets).\n const trend = useMemo(() => {\n const buckets = new Map<string, number>();\n for (const r of records.data ?? []) {\n const d = new Date(r.periodStart);\n if (Number.isNaN(d.getTime())) continue;\n const key = `${d.getUTCFullYear()}-${String(d.getUTCMonth() + 1).padStart(2, '0')}`;\n buckets.set(key, (buckets.get(key) ?? 0) + r.amount);\n }\n const series = [...buckets.entries()].sort(([a], [b]) => a.localeCompare(b));\n const points = series.map(([, v]) => v);\n const last = points[points.length - 1] ?? 0;\n const prev = points[points.length - 2] ?? 0;\n const deltaPct = prev > 0 ? Math.round(((last - prev) / prev) * 100) : null;\n return { points, deltaPct };\n }, [records.data]);\n\n const onSearch = useCallback(\n (e: React.FormEvent) => {\n e.preventDefault();\n setAppliedService(serviceInput.trim());\n },\n [serviceInput]\n );\n\n const clearService = useCallback(() => {\n setServiceInput('');\n setAppliedService('');\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: 'resource',\n header: t('adaptercloud.costs.resource', 'Resource'),\n priority: 'secondary',\n cell: (r) =>\n r.resource?.name ? (\n <button\n type=\"button\"\n onClick={() => r.resourceId && navigateTo(`/resources/${r.resourceId}`)}\n className=\"max-w-[12rem] truncate text-left text-sm text-accent-blue hover:underline\"\n >\n {r.resource.name}\n </button>\n ) : r.resourceId ? (\n <span className=\"font-mono text-xs text-text-secondary\">\n {r.resourceId.slice(0, 10)}…\n </span>\n ) : (\n <span className=\"text-text-tertiary\">—</span>\n ),\n },\n {\n key: 'connection',\n header: t('adaptercloud.costs.connection', 'Connection'),\n priority: 'hidden-on-mobile',\n cell: (r) =>\n r.connection?.name ? (\n <span className=\"max-w-[12rem] truncate text-sm text-text-primary\">\n {r.connection.name}\n </span>\n ) : (\n <span className=\"text-text-tertiary\">—</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 + monthly trend */}\n {sortedLines.length > 0 && (\n <div className=\"mb-8 grid grid-cols-1 gap-6 lg:grid-cols-3\">\n <section className=\"lg:col-span-2\">\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 <BarChart data={serviceBars} labelWidthClassName=\"w-24 sm:w-32\" />\n </GlassCard>\n </section>\n\n <section>\n <h2 className=\"mb-4 text-lg font-semibold text-text-primary\">\n {t('adaptercloud.costs.trend', 'Monthly Trend')}\n </h2>\n <GlassCard className=\"flex h-[calc(100%-2.75rem)] flex-col justify-between p-5\">\n {trend.points.length >= 2 ? (\n <>\n <div className=\"flex items-baseline gap-2\">\n <span className=\"text-2xl font-bold tabular-nums text-text-primary\">\n {formatMoney(trend.points[trend.points.length - 1] ?? 0, currency)}\n </span>\n {trend.deltaPct !== null && (\n <span\n className={`text-xs font-semibold tabular-nums ${\n trend.deltaPct > 0\n ? 'text-status-error-text'\n : 'text-status-success-text'\n }`}\n >\n {trend.deltaPct > 0 ? '▲' : '▼'} {Math.abs(trend.deltaPct)}%{' '}\n {t('adaptercloud.costs.vsPrev', 'vs prev')}\n </span>\n )}\n </div>\n <Sparkline\n points={trend.points}\n className=\"mt-4 h-16 w-full text-accent-blue\"\n />\n </>\n ) : (\n <p className=\"py-6 text-center text-sm text-text-secondary\">\n {t(\n 'adaptercloud.costs.trendEmpty',\n 'Ingest at least two months of cost to see a trend.'\n )}\n </p>\n )}\n </GlassCard>\n </section>\n </div>\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\n onSubmit={onSearch}\n role=\"search\"\n className=\"flex w-full items-center gap-2 sm:w-auto\"\n >\n <div className=\"relative w-full sm:w-56\">\n <input\n type=\"text\"\n value={serviceInput}\n onChange={(e) => setServiceInput(e.target.value)}\n aria-label={t('adaptercloud.costs.filterService', 'Filter by service')}\n placeholder={t('adaptercloud.costs.filterServicePlaceholder', 'Filter by service…')}\n className=\"w-full rounded-lg border border-border-subtle bg-bg-surface py-2 pl-3 pr-9 text-sm text-text-primary placeholder:text-text-secondary focus:outline-none focus:ring-2 focus:ring-[var(--color-focus-ring)]\"\n />\n {appliedService && (\n <button\n type=\"button\"\n onClick={clearService}\n aria-label={t('adaptercloud.costs.clearServiceFilter', 'Clear service filter')}\n className=\"absolute right-2 top-1/2 -translate-y-1/2 rounded p-0.5 text-text-secondary transition-colors hover:text-text-primary focus:outline-none focus:ring-2 focus:ring-[var(--color-focus-ring)]\"\n >\n <X className=\"h-4 w-4\" />\n </button>\n )}\n </div>\n <button\n type=\"submit\"\n aria-label={t('adaptercloud.costs.applyServiceFilter', 'Apply service filter')}\n className=\"inline-flex shrink-0 items-center justify-center rounded-lg border border-border-subtle bg-bg-surface px-3 py-2 text-text-primary transition-colors hover:bg-accent focus:outline-none focus:ring-2 focus:ring-[var(--color-focus-ring)]\"\n >\n <Search className=\"h-4 w-4\" />\n </button>\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 emptyAction={\n <button\n type=\"button\"\n onClick={() => navigateTo('/connections')}\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.costs.goToConnections', 'Go to connections')}\n </button>\n }\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. Each row surfaces color-coded\n * KIND / ENFORCEMENT / SEVERITY badges, an applies-to chip set, and an inline\n * Active toggle wired to `adaUpdatePolicy` (optimistic, toasts on success).\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, useUpdatePolicy } from '../../hooks/useAdapterCloudApi';\nimport { policyKindLabels, policyEnforcementLabels } from '../../utils/domain';\nimport { formatRelativeTime } from '../../utils/formatters';\nimport { cn } from '../../utils/cn';\nimport type { AdaPolicy, AdaPolicyEnforcement, AdaPolicyKind } from '../../types';\n\nconst PAGE_SIZE = 20;\n\ntype ActiveFilter = 'all' | 'active' | 'inactive';\n\n// ---------------------------------------------------------------------------\n// Color-coded governance badges (semantic tokens only), mirroring SeverityBadge.\n// ---------------------------------------------------------------------------\n\nconst badgeBase = 'inline-flex items-center rounded-full px-2.5 py-0.5 text-xs font-medium';\n\n/** Kind → semantic token. Security/Guardrail/Compliance read as governance\n * (accent-blue), Cost as spend (success/green), the rest as neutral. */\nconst policyKindBadgeClass: Record<AdaPolicyKind, string> = {\n SECURITY: 'bg-accent-blue-subtle text-accent-blue',\n GUARDRAIL: 'bg-accent-blue-subtle text-accent-blue',\n COMPLIANCE: 'bg-accent-blue-subtle text-accent-blue',\n COST: 'bg-status-success-bg-subtle text-status-success-text',\n NAMING: 'bg-bg-sunken text-text-secondary',\n TAGGING: 'bg-bg-sunken text-text-secondary',\n};\n\n/** Enforcement → semantic token. Block = error, Warn = warning, Advisory = neutral. */\nconst policyEnforcementBadgeClass: Record<AdaPolicyEnforcement, string> = {\n BLOCK: 'bg-status-error-bg-subtle text-status-error-text',\n WARN: 'bg-status-warning-bg-subtle text-status-warning-text',\n ADVISORY: 'bg-bg-sunken text-text-secondary',\n};\n\nfunction KindBadge({ kind }: { kind: AdaPolicyKind }) {\n return <span className={cn(badgeBase, policyKindBadgeClass[kind])}>{policyKindLabels[kind]}</span>;\n}\n\nfunction EnforcementBadge({ enforcement }: { enforcement: AdaPolicyEnforcement }) {\n return (\n <span className={cn(badgeBase, policyEnforcementBadgeClass[enforcement])}>\n {policyEnforcementLabels[enforcement]}\n </span>\n );\n}\n\n/** Labeled meta column (eyebrow + value) used across each policy row. */\nfunction MetaCol({ label, children }: { label: string; children: React.ReactNode }) {\n return (\n <div className=\"flex flex-col gap-1\">\n <span className=\"text-xs font-semibold uppercase tracking-wide text-text-secondary\">\n {label}\n </span>\n {children}\n </div>\n );\n}\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 // Local optimistic override of isActive per policy id while the mutation is in\n // flight, so the toggle flips instantly instead of waiting for the refetch.\n const [pendingActive, setPendingActive] = useState<Record<string, boolean>>({});\n\n const query = usePolicies({\n kind: kind || undefined,\n isActive: active === 'all' ? undefined : active === 'active',\n pagination: { limit: PAGE_SIZE, offset },\n });\n const update = useUpdatePolicy();\n\n const items = query.data?.items ?? [];\n const hasMore = query.data?.pageInfo.hasMore ?? false;\n\n const toggleActive = useCallback(\n (policy: AdaPolicy) => {\n const next = !(pendingActive[policy.id] ?? policy.isActive);\n setPendingActive((m) => ({ ...m, [policy.id]: next }));\n update.mutate(\n { id: policy.id, input: { isActive: next } },\n {\n onSettled: () =>\n setPendingActive((m) => {\n const copy = { ...m };\n delete copy[policy.id];\n return copy;\n }),\n }\n );\n },\n [update, pendingActive]\n );\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 aria-label={t('adaptercloud.policies.filterByKind', 'Filter by kind')}\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 aria-label={t('adaptercloud.policies.filterByActive', 'Filter by active state')}\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=\"space-y-3\">\n {items.map((policy) => {\n const isActiveNow = pendingActive[policy.id] ?? policy.isActive;\n const isToggling = policy.id in pendingActive;\n return (\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 flex-col gap-4 lg:flex-row lg:items-center lg:gap-6\">\n <div className=\"min-w-0 lg:flex-1\">\n <h3 className=\"truncate text-base font-semibold text-text-primary\">\n {policy.name}\n </h3>\n {policy.description && (\n <p className=\"mt-0.5 line-clamp-1 text-sm text-text-secondary\">\n {policy.description}\n </p>\n )}\n </div>\n\n <MetaCol label={t('adaptercloud.policies.kind', 'Kind')}>\n <KindBadge kind={policy.kind} />\n </MetaCol>\n\n <MetaCol label={t('adaptercloud.policies.enforcement', 'Enforcement')}>\n <EnforcementBadge enforcement={policy.enforcement} />\n </MetaCol>\n\n <MetaCol label={t('adaptercloud.policies.severity', 'Severity')}>\n <SeverityBadge severity={policy.severity} />\n </MetaCol>\n\n <MetaCol label={t('adaptercloud.policies.activeLabel', 'Active')}>\n <div className=\"flex items-center gap-2\">\n <button\n type=\"button\"\n role=\"switch\"\n aria-checked={isActiveNow}\n aria-label={\n isActiveNow\n ? t('adaptercloud.policies.deactivate', 'Deactivate policy')\n : t('adaptercloud.policies.activate', 'Activate policy')\n }\n disabled={isToggling}\n onClick={(e) => {\n e.stopPropagation();\n toggleActive(policy);\n }}\n className={cn(\n 'relative inline-flex h-5 w-9 shrink-0 items-center rounded-full border transition-colors focus:outline-none focus-visible:ring-2 focus-visible:ring-[var(--color-focus-ring)] disabled:opacity-60',\n isActiveNow\n ? 'border-transparent bg-action-primary-bg'\n : 'border-border-subtle bg-bg-sunken'\n )}\n >\n <span\n aria-hidden=\"true\"\n className={cn(\n 'inline-block h-3.5 w-3.5 transform rounded-full shadow-sm transition-transform',\n isActiveNow\n ? 'translate-x-4 bg-action-primary-text'\n : 'translate-x-0.5 bg-text-secondary'\n )}\n />\n </button>\n <span className=\"text-xs text-text-secondary\">\n {isActiveNow\n ? t('adaptercloud.policies.active', 'Active')\n : t('adaptercloud.policies.inactive', 'Inactive')}\n </span>\n </div>\n </MetaCol>\n\n {policy.appliesTo.length > 0 && (\n <MetaCol label={t('adaptercloud.policies.appliesTo', 'Applies to')}>\n <div className=\"flex flex-wrap items-center gap-1.5\">\n {policy.appliesTo.slice(0, 2).map((sel) => (\n <span\n key={sel}\n className=\"inline-flex items-center rounded-full bg-accent-blue-subtle px-2 py-0.5 font-mono text-xs text-accent-blue\"\n >\n {sel}\n </span>\n ))}\n {policy.appliesTo.length > 2 && (\n <span className=\"inline-flex items-center rounded-full bg-bg-sunken px-2 py-0.5 text-xs text-text-secondary\">\n +{policy.appliesTo.length - 2}\n </span>\n )}\n </div>\n </MetaCol>\n )}\n </div>\n\n <p className=\"mt-3 text-xs text-text-secondary\">\n {t('adaptercloud.common.updated', 'Updated')} {formatRelativeTime(policy.updatedAt)}\n </p>\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 </PageLayout>\n );\n}\n"],"mappings":"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"}
|
|
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. Each row surfaces color-coded\n * KIND / ENFORCEMENT / SEVERITY badges, an applies-to chip set, and an inline\n * Active toggle wired to `adaUpdatePolicy` (optimistic, toasts on success).\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, useUpdatePolicy } from '../../hooks/useAdapterCloudApi';\nimport { policyKindLabels, policyEnforcementLabels } from '../../utils/domain';\nimport { formatRelativeTime } from '../../utils/formatters';\nimport { cn } from '../../utils/cn';\nimport type { AdaPolicy, AdaPolicyEnforcement, AdaPolicyKind } from '../../types';\n\nconst PAGE_SIZE = 20;\n\ntype ActiveFilter = 'all' | 'active' | 'inactive';\n\n// ---------------------------------------------------------------------------\n// Color-coded governance badges (semantic tokens only), mirroring SeverityBadge.\n// ---------------------------------------------------------------------------\n\nconst badgeBase = 'inline-flex items-center rounded-full px-2.5 py-0.5 text-xs font-medium';\n\n/** Kind → semantic token. Security/Guardrail/Compliance read as governance\n * (accent-blue), Cost as spend (success/green), the rest as neutral. */\nconst policyKindBadgeClass: Record<AdaPolicyKind, string> = {\n SECURITY: 'bg-accent-blue-subtle text-accent-blue',\n GUARDRAIL: 'bg-accent-blue-subtle text-accent-blue',\n COMPLIANCE: 'bg-accent-blue-subtle text-accent-blue',\n COST: 'bg-status-success-bg-subtle text-status-success-text',\n NAMING: 'bg-bg-sunken text-text-secondary',\n TAGGING: 'bg-bg-sunken text-text-secondary',\n};\n\n/** Enforcement → semantic token. Block = error, Warn = warning, Advisory = neutral. */\nconst policyEnforcementBadgeClass: Record<AdaPolicyEnforcement, string> = {\n BLOCK: 'bg-status-error-bg-subtle text-status-error-text',\n WARN: 'bg-status-warning-bg-subtle text-status-warning-text',\n ADVISORY: 'bg-bg-sunken text-text-secondary',\n};\n\nfunction KindBadge({ kind }: { kind: AdaPolicyKind }) {\n return (\n <span className={cn(badgeBase, policyKindBadgeClass[kind])}>{policyKindLabels[kind]}</span>\n );\n}\n\nfunction EnforcementBadge({ enforcement }: { enforcement: AdaPolicyEnforcement }) {\n return (\n <span className={cn(badgeBase, policyEnforcementBadgeClass[enforcement])}>\n {policyEnforcementLabels[enforcement]}\n </span>\n );\n}\n\n/** Labeled meta column (eyebrow + value) used across each policy row. */\nfunction MetaCol({ label, children }: { label: string; children: React.ReactNode }) {\n return (\n <div className=\"flex flex-col gap-1\">\n <span className=\"text-xs font-semibold uppercase tracking-wide text-text-secondary\">\n {label}\n </span>\n {children}\n </div>\n );\n}\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 // Local optimistic override of isActive per policy id while the mutation is in\n // flight, so the toggle flips instantly instead of waiting for the refetch.\n const [pendingActive, setPendingActive] = useState<Record<string, boolean>>({});\n\n const query = usePolicies({\n kind: kind || undefined,\n isActive: active === 'all' ? undefined : active === 'active',\n pagination: { limit: PAGE_SIZE, offset },\n });\n const update = useUpdatePolicy();\n\n const items = query.data?.items ?? [];\n const hasMore = query.data?.pageInfo.hasMore ?? false;\n\n const toggleActive = useCallback(\n (policy: AdaPolicy) => {\n const next = !(pendingActive[policy.id] ?? policy.isActive);\n setPendingActive((m) => ({ ...m, [policy.id]: next }));\n update.mutate(\n { id: policy.id, input: { isActive: next } },\n {\n onSettled: () =>\n setPendingActive((m) => {\n const copy = { ...m };\n delete copy[policy.id];\n return copy;\n }),\n }\n );\n },\n [update, pendingActive]\n );\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 aria-label={t('adaptercloud.policies.filterByKind', 'Filter by kind')}\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 aria-label={t('adaptercloud.policies.filterByActive', 'Filter by active state')}\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=\"space-y-3\">\n {items.map((policy) => {\n const isActiveNow = pendingActive[policy.id] ?? policy.isActive;\n const isToggling = policy.id in pendingActive;\n return (\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 flex-col gap-4 lg:flex-row lg:items-center lg:gap-6\">\n <div className=\"min-w-0 lg:flex-1\">\n <h3 className=\"truncate text-base font-semibold text-text-primary\">\n {policy.name}\n </h3>\n {policy.description && (\n <p className=\"mt-0.5 line-clamp-1 text-sm text-text-secondary\">\n {policy.description}\n </p>\n )}\n </div>\n\n <MetaCol label={t('adaptercloud.policies.kind', 'Kind')}>\n <KindBadge kind={policy.kind} />\n </MetaCol>\n\n <MetaCol label={t('adaptercloud.policies.enforcement', 'Enforcement')}>\n <EnforcementBadge enforcement={policy.enforcement} />\n </MetaCol>\n\n <MetaCol label={t('adaptercloud.policies.severity', 'Severity')}>\n <SeverityBadge severity={policy.severity} />\n </MetaCol>\n\n <MetaCol label={t('adaptercloud.policies.activeLabel', 'Active')}>\n <div className=\"flex items-center gap-2\">\n <button\n type=\"button\"\n role=\"switch\"\n aria-checked={isActiveNow}\n aria-label={\n isActiveNow\n ? t('adaptercloud.policies.deactivate', 'Deactivate policy')\n : t('adaptercloud.policies.activate', 'Activate policy')\n }\n disabled={isToggling}\n onClick={(e) => {\n e.stopPropagation();\n toggleActive(policy);\n }}\n className={cn(\n 'relative inline-flex h-5 w-9 shrink-0 items-center rounded-full border transition-colors focus:outline-none focus-visible:ring-2 focus-visible:ring-[var(--color-focus-ring)] disabled:opacity-60',\n isActiveNow\n ? 'border-transparent bg-action-primary-bg'\n : 'border-border-subtle bg-bg-sunken'\n )}\n >\n <span\n aria-hidden=\"true\"\n className={cn(\n 'inline-block h-3.5 w-3.5 transform rounded-full shadow-sm transition-transform',\n isActiveNow\n ? 'translate-x-4 bg-action-primary-text'\n : 'translate-x-0.5 bg-text-secondary'\n )}\n />\n </button>\n <span className=\"text-xs text-text-secondary\">\n {isActiveNow\n ? t('adaptercloud.policies.active', 'Active')\n : t('adaptercloud.policies.inactive', 'Inactive')}\n </span>\n </div>\n </MetaCol>\n\n {policy.appliesTo.length > 0 && (\n <MetaCol label={t('adaptercloud.policies.appliesTo', 'Applies to')}>\n <div className=\"flex flex-wrap items-center gap-1.5\">\n {policy.appliesTo.slice(0, 2).map((sel) => (\n <span\n key={sel}\n className=\"inline-flex items-center rounded-full bg-accent-blue-subtle px-2 py-0.5 font-mono text-xs text-accent-blue\"\n >\n {sel}\n </span>\n ))}\n {policy.appliesTo.length > 2 && (\n <span className=\"inline-flex items-center rounded-full bg-bg-sunken px-2 py-0.5 text-xs text-text-secondary\">\n +{policy.appliesTo.length - 2}\n </span>\n )}\n </div>\n </MetaCol>\n )}\n </div>\n\n <p className=\"mt-3 text-xs text-text-secondary\">\n {t('adaptercloud.common.updated', 'Updated')}{' '}\n {formatRelativeTime(policy.updatedAt)}\n </p>\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 </PageLayout>\n );\n}\n"],"mappings":"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"}
|
|
@@ -159,10 +159,8 @@ function T() {
|
|
|
159
159
|
})]
|
|
160
160
|
}), j.violations && j.violations.length > 0 ? /* @__PURE__ */ S("div", {
|
|
161
161
|
className: "divide-y divide-border rounded-xl border border-border-subtle bg-bg-surface",
|
|
162
|
-
children: j.violations.slice(0, 10).map((e) => /* @__PURE__ */ C("
|
|
163
|
-
|
|
164
|
-
onClick: () => E(`/violations?policyId=${j.id}`),
|
|
165
|
-
className: "flex w-full items-start gap-3 px-4 py-3 text-left transition-colors hover:bg-accent/50",
|
|
162
|
+
children: j.violations.slice(0, 10).map((e) => /* @__PURE__ */ C("div", {
|
|
163
|
+
className: "flex w-full items-start gap-3 px-4 py-3 transition-colors hover:bg-accent/50",
|
|
166
164
|
children: [
|
|
167
165
|
/* @__PURE__ */ S(g, { className: "mt-0.5 h-4 w-4 shrink-0 text-status-error-text" }),
|
|
168
166
|
/* @__PURE__ */ C("div", {
|
|
@@ -170,12 +168,27 @@ function T() {
|
|
|
170
168
|
children: [/* @__PURE__ */ S("p", {
|
|
171
169
|
className: "truncate text-sm font-medium text-text-primary",
|
|
172
170
|
children: e.message
|
|
173
|
-
}), /* @__PURE__ */
|
|
174
|
-
className: "mt-0.5 text-xs text-text-secondary",
|
|
175
|
-
children:
|
|
171
|
+
}), /* @__PURE__ */ C("p", {
|
|
172
|
+
className: "mt-0.5 flex flex-wrap items-center gap-x-2 gap-y-0.5 text-xs text-text-secondary",
|
|
173
|
+
children: [
|
|
174
|
+
/* @__PURE__ */ S("span", { children: d(e.detectedAt) }),
|
|
175
|
+
/* @__PURE__ */ S("span", {
|
|
176
|
+
"aria-hidden": "true",
|
|
177
|
+
children: "·"
|
|
178
|
+
}),
|
|
179
|
+
/* @__PURE__ */ S("button", {
|
|
180
|
+
type: "button",
|
|
181
|
+
onClick: () => E(`/resources/${e.resourceId}`),
|
|
182
|
+
className: "font-medium text-accent-blue hover:underline",
|
|
183
|
+
children: O("adaptercloud.violations.viewResource", "View resource")
|
|
184
|
+
})
|
|
185
|
+
]
|
|
176
186
|
})]
|
|
177
187
|
}),
|
|
178
|
-
/* @__PURE__ */
|
|
188
|
+
/* @__PURE__ */ C("div", {
|
|
189
|
+
className: "flex shrink-0 items-center gap-2",
|
|
190
|
+
children: [/* @__PURE__ */ S(o, { severity: e.severity }), /* @__PURE__ */ S(a, { status: e.status })]
|
|
191
|
+
})
|
|
179
192
|
]
|
|
180
193
|
}, e.id))
|
|
181
194
|
}) : /* @__PURE__ */ S("p", {
|
|
@@ -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, useState } from 'react';\nimport { useParams } from 'react-router-dom';\nimport { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';\nimport { ArrowLeft, Archive, AlertCircle, Pencil } 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 { ConfirmDialog } from '../../components/ConfirmDialog';\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 [confirmArchiveOpen, setConfirmArchiveOpen] = useState(false);\n\n const handleArchive = useCallback(() => {\n if (!id) return;\n archive.mutate(id, {\n onSuccess: () => {\n setConfirmArchiveOpen(false);\n navigateTo('/policies');\n },\n });\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 <>\n <button\n type=\"button\"\n onClick={() => navigateTo(`/policies/${policy.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 <button\n type=\"button\"\n onClick={() => setConfirmArchiveOpen(true)}\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 </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-accent-blue 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\n <ConfirmDialog\n open={confirmArchiveOpen}\n onOpenChange={setConfirmArchiveOpen}\n title={t('adaptercloud.policies.archiveTitle', 'Archive this policy?')}\n description={t(\n 'adaptercloud.policies.archivePrompt',\n 'The policy will be archived and removed from every governance view. This cannot be undone from the UI.'\n )}\n confirmLabel={t('adaptercloud.common.archive', 'Archive')}\n cancelLabel={t('adaptercloud.common.cancel', 'Cancel')}\n onConfirm={handleArchive}\n isPending={archive.isPending}\n tone=\"danger\"\n />\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, useState } from 'react';\nimport { useParams } from 'react-router-dom';\nimport { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';\nimport { ArrowLeft, Archive, AlertCircle, Pencil } 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 { ConfirmDialog } from '../../components/ConfirmDialog';\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 [confirmArchiveOpen, setConfirmArchiveOpen] = useState(false);\n\n const handleArchive = useCallback(() => {\n if (!id) return;\n archive.mutate(id, {\n onSuccess: () => {\n setConfirmArchiveOpen(false);\n navigateTo('/policies');\n },\n });\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 <>\n <button\n type=\"button\"\n onClick={() => navigateTo(`/policies/${policy.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 <button\n type=\"button\"\n onClick={() => setConfirmArchiveOpen(true)}\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 </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-accent-blue 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 <div\n key={v.id}\n className=\"flex w-full items-start gap-3 px-4 py-3 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 flex flex-wrap items-center gap-x-2 gap-y-0.5 text-xs text-text-secondary\">\n <span>{formatRelativeTime(v.detectedAt)}</span>\n <span aria-hidden=\"true\">·</span>\n <button\n type=\"button\"\n onClick={() => navigateTo(`/resources/${v.resourceId}`)}\n className=\"font-medium text-accent-blue hover:underline\"\n >\n {t('adaptercloud.violations.viewResource', 'View resource')}\n </button>\n </p>\n </div>\n <div className=\"flex shrink-0 items-center gap-2\">\n <SeverityBadge severity={v.severity} />\n <StatusBadge status={v.status} />\n </div>\n </div>\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\n <ConfirmDialog\n open={confirmArchiveOpen}\n onOpenChange={setConfirmArchiveOpen}\n title={t('adaptercloud.policies.archiveTitle', 'Archive this policy?')}\n description={t(\n 'adaptercloud.policies.archivePrompt',\n 'The policy will be archived and removed from every governance view. This cannot be undone from the UI.'\n )}\n confirmLabel={t('adaptercloud.common.archive', 'Archive')}\n cancelLabel={t('adaptercloud.common.cancel', 'Cancel')}\n onConfirm={handleArchive}\n isPending={archive.isPending}\n tone=\"danger\"\n />\n </PageLayout>\n );\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"TopologyPage.js","names":[],"sources":["../../../../src/adaptercloud/pages/topology/TopologyPage.tsx"],"sourcesContent":["/**\n * Estate Topology Page\n * @module adaptercloud/pages/topology\n *\n * The estate-wide dependency graph — every discovered resource laid out in one\n * live node-link map, grouped into human \"zones\" (Public Cloud, Kubernetes,\n * Edge / IoT, Data Systems, Network) and wired together by the topology edges\n * (`adaListTopologyEdges`). Selecting a node opens an inspector with its\n * dependency count and downstream blast-radius. All data is live from the\n * deployed `wspace-adaptercloud-svc` subgraph — nodes from `adaListResources`,\n * edges from `adaListTopologyEdges`. No mocks.\n */\n\nimport { useCallback, useMemo, useState } from 'react';\nimport { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';\nimport {\n Waypoints,\n Search,\n X,\n RefreshCw,\n ArrowRight,\n Layers,\n Boxes,\n GitBranch,\n} 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 { useResources, useTopology } from '../../hooks/useAdapterCloudApi';\nimport { resourceStateLabels, humanizeEnum } from '../../utils/domain';\nimport { TopologyGraph, type TopologyNode, type TopologyEdge } from '../../components/viz';\nimport type { AdaEdgeKind, AdaResourceState } from '../../types';\n\n/** Canonical edge kinds — drives the legend so it stays in sync with the enum. */\nconst EDGE_KINDS: AdaEdgeKind[] = [\n 'DEPENDS_ON',\n 'CONNECTS_TO',\n 'ROUTES_TO',\n 'REPLICATES_TO',\n 'CONTAINS',\n];\n\n/**\n * Derive a human \"zone\" (graph column) for a resource from its type prefix,\n * falling back to the connection name. Data stores route to \"Data Systems\" even\n * when they carry a cloud prefix, matching how operators think about the estate.\n */\nfunction zoneFor(resourceType: string, connectionName?: string | null): string {\n const t = resourceType.toLowerCase();\n\n // Kubernetes clusters/workloads\n if (/^(gke|k8s|kubernetes|eks|aks)\\./.test(t)) return 'Kubernetes';\n // Edge / IoT devices and gateways\n if (/^(iot|edge)\\./.test(t)) return 'Edge / IoT';\n // Data systems — warehouses, databases, object storage\n if (\n /^snowflake\\./.test(t) ||\n /\\.(db|rds|postgres|mysql|redshift|bigquery|dynamodb|s3|bucket|storage)\\b/.test(t) ||\n t.endsWith('.db')\n ) {\n return 'Data Systems';\n }\n // Network fabric — CDNs, VPCs, routers\n if (t.endsWith('.network') || /^cloudflare\\./.test(t) || /\\.(vpc|cdn)\\b/.test(t)) {\n return 'Network';\n }\n // Public cloud compute/etc.\n if (/^(aws|azure|gcp|oci|alicloud)\\./.test(t)) return 'Public Cloud';\n\n // Fallback: infer from the connection name\n const c = (connectionName ?? '').toLowerCase();\n if (c) {\n if (/gke|k8s|kube/.test(c)) return 'Kubernetes';\n if (/iot|edge/.test(c)) return 'Edge / IoT';\n if (/snowflake|warehouse|postgres|database|\\bdb\\b/.test(c)) return 'Data Systems';\n if (/cloudflare|network|vpc|cdn/.test(c)) return 'Network';\n if (/aws|azure|gcp|cloud/.test(c)) return 'Public Cloud';\n }\n return 'Other';\n}\n\nexport function TopologyPage() {\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 resourcesQuery = useResources({ pagination: { limit: 100 } });\n const topologyQuery = useTopology();\n\n const [search, setSearch] = useState('');\n const [kindFilter, setKindFilter] = useState<AdaEdgeKind | ''>('');\n const [selectedId, setSelectedId] = useState<string | null>(null);\n\n const resources = useMemo(() => resourcesQuery.data?.items ?? [], [resourcesQuery.data]);\n const topoEdges = useMemo(() => topologyQuery.data ?? [], [topologyQuery.data]);\n\n // ---- Build the node set from resources -----------------------------------\n const allNodes: TopologyNode[] = useMemo(\n () =>\n resources.map((r) => ({\n id: r.id,\n label: r.name,\n type: r.resourceType,\n state: r.state,\n group: zoneFor(r.resourceType, r.connection?.name),\n })),\n [resources]\n );\n\n const nodeIds = useMemo(() => new Set(allNodes.map((n) => n.id)), [allNodes]);\n const stateById = useMemo(\n () => new Map(resources.map((r) => [r.id, r.state] as const)),\n [resources]\n );\n const nodeById = useMemo(() => new Map(allNodes.map((n) => [n.id, n] as const)), [allNodes]);\n\n // ---- Build edges — only where BOTH endpoints are in the node set ---------\n const allEdges: TopologyEdge[] = useMemo(\n () =>\n topoEdges\n .filter((e) => nodeIds.has(e.fromResourceId) && nodeIds.has(e.toResourceId))\n .map((e) => ({\n id: e.id,\n from: e.fromResourceId,\n to: e.toResourceId,\n kind: e.kind,\n drifted:\n stateById.get(e.fromResourceId) === 'DRIFTED' ||\n stateById.get(e.toResourceId) === 'DRIFTED',\n })),\n [topoEdges, nodeIds, stateById]\n );\n\n // Forward adjacency for inspector metrics (dependencies + blast radius).\n const outAdj = useMemo(() => {\n const adj = new Map<string, string[]>();\n for (const e of allEdges) {\n const list = adj.get(e.from) ?? [];\n list.push(e.to);\n adj.set(e.from, list);\n }\n return adj;\n }, [allEdges]);\n\n // ---- Search + edge-kind filtering (what the graph actually renders) ------\n const q = search.trim().toLowerCase();\n const displayNodes = useMemo(() => {\n if (!q) return allNodes;\n return allNodes.filter(\n (n) => n.label.toLowerCase().includes(q) || (n.type ?? '').toLowerCase().includes(q)\n );\n }, [allNodes, q]);\n\n const displayNodeIds = useMemo(() => new Set(displayNodes.map((n) => n.id)), [displayNodes]);\n\n const displayEdges = useMemo(\n () =>\n allEdges.filter(\n (e) =>\n displayNodeIds.has(e.from) &&\n displayNodeIds.has(e.to) &&\n (kindFilter === '' || e.kind === kindFilter)\n ),\n [allEdges, displayNodeIds, kindFilter]\n );\n\n // Count edges by kind for the legend badges.\n const kindCounts = useMemo(() => {\n const counts: Record<string, number> = {};\n for (const e of allEdges) counts[e.kind ?? ''] = (counts[e.kind ?? ''] ?? 0) + 1;\n return counts;\n }, [allEdges]);\n\n // ---- Inspector metrics for the selected node -----------------------------\n const inspector = useMemo(() => {\n if (!selectedId) return null;\n const node = nodeById.get(selectedId);\n if (!node) return null;\n\n const dependencies = outAdj.get(selectedId)?.length ?? 0;\n\n // Blast radius = every resource transitively reachable downstream.\n const seen = new Set<string>();\n const stack = [...(outAdj.get(selectedId) ?? [])];\n while (stack.length) {\n const cur = stack.pop();\n if (cur === undefined || cur === selectedId || seen.has(cur)) continue;\n seen.add(cur);\n for (const next of outAdj.get(cur) ?? []) stack.push(next);\n }\n\n return { node, dependencies, blastRadius: seen.size };\n }, [selectedId, nodeById, outAdj]);\n\n const zoneCount = useMemo(() => new Set(allNodes.map((n) => n.group)).size, [allNodes]);\n\n const onSelect = useCallback((id: string) => {\n setSelectedId((prev) => (prev === id ? null : id));\n }, []);\n\n const isLoading = resourcesQuery.isLoading || topologyQuery.isLoading;\n const isError = resourcesQuery.isError || topologyQuery.isError;\n\n const refetchAll = useCallback(() => {\n void resourcesQuery.refetch();\n void topologyQuery.refetch();\n }, [resourcesQuery, topologyQuery]);\n\n return (\n <PageLayout\n title={t('adaptercloud.topology.title', 'Estate Topology')}\n description={t(\n 'adaptercloud.topology.subtitle',\n 'A live dependency map of your entire infrastructure — every system in one graph.'\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 {/* Toolbar — search + edge-kind filter + estate counts */}\n <div className=\"mb-6 flex flex-wrap items-center gap-3\">\n <div className=\"relative w-full sm:w-64\">\n <Search className=\"pointer-events-none absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-text-secondary\" />\n <input\n type=\"text\"\n value={search}\n onChange={(e) => setSearch(e.target.value)}\n aria-label={t('adaptercloud.topology.searchNodes', 'Search nodes')}\n placeholder={t('adaptercloud.topology.searchPlaceholder', 'Search nodes…')}\n className=\"w-full rounded-lg border border-border-subtle bg-bg-surface py-2 pl-9 pr-9 text-sm text-text-primary placeholder:text-text-secondary focus:outline-none focus:ring-2 focus:ring-[var(--color-focus-ring)]\"\n />\n {search && (\n <button\n type=\"button\"\n onClick={() => setSearch('')}\n aria-label={t('adaptercloud.topology.clearSearch', 'Clear search')}\n className=\"absolute right-2 top-1/2 -translate-y-1/2 rounded p-0.5 text-text-secondary transition-colors hover:text-text-primary focus:outline-none focus:ring-2 focus:ring-[var(--color-focus-ring)]\"\n >\n <X className=\"h-4 w-4\" />\n </button>\n )}\n </div>\n\n <select\n value={kindFilter}\n onChange={(e) => setKindFilter(e.target.value as AdaEdgeKind | '')}\n aria-label={t('adaptercloud.topology.filterByKind', 'Filter by edge kind')}\n className=\"w-full 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)] sm:w-auto\"\n >\n <option value=\"\">{t('adaptercloud.topology.allKinds', 'All edge kinds')}</option>\n {EDGE_KINDS.map((k) => (\n <option key={k} value={k}>\n {humanizeEnum(k)}\n </option>\n ))}\n </select>\n\n {!isLoading && !isError && allNodes.length > 0 && (\n <div className=\"ml-auto flex flex-wrap items-center gap-4 text-xs text-text-secondary\">\n <span className=\"inline-flex items-center gap-1.5\">\n <Boxes className=\"h-3.5 w-3.5\" />\n <span className=\"tabular-nums font-medium text-text-primary\">{allNodes.length}</span>{' '}\n {t('adaptercloud.topology.nodes', 'nodes')}\n </span>\n <span className=\"inline-flex items-center gap-1.5\">\n <GitBranch className=\"h-3.5 w-3.5\" />\n <span className=\"tabular-nums font-medium text-text-primary\">{allEdges.length}</span>{' '}\n {t('adaptercloud.topology.edges', 'edges')}\n </span>\n <span className=\"inline-flex items-center gap-1.5\">\n <Layers className=\"h-3.5 w-3.5\" />\n <span className=\"tabular-nums font-medium text-text-primary\">{zoneCount}</span>{' '}\n {t('adaptercloud.topology.zones', 'zones')}\n </span>\n </div>\n )}\n </div>\n\n <DataState\n isLoading={isLoading}\n isError={isError}\n isEmpty={allNodes.length === 0}\n error={resourcesQuery.error ?? topologyQuery.error}\n onRetry={refetchAll}\n emptyTitle={t('adaptercloud.topology.emptyTitle', 'No topology yet')}\n emptyDescription={t(\n 'adaptercloud.topology.emptyDescription',\n 'No topology yet — register resources and their dependencies to see them mapped here.'\n )}\n emptyIcon={<Waypoints className=\"h-10 w-10\" />}\n emptyAction={\n <button\n type=\"button\"\n onClick={() => navigateTo('/resources')}\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.topology.goToResources', 'Go to resources')}\n </button>\n }\n >\n <div className=\"grid gap-6 lg:grid-cols-[minmax(0,1fr)_20rem]\">\n {/* Graph + legend */}\n <div className=\"min-w-0 space-y-4\">\n {allEdges.length === 0 && (\n <p className=\"rounded-lg border border-border-subtle bg-bg-sunken/50 px-4 py-3 text-sm text-text-secondary\">\n {t(\n 'adaptercloud.topology.noEdges',\n 'No dependency edges recorded yet — nodes are shown grouped by zone. Add dependencies from a resource to connect the graph.'\n )}\n </p>\n )}\n\n {displayNodes.length > 0 ? (\n <TopologyGraph\n nodes={displayNodes}\n edges={displayEdges}\n selectedId={selectedId}\n onSelect={onSelect}\n />\n ) : (\n <div className=\"flex min-h-[200px] flex-col items-center justify-center gap-2 rounded-xl border border-border-subtle bg-bg-surface p-8 text-center\">\n <Search className=\"h-8 w-8 text-text-secondary\" />\n <p className=\"text-sm text-text-secondary\">\n {t('adaptercloud.topology.noMatches', 'No nodes match your search.')}\n </p>\n </div>\n )}\n\n {/* Edge-kinds legend */}\n <GlassCard className=\"p-4\">\n <h3 className=\"mb-3 text-sm font-semibold text-text-primary\">\n {t('adaptercloud.topology.legend', 'Legend')}\n </h3>\n <div className=\"grid gap-4 sm:grid-cols-2\">\n {/* Edge signals (how the graph paints edges) */}\n <div>\n <p className=\"mb-2 text-xs font-medium uppercase tracking-wide text-text-secondary\">\n {t('adaptercloud.topology.edgeSignals', 'Edge signals')}\n </p>\n <ul className=\"space-y-1.5 text-xs text-text-secondary\">\n <li className=\"flex items-center gap-2\">\n <span className=\"h-0.5 w-6 shrink-0 rounded bg-border-strong\" aria-hidden />\n {t('adaptercloud.topology.dependency', 'Dependency')}\n </li>\n <li className=\"flex items-center gap-2\">\n <span className=\"h-0.5 w-6 shrink-0 rounded bg-accent-blue\" aria-hidden />\n {t('adaptercloud.topology.selectedPath', 'Selected node path')}\n </li>\n <li className=\"flex items-center gap-2\">\n <span\n className=\"h-0.5 w-6 shrink-0 rounded bg-status-warning-text\"\n aria-hidden\n />\n {t('adaptercloud.topology.drift', 'Drift (endpoint drifted)')}\n </li>\n </ul>\n </div>\n\n {/* Edge kinds (semantic relationship types) */}\n <div>\n <p className=\"mb-2 text-xs font-medium uppercase tracking-wide text-text-secondary\">\n {t('adaptercloud.topology.edgeKinds', 'Edge kinds')}\n </p>\n <ul className=\"flex flex-wrap gap-1.5\">\n {EDGE_KINDS.map((k) => (\n <li key={k}>\n <span className=\"inline-flex items-center gap-1.5 rounded-full bg-bg-sunken px-2.5 py-0.5 text-xs text-text-secondary\">\n {humanizeEnum(k)}\n <span className=\"tabular-nums font-medium text-text-primary\">\n {kindCounts[k] ?? 0}\n </span>\n </span>\n </li>\n ))}\n </ul>\n </div>\n </div>\n </GlassCard>\n </div>\n\n {/* Inspector */}\n <aside className=\"lg:sticky lg:top-6 lg:h-fit\">\n <GlassCard className=\"p-5\">\n {inspector ? (\n <div className=\"space-y-5\">\n <div className=\"flex items-start justify-between gap-3\">\n <div className=\"flex min-w-0 items-center gap-2.5\">\n <span className=\"flex h-9 w-9 shrink-0 items-center justify-center rounded-lg bg-bg-sunken\">\n <Waypoints className=\"h-4.5 w-4.5 text-accent-blue\" />\n </span>\n <div className=\"min-w-0\">\n <h3 className=\"truncate text-base font-semibold text-text-primary\">\n {inspector.node.label}\n </h3>\n <p className=\"text-xs text-text-secondary\">\n {t('adaptercloud.topology.inspector', 'Inspector')}\n </p>\n </div>\n </div>\n <button\n type=\"button\"\n onClick={() => setSelectedId(null)}\n aria-label={t('adaptercloud.topology.closeInspector', 'Close inspector')}\n className=\"rounded p-1 text-text-secondary transition-colors hover:bg-accent hover:text-text-primary focus:outline-none focus:ring-2 focus:ring-[var(--color-focus-ring)]\"\n >\n <X className=\"h-4 w-4\" />\n </button>\n </div>\n\n <dl className=\"space-y-3 text-sm\">\n <div className=\"flex items-center justify-between gap-3\">\n <dt className=\"text-text-secondary\">\n {t('adaptercloud.topology.type', 'Type')}\n </dt>\n <dd className=\"truncate font-mono text-xs text-text-primary\">\n {inspector.node.type ?? '—'}\n </dd>\n </div>\n <div className=\"flex items-center justify-between gap-3\">\n <dt className=\"text-text-secondary\">\n {t('adaptercloud.topology.zone', 'Zone')}\n </dt>\n <dd className=\"text-text-primary\">{inspector.node.group}</dd>\n </div>\n <div className=\"flex items-center justify-between gap-3\">\n <dt className=\"text-text-secondary\">\n {t('adaptercloud.topology.state', 'State')}\n </dt>\n <dd>\n <StatusBadge\n status={inspector.node.state ?? ''}\n label={\n inspector.node.state\n ? resourceStateLabels[inspector.node.state as AdaResourceState]\n : undefined\n }\n />\n </dd>\n </div>\n </dl>\n\n <div className=\"grid grid-cols-2 gap-3\">\n <div className=\"rounded-lg border border-border-subtle bg-bg-sunken/50 p-3\">\n <p className=\"text-2xl font-bold tabular-nums text-text-primary\">\n {inspector.dependencies}\n </p>\n <p className=\"mt-0.5 text-xs text-text-secondary\">\n {t('adaptercloud.topology.dependencies', 'Dependencies')}\n </p>\n </div>\n <div className=\"rounded-lg border border-border-subtle bg-bg-sunken/50 p-3\">\n <p className=\"text-2xl font-bold tabular-nums text-text-primary\">\n {inspector.blastRadius}\n </p>\n <p className=\"mt-0.5 text-xs text-text-secondary\">\n {t('adaptercloud.topology.blastRadius', 'Blast radius')}\n </p>\n </div>\n </div>\n\n <button\n type=\"button\"\n onClick={() => navigateTo(`/resources/${inspector.node.id}`)}\n className=\"inline-flex w-full items-center justify-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 {t('adaptercloud.topology.viewResource', 'View resource')}\n <ArrowRight className=\"h-4 w-4\" />\n </button>\n </div>\n ) : (\n <div className=\"flex flex-col items-center gap-3 py-8 text-center\">\n <span className=\"flex h-11 w-11 items-center justify-center rounded-full bg-bg-sunken\">\n <Waypoints className=\"h-5 w-5 text-text-secondary\" />\n </span>\n <p className=\"text-sm font-medium text-text-primary\">\n {t('adaptercloud.topology.inspectorTitle', 'Inspector')}\n </p>\n <p className=\"max-w-[16rem] text-xs text-text-secondary\">\n {t(\n 'adaptercloud.topology.inspectorHint',\n 'Select any node in the graph to see its type, state, dependencies and downstream blast radius.'\n )}\n </p>\n </div>\n )}\n </GlassCard>\n </aside>\n </div>\n </DataState>\n </PageLayout>\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"TopologyPage.js","names":[],"sources":["../../../../src/adaptercloud/pages/topology/TopologyPage.tsx"],"sourcesContent":["/**\n * Estate Topology Page\n * @module adaptercloud/pages/topology\n *\n * The estate-wide dependency graph — every discovered resource laid out in one\n * live node-link map, grouped into human \"zones\" (Public Cloud, Kubernetes,\n * Edge / IoT, Data Systems, Network) and wired together by the topology edges\n * (`adaListTopologyEdges`). Selecting a node opens an inspector with its\n * dependency count and downstream blast-radius. All data is live from the\n * deployed `wspace-adaptercloud-svc` subgraph — nodes from `adaListResources`,\n * edges from `adaListTopologyEdges`. No mocks.\n */\n\nimport { useCallback, useMemo, useState } from 'react';\nimport { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';\nimport {\n Waypoints,\n Search,\n X,\n RefreshCw,\n ArrowRight,\n Layers,\n Boxes,\n GitBranch,\n} 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 { useResources, useTopology } from '../../hooks/useAdapterCloudApi';\nimport { resourceStateLabels, humanizeEnum } from '../../utils/domain';\nimport { TopologyGraph, type TopologyNode, type TopologyEdge } from '../../components/viz';\nimport type { AdaEdgeKind, AdaResourceState } from '../../types';\n\n/** Canonical edge kinds — drives the legend so it stays in sync with the enum. */\nconst EDGE_KINDS: AdaEdgeKind[] = [\n 'DEPENDS_ON',\n 'CONNECTS_TO',\n 'ROUTES_TO',\n 'REPLICATES_TO',\n 'CONTAINS',\n];\n\n/**\n * Derive a human \"zone\" (graph column) for a resource from its type prefix,\n * falling back to the connection name. Data stores route to \"Data Systems\" even\n * when they carry a cloud prefix, matching how operators think about the estate.\n */\nfunction zoneFor(resourceType: string, connectionName?: string | null): string {\n const t = resourceType.toLowerCase();\n\n // Kubernetes clusters/workloads\n if (/^(gke|k8s|kubernetes|eks|aks)\\./.test(t)) return 'Kubernetes';\n // Edge / IoT devices and gateways\n if (/^(iot|edge)\\./.test(t)) return 'Edge / IoT';\n // Data systems — warehouses, databases, object storage\n if (\n /^snowflake\\./.test(t) ||\n /\\.(db|rds|postgres|mysql|redshift|bigquery|dynamodb|s3|bucket|storage)\\b/.test(t) ||\n t.endsWith('.db')\n ) {\n return 'Data Systems';\n }\n // Network fabric — CDNs, VPCs, routers\n if (t.endsWith('.network') || /^cloudflare\\./.test(t) || /\\.(vpc|cdn)\\b/.test(t)) {\n return 'Network';\n }\n // Public cloud compute/etc.\n if (/^(aws|azure|gcp|oci|alicloud)\\./.test(t)) return 'Public Cloud';\n\n // Fallback: infer from the connection name\n const c = (connectionName ?? '').toLowerCase();\n if (c) {\n if (/gke|k8s|kube/.test(c)) return 'Kubernetes';\n if (/iot|edge/.test(c)) return 'Edge / IoT';\n if (/snowflake|warehouse|postgres|database|\\bdb\\b/.test(c)) return 'Data Systems';\n if (/cloudflare|network|vpc|cdn/.test(c)) return 'Network';\n if (/aws|azure|gcp|cloud/.test(c)) return 'Public Cloud';\n }\n return 'Other';\n}\n\nexport function TopologyPage() {\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 resourcesQuery = useResources({ pagination: { limit: 100 } });\n const topologyQuery = useTopology();\n\n const [search, setSearch] = useState('');\n const [kindFilter, setKindFilter] = useState<AdaEdgeKind | ''>('');\n const [selectedId, setSelectedId] = useState<string | null>(null);\n\n const resources = useMemo(() => resourcesQuery.data?.items ?? [], [resourcesQuery.data]);\n const topoEdges = useMemo(() => topologyQuery.data ?? [], [topologyQuery.data]);\n\n // ---- Build the node set from resources -----------------------------------\n const allNodes: TopologyNode[] = useMemo(\n () =>\n resources.map((r) => ({\n id: r.id,\n label: r.name,\n type: r.resourceType,\n state: r.state,\n group: zoneFor(r.resourceType, r.connection?.name),\n })),\n [resources]\n );\n\n const nodeIds = useMemo(() => new Set(allNodes.map((n) => n.id)), [allNodes]);\n const stateById = useMemo(\n () => new Map(resources.map((r) => [r.id, r.state] as const)),\n [resources]\n );\n const nodeById = useMemo(() => new Map(allNodes.map((n) => [n.id, n] as const)), [allNodes]);\n\n // ---- Build edges — only where BOTH endpoints are in the node set ---------\n const allEdges: TopologyEdge[] = useMemo(\n () =>\n topoEdges\n .filter((e) => nodeIds.has(e.fromResourceId) && nodeIds.has(e.toResourceId))\n .map((e) => ({\n id: e.id,\n from: e.fromResourceId,\n to: e.toResourceId,\n kind: e.kind,\n drifted:\n stateById.get(e.fromResourceId) === 'DRIFTED' ||\n stateById.get(e.toResourceId) === 'DRIFTED',\n })),\n [topoEdges, nodeIds, stateById]\n );\n\n // Forward adjacency for inspector metrics (dependencies + blast radius).\n const outAdj = useMemo(() => {\n const adj = new Map<string, string[]>();\n for (const e of allEdges) {\n const list = adj.get(e.from) ?? [];\n list.push(e.to);\n adj.set(e.from, list);\n }\n return adj;\n }, [allEdges]);\n\n // ---- Search + edge-kind filtering (what the graph actually renders) ------\n const q = search.trim().toLowerCase();\n const displayNodes = useMemo(() => {\n if (!q) return allNodes;\n return allNodes.filter(\n (n) => n.label.toLowerCase().includes(q) || (n.type ?? '').toLowerCase().includes(q)\n );\n }, [allNodes, q]);\n\n const displayNodeIds = useMemo(() => new Set(displayNodes.map((n) => n.id)), [displayNodes]);\n\n const displayEdges = useMemo(\n () =>\n allEdges.filter(\n (e) =>\n displayNodeIds.has(e.from) &&\n displayNodeIds.has(e.to) &&\n (kindFilter === '' || e.kind === kindFilter)\n ),\n [allEdges, displayNodeIds, kindFilter]\n );\n\n // Count edges by kind for the legend badges.\n const kindCounts = useMemo(() => {\n const counts: Record<string, number> = {};\n for (const e of allEdges) counts[e.kind ?? ''] = (counts[e.kind ?? ''] ?? 0) + 1;\n return counts;\n }, [allEdges]);\n\n // ---- Inspector metrics for the selected node -----------------------------\n const inspector = useMemo(() => {\n if (!selectedId) return null;\n const node = nodeById.get(selectedId);\n if (!node) return null;\n\n const dependencies = outAdj.get(selectedId)?.length ?? 0;\n\n // Blast radius = every resource transitively reachable downstream.\n const seen = new Set<string>();\n const stack = [...(outAdj.get(selectedId) ?? [])];\n while (stack.length) {\n const cur = stack.pop();\n if (cur === undefined || cur === selectedId || seen.has(cur)) continue;\n seen.add(cur);\n for (const next of outAdj.get(cur) ?? []) stack.push(next);\n }\n\n return { node, dependencies, blastRadius: seen.size };\n }, [selectedId, nodeById, outAdj]);\n\n const zoneCount = useMemo(() => new Set(allNodes.map((n) => n.group)).size, [allNodes]);\n\n const onSelect = useCallback((id: string) => {\n setSelectedId((prev) => (prev === id ? null : id));\n }, []);\n\n const isLoading = resourcesQuery.isLoading || topologyQuery.isLoading;\n const isError = resourcesQuery.isError || topologyQuery.isError;\n\n const refetchAll = useCallback(() => {\n void resourcesQuery.refetch();\n void topologyQuery.refetch();\n }, [resourcesQuery, topologyQuery]);\n\n return (\n <PageLayout\n title={t('adaptercloud.topology.title', 'Estate Topology')}\n description={t(\n 'adaptercloud.topology.subtitle',\n 'A live dependency map of your entire infrastructure — every system in one graph.'\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 {/* Toolbar — search + edge-kind filter + estate counts */}\n <div className=\"mb-6 flex flex-wrap items-center gap-3\">\n <div className=\"relative w-full sm:w-64\">\n <Search className=\"pointer-events-none absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-text-secondary\" />\n <input\n type=\"text\"\n value={search}\n onChange={(e) => setSearch(e.target.value)}\n aria-label={t('adaptercloud.topology.searchNodes', 'Search nodes')}\n placeholder={t('adaptercloud.topology.searchPlaceholder', 'Search nodes…')}\n className=\"w-full rounded-lg border border-border-subtle bg-bg-surface py-2 pl-9 pr-9 text-sm text-text-primary placeholder:text-text-secondary focus:outline-none focus:ring-2 focus:ring-[var(--color-focus-ring)]\"\n />\n {search && (\n <button\n type=\"button\"\n onClick={() => setSearch('')}\n aria-label={t('adaptercloud.topology.clearSearch', 'Clear search')}\n className=\"absolute right-2 top-1/2 -translate-y-1/2 rounded p-0.5 text-text-secondary transition-colors hover:text-text-primary focus:outline-none focus:ring-2 focus:ring-[var(--color-focus-ring)]\"\n >\n <X className=\"h-4 w-4\" />\n </button>\n )}\n </div>\n\n <select\n value={kindFilter}\n onChange={(e) => setKindFilter(e.target.value as AdaEdgeKind | '')}\n aria-label={t('adaptercloud.topology.filterByKind', 'Filter by edge kind')}\n className=\"w-full 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)] sm:w-auto\"\n >\n <option value=\"\">{t('adaptercloud.topology.allKinds', 'All edge kinds')}</option>\n {EDGE_KINDS.map((k) => (\n <option key={k} value={k}>\n {humanizeEnum(k)}\n </option>\n ))}\n </select>\n\n {!isLoading && !isError && allNodes.length > 0 && (\n <div className=\"ml-auto flex flex-wrap items-center gap-4 text-xs text-text-secondary\">\n <span className=\"inline-flex items-center gap-1.5\">\n <Boxes className=\"h-3.5 w-3.5\" />\n <span className=\"tabular-nums font-medium text-text-primary\">\n {allNodes.length}\n </span>{' '}\n {t('adaptercloud.topology.nodes', 'nodes')}\n </span>\n <span className=\"inline-flex items-center gap-1.5\">\n <GitBranch className=\"h-3.5 w-3.5\" />\n <span className=\"tabular-nums font-medium text-text-primary\">\n {allEdges.length}\n </span>{' '}\n {t('adaptercloud.topology.edges', 'edges')}\n </span>\n <span className=\"inline-flex items-center gap-1.5\">\n <Layers className=\"h-3.5 w-3.5\" />\n <span className=\"tabular-nums font-medium text-text-primary\">{zoneCount}</span>{' '}\n {t('adaptercloud.topology.zones', 'zones')}\n </span>\n </div>\n )}\n </div>\n\n <DataState\n isLoading={isLoading}\n isError={isError}\n isEmpty={allNodes.length === 0}\n error={resourcesQuery.error ?? topologyQuery.error}\n onRetry={refetchAll}\n emptyTitle={t('adaptercloud.topology.emptyTitle', 'No topology yet')}\n emptyDescription={t(\n 'adaptercloud.topology.emptyDescription',\n 'No topology yet — register resources and their dependencies to see them mapped here.'\n )}\n emptyIcon={<Waypoints className=\"h-10 w-10\" />}\n emptyAction={\n <button\n type=\"button\"\n onClick={() => navigateTo('/resources')}\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.topology.goToResources', 'Go to resources')}\n </button>\n }\n >\n <div className=\"grid gap-6 lg:grid-cols-[minmax(0,1fr)_20rem]\">\n {/* Graph + legend */}\n <div className=\"min-w-0 space-y-4\">\n {allEdges.length === 0 && (\n <p className=\"rounded-lg border border-border-subtle bg-bg-sunken/50 px-4 py-3 text-sm text-text-secondary\">\n {t(\n 'adaptercloud.topology.noEdges',\n 'No dependency edges recorded yet — nodes are shown grouped by zone. Add dependencies from a resource to connect the graph.'\n )}\n </p>\n )}\n\n {displayNodes.length > 0 ? (\n <TopologyGraph\n nodes={displayNodes}\n edges={displayEdges}\n selectedId={selectedId}\n onSelect={onSelect}\n />\n ) : (\n <div className=\"flex min-h-[200px] flex-col items-center justify-center gap-2 rounded-xl border border-border-subtle bg-bg-surface p-8 text-center\">\n <Search className=\"h-8 w-8 text-text-secondary\" />\n <p className=\"text-sm text-text-secondary\">\n {t('adaptercloud.topology.noMatches', 'No nodes match your search.')}\n </p>\n </div>\n )}\n\n {/* Edge-kinds legend */}\n <GlassCard className=\"p-4\">\n <h3 className=\"mb-3 text-sm font-semibold text-text-primary\">\n {t('adaptercloud.topology.legend', 'Legend')}\n </h3>\n <div className=\"grid gap-4 sm:grid-cols-2\">\n {/* Edge signals (how the graph paints edges) */}\n <div>\n <p className=\"mb-2 text-xs font-medium uppercase tracking-wide text-text-secondary\">\n {t('adaptercloud.topology.edgeSignals', 'Edge signals')}\n </p>\n <ul className=\"space-y-1.5 text-xs text-text-secondary\">\n <li className=\"flex items-center gap-2\">\n <span className=\"h-0.5 w-6 shrink-0 rounded bg-border-strong\" aria-hidden />\n {t('adaptercloud.topology.dependency', 'Dependency')}\n </li>\n <li className=\"flex items-center gap-2\">\n <span className=\"h-0.5 w-6 shrink-0 rounded bg-accent-blue\" aria-hidden />\n {t('adaptercloud.topology.selectedPath', 'Selected node path')}\n </li>\n <li className=\"flex items-center gap-2\">\n <span\n className=\"h-0.5 w-6 shrink-0 rounded bg-status-warning-text\"\n aria-hidden\n />\n {t('adaptercloud.topology.drift', 'Drift (endpoint drifted)')}\n </li>\n </ul>\n </div>\n\n {/* Edge kinds (semantic relationship types) */}\n <div>\n <p className=\"mb-2 text-xs font-medium uppercase tracking-wide text-text-secondary\">\n {t('adaptercloud.topology.edgeKinds', 'Edge kinds')}\n </p>\n <ul className=\"flex flex-wrap gap-1.5\">\n {EDGE_KINDS.map((k) => (\n <li key={k}>\n <span className=\"inline-flex items-center gap-1.5 rounded-full bg-bg-sunken px-2.5 py-0.5 text-xs text-text-secondary\">\n {humanizeEnum(k)}\n <span className=\"tabular-nums font-medium text-text-primary\">\n {kindCounts[k] ?? 0}\n </span>\n </span>\n </li>\n ))}\n </ul>\n </div>\n </div>\n </GlassCard>\n </div>\n\n {/* Inspector */}\n <aside className=\"lg:sticky lg:top-6 lg:h-fit\">\n <GlassCard className=\"p-5\">\n {inspector ? (\n <div className=\"space-y-5\">\n <div className=\"flex items-start justify-between gap-3\">\n <div className=\"flex min-w-0 items-center gap-2.5\">\n <span className=\"flex h-9 w-9 shrink-0 items-center justify-center rounded-lg bg-bg-sunken\">\n <Waypoints className=\"h-4.5 w-4.5 text-accent-blue\" />\n </span>\n <div className=\"min-w-0\">\n <h3 className=\"truncate text-base font-semibold text-text-primary\">\n {inspector.node.label}\n </h3>\n <p className=\"text-xs text-text-secondary\">\n {t('adaptercloud.topology.inspector', 'Inspector')}\n </p>\n </div>\n </div>\n <button\n type=\"button\"\n onClick={() => setSelectedId(null)}\n aria-label={t('adaptercloud.topology.closeInspector', 'Close inspector')}\n className=\"rounded p-1 text-text-secondary transition-colors hover:bg-accent hover:text-text-primary focus:outline-none focus:ring-2 focus:ring-[var(--color-focus-ring)]\"\n >\n <X className=\"h-4 w-4\" />\n </button>\n </div>\n\n <dl className=\"space-y-3 text-sm\">\n <div className=\"flex items-center justify-between gap-3\">\n <dt className=\"text-text-secondary\">\n {t('adaptercloud.topology.type', 'Type')}\n </dt>\n <dd className=\"truncate font-mono text-xs text-text-primary\">\n {inspector.node.type ?? '—'}\n </dd>\n </div>\n <div className=\"flex items-center justify-between gap-3\">\n <dt className=\"text-text-secondary\">\n {t('adaptercloud.topology.zone', 'Zone')}\n </dt>\n <dd className=\"text-text-primary\">{inspector.node.group}</dd>\n </div>\n <div className=\"flex items-center justify-between gap-3\">\n <dt className=\"text-text-secondary\">\n {t('adaptercloud.topology.state', 'State')}\n </dt>\n <dd>\n <StatusBadge\n status={inspector.node.state ?? ''}\n label={\n inspector.node.state\n ? resourceStateLabels[inspector.node.state as AdaResourceState]\n : undefined\n }\n />\n </dd>\n </div>\n </dl>\n\n <div className=\"grid grid-cols-2 gap-3\">\n <div className=\"rounded-lg border border-border-subtle bg-bg-sunken/50 p-3\">\n <p className=\"text-2xl font-bold tabular-nums text-text-primary\">\n {inspector.dependencies}\n </p>\n <p className=\"mt-0.5 text-xs text-text-secondary\">\n {t('adaptercloud.topology.dependencies', 'Dependencies')}\n </p>\n </div>\n <div className=\"rounded-lg border border-border-subtle bg-bg-sunken/50 p-3\">\n <p className=\"text-2xl font-bold tabular-nums text-text-primary\">\n {inspector.blastRadius}\n </p>\n <p className=\"mt-0.5 text-xs text-text-secondary\">\n {t('adaptercloud.topology.blastRadius', 'Blast radius')}\n </p>\n </div>\n </div>\n\n <button\n type=\"button\"\n onClick={() => navigateTo(`/resources/${inspector.node.id}`)}\n className=\"inline-flex w-full items-center justify-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 {t('adaptercloud.topology.viewResource', 'View resource')}\n <ArrowRight className=\"h-4 w-4\" />\n </button>\n </div>\n ) : (\n <div className=\"flex flex-col items-center gap-3 py-8 text-center\">\n <span className=\"flex h-11 w-11 items-center justify-center rounded-full bg-bg-sunken\">\n <Waypoints className=\"h-5 w-5 text-text-secondary\" />\n </span>\n <p className=\"text-sm font-medium text-text-primary\">\n {t('adaptercloud.topology.inspectorTitle', 'Inspector')}\n </p>\n <p className=\"max-w-[16rem] text-xs text-text-secondary\">\n {t(\n 'adaptercloud.topology.inspectorHint',\n 'Select any node in the graph to see its type, state, dependencies and downstream blast radius.'\n )}\n </p>\n </div>\n )}\n </GlassCard>\n </aside>\n </div>\n </DataState>\n </PageLayout>\n );\n}\n"],"mappings":";;;;;;;;;;;;;;AAoCA,IAAM,IAA4B;CAChC;CACA;CACA;CACA;CACA;CACD;AAOD,SAAS,EAAQ,GAAsB,GAAwC;CAC7E,IAAM,IAAI,EAAa,aAAa;AAGpC,KAAI,kCAAkC,KAAK,EAAE,CAAE,QAAO;AAEtD,KAAI,gBAAgB,KAAK,EAAE,CAAE,QAAO;AAEpC,KACE,eAAe,KAAK,EAAE,IACtB,2EAA2E,KAAK,EAAE,IAClF,EAAE,SAAS,MAAM,CAEjB,QAAO;AAGT,KAAI,EAAE,SAAS,WAAW,IAAI,gBAAgB,KAAK,EAAE,IAAI,gBAAgB,KAAK,EAAE,CAC9E,QAAO;AAGT,KAAI,kCAAkC,KAAK,EAAE,CAAE,QAAO;CAGtD,IAAM,KAAK,KAAkB,IAAI,aAAa;AAC9C,KAAI,GAAG;AACL,MAAI,eAAe,KAAK,EAAE,CAAE,QAAO;AACnC,MAAI,WAAW,KAAK,EAAE,CAAE,QAAO;AAC/B,MAAI,+CAA+C,KAAK,EAAE,CAAE,QAAO;AACnE,MAAI,6BAA6B,KAAK,EAAE,CAAE,QAAO;AACjD,MAAI,sBAAsB,KAAK,EAAE,CAAE,QAAO;;AAE5C,QAAO;;AAGT,SAAgB,IAAe;CAC7B,IAAM,EAAE,kBAAe,GAAiB,EAClC,EAAE,GAAG,MAAc,IAAS,EAC5B,IAAI,GACP,GAAa,MAA6B;EACzC,IAAM,IAAQ,EAAU,EAAI;AAC5B,SAAO,MAAU,IAAM,IAAW;IAEpC,CAAC,EAAU,CACZ,EAEK,IAAiB,EAAa,EAAE,YAAY,EAAE,OAAO,KAAK,EAAE,CAAC,EAC7D,IAAgB,GAAa,EAE7B,CAAC,GAAQ,KAAa,EAAS,GAAG,EAClC,CAAC,GAAY,KAAiB,EAA2B,GAAG,EAC5D,CAAC,GAAY,KAAiB,EAAwB,KAAK,EAE3D,IAAY,QAAc,EAAe,MAAM,SAAS,EAAE,EAAE,CAAC,EAAe,KAAK,CAAC,EAClF,IAAY,QAAc,EAAc,QAAQ,EAAE,EAAE,CAAC,EAAc,KAAK,CAAC,EAGzE,IAA2B,QAE7B,EAAU,KAAK,OAAO;EACpB,IAAI,EAAE;EACN,OAAO,EAAE;EACT,MAAM,EAAE;EACR,OAAO,EAAE;EACT,OAAO,EAAQ,EAAE,cAAc,EAAE,YAAY,KAAK;EACnD,EAAE,EACL,CAAC,EAAU,CACZ,EAEK,IAAU,QAAc,IAAI,IAAI,EAAS,KAAK,MAAM,EAAE,GAAG,CAAC,EAAE,CAAC,EAAS,CAAC,EACvE,IAAY,QACV,IAAI,IAAI,EAAU,KAAK,MAAM,CAAC,EAAE,IAAI,EAAE,MAAM,CAAU,CAAC,EAC7D,CAAC,EAAU,CACZ,EACK,IAAW,QAAc,IAAI,IAAI,EAAS,KAAK,MAAM,CAAC,EAAE,IAAI,EAAE,CAAU,CAAC,EAAE,CAAC,EAAS,CAAC,EAGtF,IAA2B,QAE7B,EACG,QAAQ,MAAM,EAAQ,IAAI,EAAE,eAAe,IAAI,EAAQ,IAAI,EAAE,aAAa,CAAC,CAC3E,KAAK,OAAO;EACX,IAAI,EAAE;EACN,MAAM,EAAE;EACR,IAAI,EAAE;EACN,MAAM,EAAE;EACR,SACE,EAAU,IAAI,EAAE,eAAe,KAAK,aACpC,EAAU,IAAI,EAAE,aAAa,KAAK;EACrC,EAAE,EACP;EAAC;EAAW;EAAS;EAAU,CAChC,EAGK,IAAS,QAAc;EAC3B,IAAM,oBAAM,IAAI,KAAuB;AACvC,OAAK,IAAM,KAAK,GAAU;GACxB,IAAM,IAAO,EAAI,IAAI,EAAE,KAAK,IAAI,EAAE;AAElC,GADA,EAAK,KAAK,EAAE,GAAG,EACf,EAAI,IAAI,EAAE,MAAM,EAAK;;AAEvB,SAAO;IACN,CAAC,EAAS,CAAC,EAGR,IAAI,EAAO,MAAM,CAAC,aAAa,EAC/B,IAAe,QACd,IACE,EAAS,QACb,MAAM,EAAE,MAAM,aAAa,CAAC,SAAS,EAAE,KAAK,EAAE,QAAQ,IAAI,aAAa,CAAC,SAAS,EAAE,CACrF,GAHc,GAId,CAAC,GAAU,EAAE,CAAC,EAEX,IAAiB,QAAc,IAAI,IAAI,EAAa,KAAK,MAAM,EAAE,GAAG,CAAC,EAAE,CAAC,EAAa,CAAC,EAEtF,IAAe,QAEjB,EAAS,QACN,MACC,EAAe,IAAI,EAAE,KAAK,IAC1B,EAAe,IAAI,EAAE,GAAG,KACvB,MAAe,MAAM,EAAE,SAAS,GACpC,EACH;EAAC;EAAU;EAAgB;EAAW,CACvC,EAGK,IAAa,QAAc;EAC/B,IAAM,IAAiC,EAAE;AACzC,OAAK,IAAM,KAAK,EAAU,GAAO,EAAE,QAAQ,OAAO,EAAO,EAAE,QAAQ,OAAO,KAAK;AAC/E,SAAO;IACN,CAAC,EAAS,CAAC,EAGR,IAAY,QAAc;AAC9B,MAAI,CAAC,EAAY,QAAO;EACxB,IAAM,IAAO,EAAS,IAAI,EAAW;AACrC,MAAI,CAAC,EAAM,QAAO;EAElB,IAAM,IAAe,EAAO,IAAI,EAAW,EAAE,UAAU,GAGjD,oBAAO,IAAI,KAAa,EACxB,IAAQ,CAAC,GAAI,EAAO,IAAI,EAAW,IAAI,EAAE,CAAE;AACjD,SAAO,EAAM,SAAQ;GACnB,IAAM,IAAM,EAAM,KAAK;AACnB,eAAQ,KAAA,KAAa,MAAQ,KAAc,EAAK,IAAI,EAAI,GAC5D;MAAK,IAAI,EAAI;AACb,SAAK,IAAM,KAAQ,EAAO,IAAI,EAAI,IAAI,EAAE,CAAE,GAAM,KAAK,EAAK;;;AAG5D,SAAO;GAAE;GAAM;GAAc,aAAa,EAAK;GAAM;IACpD;EAAC;EAAY;EAAU;EAAO,CAAC,EAE5B,IAAY,QAAc,IAAI,IAAI,EAAS,KAAK,MAAM,EAAE,MAAM,CAAC,CAAC,MAAM,CAAC,EAAS,CAAC,EAEjF,IAAW,GAAa,MAAe;AAC3C,KAAe,MAAU,MAAS,IAAK,OAAO,EAAI;IACjD,EAAE,CAAC,EAEA,IAAY,EAAe,aAAa,EAAc,WACtD,IAAU,EAAe,WAAW,EAAc,SAElD,IAAa,QAAkB;AAE9B,EADA,EAAe,SAAS,EACxB,EAAc,SAAS;IAC3B,CAAC,GAAgB,EAAc,CAAC;AAEnC,QACE,kBAAC,GAAD;EACE,OAAO,EAAE,+BAA+B,kBAAkB;EAC1D,aAAa,EACX,kCACA,mFACD;EACD,SACE,kBAAC,UAAD;GACE,MAAK;GACL,SAAS;GACT,WAAU;aAHZ,CAKE,kBAAC,GAAD,EAAW,WAAU,WAAY,CAAA,EAChC,EAAE,+BAA+B,UAAU,CACrC;;YAdb,CAkBE,kBAAC,OAAD;GAAK,WAAU;aAAf;IACE,kBAAC,OAAD;KAAK,WAAU;eAAf;MACE,kBAAC,GAAD,EAAQ,WAAU,4FAA6F,CAAA;MAC/G,kBAAC,SAAD;OACE,MAAK;OACL,OAAO;OACP,WAAW,MAAM,EAAU,EAAE,OAAO,MAAM;OAC1C,cAAY,EAAE,qCAAqC,eAAe;OAClE,aAAa,EAAE,2CAA2C,gBAAgB;OAC1E,WAAU;OACV,CAAA;MACD,KACC,kBAAC,UAAD;OACE,MAAK;OACL,eAAe,EAAU,GAAG;OAC5B,cAAY,EAAE,qCAAqC,eAAe;OAClE,WAAU;iBAEV,kBAAC,GAAD,EAAG,WAAU,WAAY,CAAA;OAClB,CAAA;MAEP;;IAEN,kBAAC,UAAD;KACE,OAAO;KACP,WAAW,MAAM,EAAc,EAAE,OAAO,MAA0B;KAClE,cAAY,EAAE,sCAAsC,sBAAsB;KAC1E,WAAU;eAJZ,CAME,kBAAC,UAAD;MAAQ,OAAM;gBAAI,EAAE,kCAAkC,iBAAiB;MAAU,CAAA,EAChF,EAAW,KAAK,MACf,kBAAC,UAAD;MAAgB,OAAO;gBACpB,EAAa,EAAE;MACT,EAFI,EAEJ,CACT,CACK;;IAER,CAAC,KAAa,CAAC,KAAW,EAAS,SAAS,KAC3C,kBAAC,OAAD;KAAK,WAAU;eAAf;MACE,kBAAC,QAAD;OAAM,WAAU;iBAAhB;QACE,kBAAC,GAAD,EAAO,WAAU,eAAgB,CAAA;QACjC,kBAAC,QAAD;SAAM,WAAU;mBACb,EAAS;SACL,CAAA;QAAC;QACP,EAAE,+BAA+B,QAAQ;QACrC;;MACP,kBAAC,QAAD;OAAM,WAAU;iBAAhB;QACE,kBAAC,GAAD,EAAW,WAAU,eAAgB,CAAA;QACrC,kBAAC,QAAD;SAAM,WAAU;mBACb,EAAS;SACL,CAAA;QAAC;QACP,EAAE,+BAA+B,QAAQ;QACrC;;MACP,kBAAC,QAAD;OAAM,WAAU;iBAAhB;QACE,kBAAC,GAAD,EAAQ,WAAU,eAAgB,CAAA;QAClC,kBAAC,QAAD;SAAM,WAAU;mBAA8C;SAAiB,CAAA;QAAC;QAC/E,EAAE,+BAA+B,QAAQ;QACrC;;MACH;;IAEJ;MAEN,kBAAC,GAAD;GACa;GACF;GACT,SAAS,EAAS,WAAW;GAC7B,OAAO,EAAe,SAAS,EAAc;GAC7C,SAAS;GACT,YAAY,EAAE,oCAAoC,kBAAkB;GACpE,kBAAkB,EAChB,0CACA,uFACD;GACD,WAAW,kBAAC,GAAD,EAAW,WAAU,aAAc,CAAA;GAC9C,aACE,kBAAC,UAAD;IACE,MAAK;IACL,eAAe,EAAW,aAAa;IACvC,WAAU;cAET,EAAE,uCAAuC,kBAAkB;IACrD,CAAA;aAGX,kBAAC,OAAD;IAAK,WAAU;cAAf,CAEE,kBAAC,OAAD;KAAK,WAAU;eAAf;MACG,EAAS,WAAW,KACnB,kBAAC,KAAD;OAAG,WAAU;iBACV,EACC,iCACA,6HACD;OACC,CAAA;MAGL,EAAa,SAAS,IACrB,kBAAC,IAAD;OACE,OAAO;OACP,OAAO;OACK;OACF;OACV,CAAA,GAEF,kBAAC,OAAD;OAAK,WAAU;iBAAf,CACE,kBAAC,GAAD,EAAQ,WAAU,+BAAgC,CAAA,EAClD,kBAAC,KAAD;QAAG,WAAU;kBACV,EAAE,mCAAmC,8BAA8B;QAClE,CAAA,CACA;;MAIR,kBAAC,GAAD;OAAW,WAAU;iBAArB,CACE,kBAAC,MAAD;QAAI,WAAU;kBACX,EAAE,gCAAgC,SAAS;QACzC,CAAA,EACL,kBAAC,OAAD;QAAK,WAAU;kBAAf,CAEE,kBAAC,OAAD,EAAA,UAAA,CACE,kBAAC,KAAD;SAAG,WAAU;mBACV,EAAE,qCAAqC,eAAe;SACrD,CAAA,EACJ,kBAAC,MAAD;SAAI,WAAU;mBAAd;UACE,kBAAC,MAAD;WAAI,WAAU;qBAAd,CACE,kBAAC,QAAD;YAAM,WAAU;YAA8C,eAAA;YAAc,CAAA,EAC3E,EAAE,oCAAoC,aAAa,CACjD;;UACL,kBAAC,MAAD;WAAI,WAAU;qBAAd,CACE,kBAAC,QAAD;YAAM,WAAU;YAA4C,eAAA;YAAc,CAAA,EACzE,EAAE,sCAAsC,qBAAqB,CAC3D;;UACL,kBAAC,MAAD;WAAI,WAAU;qBAAd,CACE,kBAAC,QAAD;YACE,WAAU;YACV,eAAA;YACA,CAAA,EACD,EAAE,+BAA+B,2BAA2B,CAC1D;;UACF;WACD,EAAA,CAAA,EAGN,kBAAC,OAAD,EAAA,UAAA,CACE,kBAAC,KAAD;SAAG,WAAU;mBACV,EAAE,mCAAmC,aAAa;SACjD,CAAA,EACJ,kBAAC,MAAD;SAAI,WAAU;mBACX,EAAW,KAAK,MACf,kBAAC,MAAD,EAAA,UACE,kBAAC,QAAD;UAAM,WAAU;oBAAhB,CACG,EAAa,EAAE,EAChB,kBAAC,QAAD;WAAM,WAAU;qBACb,EAAW,MAAM;WACb,CAAA,CACF;aACJ,EAPI,EAOJ,CACL;SACC,CAAA,CACD,EAAA,CAAA,CACF;UACI;;MACR;QAGN,kBAAC,SAAD;KAAO,WAAU;eACf,kBAAC,GAAD;MAAW,WAAU;gBAClB,IACC,kBAAC,OAAD;OAAK,WAAU;iBAAf;QACE,kBAAC,OAAD;SAAK,WAAU;mBAAf,CACE,kBAAC,OAAD;UAAK,WAAU;oBAAf,CACE,kBAAC,QAAD;WAAM,WAAU;qBACd,kBAAC,GAAD,EAAW,WAAU,gCAAiC,CAAA;WACjD,CAAA,EACP,kBAAC,OAAD;WAAK,WAAU;qBAAf,CACE,kBAAC,MAAD;YAAI,WAAU;sBACX,EAAU,KAAK;YACb,CAAA,EACL,kBAAC,KAAD;YAAG,WAAU;sBACV,EAAE,mCAAmC,YAAY;YAChD,CAAA,CACA;aACF;aACN,kBAAC,UAAD;UACE,MAAK;UACL,eAAe,EAAc,KAAK;UAClC,cAAY,EAAE,wCAAwC,kBAAkB;UACxE,WAAU;oBAEV,kBAAC,GAAD,EAAG,WAAU,WAAY,CAAA;UAClB,CAAA,CACL;;QAEN,kBAAC,MAAD;SAAI,WAAU;mBAAd;UACE,kBAAC,OAAD;WAAK,WAAU;qBAAf,CACE,kBAAC,MAAD;YAAI,WAAU;sBACX,EAAE,8BAA8B,OAAO;YACrC,CAAA,EACL,kBAAC,MAAD;YAAI,WAAU;sBACX,EAAU,KAAK,QAAQ;YACrB,CAAA,CACD;;UACN,kBAAC,OAAD;WAAK,WAAU;qBAAf,CACE,kBAAC,MAAD;YAAI,WAAU;sBACX,EAAE,8BAA8B,OAAO;YACrC,CAAA,EACL,kBAAC,MAAD;YAAI,WAAU;sBAAqB,EAAU,KAAK;YAAW,CAAA,CACzD;;UACN,kBAAC,OAAD;WAAK,WAAU;qBAAf,CACE,kBAAC,MAAD;YAAI,WAAU;sBACX,EAAE,+BAA+B,QAAQ;YACvC,CAAA,EACL,kBAAC,MAAD,EAAA,UACE,kBAAC,GAAD;YACE,QAAQ,EAAU,KAAK,SAAS;YAChC,OACE,EAAU,KAAK,QACX,EAAoB,EAAU,KAAK,SACnC,KAAA;YAEN,CAAA,EACC,CAAA,CACD;;UACH;;QAEL,kBAAC,OAAD;SAAK,WAAU;mBAAf,CACE,kBAAC,OAAD;UAAK,WAAU;oBAAf,CACE,kBAAC,KAAD;WAAG,WAAU;qBACV,EAAU;WACT,CAAA,EACJ,kBAAC,KAAD;WAAG,WAAU;qBACV,EAAE,sCAAsC,eAAe;WACtD,CAAA,CACA;aACN,kBAAC,OAAD;UAAK,WAAU;oBAAf,CACE,kBAAC,KAAD;WAAG,WAAU;qBACV,EAAU;WACT,CAAA,EACJ,kBAAC,KAAD;WAAG,WAAU;qBACV,EAAE,qCAAqC,eAAe;WACrD,CAAA,CACA;YACF;;QAEN,kBAAC,UAAD;SACE,MAAK;SACL,eAAe,EAAW,cAAc,EAAU,KAAK,KAAK;SAC5D,WAAU;mBAHZ,CAKG,EAAE,sCAAsC,gBAAgB,EACzD,kBAAC,GAAD,EAAY,WAAU,WAAY,CAAA,CAC3B;;QACL;WAEN,kBAAC,OAAD;OAAK,WAAU;iBAAf;QACE,kBAAC,QAAD;SAAM,WAAU;mBACd,kBAAC,GAAD,EAAW,WAAU,+BAAgC,CAAA;SAChD,CAAA;QACP,kBAAC,KAAD;SAAG,WAAU;mBACV,EAAE,wCAAwC,YAAY;SACrD,CAAA;QACJ,kBAAC,KAAD;SAAG,WAAU;mBACV,EACC,uCACA,iGACD;SACC,CAAA;QACA;;MAEE,CAAA;KACN,CAAA,CACJ;;GACI,CAAA,CACD"}
|
|
@@ -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 ChevronDown,\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 { cn } from '../../utils/cn';\nimport type { AdaPolicyViolation, AdaViolationStatus } from '../../types';\n\n/** Segmented status tabs — All plus each `AdaViolationStatus`, in queue order. */\nconst STATUS_TABS: (AdaViolationStatus | '')[] = [\n '',\n 'OPEN',\n 'ACKNOWLEDGED',\n 'RESOLVED',\n 'WAIVED',\n];\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 const [expanded, setExpanded] = useState<Set<string>>(new Set());\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 // Live per-status counts for the segmented tabs. Each is a cheap `limit: 1`\n // query read for its `pageInfo.totalCount` (the estate-wide total for that\n // filter); \"All\" is the same query without a status. They respect the current\n // policy/resource URL scope so the badges match the visible queue.\n const countBase = { policyId, resourceId, pagination: { limit: 1, offset: 0 } };\n const allCount = useViolations(countBase);\n const openCount = useViolations({ ...countBase, status: 'OPEN' });\n const ackCount = useViolations({ ...countBase, status: 'ACKNOWLEDGED' });\n const resolvedCount = useViolations({ ...countBase, status: 'RESOLVED' });\n const waivedCount = useViolations({ ...countBase, status: 'WAIVED' });\n\n const tabCounts: Record<AdaViolationStatus | '', number | undefined> = {\n '': allCount.data?.pageInfo.totalCount ?? undefined,\n OPEN: openCount.data?.pageInfo.totalCount ?? undefined,\n ACKNOWLEDGED: ackCount.data?.pageInfo.totalCount ?? undefined,\n RESOLVED: resolvedCount.data?.pageInfo.totalCount ?? undefined,\n WAIVED: waivedCount.data?.pageInfo.totalCount ?? undefined,\n };\n\n const toggleEvidence = useCallback((id: string) => {\n setExpanded((prev) => {\n const next = new Set(prev);\n if (next.has(id)) next.delete(id);\n else next.add(id);\n return next;\n });\n }, []);\n\n // Severity ordering is applied ONLY within the current server page. The queue\n // pages by offset, so this does not globally reorder across pages — a page-2\n // CRITICAL still sorts below a page-1 LOW. Kept as a within-page readability\n // aid (the service has no severity sort arg to make it global).\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\n className=\"mb-6 flex flex-wrap items-center gap-2\"\n role=\"tablist\"\n aria-label={t('adaptercloud.violations.filterByStatus', 'Filter by status')}\n >\n {STATUS_TABS.map((tab) => {\n const active = status === tab;\n const count = tabCounts[tab];\n const label =\n tab === ''\n ? t('adaptercloud.violations.allStatuses', 'All')\n : violationStatusLabels[tab];\n return (\n <button\n key={tab || 'all'}\n type=\"button\"\n role=\"tab\"\n aria-selected={active}\n onClick={() => {\n setStatus(tab);\n setOffset(0);\n }}\n className={cn(\n 'inline-flex items-center gap-2 rounded-full border px-3.5 py-1.5 text-sm font-medium transition-colors',\n active\n ? 'border-transparent bg-action-primary-bg text-action-primary-text'\n : 'border-border-subtle text-text-secondary hover:bg-accent'\n )}\n >\n {label}\n {count !== undefined && (\n <span\n className={cn(\n 'inline-flex min-w-5 items-center justify-center rounded-full px-1.5 text-xs font-semibold tabular-nums',\n active\n ? 'bg-action-primary-text/20 text-action-primary-text'\n : 'bg-bg-sunken text-text-secondary'\n )}\n >\n {count}\n </span>\n )}\n </button>\n );\n })}\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\n role=\"alert\"\n aria-live=\"polite\"\n className=\"mb-4 rounded-lg bg-status-error-bg-subtle px-3 py-2 text-sm text-status-error-text\"\n >\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-accent-blue 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 {v.evidence != null && (\n <div className=\"mt-2\">\n <button\n type=\"button\"\n onClick={() => toggleEvidence(v.id)}\n aria-expanded={expanded.has(v.id)}\n className=\"inline-flex items-center gap-1 text-xs font-medium text-text-secondary transition-colors hover:text-text-primary\"\n >\n {expanded.has(v.id) ? (\n <ChevronDown className=\"h-3.5 w-3.5\" />\n ) : (\n <ChevronRight className=\"h-3.5 w-3.5\" />\n )}\n {t('adaptercloud.violations.evidence', 'Evidence')}\n </button>\n {expanded.has(v.id) && (\n <pre className=\"mt-2 overflow-x-auto rounded-lg border border-border-subtle bg-bg-sunken p-3 font-mono text-xs text-text-secondary\">\n {JSON.stringify(v.evidence, null, 2)}\n </pre>\n )}\n </div>\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 <label htmlFor=\"ada-waiver-reason\" className=\"mb-1.5 block 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 </label>\n <textarea\n id=\"ada-waiver-reason\"\n value={waiverReason}\n onChange={(e) => setWaiverReason(e.target.value)}\n rows={4}\n autoFocus\n aria-label={t('adaptercloud.violations.waiverReason', 'Waiver reason')}\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 && (\n <p role=\"alert\" aria-live=\"polite\" className=\"mt-2 text-sm text-status-error-text\">\n {actionError}\n </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 ChevronDown,\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 { cn } from '../../utils/cn';\nimport type { AdaPolicyViolation, AdaViolationStatus } from '../../types';\n\n/** Segmented status tabs — All plus each `AdaViolationStatus`, in queue order. */\nconst STATUS_TABS: (AdaViolationStatus | '')[] = ['', 'OPEN', 'ACKNOWLEDGED', 'RESOLVED', 'WAIVED'];\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 const [expanded, setExpanded] = useState<Set<string>>(new Set());\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 // Live per-status counts for the segmented tabs. Each is a cheap `limit: 1`\n // query read for its `pageInfo.totalCount` (the estate-wide total for that\n // filter); \"All\" is the same query without a status. They respect the current\n // policy/resource URL scope so the badges match the visible queue.\n const countBase = { policyId, resourceId, pagination: { limit: 1, offset: 0 } };\n const allCount = useViolations(countBase);\n const openCount = useViolations({ ...countBase, status: 'OPEN' });\n const ackCount = useViolations({ ...countBase, status: 'ACKNOWLEDGED' });\n const resolvedCount = useViolations({ ...countBase, status: 'RESOLVED' });\n const waivedCount = useViolations({ ...countBase, status: 'WAIVED' });\n\n const tabCounts: Record<AdaViolationStatus | '', number | undefined> = {\n '': allCount.data?.pageInfo.totalCount ?? undefined,\n OPEN: openCount.data?.pageInfo.totalCount ?? undefined,\n ACKNOWLEDGED: ackCount.data?.pageInfo.totalCount ?? undefined,\n RESOLVED: resolvedCount.data?.pageInfo.totalCount ?? undefined,\n WAIVED: waivedCount.data?.pageInfo.totalCount ?? undefined,\n };\n\n const toggleEvidence = useCallback((id: string) => {\n setExpanded((prev) => {\n const next = new Set(prev);\n if (next.has(id)) next.delete(id);\n else next.add(id);\n return next;\n });\n }, []);\n\n // Severity ordering is applied ONLY within the current server page. The queue\n // pages by offset, so this does not globally reorder across pages — a page-2\n // CRITICAL still sorts below a page-1 LOW. Kept as a within-page readability\n // aid (the service has no severity sort arg to make it global).\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\n className=\"mb-6 flex flex-wrap items-center gap-2\"\n role=\"tablist\"\n aria-label={t('adaptercloud.violations.filterByStatus', 'Filter by status')}\n >\n {STATUS_TABS.map((tab) => {\n const active = status === tab;\n const count = tabCounts[tab];\n const label =\n tab === ''\n ? t('adaptercloud.violations.allStatuses', 'All')\n : violationStatusLabels[tab];\n return (\n <button\n key={tab || 'all'}\n type=\"button\"\n role=\"tab\"\n aria-selected={active}\n onClick={() => {\n setStatus(tab);\n setOffset(0);\n }}\n className={cn(\n 'inline-flex items-center gap-2 rounded-full border px-3.5 py-1.5 text-sm font-medium transition-colors',\n active\n ? 'border-transparent bg-action-primary-bg text-action-primary-text'\n : 'border-border-subtle text-text-secondary hover:bg-accent'\n )}\n >\n {label}\n {count !== undefined && (\n <span\n className={cn(\n 'inline-flex min-w-5 items-center justify-center rounded-full px-1.5 text-xs font-semibold tabular-nums',\n active\n ? 'bg-action-primary-text/20 text-action-primary-text'\n : 'bg-bg-sunken text-text-secondary'\n )}\n >\n {count}\n </span>\n )}\n </button>\n );\n })}\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\n role=\"alert\"\n aria-live=\"polite\"\n className=\"mb-4 rounded-lg bg-status-error-bg-subtle px-3 py-2 text-sm text-status-error-text\"\n >\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-accent-blue 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 {v.evidence != null && (\n <div className=\"mt-2\">\n <button\n type=\"button\"\n onClick={() => toggleEvidence(v.id)}\n aria-expanded={expanded.has(v.id)}\n className=\"inline-flex items-center gap-1 text-xs font-medium text-text-secondary transition-colors hover:text-text-primary\"\n >\n {expanded.has(v.id) ? (\n <ChevronDown className=\"h-3.5 w-3.5\" />\n ) : (\n <ChevronRight className=\"h-3.5 w-3.5\" />\n )}\n {t('adaptercloud.violations.evidence', 'Evidence')}\n </button>\n {expanded.has(v.id) && (\n <pre className=\"mt-2 overflow-x-auto rounded-lg border border-border-subtle bg-bg-sunken p-3 font-mono text-xs text-text-secondary\">\n {JSON.stringify(v.evidence, null, 2)}\n </pre>\n )}\n </div>\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 <label htmlFor=\"ada-waiver-reason\" className=\"mb-1.5 block 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 </label>\n <textarea\n id=\"ada-waiver-reason\"\n value={waiverReason}\n onChange={(e) => setWaiverReason(e.target.value)}\n rows={4}\n autoFocus\n aria-label={t('adaptercloud.violations.waiverReason', 'Waiver reason')}\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 && (\n <p role=\"alert\" aria-live=\"polite\" className=\"mt-2 text-sm text-status-error-text\">\n {actionError}\n </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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"}
|