@burdenoff/microfe-bigconsole 2026.812.1 → 2026.812.2
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/bigconsole/components/widgets/PropertiesPanel.js +480 -473
- package/dist/bigconsole/components/widgets/PropertiesPanel.js.map +1 -1
- package/dist/bigconsole/components/widgets/WidgetRegistry.js +4 -4
- package/dist/bigconsole/components/widgets/WidgetRegistry.js.map +1 -1
- package/dist/bigconsole/components/widgets/gauge/CircularGauge.js +36 -32
- package/dist/bigconsole/components/widgets/gauge/CircularGauge.js.map +1 -1
- package/dist/bigconsole/components/widgets/gauge/GaugeConfig.js +60 -50
- package/dist/bigconsole/components/widgets/gauge/GaugeConfig.js.map +1 -1
- package/dist/bigconsole/components/widgets/gauge/GaugeWidget.js +63 -34
- package/dist/bigconsole/components/widgets/gauge/GaugeWidget.js.map +1 -1
- package/dist/bigconsole/components/widgets/gauge/LinearGauge.js +46 -45
- package/dist/bigconsole/components/widgets/gauge/LinearGauge.js.map +1 -1
- package/dist/bigconsole/components/widgets/kpi-comparison/KPIComparisonWidget.js +152 -89
- package/dist/bigconsole/components/widgets/kpi-comparison/KPIComparisonWidget.js.map +1 -1
- package/dist/bigconsole/components/widgets/metric-card/MetricCardConfig.js +5 -1
- package/dist/bigconsole/components/widgets/metric-card/MetricCardConfig.js.map +1 -1
- package/dist/bigconsole/components/widgets/metric-card/MetricCardWidget.js +112 -89
- package/dist/bigconsole/components/widgets/metric-card/MetricCardWidget.js.map +1 -1
- package/dist/bigconsole/components/widgets/metric-card/SparklineChart.js +21 -13
- package/dist/bigconsole/components/widgets/metric-card/SparklineChart.js.map +1 -1
- package/dist/bigconsole/components/widgets/metric-card/index.js +4 -4
- package/dist/bigconsole/components/widgets/progress/ProgressWidget.js +129 -114
- package/dist/bigconsole/components/widgets/progress/ProgressWidget.js.map +1 -1
- package/dist/bigconsole/components/widgets/utils/targetProgress.js +13 -0
- package/dist/bigconsole/components/widgets/utils/targetProgress.js.map +1 -0
- package/package.json +1 -1
|
@@ -3,155 +3,154 @@ import i from "./ProgressBar.js";
|
|
|
3
3
|
import { memo as a, useCallback as o, useMemo as s } from "react";
|
|
4
4
|
import { jsx as c, jsxs as l } from "react/jsx-runtime";
|
|
5
5
|
//#region src/bigconsole/components/widgets/progress/ProgressWidget.tsx
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
6
|
+
function u(e) {
|
|
7
|
+
if (typeof e == "number" && Number.isFinite(e)) return e;
|
|
8
|
+
if (typeof e == "string") {
|
|
9
|
+
let t = e.trim();
|
|
10
|
+
if (t === "") return null;
|
|
11
|
+
let n = Number(t);
|
|
12
|
+
if (Number.isFinite(n)) return n;
|
|
13
|
+
}
|
|
14
|
+
return null;
|
|
15
|
+
}
|
|
16
|
+
function d(e, t) {
|
|
17
|
+
for (let n of [e, t]) if (typeof n == "number" && Number.isFinite(n) && n > 0) return n;
|
|
18
|
+
return 100;
|
|
19
|
+
}
|
|
20
|
+
function f(e, t) {
|
|
21
|
+
switch (t) {
|
|
22
|
+
case "currency": return new Intl.NumberFormat("en-US", {
|
|
23
|
+
style: "currency",
|
|
24
|
+
currency: "USD",
|
|
25
|
+
maximumFractionDigits: 0
|
|
26
|
+
}).format(e);
|
|
27
|
+
case "compact": return new Intl.NumberFormat("en-US", {
|
|
28
|
+
notation: "compact",
|
|
29
|
+
maximumFractionDigits: 1
|
|
30
|
+
}).format(e);
|
|
31
|
+
default: return new Intl.NumberFormat("en-US").format(e);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
function p(e) {
|
|
35
|
+
let t = u(e.target);
|
|
36
|
+
return {
|
|
37
|
+
...t === null ? {} : { target: t },
|
|
38
|
+
...typeof e.description == "string" ? { description: e.description } : {}
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
var m = a(function({ widget: a, data: m, onClick: h, onDrilldown: g }) {
|
|
42
|
+
let _ = a.config, v = s(() => {
|
|
43
|
+
if (!m) return;
|
|
44
|
+
if (typeof m == "object" && !Array.isArray(m) && "value" in m) {
|
|
45
|
+
let e = m, t = u(e.value);
|
|
46
|
+
if (t !== null) {
|
|
47
|
+
let n = u(e.max), r = u(e.target);
|
|
48
|
+
return {
|
|
49
|
+
...m,
|
|
50
|
+
value: t,
|
|
51
|
+
max: n ?? void 0,
|
|
52
|
+
target: r ?? void 0
|
|
17
53
|
};
|
|
18
54
|
}
|
|
19
55
|
}
|
|
20
|
-
let i = e(
|
|
56
|
+
let i = e(m);
|
|
21
57
|
if (!i) return;
|
|
22
|
-
let a =
|
|
23
|
-
if (typeof e == "number" && Number.isFinite(e)) return e;
|
|
24
|
-
if (typeof e == "string") {
|
|
25
|
-
let t = Number(e);
|
|
26
|
-
if (Number.isFinite(t)) return t;
|
|
27
|
-
}
|
|
28
|
-
return null;
|
|
29
|
-
}, c = typeof i.label == "string" ? i.label : typeof i.name == "string" ? i.name : void 0;
|
|
58
|
+
let a = _?.valueField, o = (e, t) => t.split(".").reduce((e, t) => e && typeof e == "object" ? e[t] : void 0, e), s = typeof i.label == "string" ? i.label : typeof i.name == "string" ? i.name : void 0;
|
|
30
59
|
if (a) {
|
|
31
|
-
let e = r(
|
|
60
|
+
let e = r(m) ?? [i];
|
|
32
61
|
for (let n of e) if (n && typeof n == "object") {
|
|
33
|
-
let e = n, r =
|
|
62
|
+
let e = n, r = u(o(e, a));
|
|
34
63
|
if (r !== null) {
|
|
35
|
-
let n = typeof e.label == "string" ? e.label : typeof e.name == "string" ? e.name :
|
|
64
|
+
let n = typeof e.label == "string" ? e.label : typeof e.name == "string" ? e.name : s;
|
|
36
65
|
return {
|
|
37
66
|
value: r,
|
|
38
|
-
max:
|
|
39
|
-
label: n
|
|
67
|
+
max: _?.max ?? u(e.max) ?? t(m, a) ?? void 0,
|
|
68
|
+
label: n,
|
|
69
|
+
...p(e)
|
|
40
70
|
};
|
|
41
71
|
}
|
|
42
72
|
}
|
|
43
73
|
return;
|
|
44
74
|
}
|
|
45
|
-
let
|
|
46
|
-
if (!
|
|
47
|
-
let
|
|
48
|
-
if (
|
|
49
|
-
value:
|
|
50
|
-
max:
|
|
51
|
-
label:
|
|
75
|
+
let c = n(i);
|
|
76
|
+
if (!c) return;
|
|
77
|
+
let l = u(i[c]);
|
|
78
|
+
if (l !== null) return {
|
|
79
|
+
value: l,
|
|
80
|
+
max: _?.max ?? u(i.max) ?? t(m, c) ?? void 0,
|
|
81
|
+
label: s,
|
|
82
|
+
...p(i)
|
|
52
83
|
};
|
|
53
|
-
}, [
|
|
54
|
-
if (
|
|
55
|
-
|
|
56
|
-
return m.value / e * 100;
|
|
57
|
-
}, [m, p?.max]), g = s(() => {
|
|
58
|
-
if (!m?.target) return;
|
|
59
|
-
let e = m.max || p?.max || 100;
|
|
60
|
-
return m.target / e * 100;
|
|
61
|
-
}, [m, p?.max]), _ = s(() => p?.milestones?.length ? p.milestones.map((e) => ({
|
|
84
|
+
}, [m, _]), y = s(() => v ? v.value / d(v.max, _?.max) * 100 : 0, [v, _?.max]), b = s(() => {
|
|
85
|
+
if (v?.target !== void 0) return v.target / d(v.max, _?.max) * 100;
|
|
86
|
+
}, [v, _?.max]), x = s(() => _?.milestones?.length ? _.milestones.map((e) => ({
|
|
62
87
|
position: e.position ?? e.value ?? 0,
|
|
63
88
|
label: e.label,
|
|
64
|
-
reached:
|
|
65
|
-
})) : [], [
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
}
|
|
79
|
-
}, [m, p?.format]), b = s(() => {
|
|
80
|
-
let e = m?.max || p?.max || 100;
|
|
81
|
-
switch (p?.format || "number") {
|
|
82
|
-
case "currency": return new Intl.NumberFormat("en-US", {
|
|
83
|
-
style: "currency",
|
|
84
|
-
currency: "USD",
|
|
85
|
-
maximumFractionDigits: 0
|
|
86
|
-
}).format(e);
|
|
87
|
-
case "compact": return new Intl.NumberFormat("en-US", {
|
|
88
|
-
notation: "compact",
|
|
89
|
-
maximumFractionDigits: 1
|
|
90
|
-
}).format(e);
|
|
91
|
-
default: return new Intl.NumberFormat("en-US").format(e);
|
|
92
|
-
}
|
|
89
|
+
reached: y >= (e.position ?? e.value ?? 0)
|
|
90
|
+
})) : [], [_?.milestones, y]), S = s(() => _?.colorMode === "dynamic" ? "dynamic" : _?.colorMode === "success" ? "success" : _?.colorMode === "warning" ? "warning" : _?.colorMode === "error" ? "error" : "default", [_?.colorMode]), C = s(() => v ? f(v.value, _?.format || "number") : "0", [v, _?.format]), w = s(() => f(d(v?.max, _?.max), _?.format || "number"), [
|
|
91
|
+
v?.max,
|
|
92
|
+
_?.max,
|
|
93
|
+
_?.format
|
|
94
|
+
]), T = s(() => {
|
|
95
|
+
if (v?.target !== void 0) return f(v.target, _?.format || "number");
|
|
96
|
+
}, [v?.target, _?.format]), E = o(() => {
|
|
97
|
+
g && v ? g({
|
|
98
|
+
value: v.value,
|
|
99
|
+
max: v.max,
|
|
100
|
+
target: v.target,
|
|
101
|
+
label: v.label,
|
|
102
|
+
percentage: y
|
|
103
|
+
}) : h && a.drilldowns?.length && h();
|
|
93
104
|
}, [
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
p?.format
|
|
97
|
-
]), x = o(() => {
|
|
98
|
-
f && m ? f({
|
|
99
|
-
value: m.value,
|
|
100
|
-
max: m.max,
|
|
101
|
-
target: m.target,
|
|
102
|
-
label: m.label,
|
|
103
|
-
percentage: h
|
|
104
|
-
}) : d && a.drilldowns?.length && d();
|
|
105
|
-
}, [
|
|
106
|
-
d,
|
|
107
|
-
f,
|
|
105
|
+
h,
|
|
106
|
+
g,
|
|
108
107
|
a.drilldowns,
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
]),
|
|
112
|
-
return
|
|
108
|
+
v,
|
|
109
|
+
y
|
|
110
|
+
]), D = !!(a.drilldown?.enabled || h && a.drilldowns?.length);
|
|
111
|
+
return v ? /* @__PURE__ */ l("div", {
|
|
113
112
|
className: `
|
|
114
113
|
flex flex-col h-full p-4
|
|
115
|
-
${
|
|
114
|
+
${D ? "cursor-pointer hover:bg-bg-sunken/50 transition-colors" : ""}
|
|
116
115
|
`,
|
|
117
|
-
onClick:
|
|
118
|
-
role:
|
|
119
|
-
tabIndex:
|
|
116
|
+
onClick: E,
|
|
117
|
+
role: D ? "button" : void 0,
|
|
118
|
+
tabIndex: D ? 0 : void 0,
|
|
120
119
|
onKeyDown: (e) => {
|
|
121
|
-
|
|
120
|
+
D && (e.key === "Enter" || e.key === " ") && (e.preventDefault(), E());
|
|
122
121
|
},
|
|
123
122
|
children: [
|
|
124
123
|
/* @__PURE__ */ l("div", {
|
|
125
124
|
className: "flex items-start justify-between mb-4",
|
|
126
125
|
children: [/* @__PURE__ */ l("div", {
|
|
127
126
|
className: "flex-1",
|
|
128
|
-
children: [
|
|
127
|
+
children: [v.label && /* @__PURE__ */ c("h4", {
|
|
129
128
|
className: "text-sm font-medium text-text-primary",
|
|
130
|
-
children:
|
|
131
|
-
}),
|
|
129
|
+
children: v.label
|
|
130
|
+
}), v.description && /* @__PURE__ */ c("p", {
|
|
132
131
|
className: "text-xs text-text-secondary mt-0.5",
|
|
133
|
-
children:
|
|
132
|
+
children: v.description
|
|
134
133
|
})]
|
|
135
134
|
}), /* @__PURE__ */ l("div", {
|
|
136
135
|
className: "text-right",
|
|
137
136
|
children: [/* @__PURE__ */ c("span", {
|
|
138
137
|
className: "text-2xl font-bold text-text-primary",
|
|
139
|
-
children:
|
|
140
|
-
}),
|
|
138
|
+
children: C
|
|
139
|
+
}), _?.showMax !== !1 && /* @__PURE__ */ l("span", {
|
|
141
140
|
className: "text-sm text-text-secondary",
|
|
142
|
-
children: [" / ",
|
|
141
|
+
children: [" / ", w]
|
|
143
142
|
})]
|
|
144
143
|
})]
|
|
145
144
|
}),
|
|
146
145
|
/* @__PURE__ */ c(i, {
|
|
147
|
-
value:
|
|
148
|
-
target:
|
|
149
|
-
milestones:
|
|
150
|
-
height:
|
|
151
|
-
color:
|
|
152
|
-
showPercentage:
|
|
146
|
+
value: y,
|
|
147
|
+
target: b,
|
|
148
|
+
milestones: x,
|
|
149
|
+
height: _?.barHeight || 12,
|
|
150
|
+
color: S,
|
|
151
|
+
showPercentage: _?.showPercentage !== !1
|
|
153
152
|
}),
|
|
154
|
-
|
|
153
|
+
v.target !== void 0 && /* @__PURE__ */ l("div", {
|
|
155
154
|
className: "mt-3 flex items-center gap-2 text-xs",
|
|
156
155
|
children: [/* @__PURE__ */ c("svg", {
|
|
157
156
|
className: "w-4 h-4 text-text-secondary",
|
|
@@ -161,20 +160,19 @@ var u = a(function({ widget: a, data: u, onClick: d, onDrilldown: f }) {
|
|
|
161
160
|
}), /* @__PURE__ */ l("span", {
|
|
162
161
|
className: "text-text-secondary",
|
|
163
162
|
children: [
|
|
164
|
-
"Target:",
|
|
165
|
-
" ",
|
|
163
|
+
"Target: ",
|
|
166
164
|
/* @__PURE__ */ c("span", {
|
|
167
165
|
className: "font-medium text-text-primary",
|
|
168
|
-
children:
|
|
166
|
+
children: T
|
|
169
167
|
}),
|
|
170
|
-
|
|
168
|
+
y >= (b || 0) && /* @__PURE__ */ c("span", {
|
|
171
169
|
className: "text-status-success-text ml-1",
|
|
172
170
|
children: "Reached!"
|
|
173
171
|
})
|
|
174
172
|
]
|
|
175
173
|
})]
|
|
176
174
|
}),
|
|
177
|
-
|
|
175
|
+
D && /* @__PURE__ */ c("div", {
|
|
178
176
|
className: "mt-auto pt-3 flex items-center justify-end",
|
|
179
177
|
children: /* @__PURE__ */ l("span", {
|
|
180
178
|
className: "text-xs text-text-secondary flex items-center gap-1",
|
|
@@ -193,9 +191,26 @@ var u = a(function({ widget: a, data: u, onClick: d, onDrilldown: f }) {
|
|
|
193
191
|
})
|
|
194
192
|
})
|
|
195
193
|
]
|
|
196
|
-
}) :
|
|
194
|
+
}) : /* @__PURE__ */ c("div", {
|
|
195
|
+
className: "flex items-center justify-center h-full text-text-secondary text-sm",
|
|
196
|
+
children: /* @__PURE__ */ l("div", {
|
|
197
|
+
className: "text-center",
|
|
198
|
+
children: [/* @__PURE__ */ c("svg", {
|
|
199
|
+
className: "w-12 h-12 mx-auto mb-2 text-text-tertiary",
|
|
200
|
+
fill: "none",
|
|
201
|
+
stroke: "currentColor",
|
|
202
|
+
viewBox: "0 0 24 24",
|
|
203
|
+
children: /* @__PURE__ */ c("path", {
|
|
204
|
+
strokeLinecap: "round",
|
|
205
|
+
strokeLinejoin: "round",
|
|
206
|
+
strokeWidth: 1.5,
|
|
207
|
+
d: "M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z"
|
|
208
|
+
})
|
|
209
|
+
}), /* @__PURE__ */ c("p", { children: "No progress data" })]
|
|
210
|
+
})
|
|
211
|
+
});
|
|
197
212
|
});
|
|
198
213
|
//#endregion
|
|
199
|
-
export {
|
|
214
|
+
export { m as default };
|
|
200
215
|
|
|
201
216
|
//# sourceMappingURL=ProgressWidget.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ProgressWidget.js","names":[],"sources":["../../../../../src/bigconsole/components/widgets/progress/ProgressWidget.tsx"],"sourcesContent":["/**\n * ProgressWidget Component\n *\n * Progress bar widget with target, milestones, and color stages.\n */\n\nimport { type FC, memo, useMemo, useCallback } from 'react';\nimport type { Widget, ProgressConfig } from '../../../types';\nimport { ProgressBar, type Milestone } from './ProgressBar';\nimport { firstObjectRow, inferValueField, inferMaxValue, unwrapRows } from '../utils/inferFields';\n\n// ============================================================================\n// Types\n// ============================================================================\n\nexport interface ProgressWidgetProps {\n /** Widget configuration */\n widget: Widget;\n /** Progress data */\n data?: ProgressData;\n /** Click handler for drilldown */\n onClick?: () => void;\n /** Drilldown handler - receives progress data */\n onDrilldown?: (selectedData: Record<string, unknown>) => void;\n}\n\nexport interface ProgressData {\n /** Current value */\n value: number;\n /** Maximum value (for calculating percentage) */\n max?: number;\n /** Target value */\n target?: number;\n /** Label for the progress */\n label?: string;\n /** Description text */\n description?: string;\n}\n\n// ============================================================================\n// Component\n// ============================================================================\n\nexport const ProgressWidget: FC<ProgressWidgetProps> = memo(function ProgressWidget({\n widget,\n data: rawData,\n onClick,\n onDrilldown,\n}) {\n const config = widget.config as unknown as ProgressConfig | undefined;\n\n // BOFF-2246 smart default: if rawData is an array (or envelope) rather than\n // the expected `ProgressData` single object, pull the configured valueField\n // (flat OR dotted, e.g. `metrics.revenue`) or fall back to the first\n // numeric field. Auto-compute a sensible max from the dataset.\n const normalizedData = useMemo((): ProgressData | undefined => {\n if (!rawData) return undefined;\n // Already the expected shape — pass through. Accept both number and\n // numeric-string `value`, since parsers sometimes serialize metrics as\n // strings and the pre-BOFF-2246 code path rendered those via implicit\n // JS coercion in percentage math.\n if (typeof rawData === 'object' && !Array.isArray(rawData) && 'value' in rawData) {\n const rawValue = (rawData as { value: unknown }).value;\n if (typeof rawValue === 'number') {\n return rawData as ProgressData;\n }\n if (typeof rawValue === 'string') {\n const parsed = Number(rawValue);\n if (Number.isFinite(parsed)) {\n // Coerce to number so downstream math is deterministic.\n return { ...(rawData as ProgressData), value: parsed };\n }\n }\n }\n const row = firstObjectRow(rawData);\n if (!row) return undefined;\n const configuredField = (config as Record<string, unknown> | undefined)?.valueField as string | undefined;\n\n // Supports dotted paths like `metrics.revenue` so nested valueFields\n // configured in widget config resolve on array/envelope payloads the\n // same way they do on single-object payloads.\n const getNestedValue = (obj: Record<string, unknown>, path: string): unknown =>\n path.split('.').reduce<unknown>((current, key) => {\n return current && typeof current === 'object' ? (current as Record<string, unknown>)[key] : undefined;\n }, obj);\n\n // Coerce `number` or finite numeric string to a number. Mirrors the\n // pass-through branch above so the row path doesn't silently fall back\n // to inference when a parser serializes metrics as strings.\n const toNumber = (v: unknown): number | null => {\n if (typeof v === 'number' && Number.isFinite(v)) return v;\n if (typeof v === 'string') {\n const n = Number(v);\n if (Number.isFinite(n)) return n;\n }\n return null;\n };\n\n const labelFromRow =\n typeof row.label === 'string' ? row.label : typeof row.name === 'string' ? row.name : undefined;\n\n // Configured field is authoritative — scan every row for it so sparse\n // envelopes like `{data:[{id:1},{revenue:120}]}` with\n // `valueField:\"revenue\"` still resolve. If no row carries it, return\n // undefined rather than substituting an unrelated numeric column from\n // row[0] via inference (that would silently misrepresent user config).\n if (configuredField) {\n const rows = unwrapRows(rawData) ?? [row];\n for (const r of rows) {\n if (r && typeof r === 'object') {\n const resolvedRow = r as Record<string, unknown>;\n const resolved = toNumber(getNestedValue(resolvedRow, configuredField));\n if (resolved !== null) {\n const rowLabel =\n typeof resolvedRow.label === 'string'\n ? resolvedRow.label\n : typeof resolvedRow.name === 'string'\n ? resolvedRow.name\n : labelFromRow;\n return {\n value: resolved,\n max: config?.max ?? inferMaxValue(rawData, configuredField) ?? undefined,\n label: rowLabel,\n };\n }\n }\n }\n return undefined;\n }\n\n // Fall back to inference (only when no configured field). `inferValueField`\n // already filters to numeric fields, but we still coerce here for symmetry\n // with the configured branch above.\n const inferred = inferValueField(row);\n if (!inferred) return undefined;\n const inferredValue = toNumber(row[inferred]);\n if (inferredValue === null) return undefined;\n return {\n value: inferredValue,\n max: config?.max ?? inferMaxValue(rawData, inferred) ?? undefined,\n label: labelFromRow,\n };\n }, [rawData, config]);\n\n // Downstream render uses the normalized shape. Name kept as `data` so the\n // rest of the component body (formattedValue, handleClick, header, etc.)\n // stays unchanged.\n const data = normalizedData;\n\n // Calculate percentage\n const percentage = useMemo(() => {\n if (!data) return 0;\n const max = data.max || config?.max || 100;\n return (data.value / max) * 100;\n }, [data, config?.max]);\n\n // Calculate target percentage\n const targetPercentage = useMemo(() => {\n if (!data?.target) return undefined;\n const max = data.max || config?.max || 100;\n return (data.target / max) * 100;\n }, [data, config?.max]);\n\n // Build milestones from config\n const milestones: Milestone[] = useMemo(() => {\n if (!config?.milestones?.length) return [];\n return config.milestones.map((m) => ({\n position: m.position ?? m.value ?? 0,\n label: m.label,\n reached: percentage >= (m.position ?? m.value ?? 0),\n }));\n }, [config?.milestones, percentage]);\n\n // Determine color mode\n const colorMode = useMemo(() => {\n if (config?.colorMode === 'dynamic') return 'dynamic';\n if (config?.colorMode === 'success') return 'success';\n if (config?.colorMode === 'warning') return 'warning';\n if (config?.colorMode === 'error') return 'error';\n return 'default';\n }, [config?.colorMode]);\n\n // Format value\n const formattedValue = useMemo(() => {\n if (!data) return '0';\n const format = config?.format || 'number';\n\n switch (format) {\n case 'currency':\n return new Intl.NumberFormat('en-US', {\n style: 'currency',\n currency: 'USD',\n maximumFractionDigits: 0,\n }).format(data.value);\n case 'compact':\n return new Intl.NumberFormat('en-US', {\n notation: 'compact',\n maximumFractionDigits: 1,\n }).format(data.value);\n case 'number':\n default:\n return new Intl.NumberFormat('en-US').format(data.value);\n }\n }, [data, config?.format]);\n\n const formattedMax = useMemo(() => {\n const max = data?.max || config?.max || 100;\n const format = config?.format || 'number';\n\n switch (format) {\n case 'currency':\n return new Intl.NumberFormat('en-US', {\n style: 'currency',\n currency: 'USD',\n maximumFractionDigits: 0,\n }).format(max);\n case 'compact':\n return new Intl.NumberFormat('en-US', {\n notation: 'compact',\n maximumFractionDigits: 1,\n }).format(max);\n case 'number':\n default:\n return new Intl.NumberFormat('en-US').format(max);\n }\n }, [data?.max, config?.max, config?.format]);\n\n const handleClick = useCallback(() => {\n // Support both legacy onClick and new onDrilldown\n if (onDrilldown && data) {\n onDrilldown({\n value: data.value,\n max: data.max,\n target: data.target,\n label: data.label,\n percentage,\n });\n } else if (onClick && widget.drilldowns?.length) {\n onClick();\n }\n }, [onClick, onDrilldown, widget.drilldowns, data, percentage]);\n\n const hasDrilldown = !!widget.drilldown?.enabled;\n const isClickable = Boolean(hasDrilldown || (onClick && widget.drilldowns?.length));\n\n if (!data) {\n return null;\n }\n\n return (\n <div\n className={`\n flex flex-col h-full p-4\n ${isClickable ? 'cursor-pointer hover:bg-bg-sunken/50 transition-colors' : ''}\n `}\n onClick={handleClick}\n role={isClickable ? 'button' : undefined}\n tabIndex={isClickable ? 0 : undefined}\n onKeyDown={(e) => {\n if (isClickable && (e.key === 'Enter' || e.key === ' ')) {\n e.preventDefault();\n handleClick();\n }\n }}\n >\n {/* Header */}\n <div className=\"flex items-start justify-between mb-4\">\n <div className=\"flex-1\">\n {data.label && <h4 className=\"text-sm font-medium text-text-primary\">{data.label}</h4>}\n {data.description && <p className=\"text-xs text-text-secondary mt-0.5\">{data.description}</p>}\n </div>\n\n {/* Value display */}\n <div className=\"text-right\">\n <span className=\"text-2xl font-bold text-text-primary\">{formattedValue}</span>\n {config?.showMax !== false && (\n <span className=\"text-sm text-text-secondary\">\n {' / '}\n {formattedMax}\n </span>\n )}\n </div>\n </div>\n\n {/* Progress bar */}\n <ProgressBar\n value={percentage}\n target={targetPercentage}\n milestones={milestones}\n height={config?.barHeight || 12}\n color={colorMode}\n showPercentage={config?.showPercentage !== false}\n />\n\n {/* Target info */}\n {data.target !== undefined && (\n <div className=\"mt-3 flex items-center gap-2 text-xs\">\n <svg className=\"w-4 h-4 text-text-secondary\" fill=\"currentColor\" viewBox=\"0 0 24 24\">\n <path d=\"M5 21V3h14l-3 9 3 9H5z\" />\n </svg>\n <span className=\"text-text-secondary\">\n Target:{' '}\n <span className=\"font-medium text-text-primary\">{new Intl.NumberFormat('en-US').format(data.target)}</span>\n {percentage >= (targetPercentage || 0) && <span className=\"text-status-success-text ml-1\">Reached!</span>}\n </span>\n </div>\n )}\n\n {/* Drilldown indicator */}\n {isClickable && (\n <div className=\"mt-auto pt-3 flex items-center justify-end\">\n <span className=\"text-xs text-text-secondary flex items-center gap-1\">\n View details\n <svg className=\"w-3 h-3\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n <path strokeLinecap=\"round\" strokeLinejoin=\"round\" strokeWidth={2} d=\"M9 5l7 7-7 7\" />\n </svg>\n </span>\n </div>\n )}\n </div>\n );\n});\n\nexport default ProgressWidget;\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"ProgressWidget.js","names":[],"sources":["../../../../../src/bigconsole/components/widgets/progress/ProgressWidget.tsx"],"sourcesContent":["/**\n * ProgressWidget Component\n *\n * Progress bar widget with target, milestones, and color stages.\n */\n\nimport { type FC, memo, useMemo, useCallback } from 'react';\nimport type { Widget, ProgressConfig } from '../../../types';\nimport { ProgressBar, type Milestone } from './ProgressBar';\nimport { firstObjectRow, inferValueField, inferMaxValue, unwrapRows } from '../utils/inferFields';\n\n// ============================================================================\n// Types\n// ============================================================================\n\nexport interface ProgressWidgetProps {\n /** Widget configuration */\n widget: Widget;\n /** Progress data */\n data?: ProgressData;\n /** Click handler for drilldown */\n onClick?: () => void;\n /** Drilldown handler - receives progress data */\n onDrilldown?: (selectedData: Record<string, unknown>) => void;\n}\n\nexport interface ProgressData {\n /** Current value */\n value: number;\n /** Maximum value (for calculating percentage) */\n max?: number;\n /** Target value */\n target?: number;\n /** Label for the progress */\n label?: string;\n /** Description text */\n description?: string;\n}\n\n// ============================================================================\n// Helpers\n// ============================================================================\n\n/**\n * Coerce a `number` or finite numeric string to a number, else null.\n *\n * Parser output is untrusted JSON and routinely serializes metrics as strings,\n * so every numeric field (`value`, `max`, `target`) goes through this rather\n * than relying on implicit coercion or truthiness.\n */\nfunction toNumber(v: unknown): number | null {\n if (typeof v === 'number' && Number.isFinite(v)) return v;\n if (typeof v === 'string') {\n // `Number('')` and `Number(' ')` are 0, not NaN. An absent/blank field\n // must stay null — otherwise a parser emitting `{target: \"\"}` would render\n // a literal \"Target: 0\" with a marker pinned at 0%.\n const trimmed = v.trim();\n if (trimmed === '') return null;\n const n = Number(trimmed);\n if (Number.isFinite(n)) return n;\n }\n return null;\n}\n\n/**\n * Resolve the denominator for percentage maths.\n *\n * Picks the first candidate that is a usable positive number, so a max of `0`\n * (or a non-finite one) can never produce an Infinity/NaN percentage. The\n * previous `data.max || config?.max || 100` chain also treated a legitimate\n * max of 0 and a *string* max inconsistently.\n */\nfunction resolveMax(dataMax: number | undefined, configMax: number | undefined): number {\n for (const candidate of [dataMax, configMax]) {\n if (typeof candidate === 'number' && Number.isFinite(candidate) && candidate > 0) return candidate;\n }\n return 100;\n}\n\ntype ProgressValueFormat = NonNullable<ProgressConfig['format']>;\n\n/**\n * Format a progress number according to the widget's configured format.\n *\n * Shared by value, max AND target so the three figures shown together stay\n * mutually consistent — the target used to be formatted with a bare\n * `Intl.NumberFormat`, rendering e.g. \"$4,823,500 / $5,000,000\" next to a\n * target of \"5,000,000\".\n */\nfunction formatProgressNumber(value: number, format: ProgressValueFormat): string {\n switch (format) {\n case 'currency':\n return new Intl.NumberFormat('en-US', {\n style: 'currency',\n currency: 'USD',\n maximumFractionDigits: 0,\n }).format(value);\n case 'compact':\n return new Intl.NumberFormat('en-US', {\n notation: 'compact',\n maximumFractionDigits: 1,\n }).format(value);\n // 'percentage' deliberately shares the plain-number branch so value, max\n // and target remain consistent (matches the pre-existing value/max behaviour).\n case 'percentage':\n case 'number':\n default:\n return new Intl.NumberFormat('en-US').format(value);\n }\n}\n\n/**\n * Optional presentation fields a DataSink row carried.\n *\n * The array/envelope branches below rebuild a fresh object, so without this\n * they silently dropped `target` and `description` — an array payload rendered\n * with no target marker, no target footer and no description while the\n * identical single-object payload rendered all three.\n */\nfunction rowExtras(row: Record<string, unknown>): Pick<ProgressData, 'target' | 'description'> {\n const target = toNumber(row.target);\n return {\n ...(target !== null ? { target } : {}),\n ...(typeof row.description === 'string' ? { description: row.description } : {}),\n };\n}\n\n// ============================================================================\n// Component\n// ============================================================================\n\nexport const ProgressWidget: FC<ProgressWidgetProps> = memo(function ProgressWidget({\n widget,\n data: rawData,\n onClick,\n onDrilldown,\n}) {\n const config = widget.config as unknown as ProgressConfig | undefined;\n\n // BOFF-2246 smart default: if rawData is an array (or envelope) rather than\n // the expected `ProgressData` single object, pull the configured valueField\n // (flat OR dotted, e.g. `metrics.revenue`) or fall back to the first\n // numeric field. Auto-compute a sensible max from the dataset.\n const normalizedData = useMemo((): ProgressData | undefined => {\n if (!rawData) return undefined;\n // Already the expected shape — pass through. Accept both number and\n // numeric-string `value`, since parsers sometimes serialize metrics as\n // strings and the pre-BOFF-2246 code path rendered those via implicit\n // JS coercion in percentage math.\n if (typeof rawData === 'object' && !Array.isArray(rawData) && 'value' in rawData) {\n const source = rawData as unknown as Record<string, unknown>;\n const numericValue = toNumber(source.value);\n if (numericValue !== null) {\n // Coerce `max`/`target` with the same rule as `value`. Parser payloads\n // are untrusted JSON: a string max escapes the numeric guards below\n // (e.g. \"0\" is truthy, so `data.max || …` would select it and yield an\n // Infinity percentage), and a string target breaks the marker maths.\n const numericMax = toNumber(source.max);\n const numericTarget = toNumber(source.target);\n return {\n ...(rawData as ProgressData),\n value: numericValue,\n max: numericMax ?? undefined,\n target: numericTarget ?? undefined,\n };\n }\n }\n const row = firstObjectRow(rawData);\n if (!row) return undefined;\n const configuredField = (config as Record<string, unknown> | undefined)?.valueField as string | undefined;\n\n // Supports dotted paths like `metrics.revenue` so nested valueFields\n // configured in widget config resolve on array/envelope payloads the\n // same way they do on single-object payloads.\n const getNestedValue = (obj: Record<string, unknown>, path: string): unknown =>\n path.split('.').reduce<unknown>((current, key) => {\n return current && typeof current === 'object' ? (current as Record<string, unknown>)[key] : undefined;\n }, obj);\n\n const labelFromRow =\n typeof row.label === 'string' ? row.label : typeof row.name === 'string' ? row.name : undefined;\n\n // Configured field is authoritative — scan every row for it so sparse\n // envelopes like `{data:[{id:1},{revenue:120}]}` with\n // `valueField:\"revenue\"` still resolve. If no row carries it, return\n // undefined rather than substituting an unrelated numeric column from\n // row[0] via inference (that would silently misrepresent user config).\n if (configuredField) {\n const rows = unwrapRows(rawData) ?? [row];\n for (const r of rows) {\n if (r && typeof r === 'object') {\n const resolvedRow = r as Record<string, unknown>;\n const resolved = toNumber(getNestedValue(resolvedRow, configuredField));\n if (resolved !== null) {\n const rowLabel =\n typeof resolvedRow.label === 'string'\n ? resolvedRow.label\n : typeof resolvedRow.name === 'string'\n ? resolvedRow.name\n : labelFromRow;\n return {\n value: resolved,\n // Explicit config wins, then the row's own max (authoritative for\n // that row), then a dataset-derived guess.\n max: config?.max ?? toNumber(resolvedRow.max) ?? inferMaxValue(rawData, configuredField) ?? undefined,\n label: rowLabel,\n ...rowExtras(resolvedRow),\n };\n }\n }\n }\n return undefined;\n }\n\n // Fall back to inference (only when no configured field). `inferValueField`\n // already filters to numeric fields, but we still coerce here for symmetry\n // with the configured branch above.\n const inferred = inferValueField(row);\n if (!inferred) return undefined;\n const inferredValue = toNumber(row[inferred]);\n if (inferredValue === null) return undefined;\n return {\n value: inferredValue,\n max: config?.max ?? toNumber(row.max) ?? inferMaxValue(rawData, inferred) ?? undefined,\n label: labelFromRow,\n ...rowExtras(row),\n };\n }, [rawData, config]);\n\n // Downstream render uses the normalized shape. Name kept as `data` so the\n // rest of the component body (formattedValue, handleClick, header, etc.)\n // stays unchanged.\n const data = normalizedData;\n\n // Calculate percentage\n const percentage = useMemo(() => {\n if (!data) return 0;\n return (data.value / resolveMax(data.max, config?.max)) * 100;\n }, [data, config?.max]);\n\n // Calculate target percentage. `=== undefined` rather than a truthiness\n // check so a legitimate target of 0 still renders its marker.\n const targetPercentage = useMemo(() => {\n if (data?.target === undefined) return undefined;\n return (data.target / resolveMax(data.max, config?.max)) * 100;\n }, [data, config?.max]);\n\n // Build milestones from config\n const milestones: Milestone[] = useMemo(() => {\n if (!config?.milestones?.length) return [];\n return config.milestones.map((m) => ({\n position: m.position ?? m.value ?? 0,\n label: m.label,\n reached: percentage >= (m.position ?? m.value ?? 0),\n }));\n }, [config?.milestones, percentage]);\n\n // Determine color mode\n const colorMode = useMemo(() => {\n if (config?.colorMode === 'dynamic') return 'dynamic';\n if (config?.colorMode === 'success') return 'success';\n if (config?.colorMode === 'warning') return 'warning';\n if (config?.colorMode === 'error') return 'error';\n return 'default';\n }, [config?.colorMode]);\n\n // Format value\n const formattedValue = useMemo(() => {\n if (!data) return '0';\n return formatProgressNumber(data.value, config?.format || 'number');\n }, [data, config?.format]);\n\n const formattedMax = useMemo(() => {\n return formatProgressNumber(resolveMax(data?.max, config?.max), config?.format || 'number');\n }, [data?.max, config?.max, config?.format]);\n\n const formattedTarget = useMemo(() => {\n if (data?.target === undefined) return undefined;\n return formatProgressNumber(data.target, config?.format || 'number');\n }, [data?.target, config?.format]);\n\n const handleClick = useCallback(() => {\n // Support both legacy onClick and new onDrilldown\n if (onDrilldown && data) {\n onDrilldown({\n value: data.value,\n max: data.max,\n target: data.target,\n label: data.label,\n percentage,\n });\n } else if (onClick && widget.drilldowns?.length) {\n onClick();\n }\n }, [onClick, onDrilldown, widget.drilldowns, data, percentage]);\n\n const hasDrilldown = !!widget.drilldown?.enabled;\n const isClickable = Boolean(hasDrilldown || (onClick && widget.drilldowns?.length));\n\n if (!data) {\n // WidgetWrapper's `isUsableData` admits payloads this widget cannot turn\n // into a number (e.g. `{\"value\":\"n/a\"}` or `[{\"name\":\"a\",\"status\":\"ok\"}]`),\n // so its own empty state never shows. Returning bare null left an\n // unexplained blank card; render the same empty state the sibling widgets\n // use instead.\n return (\n <div className=\"flex items-center justify-center h-full text-text-secondary text-sm\">\n <div className=\"text-center\">\n <svg\n className=\"w-12 h-12 mx-auto mb-2 text-text-tertiary\"\n fill=\"none\"\n stroke=\"currentColor\"\n viewBox=\"0 0 24 24\"\n >\n <path\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n strokeWidth={1.5}\n d=\"M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z\"\n />\n </svg>\n <p>No progress data</p>\n </div>\n </div>\n );\n }\n\n return (\n <div\n className={`\n flex flex-col h-full p-4\n ${isClickable ? 'cursor-pointer hover:bg-bg-sunken/50 transition-colors' : ''}\n `}\n onClick={handleClick}\n role={isClickable ? 'button' : undefined}\n tabIndex={isClickable ? 0 : undefined}\n onKeyDown={(e) => {\n if (isClickable && (e.key === 'Enter' || e.key === ' ')) {\n e.preventDefault();\n handleClick();\n }\n }}\n >\n {/* Header */}\n <div className=\"flex items-start justify-between mb-4\">\n <div className=\"flex-1\">\n {data.label && <h4 className=\"text-sm font-medium text-text-primary\">{data.label}</h4>}\n {data.description && <p className=\"text-xs text-text-secondary mt-0.5\">{data.description}</p>}\n </div>\n\n {/* Value display */}\n <div className=\"text-right\">\n <span className=\"text-2xl font-bold text-text-primary\">{formattedValue}</span>\n {config?.showMax !== false && (\n <span className=\"text-sm text-text-secondary\">\n {' / '}\n {formattedMax}\n </span>\n )}\n </div>\n </div>\n\n {/* Progress bar */}\n <ProgressBar\n value={percentage}\n target={targetPercentage}\n milestones={milestones}\n height={config?.barHeight || 12}\n color={colorMode}\n showPercentage={config?.showPercentage !== false}\n />\n\n {/* Target info */}\n {data.target !== undefined && (\n <div className=\"mt-3 flex items-center gap-2 text-xs\">\n <svg className=\"w-4 h-4 text-text-secondary\" fill=\"currentColor\" viewBox=\"0 0 24 24\">\n <path d=\"M5 21V3h14l-3 9 3 9H5z\" />\n </svg>\n <span className=\"text-text-secondary\">\n Target: <span className=\"font-medium text-text-primary\">{formattedTarget}</span>\n {percentage >= (targetPercentage || 0) && <span className=\"text-status-success-text ml-1\">Reached!</span>}\n </span>\n </div>\n )}\n\n {/* Drilldown indicator */}\n {isClickable && (\n <div className=\"mt-auto pt-3 flex items-center justify-end\">\n <span className=\"text-xs text-text-secondary flex items-center gap-1\">\n View details\n <svg className=\"w-3 h-3\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n <path strokeLinecap=\"round\" strokeLinejoin=\"round\" strokeWidth={2} d=\"M9 5l7 7-7 7\" />\n </svg>\n </span>\n </div>\n )}\n </div>\n );\n});\n\nexport default ProgressWidget;\n"],"mappings":"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"}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
//#region src/bigconsole/components/widgets/utils/targetProgress.ts
|
|
2
|
+
function e(e, t) {
|
|
3
|
+
if (typeof e != "number" || !Number.isFinite(e) || t === void 0 || t === 0 || !Number.isFinite(t)) return null;
|
|
4
|
+
let n = e / t * 100;
|
|
5
|
+
return Number.isFinite(n) ? n : null;
|
|
6
|
+
}
|
|
7
|
+
function t(e) {
|
|
8
|
+
return Math.min(Math.max(e, 0), 100);
|
|
9
|
+
}
|
|
10
|
+
//#endregion
|
|
11
|
+
export { t as clampProgressWidth, e as getTargetProgress };
|
|
12
|
+
|
|
13
|
+
//# sourceMappingURL=targetProgress.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"targetProgress.js","names":[],"sources":["../../../../../src/bigconsole/components/widgets/utils/targetProgress.ts"],"sourcesContent":["/**\n * Shared target-progress maths for widgets that render a \"vs target\" bar.\n *\n * Kept in one place so the KPI comparison and metric card widgets cannot drift\n * apart on the guard conditions — both previously divided by the target with no\n * protection and rendered \"NaN%\" / \"Infinity%\".\n */\n\n/**\n * Progress toward a target, as a percentage.\n *\n * Returns null whenever no meaningful ratio exists, so callers can suppress the\n * whole target block rather than render a broken figure:\n * - a non-numeric value (parser output may be a string, e.g. a status flag)\n * - an absent, zero or non-finite target (JSON payloads carry `0` and `null`)\n *\n * `Number.isFinite` does not coerce, so a runtime `null` is rejected too.\n */\nexport function getTargetProgress(value: number | string | undefined, target: number | undefined): number | null {\n if (typeof value !== 'number' || !Number.isFinite(value)) return null;\n // Order matters: the `undefined` check narrows `target` to `number`, so no\n // cast is needed (`Number.isFinite` is not a TypeScript type guard).\n if (target === undefined || target === 0 || !Number.isFinite(target)) return null;\n const progress = (value / target) * 100;\n return Number.isFinite(progress) ? progress : null;\n}\n\n/**\n * Clamp a progress percentage to a CSS-safe bar width.\n *\n * A negative value would otherwise emit a negative width, which the browser\n * drops entirely, leaving the bar at its previous size.\n */\nexport function clampProgressWidth(progress: number): number {\n return Math.min(Math.max(progress, 0), 100);\n}\n"],"mappings":";AAkBA,SAAgB,EAAkB,GAAoC,GAA2C;AAI/G,KAHI,OAAO,KAAU,YAAY,CAAC,OAAO,SAAS,EAAM,IAGpD,MAAW,KAAA,KAAa,MAAW,KAAK,CAAC,OAAO,SAAS,EAAO,CAAE,QAAO;CAC7E,IAAM,IAAY,IAAQ,IAAU;AACpC,QAAO,OAAO,SAAS,EAAS,GAAG,IAAW;;AAShD,SAAgB,EAAmB,GAA0B;AAC3D,QAAO,KAAK,IAAI,KAAK,IAAI,GAAU,EAAE,EAAE,IAAI"}
|