@burdenoff/microfe-adaptercloud 2026.706.2 → 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 +18 -14
- package/dist/adaptercloud/AdapterCloudRoutes.js.map +1 -1
- package/dist/adaptercloud/components/ConfirmDialog.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 +22 -4
- package/dist/adaptercloud/hooks/useAdapterCloudApi.js.map +1 -1
- package/dist/adaptercloud/pages/OverviewPage.js.map +1 -1
- package/dist/adaptercloud/pages/adapters/AdapterDetailPage.js +176 -123
- package/dist/adaptercloud/pages/adapters/AdapterDetailPage.js.map +1 -1
- package/dist/adaptercloud/pages/adapters/AdapterFormPage.js +261 -179
- package/dist/adaptercloud/pages/adapters/AdapterFormPage.js.map +1 -1
- package/dist/adaptercloud/pages/adapters/AdaptersListPage.js.map +1 -1
- package/dist/adaptercloud/pages/connections/ConnectionsListPage.js +158 -94
- package/dist/adaptercloud/pages/connections/ConnectionsListPage.js.map +1 -1
- package/dist/adaptercloud/pages/costs/CostsPage.js +160 -111
- package/dist/adaptercloud/pages/costs/CostsPage.js.map +1 -1
- package/dist/adaptercloud/pages/drift/DriftListPage.js +163 -84
- package/dist/adaptercloud/pages/drift/DriftListPage.js.map +1 -1
- package/dist/adaptercloud/pages/policies/PoliciesListPage.js +177 -96
- package/dist/adaptercloud/pages/policies/PoliciesListPage.js.map +1 -1
- package/dist/adaptercloud/pages/resources/ResourceDetailPage.js +145 -131
- package/dist/adaptercloud/pages/resources/ResourceDetailPage.js.map +1 -1
- package/dist/adaptercloud/pages/resources/ResourcesListPage.js +10 -1
- package/dist/adaptercloud/pages/resources/ResourcesListPage.js.map +1 -1
- package/dist/adaptercloud/pages/sync-runs/SyncRunsListPage.js +226 -165
- 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 +184 -131
- package/dist/adaptercloud/pages/violations/ViolationsListPage.js.map +1 -1
- package/package.json +1 -1
|
@@ -5,44 +5,65 @@ import { formatMoney as r } from "../../utils/domain.js";
|
|
|
5
5
|
import { DataState as i } from "../../components/DataState.js";
|
|
6
6
|
import { useCostRecords as a, useCostSummary as o } from "../../hooks/useAdapterCloudApi.js";
|
|
7
7
|
import { formatDate as s } from "../../utils/formatters.js";
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
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";
|
|
13
16
|
//#region src/adaptercloud/pages/costs/CostsPage.tsx
|
|
14
|
-
function
|
|
15
|
-
let { navigateTo:
|
|
16
|
-
let n =
|
|
17
|
+
function w() {
|
|
18
|
+
let { navigateTo: w } = e(), { t: T } = p(), E = u((e, t) => {
|
|
19
|
+
let n = T(e);
|
|
17
20
|
return n === e ? t : n;
|
|
18
|
-
}, [
|
|
19
|
-
e.
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
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 = [
|
|
23
44
|
{
|
|
24
45
|
key: "service",
|
|
25
|
-
header:
|
|
46
|
+
header: E("adaptercloud.costs.service", "Service"),
|
|
26
47
|
priority: "primary",
|
|
27
|
-
cell: (e) => /* @__PURE__ */
|
|
48
|
+
cell: (e) => /* @__PURE__ */ y("span", {
|
|
28
49
|
className: "font-medium text-text-primary",
|
|
29
50
|
children: e.service
|
|
30
51
|
})
|
|
31
52
|
},
|
|
32
53
|
{
|
|
33
54
|
key: "amount",
|
|
34
|
-
header:
|
|
55
|
+
header: E("adaptercloud.costs.amount", "Amount"),
|
|
35
56
|
priority: "primary",
|
|
36
|
-
cell: (e) => /* @__PURE__ */
|
|
57
|
+
cell: (e) => /* @__PURE__ */ y("span", {
|
|
37
58
|
className: "tabular-nums text-text-primary",
|
|
38
59
|
children: r(e.amount, e.currency)
|
|
39
60
|
})
|
|
40
61
|
},
|
|
41
62
|
{
|
|
42
63
|
key: "period",
|
|
43
|
-
header:
|
|
64
|
+
header: E("adaptercloud.costs.period", "Period"),
|
|
44
65
|
priority: "secondary",
|
|
45
|
-
cell: (e) => /* @__PURE__ */
|
|
66
|
+
cell: (e) => /* @__PURE__ */ b("span", {
|
|
46
67
|
className: "text-sm text-text-secondary",
|
|
47
68
|
children: [
|
|
48
69
|
s(e.periodStart, {
|
|
@@ -59,136 +80,164 @@ function x() {
|
|
|
59
80
|
})
|
|
60
81
|
},
|
|
61
82
|
{
|
|
62
|
-
key: "
|
|
63
|
-
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"),
|
|
64
102
|
priority: "hidden-on-mobile",
|
|
65
|
-
cell: (e) => /* @__PURE__ */
|
|
66
|
-
className: "text-
|
|
67
|
-
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: "—"
|
|
68
109
|
})
|
|
69
110
|
}
|
|
70
111
|
];
|
|
71
|
-
return /* @__PURE__ */
|
|
72
|
-
title:
|
|
73
|
-
description:
|
|
74
|
-
children: [/* @__PURE__ */
|
|
75
|
-
isLoading:
|
|
76
|
-
isError:
|
|
77
|
-
error:
|
|
78
|
-
onRetry: () => void
|
|
79
|
-
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, {
|
|
80
121
|
className: "mb-8",
|
|
81
|
-
children: [/* @__PURE__ */
|
|
122
|
+
children: [/* @__PURE__ */ y("div", {
|
|
82
123
|
className: "mb-4 flex items-center justify-between",
|
|
83
|
-
children: /* @__PURE__ */
|
|
124
|
+
children: /* @__PURE__ */ y("h2", {
|
|
84
125
|
className: "text-lg font-semibold text-text-primary",
|
|
85
|
-
children:
|
|
126
|
+
children: E("adaptercloud.costs.summary", "Cost Summary")
|
|
86
127
|
})
|
|
87
|
-
}), /* @__PURE__ */
|
|
128
|
+
}), /* @__PURE__ */ b("div", {
|
|
88
129
|
className: "grid grid-cols-2 gap-4 lg:grid-cols-4",
|
|
89
|
-
children: [/* @__PURE__ */
|
|
130
|
+
children: [/* @__PURE__ */ y(n, {
|
|
90
131
|
featured: !0,
|
|
91
|
-
icon: /* @__PURE__ */
|
|
92
|
-
value:
|
|
93
|
-
label:
|
|
94
|
-
}),
|
|
95
|
-
icon: /* @__PURE__ */
|
|
96
|
-
value: r(e.total,
|
|
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),
|
|
97
138
|
label: e.service
|
|
98
139
|
}, e.service))]
|
|
99
140
|
})]
|
|
100
|
-
}),
|
|
101
|
-
className: "mb-8",
|
|
102
|
-
children: [/* @__PURE__ */
|
|
103
|
-
className: "
|
|
104
|
-
children:
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
className: "
|
|
109
|
-
children:
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
className: "mb-1 flex items-center justify-between text-sm",
|
|
113
|
-
children: [/* @__PURE__ */ g("span", {
|
|
114
|
-
className: "truncate text-text-primary",
|
|
115
|
-
children: e.service
|
|
116
|
-
}), /* @__PURE__ */ _("span", {
|
|
117
|
-
className: "tabular-nums text-text-secondary",
|
|
118
|
-
children: [
|
|
119
|
-
r(e.total, A),
|
|
120
|
-
" · ",
|
|
121
|
-
t,
|
|
122
|
-
"%"
|
|
123
|
-
]
|
|
124
|
-
})]
|
|
125
|
-
}), /* @__PURE__ */ g("div", {
|
|
126
|
-
className: "h-1.5 w-full overflow-hidden rounded-full bg-bg-sunken",
|
|
127
|
-
children: /* @__PURE__ */ g("div", {
|
|
128
|
-
className: "h-full rounded-full bg-action-primary-bg",
|
|
129
|
-
style: { width: `${Math.max(t, 2)}%` }
|
|
130
|
-
})
|
|
131
|
-
})] }, 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"
|
|
132
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.")
|
|
133
182
|
})
|
|
134
|
-
})]
|
|
183
|
+
})] })]
|
|
135
184
|
})]
|
|
136
|
-
}), /* @__PURE__ */
|
|
185
|
+
}), /* @__PURE__ */ b("section", { children: [/* @__PURE__ */ b("div", {
|
|
137
186
|
className: "mb-4 flex flex-wrap items-center justify-between gap-3",
|
|
138
|
-
children: [/* @__PURE__ */
|
|
187
|
+
children: [/* @__PURE__ */ y("h2", {
|
|
139
188
|
className: "text-lg font-semibold text-text-primary",
|
|
140
|
-
children:
|
|
141
|
-
}), /* @__PURE__ */
|
|
142
|
-
onSubmit:
|
|
189
|
+
children: E("adaptercloud.costs.records", "Cost Records")
|
|
190
|
+
}), /* @__PURE__ */ b("form", {
|
|
191
|
+
onSubmit: L,
|
|
143
192
|
role: "search",
|
|
144
193
|
className: "flex w-full items-center gap-2 sm:w-auto",
|
|
145
|
-
children: [/* @__PURE__ */
|
|
194
|
+
children: [/* @__PURE__ */ b("div", {
|
|
146
195
|
className: "relative w-full sm:w-56",
|
|
147
|
-
children: [/* @__PURE__ */
|
|
196
|
+
children: [/* @__PURE__ */ y("input", {
|
|
148
197
|
type: "text",
|
|
149
|
-
value:
|
|
150
|
-
onChange: (e) =>
|
|
151
|
-
"aria-label":
|
|
152
|
-
placeholder:
|
|
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…"),
|
|
153
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)]"
|
|
154
|
-
}),
|
|
203
|
+
}), k && /* @__PURE__ */ y("button", {
|
|
155
204
|
type: "button",
|
|
156
|
-
onClick:
|
|
157
|
-
"aria-label":
|
|
205
|
+
onClick: R,
|
|
206
|
+
"aria-label": E("adaptercloud.costs.clearServiceFilter", "Clear service filter"),
|
|
158
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)]",
|
|
159
|
-
children: /* @__PURE__ */
|
|
208
|
+
children: /* @__PURE__ */ y(_, { className: "h-4 w-4" })
|
|
160
209
|
})]
|
|
161
|
-
}), /* @__PURE__ */
|
|
210
|
+
}), /* @__PURE__ */ y("button", {
|
|
162
211
|
type: "submit",
|
|
163
|
-
"aria-label":
|
|
212
|
+
"aria-label": E("adaptercloud.costs.applyServiceFilter", "Apply service filter"),
|
|
164
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)]",
|
|
165
|
-
children: /* @__PURE__ */ g
|
|
214
|
+
children: /* @__PURE__ */ y(g, { className: "h-4 w-4" })
|
|
166
215
|
})]
|
|
167
216
|
})]
|
|
168
|
-
}), /* @__PURE__ */
|
|
169
|
-
isLoading:
|
|
170
|
-
isError:
|
|
171
|
-
isEmpty: (
|
|
172
|
-
error:
|
|
173
|
-
onRetry: () => void
|
|
174
|
-
emptyTitle:
|
|
175
|
-
emptyDescription:
|
|
176
|
-
emptyIcon: /* @__PURE__ */
|
|
177
|
-
emptyAction: /* @__PURE__ */
|
|
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", {
|
|
178
227
|
type: "button",
|
|
179
|
-
onClick: () =>
|
|
228
|
+
onClick: () => w("/connections"),
|
|
180
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",
|
|
181
|
-
children:
|
|
230
|
+
children: E("adaptercloud.costs.goToConnections", "Go to connections")
|
|
182
231
|
}),
|
|
183
|
-
children: /* @__PURE__ */
|
|
184
|
-
data:
|
|
185
|
-
columns:
|
|
232
|
+
children: /* @__PURE__ */ y(C, {
|
|
233
|
+
data: M.data ?? [],
|
|
234
|
+
columns: z,
|
|
186
235
|
rowKey: (e) => e.id
|
|
187
236
|
})
|
|
188
237
|
})] })]
|
|
189
238
|
});
|
|
190
239
|
}
|
|
191
240
|
//#endregion
|
|
192
|
-
export {
|
|
241
|
+
export { w as CostsPage };
|
|
193
242
|
|
|
194
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, 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 { 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 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: '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} role=\"search\" className=\"flex w-full items-center gap-2 sm:w-auto\">\n <div className=\"relative w-full sm:w-56\">\n <input\n type=\"text\"\n value={serviceInput}\n onChange={(e) => setServiceInput(e.target.value)}\n aria-label={t('adaptercloud.costs.filterService', 'Filter by service')}\n placeholder={t('adaptercloud.costs.filterServicePlaceholder', 'Filter by service…')}\n className=\"w-full rounded-lg border border-border-subtle bg-bg-surface py-2 pl-3 pr-9 text-sm text-text-primary placeholder:text-text-secondary focus:outline-none focus:ring-2 focus:ring-[var(--color-focus-ring)]\"\n />\n {appliedService && (\n <button\n type=\"button\"\n onClick={clearService}\n aria-label={t('adaptercloud.costs.clearServiceFilter', 'Clear service filter')}\n className=\"absolute right-2 top-1/2 -translate-y-1/2 rounded p-0.5 text-text-secondary transition-colors hover:text-text-primary focus:outline-none focus:ring-2 focus:ring-[var(--color-focus-ring)]\"\n >\n <X className=\"h-4 w-4\" />\n </button>\n )}\n </div>\n <button\n type=\"submit\"\n aria-label={t('adaptercloud.costs.applyServiceFilter', 'Apply service filter')}\n className=\"inline-flex shrink-0 items-center justify-center rounded-lg border border-border-subtle bg-bg-surface px-3 py-2 text-text-primary transition-colors hover:bg-accent focus:outline-none focus:ring-2 focus:ring-[var(--color-focus-ring)]\"\n >\n <Search className=\"h-4 w-4\" />\n </button>\n </form>\n </div>\n\n <DataState\n isLoading={records.isLoading}\n isError={records.isError}\n isEmpty={(records.data ?? []).length === 0}\n error={records.error}\n onRetry={() => void records.refetch()}\n emptyTitle={t('adaptercloud.costs.emptyTitle', 'No cost records')}\n emptyDescription={t(\n 'adaptercloud.costs.emptyDescription',\n 'Run a cost-ingest sync against a connection to populate the ledger.'\n )}\n emptyIcon={<Receipt className=\"h-10 w-10\" />}\n emptyAction={\n <button\n type=\"button\"\n onClick={() => navigateTo('/connections')}\n className=\"rounded-lg bg-action-primary-bg px-4 py-2 text-sm font-medium text-action-primary-text transition-colors hover:opacity-90\"\n >\n {t('adaptercloud.costs.goToConnections', 'Go to connections')}\n </button>\n }\n >\n <ResponsiveTable data={records.data ?? []} columns={recordColumns} rowKey={(r) => r.id} />\n </DataState>\n </section>\n </PageLayout>\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"CostsPage.js","names":[],"sources":["../../../../src/adaptercloud/pages/costs/CostsPage.tsx"],"sourcesContent":["/**\n * Costs (FinOps) Page\n * @module adaptercloud/pages/costs\n *\n * Workspace FinOps: the cost summary by service (`adaCostSummary`) plus the raw\n * cost-record ledger (`adaListCostRecords`), optionally filtered by service.\n */\n\nimport { useState, useCallback, useMemo } from 'react';\nimport { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';\nimport { DollarSign, Search, Receipt, X } from 'lucide-react';\nimport {\n EmphasisPanel,\n GlassCard,\n ResponsiveTable,\n type ResponsiveTableColumn,\n} from '@burdenoff/fe-libs/ui';\nimport { PageLayout } from '../../components/PageLayout';\nimport { MetricsCard } from '../../components/MetricsCard';\nimport { DataState } from '../../components/DataState';\nimport { BarChart, Sparkline } from '../../components/viz';\nimport { useAdapterCloud } from '../../context/AdapterCloudContext';\nimport { useCostSummary, useCostRecords } from '../../hooks/useAdapterCloudApi';\nimport { formatMoney } from '../../utils/domain';\nimport { formatDate } from '../../utils/formatters';\nimport type { AdaCostRecord } from '../../types';\n\nexport function CostsPage() {\n const { navigateTo } = useAdapterCloud();\n const { t: translate } = useI18n();\n const t = useCallback(\n (key: string, fallback: string): string => {\n const value = translate(key);\n return value === key ? fallback : value;\n },\n [translate]\n );\n\n const [serviceInput, setServiceInput] = useState('');\n const [appliedService, setAppliedService] = useState('');\n\n const summary = useCostSummary();\n const records = useCostRecords({ service: appliedService || undefined });\n\n const currency = summary.data?.currency ?? 'USD';\n\n const sortedLines = useMemo(\n () => [...(summary.data?.lines ?? [])].sort((a, b) => b.total - a.total),\n [summary.data]\n );\n\n // Cost-by-service bars for the BarChart (service · magnitude · $).\n const serviceBars = useMemo(\n () =>\n sortedLines.map((line) => ({\n key: line.service,\n label: line.service,\n value: line.total,\n display: formatMoney(line.total, currency),\n })),\n [sortedLines, currency]\n );\n\n // Monthly cost trend, derived from the raw ledger (periodStart → YYYY-MM buckets).\n const trend = useMemo(() => {\n const buckets = new Map<string, number>();\n for (const r of records.data ?? []) {\n const d = new Date(r.periodStart);\n if (Number.isNaN(d.getTime())) continue;\n const key = `${d.getUTCFullYear()}-${String(d.getUTCMonth() + 1).padStart(2, '0')}`;\n buckets.set(key, (buckets.get(key) ?? 0) + r.amount);\n }\n const series = [...buckets.entries()].sort(([a], [b]) => a.localeCompare(b));\n const points = series.map(([, v]) => v);\n const last = points[points.length - 1] ?? 0;\n const prev = points[points.length - 2] ?? 0;\n const deltaPct = prev > 0 ? Math.round(((last - prev) / prev) * 100) : null;\n return { points, deltaPct };\n }, [records.data]);\n\n const onSearch = useCallback(\n (e: React.FormEvent) => {\n e.preventDefault();\n setAppliedService(serviceInput.trim());\n },\n [serviceInput]\n );\n\n const clearService = useCallback(() => {\n setServiceInput('');\n setAppliedService('');\n }, []);\n\n const recordColumns: ResponsiveTableColumn<AdaCostRecord>[] = [\n {\n key: 'service',\n header: t('adaptercloud.costs.service', 'Service'),\n priority: 'primary',\n cell: (r) => <span className=\"font-medium text-text-primary\">{r.service}</span>,\n },\n {\n key: 'amount',\n header: t('adaptercloud.costs.amount', 'Amount'),\n priority: 'primary',\n cell: (r) => (\n <span className=\"tabular-nums text-text-primary\">{formatMoney(r.amount, r.currency)}</span>\n ),\n },\n {\n key: 'period',\n header: t('adaptercloud.costs.period', 'Period'),\n priority: 'secondary',\n cell: (r) => (\n <span className=\"text-sm text-text-secondary\">\n {formatDate(r.periodStart, { hour: undefined, minute: undefined })} –{' '}\n {formatDate(r.periodEnd, { hour: undefined, minute: undefined })}\n </span>\n ),\n },\n {\n key: 'resource',\n header: t('adaptercloud.costs.resource', 'Resource'),\n priority: 'secondary',\n cell: (r) =>\n r.resource?.name ? (\n <button\n type=\"button\"\n onClick={() => r.resourceId && navigateTo(`/resources/${r.resourceId}`)}\n className=\"max-w-[12rem] truncate text-left text-sm text-accent-blue hover:underline\"\n >\n {r.resource.name}\n </button>\n ) : r.resourceId ? (\n <span className=\"font-mono text-xs text-text-secondary\">{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":";;;;;;;;;;;;;;;;AA2BA,SAAgB,IAAY;CAC1B,IAAM,EAAE,kBAAe,GAAiB,EAClC,EAAE,GAAG,MAAc,GAAS,EAC5B,IAAI,GACP,GAAa,MAA6B;EACzC,IAAM,IAAQ,EAAU,EAAI;AAC5B,SAAO,MAAU,IAAM,IAAW;IAEpC,CAAC,EAAU,CACZ,EAEK,CAAC,GAAc,KAAmB,EAAS,GAAG,EAC9C,CAAC,GAAgB,KAAqB,EAAS,GAAG,EAElD,IAAU,GAAgB,EAC1B,IAAU,EAAe,EAAE,SAAS,KAAkB,KAAA,GAAW,CAAC,EAElE,IAAW,EAAQ,MAAM,YAAY,OAErC,IAAc,QACZ,CAAC,GAAI,EAAQ,MAAM,SAAS,EAAE,CAAE,CAAC,MAAM,GAAG,MAAM,EAAE,QAAQ,EAAE,MAAM,EACxE,CAAC,EAAQ,KAAK,CACf,EAGK,IAAc,QAEhB,EAAY,KAAK,OAAU;EACzB,KAAK,EAAK;EACV,OAAO,EAAK;EACZ,OAAO,EAAK;EACZ,SAAS,EAAY,EAAK,OAAO,EAAS;EAC3C,EAAE,EACL,CAAC,GAAa,EAAS,CACxB,EAGK,IAAQ,QAAc;EAC1B,IAAM,oBAAU,IAAI,KAAqB;AACzC,OAAK,IAAM,KAAK,EAAQ,QAAQ,EAAE,EAAE;GAClC,IAAM,IAAI,IAAI,KAAK,EAAE,YAAY;AACjC,OAAI,OAAO,MAAM,EAAE,SAAS,CAAC,CAAE;GAC/B,IAAM,IAAM,GAAG,EAAE,gBAAgB,CAAC,GAAG,OAAO,EAAE,aAAa,GAAG,EAAE,CAAC,SAAS,GAAG,IAAI;AACjF,KAAQ,IAAI,IAAM,EAAQ,IAAI,EAAI,IAAI,KAAK,EAAE,OAAO;;EAGtD,IAAM,IADS,CAAC,GAAG,EAAQ,SAAS,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,OAAO,EAAE,cAAc,EAAE,CAAC,CACtD,KAAK,GAAG,OAAO,EAAE,EACjC,IAAO,EAAO,EAAO,SAAS,MAAM,GACpC,IAAO,EAAO,EAAO,SAAS,MAAM;AAE1C,SAAO;GAAE;GAAQ,UADA,IAAO,IAAI,KAAK,OAAQ,IAAO,KAAQ,IAAQ,IAAI,GAAG;GAC5C;IAC1B,CAAC,EAAQ,KAAK,CAAC,EAEZ,IAAW,GACd,MAAuB;AAEtB,EADA,EAAE,gBAAgB,EAClB,EAAkB,EAAa,MAAM,CAAC;IAExC,CAAC,EAAa,CACf,EAEK,IAAe,QAAkB;AAErC,EADA,EAAgB,GAAG,EACnB,EAAkB,GAAG;IACpB,EAAE,CAAC,EAEA,IAAwD;EAC5D;GACE,KAAK;GACL,QAAQ,EAAE,8BAA8B,UAAU;GAClD,UAAU;GACV,OAAO,MAAM,kBAAC,QAAD;IAAM,WAAU;cAAiC,EAAE;IAAe,CAAA;GAChF;EACD;GACE,KAAK;GACL,QAAQ,EAAE,6BAA6B,SAAS;GAChD,UAAU;GACV,OAAO,MACL,kBAAC,QAAD;IAAM,WAAU;cAAkC,EAAY,EAAE,QAAQ,EAAE,SAAS;IAAQ,CAAA;GAE9F;EACD;GACE,KAAK;GACL,QAAQ,EAAE,6BAA6B,SAAS;GAChD,UAAU;GACV,OAAO,MACL,kBAAC,QAAD;IAAM,WAAU;cAAhB;KACG,EAAW,EAAE,aAAa;MAAE,MAAM,KAAA;MAAW,QAAQ,KAAA;MAAW,CAAC;KAAC;KAAG;KACrE,EAAW,EAAE,WAAW;MAAE,MAAM,KAAA;MAAW,QAAQ,KAAA;MAAW,CAAC;KAC3D;;GAEV;EACD;GACE,KAAK;GACL,QAAQ,EAAE,+BAA+B,WAAW;GACpD,UAAU;GACV,OAAO,MACL,EAAE,UAAU,OACV,kBAAC,UAAD;IACE,MAAK;IACL,eAAe,EAAE,cAAc,EAAW,cAAc,EAAE,aAAa;IACvE,WAAU;cAET,EAAE,SAAS;IACL,CAAA,GACP,EAAE,aACJ,kBAAC,QAAD;IAAM,WAAU;cAAhB,CAAyD,EAAE,WAAW,MAAM,GAAG,GAAG,EAAC,IAAQ;QAE3F,kBAAC,QAAD;IAAM,WAAU;cAAqB;IAAQ,CAAA;GAElD;EACD;GACE,KAAK;GACL,QAAQ,EAAE,iCAAiC,aAAa;GACxD,UAAU;GACV,OAAO,MACL,EAAE,YAAY,OACZ,kBAAC,QAAD;IAAM,WAAU;cAAoD,EAAE,WAAW;IAAY,CAAA,GAE7F,kBAAC,QAAD;IAAM,WAAU;cAAqB;IAAQ,CAAA;GAElD;EACF;AAED,QACE,kBAAC,GAAD;EACE,OAAO,EAAE,4BAA4B,SAAS;EAC9C,aAAa,EACX,+BACA,0FACD;YALH,CAOE,kBAAC,GAAD;GACE,WAAW,EAAQ;GACnB,SAAS,EAAQ;GACjB,OAAO,EAAQ;GACf,eAAe,KAAK,EAAQ,SAAS;aAJvC,CAOE,kBAAC,GAAD;IAAe,WAAU;cAAzB,CACE,kBAAC,OAAD;KAAK,WAAU;eACb,kBAAC,MAAD;MAAI,WAAU;gBACX,EAAE,8BAA8B,eAAe;MAC7C,CAAA;KACD,CAAA,EACN,kBAAC,OAAD;KAAK,WAAU;eAAf,CACE,kBAAC,GAAD;MACE,UAAA;MACA,MAAM,kBAAC,GAAD,EAAY,WAAU,WAAY,CAAA;MACxC,OAAO,EAAQ,OAAO,EAAY,EAAQ,KAAK,OAAO,EAAS,GAAG;MAClE,OAAO,EAAE,iCAAiC,cAAc;MACxD,CAAA,EACD,EAAY,MAAM,GAAG,EAAE,CAAC,KAAK,MAC5B,kBAAC,GAAD;MAEE,MAAM,kBAAC,GAAD,EAAS,WAAU,WAAY,CAAA;MACrC,OAAO,EAAY,EAAK,OAAO,EAAS;MACxC,OAAO,EAAK;MACZ,EAJK,EAAK,QAIV,CACF,CACE;OACQ;OAGf,EAAY,SAAS,KACpB,kBAAC,OAAD;IAAK,WAAU;cAAf,CACE,kBAAC,WAAD;KAAS,WAAU;eAAnB,CACE,kBAAC,MAAD;MAAI,WAAU;gBACX,EAAE,gCAAgC,mBAAmB;MACnD,CAAA,EACL,kBAAC,GAAD;MAAW,WAAU;gBACnB,kBAAC,GAAD;OAAU,MAAM;OAAa,qBAAoB;OAAiB,CAAA;MACxD,CAAA,CACJ;QAEV,kBAAC,WAAD,EAAA,UAAA,CACE,kBAAC,MAAD;KAAI,WAAU;eACX,EAAE,4BAA4B,gBAAgB;KAC5C,CAAA,EACL,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,EAAS;OAC7D,CAAA,EACN,EAAM,aAAa,QAClB,kBAAC,QAAD;OACE,WAAW,sCACT,EAAM,WAAW,IAAI,2BAA2B;iBAFpD;QAKG,EAAM,WAAW,IAAI,MAAM;QAAI;QAAE,KAAK,IAAI,EAAM,SAAS;QAAC;QAAE;QAC5D,EAAE,6BAA6B,UAAU;QACrC;SAEL;SACN,kBAAC,GAAD;MAAW,QAAQ,EAAM;MAAQ,WAAU;MAAsC,CAAA,CAChF,EAAA,CAAA,GAEH,kBAAC,KAAD;MAAG,WAAU;gBACV,EAAE,iCAAiC,qDAAqD;MACvF,CAAA;KAEI,CAAA,CACJ,EAAA,CAAA,CACN;MAEE;MAGZ,kBAAC,WAAD,EAAA,UAAA,CACE,kBAAC,OAAD;GAAK,WAAU;aAAf,CACE,kBAAC,MAAD;IAAI,WAAU;cACX,EAAE,8BAA8B,eAAe;IAC7C,CAAA,EACL,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,MAAM;MAChD,cAAY,EAAE,oCAAoC,oBAAoB;MACtE,aAAa,EAAE,+CAA+C,qBAAqB;MACnF,WAAU;MACV,CAAA,EACD,KACC,kBAAC,UAAD;MACE,MAAK;MACL,SAAS;MACT,cAAY,EAAE,yCAAyC,uBAAuB;MAC9E,WAAU;gBAEV,kBAAC,GAAD,EAAG,WAAU,WAAY,CAAA;MAClB,CAAA,CAEP;QACN,kBAAC,UAAD;KACE,MAAK;KACL,cAAY,EAAE,yCAAyC,uBAAuB;KAC9E,WAAU;eAEV,kBAAC,GAAD,EAAQ,WAAU,WAAY,CAAA;KACvB,CAAA,CACJ;MACH;MAEN,kBAAC,GAAD;GACE,WAAW,EAAQ;GACnB,SAAS,EAAQ;GACjB,UAAU,EAAQ,QAAQ,EAAE,EAAE,WAAW;GACzC,OAAO,EAAQ;GACf,eAAe,KAAK,EAAQ,SAAS;GACrC,YAAY,EAAE,iCAAiC,kBAAkB;GACjE,kBAAkB,EAChB,uCACA,sEACD;GACD,WAAW,kBAAC,GAAD,EAAS,WAAU,aAAc,CAAA;GAC5C,aACE,kBAAC,UAAD;IACE,MAAK;IACL,eAAe,EAAW,eAAe;IACzC,WAAU;cAET,EAAE,sCAAsC,oBAAoB;IACtD,CAAA;aAGX,kBAAC,GAAD;IAAiB,MAAM,EAAQ,QAAQ,EAAE;IAAE,SAAS;IAAe,SAAS,MAAM,EAAE;IAAM,CAAA;GAChF,CAAA,CACJ,EAAA,CAAA,CACC"}
|