@burdenoff/microfe-adaptercloud 2026.706.1 → 2026.706.3
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/AdapterCloudRoot.js +52 -44
- package/dist/adaptercloud/AdapterCloudRoot.js.map +1 -1
- package/dist/adaptercloud/AdapterCloudRoutes.js +22 -14
- package/dist/adaptercloud/AdapterCloudRoutes.js.map +1 -1
- package/dist/adaptercloud/components/ConfirmDialog.js +27 -0
- package/dist/adaptercloud/components/ConfirmDialog.js.map +1 -0
- package/dist/adaptercloud/components/DataState.js +1 -1
- package/dist/adaptercloud/components/DataState.js.map +1 -1
- package/dist/adaptercloud/components/viz/BarChart.js +32 -0
- package/dist/adaptercloud/components/viz/BarChart.js.map +1 -0
- package/dist/adaptercloud/components/viz/Sparkline.js +50 -0
- package/dist/adaptercloud/components/viz/Sparkline.js.map +1 -0
- package/dist/adaptercloud/components/viz/StackedBar.js +1 -0
- package/dist/adaptercloud/components/viz/TopologyGraph.js +96 -0
- package/dist/adaptercloud/components/viz/TopologyGraph.js.map +1 -0
- package/dist/adaptercloud/components/viz/index.js +4 -0
- package/dist/adaptercloud/hooks/useAdapterCloudApi.js +71 -34
- package/dist/adaptercloud/hooks/useAdapterCloudApi.js.map +1 -1
- package/dist/adaptercloud/pages/OverviewPage.js +159 -156
- package/dist/adaptercloud/pages/OverviewPage.js.map +1 -1
- package/dist/adaptercloud/pages/adapters/AdapterDetailPage.js +179 -113
- package/dist/adaptercloud/pages/adapters/AdapterDetailPage.js.map +1 -1
- package/dist/adaptercloud/pages/adapters/AdapterFormPage.js +263 -179
- package/dist/adaptercloud/pages/adapters/AdapterFormPage.js.map +1 -1
- package/dist/adaptercloud/pages/adapters/AdaptersListPage.js +2 -0
- package/dist/adaptercloud/pages/adapters/AdaptersListPage.js.map +1 -1
- package/dist/adaptercloud/pages/connections/ConnectionDetailPage.js +141 -124
- package/dist/adaptercloud/pages/connections/ConnectionDetailPage.js.map +1 -1
- package/dist/adaptercloud/pages/connections/ConnectionFormPage.js +2 -0
- package/dist/adaptercloud/pages/connections/ConnectionFormPage.js.map +1 -1
- package/dist/adaptercloud/pages/connections/ConnectionsListPage.js +158 -93
- package/dist/adaptercloud/pages/connections/ConnectionsListPage.js.map +1 -1
- package/dist/adaptercloud/pages/costs/CostsPage.js +183 -109
- package/dist/adaptercloud/pages/costs/CostsPage.js.map +1 -1
- package/dist/adaptercloud/pages/drift/DriftListPage.js +172 -71
- package/dist/adaptercloud/pages/drift/DriftListPage.js.map +1 -1
- package/dist/adaptercloud/pages/policies/PoliciesListPage.js +179 -96
- package/dist/adaptercloud/pages/policies/PoliciesListPage.js.map +1 -1
- package/dist/adaptercloud/pages/policies/PolicyDetailPage.js +109 -91
- package/dist/adaptercloud/pages/policies/PolicyDetailPage.js.map +1 -1
- package/dist/adaptercloud/pages/policies/PolicyFormPage.js +211 -184
- package/dist/adaptercloud/pages/policies/PolicyFormPage.js.map +1 -1
- package/dist/adaptercloud/pages/resources/ResourceDetailPage.js +183 -127
- package/dist/adaptercloud/pages/resources/ResourceDetailPage.js.map +1 -1
- package/dist/adaptercloud/pages/resources/ResourcesListPage.js +121 -84
- package/dist/adaptercloud/pages/resources/ResourcesListPage.js.map +1 -1
- package/dist/adaptercloud/pages/sync-runs/SyncRunsListPage.js +236 -153
- package/dist/adaptercloud/pages/sync-runs/SyncRunsListPage.js.map +1 -1
- package/dist/adaptercloud/pages/topology/TopologyPage.js +397 -0
- package/dist/adaptercloud/pages/topology/TopologyPage.js.map +1 -0
- package/dist/adaptercloud/pages/violations/ViolationsListPage.js +192 -131
- package/dist/adaptercloud/pages/violations/ViolationsListPage.js.map +1 -1
- package/package.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ConnectionsListPage.js","names":[],"sources":["../../../../src/adaptercloud/pages/connections/ConnectionsListPage.tsx"],"sourcesContent":["/**\n * Connections List Page\n * @module adaptercloud/pages/connections\n *\n * Every configured, authenticated target in the workspace. Lists from\n * `adaListConnections` with a status filter and offset pagination.\n */\n\nimport { useState, useCallback } from 'react';\nimport { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';\nimport { Cable, Plus, ChevronLeft, ChevronRight, MapPin, Server } from 'lucide-react';\nimport { GlassCard } from '@burdenoff/fe-libs/ui';\nimport { PageLayout } from '../../components/PageLayout';\nimport { StatusBadge } from '../../components/StatusBadge';\nimport { DataState } from '../../components/DataState';\nimport { useAdapterCloud } from '../../context/AdapterCloudContext';\nimport { useConnections } from '../../hooks/useAdapterCloudApi';\nimport { authMethodLabels, connectionStatusLabels } from '../../utils/domain';\nimport { formatRelativeTime } from '../../utils/formatters';\nimport type { AdaConnectionStatus } from '../../types';\n\nconst PAGE_SIZE = 20;\n\nexport function ConnectionsListPage() {\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<AdaConnectionStatus | ''>('');\n const [offset, setOffset] = useState(0);\n\n const query = useConnections({\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 return (\n <PageLayout\n title={t('adaptercloud.connections.title', 'Connections')}\n description={t(\n 'adaptercloud.connections.subtitle',\n 'Configured, authenticated targets — each connection is where the fabric actually reaches.'\n )}\n actions={\n <button\n type=\"button\"\n onClick={() => navigateTo('/connections/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.connections.new', 'New connection')}\n </button>\n }\n >\n <div className=\"mb-6 flex flex-wrap gap-3\">\n <select\n value={status}\n onChange={(e) => {\n setStatus(e.target.value as AdaConnectionStatus | '');\n setOffset(0);\n }}\n className=\"rounded-lg border border-border-subtle bg-bg-surface px-3 py-2 text-sm text-text-primary focus:outline-none focus:ring-2 focus:ring-[var(--color-focus-ring)]\"\n >\n <option value=\"\">{t('adaptercloud.connections.allStatuses', 'All statuses')}</option>\n {(Object.keys(connectionStatusLabels) as AdaConnectionStatus[]).map((s) => (\n <option key={s} value={s}>\n {connectionStatusLabels[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.connections.emptyTitle', 'No connections yet')}\n emptyDescription={t(\n 'adaptercloud.connections.emptyDescription',\n 'Create a connection to a real target to start discovering resources.'\n )}\n emptyIcon={<Cable className=\"h-10 w-10\" />}\n emptyAction={\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.connections.new', 'New connection')}\n </button>\n }\n >\n <div className=\"grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3\">\n {items.map((conn) => (\n <GlassCard\n key={conn.id}\n role=\"button\"\n tabIndex={0}\n onClick={() => navigateTo(`/connections/${conn.id}`)}\n onKeyDown={(e) => {\n if (e.key === 'Enter' || e.key === ' ') {\n e.preventDefault();\n navigateTo(`/connections/${conn.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 {conn.name}\n </h3>\n {conn.adapter && (\n <p className=\"mt-0.5 truncate text-xs text-text-secondary\">\n {conn.adapter.name}\n </p>\n )}\n </div>\n <StatusBadge status={conn.status} />\n </div>\n\n <div className=\"mt-3 flex flex-wrap items-center gap-3 text-xs text-text-secondary\">\n <span className=\"inline-flex items-center rounded-full bg-bg-sunken px-2 py-0.5\">\n {authMethodLabels[conn.authMethod]}\n </span>\n {conn.region && (\n <span className=\"flex items-center gap-1\">\n <MapPin className=\"h-3 w-3\" /> {conn.region}\n </span>\n )}\n {conn.environment && <span>· {conn.environment}</span>}\n </div>\n\n <div className=\"mt-4 flex items-center gap-2 text-sm text-text-primary\">\n <Server className=\"h-4 w-4 text-text-secondary\" />\n <span className=\"font-semibold tabular-nums\">{conn.resourceCount}</span>\n <span className=\"text-text-secondary\">\n {t('adaptercloud.connections.resources', 'resources')}\n </span>\n </div>\n\n {conn.lastError && (\n <p className=\"mt-3 line-clamp-2 rounded-lg bg-status-error-bg-subtle px-2 py-1 text-xs text-status-error-text\">\n {conn.lastError}\n </p>\n )}\n\n <p className=\"mt-4 text-xs text-text-secondary\">\n {conn.lastSyncedAt\n ? `${t('adaptercloud.connections.lastSynced', 'Last synced')} ${formatRelativeTime(conn.lastSyncedAt)}`\n : t('adaptercloud.connections.neverSynced', 'Never synced')}\n </p>\n </GlassCard>\n ))}\n </div>\n\n {(offset > 0 || hasMore) && (\n <div className=\"mt-6 flex items-center justify-between\">\n <button\n type=\"button\"\n disabled={offset === 0}\n onClick={() => setOffset((o) => Math.max(0, o - PAGE_SIZE))}\n className=\"inline-flex items-center gap-1 rounded-lg border border-border-subtle px-3 py-2 text-sm font-medium text-text-primary transition-colors hover:bg-accent disabled:cursor-not-allowed disabled:opacity-50\"\n >\n <ChevronLeft className=\"h-4 w-4\" />\n {t('adaptercloud.common.prev', 'Previous')}\n </button>\n <span className=\"text-sm text-text-secondary\">\n {t('adaptercloud.common.showing', 'Showing')} {offset + 1}–{offset + items.length}\n </span>\n <button\n type=\"button\"\n disabled={!hasMore}\n onClick={() => setOffset((o) => o + PAGE_SIZE)}\n className=\"inline-flex items-center gap-1 rounded-lg border border-border-subtle px-3 py-2 text-sm font-medium text-text-primary transition-colors hover:bg-accent disabled:cursor-not-allowed disabled:opacity-50\"\n >\n {t('adaptercloud.common.next', 'Next')}\n <ChevronRight className=\"h-4 w-4\" />\n </button>\n </div>\n )}\n </DataState>\n </PageLayout>\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"ConnectionsListPage.js","names":[],"sources":["../../../../src/adaptercloud/pages/connections/ConnectionsListPage.tsx"],"sourcesContent":["/**\n * Connections List Page\n * @module adaptercloud/pages/connections\n *\n * Every configured, authenticated target in the workspace. Lists from\n * `adaListConnections` with an adapter + status filter, a client-side search,\n * and offset pagination. Each card carries a per-adapter-kind glyph tile so the\n * fabric's reach reads at a glance (mirrors the aws/azure/gke/snowflake logos in\n * the mock).\n */\n\nimport { useState, useCallback, useMemo } from 'react';\nimport { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';\nimport {\n Cable,\n Plus,\n ChevronLeft,\n ChevronRight,\n MapPin,\n Server,\n Search,\n Cloud,\n Radio,\n Cpu,\n Boxes,\n Network,\n Database,\n HardDrive,\n Puzzle,\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 { useConnections, useAdapters } from '../../hooks/useAdapterCloudApi';\nimport { authMethodLabels, connectionStatusLabels } from '../../utils/domain';\nimport { formatRelativeTime } from '../../utils/formatters';\nimport type { AdaAdapterKind, AdaConnectionStatus } from '../../types';\n\nconst PAGE_SIZE = 20;\n\n/**\n * Per-adapter-kind glyph — stands in for the vendor logos (aws / azure / gke /\n * snowflake) in the mock. Keyed off the connection's bound adapter kind; every\n * `AdaAdapterKind` maps to one of the eight allowed lucide marks.\n */\nconst adapterKindIcon: Record<AdaAdapterKind, LucideIcon> = {\n PUBLIC_CLOUD: Cloud,\n PRIVATE_CLOUD: Cloud,\n SAAS: Cloud,\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 ConnectionsListPage() {\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<AdaConnectionStatus | ''>('');\n const [adapterId, setAdapterId] = useState('');\n const [search, setSearch] = useState('');\n const [offset, setOffset] = useState(0);\n\n const query = useConnections({\n adapterId: adapterId || undefined,\n status: status || undefined,\n pagination: { limit: PAGE_SIZE, offset },\n });\n\n // Published adapters power the adapter filter dropdown (mock's \"Adapter filter\").\n const adaptersQuery = useAdapters({ status: 'PUBLISHED', pagination: { limit: 100 } });\n const adapterOptions = adaptersQuery.data?.items ?? [];\n\n const items = query.data?.items ?? [];\n const hasMore = query.data?.pageInfo.hasMore ?? false;\n\n // Client-side search over the loaded page (name / region / environment).\n const filtered = useMemo(() => {\n const q = search.trim().toLowerCase();\n if (!q) return items;\n return items.filter(\n (c) =>\n c.name.toLowerCase().includes(q) ||\n (c.region?.toLowerCase().includes(q) ?? false) ||\n (c.environment?.toLowerCase().includes(q) ?? false)\n );\n }, [items, search]);\n\n return (\n <PageLayout\n title={t('adaptercloud.connections.title', 'Connections')}\n description={t(\n 'adaptercloud.connections.subtitle',\n 'Configured, authenticated targets — each connection is where the fabric actually reaches.'\n )}\n actions={\n <button\n type=\"button\"\n onClick={() => navigateTo('/connections/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.connections.new', 'New connection')}\n </button>\n }\n >\n <div className=\"mb-6 flex flex-wrap items-center gap-3\">\n <select\n value={status}\n onChange={(e) => {\n setStatus(e.target.value as AdaConnectionStatus | '');\n setOffset(0);\n }}\n aria-label={t('adaptercloud.connections.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.connections.allStatuses', 'All statuses')}</option>\n {(Object.keys(connectionStatusLabels) as AdaConnectionStatus[]).map((s) => (\n <option key={s} value={s}>\n {connectionStatusLabels[s]}\n </option>\n ))}\n </select>\n\n <select\n value={adapterId}\n onChange={(e) => {\n setAdapterId(e.target.value);\n setOffset(0);\n }}\n aria-label={t('adaptercloud.connections.filterByAdapter', 'Filter by adapter')}\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.connections.allAdapters', 'All adapters')}</option>\n {adapterOptions.map((a) => (\n <option key={a.id} value={a.id}>\n {a.name}\n </option>\n ))}\n </select>\n\n <div className=\"relative w-full sm:ml-auto sm:w-64\">\n <Search\n aria-hidden=\"true\"\n className=\"pointer-events-none absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-text-secondary\"\n />\n <input\n type=\"search\"\n value={search}\n onChange={(e) => setSearch(e.target.value)}\n aria-label={t('adaptercloud.connections.search', 'Search connections')}\n placeholder={t('adaptercloud.connections.searchPlaceholder', 'Search…')}\n className=\"w-full rounded-lg border border-border-subtle bg-bg-surface py-2 pl-9 pr-3 text-sm text-text-primary placeholder:text-text-secondary focus:outline-none focus:ring-2 focus:ring-[var(--color-focus-ring)]\"\n />\n </div>\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.connections.emptyTitle', 'No connections yet')}\n emptyDescription={t(\n 'adaptercloud.connections.emptyDescription',\n 'Create a connection to a real target to start discovering resources.'\n )}\n emptyIcon={<Cable className=\"h-10 w-10\" />}\n emptyAction={\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.connections.new', 'New connection')}\n </button>\n }\n >\n {filtered.length === 0 ? (\n <p className=\"rounded-xl border border-border-subtle bg-bg-surface px-4 py-10 text-center text-sm text-text-secondary\">\n {t('adaptercloud.connections.noSearchMatch', 'No connections match your search.')}\n </p>\n ) : (\n <div className=\"grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3\">\n {filtered.map((conn) => {\n const KindIcon = conn.adapter?.kind ? adapterKindIcon[conn.adapter.kind] : Cable;\n return (\n <GlassCard\n key={conn.id}\n role=\"button\"\n tabIndex={0}\n onClick={() => navigateTo(`/connections/${conn.id}`)}\n onKeyDown={(e) => {\n if (e.key === 'Enter' || e.key === ' ') {\n e.preventDefault();\n navigateTo(`/connections/${conn.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 gap-3\">\n <div className=\"flex h-10 w-10 shrink-0 items-center justify-center rounded-lg bg-bg-sunken text-accent-blue\">\n <KindIcon className=\"h-5 w-5\" aria-hidden=\"true\" />\n </div>\n <div className=\"min-w-0 flex-1\">\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 {conn.name}\n </h3>\n {conn.adapter && (\n <p className=\"mt-0.5 truncate text-xs text-text-secondary\">\n {conn.adapter.name}\n </p>\n )}\n </div>\n <StatusBadge status={conn.status} />\n </div>\n </div>\n </div>\n\n <div className=\"mt-3 flex flex-wrap items-center gap-3 text-xs text-text-secondary\">\n <span className=\"inline-flex items-center rounded-full bg-bg-sunken px-2 py-0.5\">\n {authMethodLabels[conn.authMethod]}\n </span>\n {conn.region && (\n <span className=\"flex items-center gap-1\">\n <MapPin className=\"h-3 w-3\" /> {conn.region}\n </span>\n )}\n {conn.environment && <span>· {conn.environment}</span>}\n </div>\n\n <div className=\"mt-4 flex items-center gap-2 text-sm text-text-primary\">\n <Server className=\"h-4 w-4 text-text-secondary\" />\n <span className=\"font-semibold tabular-nums\">{conn.resourceCount}</span>\n <span className=\"text-text-secondary\">\n {t('adaptercloud.connections.resources', 'resources')}\n </span>\n </div>\n\n {conn.lastError && (\n <p className=\"mt-3 line-clamp-2 rounded-lg bg-status-error-bg-subtle px-2 py-1 text-xs text-status-error-text\">\n {conn.lastError}\n </p>\n )}\n\n <p className=\"mt-4 text-xs text-text-secondary\">\n {conn.lastSyncedAt\n ? `${t('adaptercloud.connections.lastSynced', 'Last synced')} ${formatRelativeTime(conn.lastSyncedAt)}`\n : t('adaptercloud.connections.neverSynced', 'Never synced')}\n </p>\n </GlassCard>\n );\n })}\n </div>\n )}\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,54 +1,78 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import
|
|
11
|
-
import {
|
|
1
|
+
import { useAdapterCloud as e } from "../../context/AdapterCloudContext.js";
|
|
2
|
+
import { PageLayout as t } from "../../components/PageLayout.js";
|
|
3
|
+
import { MetricsCard as n } from "../../components/MetricsCard.js";
|
|
4
|
+
import { formatMoney as r } from "../../utils/domain.js";
|
|
5
|
+
import { DataState as i } from "../../components/DataState.js";
|
|
6
|
+
import { useCostRecords as a, useCostSummary as o } from "../../hooks/useAdapterCloudApi.js";
|
|
7
|
+
import { formatDate as s } from "../../utils/formatters.js";
|
|
8
|
+
import { BarChart as c } from "../../components/viz/BarChart.js";
|
|
9
|
+
import { Sparkline as l } from "../../components/viz/Sparkline.js";
|
|
10
|
+
import "../../components/viz/index.js";
|
|
11
|
+
import { useCallback as u, useMemo as d, useState as f } from "react";
|
|
12
|
+
import { useI18n as p } from "@burdenoff/fe-libs/shared/providers/shell/I18nProvider";
|
|
13
|
+
import { DollarSign as m, Receipt as h, Search as g, X as _ } from "lucide-react";
|
|
14
|
+
import { Fragment as v, jsx as y, jsxs as b } from "react/jsx-runtime";
|
|
15
|
+
import { EmphasisPanel as x, GlassCard as S, ResponsiveTable as C } from "@burdenoff/fe-libs/ui";
|
|
12
16
|
//#region src/adaptercloud/pages/costs/CostsPage.tsx
|
|
13
|
-
function
|
|
14
|
-
let { t:
|
|
15
|
-
let n =
|
|
17
|
+
function w() {
|
|
18
|
+
let { navigateTo: w } = e(), { t: T } = p(), E = u((e, t) => {
|
|
19
|
+
let n = T(e);
|
|
16
20
|
return n === e ? t : n;
|
|
17
|
-
}, [
|
|
18
|
-
e.
|
|
19
|
-
|
|
21
|
+
}, [T]), [D, O] = f(""), [k, A] = f(""), j = o(), M = a({ service: k || void 0 }), N = j.data?.currency ?? "USD", P = d(() => [...j.data?.lines ?? []].sort((e, t) => t.total - e.total), [j.data]), F = d(() => P.map((e) => ({
|
|
22
|
+
key: e.service,
|
|
23
|
+
label: e.service,
|
|
24
|
+
value: e.total,
|
|
25
|
+
display: r(e.total, N)
|
|
26
|
+
})), [P, N]), I = d(() => {
|
|
27
|
+
let e = /* @__PURE__ */ new Map();
|
|
28
|
+
for (let t of M.data ?? []) {
|
|
29
|
+
let n = new Date(t.periodStart);
|
|
30
|
+
if (Number.isNaN(n.getTime())) continue;
|
|
31
|
+
let r = `${n.getUTCFullYear()}-${String(n.getUTCMonth() + 1).padStart(2, "0")}`;
|
|
32
|
+
e.set(r, (e.get(r) ?? 0) + t.amount);
|
|
33
|
+
}
|
|
34
|
+
let t = [...e.entries()].sort(([e], [t]) => e.localeCompare(t)).map(([, e]) => e), n = t[t.length - 1] ?? 0, r = t[t.length - 2] ?? 0;
|
|
35
|
+
return {
|
|
36
|
+
points: t,
|
|
37
|
+
deltaPct: r > 0 ? Math.round((n - r) / r * 100) : null
|
|
38
|
+
};
|
|
39
|
+
}, [M.data]), L = u((e) => {
|
|
40
|
+
e.preventDefault(), A(D.trim());
|
|
41
|
+
}, [D]), R = u(() => {
|
|
42
|
+
O(""), A("");
|
|
43
|
+
}, []), z = [
|
|
20
44
|
{
|
|
21
45
|
key: "service",
|
|
22
|
-
header:
|
|
46
|
+
header: E("adaptercloud.costs.service", "Service"),
|
|
23
47
|
priority: "primary",
|
|
24
|
-
cell: (e) => /* @__PURE__ */
|
|
48
|
+
cell: (e) => /* @__PURE__ */ y("span", {
|
|
25
49
|
className: "font-medium text-text-primary",
|
|
26
50
|
children: e.service
|
|
27
51
|
})
|
|
28
52
|
},
|
|
29
53
|
{
|
|
30
54
|
key: "amount",
|
|
31
|
-
header:
|
|
55
|
+
header: E("adaptercloud.costs.amount", "Amount"),
|
|
32
56
|
priority: "primary",
|
|
33
|
-
cell: (e) => /* @__PURE__ */
|
|
57
|
+
cell: (e) => /* @__PURE__ */ y("span", {
|
|
34
58
|
className: "tabular-nums text-text-primary",
|
|
35
|
-
children:
|
|
59
|
+
children: r(e.amount, e.currency)
|
|
36
60
|
})
|
|
37
61
|
},
|
|
38
62
|
{
|
|
39
63
|
key: "period",
|
|
40
|
-
header:
|
|
64
|
+
header: E("adaptercloud.costs.period", "Period"),
|
|
41
65
|
priority: "secondary",
|
|
42
|
-
cell: (e) => /* @__PURE__ */
|
|
66
|
+
cell: (e) => /* @__PURE__ */ b("span", {
|
|
43
67
|
className: "text-sm text-text-secondary",
|
|
44
68
|
children: [
|
|
45
|
-
|
|
69
|
+
s(e.periodStart, {
|
|
46
70
|
hour: void 0,
|
|
47
71
|
minute: void 0
|
|
48
72
|
}),
|
|
49
73
|
" –",
|
|
50
74
|
" ",
|
|
51
|
-
|
|
75
|
+
s(e.periodEnd, {
|
|
52
76
|
hour: void 0,
|
|
53
77
|
minute: void 0
|
|
54
78
|
})
|
|
@@ -56,114 +80,164 @@ function y() {
|
|
|
56
80
|
})
|
|
57
81
|
},
|
|
58
82
|
{
|
|
59
|
-
key: "
|
|
60
|
-
header:
|
|
83
|
+
key: "resource",
|
|
84
|
+
header: E("adaptercloud.costs.resource", "Resource"),
|
|
85
|
+
priority: "secondary",
|
|
86
|
+
cell: (e) => e.resource?.name ? /* @__PURE__ */ y("button", {
|
|
87
|
+
type: "button",
|
|
88
|
+
onClick: () => e.resourceId && w(`/resources/${e.resourceId}`),
|
|
89
|
+
className: "max-w-[12rem] truncate text-left text-sm text-accent-blue hover:underline",
|
|
90
|
+
children: e.resource.name
|
|
91
|
+
}) : e.resourceId ? /* @__PURE__ */ b("span", {
|
|
92
|
+
className: "font-mono text-xs text-text-secondary",
|
|
93
|
+
children: [e.resourceId.slice(0, 10), "…"]
|
|
94
|
+
}) : /* @__PURE__ */ y("span", {
|
|
95
|
+
className: "text-text-tertiary",
|
|
96
|
+
children: "—"
|
|
97
|
+
})
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
key: "connection",
|
|
101
|
+
header: E("adaptercloud.costs.connection", "Connection"),
|
|
61
102
|
priority: "hidden-on-mobile",
|
|
62
|
-
cell: (e) => /* @__PURE__ */
|
|
63
|
-
className: "text-
|
|
64
|
-
children: e.
|
|
103
|
+
cell: (e) => e.connection?.name ? /* @__PURE__ */ y("span", {
|
|
104
|
+
className: "max-w-[12rem] truncate text-sm text-text-primary",
|
|
105
|
+
children: e.connection.name
|
|
106
|
+
}) : /* @__PURE__ */ y("span", {
|
|
107
|
+
className: "text-text-tertiary",
|
|
108
|
+
children: "—"
|
|
65
109
|
})
|
|
66
110
|
}
|
|
67
111
|
];
|
|
68
|
-
return /* @__PURE__ */
|
|
69
|
-
title:
|
|
70
|
-
description:
|
|
71
|
-
children: [/* @__PURE__ */
|
|
72
|
-
isLoading:
|
|
73
|
-
isError:
|
|
74
|
-
error:
|
|
75
|
-
onRetry: () => void
|
|
76
|
-
children: [/* @__PURE__ */
|
|
112
|
+
return /* @__PURE__ */ b(t, {
|
|
113
|
+
title: E("adaptercloud.costs.title", "FinOps"),
|
|
114
|
+
description: E("adaptercloud.costs.subtitle", "Spend attributed to the same resources you operate — cost and topology share one model."),
|
|
115
|
+
children: [/* @__PURE__ */ b(i, {
|
|
116
|
+
isLoading: j.isLoading,
|
|
117
|
+
isError: j.isError,
|
|
118
|
+
error: j.error,
|
|
119
|
+
onRetry: () => void j.refetch(),
|
|
120
|
+
children: [/* @__PURE__ */ b(x, {
|
|
77
121
|
className: "mb-8",
|
|
78
|
-
children: [/* @__PURE__ */
|
|
122
|
+
children: [/* @__PURE__ */ y("div", {
|
|
79
123
|
className: "mb-4 flex items-center justify-between",
|
|
80
|
-
children: /* @__PURE__ */
|
|
124
|
+
children: /* @__PURE__ */ y("h2", {
|
|
81
125
|
className: "text-lg font-semibold text-text-primary",
|
|
82
|
-
children:
|
|
126
|
+
children: E("adaptercloud.costs.summary", "Cost Summary")
|
|
83
127
|
})
|
|
84
|
-
}), /* @__PURE__ */
|
|
128
|
+
}), /* @__PURE__ */ b("div", {
|
|
85
129
|
className: "grid grid-cols-2 gap-4 lg:grid-cols-4",
|
|
86
|
-
children: [/* @__PURE__ */
|
|
130
|
+
children: [/* @__PURE__ */ y(n, {
|
|
87
131
|
featured: !0,
|
|
88
|
-
icon: /* @__PURE__ */ m
|
|
89
|
-
value:
|
|
90
|
-
label:
|
|
91
|
-
}),
|
|
92
|
-
icon: /* @__PURE__ */
|
|
93
|
-
value:
|
|
132
|
+
icon: /* @__PURE__ */ y(m, { className: "h-5 w-5" }),
|
|
133
|
+
value: j.data ? r(j.data.total, N) : "—",
|
|
134
|
+
label: E("adaptercloud.costs.totalSpend", "Total Spend")
|
|
135
|
+
}), P.slice(0, 3).map((e) => /* @__PURE__ */ y(n, {
|
|
136
|
+
icon: /* @__PURE__ */ y(h, { className: "h-5 w-5" }),
|
|
137
|
+
value: r(e.total, N),
|
|
94
138
|
label: e.service
|
|
95
139
|
}, e.service))]
|
|
96
140
|
})]
|
|
97
|
-
}),
|
|
98
|
-
className: "mb-8",
|
|
99
|
-
children: [/* @__PURE__ */
|
|
100
|
-
className: "
|
|
101
|
-
children:
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
className: "
|
|
106
|
-
children:
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
className: "mb-1 flex items-center justify-between text-sm",
|
|
110
|
-
children: [/* @__PURE__ */ m("span", {
|
|
111
|
-
className: "truncate text-text-primary",
|
|
112
|
-
children: e.service
|
|
113
|
-
}), /* @__PURE__ */ h("span", {
|
|
114
|
-
className: "tabular-nums text-text-secondary",
|
|
115
|
-
children: [
|
|
116
|
-
n(e.total, D),
|
|
117
|
-
" · ",
|
|
118
|
-
t,
|
|
119
|
-
"%"
|
|
120
|
-
]
|
|
121
|
-
})]
|
|
122
|
-
}), /* @__PURE__ */ m("div", {
|
|
123
|
-
className: "h-1.5 w-full overflow-hidden rounded-full bg-bg-sunken",
|
|
124
|
-
children: /* @__PURE__ */ m("div", {
|
|
125
|
-
className: "h-full rounded-full bg-action-primary-bg",
|
|
126
|
-
style: { width: `${Math.max(t, 2)}%` }
|
|
127
|
-
})
|
|
128
|
-
})] }, e.service);
|
|
141
|
+
}), P.length > 0 && /* @__PURE__ */ b("div", {
|
|
142
|
+
className: "mb-8 grid grid-cols-1 gap-6 lg:grid-cols-3",
|
|
143
|
+
children: [/* @__PURE__ */ b("section", {
|
|
144
|
+
className: "lg:col-span-2",
|
|
145
|
+
children: [/* @__PURE__ */ y("h2", {
|
|
146
|
+
className: "mb-4 text-lg font-semibold text-text-primary",
|
|
147
|
+
children: E("adaptercloud.costs.byService", "Spend by Service")
|
|
148
|
+
}), /* @__PURE__ */ y(S, {
|
|
149
|
+
className: "p-5",
|
|
150
|
+
children: /* @__PURE__ */ y(c, {
|
|
151
|
+
data: F,
|
|
152
|
+
labelWidthClassName: "w-24 sm:w-32"
|
|
129
153
|
})
|
|
154
|
+
})]
|
|
155
|
+
}), /* @__PURE__ */ b("section", { children: [/* @__PURE__ */ y("h2", {
|
|
156
|
+
className: "mb-4 text-lg font-semibold text-text-primary",
|
|
157
|
+
children: E("adaptercloud.costs.trend", "Monthly Trend")
|
|
158
|
+
}), /* @__PURE__ */ y(S, {
|
|
159
|
+
className: "flex h-[calc(100%-2.75rem)] flex-col justify-between p-5",
|
|
160
|
+
children: I.points.length >= 2 ? /* @__PURE__ */ b(v, { children: [/* @__PURE__ */ b("div", {
|
|
161
|
+
className: "flex items-baseline gap-2",
|
|
162
|
+
children: [/* @__PURE__ */ y("span", {
|
|
163
|
+
className: "text-2xl font-bold tabular-nums text-text-primary",
|
|
164
|
+
children: r(I.points[I.points.length - 1] ?? 0, N)
|
|
165
|
+
}), I.deltaPct !== null && /* @__PURE__ */ b("span", {
|
|
166
|
+
className: `text-xs font-semibold tabular-nums ${I.deltaPct > 0 ? "text-status-error-text" : "text-status-success-text"}`,
|
|
167
|
+
children: [
|
|
168
|
+
I.deltaPct > 0 ? "▲" : "▼",
|
|
169
|
+
" ",
|
|
170
|
+
Math.abs(I.deltaPct),
|
|
171
|
+
"%",
|
|
172
|
+
" ",
|
|
173
|
+
E("adaptercloud.costs.vsPrev", "vs prev")
|
|
174
|
+
]
|
|
175
|
+
})]
|
|
176
|
+
}), /* @__PURE__ */ y(l, {
|
|
177
|
+
points: I.points,
|
|
178
|
+
className: "mt-4 h-16 w-full text-accent-blue"
|
|
179
|
+
})] }) : /* @__PURE__ */ y("p", {
|
|
180
|
+
className: "py-6 text-center text-sm text-text-secondary",
|
|
181
|
+
children: E("adaptercloud.costs.trendEmpty", "Ingest at least two months of cost to see a trend.")
|
|
130
182
|
})
|
|
131
|
-
})]
|
|
183
|
+
})] })]
|
|
132
184
|
})]
|
|
133
|
-
}), /* @__PURE__ */
|
|
185
|
+
}), /* @__PURE__ */ b("section", { children: [/* @__PURE__ */ b("div", {
|
|
134
186
|
className: "mb-4 flex flex-wrap items-center justify-between gap-3",
|
|
135
|
-
children: [/* @__PURE__ */
|
|
187
|
+
children: [/* @__PURE__ */ y("h2", {
|
|
136
188
|
className: "text-lg font-semibold text-text-primary",
|
|
137
|
-
children:
|
|
138
|
-
}), /* @__PURE__ */
|
|
139
|
-
onSubmit:
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
189
|
+
children: E("adaptercloud.costs.records", "Cost Records")
|
|
190
|
+
}), /* @__PURE__ */ b("form", {
|
|
191
|
+
onSubmit: L,
|
|
192
|
+
role: "search",
|
|
193
|
+
className: "flex w-full items-center gap-2 sm:w-auto",
|
|
194
|
+
children: [/* @__PURE__ */ b("div", {
|
|
195
|
+
className: "relative w-full sm:w-56",
|
|
196
|
+
children: [/* @__PURE__ */ y("input", {
|
|
197
|
+
type: "text",
|
|
198
|
+
value: D,
|
|
199
|
+
onChange: (e) => O(e.target.value),
|
|
200
|
+
"aria-label": E("adaptercloud.costs.filterService", "Filter by service"),
|
|
201
|
+
placeholder: E("adaptercloud.costs.filterServicePlaceholder", "Filter by service…"),
|
|
202
|
+
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)]"
|
|
203
|
+
}), k && /* @__PURE__ */ y("button", {
|
|
204
|
+
type: "button",
|
|
205
|
+
onClick: R,
|
|
206
|
+
"aria-label": E("adaptercloud.costs.clearServiceFilter", "Clear service filter"),
|
|
207
|
+
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)]",
|
|
208
|
+
children: /* @__PURE__ */ y(_, { className: "h-4 w-4" })
|
|
209
|
+
})]
|
|
210
|
+
}), /* @__PURE__ */ y("button", {
|
|
211
|
+
type: "submit",
|
|
212
|
+
"aria-label": E("adaptercloud.costs.applyServiceFilter", "Apply service filter"),
|
|
213
|
+
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)]",
|
|
214
|
+
children: /* @__PURE__ */ y(g, { className: "h-4 w-4" })
|
|
147
215
|
})]
|
|
148
216
|
})]
|
|
149
|
-
}), /* @__PURE__ */
|
|
150
|
-
isLoading:
|
|
151
|
-
isError:
|
|
152
|
-
isEmpty: (
|
|
153
|
-
error:
|
|
154
|
-
onRetry: () => void
|
|
155
|
-
emptyTitle:
|
|
156
|
-
emptyDescription:
|
|
157
|
-
emptyIcon: /* @__PURE__ */
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
217
|
+
}), /* @__PURE__ */ y(i, {
|
|
218
|
+
isLoading: M.isLoading,
|
|
219
|
+
isError: M.isError,
|
|
220
|
+
isEmpty: (M.data ?? []).length === 0,
|
|
221
|
+
error: M.error,
|
|
222
|
+
onRetry: () => void M.refetch(),
|
|
223
|
+
emptyTitle: E("adaptercloud.costs.emptyTitle", "No cost records"),
|
|
224
|
+
emptyDescription: E("adaptercloud.costs.emptyDescription", "Run a cost-ingest sync against a connection to populate the ledger."),
|
|
225
|
+
emptyIcon: /* @__PURE__ */ y(h, { className: "h-10 w-10" }),
|
|
226
|
+
emptyAction: /* @__PURE__ */ y("button", {
|
|
227
|
+
type: "button",
|
|
228
|
+
onClick: () => w("/connections"),
|
|
229
|
+
className: "rounded-lg bg-action-primary-bg px-4 py-2 text-sm font-medium text-action-primary-text transition-colors hover:opacity-90",
|
|
230
|
+
children: E("adaptercloud.costs.goToConnections", "Go to connections")
|
|
231
|
+
}),
|
|
232
|
+
children: /* @__PURE__ */ y(C, {
|
|
233
|
+
data: M.data ?? [],
|
|
234
|
+
columns: z,
|
|
161
235
|
rowKey: (e) => e.id
|
|
162
236
|
})
|
|
163
237
|
})] })]
|
|
164
238
|
});
|
|
165
239
|
}
|
|
166
240
|
//#endregion
|
|
167
|
-
export {
|
|
241
|
+
export { w as CostsPage };
|
|
168
242
|
|
|
169
243
|
//# sourceMappingURL=CostsPage.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"CostsPage.js","names":[],"sources":["../../../../src/adaptercloud/pages/costs/CostsPage.tsx"],"sourcesContent":["/**\n * Costs (FinOps) Page\n * @module adaptercloud/pages/costs\n *\n * Workspace FinOps: the cost summary by service (`adaCostSummary`) plus the raw\n * cost-record ledger (`adaListCostRecords`), optionally filtered by service.\n */\n\nimport { useState, useCallback, useMemo } from 'react';\nimport { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';\nimport { DollarSign, Search, Receipt } from 'lucide-react';\nimport {\n EmphasisPanel,\n GlassCard,\n ResponsiveTable,\n type ResponsiveTableColumn,\n} from '@burdenoff/fe-libs/ui';\nimport { PageLayout } from '../../components/PageLayout';\nimport { MetricsCard } from '../../components/MetricsCard';\nimport { DataState } from '../../components/DataState';\nimport { useCostSummary, useCostRecords } from '../../hooks/useAdapterCloudApi';\nimport { formatMoney } from '../../utils/domain';\nimport { formatDate } from '../../utils/formatters';\nimport type { AdaCostRecord } from '../../types';\n\nexport function CostsPage() {\n const { t: translate } = useI18n();\n const t = useCallback(\n (key: string, fallback: string): string => {\n const value = translate(key);\n return value === key ? fallback : value;\n },\n [translate]\n );\n\n const [serviceInput, setServiceInput] = useState('');\n const [appliedService, setAppliedService] = useState('');\n\n const summary = useCostSummary();\n const records = useCostRecords({ service: appliedService || undefined });\n\n const currency = summary.data?.currency ?? 'USD';\n\n const sortedLines = useMemo(\n () => [...(summary.data?.lines ?? [])].sort((a, b) => b.total - a.total),\n [summary.data]\n );\n\n const onSearch = useCallback(\n (e: React.FormEvent) => {\n e.preventDefault();\n setAppliedService(serviceInput.trim());\n },\n [serviceInput]\n );\n\n const recordColumns: ResponsiveTableColumn<AdaCostRecord>[] = [\n {\n key: 'service',\n header: t('adaptercloud.costs.service', 'Service'),\n priority: 'primary',\n cell: (r) => <span className=\"font-medium text-text-primary\">{r.service}</span>,\n },\n {\n key: 'amount',\n header: t('adaptercloud.costs.amount', 'Amount'),\n priority: 'primary',\n cell: (r) => (\n <span className=\"tabular-nums text-text-primary\">{formatMoney(r.amount, r.currency)}</span>\n ),\n },\n {\n key: 'period',\n header: t('adaptercloud.costs.period', 'Period'),\n priority: 'secondary',\n cell: (r) => (\n <span className=\"text-sm text-text-secondary\">\n {formatDate(r.periodStart, { hour: undefined, minute: undefined })} –{' '}\n {formatDate(r.periodEnd, { hour: undefined, minute: undefined })}\n </span>\n ),\n },\n {\n key: 'scope',\n header: t('adaptercloud.costs.scope', 'Scope'),\n priority: 'hidden-on-mobile',\n cell: (r) => (\n <span className=\"text-xs text-text-secondary\">\n {r.resourceId\n ? t('adaptercloud.costs.resourceScope', 'Resource')\n : r.connectionId\n ? t('adaptercloud.costs.connectionScope', 'Connection')\n : t('adaptercloud.costs.workspaceScope', 'Workspace')}\n </span>\n ),\n },\n ];\n\n return (\n <PageLayout\n title={t('adaptercloud.costs.title', 'FinOps')}\n description={t(\n 'adaptercloud.costs.subtitle',\n 'Spend attributed to the same resources you operate — cost and topology share one model.'\n )}\n >\n <DataState\n isLoading={summary.isLoading}\n isError={summary.isError}\n error={summary.error}\n onRetry={() => void summary.refetch()}\n >\n {/* Total + by-service */}\n <EmphasisPanel className=\"mb-8\">\n <div className=\"mb-4 flex items-center justify-between\">\n <h2 className=\"text-lg font-semibold text-text-primary\">\n {t('adaptercloud.costs.summary', 'Cost Summary')}\n </h2>\n </div>\n <div className=\"grid grid-cols-2 gap-4 lg:grid-cols-4\">\n <MetricsCard\n featured\n icon={<DollarSign className=\"h-5 w-5\" />}\n value={summary.data ? formatMoney(summary.data.total, currency) : '—'}\n label={t('adaptercloud.costs.totalSpend', 'Total Spend')}\n />\n {sortedLines.slice(0, 3).map((line) => (\n <MetricsCard\n key={line.service}\n icon={<Receipt className=\"h-5 w-5\" />}\n value={formatMoney(line.total, currency)}\n label={line.service}\n />\n ))}\n </div>\n </EmphasisPanel>\n\n {/* Full by-service breakdown */}\n {sortedLines.length > 0 && (\n <section className=\"mb-8\">\n <h2 className=\"mb-4 text-lg font-semibold text-text-primary\">\n {t('adaptercloud.costs.byService', 'Spend by Service')}\n </h2>\n <GlassCard className=\"p-5\">\n <ul className=\"space-y-3\">\n {sortedLines.map((line) => {\n const pct = summary.data?.total\n ? Math.round((line.total / summary.data.total) * 100)\n : 0;\n return (\n <li key={line.service}>\n <div className=\"mb-1 flex items-center justify-between text-sm\">\n <span className=\"truncate text-text-primary\">{line.service}</span>\n <span className=\"tabular-nums text-text-secondary\">\n {formatMoney(line.total, currency)} · {pct}%\n </span>\n </div>\n <div className=\"h-1.5 w-full overflow-hidden rounded-full bg-bg-sunken\">\n <div\n className=\"h-full rounded-full bg-action-primary-bg\"\n style={{ width: `${Math.max(pct, 2)}%` }}\n />\n </div>\n </li>\n );\n })}\n </ul>\n </GlassCard>\n </section>\n )}\n </DataState>\n\n {/* Cost records ledger */}\n <section>\n <div className=\"mb-4 flex flex-wrap items-center justify-between gap-3\">\n <h2 className=\"text-lg font-semibold text-text-primary\">\n {t('adaptercloud.costs.records', 'Cost Records')}\n </h2>\n <form onSubmit={onSearch} className=\"relative\">\n <Search className=\"absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-text-secondary\" />\n <input\n type=\"text\"\n value={serviceInput}\n onChange={(e) => setServiceInput(e.target.value)}\n placeholder={t('adaptercloud.costs.filterService', 'Filter by service…')}\n className=\"w-56 rounded-lg border border-border-subtle bg-bg-surface py-2 pl-9 pr-3 text-sm text-text-primary placeholder:text-text-secondary focus:outline-none focus:ring-2 focus:ring-[var(--color-focus-ring)]\"\n />\n </form>\n </div>\n\n <DataState\n isLoading={records.isLoading}\n isError={records.isError}\n isEmpty={(records.data ?? []).length === 0}\n error={records.error}\n onRetry={() => void records.refetch()}\n emptyTitle={t('adaptercloud.costs.emptyTitle', 'No cost records')}\n emptyDescription={t(\n 'adaptercloud.costs.emptyDescription',\n 'Run a cost-ingest sync against a connection to populate the ledger.'\n )}\n emptyIcon={<Receipt className=\"h-10 w-10\" />}\n >\n <ResponsiveTable 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\">{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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"}
|