@burdenoff/microfe-adaptercloud 2026.825.4 → 2026.827.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/ActiveSwitch.d.ts +17 -0
- package/dist/adaptercloud/components/ActiveSwitch.d.ts.map +1 -0
- package/dist/adaptercloud/components/ActiveSwitch.js +28 -0
- package/dist/adaptercloud/components/ActiveSwitch.js.map +1 -0
- package/dist/adaptercloud/components/index.d.ts +1 -0
- package/dist/adaptercloud/components/index.d.ts.map +1 -1
- package/dist/adaptercloud/hooks/useAdapterCloudApi.d.ts +28 -2
- package/dist/adaptercloud/hooks/useAdapterCloudApi.d.ts.map +1 -1
- package/dist/adaptercloud/hooks/useAdapterCloudApi.js +43 -18
- package/dist/adaptercloud/hooks/useAdapterCloudApi.js.map +1 -1
- package/dist/adaptercloud/pages/OverviewPage.d.ts.map +1 -1
- package/dist/adaptercloud/pages/OverviewPage.js +4 -4
- package/dist/adaptercloud/pages/OverviewPage.js.map +1 -1
- package/dist/adaptercloud/pages/adapters/AdaptersListPage.d.ts.map +1 -1
- package/dist/adaptercloud/pages/adapters/AdaptersListPage.js +6 -5
- package/dist/adaptercloud/pages/adapters/AdaptersListPage.js.map +1 -1
- package/dist/adaptercloud/pages/connections/ConnectionDetailPage.d.ts.map +1 -1
- package/dist/adaptercloud/pages/connections/ConnectionDetailPage.js +217 -104
- package/dist/adaptercloud/pages/connections/ConnectionDetailPage.js.map +1 -1
- package/dist/adaptercloud/pages/connections/ConnectionsListPage.d.ts.map +1 -1
- package/dist/adaptercloud/pages/connections/ConnectionsListPage.js +6 -5
- package/dist/adaptercloud/pages/connections/ConnectionsListPage.js.map +1 -1
- package/dist/adaptercloud/pages/costs/CostsPage.d.ts.map +1 -1
- package/dist/adaptercloud/pages/costs/CostsPage.js +183 -156
- package/dist/adaptercloud/pages/costs/CostsPage.js.map +1 -1
- package/dist/adaptercloud/pages/drift/DriftListPage.d.ts.map +1 -1
- package/dist/adaptercloud/pages/drift/DriftListPage.js +108 -107
- package/dist/adaptercloud/pages/drift/DriftListPage.js.map +1 -1
- package/dist/adaptercloud/pages/policies/PoliciesListPage.d.ts.map +1 -1
- package/dist/adaptercloud/pages/policies/PoliciesListPage.js +141 -131
- package/dist/adaptercloud/pages/policies/PoliciesListPage.js.map +1 -1
- package/dist/adaptercloud/pages/policies/PolicyDetailPage.d.ts.map +1 -1
- package/dist/adaptercloud/pages/policies/PolicyDetailPage.js +171 -117
- package/dist/adaptercloud/pages/policies/PolicyDetailPage.js.map +1 -1
- package/dist/adaptercloud/pages/resources/ResourcesListPage.d.ts.map +1 -1
- package/dist/adaptercloud/pages/resources/ResourcesListPage.js +85 -66
- package/dist/adaptercloud/pages/resources/ResourcesListPage.js.map +1 -1
- package/dist/adaptercloud/pages/sync-runs/SyncRunsListPage.d.ts.map +1 -1
- package/dist/adaptercloud/pages/sync-runs/SyncRunsListPage.js +148 -121
- package/dist/adaptercloud/pages/sync-runs/SyncRunsListPage.js.map +1 -1
- package/dist/adaptercloud/pages/violations/ViolationsListPage.d.ts.map +1 -1
- package/dist/adaptercloud/pages/violations/ViolationsListPage.js +145 -144
- package/dist/adaptercloud/pages/violations/ViolationsListPage.js.map +1 -1
- package/dist/utils/pageContext.d.ts +0 -2
- package/dist/utils/pageContext.d.ts.map +1 -1
- package/dist/utils/pageContext.js +1 -3
- package/dist/utils/pageContext.js.map +1 -1
- package/package.json +1 -1
|
@@ -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\">\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":";;;;;;;;;;;;;;;;AA2BA,SAAgB,IAAY;CAC1B,IAAM,EAAE,kBAAe,EAAgB,GACjC,EAAE,GAAG,MAAc,EAAQ,GAC3B,IAAI,GACP,GAAa,MAA6B;EACzC,IAAM,IAAQ,EAAU,CAAG;EAC3B,OAAO,MAAU,IAAM,IAAW;CACpC,GACA,CAAC,CAAS,CACZ,GAEM,CAAC,GAAc,KAAmB,EAAS,EAAE,GAC7C,CAAC,GAAgB,KAAqB,EAAS,EAAE,GAEjD,IAAU,EAAe,GACzB,IAAU,EAAe,EAAE,SAAS,KAAkB,KAAA,EAAU,CAAC,GAEjE,IAAW,EAAQ,MAAM,YAAY,OAErC,IAAc,QACZ,CAAC,GAAI,EAAQ,MAAM,SAAS,CAAC,CAAE,EAAE,MAAM,GAAG,MAAM,EAAE,QAAQ,EAAE,KAAK,GACvE,CAAC,EAAQ,IAAI,CACf,GAGM,IAAc,QAEhB,EAAY,KAAK,OAAU;EACzB,KAAK,EAAK;EACV,OAAO,EAAK;EACZ,OAAO,EAAK;EACZ,SAAS,EAAY,EAAK,OAAO,CAAQ;CAC3C,EAAE,GACJ,CAAC,GAAa,CAAQ,CACxB,GAGM,IAAQ,QAAc;EAC1B,IAAM,oBAAU,IAAI,IAAoB;EACxC,KAAK,IAAM,KAAK,EAAQ,QAAQ,CAAC,GAAG;GAClC,IAAM,IAAI,IAAI,KAAK,EAAE,WAAW;GAChC,IAAI,OAAO,MAAM,EAAE,QAAQ,CAAC,GAAG;GAC/B,IAAM,IAAM,GAAG,EAAE,eAAe,EAAE,GAAG,OAAO,EAAE,YAAY,IAAI,CAAC,EAAE,SAAS,GAAG,GAAG;GAChF,EAAQ,IAAI,IAAM,EAAQ,IAAI,CAAG,KAAK,KAAK,EAAE,MAAM;EACrD;EAEA,IAAM,IADS,CAAC,GAAG,EAAQ,QAAQ,CAAC,EAAE,MAAM,CAAC,IAAI,CAAC,OAAO,EAAE,cAAc,CAAC,CAC3D,EAAO,KAAK,GAAG,OAAO,CAAC,GAChC,IAAO,EAAO,EAAO,SAAS,MAAM,GACpC,IAAO,EAAO,EAAO,SAAS,MAAM;EAE1C,OAAO;GAAE;GAAQ,UADA,IAAO,IAAI,KAAK,OAAQ,IAAO,KAAQ,IAAQ,GAAG,IAAI;EAC7C;CAC5B,GAAG,CAAC,EAAQ,IAAI,CAAC,GAEX,IAAW,GACd,MAAuB;EAEtB,AADA,EAAE,eAAe,GACjB,EAAkB,EAAa,KAAK,CAAC;CACvC,GACA,CAAC,CAAY,CACf,GAEM,IAAe,QAAkB;EAErC,AADA,EAAgB,EAAE,GAClB,EAAkB,EAAE;CACtB,GAAG,CAAC,CAAC,GAEC,IAAwD;EAC5D;GACE,KAAK;GACL,QAAQ,EAAE,8BAA8B,SAAS;GACjD,UAAU;GACV,OAAO,MAAM,kBAAC,QAAD;IAAM,WAAU;cAAiC,EAAE;GAAc,CAAA;EAChF;EACA;GACE,KAAK;GACL,QAAQ,EAAE,6BAA6B,QAAQ;GAC/C,UAAU;GACV,OAAO,MACL,kBAAC,QAAD;IAAM,WAAU;cAAkC,EAAY,EAAE,QAAQ,EAAE,QAAQ;GAAQ,CAAA;EAE9F;EACA;GACE,KAAK;GACL,QAAQ,EAAE,6BAA6B,QAAQ;GAC/C,UAAU;GACV,OAAO,MACL,kBAAC,QAAD;IAAM,WAAU;cAAhB;KACG,EAAW,EAAE,aAAa;MAAE,MAAM,KAAA;MAAW,QAAQ,KAAA;KAAU,CAAC;KAAE;KAAG;KACrE,EAAW,EAAE,WAAW;MAAE,MAAM,KAAA;MAAW,QAAQ,KAAA;KAAU,CAAC;IAC3D;;EAEV;EACA;GACE,KAAK;GACL,QAAQ,EAAE,+BAA+B,UAAU;GACnD,UAAU;GACV,OAAO,MACL,EAAE,UAAU,OACV,kBAAC,UAAD;IACE,MAAK;IACL,eAAe,EAAE,cAAc,EAAW,cAAc,EAAE,YAAY;IACtE,WAAU;cAET,EAAE,SAAS;GACN,CAAA,IACN,EAAE,aACJ,kBAAC,QAAD;IAAM,WAAU;cAAhB,CACG,EAAE,WAAW,MAAM,GAAG,EAAE,GAAE,GACvB;QAEN,kBAAC,QAAD;IAAM,WAAU;cAAqB;GAAO,CAAA;EAElD;EACA;GACE,KAAK;GACL,QAAQ,EAAE,iCAAiC,YAAY;GACvD,UAAU;GACV,OAAO,MACL,EAAE,YAAY,OACZ,kBAAC,QAAD;IAAM,WAAU;cACb,EAAE,WAAW;GACV,CAAA,IAEN,kBAAC,QAAD;IAAM,WAAU;cAAqB;GAAO,CAAA;EAElD;CACF;CAEA,OACE,kBAAC,GAAD;EACE,OAAO,EAAE,4BAA4B,QAAQ;EAC7C,aAAa,EACX,+BACA,yFACF;YALF,CAOE,kBAAC,GAAD;GACE,WAAW,EAAQ;GACnB,SAAS,EAAQ;GACjB,OAAO,EAAQ;GACf,eAAe,KAAK,EAAQ,QAAQ;aAJtC,CAOE,kBAAC,GAAD;IAAe,WAAU;cAAzB,CACE,kBAAC,OAAD;KAAK,WAAU;eACb,kBAAC,MAAD;MAAI,WAAU;gBACX,EAAE,8BAA8B,cAAc;KAC7C,CAAA;IACD,CAAA,GACL,kBAAC,OAAD;KAAK,WAAU;eAAf,CACE,kBAAC,GAAD;MACE,UAAA;MACA,MAAM,kBAAC,GAAD,EAAY,WAAU,UAAW,CAAA;MACvC,OAAO,EAAQ,OAAO,EAAY,EAAQ,KAAK,OAAO,CAAQ,IAAI;MAClE,OAAO,EAAE,iCAAiC,aAAa;KACxD,CAAA,GACA,EAAY,MAAM,GAAG,CAAC,EAAE,KAAK,MAC5B,kBAAC,GAAD;MAEE,MAAM,kBAAC,GAAD,EAAS,WAAU,UAAW,CAAA;MACpC,OAAO,EAAY,EAAK,OAAO,CAAQ;MACvC,OAAO,EAAK;KACb,GAJM,EAAK,OAIX,CACF,CACE;MACQ;OAGd,EAAY,SAAS,KACpB,kBAAC,OAAD;IAAK,WAAU;cAAf,CACE,kBAAC,WAAD;KAAS,WAAU;eAAnB,CACE,kBAAC,MAAD;MAAI,WAAU;gBACX,EAAE,gCAAgC,kBAAkB;KACnD,CAAA,GACJ,kBAAC,GAAD;MAAW,WAAU;gBACnB,kBAAC,GAAD;OAAU,MAAM;OAAa,qBAAoB;MAAgB,CAAA;KACxD,CAAA,CACJ;QAET,kBAAC,WAAD,EAAA,UAAA,CACE,kBAAC,MAAD;KAAI,WAAU;eACX,EAAE,4BAA4B,eAAe;IAC5C,CAAA,GACJ,kBAAC,GAAD;KAAW,WAAU;eAClB,EAAM,OAAO,UAAU,IACtB,kBAAA,GAAA,EAAA,UAAA,CACE,kBAAC,OAAD;MAAK,WAAU;gBAAf,CACE,kBAAC,QAAD;OAAM,WAAU;iBACb,EAAY,EAAM,OAAO,EAAM,OAAO,SAAS,MAAM,GAAG,CAAQ;MAC7D,CAAA,GACL,EAAM,aAAa,QAClB,kBAAC,QAAD;OACE,WAAW,sCACT,EAAM,WAAW,IACb,2BACA;iBAJR;QAOG,EAAM,WAAW,IAAI,MAAM;QAAI;QAAE,KAAK,IAAI,EAAM,QAAQ;QAAE;QAAE;QAC5D,EAAE,6BAA6B,SAAS;OACrC;QAEL;SACL,kBAAC,GAAD;MACE,QAAQ,EAAM;MACd,WAAU;KACX,CAAA,CACD,EAAA,CAAA,IAEF,kBAAC,KAAD;MAAG,WAAU;gBACV,EACC,iCACA,oDACF;KACC,CAAA;IAEI,CAAA,CACJ,EAAA,CAAA,CACN;KAEE;MAGX,kBAAC,WAAD,EAAA,UAAA,CACE,kBAAC,OAAD;GAAK,WAAU;aAAf,CACE,kBAAC,MAAD;IAAI,WAAU;cACX,EAAE,8BAA8B,cAAc;GAC7C,CAAA,GACJ,kBAAC,QAAD;IACE,UAAU;IACV,MAAK;IACL,WAAU;cAHZ,CAKE,kBAAC,OAAD;KAAK,WAAU;eAAf,CACE,kBAAC,SAAD;MACE,MAAK;MACL,OAAO;MACP,WAAW,MAAM,EAAgB,EAAE,OAAO,KAAK;MAC/C,cAAY,EAAE,oCAAoC,mBAAmB;MACrE,aAAa,EAAE,+CAA+C,oBAAoB;MAClF,WAAU;KACX,CAAA,GACA,KACC,kBAAC,UAAD;MACE,MAAK;MACL,SAAS;MACT,cAAY,EAAE,yCAAyC,sBAAsB;MAC7E,WAAU;gBAEV,kBAAC,GAAD,EAAG,WAAU,UAAW,CAAA;KAClB,CAAA,CAEP;QACL,kBAAC,UAAD;KACE,MAAK;KACL,cAAY,EAAE,yCAAyC,sBAAsB;KAC7E,WAAU;eAEV,kBAAC,GAAD,EAAQ,WAAU,UAAW,CAAA;IACvB,CAAA,CACJ;KACH;MAEL,kBAAC,GAAD;GACE,WAAW,EAAQ;GACnB,SAAS,EAAQ;GACjB,UAAU,EAAQ,QAAQ,CAAC,GAAG,WAAW;GACzC,OAAO,EAAQ;GACf,eAAe,KAAK,EAAQ,QAAQ;GACpC,YAAY,EAAE,iCAAiC,iBAAiB;GAChE,kBAAkB,EAChB,uCACA,qEACF;GACA,WAAW,kBAAC,GAAD,EAAS,WAAU,YAAa,CAAA;GAC3C,aACE,kBAAC,UAAD;IACE,MAAK;IACL,eAAe,EAAW,cAAc;IACxC,WAAU;cAET,EAAE,sCAAsC,mBAAmB;GACtD,CAAA;aAGV,kBAAC,GAAD;IAAiB,MAAM,EAAQ,QAAQ,CAAC;IAAG,SAAS;IAAe,SAAS,MAAM,EAAE;GAAK,CAAA;EAChF,CAAA,CACJ,EAAA,CAAA,CACC;;AAEhB"}
|
|
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 { useConnections, useCostSummary, useCostRecords } from '../../hooks/useAdapterCloudApi';\nimport { formatMoney } from '../../utils/domain';\nimport { formatDate } from '../../utils/formatters';\nimport type { AdaCostRecord } from '../../types';\n\n/** How many connections the scope dropdown offers (one page of the inventory). */\nconst CONNECTION_OPTIONS_LIMIT = 100;\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 const [connectionId, setConnectionId] = useState('');\n\n // `adaCostSummary(connectionId)` and `adaListCostRecords(connectionId)` both\n // scope server-side, so one control narrows the total, the by-service\n // breakdown, the trend and the ledger consistently.\n const connectionsQuery = useConnections({ pagination: { limit: CONNECTION_OPTIONS_LIMIT } });\n const connectionOptions = connectionsQuery.data?.items ?? [];\n\n const summary = useCostSummary(connectionId || undefined);\n const records = useCostRecords({\n connectionId: connectionId || undefined,\n service: appliedService || undefined,\n });\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 {/* Estate scope — narrows every panel on this page to one connection. */}\n <div className=\"mb-6 flex flex-wrap items-center gap-3\">\n <label htmlFor=\"ada-cost-connection\" className=\"text-sm text-text-secondary\">\n {t('adaptercloud.costs.connection', 'Connection')}\n </label>\n <select\n id=\"ada-cost-connection\"\n value={connectionId}\n onChange={(e) => setConnectionId(e.target.value)}\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.costs.allConnections', 'All connections')}</option>\n {connectionOptions.map((c) => (\n <option key={c.id} value={c.id}>\n {c.name}\n </option>\n ))}\n </select>\n </div>\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":"DriftListPage.d.ts","sourceRoot":"","sources":["../../../../src/adaptercloud/pages/drift/DriftListPage.tsx"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAsCH,wBAAgB,aAAa,
|
|
1
|
+
{"version":3,"file":"DriftListPage.d.ts","sourceRoot":"","sources":["../../../../src/adaptercloud/pages/drift/DriftListPage.tsx"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAsCH,wBAAgB,aAAa,gCA2P5B"}
|
|
@@ -5,16 +5,16 @@ import { driftStatusLabels as r, severityRank as i } from "../../utils/domain.js
|
|
|
5
5
|
import { StatusBadge as a } from "../../components/StatusBadge.js";
|
|
6
6
|
import { SeverityBadge as o } from "../../components/SeverityBadge.js";
|
|
7
7
|
import { DataState as s } from "../../components/DataState.js";
|
|
8
|
-
import {
|
|
9
|
-
import { formatDate as
|
|
10
|
-
import { ConfirmDialog as
|
|
11
|
-
import { useCallback as
|
|
12
|
-
import { useI18n as
|
|
13
|
-
import { ArrowRight as
|
|
14
|
-
import { jsx as
|
|
15
|
-
import { GlassCard as
|
|
8
|
+
import { useDriftCount as c, useDriftRecords as l, useRemediateDrift as u } from "../../hooks/useAdapterCloudApi.js";
|
|
9
|
+
import { formatDate as d } from "../../utils/formatters.js";
|
|
10
|
+
import { ConfirmDialog as f } from "../../components/ConfirmDialog.js";
|
|
11
|
+
import { useCallback as p, useMemo as m, useState as h } from "react";
|
|
12
|
+
import { useI18n as g } from "@burdenoff/fe-libs/shared/providers/shell/I18nProvider";
|
|
13
|
+
import { ArrowRight as _, GitCompareArrows as v, Loader2 as y, Wrench as b } from "lucide-react";
|
|
14
|
+
import { jsx as x, jsxs as S } from "react/jsx-runtime";
|
|
15
|
+
import { GlassCard as C } from "@burdenoff/fe-libs/ui";
|
|
16
16
|
//#region src/adaptercloud/pages/drift/DriftListPage.tsx
|
|
17
|
-
var
|
|
17
|
+
var w = [
|
|
18
18
|
{
|
|
19
19
|
status: "DETECTED",
|
|
20
20
|
className: "bg-status-warning-bg-subtle text-status-warning-text"
|
|
@@ -32,202 +32,203 @@ var C = [
|
|
|
32
32
|
className: "bg-bg-sunken text-text-secondary"
|
|
33
33
|
}
|
|
34
34
|
];
|
|
35
|
-
function
|
|
35
|
+
function T(e) {
|
|
36
36
|
return e === null || [
|
|
37
37
|
"string",
|
|
38
38
|
"number",
|
|
39
39
|
"boolean"
|
|
40
40
|
].includes(typeof e);
|
|
41
41
|
}
|
|
42
|
-
function
|
|
42
|
+
function E(e) {
|
|
43
43
|
return e == null ? "—" : typeof e == "string" ? e : JSON.stringify(e);
|
|
44
44
|
}
|
|
45
|
-
function
|
|
46
|
-
let { navigateTo:
|
|
47
|
-
let n =
|
|
45
|
+
function D() {
|
|
46
|
+
let { navigateTo: D } = e(), { t: O } = g(), k = p((e, t) => {
|
|
47
|
+
let n = O(e);
|
|
48
48
|
return n === e ? t : n;
|
|
49
|
-
}, [
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
49
|
+
}, [O]), [A, j] = h("DETECTED"), [M, N] = h(null), [P, F] = h(null), I = l({ status: A || void 0 }), L = u(), R = c("DETECTED"), z = c("ACKNOWLEDGED"), B = c("REMEDIATED"), V = c("IGNORED"), H = m(() => ({
|
|
50
|
+
DETECTED: R.data ?? null,
|
|
51
|
+
ACKNOWLEDGED: z.data ?? null,
|
|
52
|
+
REMEDIATED: B.data ?? null,
|
|
53
|
+
IGNORED: V.data ?? null
|
|
54
|
+
}), [
|
|
55
|
+
R.data,
|
|
56
|
+
z.data,
|
|
57
|
+
B.data,
|
|
58
|
+
V.data
|
|
59
|
+
]), U = Object.values(H).some((e) => e != null && e > 0), W = [...I.data ?? []].sort((e, t) => i[t.severity] - i[e.severity]), G = L.isPending ? L.variables : void 0, K = p((e) => {
|
|
60
|
+
N(null), L.mutate(e, {
|
|
61
|
+
onSuccess: () => F(null),
|
|
62
|
+
onError: (e) => N(e instanceof Error ? e.message : "Failed to remediate drift.")
|
|
62
63
|
});
|
|
63
|
-
}, [
|
|
64
|
-
return /* @__PURE__ */
|
|
65
|
-
title:
|
|
66
|
-
description:
|
|
64
|
+
}, [L]);
|
|
65
|
+
return /* @__PURE__ */ S(n, {
|
|
66
|
+
title: k("adaptercloud.drift.title", "Drift"),
|
|
67
|
+
description: k("adaptercloud.drift.subtitle", "Divergences between observed and desired state, surfaced during reconciliation."),
|
|
67
68
|
children: [
|
|
68
|
-
|
|
69
|
+
U && /* @__PURE__ */ x("div", {
|
|
69
70
|
className: "mb-4 flex flex-wrap gap-2",
|
|
70
|
-
children:
|
|
71
|
-
let n =
|
|
72
|
-
return /* @__PURE__ */
|
|
71
|
+
children: w.map((e) => {
|
|
72
|
+
let n = A === e.status;
|
|
73
|
+
return /* @__PURE__ */ S("button", {
|
|
73
74
|
type: "button",
|
|
74
|
-
onClick: () =>
|
|
75
|
+
onClick: () => j(n ? "" : e.status),
|
|
75
76
|
"aria-pressed": n,
|
|
76
77
|
className: t("inline-flex items-center gap-2 rounded-full px-3 py-1.5 text-sm font-medium transition-shadow", e.className, n && "ring-2 ring-inset ring-[var(--color-focus-ring)]"),
|
|
77
|
-
children: [/* @__PURE__ */
|
|
78
|
+
children: [/* @__PURE__ */ x("span", {
|
|
78
79
|
className: "text-base font-semibold tabular-nums",
|
|
79
|
-
children:
|
|
80
|
-
}), /* @__PURE__ */
|
|
80
|
+
children: H[e.status] ?? "—"
|
|
81
|
+
}), /* @__PURE__ */ x("span", { children: r[e.status].toLowerCase() })]
|
|
81
82
|
}, e.status);
|
|
82
83
|
})
|
|
83
84
|
}),
|
|
84
|
-
/* @__PURE__ */
|
|
85
|
+
/* @__PURE__ */ x("div", {
|
|
85
86
|
className: "mb-6 flex flex-wrap gap-3",
|
|
86
|
-
children: /* @__PURE__ */
|
|
87
|
-
value:
|
|
88
|
-
onChange: (e) =>
|
|
89
|
-
"aria-label":
|
|
87
|
+
children: /* @__PURE__ */ S("select", {
|
|
88
|
+
value: A,
|
|
89
|
+
onChange: (e) => j(e.target.value),
|
|
90
|
+
"aria-label": k("adaptercloud.drift.filterByStatus", "Filter by status"),
|
|
90
91
|
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)]",
|
|
91
|
-
children: [/* @__PURE__ */
|
|
92
|
+
children: [/* @__PURE__ */ x("option", {
|
|
92
93
|
value: "",
|
|
93
|
-
children:
|
|
94
|
-
}), Object.keys(r).map((e) => /* @__PURE__ */
|
|
94
|
+
children: k("adaptercloud.drift.allStatuses", "All statuses")
|
|
95
|
+
}), Object.keys(r).map((e) => /* @__PURE__ */ x("option", {
|
|
95
96
|
value: e,
|
|
96
97
|
children: r[e]
|
|
97
98
|
}, e))]
|
|
98
99
|
})
|
|
99
100
|
}),
|
|
100
|
-
|
|
101
|
+
M && /* @__PURE__ */ x("p", {
|
|
101
102
|
role: "alert",
|
|
102
103
|
"aria-live": "polite",
|
|
103
104
|
className: "mb-4 rounded-lg bg-status-error-bg-subtle px-3 py-2 text-sm text-status-error-text",
|
|
104
|
-
children:
|
|
105
|
+
children: M
|
|
105
106
|
}),
|
|
106
|
-
/* @__PURE__ */
|
|
107
|
-
isLoading:
|
|
108
|
-
isError:
|
|
109
|
-
isEmpty:
|
|
110
|
-
error:
|
|
111
|
-
onRetry: () => void
|
|
112
|
-
emptyTitle:
|
|
113
|
-
emptyDescription:
|
|
114
|
-
emptyIcon: /* @__PURE__ */
|
|
115
|
-
children: /* @__PURE__ */
|
|
107
|
+
/* @__PURE__ */ x(s, {
|
|
108
|
+
isLoading: I.isLoading,
|
|
109
|
+
isError: I.isError,
|
|
110
|
+
isEmpty: W.length === 0,
|
|
111
|
+
error: I.error,
|
|
112
|
+
onRetry: () => void I.refetch(),
|
|
113
|
+
emptyTitle: k("adaptercloud.drift.emptyTitle", "No drift"),
|
|
114
|
+
emptyDescription: k("adaptercloud.drift.emptyDescription", "Nothing matches this filter — observed state matches desired state."),
|
|
115
|
+
emptyIcon: /* @__PURE__ */ x(v, { className: "h-10 w-10" }),
|
|
116
|
+
children: /* @__PURE__ */ x("div", {
|
|
116
117
|
className: "space-y-3",
|
|
117
|
-
children:
|
|
118
|
+
children: W.map((e) => {
|
|
118
119
|
let t = e.status === "DETECTED" || e.status === "ACKNOWLEDGED";
|
|
119
|
-
return /* @__PURE__ */
|
|
120
|
+
return /* @__PURE__ */ x(C, {
|
|
120
121
|
className: "p-5",
|
|
121
|
-
children: /* @__PURE__ */
|
|
122
|
+
children: /* @__PURE__ */ S("div", {
|
|
122
123
|
className: "flex flex-wrap items-start justify-between gap-3",
|
|
123
|
-
children: [/* @__PURE__ */
|
|
124
|
+
children: [/* @__PURE__ */ S("div", {
|
|
124
125
|
className: "min-w-0 flex-1",
|
|
125
126
|
children: [
|
|
126
|
-
/* @__PURE__ */
|
|
127
|
+
/* @__PURE__ */ S("div", {
|
|
127
128
|
className: "mb-1.5 flex flex-wrap items-center gap-2",
|
|
128
|
-
children: [/* @__PURE__ */
|
|
129
|
+
children: [/* @__PURE__ */ x(o, { severity: e.severity }), /* @__PURE__ */ x(a, {
|
|
129
130
|
status: e.status,
|
|
130
131
|
label: r[e.status]
|
|
131
132
|
})]
|
|
132
133
|
}),
|
|
133
|
-
/* @__PURE__ */
|
|
134
|
+
/* @__PURE__ */ x("p", {
|
|
134
135
|
className: "break-all font-mono text-sm font-medium text-text-primary",
|
|
135
136
|
children: e.attributePath
|
|
136
137
|
}),
|
|
137
|
-
|
|
138
|
+
T(e.expectedValue) && T(e.observedValue) ? /* @__PURE__ */ S("div", {
|
|
138
139
|
className: "mt-3 flex flex-wrap items-end gap-2",
|
|
139
140
|
children: [
|
|
140
|
-
/* @__PURE__ */
|
|
141
|
+
/* @__PURE__ */ S("div", {
|
|
141
142
|
className: "min-w-0",
|
|
142
|
-
children: [/* @__PURE__ */
|
|
143
|
+
children: [/* @__PURE__ */ x("p", {
|
|
143
144
|
className: "mb-1 text-xs font-semibold uppercase tracking-wide text-text-muted",
|
|
144
|
-
children:
|
|
145
|
-
}), /* @__PURE__ */
|
|
145
|
+
children: k("adaptercloud.drift.expected", "Expected")
|
|
146
|
+
}), /* @__PURE__ */ x("span", {
|
|
146
147
|
className: "inline-block max-w-full break-all rounded-md bg-status-error-bg-subtle px-2.5 py-1 font-mono text-xs text-status-error-text",
|
|
147
|
-
children:
|
|
148
|
+
children: E(e.expectedValue)
|
|
148
149
|
})]
|
|
149
150
|
}),
|
|
150
|
-
/* @__PURE__ */
|
|
151
|
-
/* @__PURE__ */
|
|
151
|
+
/* @__PURE__ */ x(_, { className: "mb-1.5 h-4 w-4 shrink-0 text-text-tertiary" }),
|
|
152
|
+
/* @__PURE__ */ S("div", {
|
|
152
153
|
className: "min-w-0",
|
|
153
|
-
children: [/* @__PURE__ */
|
|
154
|
+
children: [/* @__PURE__ */ x("p", {
|
|
154
155
|
className: "mb-1 text-xs font-semibold uppercase tracking-wide text-text-muted",
|
|
155
|
-
children:
|
|
156
|
-
}), /* @__PURE__ */
|
|
156
|
+
children: k("adaptercloud.drift.observed", "Observed")
|
|
157
|
+
}), /* @__PURE__ */ x("span", {
|
|
157
158
|
className: "inline-block max-w-full break-all rounded-md bg-status-success-bg-subtle px-2.5 py-1 font-mono text-xs text-status-success-text",
|
|
158
|
-
children:
|
|
159
|
+
children: E(e.observedValue)
|
|
159
160
|
})]
|
|
160
161
|
})
|
|
161
162
|
]
|
|
162
|
-
}) : /* @__PURE__ */
|
|
163
|
+
}) : /* @__PURE__ */ S("div", {
|
|
163
164
|
className: "mt-3 grid grid-cols-1 gap-3 sm:grid-cols-2",
|
|
164
|
-
children: [/* @__PURE__ */
|
|
165
|
+
children: [/* @__PURE__ */ S("div", {
|
|
165
166
|
className: "rounded-lg bg-bg-sunken p-3",
|
|
166
|
-
children: [/* @__PURE__ */
|
|
167
|
+
children: [/* @__PURE__ */ x("p", {
|
|
167
168
|
className: "mb-1 text-xs font-semibold text-text-secondary",
|
|
168
|
-
children:
|
|
169
|
-
}), /* @__PURE__ */
|
|
169
|
+
children: k("adaptercloud.drift.expected", "Expected")
|
|
170
|
+
}), /* @__PURE__ */ x("pre", {
|
|
170
171
|
className: "overflow-auto text-xs text-status-error-text",
|
|
171
172
|
children: JSON.stringify(e.expectedValue, null, 2)
|
|
172
173
|
})]
|
|
173
|
-
}), /* @__PURE__ */
|
|
174
|
+
}), /* @__PURE__ */ S("div", {
|
|
174
175
|
className: "rounded-lg bg-bg-sunken p-3",
|
|
175
|
-
children: [/* @__PURE__ */
|
|
176
|
+
children: [/* @__PURE__ */ x("p", {
|
|
176
177
|
className: "mb-1 text-xs font-semibold text-text-secondary",
|
|
177
|
-
children:
|
|
178
|
-
}), /* @__PURE__ */
|
|
178
|
+
children: k("adaptercloud.drift.observed", "Observed")
|
|
179
|
+
}), /* @__PURE__ */ x("pre", {
|
|
179
180
|
className: "overflow-auto text-xs text-status-success-text",
|
|
180
181
|
children: JSON.stringify(e.observedValue, null, 2)
|
|
181
182
|
})]
|
|
182
183
|
})]
|
|
183
184
|
}),
|
|
184
|
-
/* @__PURE__ */
|
|
185
|
+
/* @__PURE__ */ S("p", {
|
|
185
186
|
className: "mt-2 text-xs text-text-secondary",
|
|
186
187
|
children: [
|
|
187
|
-
|
|
188
|
+
k("adaptercloud.drift.detected", "Detected"),
|
|
188
189
|
" ",
|
|
189
|
-
|
|
190
|
+
d(e.detectedAt),
|
|
190
191
|
" · ",
|
|
191
|
-
/* @__PURE__ */
|
|
192
|
+
/* @__PURE__ */ x("button", {
|
|
192
193
|
type: "button",
|
|
193
|
-
onClick: () =>
|
|
194
|
+
onClick: () => D(`/resources/${e.resourceId}`),
|
|
194
195
|
className: "hover:underline",
|
|
195
|
-
children:
|
|
196
|
+
children: k("adaptercloud.drift.viewResource", "View resource")
|
|
196
197
|
})
|
|
197
198
|
]
|
|
198
199
|
})
|
|
199
200
|
]
|
|
200
|
-
}), t && /* @__PURE__ */
|
|
201
|
+
}), t && /* @__PURE__ */ S("button", {
|
|
201
202
|
type: "button",
|
|
202
|
-
disabled:
|
|
203
|
+
disabled: G === e.id,
|
|
203
204
|
onClick: () => {
|
|
204
|
-
|
|
205
|
+
N(null), F(e);
|
|
205
206
|
},
|
|
206
207
|
className: "inline-flex shrink-0 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",
|
|
207
|
-
children: [
|
|
208
|
+
children: [G === e.id ? /* @__PURE__ */ x(y, { className: "h-3.5 w-3.5 animate-spin" }) : /* @__PURE__ */ x(b, { className: "h-3.5 w-3.5" }), k("adaptercloud.drift.remediate", "Remediate")]
|
|
208
209
|
})]
|
|
209
210
|
})
|
|
210
211
|
}, e.id);
|
|
211
212
|
})
|
|
212
213
|
})
|
|
213
214
|
}),
|
|
214
|
-
/* @__PURE__ */
|
|
215
|
-
open: !!
|
|
215
|
+
/* @__PURE__ */ x(f, {
|
|
216
|
+
open: !!P,
|
|
216
217
|
onOpenChange: (e) => {
|
|
217
|
-
e ||
|
|
218
|
+
e || F(null);
|
|
218
219
|
},
|
|
219
|
-
title:
|
|
220
|
-
description:
|
|
221
|
-
confirmLabel:
|
|
222
|
-
cancelLabel:
|
|
223
|
-
onConfirm: () =>
|
|
224
|
-
isPending: !!
|
|
220
|
+
title: k("adaptercloud.drift.remediateTitle", "Mark this drift remediated?"),
|
|
221
|
+
description: k("adaptercloud.drift.remediatePrompt", "This records the divergence as remediated. Confirm only if the observed state now matches the desired state."),
|
|
222
|
+
confirmLabel: k("adaptercloud.drift.remediate", "Remediate"),
|
|
223
|
+
cancelLabel: k("adaptercloud.common.cancel", "Cancel"),
|
|
224
|
+
onConfirm: () => P && K(P.id),
|
|
225
|
+
isPending: !!P && G === P?.id,
|
|
225
226
|
tone: "primary"
|
|
226
227
|
})
|
|
227
228
|
]
|
|
228
229
|
});
|
|
229
230
|
}
|
|
230
231
|
//#endregion
|
|
231
|
-
export {
|
|
232
|
+
export { D as DriftListPage };
|
|
232
233
|
|
|
233
234
|
//# sourceMappingURL=DriftListPage.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"DriftListPage.js","names":[],"sources":["../../../../src/adaptercloud/pages/drift/DriftListPage.tsx"],"sourcesContent":["/**\n * Drift Board Page\n * @module adaptercloud/pages/drift\n *\n * Divergences between observed and desired state (`adaListDriftRecords`).\n * Filters by status; each detected/acknowledged record can be remediated via\n * `adaRemediateDrift`.\n */\n\nimport { useState, useCallback, useMemo } from 'react';\nimport { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';\nimport { GitCompareArrows, Wrench, Loader2, ArrowRight } 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 { useDriftRecords, useRemediateDrift } from '../../hooks/useAdapterCloudApi';\nimport { driftStatusLabels, severityRank } from '../../utils/domain';\nimport { formatDate } from '../../utils/formatters';\nimport { cn } from '../../utils/cn';\nimport type { AdaDriftRecord, AdaDriftStatus, Json } from '../../types';\n\n/** Summary-chip config — order + tokenized colors for each drift status. */\nconst SUMMARY_CHIPS: { status: AdaDriftStatus; className: string }[] = [\n { status: 'DETECTED', className: 'bg-status-warning-bg-subtle text-status-warning-text' },\n { status: 'ACKNOWLEDGED', className: 'bg-accent-blue-subtle text-accent-blue' },\n { status: 'REMEDIATED', className: 'bg-status-success-bg-subtle text-status-success-text' },\n { status: 'IGNORED', className: 'bg-bg-sunken text-text-secondary' },\n];\n\n/** A drift value is renderable as a compact diff pill only when it is scalar. */\nfunction isScalar(value: Json): boolean {\n return value === null || ['string', 'number', 'boolean'].includes(typeof value);\n}\n\n/** Human-readable one-line rendering of a scalar drift value. */\nfunction scalarText(value: Json): string {\n if (value === null || value === undefined) return '—';\n if (typeof value === 'string') return value;\n return JSON.stringify(value);\n}\n\nexport function DriftListPage() {\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 [status, setStatus] = useState<AdaDriftStatus | ''>('DETECTED');\n const [actionError, setActionError] = useState<string | null>(null);\n const [confirmTarget, setConfirmTarget] = useState<AdaDriftRecord | null>(null);\n\n const query = useDriftRecords({ status: status || undefined });\n const remediate = useRemediateDrift();\n\n // Estate-wide, all-status fetch feeding the summary strip. When the status\n // filter is \"All\" this shares a query key with the main list (React Query\n // dedupes to a single request); otherwise it is one extra cheap fetch.\n const allDrift = useDriftRecords({});\n const summaryCounts = useMemo(() => {\n const counts: Record<AdaDriftStatus, number> = {\n DETECTED: 0,\n ACKNOWLEDGED: 0,\n REMEDIATED: 0,\n IGNORED: 0,\n };\n for (const record of allDrift.data ?? []) counts[record.status] += 1;\n return counts;\n }, [allDrift.data]);\n const hasSummary = (allDrift.data?.length ?? 0) > 0;\n\n // Note: this severity sort only reorders the current server result set.\n // adaListDriftRecords returns a single capped array (no pagination), so the\n // ordering is global across what is loaded, not a partial-page reshuffle.\n const items = [...(query.data ?? [])].sort(\n (a, b) => severityRank[b.severity] - severityRank[a.severity]\n );\n\n // Per-id pending: React Query exposes the in-flight mutation argument via\n // `variables`, so only the acted row spins/disables — not the whole list.\n const pendingId = remediate.isPending ? remediate.variables : undefined;\n\n const handleRemediate = useCallback(\n (id: string) => {\n setActionError(null);\n remediate.mutate(id, {\n onSuccess: () => setConfirmTarget(null),\n onError: (err) =>\n setActionError(err instanceof Error ? err.message : 'Failed to remediate drift.'),\n });\n },\n [remediate]\n );\n\n return (\n <PageLayout\n title={t('adaptercloud.drift.title', 'Drift')}\n description={t(\n 'adaptercloud.drift.subtitle',\n 'Divergences between observed and desired state, surfaced during reconciliation.'\n )}\n >\n {hasSummary && (\n <div className=\"mb-4 flex flex-wrap gap-2\">\n {SUMMARY_CHIPS.map((chip) => {\n const active = status === chip.status;\n return (\n <button\n key={chip.status}\n type=\"button\"\n onClick={() => setStatus(active ? '' : chip.status)}\n aria-pressed={active}\n className={cn(\n 'inline-flex items-center gap-2 rounded-full px-3 py-1.5 text-sm font-medium transition-shadow',\n chip.className,\n active && 'ring-2 ring-inset ring-[var(--color-focus-ring)]'\n )}\n >\n <span className=\"text-base font-semibold tabular-nums\">\n {summaryCounts[chip.status]}\n </span>\n <span>{driftStatusLabels[chip.status].toLowerCase()}</span>\n </button>\n );\n })}\n </div>\n )}\n\n <div className=\"mb-6 flex flex-wrap gap-3\">\n <select\n value={status}\n onChange={(e) => setStatus(e.target.value as AdaDriftStatus | '')}\n aria-label={t('adaptercloud.drift.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.drift.allStatuses', 'All statuses')}</option>\n {(Object.keys(driftStatusLabels) as AdaDriftStatus[]).map((s) => (\n <option key={s} value={s}>\n {driftStatusLabels[s]}\n </option>\n ))}\n </select>\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.drift.emptyTitle', 'No drift')}\n emptyDescription={t(\n 'adaptercloud.drift.emptyDescription',\n 'Nothing matches this filter — observed state matches desired state.'\n )}\n emptyIcon={<GitCompareArrows className=\"h-10 w-10\" />}\n >\n <div className=\"space-y-3\">\n {items.map((d) => {\n const canRemediate = d.status === 'DETECTED' || d.status === 'ACKNOWLEDGED';\n return (\n <GlassCard key={d.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={d.severity} />\n <StatusBadge status={d.status} label={driftStatusLabels[d.status]} />\n </div>\n <p className=\"break-all font-mono text-sm font-medium text-text-primary\">\n {d.attributePath}\n </p>\n {isScalar(d.expectedValue) && isScalar(d.observedValue) ? (\n // Compact before → after diff for scalar drifts (the common\n // case, e.g. t3.medium → t3.large): expected in red, observed\n // in green, mirroring the mock's inline diff pills.\n <div className=\"mt-3 flex flex-wrap items-end gap-2\">\n <div className=\"min-w-0\">\n <p className=\"mb-1 text-xs font-semibold uppercase tracking-wide text-text-muted\">\n {t('adaptercloud.drift.expected', 'Expected')}\n </p>\n <span className=\"inline-block max-w-full break-all rounded-md bg-status-error-bg-subtle px-2.5 py-1 font-mono text-xs text-status-error-text\">\n {scalarText(d.expectedValue)}\n </span>\n </div>\n <ArrowRight className=\"mb-1.5 h-4 w-4 shrink-0 text-text-tertiary\" />\n <div className=\"min-w-0\">\n <p className=\"mb-1 text-xs font-semibold uppercase tracking-wide text-text-muted\">\n {t('adaptercloud.drift.observed', 'Observed')}\n </p>\n <span className=\"inline-block max-w-full break-all rounded-md bg-status-success-bg-subtle px-2.5 py-1 font-mono text-xs text-status-success-text\">\n {scalarText(d.observedValue)}\n </span>\n </div>\n </div>\n ) : (\n // Structured (object/array) drift — keep the full JSON blocks.\n <div className=\"mt-3 grid grid-cols-1 gap-3 sm:grid-cols-2\">\n <div className=\"rounded-lg bg-bg-sunken p-3\">\n <p className=\"mb-1 text-xs font-semibold text-text-secondary\">\n {t('adaptercloud.drift.expected', 'Expected')}\n </p>\n <pre className=\"overflow-auto text-xs text-status-error-text\">\n {JSON.stringify(d.expectedValue, null, 2)}\n </pre>\n </div>\n <div className=\"rounded-lg bg-bg-sunken p-3\">\n <p className=\"mb-1 text-xs font-semibold text-text-secondary\">\n {t('adaptercloud.drift.observed', 'Observed')}\n </p>\n <pre className=\"overflow-auto text-xs text-status-success-text\">\n {JSON.stringify(d.observedValue, null, 2)}\n </pre>\n </div>\n </div>\n )}\n <p className=\"mt-2 text-xs text-text-secondary\">\n {t('adaptercloud.drift.detected', 'Detected')} {formatDate(d.detectedAt)}\n {' · '}\n <button\n type=\"button\"\n onClick={() => navigateTo(`/resources/${d.resourceId}`)}\n className=\"hover:underline\"\n >\n {t('adaptercloud.drift.viewResource', 'View resource')}\n </button>\n </p>\n </div>\n\n {canRemediate && (\n <button\n type=\"button\"\n disabled={pendingId === d.id}\n onClick={() => {\n setActionError(null);\n setConfirmTarget(d);\n }}\n className=\"inline-flex shrink-0 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 {pendingId === d.id ? (\n <Loader2 className=\"h-3.5 w-3.5 animate-spin\" />\n ) : (\n <Wrench className=\"h-3.5 w-3.5\" />\n )}\n {t('adaptercloud.drift.remediate', 'Remediate')}\n </button>\n )}\n </div>\n </GlassCard>\n );\n })}\n </div>\n </DataState>\n\n <ConfirmDialog\n open={Boolean(confirmTarget)}\n onOpenChange={(open) => {\n if (!open) setConfirmTarget(null);\n }}\n title={t('adaptercloud.drift.remediateTitle', 'Mark this drift remediated?')}\n description={t(\n 'adaptercloud.drift.remediatePrompt',\n 'This records the divergence as remediated. Confirm only if the observed state now matches the desired state.'\n )}\n confirmLabel={t('adaptercloud.drift.remediate', 'Remediate')}\n cancelLabel={t('adaptercloud.common.cancel', 'Cancel')}\n onConfirm={() => confirmTarget && handleRemediate(confirmTarget.id)}\n isPending={Boolean(confirmTarget) && pendingId === confirmTarget?.id}\n tone=\"primary\"\n />\n </PageLayout>\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"DriftListPage.js","names":[],"sources":["../../../../src/adaptercloud/pages/drift/DriftListPage.tsx"],"sourcesContent":["/**\n * Drift Board Page\n * @module adaptercloud/pages/drift\n *\n * Divergences between observed and desired state (`adaListDriftRecords`).\n * Filters by status; each detected/acknowledged record can be remediated via\n * `adaRemediateDrift`.\n */\n\nimport { useState, useCallback, useMemo } from 'react';\nimport { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';\nimport { GitCompareArrows, Wrench, Loader2, ArrowRight } 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 { useDriftCount, useDriftRecords, useRemediateDrift } from '../../hooks/useAdapterCloudApi';\nimport { driftStatusLabels, severityRank } from '../../utils/domain';\nimport { formatDate } from '../../utils/formatters';\nimport { cn } from '../../utils/cn';\nimport type { AdaDriftRecord, AdaDriftStatus, Json } from '../../types';\n\n/** Summary-chip config — order + tokenized colors for each drift status. */\nconst SUMMARY_CHIPS: { status: AdaDriftStatus; className: string }[] = [\n { status: 'DETECTED', className: 'bg-status-warning-bg-subtle text-status-warning-text' },\n { status: 'ACKNOWLEDGED', className: 'bg-accent-blue-subtle text-accent-blue' },\n { status: 'REMEDIATED', className: 'bg-status-success-bg-subtle text-status-success-text' },\n { status: 'IGNORED', className: 'bg-bg-sunken text-text-secondary' },\n];\n\n/** A drift value is renderable as a compact diff pill only when it is scalar. */\nfunction isScalar(value: Json): boolean {\n return value === null || ['string', 'number', 'boolean'].includes(typeof value);\n}\n\n/** Human-readable one-line rendering of a scalar drift value. */\nfunction scalarText(value: Json): string {\n if (value === null || value === undefined) return '—';\n if (typeof value === 'string') return value;\n return JSON.stringify(value);\n}\n\nexport function DriftListPage() {\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 [status, setStatus] = useState<AdaDriftStatus | ''>('DETECTED');\n const [actionError, setActionError] = useState<string | null>(null);\n const [confirmTarget, setConfirmTarget] = useState<AdaDriftRecord | null>(null);\n\n const query = useDriftRecords({ status: status || undefined });\n const remediate = useRemediateDrift();\n\n // Summary strip. Each chip reads the estate-wide `pageInfo.totalCount` for\n // its status via a `limit: 1` query, so the numbers stay exact past the\n // service's 100-row page ceiling — counting the loaded list would plateau\n // there and quietly understate a large estate.\n const detectedCount = useDriftCount('DETECTED');\n const acknowledgedCount = useDriftCount('ACKNOWLEDGED');\n const remediatedCount = useDriftCount('REMEDIATED');\n const ignoredCount = useDriftCount('IGNORED');\n\n const summaryCounts = useMemo<Record<AdaDriftStatus, number | null>>(\n () => ({\n DETECTED: detectedCount.data ?? null,\n ACKNOWLEDGED: acknowledgedCount.data ?? null,\n REMEDIATED: remediatedCount.data ?? null,\n IGNORED: ignoredCount.data ?? null,\n }),\n [detectedCount.data, acknowledgedCount.data, remediatedCount.data, ignoredCount.data]\n );\n\n // Show the strip only once at least one exact count has arrived and the\n // estate actually has drift — never render a row of unknown dashes.\n const hasSummary = Object.values(summaryCounts).some((count) => count != null && count > 0);\n\n // Note: this severity sort only reorders the current server result set.\n // adaListDriftRecords returns a single capped array (no pagination), so the\n // ordering is global across what is loaded, not a partial-page reshuffle.\n const items = [...(query.data ?? [])].sort(\n (a, b) => severityRank[b.severity] - severityRank[a.severity]\n );\n\n // Per-id pending: React Query exposes the in-flight mutation argument via\n // `variables`, so only the acted row spins/disables — not the whole list.\n const pendingId = remediate.isPending ? remediate.variables : undefined;\n\n const handleRemediate = useCallback(\n (id: string) => {\n setActionError(null);\n remediate.mutate(id, {\n onSuccess: () => setConfirmTarget(null),\n onError: (err) =>\n setActionError(err instanceof Error ? err.message : 'Failed to remediate drift.'),\n });\n },\n [remediate]\n );\n\n return (\n <PageLayout\n title={t('adaptercloud.drift.title', 'Drift')}\n description={t(\n 'adaptercloud.drift.subtitle',\n 'Divergences between observed and desired state, surfaced during reconciliation.'\n )}\n >\n {hasSummary && (\n <div className=\"mb-4 flex flex-wrap gap-2\">\n {SUMMARY_CHIPS.map((chip) => {\n const active = status === chip.status;\n return (\n <button\n key={chip.status}\n type=\"button\"\n onClick={() => setStatus(active ? '' : chip.status)}\n aria-pressed={active}\n className={cn(\n 'inline-flex items-center gap-2 rounded-full px-3 py-1.5 text-sm font-medium transition-shadow',\n chip.className,\n active && 'ring-2 ring-inset ring-[var(--color-focus-ring)]'\n )}\n >\n <span className=\"text-base font-semibold tabular-nums\">\n {summaryCounts[chip.status] ?? '—'}\n </span>\n <span>{driftStatusLabels[chip.status].toLowerCase()}</span>\n </button>\n );\n })}\n </div>\n )}\n\n <div className=\"mb-6 flex flex-wrap gap-3\">\n <select\n value={status}\n onChange={(e) => setStatus(e.target.value as AdaDriftStatus | '')}\n aria-label={t('adaptercloud.drift.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.drift.allStatuses', 'All statuses')}</option>\n {(Object.keys(driftStatusLabels) as AdaDriftStatus[]).map((s) => (\n <option key={s} value={s}>\n {driftStatusLabels[s]}\n </option>\n ))}\n </select>\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.drift.emptyTitle', 'No drift')}\n emptyDescription={t(\n 'adaptercloud.drift.emptyDescription',\n 'Nothing matches this filter — observed state matches desired state.'\n )}\n emptyIcon={<GitCompareArrows className=\"h-10 w-10\" />}\n >\n <div className=\"space-y-3\">\n {items.map((d) => {\n const canRemediate = d.status === 'DETECTED' || d.status === 'ACKNOWLEDGED';\n return (\n <GlassCard key={d.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={d.severity} />\n <StatusBadge status={d.status} label={driftStatusLabels[d.status]} />\n </div>\n <p className=\"break-all font-mono text-sm font-medium text-text-primary\">\n {d.attributePath}\n </p>\n {isScalar(d.expectedValue) && isScalar(d.observedValue) ? (\n // Compact before → after diff for scalar drifts (the common\n // case, e.g. t3.medium → t3.large): expected in red, observed\n // in green, mirroring the mock's inline diff pills.\n <div className=\"mt-3 flex flex-wrap items-end gap-2\">\n <div className=\"min-w-0\">\n <p className=\"mb-1 text-xs font-semibold uppercase tracking-wide text-text-muted\">\n {t('adaptercloud.drift.expected', 'Expected')}\n </p>\n <span className=\"inline-block max-w-full break-all rounded-md bg-status-error-bg-subtle px-2.5 py-1 font-mono text-xs text-status-error-text\">\n {scalarText(d.expectedValue)}\n </span>\n </div>\n <ArrowRight className=\"mb-1.5 h-4 w-4 shrink-0 text-text-tertiary\" />\n <div className=\"min-w-0\">\n <p className=\"mb-1 text-xs font-semibold uppercase tracking-wide text-text-muted\">\n {t('adaptercloud.drift.observed', 'Observed')}\n </p>\n <span className=\"inline-block max-w-full break-all rounded-md bg-status-success-bg-subtle px-2.5 py-1 font-mono text-xs text-status-success-text\">\n {scalarText(d.observedValue)}\n </span>\n </div>\n </div>\n ) : (\n // Structured (object/array) drift — keep the full JSON blocks.\n <div className=\"mt-3 grid grid-cols-1 gap-3 sm:grid-cols-2\">\n <div className=\"rounded-lg bg-bg-sunken p-3\">\n <p className=\"mb-1 text-xs font-semibold text-text-secondary\">\n {t('adaptercloud.drift.expected', 'Expected')}\n </p>\n <pre className=\"overflow-auto text-xs text-status-error-text\">\n {JSON.stringify(d.expectedValue, null, 2)}\n </pre>\n </div>\n <div className=\"rounded-lg bg-bg-sunken p-3\">\n <p className=\"mb-1 text-xs font-semibold text-text-secondary\">\n {t('adaptercloud.drift.observed', 'Observed')}\n </p>\n <pre className=\"overflow-auto text-xs text-status-success-text\">\n {JSON.stringify(d.observedValue, null, 2)}\n </pre>\n </div>\n </div>\n )}\n <p className=\"mt-2 text-xs text-text-secondary\">\n {t('adaptercloud.drift.detected', 'Detected')} {formatDate(d.detectedAt)}\n {' · '}\n <button\n type=\"button\"\n onClick={() => navigateTo(`/resources/${d.resourceId}`)}\n className=\"hover:underline\"\n >\n {t('adaptercloud.drift.viewResource', 'View resource')}\n </button>\n </p>\n </div>\n\n {canRemediate && (\n <button\n type=\"button\"\n disabled={pendingId === d.id}\n onClick={() => {\n setActionError(null);\n setConfirmTarget(d);\n }}\n className=\"inline-flex shrink-0 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 {pendingId === d.id ? (\n <Loader2 className=\"h-3.5 w-3.5 animate-spin\" />\n ) : (\n <Wrench className=\"h-3.5 w-3.5\" />\n )}\n {t('adaptercloud.drift.remediate', 'Remediate')}\n </button>\n )}\n </div>\n </GlassCard>\n );\n })}\n </div>\n </DataState>\n\n <ConfirmDialog\n open={Boolean(confirmTarget)}\n onOpenChange={(open) => {\n if (!open) setConfirmTarget(null);\n }}\n title={t('adaptercloud.drift.remediateTitle', 'Mark this drift remediated?')}\n description={t(\n 'adaptercloud.drift.remediatePrompt',\n 'This records the divergence as remediated. Confirm only if the observed state now matches the desired state.'\n )}\n confirmLabel={t('adaptercloud.drift.remediate', 'Remediate')}\n cancelLabel={t('adaptercloud.common.cancel', 'Cancel')}\n onConfirm={() => confirmTarget && handleRemediate(confirmTarget.id)}\n isPending={Boolean(confirmTarget) && pendingId === confirmTarget?.id}\n tone=\"primary\"\n />\n </PageLayout>\n );\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"PoliciesListPage.d.ts","sourceRoot":"","sources":["../../../../src/adaptercloud/pages/policies/PoliciesListPage.tsx"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;
|
|
1
|
+
{"version":3,"file":"PoliciesListPage.d.ts","sourceRoot":"","sources":["../../../../src/adaptercloud/pages/policies/PoliciesListPage.tsx"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAsGH,wBAAgB,gBAAgB,gCAqP/B"}
|