@dlh.io/bideux 0.1.0
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/LICENSE +202 -0
- package/README.md +90 -0
- package/dist/all.d.ts +14 -0
- package/dist/all.js +15 -0
- package/dist/all.js.map +1 -0
- package/dist/chunk-62IQGXLW.js +11470 -0
- package/dist/chunk-62IQGXLW.js.map +1 -0
- package/dist/chunk-75CRTQK7.js +315 -0
- package/dist/chunk-75CRTQK7.js.map +1 -0
- package/dist/chunk-FVO6E22Z.js +225 -0
- package/dist/chunk-FVO6E22Z.js.map +1 -0
- package/dist/chunk-GRITOB4M.js +337 -0
- package/dist/chunk-GRITOB4M.js.map +1 -0
- package/dist/chunk-HPFHIZWO.js +40 -0
- package/dist/chunk-HPFHIZWO.js.map +1 -0
- package/dist/chunk-IMNUQGHE.js +2864 -0
- package/dist/chunk-IMNUQGHE.js.map +1 -0
- package/dist/chunk-LHD445PJ.js +571 -0
- package/dist/chunk-LHD445PJ.js.map +1 -0
- package/dist/chunk-LPLBBHOW.js +386 -0
- package/dist/chunk-LPLBBHOW.js.map +1 -0
- package/dist/chunk-NGM5R3LW.js +881 -0
- package/dist/chunk-NGM5R3LW.js.map +1 -0
- package/dist/chunk-OMPR32AR.js +1320 -0
- package/dist/chunk-OMPR32AR.js.map +1 -0
- package/dist/chunk-QDQ5VVTC.js +255 -0
- package/dist/chunk-QDQ5VVTC.js.map +1 -0
- package/dist/chunk-RRWZKNDZ.js +1004 -0
- package/dist/chunk-RRWZKNDZ.js.map +1 -0
- package/dist/chunk-SRWIBMEB.js +178 -0
- package/dist/chunk-SRWIBMEB.js.map +1 -0
- package/dist/element/chunk-AOKCBPMV.js +1646 -0
- package/dist/element/chunk-AOKCBPMV.js.map +1 -0
- package/dist/element/chunk-B67SYRQP.js +281 -0
- package/dist/element/chunk-B67SYRQP.js.map +1 -0
- package/dist/element/chunk-FNRVFA2C.js +55 -0
- package/dist/element/chunk-FNRVFA2C.js.map +1 -0
- package/dist/element/chunk-G3C722QP.js +4707 -0
- package/dist/element/chunk-G3C722QP.js.map +1 -0
- package/dist/element/chunk-H5IFOSQP.js +346 -0
- package/dist/element/chunk-H5IFOSQP.js.map +1 -0
- package/dist/element/chunk-LXR3CLCN.js +30 -0
- package/dist/element/chunk-LXR3CLCN.js.map +1 -0
- package/dist/element/chunk-TFV4CJR4.js +183 -0
- package/dist/element/chunk-TFV4CJR4.js.map +1 -0
- package/dist/element/chunk-TXSK3CIN.js +859 -0
- package/dist/element/chunk-TXSK3CIN.js.map +1 -0
- package/dist/element/chunk-WDTTBY3S.js +45 -0
- package/dist/element/chunk-WDTTBY3S.js.map +1 -0
- package/dist/element/chunk-ZI3RYCAR.js +233 -0
- package/dist/element/chunk-ZI3RYCAR.js.map +1 -0
- package/dist/element/element.d.ts +3044 -0
- package/dist/element/element.js +146554 -0
- package/dist/element/element.js.map +1 -0
- package/dist/element/revo-grid.entry-7FHEBN3O.js +4817 -0
- package/dist/element/revo-grid.entry-7FHEBN3O.js.map +1 -0
- package/dist/element/revogr-attribution_7.entry-LSOFVZRA.js +1328 -0
- package/dist/element/revogr-attribution_7.entry-LSOFVZRA.js.map +1 -0
- package/dist/element/revogr-clipboard_3.entry-V74UIBPJ.js +494 -0
- package/dist/element/revogr-clipboard_3.entry-V74UIBPJ.js.map +1 -0
- package/dist/element/revogr-data_4.entry-2UBOKTFN.js +1041 -0
- package/dist/element/revogr-data_4.entry-2UBOKTFN.js.map +1 -0
- package/dist/element/revogr-filter-panel.entry-B24PLQU4.js +552 -0
- package/dist/element/revogr-filter-panel.entry-B24PLQU4.js.map +1 -0
- package/dist/index.d.ts +1041 -0
- package/dist/index.js +10 -0
- package/dist/index.js.map +1 -0
- package/dist/react.d.ts +1915 -0
- package/dist/react.js +14 -0
- package/dist/react.js.map +1 -0
- package/dist/renderers/echarts.d.ts +44 -0
- package/dist/renderers/echarts.js +6 -0
- package/dist/renderers/echarts.js.map +1 -0
- package/dist/renderers/index.d.ts +48 -0
- package/dist/renderers/index.js +12 -0
- package/dist/renderers/index.js.map +1 -0
- package/dist/renderers/revogrid.d.ts +76 -0
- package/dist/renderers/revogrid.js +6 -0
- package/dist/renderers/revogrid.js.map +1 -0
- package/dist/renderers/tabulator.d.ts +37 -0
- package/dist/renderers/tabulator.js +6 -0
- package/dist/renderers/tabulator.js.map +1 -0
- package/dist/result-table-CPls-277.d.ts +1885 -0
- package/dist/schema/dashboard.schema.json +4374 -0
- package/dist/schema/semantic-model.schema.json +1200 -0
- package/dist/schema/widget.schema.json +1793 -0
- package/dist/styles.css +156 -0
- package/dist/theme.css +1789 -0
- package/dist/types-CHFSsU_X.d.ts +1007 -0
- package/package.json +119 -0
|
@@ -0,0 +1,2864 @@
|
|
|
1
|
+
import { parseDocument, isMap, LineCounter, isSeq, isScalar, Document } from 'yaml';
|
|
2
|
+
|
|
3
|
+
// ../dashboard-core/src/dashboard-template.ts
|
|
4
|
+
function dashboardTemplate(options) {
|
|
5
|
+
const { slug, name, description = "" } = options;
|
|
6
|
+
return [
|
|
7
|
+
'version: "1.0"',
|
|
8
|
+
"kind: dashboard",
|
|
9
|
+
"metadata:",
|
|
10
|
+
` id: ${slug}`,
|
|
11
|
+
` name: ${JSON.stringify(name)}`,
|
|
12
|
+
` description: ${JSON.stringify(description)}`,
|
|
13
|
+
"spec:",
|
|
14
|
+
" config:",
|
|
15
|
+
" filterPanel:",
|
|
16
|
+
" position: top",
|
|
17
|
+
" collapsible: true",
|
|
18
|
+
" tabs:",
|
|
19
|
+
" - id: main",
|
|
20
|
+
" label: Main",
|
|
21
|
+
" widgets: []",
|
|
22
|
+
""
|
|
23
|
+
].join("\n");
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// ../dashboard-core/src/grid-config.ts
|
|
27
|
+
var MAX_LAYOUT_WIDTHS = {
|
|
28
|
+
mobile: 480,
|
|
29
|
+
tablet: 768,
|
|
30
|
+
desktop: 1024,
|
|
31
|
+
wide: 1440
|
|
32
|
+
};
|
|
33
|
+
var DEFAULT_GRID_CONFIG = {
|
|
34
|
+
compactType: "vertical",
|
|
35
|
+
preventCollision: false,
|
|
36
|
+
allowOverlap: false,
|
|
37
|
+
isBounded: true,
|
|
38
|
+
draggableHandle: ".custom-drag-handle",
|
|
39
|
+
draggableCancel: ".widget-edit-btn, .widget-remove-btn",
|
|
40
|
+
resizeHandles: ["se", "sw"],
|
|
41
|
+
containerPadding: null,
|
|
42
|
+
maxRows: 12,
|
|
43
|
+
maxLayoutWidth: "wide",
|
|
44
|
+
snap: false,
|
|
45
|
+
squareCells: true
|
|
46
|
+
};
|
|
47
|
+
function resolveSnap(grid, snap) {
|
|
48
|
+
if (grid.allowOverlap) {
|
|
49
|
+
return { compactType: null, preventCollision: false };
|
|
50
|
+
}
|
|
51
|
+
if (!snap) {
|
|
52
|
+
return { compactType: null, preventCollision: true };
|
|
53
|
+
}
|
|
54
|
+
return {
|
|
55
|
+
compactType: grid.compactType ?? "vertical",
|
|
56
|
+
preventCollision: false
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// ../dashboard-core/src/layout.ts
|
|
61
|
+
var BREAKPOINT_MAP = {
|
|
62
|
+
lg: "wide",
|
|
63
|
+
md: "desktop",
|
|
64
|
+
sm: "tablet",
|
|
65
|
+
xs: "mobile",
|
|
66
|
+
xxs: "mobile"
|
|
67
|
+
};
|
|
68
|
+
var NARROW_KEYS = ["xs", "xxs"];
|
|
69
|
+
var CONTROL_COMPONENTS = /* @__PURE__ */ new Set(["filter", "select"]);
|
|
70
|
+
function isControlComponent(component) {
|
|
71
|
+
return CONTROL_COMPONENTS.has(component);
|
|
72
|
+
}
|
|
73
|
+
function filterPanelConfig(dashboard) {
|
|
74
|
+
const config = dashboard.spec.config;
|
|
75
|
+
if (config === void 0 || config === null) return void 0;
|
|
76
|
+
const panel = config.filterPanel ?? config["filter_panel"];
|
|
77
|
+
if (typeof panel !== "object" || panel === null || Array.isArray(panel)) {
|
|
78
|
+
return void 0;
|
|
79
|
+
}
|
|
80
|
+
return panel;
|
|
81
|
+
}
|
|
82
|
+
function isPanelledControl(widget, dashboard) {
|
|
83
|
+
return Boolean(filterPanelConfig(dashboard)) && isControlComponent(widget.spec.component ?? "echarts");
|
|
84
|
+
}
|
|
85
|
+
function stripPanelledControls(layouts, widgets, dashboard) {
|
|
86
|
+
const controls = new Set(
|
|
87
|
+
Object.values(widgets).filter((widget) => isPanelledControl(widget, dashboard)).map((widget) => widget.metadata.id)
|
|
88
|
+
);
|
|
89
|
+
if (controls.size === 0) return layouts;
|
|
90
|
+
const out = {};
|
|
91
|
+
for (const [key, list] of Object.entries(layouts)) {
|
|
92
|
+
out[key] = (list ?? []).filter((item) => !controls.has(item.i));
|
|
93
|
+
}
|
|
94
|
+
return out;
|
|
95
|
+
}
|
|
96
|
+
function clamp(n, min, max) {
|
|
97
|
+
return Math.max(min, Math.min(n, max));
|
|
98
|
+
}
|
|
99
|
+
function layoutForBreakpoint(widget, bp, colCount, maxRows) {
|
|
100
|
+
const base = widget.spec.layout ?? {};
|
|
101
|
+
const responsive = base.responsive ?? {};
|
|
102
|
+
const bpLayout = responsive[bp] ?? base;
|
|
103
|
+
const minW = clamp(bpLayout.minW ?? base.minW ?? 2, 1, colCount);
|
|
104
|
+
const minH = Math.max(1, bpLayout.minH ?? base.minH ?? 2);
|
|
105
|
+
const w = clamp(bpLayout.w ?? base.w ?? 4, minW, colCount);
|
|
106
|
+
const x = clamp(bpLayout.x ?? base.x ?? 0, 0, colCount - w);
|
|
107
|
+
let h = Math.max(minH, bpLayout.h ?? base.h ?? 4);
|
|
108
|
+
const y = Math.max(0, bpLayout.y ?? base.y ?? 0);
|
|
109
|
+
if (maxRows !== void 0 && maxRows > 0) {
|
|
110
|
+
h = Math.min(h, maxRows);
|
|
111
|
+
}
|
|
112
|
+
const layout = {
|
|
113
|
+
i: widget.metadata.id,
|
|
114
|
+
x,
|
|
115
|
+
y,
|
|
116
|
+
w,
|
|
117
|
+
h,
|
|
118
|
+
minW,
|
|
119
|
+
minH
|
|
120
|
+
};
|
|
121
|
+
const maxW = bpLayout.maxW ?? base.maxW;
|
|
122
|
+
if (typeof maxW === "number" && maxW > 0) {
|
|
123
|
+
layout.maxW = clamp(maxW, 1, colCount);
|
|
124
|
+
}
|
|
125
|
+
const maxH = bpLayout.maxH ?? base.maxH;
|
|
126
|
+
if (typeof maxH === "number" && maxH > 0) {
|
|
127
|
+
layout.maxH = Math.max(1, maxH);
|
|
128
|
+
}
|
|
129
|
+
const isBounded = bpLayout.isBounded ?? base.isBounded;
|
|
130
|
+
if (typeof isBounded === "boolean") layout.isBounded = isBounded;
|
|
131
|
+
const isDraggable = bpLayout.isDraggable ?? base.isDraggable;
|
|
132
|
+
if (typeof isDraggable === "boolean") layout.isDraggable = isDraggable;
|
|
133
|
+
const isResizable = bpLayout.isResizable ?? base.isResizable;
|
|
134
|
+
if (typeof isResizable === "boolean") layout.isResizable = isResizable;
|
|
135
|
+
const resizeHandles = bpLayout.resizeHandles ?? base.resizeHandles;
|
|
136
|
+
if (Array.isArray(resizeHandles) && resizeHandles.length > 0) {
|
|
137
|
+
layout.resizeHandles = resizeHandles;
|
|
138
|
+
}
|
|
139
|
+
return layout;
|
|
140
|
+
}
|
|
141
|
+
function itemsCollide(a, b) {
|
|
142
|
+
if (a.i === b.i) return false;
|
|
143
|
+
return a.x < b.x + b.w && a.x + a.w > b.x && a.y < b.y + b.h && a.y + a.h > b.y;
|
|
144
|
+
}
|
|
145
|
+
function firstFreeSlot(existing, w, h, colCount) {
|
|
146
|
+
const width = Math.min(w, colCount);
|
|
147
|
+
const bottomY = existing.reduce(
|
|
148
|
+
(max, item) => Math.max(max, item.y + item.h),
|
|
149
|
+
0
|
|
150
|
+
);
|
|
151
|
+
for (let y = 0; y <= bottomY; y += 1) {
|
|
152
|
+
for (let x = 0; x + width <= colCount; x += 1) {
|
|
153
|
+
const candidate = { i: "__probe__", x, y, w: width, h };
|
|
154
|
+
if (!existing.some((item) => itemsCollide(candidate, item))) {
|
|
155
|
+
return { x, y };
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
return { x: 0, y: bottomY };
|
|
160
|
+
}
|
|
161
|
+
function resolveCollisions(items) {
|
|
162
|
+
const ordered = [...items].sort((a, b) => a.y - b.y || a.x - b.x);
|
|
163
|
+
const placed = [];
|
|
164
|
+
for (const item of ordered) {
|
|
165
|
+
let y = Math.max(0, item.y);
|
|
166
|
+
let candidate = { ...item, y };
|
|
167
|
+
while (placed.some((other) => itemsCollide(candidate, other))) {
|
|
168
|
+
y += 1;
|
|
169
|
+
candidate = { ...item, y };
|
|
170
|
+
}
|
|
171
|
+
placed.push(candidate);
|
|
172
|
+
}
|
|
173
|
+
const byId = new Map(placed.map((item) => [item.i, item]));
|
|
174
|
+
return items.map((item) => byId.get(item.i) ?? item);
|
|
175
|
+
}
|
|
176
|
+
function requiredRows(layouts) {
|
|
177
|
+
let rows = 0;
|
|
178
|
+
for (const list of Object.values(layouts)) {
|
|
179
|
+
for (const item of list ?? []) {
|
|
180
|
+
rows = Math.max(rows, item.y + item.h);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
return rows;
|
|
184
|
+
}
|
|
185
|
+
function overlappingIds(layouts) {
|
|
186
|
+
const found = /* @__PURE__ */ new Set();
|
|
187
|
+
for (const list of Object.values(layouts)) {
|
|
188
|
+
const items = list ?? [];
|
|
189
|
+
for (let i = 0; i < items.length; i += 1) {
|
|
190
|
+
for (let j = i + 1; j < items.length; j += 1) {
|
|
191
|
+
const first = items[i];
|
|
192
|
+
const second = items[j];
|
|
193
|
+
if (first !== void 0 && second !== void 0 && itemsCollide(first, second)) {
|
|
194
|
+
found.add(first.i);
|
|
195
|
+
found.add(second.i);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
return [...found];
|
|
201
|
+
}
|
|
202
|
+
function movedIds(before, after, only) {
|
|
203
|
+
const found = /* @__PURE__ */ new Set();
|
|
204
|
+
for (const [key, list] of Object.entries(before)) {
|
|
205
|
+
if (only !== void 0 && key !== only) continue;
|
|
206
|
+
const afterById = new Map(
|
|
207
|
+
(after[key] ?? []).map((item) => [item.i, item])
|
|
208
|
+
);
|
|
209
|
+
for (const item of list ?? []) {
|
|
210
|
+
const moved = afterById.get(item.i);
|
|
211
|
+
if (!moved) continue;
|
|
212
|
+
if (moved.x !== item.x || moved.y !== item.y || moved.w !== item.w || moved.h !== item.h) {
|
|
213
|
+
found.add(item.i);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
return [...found];
|
|
218
|
+
}
|
|
219
|
+
function maybeResolve(items, resolve) {
|
|
220
|
+
return resolve ? resolveCollisions(items) : items;
|
|
221
|
+
}
|
|
222
|
+
function placeWidgetInFirstGap(widget, bp, colCount, existing, maxRows) {
|
|
223
|
+
const layout = layoutForBreakpoint(widget, bp, colCount, maxRows);
|
|
224
|
+
const slot = firstFreeSlot(existing, layout.w, layout.h, colCount);
|
|
225
|
+
layout.x = slot.x;
|
|
226
|
+
layout.y = slot.y;
|
|
227
|
+
return layout;
|
|
228
|
+
}
|
|
229
|
+
function dashboardColumns(dashboard, gridOverrides) {
|
|
230
|
+
const responsive = dashboard.spec.responsive;
|
|
231
|
+
return {
|
|
232
|
+
lg: gridOverrides.wide?.cols ?? responsive?.wide?.cols ?? 24,
|
|
233
|
+
md: gridOverrides.desktop?.cols ?? responsive?.desktop?.cols ?? 24,
|
|
234
|
+
sm: gridOverrides.tablet?.cols ?? responsive?.tablet?.cols ?? 24,
|
|
235
|
+
xs: gridOverrides.mobile?.cols ?? responsive?.mobile?.cols ?? 24,
|
|
236
|
+
xxs: gridOverrides.mobile?.cols ?? responsive?.mobile?.cols ?? 24
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
var LAYOUT_BREAKPOINTS = {
|
|
240
|
+
lg: { bp: "wide", cols: "lg" },
|
|
241
|
+
md: { bp: "desktop", cols: "md" },
|
|
242
|
+
sm: { bp: "tablet", cols: "sm" },
|
|
243
|
+
xs: { bp: "mobile", cols: "xs" },
|
|
244
|
+
xxs: { bp: "mobile", cols: "xxs" }
|
|
245
|
+
};
|
|
246
|
+
function ensureLayoutCoverage(layouts, widgets, dashboard, gridOverrides = {}, unplacedWidgetIds = [], maxRows) {
|
|
247
|
+
const cols = dashboardColumns(dashboard, gridOverrides);
|
|
248
|
+
const base = buildBaseLayouts(
|
|
249
|
+
widgets,
|
|
250
|
+
dashboard,
|
|
251
|
+
gridOverrides,
|
|
252
|
+
unplacedWidgetIds,
|
|
253
|
+
maxRows,
|
|
254
|
+
{ resolve: false }
|
|
255
|
+
);
|
|
256
|
+
const out = {};
|
|
257
|
+
const excluded = new Set(unplacedWidgetIds);
|
|
258
|
+
for (const [key, { bp, cols: colsKey }] of Object.entries(
|
|
259
|
+
LAYOUT_BREAKPOINTS
|
|
260
|
+
)) {
|
|
261
|
+
const existing = [...layouts[key] ?? []].filter(
|
|
262
|
+
(item) => {
|
|
263
|
+
const widget = widgets[item.i];
|
|
264
|
+
return widget !== void 0 && !excluded.has(item.i) && !isPanelledControl(widget, dashboard);
|
|
265
|
+
}
|
|
266
|
+
);
|
|
267
|
+
const ids = new Set(existing.map((item) => item.i));
|
|
268
|
+
const colCount = cols[colsKey];
|
|
269
|
+
for (const widget of Object.values(widgets)) {
|
|
270
|
+
if (ids.has(widget.metadata.id)) continue;
|
|
271
|
+
if (!(base[key] ?? []).some((item) => item.i === widget.metadata.id)) {
|
|
272
|
+
continue;
|
|
273
|
+
}
|
|
274
|
+
existing.push(
|
|
275
|
+
placeWidgetInFirstGap(widget, bp, colCount, existing, maxRows)
|
|
276
|
+
);
|
|
277
|
+
ids.add(widget.metadata.id);
|
|
278
|
+
}
|
|
279
|
+
out[key] = existing;
|
|
280
|
+
}
|
|
281
|
+
return out;
|
|
282
|
+
}
|
|
283
|
+
function rescaleLayoutColumns(layouts, key, oldCols, newCols) {
|
|
284
|
+
const safeOldCols = Math.max(1, Math.floor(oldCols));
|
|
285
|
+
const safeNewCols = Math.max(1, Math.floor(newCols));
|
|
286
|
+
if (safeOldCols === safeNewCols) return layouts;
|
|
287
|
+
const list = layouts[key];
|
|
288
|
+
if (!list) return layouts;
|
|
289
|
+
return {
|
|
290
|
+
...layouts,
|
|
291
|
+
[key]: list.map((item) => {
|
|
292
|
+
const w = clamp(
|
|
293
|
+
Math.round(item.w * safeNewCols / safeOldCols),
|
|
294
|
+
1,
|
|
295
|
+
safeNewCols
|
|
296
|
+
);
|
|
297
|
+
const x = clamp(
|
|
298
|
+
Math.round(item.x * safeNewCols / safeOldCols),
|
|
299
|
+
0,
|
|
300
|
+
safeNewCols - w
|
|
301
|
+
);
|
|
302
|
+
const minW = item.minW === void 0 ? void 0 : clamp(Math.round(item.minW * safeNewCols / safeOldCols), 1, w);
|
|
303
|
+
const maxW = item.maxW === void 0 ? void 0 : clamp(
|
|
304
|
+
Math.round(item.maxW * safeNewCols / safeOldCols),
|
|
305
|
+
w,
|
|
306
|
+
safeNewCols
|
|
307
|
+
);
|
|
308
|
+
return {
|
|
309
|
+
...item,
|
|
310
|
+
x,
|
|
311
|
+
w,
|
|
312
|
+
...minW === void 0 ? {} : { minW },
|
|
313
|
+
...maxW === void 0 ? {} : { maxW }
|
|
314
|
+
};
|
|
315
|
+
})
|
|
316
|
+
};
|
|
317
|
+
}
|
|
318
|
+
function computeRequiredMaxRows(layouts, widget, dashboard, gridOverrides = {}, currentMaxRows) {
|
|
319
|
+
if (isPanelledControl(widget, dashboard)) return currentMaxRows;
|
|
320
|
+
const cols = dashboardColumns(dashboard, gridOverrides);
|
|
321
|
+
const bpMap = {
|
|
322
|
+
lg: { bp: "wide", cols: cols.lg },
|
|
323
|
+
md: { bp: "desktop", cols: cols.md },
|
|
324
|
+
sm: { bp: "tablet", cols: cols.sm },
|
|
325
|
+
xs: { bp: "mobile", cols: cols.xs },
|
|
326
|
+
xxs: { bp: "mobile", cols: cols.xxs }
|
|
327
|
+
};
|
|
328
|
+
let required = 0;
|
|
329
|
+
for (const [key, { bp, cols: colCount }] of Object.entries(bpMap)) {
|
|
330
|
+
const list = layouts[key] ?? [];
|
|
331
|
+
const bottomY = list.reduce(
|
|
332
|
+
(max, item) => Math.max(max, item.y + item.h),
|
|
333
|
+
0
|
|
334
|
+
);
|
|
335
|
+
const h = layoutForBreakpoint(widget, bp, colCount, void 0).h;
|
|
336
|
+
required = Math.max(required, bottomY + h);
|
|
337
|
+
}
|
|
338
|
+
if (currentMaxRows === void 0 || !Number.isFinite(currentMaxRows)) {
|
|
339
|
+
return currentMaxRows;
|
|
340
|
+
}
|
|
341
|
+
return Math.max(currentMaxRows, required);
|
|
342
|
+
}
|
|
343
|
+
function addWidgetToLayouts(layouts, widget, dashboard, gridOverrides = {}, maxRows) {
|
|
344
|
+
if (isPanelledControl(widget, dashboard)) {
|
|
345
|
+
const cleared = {};
|
|
346
|
+
for (const [key, list] of Object.entries(layouts)) {
|
|
347
|
+
cleared[key] = (list ?? []).filter(
|
|
348
|
+
(item) => item.i !== widget.metadata.id
|
|
349
|
+
);
|
|
350
|
+
}
|
|
351
|
+
return cleared;
|
|
352
|
+
}
|
|
353
|
+
const cols = dashboardColumns(dashboard, gridOverrides);
|
|
354
|
+
const bpMap = {
|
|
355
|
+
lg: { bp: "wide", cols: cols.lg },
|
|
356
|
+
md: { bp: "desktop", cols: cols.md },
|
|
357
|
+
sm: { bp: "tablet", cols: cols.sm },
|
|
358
|
+
xs: { bp: "mobile", cols: cols.xs },
|
|
359
|
+
xxs: { bp: "mobile", cols: cols.xxs }
|
|
360
|
+
};
|
|
361
|
+
const next = {};
|
|
362
|
+
for (const [key, { bp, cols: colCount }] of Object.entries(bpMap)) {
|
|
363
|
+
const list = (layouts[key] ?? []).filter(
|
|
364
|
+
(item) => item.i !== widget.metadata.id
|
|
365
|
+
);
|
|
366
|
+
next[key] = [
|
|
367
|
+
...list,
|
|
368
|
+
placeWidgetInFirstGap(widget, bp, colCount, list, maxRows)
|
|
369
|
+
];
|
|
370
|
+
}
|
|
371
|
+
return next;
|
|
372
|
+
}
|
|
373
|
+
function filterLayoutsByIds(layouts, ids) {
|
|
374
|
+
const out = {};
|
|
375
|
+
for (const [key, list] of Object.entries(layouts)) {
|
|
376
|
+
out[key] = (list ?? []).filter((item) => ids.has(item.i));
|
|
377
|
+
}
|
|
378
|
+
return out;
|
|
379
|
+
}
|
|
380
|
+
function clampLayoutItem(item, colCount, maxRows) {
|
|
381
|
+
const w = Math.min(item.w, colCount);
|
|
382
|
+
const x = Math.max(0, Math.min(item.x, colCount - w));
|
|
383
|
+
let h = item.h;
|
|
384
|
+
const y = Math.max(0, item.y);
|
|
385
|
+
if (maxRows !== void 0 && maxRows > 0) {
|
|
386
|
+
h = Math.min(h, maxRows);
|
|
387
|
+
}
|
|
388
|
+
return { ...item, x, y, w, h };
|
|
389
|
+
}
|
|
390
|
+
function sanitizeLayouts(layouts, cols, maxRows) {
|
|
391
|
+
const colCounts = {
|
|
392
|
+
lg: cols.lg,
|
|
393
|
+
md: cols.md,
|
|
394
|
+
sm: cols.sm,
|
|
395
|
+
xs: cols.xs,
|
|
396
|
+
xxs: cols.xxs
|
|
397
|
+
};
|
|
398
|
+
const out = {};
|
|
399
|
+
for (const [key, list] of Object.entries(layouts)) {
|
|
400
|
+
const colCount = colCounts[key] ?? cols.md;
|
|
401
|
+
out[key] = resolveCollisions(
|
|
402
|
+
(list ?? []).map((item) => clampLayoutItem(item, colCount, maxRows))
|
|
403
|
+
);
|
|
404
|
+
}
|
|
405
|
+
return out;
|
|
406
|
+
}
|
|
407
|
+
function mergeLayouts(base, overlay) {
|
|
408
|
+
const out = {};
|
|
409
|
+
const keys = /* @__PURE__ */ new Set([...Object.keys(base), ...Object.keys(overlay)]);
|
|
410
|
+
for (const key of keys) {
|
|
411
|
+
const baseList = base[key] ?? [];
|
|
412
|
+
const overlayList = overlay[key] ?? [];
|
|
413
|
+
const map = new Map(baseList.map((item) => [item.i, item]));
|
|
414
|
+
for (const item of overlayList) {
|
|
415
|
+
map.set(item.i, item);
|
|
416
|
+
}
|
|
417
|
+
out[key] = Array.from(map.values());
|
|
418
|
+
}
|
|
419
|
+
return out;
|
|
420
|
+
}
|
|
421
|
+
function syncDesktopWideLayouts(layouts, cols) {
|
|
422
|
+
if (cols.lg !== cols.md) return layouts;
|
|
423
|
+
const md = layouts.md ?? [];
|
|
424
|
+
const lg = layouts.lg ?? [];
|
|
425
|
+
if (md.length === 0 && lg.length === 0) return layouts;
|
|
426
|
+
const out = { ...layouts };
|
|
427
|
+
if (md.length === 0 && lg.length > 0) {
|
|
428
|
+
out.md = lg.map((item) => ({ ...item }));
|
|
429
|
+
} else if (lg.length === 0 && md.length > 0) {
|
|
430
|
+
out.lg = md.map((item) => ({ ...item }));
|
|
431
|
+
}
|
|
432
|
+
return out;
|
|
433
|
+
}
|
|
434
|
+
function syncActiveLayoutToDesktopWide(layouts, activeKey, cols) {
|
|
435
|
+
if (cols.lg !== cols.md) return layouts;
|
|
436
|
+
if (activeKey !== "md" && activeKey !== "lg") return layouts;
|
|
437
|
+
const sibling = activeKey === "md" ? "lg" : "md";
|
|
438
|
+
const activeList = layouts[activeKey] ?? [];
|
|
439
|
+
if (activeList.length === 0) return layouts;
|
|
440
|
+
return { ...layouts, [sibling]: activeList.map((item) => ({ ...item })) };
|
|
441
|
+
}
|
|
442
|
+
function stackItems(items, colCount) {
|
|
443
|
+
const ordered = [...items].sort((a, b) => a.y - b.y || a.x - b.x);
|
|
444
|
+
let y = 0;
|
|
445
|
+
const placed = /* @__PURE__ */ new Map();
|
|
446
|
+
for (const item of ordered) {
|
|
447
|
+
placed.set(item.i, {
|
|
448
|
+
...item,
|
|
449
|
+
x: 0,
|
|
450
|
+
y,
|
|
451
|
+
w: colCount,
|
|
452
|
+
...item.minW === void 0 ? {} : { minW: Math.min(item.minW, colCount) },
|
|
453
|
+
// An authored ceiling of half the grid would undo the full width.
|
|
454
|
+
...item.maxW !== void 0 ? { maxW: colCount } : {}
|
|
455
|
+
});
|
|
456
|
+
y += item.h;
|
|
457
|
+
}
|
|
458
|
+
return items.map((item) => placed.get(item.i) ?? item);
|
|
459
|
+
}
|
|
460
|
+
function stackNarrowLayouts(layouts, cols) {
|
|
461
|
+
const out = { ...layouts };
|
|
462
|
+
for (const key of NARROW_KEYS) {
|
|
463
|
+
const list = layouts[key] ?? [];
|
|
464
|
+
if (list.length === 0) continue;
|
|
465
|
+
out[key] = stackItems(list, cols[key]);
|
|
466
|
+
}
|
|
467
|
+
return out;
|
|
468
|
+
}
|
|
469
|
+
function buildBaseLayouts(widgets, dashboard, gridOverrides = {}, unplacedWidgetIds, maxRows, options = {}) {
|
|
470
|
+
const resolve = options.resolve !== false;
|
|
471
|
+
const exclude = new Set(unplacedWidgetIds ?? []);
|
|
472
|
+
const controlsArePanelled = Boolean(filterPanelConfig(dashboard));
|
|
473
|
+
const widgetList = Object.values(widgets).filter(
|
|
474
|
+
(w) => !exclude.has(w.metadata.id) && !(controlsArePanelled && isControlComponent(w.spec.component ?? "echarts"))
|
|
475
|
+
);
|
|
476
|
+
const cols = dashboardColumns(dashboard, gridOverrides);
|
|
477
|
+
const bpMap = {
|
|
478
|
+
lg: { bp: "wide", cols: cols.lg },
|
|
479
|
+
md: { bp: "desktop", cols: cols.md },
|
|
480
|
+
sm: { bp: "tablet", cols: cols.sm },
|
|
481
|
+
xs: { bp: "mobile", cols: cols.xs },
|
|
482
|
+
xxs: { bp: "mobile", cols: cols.xxs }
|
|
483
|
+
};
|
|
484
|
+
return Object.fromEntries(
|
|
485
|
+
Object.entries(bpMap).map(([key, { bp, cols: colCount }]) => [
|
|
486
|
+
key,
|
|
487
|
+
// Saved specs can declare the same cell twice, and a widget written by the
|
|
488
|
+
// Builder or the assistant starts at 0,0 by default, so the positions are
|
|
489
|
+
// separated here rather than drawn on top of each other.
|
|
490
|
+
maybeResolve(
|
|
491
|
+
widgetList.map((w) => layoutForBreakpoint(w, bp, colCount, maxRows)),
|
|
492
|
+
resolve
|
|
493
|
+
)
|
|
494
|
+
])
|
|
495
|
+
);
|
|
496
|
+
}
|
|
497
|
+
function layoutsForSave(layouts) {
|
|
498
|
+
const out = {};
|
|
499
|
+
for (const key of ["lg", "md", "sm", "xs", "xxs"]) {
|
|
500
|
+
const items = layouts[key];
|
|
501
|
+
const bp = BREAKPOINT_MAP[key];
|
|
502
|
+
if (!items || bp === void 0 || out[bp] !== void 0) continue;
|
|
503
|
+
out[bp] = items.map((item) => ({ ...item }));
|
|
504
|
+
}
|
|
505
|
+
return out;
|
|
506
|
+
}
|
|
507
|
+
var DATASET_KEYS = /* @__PURE__ */ new Set([
|
|
508
|
+
"id",
|
|
509
|
+
"name",
|
|
510
|
+
"description",
|
|
511
|
+
"data_store_id",
|
|
512
|
+
"source",
|
|
513
|
+
"from",
|
|
514
|
+
"source_type",
|
|
515
|
+
"filter_field",
|
|
516
|
+
"filter_param",
|
|
517
|
+
"dimensions",
|
|
518
|
+
"fields",
|
|
519
|
+
"metrics",
|
|
520
|
+
"materialization",
|
|
521
|
+
"cache"
|
|
522
|
+
]);
|
|
523
|
+
var RELATIONSHIP_KEYS = /* @__PURE__ */ new Set([
|
|
524
|
+
"name",
|
|
525
|
+
"description",
|
|
526
|
+
"from",
|
|
527
|
+
"to",
|
|
528
|
+
"from_columns",
|
|
529
|
+
"to_columns",
|
|
530
|
+
"relationship"
|
|
531
|
+
]);
|
|
532
|
+
var MODEL_METRIC_KEYS = /* @__PURE__ */ new Set([
|
|
533
|
+
"id",
|
|
534
|
+
"name",
|
|
535
|
+
"label",
|
|
536
|
+
"description",
|
|
537
|
+
"expression",
|
|
538
|
+
"type",
|
|
539
|
+
"format"
|
|
540
|
+
]);
|
|
541
|
+
var AGGREGATIONS = ["SUM", "AVG", "MIN", "MAX", "COUNT"];
|
|
542
|
+
function dimensionTypeFor(warehouseType) {
|
|
543
|
+
const type = warehouseType.toUpperCase();
|
|
544
|
+
if (/DATE|TIME|TIMESTAMP/.test(type)) return "date";
|
|
545
|
+
if (/INT|NUM|DEC|FLOAT|DOUBLE|REAL/.test(type)) return "number";
|
|
546
|
+
if (/BOOL/.test(type)) return "boolean";
|
|
547
|
+
return "string";
|
|
548
|
+
}
|
|
549
|
+
function aggregationExpression(aggregation, column) {
|
|
550
|
+
return `${aggregation}(${column})`;
|
|
551
|
+
}
|
|
552
|
+
function parseAggregation(expression) {
|
|
553
|
+
const match = /^\s*(\w+)\s*\(\s*([\w."]+)\s*\)\s*$/.exec(expression);
|
|
554
|
+
if (!match) return null;
|
|
555
|
+
const rawName = match[1];
|
|
556
|
+
const column = match[2];
|
|
557
|
+
if (rawName === void 0 || column === void 0) return null;
|
|
558
|
+
const name = rawName.toUpperCase();
|
|
559
|
+
if (!AGGREGATIONS.includes(name)) return null;
|
|
560
|
+
return { aggregation: name, column };
|
|
561
|
+
}
|
|
562
|
+
function parseModelDocument(specYaml) {
|
|
563
|
+
const doc = parseDocument(specYaml);
|
|
564
|
+
const first = doc.errors[0];
|
|
565
|
+
if (first) {
|
|
566
|
+
return {
|
|
567
|
+
doc: null,
|
|
568
|
+
error: {
|
|
569
|
+
message: first.message,
|
|
570
|
+
line: first.linePos?.[0].line ?? 1
|
|
571
|
+
}
|
|
572
|
+
};
|
|
573
|
+
}
|
|
574
|
+
if (!isMap(doc.contents)) {
|
|
575
|
+
return {
|
|
576
|
+
doc: null,
|
|
577
|
+
error: { message: "The model must be a YAML mapping.", line: 1 }
|
|
578
|
+
};
|
|
579
|
+
}
|
|
580
|
+
return { doc, error: null };
|
|
581
|
+
}
|
|
582
|
+
function str(node) {
|
|
583
|
+
return typeof node === "string" && node !== "" ? node : null;
|
|
584
|
+
}
|
|
585
|
+
function seqOf(doc, key) {
|
|
586
|
+
const node = doc.get(key, true);
|
|
587
|
+
return isSeq(node) ? node : null;
|
|
588
|
+
}
|
|
589
|
+
function mapsOf(seq) {
|
|
590
|
+
if (!seq) return [];
|
|
591
|
+
return seq.items.filter((item) => isMap(item));
|
|
592
|
+
}
|
|
593
|
+
function findByKey(seq, key, value) {
|
|
594
|
+
return mapsOf(seq).find((item) => item.get(key) === value) ?? null;
|
|
595
|
+
}
|
|
596
|
+
function findById(seq, id) {
|
|
597
|
+
return findByKey(seq, "id", id);
|
|
598
|
+
}
|
|
599
|
+
function setStringSeq(doc, map, key, values) {
|
|
600
|
+
if (values.length === 0) {
|
|
601
|
+
if (map.has(key)) map.delete(key);
|
|
602
|
+
return;
|
|
603
|
+
}
|
|
604
|
+
const current = stringList(map.get(key, true));
|
|
605
|
+
if (current.length === values.length && current.every((value, index) => value === values[index])) {
|
|
606
|
+
return;
|
|
607
|
+
}
|
|
608
|
+
map.set(key, doc.createNode(values));
|
|
609
|
+
}
|
|
610
|
+
function setOrDelete(map, key, value) {
|
|
611
|
+
if (value === null || value === void 0 || value === "") {
|
|
612
|
+
if (map.has(key)) map.delete(key);
|
|
613
|
+
return;
|
|
614
|
+
}
|
|
615
|
+
map.set(key, value);
|
|
616
|
+
}
|
|
617
|
+
function readDataStores(doc) {
|
|
618
|
+
return mapsOf(seqOf(doc, "data_stores")).flatMap((item) => {
|
|
619
|
+
const id = str(item.get("id"));
|
|
620
|
+
if (!id) return [];
|
|
621
|
+
return [
|
|
622
|
+
{
|
|
623
|
+
id,
|
|
624
|
+
name: str(item.get("name")),
|
|
625
|
+
vendor: str(item.get("vendor")),
|
|
626
|
+
dataModelId: str(item.get("data_model_id")),
|
|
627
|
+
isDefault: item.get("default") === true
|
|
628
|
+
}
|
|
629
|
+
];
|
|
630
|
+
});
|
|
631
|
+
}
|
|
632
|
+
function readDimensions(item) {
|
|
633
|
+
const node = item.get("dimensions", true) ?? item.get("fields", true);
|
|
634
|
+
if (!isSeq(node)) return [];
|
|
635
|
+
return mapsOf(node).flatMap((dim) => {
|
|
636
|
+
const id = str(dim.get("id"));
|
|
637
|
+
if (!id) return [];
|
|
638
|
+
const type = str(dim.get("type")) ?? "string";
|
|
639
|
+
return [
|
|
640
|
+
{
|
|
641
|
+
id,
|
|
642
|
+
column: str(dim.get("column")),
|
|
643
|
+
type: ["string", "number", "date", "time", "boolean"].includes(type) ? type : "string",
|
|
644
|
+
isTime: dim.get("is_time") === true
|
|
645
|
+
}
|
|
646
|
+
];
|
|
647
|
+
});
|
|
648
|
+
}
|
|
649
|
+
function readMetrics(item) {
|
|
650
|
+
const node = item.get("metrics", true);
|
|
651
|
+
if (!isSeq(node)) return [];
|
|
652
|
+
return mapsOf(node).flatMap((metric) => {
|
|
653
|
+
const id = str(metric.get("id"));
|
|
654
|
+
const expression = metric.get("expression");
|
|
655
|
+
if (!id || typeof expression !== "string") return [];
|
|
656
|
+
return [
|
|
657
|
+
{
|
|
658
|
+
id,
|
|
659
|
+
label: str(metric.get("label")) ?? str(metric.get("name")),
|
|
660
|
+
expression,
|
|
661
|
+
type: metric.get("type") === "formula" ? "formula" : "sql"
|
|
662
|
+
}
|
|
663
|
+
];
|
|
664
|
+
});
|
|
665
|
+
}
|
|
666
|
+
function stringList(node) {
|
|
667
|
+
if (!isSeq(node)) return [];
|
|
668
|
+
return node.items.flatMap((item) => {
|
|
669
|
+
const value = isScalar(item) ? item.value : item;
|
|
670
|
+
return typeof value === "string" ? [value] : [];
|
|
671
|
+
});
|
|
672
|
+
}
|
|
673
|
+
function readRollup(item) {
|
|
674
|
+
const node = item.get("materialization", true);
|
|
675
|
+
if (!isMap(node)) return null;
|
|
676
|
+
return {
|
|
677
|
+
source: str(node.get("source")),
|
|
678
|
+
grain: stringList(node.get("grain", true)),
|
|
679
|
+
metrics: stringList(node.get("metrics", true))
|
|
680
|
+
};
|
|
681
|
+
}
|
|
682
|
+
function keyName(key) {
|
|
683
|
+
const value = isScalar(key) ? key.value : key;
|
|
684
|
+
return typeof value === "string" ? value : null;
|
|
685
|
+
}
|
|
686
|
+
var PRESERVED_KEYS = /* @__PURE__ */ new Set([
|
|
687
|
+
"ai_context",
|
|
688
|
+
"primary_key",
|
|
689
|
+
"tags",
|
|
690
|
+
"labels",
|
|
691
|
+
"owner",
|
|
692
|
+
"synonyms",
|
|
693
|
+
"grain",
|
|
694
|
+
"params",
|
|
695
|
+
"time_dimension",
|
|
696
|
+
"default_time_dimension"
|
|
697
|
+
]);
|
|
698
|
+
function blockingKeys(keys) {
|
|
699
|
+
return keys.filter((key) => !PRESERVED_KEYS.has(key));
|
|
700
|
+
}
|
|
701
|
+
function preservedKeys(keys) {
|
|
702
|
+
return keys.filter((key) => PRESERVED_KEYS.has(key));
|
|
703
|
+
}
|
|
704
|
+
function unsupportedKeys(item, owned) {
|
|
705
|
+
return item.items.map((pair) => keyName(pair.key)).filter((key) => key !== null && !owned.has(key));
|
|
706
|
+
}
|
|
707
|
+
function readDatasets(doc) {
|
|
708
|
+
return mapsOf(seqOf(doc, "datasets")).flatMap((item) => {
|
|
709
|
+
const id = str(item.get("id"));
|
|
710
|
+
if (!id) return [];
|
|
711
|
+
return [
|
|
712
|
+
{
|
|
713
|
+
id,
|
|
714
|
+
name: str(item.get("name")),
|
|
715
|
+
dataStoreId: str(item.get("data_store_id")),
|
|
716
|
+
source: str(item.get("source")) ?? str(item.get("from")),
|
|
717
|
+
sourceType: str(item.get("source_type")),
|
|
718
|
+
filterField: str(item.get("filter_field")),
|
|
719
|
+
dimensions: readDimensions(item),
|
|
720
|
+
metrics: readMetrics(item),
|
|
721
|
+
rollup: readRollup(item),
|
|
722
|
+
unsupportedKeys: unsupportedKeys(item, DATASET_KEYS)
|
|
723
|
+
}
|
|
724
|
+
];
|
|
725
|
+
});
|
|
726
|
+
}
|
|
727
|
+
var CARDINALITIES = [
|
|
728
|
+
"one_to_one",
|
|
729
|
+
"many_to_one",
|
|
730
|
+
"one_to_many",
|
|
731
|
+
"many_to_many"
|
|
732
|
+
];
|
|
733
|
+
function readRelationships(doc) {
|
|
734
|
+
return mapsOf(seqOf(doc, "relationships")).flatMap((item) => {
|
|
735
|
+
const name = str(item.get("name"));
|
|
736
|
+
if (!name) return [];
|
|
737
|
+
const cardinality = str(item.get("relationship"));
|
|
738
|
+
return [
|
|
739
|
+
{
|
|
740
|
+
name,
|
|
741
|
+
from: str(item.get("from")),
|
|
742
|
+
to: str(item.get("to")),
|
|
743
|
+
fromColumns: stringList(item.get("from_columns", true)),
|
|
744
|
+
toColumns: stringList(item.get("to_columns", true)),
|
|
745
|
+
cardinality: CARDINALITIES.includes(cardinality ?? "") ? cardinality : null,
|
|
746
|
+
unsupportedKeys: unsupportedKeys(item, RELATIONSHIP_KEYS)
|
|
747
|
+
}
|
|
748
|
+
];
|
|
749
|
+
});
|
|
750
|
+
}
|
|
751
|
+
function readModelMetrics(doc) {
|
|
752
|
+
return mapsOf(seqOf(doc, "metrics")).flatMap((item) => {
|
|
753
|
+
const id = str(item.get("id"));
|
|
754
|
+
if (!id) return [];
|
|
755
|
+
const expression = item.get("expression");
|
|
756
|
+
return [
|
|
757
|
+
{
|
|
758
|
+
id,
|
|
759
|
+
label: str(item.get("name")) ?? str(item.get("label")),
|
|
760
|
+
description: str(item.get("description")),
|
|
761
|
+
// A per-dialect expression map is beyond this editor: an empty
|
|
762
|
+
// expression is how the panel knows to stay read-only.
|
|
763
|
+
expression: typeof expression === "string" ? expression : "",
|
|
764
|
+
type: item.get("type") === "formula" ? "formula" : "sql",
|
|
765
|
+
format: str(item.get("format")),
|
|
766
|
+
unsupportedKeys: unsupportedKeys(item, MODEL_METRIC_KEYS)
|
|
767
|
+
}
|
|
768
|
+
];
|
|
769
|
+
});
|
|
770
|
+
}
|
|
771
|
+
function modelText(doc) {
|
|
772
|
+
return doc.toString({ lineWidth: 0, flowCollectionPadding: false });
|
|
773
|
+
}
|
|
774
|
+
function ensureSeq(doc, key) {
|
|
775
|
+
const existing = seqOf(doc, key);
|
|
776
|
+
if (existing) return existing;
|
|
777
|
+
doc.set(key, doc.createNode([]));
|
|
778
|
+
const created = seqOf(doc, key);
|
|
779
|
+
if (!created) throw new Error(`Could not create ${key} in the model`);
|
|
780
|
+
return created;
|
|
781
|
+
}
|
|
782
|
+
function upsertDataStore(specYaml, store, originalId) {
|
|
783
|
+
const { doc } = parseModelDocument(specYaml);
|
|
784
|
+
if (!doc) return specYaml;
|
|
785
|
+
const seq = ensureSeq(doc, "data_stores");
|
|
786
|
+
const existing = findById(seq, originalId ?? store.id);
|
|
787
|
+
const target = existing ?? doc.createNode({ id: store.id });
|
|
788
|
+
target.set("id", store.id);
|
|
789
|
+
setOrDelete(target, "name", store.name);
|
|
790
|
+
setOrDelete(target, "vendor", store.vendor);
|
|
791
|
+
setOrDelete(target, "data_model_id", store.dataModelId);
|
|
792
|
+
if (store.isDefault) {
|
|
793
|
+
target.set("default", true);
|
|
794
|
+
for (const other of mapsOf(seq)) {
|
|
795
|
+
if (other !== target && other.get("default") === true)
|
|
796
|
+
other.delete("default");
|
|
797
|
+
}
|
|
798
|
+
} else if (target.has("default")) {
|
|
799
|
+
target.delete("default");
|
|
800
|
+
}
|
|
801
|
+
if (!existing) seq.add(target);
|
|
802
|
+
return modelText(doc);
|
|
803
|
+
}
|
|
804
|
+
function removeDataStore(specYaml, id) {
|
|
805
|
+
const { doc } = parseModelDocument(specYaml);
|
|
806
|
+
if (!doc) return specYaml;
|
|
807
|
+
const seq = seqOf(doc, "data_stores");
|
|
808
|
+
if (!seq) return specYaml;
|
|
809
|
+
const index = seq.items.findIndex(
|
|
810
|
+
(item) => isMap(item) && item.get("id") === id
|
|
811
|
+
);
|
|
812
|
+
if (index >= 0) seq.delete(index);
|
|
813
|
+
return modelText(doc);
|
|
814
|
+
}
|
|
815
|
+
function dimensionNode(doc, dimension) {
|
|
816
|
+
const node = {
|
|
817
|
+
id: dimension.id,
|
|
818
|
+
type: dimension.type
|
|
819
|
+
};
|
|
820
|
+
if (dimension.column && dimension.column !== dimension.id) {
|
|
821
|
+
node.column = dimension.column;
|
|
822
|
+
}
|
|
823
|
+
if (dimension.isTime) node.is_time = true;
|
|
824
|
+
const created = doc.createNode(node);
|
|
825
|
+
if (!isMap(created)) throw new Error("Could not create a dimension");
|
|
826
|
+
created.flow = false;
|
|
827
|
+
return created;
|
|
828
|
+
}
|
|
829
|
+
function metricNode(doc, metric) {
|
|
830
|
+
const node = {
|
|
831
|
+
id: metric.id,
|
|
832
|
+
type: metric.type,
|
|
833
|
+
expression: metric.expression
|
|
834
|
+
};
|
|
835
|
+
if (metric.label) node.label = metric.label;
|
|
836
|
+
const created = doc.createNode(node);
|
|
837
|
+
if (!isMap(created)) throw new Error("Could not create a metric");
|
|
838
|
+
created.flow = false;
|
|
839
|
+
return created;
|
|
840
|
+
}
|
|
841
|
+
function upsertDataset(specYaml, dataset, originalId) {
|
|
842
|
+
const { doc } = parseModelDocument(specYaml);
|
|
843
|
+
if (!doc) return specYaml;
|
|
844
|
+
const seq = ensureSeq(doc, "datasets");
|
|
845
|
+
const existing = findById(seq, originalId ?? dataset.id);
|
|
846
|
+
const target = existing ?? doc.createNode({ id: dataset.id });
|
|
847
|
+
target.set("id", dataset.id);
|
|
848
|
+
setOrDelete(target, "name", dataset.name);
|
|
849
|
+
setOrDelete(target, "data_store_id", dataset.dataStoreId);
|
|
850
|
+
if (target.has("from") && !target.has("source")) {
|
|
851
|
+
setOrDelete(target, "from", dataset.source);
|
|
852
|
+
} else {
|
|
853
|
+
setOrDelete(target, "source", dataset.source);
|
|
854
|
+
}
|
|
855
|
+
setOrDelete(target, "source_type", dataset.sourceType);
|
|
856
|
+
setOrDelete(target, "filter_field", dataset.filterField);
|
|
857
|
+
const dimensionsKey = target.has("fields") && !target.has("dimensions") ? "fields" : "dimensions";
|
|
858
|
+
if (dataset.dimensions.length > 0) {
|
|
859
|
+
target.set(
|
|
860
|
+
dimensionsKey,
|
|
861
|
+
doc.createNode(dataset.dimensions.map((d) => dimensionNode(doc, d)))
|
|
862
|
+
);
|
|
863
|
+
} else if (target.has(dimensionsKey)) {
|
|
864
|
+
target.delete(dimensionsKey);
|
|
865
|
+
}
|
|
866
|
+
if (dataset.metrics.length > 0) {
|
|
867
|
+
target.set(
|
|
868
|
+
"metrics",
|
|
869
|
+
doc.createNode(dataset.metrics.map((m) => metricNode(doc, m)))
|
|
870
|
+
);
|
|
871
|
+
} else if (target.has("metrics")) {
|
|
872
|
+
target.delete("metrics");
|
|
873
|
+
}
|
|
874
|
+
if (dataset.rollup) {
|
|
875
|
+
const rollup = { enabled: true };
|
|
876
|
+
if (dataset.rollup.source) rollup.source = dataset.rollup.source;
|
|
877
|
+
rollup.grain = dataset.rollup.grain;
|
|
878
|
+
rollup.metrics = dataset.rollup.metrics;
|
|
879
|
+
target.set("materialization", doc.createNode(rollup));
|
|
880
|
+
} else if (target.has("materialization")) {
|
|
881
|
+
target.delete("materialization");
|
|
882
|
+
}
|
|
883
|
+
if (!existing) seq.add(target);
|
|
884
|
+
return modelText(doc);
|
|
885
|
+
}
|
|
886
|
+
function removeDataset(specYaml, id) {
|
|
887
|
+
const { doc } = parseModelDocument(specYaml);
|
|
888
|
+
if (!doc) return specYaml;
|
|
889
|
+
const seq = seqOf(doc, "datasets");
|
|
890
|
+
if (!seq) return specYaml;
|
|
891
|
+
const index = seq.items.findIndex(
|
|
892
|
+
(item) => isMap(item) && item.get("id") === id
|
|
893
|
+
);
|
|
894
|
+
if (index >= 0) seq.delete(index);
|
|
895
|
+
return modelText(doc);
|
|
896
|
+
}
|
|
897
|
+
function upsertRelationship(specYaml, relationship, originalName) {
|
|
898
|
+
const { doc } = parseModelDocument(specYaml);
|
|
899
|
+
if (!doc) return specYaml;
|
|
900
|
+
const seq = ensureSeq(doc, "relationships");
|
|
901
|
+
const existing = findByKey(seq, "name", originalName ?? relationship.name);
|
|
902
|
+
const target = existing ?? doc.createNode({ name: relationship.name });
|
|
903
|
+
target.set("name", relationship.name);
|
|
904
|
+
setOrDelete(target, "from", relationship.from);
|
|
905
|
+
setOrDelete(target, "to", relationship.to);
|
|
906
|
+
setStringSeq(doc, target, "from_columns", relationship.fromColumns);
|
|
907
|
+
setStringSeq(doc, target, "to_columns", relationship.toColumns);
|
|
908
|
+
setOrDelete(target, "relationship", relationship.cardinality);
|
|
909
|
+
if (!existing) seq.add(target);
|
|
910
|
+
return modelText(doc);
|
|
911
|
+
}
|
|
912
|
+
function removeRelationship(specYaml, name) {
|
|
913
|
+
const { doc } = parseModelDocument(specYaml);
|
|
914
|
+
if (!doc) return specYaml;
|
|
915
|
+
const seq = seqOf(doc, "relationships");
|
|
916
|
+
if (!seq) return specYaml;
|
|
917
|
+
const index = seq.items.findIndex(
|
|
918
|
+
(item) => isMap(item) && item.get("name") === name
|
|
919
|
+
);
|
|
920
|
+
if (index >= 0) seq.delete(index);
|
|
921
|
+
return modelText(doc);
|
|
922
|
+
}
|
|
923
|
+
function upsertModelMetric(specYaml, metric, originalId) {
|
|
924
|
+
const { doc } = parseModelDocument(specYaml);
|
|
925
|
+
if (!doc) return specYaml;
|
|
926
|
+
const seq = ensureSeq(doc, "metrics");
|
|
927
|
+
const existing = findById(seq, originalId ?? metric.id);
|
|
928
|
+
const target = existing ?? doc.createNode({ id: metric.id });
|
|
929
|
+
target.set("id", metric.id);
|
|
930
|
+
if (target.has("label") && !target.has("name")) {
|
|
931
|
+
setOrDelete(target, "label", metric.label);
|
|
932
|
+
} else {
|
|
933
|
+
setOrDelete(target, "name", metric.label);
|
|
934
|
+
}
|
|
935
|
+
setOrDelete(target, "description", metric.description);
|
|
936
|
+
target.set("type", metric.type);
|
|
937
|
+
target.set("expression", metric.expression);
|
|
938
|
+
setOrDelete(target, "format", metric.format);
|
|
939
|
+
if (!existing) seq.add(target);
|
|
940
|
+
return modelText(doc);
|
|
941
|
+
}
|
|
942
|
+
function removeModelMetric(specYaml, id) {
|
|
943
|
+
const { doc } = parseModelDocument(specYaml);
|
|
944
|
+
if (!doc) return specYaml;
|
|
945
|
+
const seq = seqOf(doc, "metrics");
|
|
946
|
+
if (!seq) return specYaml;
|
|
947
|
+
const index = seq.items.findIndex(
|
|
948
|
+
(item) => isMap(item) && item.get("id") === id
|
|
949
|
+
);
|
|
950
|
+
if (index >= 0) seq.delete(index);
|
|
951
|
+
return modelText(doc);
|
|
952
|
+
}
|
|
953
|
+
function ratioExpression(numerator, denominator) {
|
|
954
|
+
const top = `${numerator.aggregation}(${numerator.dataset}.${numerator.column})`;
|
|
955
|
+
const bottom = `${denominator.aggregation}(${denominator.dataset}.${denominator.column})`;
|
|
956
|
+
return `${top} / NULLIF(${bottom}, 0)`;
|
|
957
|
+
}
|
|
958
|
+
var RATIO_PATTERN = /^\s*(\w+)\(([\w$]+)\.([\w$]+)\)\s*\/\s*NULLIF\(\s*(\w+)\(([\w$]+)\.([\w$]+)\)\s*,\s*0\s*\)\s*(\*\s*100)?\s*$/i;
|
|
959
|
+
function parseRatioExpression(expression) {
|
|
960
|
+
const match = RATIO_PATTERN.exec(expression);
|
|
961
|
+
if (!match) return null;
|
|
962
|
+
const topAggregation = match[1];
|
|
963
|
+
const topDataset = match[2];
|
|
964
|
+
const topColumn = match[3];
|
|
965
|
+
const bottomAggregation = match[4];
|
|
966
|
+
const bottomDataset = match[5];
|
|
967
|
+
const bottomColumn = match[6];
|
|
968
|
+
if (topAggregation === void 0 || topDataset === void 0 || topColumn === void 0 || bottomAggregation === void 0 || bottomDataset === void 0 || bottomColumn === void 0) {
|
|
969
|
+
return null;
|
|
970
|
+
}
|
|
971
|
+
const topAgg = topAggregation.toUpperCase();
|
|
972
|
+
const bottomAgg = bottomAggregation.toUpperCase();
|
|
973
|
+
if (!AGGREGATIONS.includes(topAgg) || !AGGREGATIONS.includes(bottomAgg)) {
|
|
974
|
+
return null;
|
|
975
|
+
}
|
|
976
|
+
return {
|
|
977
|
+
numerator: {
|
|
978
|
+
aggregation: topAgg,
|
|
979
|
+
dataset: topDataset,
|
|
980
|
+
column: topColumn
|
|
981
|
+
},
|
|
982
|
+
denominator: {
|
|
983
|
+
aggregation: bottomAgg,
|
|
984
|
+
dataset: bottomDataset,
|
|
985
|
+
column: bottomColumn
|
|
986
|
+
},
|
|
987
|
+
scaledBy100: Boolean(match[7])
|
|
988
|
+
};
|
|
989
|
+
}
|
|
990
|
+
function isDoubleScaledPercent(expression, format) {
|
|
991
|
+
if (format !== "percent") return false;
|
|
992
|
+
return /\*\s*100\s*$/.test(expression.trim());
|
|
993
|
+
}
|
|
994
|
+
function fragmentOf(specYaml, collection, value, key = "id") {
|
|
995
|
+
const { doc } = parseModelDocument(specYaml);
|
|
996
|
+
if (!doc) return "";
|
|
997
|
+
const item = findByKey(seqOf(doc, collection), key, value);
|
|
998
|
+
if (!item) return "";
|
|
999
|
+
return modelText(new Document(item)).trimEnd();
|
|
1000
|
+
}
|
|
1001
|
+
function datasetFragment(specYaml, id) {
|
|
1002
|
+
return fragmentOf(specYaml, "datasets", id);
|
|
1003
|
+
}
|
|
1004
|
+
function dataStoreFragment(specYaml, id) {
|
|
1005
|
+
return fragmentOf(specYaml, "data_stores", id);
|
|
1006
|
+
}
|
|
1007
|
+
function relationshipFragment(specYaml, name) {
|
|
1008
|
+
return fragmentOf(specYaml, "relationships", name, "name");
|
|
1009
|
+
}
|
|
1010
|
+
function modelMetricFragment(specYaml, id) {
|
|
1011
|
+
return fragmentOf(specYaml, "metrics", id);
|
|
1012
|
+
}
|
|
1013
|
+
function readModelOutline(specYaml) {
|
|
1014
|
+
const { doc, error } = parseModelDocument(specYaml);
|
|
1015
|
+
if (!doc) {
|
|
1016
|
+
return {
|
|
1017
|
+
dataStores: [],
|
|
1018
|
+
datasets: [],
|
|
1019
|
+
relationships: [],
|
|
1020
|
+
modelMetrics: [],
|
|
1021
|
+
error
|
|
1022
|
+
};
|
|
1023
|
+
}
|
|
1024
|
+
return {
|
|
1025
|
+
dataStores: readDataStores(doc),
|
|
1026
|
+
datasets: readDatasets(doc),
|
|
1027
|
+
relationships: readRelationships(doc),
|
|
1028
|
+
modelMetrics: readModelMetrics(doc),
|
|
1029
|
+
error: null
|
|
1030
|
+
};
|
|
1031
|
+
}
|
|
1032
|
+
|
|
1033
|
+
// ../dashboard-core/src/model-builder-checks.ts
|
|
1034
|
+
function storeOf(dataset, stores) {
|
|
1035
|
+
if (dataset.dataStoreId) return dataset.dataStoreId;
|
|
1036
|
+
return stores.find((s) => s.isDefault)?.id ?? stores[0]?.id ?? null;
|
|
1037
|
+
}
|
|
1038
|
+
function relationshipIssues(relationship, datasets, stores) {
|
|
1039
|
+
const issues = [];
|
|
1040
|
+
if (!relationship.name.trim()) issues.push("This join has no name.");
|
|
1041
|
+
const from = datasets.find((d) => d.id === relationship.from);
|
|
1042
|
+
const to = datasets.find((d) => d.id === relationship.to);
|
|
1043
|
+
if (!relationship.from || !relationship.to) {
|
|
1044
|
+
issues.push("Pick the dataset on each side of the join.");
|
|
1045
|
+
} else if (!from) {
|
|
1046
|
+
issues.push(`No dataset called ${relationship.from}.`);
|
|
1047
|
+
} else if (!to) {
|
|
1048
|
+
issues.push(`No dataset called ${relationship.to}.`);
|
|
1049
|
+
} else if (relationship.from === relationship.to) {
|
|
1050
|
+
issues.push("Both sides are the same dataset, so this join does nothing.");
|
|
1051
|
+
}
|
|
1052
|
+
if (relationship.fromColumns.length === 0 || relationship.toColumns.length === 0) {
|
|
1053
|
+
issues.push("A join needs at least one pair of columns.");
|
|
1054
|
+
} else if (relationship.fromColumns.length !== relationship.toColumns.length) {
|
|
1055
|
+
issues.push(
|
|
1056
|
+
"The columns are matched in pairs, so both sides need the same number."
|
|
1057
|
+
);
|
|
1058
|
+
}
|
|
1059
|
+
if (from && to && storeOf(from, stores) !== storeOf(to, stores) && relationship.from !== relationship.to) {
|
|
1060
|
+
issues.push(
|
|
1061
|
+
`${from.id} and ${to.id} read different data stores, so this join cannot run. Combine them in a mesh dataset instead.`
|
|
1062
|
+
);
|
|
1063
|
+
}
|
|
1064
|
+
return issues;
|
|
1065
|
+
}
|
|
1066
|
+
function datasetsInExpression(expression, datasetIds) {
|
|
1067
|
+
return datasetIds.filter(
|
|
1068
|
+
(id) => new RegExp(`(^|[^\\w.])${id}\\.`, "i").test(expression)
|
|
1069
|
+
);
|
|
1070
|
+
}
|
|
1071
|
+
function datasetsAreJoined(a, b, relationships) {
|
|
1072
|
+
const seen = /* @__PURE__ */ new Set([a]);
|
|
1073
|
+
const queue = [a];
|
|
1074
|
+
while (queue.length > 0) {
|
|
1075
|
+
const current = queue.shift();
|
|
1076
|
+
if (current === b) return true;
|
|
1077
|
+
for (const relationship of relationships) {
|
|
1078
|
+
for (const [left, right] of [
|
|
1079
|
+
[relationship.from, relationship.to],
|
|
1080
|
+
[relationship.to, relationship.from]
|
|
1081
|
+
]) {
|
|
1082
|
+
if (left === current && right && !seen.has(right)) {
|
|
1083
|
+
seen.add(right);
|
|
1084
|
+
queue.push(right);
|
|
1085
|
+
}
|
|
1086
|
+
}
|
|
1087
|
+
}
|
|
1088
|
+
}
|
|
1089
|
+
return seen.has(b);
|
|
1090
|
+
}
|
|
1091
|
+
function modelMetricIssues(metric, datasets, relationships) {
|
|
1092
|
+
const issues = [];
|
|
1093
|
+
if (!metric.id.trim())
|
|
1094
|
+
issues.push("This metric has no id for widgets to ask for.");
|
|
1095
|
+
if (!metric.expression.trim()) {
|
|
1096
|
+
issues.push("This metric has no plain expression, so edit it in YAML.");
|
|
1097
|
+
return issues;
|
|
1098
|
+
}
|
|
1099
|
+
const ids = datasets.map((d) => d.id);
|
|
1100
|
+
const referenced = datasetsInExpression(metric.expression, ids);
|
|
1101
|
+
const qualified = [
|
|
1102
|
+
...new Set(
|
|
1103
|
+
Array.from(metric.expression.matchAll(/(?:^|[^\w.])([A-Za-z_]\w*)\./g)).map((match) => match[1]).filter((name) => name !== void 0)
|
|
1104
|
+
)
|
|
1105
|
+
];
|
|
1106
|
+
for (const name of qualified) {
|
|
1107
|
+
if (!ids.includes(name)) {
|
|
1108
|
+
issues.push(`${name} is not a dataset in this model.`);
|
|
1109
|
+
}
|
|
1110
|
+
}
|
|
1111
|
+
if (referenced.length === 0) {
|
|
1112
|
+
issues.push(
|
|
1113
|
+
"Qualify each column with its dataset, for example daily_sales.NET_SALES, so the engine knows where to read it."
|
|
1114
|
+
);
|
|
1115
|
+
}
|
|
1116
|
+
const first = referenced[0];
|
|
1117
|
+
for (const other of referenced.slice(1)) {
|
|
1118
|
+
if (first !== void 0 && !datasetsAreJoined(first, other, relationships)) {
|
|
1119
|
+
issues.push(
|
|
1120
|
+
`${first} and ${other} are not connected by any relationship, so this metric cannot be computed.`
|
|
1121
|
+
);
|
|
1122
|
+
}
|
|
1123
|
+
}
|
|
1124
|
+
if (isDoubleScaledPercent(metric.expression, metric.format)) {
|
|
1125
|
+
issues.push(
|
|
1126
|
+
"This multiplies by 100 and the percent format multiplies by 100 again, so a share of 0.133 reads 1,333%. Drop the * 100 and keep the percent format."
|
|
1127
|
+
);
|
|
1128
|
+
}
|
|
1129
|
+
return issues;
|
|
1130
|
+
}
|
|
1131
|
+
var MODEL_TEMPLATES = [
|
|
1132
|
+
{
|
|
1133
|
+
id: "data_store",
|
|
1134
|
+
label: "Data store",
|
|
1135
|
+
section: "data_stores",
|
|
1136
|
+
description: "A warehouse connection. Leave data_model_id out to use the host's default store.",
|
|
1137
|
+
yaml: ` - id: ops_dw
|
|
1138
|
+
name: Operations warehouse
|
|
1139
|
+
vendor: snowflake
|
|
1140
|
+
default: true
|
|
1141
|
+
`
|
|
1142
|
+
},
|
|
1143
|
+
{
|
|
1144
|
+
id: "dataset_table",
|
|
1145
|
+
label: "Dataset from a table",
|
|
1146
|
+
section: "datasets",
|
|
1147
|
+
description: "A table or view with the dimensions and metrics widgets may ask for.",
|
|
1148
|
+
yaml: ` - id: daily_sales
|
|
1149
|
+
name: Daily sales
|
|
1150
|
+
data_store_id: ops_dw
|
|
1151
|
+
source_type: table
|
|
1152
|
+
source: "{mart_schema}.RPT_DAILY_SALES_SUMMARY"
|
|
1153
|
+
filter_field: store_key
|
|
1154
|
+
dimensions:
|
|
1155
|
+
- {id: business_date, type: date, is_time: true}
|
|
1156
|
+
- {id: store_key, type: string}
|
|
1157
|
+
metrics:
|
|
1158
|
+
- {id: net_sales, type: sql, expression: SUM(NET_SALES)}
|
|
1159
|
+
- {id: order_count, type: sql, expression: SUM(ORDER_COUNT)}
|
|
1160
|
+
`
|
|
1161
|
+
},
|
|
1162
|
+
{
|
|
1163
|
+
id: "dataset_query",
|
|
1164
|
+
label: "Dataset from SQL",
|
|
1165
|
+
section: "datasets",
|
|
1166
|
+
description: "Use when no single table has the shape you need.",
|
|
1167
|
+
yaml: ` - id: sales_by_daypart
|
|
1168
|
+
data_store_id: ops_dw
|
|
1169
|
+
source_type: query
|
|
1170
|
+
filter_field: store_key
|
|
1171
|
+
query: |
|
|
1172
|
+
SELECT store_key, business_date, daypart, SUM(net_sales) AS net_sales
|
|
1173
|
+
FROM {mart_schema}.FCT_ORDERS
|
|
1174
|
+
GROUP BY 1, 2, 3
|
|
1175
|
+
dimensions:
|
|
1176
|
+
- {id: business_date, type: date, is_time: true}
|
|
1177
|
+
- {id: daypart, type: string}
|
|
1178
|
+
metrics:
|
|
1179
|
+
- {id: net_sales, type: sql, expression: SUM(net_sales)}
|
|
1180
|
+
`
|
|
1181
|
+
},
|
|
1182
|
+
{
|
|
1183
|
+
id: "relationship",
|
|
1184
|
+
label: "Relationship (join)",
|
|
1185
|
+
section: "relationships",
|
|
1186
|
+
description: "Lets one widget mix fields from both datasets. Column lists are matched in order.",
|
|
1187
|
+
yaml: ` - name: sales_to_labor
|
|
1188
|
+
from: daily_sales
|
|
1189
|
+
to: daily_labor
|
|
1190
|
+
from_columns: [store_key, business_date]
|
|
1191
|
+
to_columns: [store_key, business_date]
|
|
1192
|
+
relationship: one_to_one
|
|
1193
|
+
`
|
|
1194
|
+
},
|
|
1195
|
+
{
|
|
1196
|
+
id: "metric_formula",
|
|
1197
|
+
label: "Metric across datasets",
|
|
1198
|
+
section: "metrics",
|
|
1199
|
+
description: "A formula over dataset metrics, computed after the query. Needs a relationship when it spans datasets.",
|
|
1200
|
+
yaml: ` - id: labor_pct
|
|
1201
|
+
name: Labor percent
|
|
1202
|
+
type: formula
|
|
1203
|
+
expression: "daily_labor.labor_cost / daily_sales.net_sales"
|
|
1204
|
+
format: percent
|
|
1205
|
+
`
|
|
1206
|
+
}
|
|
1207
|
+
];
|
|
1208
|
+
function insertModelTemplate(specYaml, template) {
|
|
1209
|
+
const block = template.yaml.endsWith("\n") ? template.yaml : `${template.yaml}
|
|
1210
|
+
`;
|
|
1211
|
+
const lineCounter = new LineCounter();
|
|
1212
|
+
const doc = parseDocument(specYaml, { lineCounter });
|
|
1213
|
+
const section = doc.get(template.section, true);
|
|
1214
|
+
const text2 = specYaml.endsWith("\n") ? specYaml : `${specYaml}
|
|
1215
|
+
`;
|
|
1216
|
+
if (isSeq(section) && section.items.length > 0) {
|
|
1217
|
+
const last = section.items[section.items.length - 1];
|
|
1218
|
+
const range = last.range;
|
|
1219
|
+
if (range) {
|
|
1220
|
+
const at = range[2];
|
|
1221
|
+
const head = text2.slice(0, at);
|
|
1222
|
+
return `${head.endsWith("\n") ? head : `${head}
|
|
1223
|
+
`}${block}${text2.slice(at)}`;
|
|
1224
|
+
}
|
|
1225
|
+
}
|
|
1226
|
+
const emptySection = new RegExp(
|
|
1227
|
+
`^${template.section}:[ \\t]*(\\[\\s*\\])?[ \\t]*$`,
|
|
1228
|
+
"m"
|
|
1229
|
+
);
|
|
1230
|
+
if (emptySection.test(text2)) {
|
|
1231
|
+
return text2.replace(
|
|
1232
|
+
emptySection,
|
|
1233
|
+
`${template.section}:
|
|
1234
|
+
${block.trimEnd()}`
|
|
1235
|
+
);
|
|
1236
|
+
}
|
|
1237
|
+
return `${text2}${template.section}:
|
|
1238
|
+
${block}`;
|
|
1239
|
+
}
|
|
1240
|
+
var KEYED_BY_ID = ["data_stores", "datasets", "metrics"];
|
|
1241
|
+
function positionOf(node, lineCounter) {
|
|
1242
|
+
if (!node.range) return null;
|
|
1243
|
+
const [start, valueEnd] = node.range;
|
|
1244
|
+
const startPos = lineCounter.linePos(start);
|
|
1245
|
+
const endPos = lineCounter.linePos(valueEnd);
|
|
1246
|
+
return {
|
|
1247
|
+
line: startPos.line,
|
|
1248
|
+
column: startPos.col,
|
|
1249
|
+
endLine: endPos.line,
|
|
1250
|
+
endColumn: endPos.col
|
|
1251
|
+
};
|
|
1252
|
+
}
|
|
1253
|
+
function indexModelYaml(specYaml) {
|
|
1254
|
+
const lineCounter = new LineCounter();
|
|
1255
|
+
const doc = parseDocument(specYaml, { lineCounter, keepSourceTokens: true });
|
|
1256
|
+
const index = /* @__PURE__ */ new Map();
|
|
1257
|
+
if (!isMap(doc.contents)) return index;
|
|
1258
|
+
for (const collection of [...KEYED_BY_ID, "relationships"]) {
|
|
1259
|
+
const seq = doc.get(collection, true);
|
|
1260
|
+
if (!isSeq(seq)) continue;
|
|
1261
|
+
for (const item of seq.items) {
|
|
1262
|
+
if (!isMap(item)) continue;
|
|
1263
|
+
const keyNode = collection === "relationships" ? "name" : "id";
|
|
1264
|
+
const id = item.get(keyNode);
|
|
1265
|
+
if (typeof id !== "string") continue;
|
|
1266
|
+
const position = positionOf(item, lineCounter);
|
|
1267
|
+
if (position) index.set(`${collection}:${id}`, position);
|
|
1268
|
+
}
|
|
1269
|
+
}
|
|
1270
|
+
return index;
|
|
1271
|
+
}
|
|
1272
|
+
function yamlSyntaxMarkers(specYaml) {
|
|
1273
|
+
const lineCounter = new LineCounter();
|
|
1274
|
+
const doc = parseDocument(specYaml, { lineCounter });
|
|
1275
|
+
return doc.errors.map((err) => {
|
|
1276
|
+
const start = lineCounter.linePos(err.pos[0]);
|
|
1277
|
+
const end = lineCounter.linePos(err.pos[1]);
|
|
1278
|
+
return {
|
|
1279
|
+
startLineNumber: start.line,
|
|
1280
|
+
startColumn: start.col,
|
|
1281
|
+
endLineNumber: end.line,
|
|
1282
|
+
endColumn: end.col,
|
|
1283
|
+
message: err.message,
|
|
1284
|
+
severity: "error"
|
|
1285
|
+
};
|
|
1286
|
+
});
|
|
1287
|
+
}
|
|
1288
|
+
function marker(position, message, severity) {
|
|
1289
|
+
const at = position ?? { line: 1, column: 1, endLine: 1, endColumn: 1 };
|
|
1290
|
+
return {
|
|
1291
|
+
startLineNumber: at.line,
|
|
1292
|
+
startColumn: at.column,
|
|
1293
|
+
// Underlining a whole dataset block buries the code in red, so mark its
|
|
1294
|
+
// first line only.
|
|
1295
|
+
endLineNumber: at.line,
|
|
1296
|
+
endColumn: at.line === at.endLine ? at.endColumn : Number.MAX_SAFE_INTEGER,
|
|
1297
|
+
message,
|
|
1298
|
+
severity
|
|
1299
|
+
};
|
|
1300
|
+
}
|
|
1301
|
+
function diagnosticsToMarkers(specYaml, diagnostics) {
|
|
1302
|
+
const index = indexModelYaml(specYaml);
|
|
1303
|
+
const markers = [];
|
|
1304
|
+
for (const issue of diagnostics.model.issues) {
|
|
1305
|
+
markers.push(marker(void 0, issue, "error"));
|
|
1306
|
+
}
|
|
1307
|
+
for (const store of diagnostics.data_stores) {
|
|
1308
|
+
const position = index.get(`data_stores:${store.id}`);
|
|
1309
|
+
for (const issue of store.issues) {
|
|
1310
|
+
markers.push(marker(position, issue, "error"));
|
|
1311
|
+
}
|
|
1312
|
+
for (const note of store.notes ?? []) {
|
|
1313
|
+
markers.push(marker(position, note, "info"));
|
|
1314
|
+
}
|
|
1315
|
+
}
|
|
1316
|
+
for (const dataset of diagnostics.datasets) {
|
|
1317
|
+
const position = index.get(`datasets:${dataset.id}`);
|
|
1318
|
+
for (const issue of dataset.issues) {
|
|
1319
|
+
markers.push(marker(position, issue, "error"));
|
|
1320
|
+
}
|
|
1321
|
+
}
|
|
1322
|
+
for (const relationship of diagnostics.relationships) {
|
|
1323
|
+
const position = index.get(`relationships:${relationship.name}`);
|
|
1324
|
+
for (const issue of relationship.issues) {
|
|
1325
|
+
markers.push(marker(position, issue, "error"));
|
|
1326
|
+
}
|
|
1327
|
+
}
|
|
1328
|
+
for (const metric of diagnostics.metrics) {
|
|
1329
|
+
const id = metric.metric.id;
|
|
1330
|
+
const position = typeof id === "string" ? index.get(`metrics:${id}`) : void 0;
|
|
1331
|
+
for (const issue of metric.issues) {
|
|
1332
|
+
markers.push(marker(position, issue, "error"));
|
|
1333
|
+
}
|
|
1334
|
+
}
|
|
1335
|
+
return markers;
|
|
1336
|
+
}
|
|
1337
|
+
function countModelIssues(diagnostics) {
|
|
1338
|
+
return diagnostics.model.issues.length + diagnostics.data_stores.reduce((n, s) => n + s.issues.length, 0) + diagnostics.datasets.reduce((n, d) => n + d.issues.length, 0) + diagnostics.relationships.reduce((n, r) => n + r.issues.length, 0) + diagnostics.metrics.reduce((n, m) => n + m.issues.length, 0);
|
|
1339
|
+
}
|
|
1340
|
+
|
|
1341
|
+
// ../dashboard-core/src/widget-builder-checks.ts
|
|
1342
|
+
function widgetColumns(datasetId, model) {
|
|
1343
|
+
const base = model.datasets.find((d) => d.id === datasetId);
|
|
1344
|
+
if (!base) return [];
|
|
1345
|
+
const columns = [
|
|
1346
|
+
...base.dimensions.map((dimension) => ({
|
|
1347
|
+
key: dimension.id,
|
|
1348
|
+
label: dimension.id,
|
|
1349
|
+
kind: "dimension",
|
|
1350
|
+
dataset: base.id,
|
|
1351
|
+
joined: false
|
|
1352
|
+
})),
|
|
1353
|
+
...base.metrics.map((metric) => ({
|
|
1354
|
+
key: metric.id,
|
|
1355
|
+
label: metric.label ?? metric.id,
|
|
1356
|
+
kind: "metric",
|
|
1357
|
+
dataset: base.id,
|
|
1358
|
+
joined: false
|
|
1359
|
+
}))
|
|
1360
|
+
];
|
|
1361
|
+
for (const other of model.datasets) {
|
|
1362
|
+
if (other.id === base.id) continue;
|
|
1363
|
+
if (!datasetsAreJoined(base.id, other.id, model.relationships)) continue;
|
|
1364
|
+
for (const metric of other.metrics) {
|
|
1365
|
+
columns.push({
|
|
1366
|
+
key: `${other.id}.${metric.id}`,
|
|
1367
|
+
label: `${metric.label ?? metric.id} (${other.id})`,
|
|
1368
|
+
kind: "metric",
|
|
1369
|
+
dataset: other.id,
|
|
1370
|
+
joined: true
|
|
1371
|
+
});
|
|
1372
|
+
}
|
|
1373
|
+
}
|
|
1374
|
+
const taken = new Set(columns.map((column) => column.key));
|
|
1375
|
+
for (const metric of model.modelMetrics) {
|
|
1376
|
+
if (taken.has(metric.id)) continue;
|
|
1377
|
+
columns.push({
|
|
1378
|
+
key: metric.id,
|
|
1379
|
+
label: metric.label ?? metric.id,
|
|
1380
|
+
kind: "metric",
|
|
1381
|
+
dataset: null,
|
|
1382
|
+
joined: true
|
|
1383
|
+
});
|
|
1384
|
+
}
|
|
1385
|
+
return columns;
|
|
1386
|
+
}
|
|
1387
|
+
function numericColumns(columns) {
|
|
1388
|
+
return columns.filter((column) => column.kind === "metric");
|
|
1389
|
+
}
|
|
1390
|
+
function labelColumns(columns) {
|
|
1391
|
+
return columns.filter((column) => column.kind === "dimension");
|
|
1392
|
+
}
|
|
1393
|
+
function widgetFindings(widget, model) {
|
|
1394
|
+
const findings = [];
|
|
1395
|
+
const add = (message, at) => {
|
|
1396
|
+
findings.push({ message, at });
|
|
1397
|
+
};
|
|
1398
|
+
if (!widget.id.trim()) add("This widget has no id.", { kind: "id" });
|
|
1399
|
+
if (!widget.datasetId) {
|
|
1400
|
+
add("Pick the dataset this widget reads.", { kind: "dataset" });
|
|
1401
|
+
return findings;
|
|
1402
|
+
}
|
|
1403
|
+
const base = model.datasets.find((d) => d.id === widget.datasetId);
|
|
1404
|
+
if (!base) {
|
|
1405
|
+
add(
|
|
1406
|
+
`No dataset called ${widget.datasetId} in the semantic model, so this widget cannot run.`,
|
|
1407
|
+
{ kind: "dataset" }
|
|
1408
|
+
);
|
|
1409
|
+
return findings;
|
|
1410
|
+
}
|
|
1411
|
+
if (widget.columns.length === 0) {
|
|
1412
|
+
add("Pick at least one column, or the query returns nothing.", {
|
|
1413
|
+
kind: "columns"
|
|
1414
|
+
});
|
|
1415
|
+
return findings;
|
|
1416
|
+
}
|
|
1417
|
+
const available = widgetColumns(widget.datasetId, model);
|
|
1418
|
+
const availableKeys = new Set(available.map((column) => column.key));
|
|
1419
|
+
for (const column of widget.columns) {
|
|
1420
|
+
if (availableKeys.has(column)) continue;
|
|
1421
|
+
const [prefix] = column.split(".");
|
|
1422
|
+
if (column.includes(".") && model.datasets.some((d) => d.id === prefix)) {
|
|
1423
|
+
add(
|
|
1424
|
+
`${column} comes from ${prefix}, which no relationship connects to ${base.id}, so the query cannot join it.`,
|
|
1425
|
+
{ kind: "column", key: column }
|
|
1426
|
+
);
|
|
1427
|
+
} else {
|
|
1428
|
+
add(
|
|
1429
|
+
`${column} is not a dimension or metric of ${base.id}, so the query would fail.`,
|
|
1430
|
+
{ kind: "column", key: column }
|
|
1431
|
+
);
|
|
1432
|
+
}
|
|
1433
|
+
}
|
|
1434
|
+
const selected = new Set(widget.columns);
|
|
1435
|
+
function checkPoints(field, what, at) {
|
|
1436
|
+
if (!field) return;
|
|
1437
|
+
if (!selected.has(field)) {
|
|
1438
|
+
add(
|
|
1439
|
+
`${what} reads ${field}, which this widget does not select, so it would draw nothing.`,
|
|
1440
|
+
at
|
|
1441
|
+
);
|
|
1442
|
+
}
|
|
1443
|
+
}
|
|
1444
|
+
checkPoints(widget.mappings.x, "The horizontal axis", {
|
|
1445
|
+
kind: "mapping",
|
|
1446
|
+
slot: "x"
|
|
1447
|
+
});
|
|
1448
|
+
checkPoints(widget.mappings.category, "The category", {
|
|
1449
|
+
kind: "mapping",
|
|
1450
|
+
slot: "category"
|
|
1451
|
+
});
|
|
1452
|
+
checkPoints(widget.mappings.value, "The value", {
|
|
1453
|
+
kind: "mapping",
|
|
1454
|
+
slot: "value"
|
|
1455
|
+
});
|
|
1456
|
+
checkPoints(widget.mappings.series, "The series split", {
|
|
1457
|
+
kind: "mapping",
|
|
1458
|
+
slot: "series"
|
|
1459
|
+
});
|
|
1460
|
+
checkPoints(widget.mappings.delta, "The comparison", {
|
|
1461
|
+
kind: "mapping",
|
|
1462
|
+
slot: "delta"
|
|
1463
|
+
});
|
|
1464
|
+
widget.series.forEach((entry, index) => {
|
|
1465
|
+
checkPoints(entry.field, `Series ${entry.name ?? entry.field}`, {
|
|
1466
|
+
kind: "series",
|
|
1467
|
+
index
|
|
1468
|
+
});
|
|
1469
|
+
});
|
|
1470
|
+
if (widget.component === "echarts") {
|
|
1471
|
+
const axis = widget.chartType === "pie" ? widget.mappings.category : widget.mappings.x;
|
|
1472
|
+
if (!axis) {
|
|
1473
|
+
add(
|
|
1474
|
+
widget.chartType === "pie" ? "Pick the column that names each slice." : "Pick the column that runs along the horizontal axis.",
|
|
1475
|
+
{
|
|
1476
|
+
kind: "mapping",
|
|
1477
|
+
slot: widget.chartType === "pie" ? "category" : "x"
|
|
1478
|
+
}
|
|
1479
|
+
);
|
|
1480
|
+
}
|
|
1481
|
+
if (!widget.mappings.value && widget.series.length === 0) {
|
|
1482
|
+
add("Pick the column to plot, or add at least one series.", {
|
|
1483
|
+
kind: "mapping",
|
|
1484
|
+
slot: "value"
|
|
1485
|
+
});
|
|
1486
|
+
}
|
|
1487
|
+
}
|
|
1488
|
+
const numericKeys = new Set(numericColumns(available).map((c) => c.key));
|
|
1489
|
+
if ((widget.component === "kpi" || widget.component === "echarts") && widget.mappings.value && availableKeys.has(widget.mappings.value) && !numericKeys.has(widget.mappings.value)) {
|
|
1490
|
+
add(
|
|
1491
|
+
`${widget.mappings.value} is a label rather than a measure, so there is no number to ${widget.component === "kpi" ? "show" : "plot"}.`,
|
|
1492
|
+
{ kind: "mapping", slot: "value" }
|
|
1493
|
+
);
|
|
1494
|
+
}
|
|
1495
|
+
if (widget.component === "kpi") {
|
|
1496
|
+
if (!widget.mappings.value) {
|
|
1497
|
+
add("Pick the column the number comes from.", {
|
|
1498
|
+
kind: "mapping",
|
|
1499
|
+
slot: "value"
|
|
1500
|
+
});
|
|
1501
|
+
}
|
|
1502
|
+
if (widget.mappings.delta && widget.mappings.delta === widget.mappings.value) {
|
|
1503
|
+
add(
|
|
1504
|
+
"The comparison reads the same column as the number, so the change would always be zero. Pick another column or leave it unset.",
|
|
1505
|
+
{ kind: "mapping", slot: "delta" }
|
|
1506
|
+
);
|
|
1507
|
+
}
|
|
1508
|
+
const grouping = available.filter(
|
|
1509
|
+
(column) => column.kind === "dimension" && selected.has(column.key)
|
|
1510
|
+
);
|
|
1511
|
+
for (const column of grouping) {
|
|
1512
|
+
add(
|
|
1513
|
+
`A single number cannot group by ${column.label}: the query returns one row per ${column.label} and the card shows only the first. Untick it in step 4.`,
|
|
1514
|
+
{ kind: "column", key: column.key }
|
|
1515
|
+
);
|
|
1516
|
+
}
|
|
1517
|
+
}
|
|
1518
|
+
if (widget.component === "pivot_table") {
|
|
1519
|
+
findings.push(...pivotFindings(widget, available));
|
|
1520
|
+
}
|
|
1521
|
+
return findings;
|
|
1522
|
+
}
|
|
1523
|
+
function pivotFindings(widget, available) {
|
|
1524
|
+
const findings = [];
|
|
1525
|
+
const add = (message, at) => {
|
|
1526
|
+
findings.push({ message, at });
|
|
1527
|
+
};
|
|
1528
|
+
const pivot = widget.pivot;
|
|
1529
|
+
const byKey = new Map(available.map((column) => [column.key, column]));
|
|
1530
|
+
if (pivot.rows.length === 0) {
|
|
1531
|
+
add("Pick at least one field to run down the side of the pivot.", {
|
|
1532
|
+
kind: "pivotRows"
|
|
1533
|
+
});
|
|
1534
|
+
}
|
|
1535
|
+
if (pivot.measures.length === 0) {
|
|
1536
|
+
add("Add at least one measure, or every cell in the pivot is empty.", {
|
|
1537
|
+
kind: "pivotMeasures"
|
|
1538
|
+
});
|
|
1539
|
+
}
|
|
1540
|
+
function checkDimension(field, where, at) {
|
|
1541
|
+
const column = byKey.get(field);
|
|
1542
|
+
if (!column) {
|
|
1543
|
+
add(
|
|
1544
|
+
`${field} is not a dimension or metric of ${widget.datasetId}, so the pivot cannot group ${where} by it.`,
|
|
1545
|
+
at
|
|
1546
|
+
);
|
|
1547
|
+
return;
|
|
1548
|
+
}
|
|
1549
|
+
if (column.kind !== "dimension") {
|
|
1550
|
+
add(
|
|
1551
|
+
`${field} is a measure rather than a dimension, so it cannot run ${where}. Put it under the measures instead.`,
|
|
1552
|
+
at
|
|
1553
|
+
);
|
|
1554
|
+
}
|
|
1555
|
+
}
|
|
1556
|
+
for (const field of pivot.rows) {
|
|
1557
|
+
checkDimension(field, "down the side", { kind: "pivotRows" });
|
|
1558
|
+
if (pivot.columns.includes(field)) {
|
|
1559
|
+
add(
|
|
1560
|
+
`${field} runs down the side and across the top at once, so every figure would fall on one diagonal. Pick it in one place.`,
|
|
1561
|
+
{ kind: "pivotColumns" }
|
|
1562
|
+
);
|
|
1563
|
+
}
|
|
1564
|
+
}
|
|
1565
|
+
for (const field of pivot.columns) {
|
|
1566
|
+
checkDimension(field, "across the top", { kind: "pivotColumns" });
|
|
1567
|
+
}
|
|
1568
|
+
if (pivot.columns.length > 2) {
|
|
1569
|
+
add(
|
|
1570
|
+
"At most two fields can be nested across the top. A third level generates more columns than a grid can read.",
|
|
1571
|
+
{ kind: "pivotColumns" }
|
|
1572
|
+
);
|
|
1573
|
+
}
|
|
1574
|
+
pivot.measures.forEach((measure, index) => {
|
|
1575
|
+
const column = byKey.get(measure.field);
|
|
1576
|
+
if (!column) {
|
|
1577
|
+
add(
|
|
1578
|
+
`${measure.field} is not a dimension or metric of ${widget.datasetId}, so there is no figure to put in the cells.`,
|
|
1579
|
+
{ kind: "pivotMeasure", index }
|
|
1580
|
+
);
|
|
1581
|
+
return;
|
|
1582
|
+
}
|
|
1583
|
+
if (column.kind !== "metric") {
|
|
1584
|
+
add(
|
|
1585
|
+
`${measure.field} is a label rather than a measure, so there is no number to total.`,
|
|
1586
|
+
{ kind: "pivotMeasure", index }
|
|
1587
|
+
);
|
|
1588
|
+
}
|
|
1589
|
+
});
|
|
1590
|
+
const measured = new Set(pivot.measures.map((measure) => measure.field));
|
|
1591
|
+
if (pivot.heatmap && !measured.has(pivot.heatmap)) {
|
|
1592
|
+
add(
|
|
1593
|
+
`The shading reads ${pivot.heatmap}, which is not one of this pivot's measures, so nothing would be shaded.`,
|
|
1594
|
+
{ kind: "pivotHeatmap" }
|
|
1595
|
+
);
|
|
1596
|
+
}
|
|
1597
|
+
if (pivot.sortMeasure && !measured.has(pivot.sortMeasure)) {
|
|
1598
|
+
add(
|
|
1599
|
+
`The rows are ordered by ${pivot.sortMeasure}, which is not one of this pivot's measures, so the order would fall back to the labels.`,
|
|
1600
|
+
{ kind: "pivotSort" }
|
|
1601
|
+
);
|
|
1602
|
+
}
|
|
1603
|
+
if (pivot.topRowsMeasure && !measured.has(pivot.topRowsMeasure)) {
|
|
1604
|
+
add(
|
|
1605
|
+
`The biggest rows are picked by ${pivot.topRowsMeasure}, which is not one of this pivot's measures, so nothing would decide which rows are kept.`,
|
|
1606
|
+
{ kind: "pivotTopRows" }
|
|
1607
|
+
);
|
|
1608
|
+
}
|
|
1609
|
+
return findings;
|
|
1610
|
+
}
|
|
1611
|
+
function widgetIssues(widget, model) {
|
|
1612
|
+
return widgetFindings(widget, model).map((finding) => finding.message);
|
|
1613
|
+
}
|
|
1614
|
+
function text(node) {
|
|
1615
|
+
return typeof node === "string" && node.length > 0 ? node : null;
|
|
1616
|
+
}
|
|
1617
|
+
function mapAt(parent, key) {
|
|
1618
|
+
const node = parent.get(key, true);
|
|
1619
|
+
return isMap(node) ? node : null;
|
|
1620
|
+
}
|
|
1621
|
+
function readDashboardOutline(specYaml) {
|
|
1622
|
+
const empty = {
|
|
1623
|
+
id: null,
|
|
1624
|
+
name: null,
|
|
1625
|
+
tabIds: [],
|
|
1626
|
+
widgetRefs: [],
|
|
1627
|
+
issues: []
|
|
1628
|
+
};
|
|
1629
|
+
const doc = parseDocument(specYaml);
|
|
1630
|
+
const first = doc.errors[0];
|
|
1631
|
+
if (first) {
|
|
1632
|
+
return {
|
|
1633
|
+
...empty,
|
|
1634
|
+
issues: [
|
|
1635
|
+
`Line ${String(first.linePos?.[0].line ?? 1)}: ${first.message.trim()}`
|
|
1636
|
+
]
|
|
1637
|
+
};
|
|
1638
|
+
}
|
|
1639
|
+
if (!isMap(doc.contents)) {
|
|
1640
|
+
return { ...empty, issues: ["A dashboard must be a YAML mapping."] };
|
|
1641
|
+
}
|
|
1642
|
+
const root = doc.contents;
|
|
1643
|
+
const issues = [];
|
|
1644
|
+
if (text(root.get("kind")) !== "dashboard") {
|
|
1645
|
+
issues.push("kind must be dashboard.");
|
|
1646
|
+
}
|
|
1647
|
+
const metadata = mapAt(root, "metadata");
|
|
1648
|
+
const id = metadata ? text(metadata.get("id")) : null;
|
|
1649
|
+
const name = metadata ? text(metadata.get("name")) : null;
|
|
1650
|
+
if (id === null) issues.push("metadata.id is required.");
|
|
1651
|
+
if (name === null) issues.push("metadata.name is required.");
|
|
1652
|
+
const spec = mapAt(root, "spec");
|
|
1653
|
+
const tabs = spec ? spec.get("tabs", true) : null;
|
|
1654
|
+
const tabIds = [];
|
|
1655
|
+
const widgetRefs = [];
|
|
1656
|
+
if (!isSeq(tabs) || tabs.items.length === 0) {
|
|
1657
|
+
issues.push("spec.tabs must list at least one page.");
|
|
1658
|
+
} else {
|
|
1659
|
+
tabs.items.forEach((tab, index) => {
|
|
1660
|
+
if (!isMap(tab)) {
|
|
1661
|
+
issues.push(`Tab ${String(index + 1)} must be a mapping.`);
|
|
1662
|
+
return;
|
|
1663
|
+
}
|
|
1664
|
+
const tabId = text(tab.get("id"));
|
|
1665
|
+
if (tabId === null) {
|
|
1666
|
+
issues.push(`Tab ${String(index + 1)} needs an id.`);
|
|
1667
|
+
} else if (tabIds.includes(tabId)) {
|
|
1668
|
+
issues.push(`Tab id ${tabId} is used twice.`);
|
|
1669
|
+
} else {
|
|
1670
|
+
tabIds.push(tabId);
|
|
1671
|
+
}
|
|
1672
|
+
const widgets = tab.get("widgets", true);
|
|
1673
|
+
if (!isSeq(widgets)) return;
|
|
1674
|
+
for (const item of widgets.items) {
|
|
1675
|
+
const ref = isMap(item) ? text(item.get("ref")) : text(item);
|
|
1676
|
+
if (ref === null) {
|
|
1677
|
+
issues.push(
|
|
1678
|
+
`Tab ${tabId ?? String(index + 1)} has a widget without a ref.`
|
|
1679
|
+
);
|
|
1680
|
+
} else {
|
|
1681
|
+
widgetRefs.push(ref);
|
|
1682
|
+
}
|
|
1683
|
+
}
|
|
1684
|
+
});
|
|
1685
|
+
}
|
|
1686
|
+
return { id, name, tabIds, widgetRefs, issues };
|
|
1687
|
+
}
|
|
1688
|
+
var BUILDER_COMPONENTS = [
|
|
1689
|
+
"echarts",
|
|
1690
|
+
"table",
|
|
1691
|
+
"tabulator",
|
|
1692
|
+
"pivot_table",
|
|
1693
|
+
"kpi"
|
|
1694
|
+
];
|
|
1695
|
+
var ALL_COMPONENTS = [
|
|
1696
|
+
"echarts",
|
|
1697
|
+
"table",
|
|
1698
|
+
"tabulator",
|
|
1699
|
+
"pivot_table",
|
|
1700
|
+
"kpi",
|
|
1701
|
+
"text",
|
|
1702
|
+
"markdown",
|
|
1703
|
+
"image",
|
|
1704
|
+
"filter"
|
|
1705
|
+
];
|
|
1706
|
+
function isBuilderComponent(component) {
|
|
1707
|
+
return BUILDER_COMPONENTS.includes(component);
|
|
1708
|
+
}
|
|
1709
|
+
var CHART_TYPES = ["line", "bar", "area", "pie", "scatter"];
|
|
1710
|
+
var DELTA_FORMATS = ["percent", "number", "currency"];
|
|
1711
|
+
var NUMBER_FORMATS = ["number", "currency", "percent"];
|
|
1712
|
+
var SORT_ORDERS = ["none", "asc", "desc"];
|
|
1713
|
+
var PIVOT_AGGREGATIONS = [
|
|
1714
|
+
"sum",
|
|
1715
|
+
"avg",
|
|
1716
|
+
"min",
|
|
1717
|
+
"max",
|
|
1718
|
+
"count",
|
|
1719
|
+
"count_distinct"
|
|
1720
|
+
];
|
|
1721
|
+
var PIVOT_SUBTOTALS = ["collapsed", "expanded", "none"];
|
|
1722
|
+
var PIVOT_DIRECTIONS = ["asc", "desc"];
|
|
1723
|
+
function str2(node) {
|
|
1724
|
+
return typeof node === "string" && node !== "" ? node : null;
|
|
1725
|
+
}
|
|
1726
|
+
function mapAt2(parent, key) {
|
|
1727
|
+
const node = parent.get(key, true);
|
|
1728
|
+
return isMap(node) ? node : null;
|
|
1729
|
+
}
|
|
1730
|
+
function seqAt(parent, key) {
|
|
1731
|
+
const node = parent.get(key, true);
|
|
1732
|
+
return isSeq(node) ? node : null;
|
|
1733
|
+
}
|
|
1734
|
+
function mapsOf2(seq) {
|
|
1735
|
+
if (!seq) return [];
|
|
1736
|
+
return seq.items.filter((item) => isMap(item));
|
|
1737
|
+
}
|
|
1738
|
+
function readSelectedColumns(data) {
|
|
1739
|
+
const merged = [
|
|
1740
|
+
...stringList2(data.get("columns", true)),
|
|
1741
|
+
...stringList2(data.get("dimensions", true)),
|
|
1742
|
+
...stringList2(data.get("metrics", true))
|
|
1743
|
+
];
|
|
1744
|
+
return [...new Set(merged)];
|
|
1745
|
+
}
|
|
1746
|
+
function stringList2(node) {
|
|
1747
|
+
if (!isSeq(node)) return [];
|
|
1748
|
+
return node.items.flatMap((item) => {
|
|
1749
|
+
const value = isScalar(item) ? item.value : item;
|
|
1750
|
+
return typeof value === "string" ? [value] : [];
|
|
1751
|
+
});
|
|
1752
|
+
}
|
|
1753
|
+
function setStringSeq2(doc, map, key, values) {
|
|
1754
|
+
const current = stringList2(map.get(key, true));
|
|
1755
|
+
if (current.length === values.length && current.every((value, index) => value === values[index])) {
|
|
1756
|
+
return;
|
|
1757
|
+
}
|
|
1758
|
+
map.set(key, doc.createNode(values));
|
|
1759
|
+
}
|
|
1760
|
+
function setOrDelete2(map, key, value) {
|
|
1761
|
+
if (value === null || value === void 0 || value === "") {
|
|
1762
|
+
if (map.has(key)) map.delete(key);
|
|
1763
|
+
return;
|
|
1764
|
+
}
|
|
1765
|
+
map.set(key, value);
|
|
1766
|
+
}
|
|
1767
|
+
function parseWidgetDocument(specYaml) {
|
|
1768
|
+
const doc = parseDocument(specYaml);
|
|
1769
|
+
const first = doc.errors[0];
|
|
1770
|
+
if (first) {
|
|
1771
|
+
return {
|
|
1772
|
+
doc: null,
|
|
1773
|
+
error: { message: first.message, line: first.linePos?.[0].line ?? 1 }
|
|
1774
|
+
};
|
|
1775
|
+
}
|
|
1776
|
+
if (!isMap(doc.contents)) {
|
|
1777
|
+
return {
|
|
1778
|
+
doc: null,
|
|
1779
|
+
error: { message: "A widget must be a YAML mapping.", line: 1 }
|
|
1780
|
+
};
|
|
1781
|
+
}
|
|
1782
|
+
return { doc, error: null };
|
|
1783
|
+
}
|
|
1784
|
+
function readChartType(display) {
|
|
1785
|
+
const echarts = display ? mapAt2(display, "echarts") : null;
|
|
1786
|
+
const option = echarts ? mapAt2(echarts, "option") : null;
|
|
1787
|
+
const first = mapsOf2(option ? seqAt(option, "series") : null)[0] ?? null;
|
|
1788
|
+
const type = first ? str2(first.get("type")) : null;
|
|
1789
|
+
if (type === "line" && first?.has("areaStyle")) return "area";
|
|
1790
|
+
return CHART_TYPES.includes(type ?? "") ? type : "line";
|
|
1791
|
+
}
|
|
1792
|
+
function readDeltaFormat(display) {
|
|
1793
|
+
const kpi = display ? mapAt2(display, "kpi") : null;
|
|
1794
|
+
const delta = kpi ? mapAt2(kpi, "delta") : null;
|
|
1795
|
+
const format = delta ? str2(delta.get("format")) : null;
|
|
1796
|
+
return DELTA_FORMATS.includes(format ?? "") ? format : "percent";
|
|
1797
|
+
}
|
|
1798
|
+
function num(node) {
|
|
1799
|
+
return typeof node === "number" && Number.isFinite(node) ? node : null;
|
|
1800
|
+
}
|
|
1801
|
+
function bool(node, fallback) {
|
|
1802
|
+
return typeof node === "boolean" ? node : fallback;
|
|
1803
|
+
}
|
|
1804
|
+
function readAppearance(display) {
|
|
1805
|
+
const kpi = display ? mapAt2(display, "kpi") : null;
|
|
1806
|
+
const echarts = display ? mapAt2(display, "echarts") : null;
|
|
1807
|
+
const option = echarts ? mapAt2(echarts, "option") : null;
|
|
1808
|
+
const legend = option ? mapAt2(option, "legend") : null;
|
|
1809
|
+
const xSort = echarts ? mapAt2(echarts, "xSort") : null;
|
|
1810
|
+
const table = display ? mapAt2(display, "table") ?? mapAt2(display, "tabulator") : null;
|
|
1811
|
+
const tabulator = display ? mapAt2(display, "tabulator") : null;
|
|
1812
|
+
const label = kpi ? mapAt2(kpi, "label") : null;
|
|
1813
|
+
const format = kpi ? str2(kpi.get("format")) : null;
|
|
1814
|
+
const order = xSort ? str2(xSort.get("order")) : null;
|
|
1815
|
+
const position = (label ? str2(label.get("position")) : null) ?? (kpi ? str2(kpi.get("position")) : null);
|
|
1816
|
+
return {
|
|
1817
|
+
format: NUMBER_FORMATS.includes(format ?? "") ? format : "number",
|
|
1818
|
+
prefix: kpi ? str2(kpi.get("prefix")) : null,
|
|
1819
|
+
suffix: kpi ? str2(kpi.get("suffix")) : null,
|
|
1820
|
+
compact: bool(kpi?.get("compact"), false),
|
|
1821
|
+
decimals: kpi ? num(kpi.get("decimals")) : null,
|
|
1822
|
+
labelPosition: position === "bottom" ? "bottom" : "top",
|
|
1823
|
+
showLegend: bool(legend?.get("show"), true),
|
|
1824
|
+
horizontal: (echarts ? str2(echarts.get("orientation")) : null) === "horizontal",
|
|
1825
|
+
sortOrder: order === "asc" || order === "desc" ? order : "none",
|
|
1826
|
+
pageSize: table ? num(table.get("page_size")) : null,
|
|
1827
|
+
sortable: bool(table?.get("sortable"), true),
|
|
1828
|
+
groupBy: tabulator ? str2(tabulator.get("group_by")) : null
|
|
1829
|
+
};
|
|
1830
|
+
}
|
|
1831
|
+
function readPivot(display) {
|
|
1832
|
+
const pivot = display ? mapAt2(display, "pivot_table") : null;
|
|
1833
|
+
const totals = pivot ? mapAt2(pivot, "totals") : null;
|
|
1834
|
+
const heatmap = pivot ? mapAt2(pivot, "heatmap") : null;
|
|
1835
|
+
const snapshot = pivot ? mapAt2(pivot, "snapshot") : null;
|
|
1836
|
+
const limits = pivot ? mapAt2(pivot, "limits") : null;
|
|
1837
|
+
const sort = pivot ? mapAt2(pivot, "sort") : null;
|
|
1838
|
+
const topRows = pivot ? mapAt2(pivot, "top_rows") : null;
|
|
1839
|
+
const subtotals = pivot ? str2(pivot.get("subtotals")) : null;
|
|
1840
|
+
const direction = sort ? str2(sort.get("direction")) : null;
|
|
1841
|
+
return {
|
|
1842
|
+
rows: stringList2(pivot?.get("rows", true)),
|
|
1843
|
+
columns: stringList2(pivot?.get("columns", true)),
|
|
1844
|
+
measures: mapsOf2(pivot ? seqAt(pivot, "measures") : null).flatMap(
|
|
1845
|
+
(entry) => {
|
|
1846
|
+
const field = str2(entry.get("field"));
|
|
1847
|
+
if (!field) return [];
|
|
1848
|
+
const agg = str2(entry.get("agg"));
|
|
1849
|
+
const format = str2(entry.get("format"));
|
|
1850
|
+
return [
|
|
1851
|
+
{
|
|
1852
|
+
field,
|
|
1853
|
+
agg: PIVOT_AGGREGATIONS.includes(agg ?? "") ? agg : "sum",
|
|
1854
|
+
label: str2(entry.get("label")),
|
|
1855
|
+
format: NUMBER_FORMATS.includes(format ?? "") ? format : null,
|
|
1856
|
+
decimals: num(entry.get("decimals"))
|
|
1857
|
+
}
|
|
1858
|
+
];
|
|
1859
|
+
}
|
|
1860
|
+
),
|
|
1861
|
+
rowTotals: bool(totals?.get("row"), true),
|
|
1862
|
+
columnTotals: bool(totals?.get("column"), true),
|
|
1863
|
+
grandTotal: bool(totals?.get("grand"), true),
|
|
1864
|
+
subtotals: PIVOT_SUBTOTALS.includes(subtotals ?? "") ? subtotals : "collapsed",
|
|
1865
|
+
emptyValue: pivot ? str2(pivot.get("empty_value")) : null,
|
|
1866
|
+
heatmap: heatmap ? str2(heatmap.get("measure")) : null,
|
|
1867
|
+
snapshotExpand: snapshot ? num(snapshot.get("expand")) : null,
|
|
1868
|
+
snapshotExpandColumns: snapshot ? num(snapshot.get("expand_columns")) : null,
|
|
1869
|
+
sortMeasure: sort ? str2(sort.get("measure")) : null,
|
|
1870
|
+
sortDirection: PIVOT_DIRECTIONS.includes(
|
|
1871
|
+
direction ?? ""
|
|
1872
|
+
) ? direction : null,
|
|
1873
|
+
topRows: topRows ? num(topRows.get("count")) : null,
|
|
1874
|
+
topRowsMeasure: topRows ? str2(topRows.get("measure")) : null,
|
|
1875
|
+
topRowsOther: bool(topRows?.get("other"), true),
|
|
1876
|
+
rowGroupLimit: limits ? num(limits.get("row_groups")) : null,
|
|
1877
|
+
columnLimit: limits ? num(limits.get("columns")) : null,
|
|
1878
|
+
explore: bool(pivot?.get("explore"), true)
|
|
1879
|
+
};
|
|
1880
|
+
}
|
|
1881
|
+
function readSeries(display) {
|
|
1882
|
+
const echarts = display ? mapAt2(display, "echarts") : null;
|
|
1883
|
+
return mapsOf2(echarts ? seqAt(echarts, "series") : null).flatMap((item) => {
|
|
1884
|
+
const field = str2(item.get("field"));
|
|
1885
|
+
if (!field) return [];
|
|
1886
|
+
return [{ name: str2(item.get("name")), field }];
|
|
1887
|
+
});
|
|
1888
|
+
}
|
|
1889
|
+
function readWidgetOutline(specYaml) {
|
|
1890
|
+
const { doc, error } = parseWidgetDocument(specYaml);
|
|
1891
|
+
if (!doc || !isMap(doc.contents)) {
|
|
1892
|
+
return {
|
|
1893
|
+
widget: null,
|
|
1894
|
+
identity: { id: "", name: null, component: "echarts" },
|
|
1895
|
+
readOnlyReason: null,
|
|
1896
|
+
error
|
|
1897
|
+
};
|
|
1898
|
+
}
|
|
1899
|
+
const root = doc.contents;
|
|
1900
|
+
const metadata = mapAt2(root, "metadata");
|
|
1901
|
+
const spec = mapAt2(root, "spec");
|
|
1902
|
+
const identity = {
|
|
1903
|
+
id: (metadata ? str2(metadata.get("id")) : null) ?? "",
|
|
1904
|
+
name: metadata ? str2(metadata.get("name")) : null,
|
|
1905
|
+
component: (spec ? str2(spec.get("component")) : null) ?? "echarts"
|
|
1906
|
+
};
|
|
1907
|
+
if (!spec) {
|
|
1908
|
+
return {
|
|
1909
|
+
widget: null,
|
|
1910
|
+
identity,
|
|
1911
|
+
readOnlyReason: "This widget has no spec block yet.",
|
|
1912
|
+
error: null
|
|
1913
|
+
};
|
|
1914
|
+
}
|
|
1915
|
+
const component = identity.component;
|
|
1916
|
+
if (!isBuilderComponent(component)) {
|
|
1917
|
+
return {
|
|
1918
|
+
widget: null,
|
|
1919
|
+
identity,
|
|
1920
|
+
readOnlyReason: `A ${component} widget holds content rather than data, so its wording lives in YAML.`,
|
|
1921
|
+
error: null
|
|
1922
|
+
};
|
|
1923
|
+
}
|
|
1924
|
+
const data = mapAt2(spec, "data");
|
|
1925
|
+
const dataKind = data ? str2(data.get("kind")) ?? "static" : "static";
|
|
1926
|
+
const mappings = mapAt2(spec, "mappings");
|
|
1927
|
+
const display = mapAt2(spec, "display");
|
|
1928
|
+
return {
|
|
1929
|
+
identity,
|
|
1930
|
+
widget: {
|
|
1931
|
+
id: identity.id,
|
|
1932
|
+
name: identity.name,
|
|
1933
|
+
component,
|
|
1934
|
+
chartType: readChartType(display),
|
|
1935
|
+
deltaFormat: readDeltaFormat(display),
|
|
1936
|
+
dataKind,
|
|
1937
|
+
datasetId: data ? str2(data.get("dataset")) : null,
|
|
1938
|
+
columns: data ? readSelectedColumns(data) : [],
|
|
1939
|
+
mappings: {
|
|
1940
|
+
x: mappings ? str2(mappings.get("x")) : null,
|
|
1941
|
+
category: mappings ? str2(mappings.get("category")) : null,
|
|
1942
|
+
value: mappings ? str2(mappings.get("value")) : null,
|
|
1943
|
+
series: mappings ? str2(mappings.get("series")) : null,
|
|
1944
|
+
delta: mappings ? str2(mappings.get("delta")) : null
|
|
1945
|
+
},
|
|
1946
|
+
series: readSeries(display),
|
|
1947
|
+
appearance: readAppearance(display),
|
|
1948
|
+
pivot: readPivot(display)
|
|
1949
|
+
},
|
|
1950
|
+
readOnlyReason: dataKind === "dataset" || dataKind === "static" ? null : `This widget reads a ${dataKind} source rather than a dataset, so the Builder leaves it to YAML.`,
|
|
1951
|
+
error: null
|
|
1952
|
+
};
|
|
1953
|
+
}
|
|
1954
|
+
function ensureMap(doc, parent, key) {
|
|
1955
|
+
const existing = mapAt2(parent, key);
|
|
1956
|
+
if (existing) return existing;
|
|
1957
|
+
const created = doc.createNode({});
|
|
1958
|
+
if (!isMap(created)) throw new Error(`Could not create ${key}`);
|
|
1959
|
+
parent.set(key, created);
|
|
1960
|
+
return created;
|
|
1961
|
+
}
|
|
1962
|
+
function widgetText(doc) {
|
|
1963
|
+
return doc.toString({ lineWidth: 0, flowCollectionPadding: false });
|
|
1964
|
+
}
|
|
1965
|
+
function writeChartType(doc, display, chartType) {
|
|
1966
|
+
const echarts = ensureMap(doc, display, "echarts");
|
|
1967
|
+
const option = ensureMap(doc, echarts, "option");
|
|
1968
|
+
const existing = mapsOf2(seqAt(option, "series"))[0] ?? null;
|
|
1969
|
+
const target = existing ?? doc.createNode({});
|
|
1970
|
+
target.set("type", chartType === "area" ? "line" : chartType);
|
|
1971
|
+
if (chartType === "area") {
|
|
1972
|
+
if (!target.has("areaStyle")) target.set("areaStyle", doc.createNode({}));
|
|
1973
|
+
} else if (target.has("areaStyle")) {
|
|
1974
|
+
target.delete("areaStyle");
|
|
1975
|
+
}
|
|
1976
|
+
if (!existing) option.set("series", doc.createNode([target]));
|
|
1977
|
+
}
|
|
1978
|
+
function writeSeries(doc, display, series) {
|
|
1979
|
+
const echarts = mapAt2(display, "echarts");
|
|
1980
|
+
if (series.length === 0) {
|
|
1981
|
+
if (echarts?.has("series")) echarts.delete("series");
|
|
1982
|
+
return;
|
|
1983
|
+
}
|
|
1984
|
+
const target = echarts ?? ensureMap(doc, display, "echarts");
|
|
1985
|
+
target.set(
|
|
1986
|
+
"series",
|
|
1987
|
+
doc.createNode(
|
|
1988
|
+
series.map(
|
|
1989
|
+
(entry) => entry.name ? { name: entry.name, field: entry.field } : { field: entry.field }
|
|
1990
|
+
)
|
|
1991
|
+
)
|
|
1992
|
+
);
|
|
1993
|
+
}
|
|
1994
|
+
function writeWidget(specYaml, widget) {
|
|
1995
|
+
const { doc } = parseWidgetDocument(specYaml);
|
|
1996
|
+
if (!doc || !isMap(doc.contents)) return specYaml;
|
|
1997
|
+
const root = doc.contents;
|
|
1998
|
+
const metadata = ensureMap(doc, root, "metadata");
|
|
1999
|
+
if (widget.id) metadata.set("id", widget.id);
|
|
2000
|
+
setOrDelete2(metadata, "name", widget.name);
|
|
2001
|
+
const spec = ensureMap(doc, root, "spec");
|
|
2002
|
+
spec.set("component", widget.component);
|
|
2003
|
+
if (widget.datasetId) {
|
|
2004
|
+
const data = ensureMap(doc, spec, "data");
|
|
2005
|
+
data.set("kind", "dataset");
|
|
2006
|
+
data.set("dataset", widget.datasetId);
|
|
2007
|
+
if (widget.columns.length > 0) {
|
|
2008
|
+
setStringSeq2(doc, data, "columns", widget.columns);
|
|
2009
|
+
} else if (data.has("columns")) {
|
|
2010
|
+
data.delete("columns");
|
|
2011
|
+
}
|
|
2012
|
+
if (data.has("dimensions")) data.delete("dimensions");
|
|
2013
|
+
if (data.has("metrics")) data.delete("metrics");
|
|
2014
|
+
}
|
|
2015
|
+
const mappings = ensureMap(doc, spec, "mappings");
|
|
2016
|
+
setOrDelete2(mappings, "x", widget.mappings.x);
|
|
2017
|
+
setOrDelete2(mappings, "category", widget.mappings.category);
|
|
2018
|
+
setOrDelete2(mappings, "value", widget.mappings.value);
|
|
2019
|
+
setOrDelete2(mappings, "series", widget.mappings.series);
|
|
2020
|
+
setOrDelete2(mappings, "delta", widget.mappings.delta);
|
|
2021
|
+
if (mappings.items.length === 0) spec.delete("mappings");
|
|
2022
|
+
if (widget.component === "echarts") {
|
|
2023
|
+
const display = ensureMap(doc, spec, "display");
|
|
2024
|
+
writeChartType(doc, display, widget.chartType);
|
|
2025
|
+
writeSeries(doc, display, widget.series);
|
|
2026
|
+
}
|
|
2027
|
+
if (widget.component === "kpi") {
|
|
2028
|
+
writeKpiDelta(doc, spec, widget);
|
|
2029
|
+
}
|
|
2030
|
+
writeAppearance(doc, spec, widget);
|
|
2031
|
+
return widgetText(doc);
|
|
2032
|
+
}
|
|
2033
|
+
function writeAppearance(doc, spec, widget) {
|
|
2034
|
+
const look = widget.appearance;
|
|
2035
|
+
if (widget.component === "kpi") {
|
|
2036
|
+
const kpi = ensureMap(doc, ensureMap(doc, spec, "display"), "kpi");
|
|
2037
|
+
kpi.set("format", look.format);
|
|
2038
|
+
setOrDelete2(kpi, "prefix", look.prefix);
|
|
2039
|
+
setOrDelete2(kpi, "suffix", look.suffix);
|
|
2040
|
+
setOrDelete2(kpi, "compact", look.compact ? true : null);
|
|
2041
|
+
setOrDelete2(kpi, "decimals", look.decimals);
|
|
2042
|
+
const label = mapAt2(kpi, "label");
|
|
2043
|
+
const target = label ?? kpi;
|
|
2044
|
+
setOrDelete2(
|
|
2045
|
+
target,
|
|
2046
|
+
"position",
|
|
2047
|
+
look.labelPosition === "bottom" ? "bottom" : null
|
|
2048
|
+
);
|
|
2049
|
+
return;
|
|
2050
|
+
}
|
|
2051
|
+
if (widget.component === "echarts") {
|
|
2052
|
+
const echarts = ensureMap(doc, ensureMap(doc, spec, "display"), "echarts");
|
|
2053
|
+
setOrDelete2(echarts, "orientation", look.horizontal ? "horizontal" : null);
|
|
2054
|
+
if (look.sortOrder === "none") {
|
|
2055
|
+
if (echarts.has("xSort")) echarts.delete("xSort");
|
|
2056
|
+
} else {
|
|
2057
|
+
const xSort = ensureMap(doc, echarts, "xSort");
|
|
2058
|
+
setOrDelete2(xSort, "by", widget.mappings.x ?? widget.mappings.category);
|
|
2059
|
+
xSort.set("order", look.sortOrder);
|
|
2060
|
+
}
|
|
2061
|
+
const option = ensureMap(doc, echarts, "option");
|
|
2062
|
+
if (look.showLegend) {
|
|
2063
|
+
const legend = mapAt2(option, "legend");
|
|
2064
|
+
if (legend?.has("show")) legend.delete("show");
|
|
2065
|
+
if (legend && legend.items.length === 0) option.delete("legend");
|
|
2066
|
+
} else {
|
|
2067
|
+
ensureMap(doc, option, "legend").set("show", false);
|
|
2068
|
+
}
|
|
2069
|
+
if (option.items.length === 0) echarts.delete("option");
|
|
2070
|
+
return;
|
|
2071
|
+
}
|
|
2072
|
+
if (widget.component === "pivot_table") {
|
|
2073
|
+
writePivot(doc, spec, widget.pivot);
|
|
2074
|
+
return;
|
|
2075
|
+
}
|
|
2076
|
+
if (widget.component === "table" || widget.component === "tabulator") {
|
|
2077
|
+
const block = ensureMap(
|
|
2078
|
+
doc,
|
|
2079
|
+
ensureMap(doc, spec, "display"),
|
|
2080
|
+
widget.component
|
|
2081
|
+
);
|
|
2082
|
+
setOrDelete2(block, "page_size", look.pageSize);
|
|
2083
|
+
setOrDelete2(block, "sortable", look.sortable ? null : false);
|
|
2084
|
+
if (widget.component === "tabulator") {
|
|
2085
|
+
setOrDelete2(block, "group_by", look.groupBy);
|
|
2086
|
+
}
|
|
2087
|
+
}
|
|
2088
|
+
}
|
|
2089
|
+
function writePivot(doc, spec, pivot) {
|
|
2090
|
+
const block = ensureMap(doc, ensureMap(doc, spec, "display"), "pivot_table");
|
|
2091
|
+
setStringSeq2(doc, block, "rows", pivot.rows);
|
|
2092
|
+
if (pivot.columns.length > 0) {
|
|
2093
|
+
setStringSeq2(doc, block, "columns", pivot.columns);
|
|
2094
|
+
} else if (block.has("columns")) {
|
|
2095
|
+
block.delete("columns");
|
|
2096
|
+
}
|
|
2097
|
+
block.set(
|
|
2098
|
+
"measures",
|
|
2099
|
+
doc.createNode(
|
|
2100
|
+
pivot.measures.map((measure) => ({
|
|
2101
|
+
field: measure.field,
|
|
2102
|
+
agg: measure.agg,
|
|
2103
|
+
...measure.label ? { label: measure.label } : {},
|
|
2104
|
+
...measure.format ? { format: measure.format } : {},
|
|
2105
|
+
...measure.decimals === null ? {} : { decimals: measure.decimals }
|
|
2106
|
+
}))
|
|
2107
|
+
)
|
|
2108
|
+
);
|
|
2109
|
+
const defaultTotals = pivot.rowTotals && pivot.columnTotals && pivot.grandTotal;
|
|
2110
|
+
if (defaultTotals) {
|
|
2111
|
+
if (block.has("totals")) block.delete("totals");
|
|
2112
|
+
} else {
|
|
2113
|
+
const totals = ensureMap(doc, block, "totals");
|
|
2114
|
+
totals.set("row", pivot.rowTotals);
|
|
2115
|
+
totals.set("column", pivot.columnTotals);
|
|
2116
|
+
totals.set("grand", pivot.grandTotal);
|
|
2117
|
+
}
|
|
2118
|
+
setOrDelete2(
|
|
2119
|
+
block,
|
|
2120
|
+
"subtotals",
|
|
2121
|
+
pivot.subtotals === "collapsed" ? null : pivot.subtotals
|
|
2122
|
+
);
|
|
2123
|
+
setOrDelete2(
|
|
2124
|
+
block,
|
|
2125
|
+
"empty_value",
|
|
2126
|
+
pivot.emptyValue === "-" ? null : pivot.emptyValue
|
|
2127
|
+
);
|
|
2128
|
+
setOrDelete2(block, "explore", pivot.explore ? null : false);
|
|
2129
|
+
if (pivot.heatmap) {
|
|
2130
|
+
ensureMap(doc, block, "heatmap").set("measure", pivot.heatmap);
|
|
2131
|
+
} else if (block.has("heatmap")) {
|
|
2132
|
+
block.delete("heatmap");
|
|
2133
|
+
}
|
|
2134
|
+
if (pivot.snapshotExpand === null && pivot.snapshotExpandColumns === null) {
|
|
2135
|
+
if (block.has("snapshot")) block.delete("snapshot");
|
|
2136
|
+
} else {
|
|
2137
|
+
const snapshot = ensureMap(doc, block, "snapshot");
|
|
2138
|
+
setOrDelete2(snapshot, "expand", pivot.snapshotExpand);
|
|
2139
|
+
setOrDelete2(snapshot, "expand_columns", pivot.snapshotExpandColumns);
|
|
2140
|
+
}
|
|
2141
|
+
if (pivot.sortMeasure === null && pivot.sortDirection === null) {
|
|
2142
|
+
if (block.has("sort")) block.delete("sort");
|
|
2143
|
+
} else {
|
|
2144
|
+
const sort = ensureMap(doc, block, "sort");
|
|
2145
|
+
setOrDelete2(sort, "measure", pivot.sortMeasure);
|
|
2146
|
+
setOrDelete2(sort, "direction", pivot.sortDirection);
|
|
2147
|
+
}
|
|
2148
|
+
if (pivot.topRows === null) {
|
|
2149
|
+
if (block.has("top_rows")) block.delete("top_rows");
|
|
2150
|
+
} else {
|
|
2151
|
+
const top = ensureMap(doc, block, "top_rows");
|
|
2152
|
+
top.set("count", pivot.topRows);
|
|
2153
|
+
setOrDelete2(top, "measure", pivot.topRowsMeasure);
|
|
2154
|
+
setOrDelete2(top, "other", pivot.topRowsOther ? null : false);
|
|
2155
|
+
}
|
|
2156
|
+
if (pivot.rowGroupLimit === null && pivot.columnLimit === null) {
|
|
2157
|
+
if (block.has("limits")) block.delete("limits");
|
|
2158
|
+
} else {
|
|
2159
|
+
const limits = ensureMap(doc, block, "limits");
|
|
2160
|
+
setOrDelete2(limits, "row_groups", pivot.rowGroupLimit);
|
|
2161
|
+
setOrDelete2(limits, "columns", pivot.columnLimit);
|
|
2162
|
+
}
|
|
2163
|
+
}
|
|
2164
|
+
function writePivotLayout(specYaml, layout) {
|
|
2165
|
+
const { doc } = parseWidgetDocument(specYaml);
|
|
2166
|
+
if (!doc || !isMap(doc.contents)) return specYaml;
|
|
2167
|
+
const spec = ensureMap(doc, doc.contents, "spec");
|
|
2168
|
+
const block = ensureMap(doc, ensureMap(doc, spec, "display"), "pivot_table");
|
|
2169
|
+
setStringSeq2(doc, block, "rows", layout.rows);
|
|
2170
|
+
if (layout.columns.length > 0) {
|
|
2171
|
+
setStringSeq2(doc, block, "columns", layout.columns);
|
|
2172
|
+
} else if (block.has("columns")) {
|
|
2173
|
+
block.delete("columns");
|
|
2174
|
+
}
|
|
2175
|
+
const existing = /* @__PURE__ */ new Map();
|
|
2176
|
+
for (const entry of mapsOf2(seqAt(block, "measures"))) {
|
|
2177
|
+
const field = str2(entry.get("field"));
|
|
2178
|
+
if (field && !existing.has(field)) existing.set(field, entry);
|
|
2179
|
+
}
|
|
2180
|
+
block.set(
|
|
2181
|
+
"measures",
|
|
2182
|
+
doc.createNode(
|
|
2183
|
+
layout.measures.map((measure) => {
|
|
2184
|
+
const entry = existing.get(measure.field);
|
|
2185
|
+
const label = entry ? str2(entry.get("label")) : null;
|
|
2186
|
+
const format = entry ? str2(entry.get("format")) : null;
|
|
2187
|
+
const decimals = entry ? num(entry.get("decimals")) : null;
|
|
2188
|
+
return {
|
|
2189
|
+
field: measure.field,
|
|
2190
|
+
agg: measure.agg,
|
|
2191
|
+
...label ? { label } : {},
|
|
2192
|
+
...format ? { format } : {},
|
|
2193
|
+
...decimals === null ? {} : { decimals }
|
|
2194
|
+
};
|
|
2195
|
+
})
|
|
2196
|
+
)
|
|
2197
|
+
);
|
|
2198
|
+
return widgetText(doc);
|
|
2199
|
+
}
|
|
2200
|
+
function writeKpiDelta(doc, spec, widget) {
|
|
2201
|
+
const comparison = widget.mappings.delta;
|
|
2202
|
+
const display = mapAt2(spec, "display");
|
|
2203
|
+
const kpi = display ? mapAt2(display, "kpi") : null;
|
|
2204
|
+
if (!comparison) {
|
|
2205
|
+
const delta2 = kpi ? mapAt2(kpi, "delta") : null;
|
|
2206
|
+
if (!delta2) return;
|
|
2207
|
+
delta2.delete("metric");
|
|
2208
|
+
delta2.delete("compare_label");
|
|
2209
|
+
if (delta2.items.length === 0) kpi?.delete("delta");
|
|
2210
|
+
if (kpi && kpi.items.length === 0) display?.delete("kpi");
|
|
2211
|
+
return;
|
|
2212
|
+
}
|
|
2213
|
+
const kpiMap = ensureMap(doc, ensureMap(doc, spec, "display"), "kpi");
|
|
2214
|
+
const delta = ensureMap(doc, kpiMap, "delta");
|
|
2215
|
+
delta.set("metric", comparison);
|
|
2216
|
+
delta.set("format", widget.deltaFormat);
|
|
2217
|
+
delta.set("compare_label", humanizeColumn(comparison));
|
|
2218
|
+
}
|
|
2219
|
+
function humanizeColumn(column) {
|
|
2220
|
+
const words = column.replace(/[_.]+/g, " ").trim();
|
|
2221
|
+
if (!words) return column;
|
|
2222
|
+
return words.charAt(0).toUpperCase() + words.slice(1);
|
|
2223
|
+
}
|
|
2224
|
+
function writeComponent(specYaml, component) {
|
|
2225
|
+
const { doc } = parseWidgetDocument(specYaml);
|
|
2226
|
+
if (!doc || !isMap(doc.contents)) return specYaml;
|
|
2227
|
+
ensureMap(doc, doc.contents, "spec").set("component", component);
|
|
2228
|
+
return widgetText(doc);
|
|
2229
|
+
}
|
|
2230
|
+
function writeIdentity(specYaml, field, value) {
|
|
2231
|
+
const { doc } = parseWidgetDocument(specYaml);
|
|
2232
|
+
if (!doc || !isMap(doc.contents)) return specYaml;
|
|
2233
|
+
const metadata = ensureMap(doc, doc.contents, "metadata");
|
|
2234
|
+
if (field === "id") {
|
|
2235
|
+
if (value) metadata.set("id", value);
|
|
2236
|
+
} else {
|
|
2237
|
+
setOrDelete2(metadata, "name", value);
|
|
2238
|
+
}
|
|
2239
|
+
return widgetText(doc);
|
|
2240
|
+
}
|
|
2241
|
+
function widgetFragment(specYaml) {
|
|
2242
|
+
const { doc } = parseWidgetDocument(specYaml);
|
|
2243
|
+
if (!doc) return "";
|
|
2244
|
+
return widgetText(doc).trimEnd();
|
|
2245
|
+
}
|
|
2246
|
+
|
|
2247
|
+
// ../dashboard-core/src/widget-template.ts
|
|
2248
|
+
var TEMPLATE_COMPONENTS = [
|
|
2249
|
+
"kpi",
|
|
2250
|
+
"filter",
|
|
2251
|
+
"table",
|
|
2252
|
+
"tabulator",
|
|
2253
|
+
"pivot_table",
|
|
2254
|
+
"text",
|
|
2255
|
+
"echarts",
|
|
2256
|
+
"image",
|
|
2257
|
+
"markdown"
|
|
2258
|
+
];
|
|
2259
|
+
var FILTER_CONTROLS = [
|
|
2260
|
+
"select",
|
|
2261
|
+
"date",
|
|
2262
|
+
"date_range",
|
|
2263
|
+
"custom"
|
|
2264
|
+
];
|
|
2265
|
+
var COMPONENT_LABELS = {
|
|
2266
|
+
kpi: "KPI",
|
|
2267
|
+
filter: "Filter",
|
|
2268
|
+
table: "Table",
|
|
2269
|
+
tabulator: "Tabulator",
|
|
2270
|
+
pivot_table: "Pivot table",
|
|
2271
|
+
text: "Text",
|
|
2272
|
+
echarts: "Chart",
|
|
2273
|
+
image: "Image",
|
|
2274
|
+
markdown: "Markdown"
|
|
2275
|
+
};
|
|
2276
|
+
function templateComponentLabel(component) {
|
|
2277
|
+
return COMPONENT_LABELS[component];
|
|
2278
|
+
}
|
|
2279
|
+
function sanitizeId(name) {
|
|
2280
|
+
return name.toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_+|_+$/g, "");
|
|
2281
|
+
}
|
|
2282
|
+
var LAYOUT = ` x: 0
|
|
2283
|
+
y: 0
|
|
2284
|
+
w: 2
|
|
2285
|
+
h: 2
|
|
2286
|
+
minW: 2
|
|
2287
|
+
minH: 2
|
|
2288
|
+
unplaced: true`;
|
|
2289
|
+
function randomSuffix() {
|
|
2290
|
+
return Math.random().toString(36).slice(2, 10);
|
|
2291
|
+
}
|
|
2292
|
+
function filterTemplate(id, widgetName, control) {
|
|
2293
|
+
const baseYaml = `version: "1.0"
|
|
2294
|
+
kind: widget
|
|
2295
|
+
metadata:
|
|
2296
|
+
id: ${id}
|
|
2297
|
+
name: ${widgetName}
|
|
2298
|
+
description: ""
|
|
2299
|
+
platform:
|
|
2300
|
+
scope: dashboard
|
|
2301
|
+
roles: []
|
|
2302
|
+
spec:
|
|
2303
|
+
component: filter
|
|
2304
|
+
layout:
|
|
2305
|
+
${LAYOUT}`;
|
|
2306
|
+
switch (control) {
|
|
2307
|
+
case "date":
|
|
2308
|
+
return `${baseYaml}
|
|
2309
|
+
data:
|
|
2310
|
+
kind: static
|
|
2311
|
+
display:
|
|
2312
|
+
filter:
|
|
2313
|
+
control: date
|
|
2314
|
+
default_value: today
|
|
2315
|
+
placeholder: Pick a date
|
|
2316
|
+
date_format: yyyy-MM-dd
|
|
2317
|
+
show_icon: true
|
|
2318
|
+
label:
|
|
2319
|
+
text: ${widgetName}
|
|
2320
|
+
tooltip: ""
|
|
2321
|
+
tooltip_icon: info
|
|
2322
|
+
`;
|
|
2323
|
+
case "date_range":
|
|
2324
|
+
return `${baseYaml}
|
|
2325
|
+
data:
|
|
2326
|
+
kind: static
|
|
2327
|
+
display:
|
|
2328
|
+
filter:
|
|
2329
|
+
control: date_range
|
|
2330
|
+
default_value: this month
|
|
2331
|
+
placeholder: Pick a range
|
|
2332
|
+
date_format: yyyy-MM-dd
|
|
2333
|
+
show_icon: true
|
|
2334
|
+
presets:
|
|
2335
|
+
- today
|
|
2336
|
+
- yesterday
|
|
2337
|
+
- last 7 days
|
|
2338
|
+
- this month
|
|
2339
|
+
- last month
|
|
2340
|
+
label:
|
|
2341
|
+
text: ${widgetName}
|
|
2342
|
+
tooltip: ""
|
|
2343
|
+
tooltip_icon: info
|
|
2344
|
+
`;
|
|
2345
|
+
case "custom":
|
|
2346
|
+
return `${baseYaml}
|
|
2347
|
+
data:
|
|
2348
|
+
kind: static
|
|
2349
|
+
display:
|
|
2350
|
+
filter:
|
|
2351
|
+
control: custom
|
|
2352
|
+
default_value: ""
|
|
2353
|
+
placeholder: Enter a value
|
|
2354
|
+
label:
|
|
2355
|
+
text: ${widgetName}
|
|
2356
|
+
tooltip: ""
|
|
2357
|
+
tooltip_icon: info
|
|
2358
|
+
`;
|
|
2359
|
+
case "select":
|
|
2360
|
+
return `${baseYaml}
|
|
2361
|
+
data:
|
|
2362
|
+
kind: static
|
|
2363
|
+
options:
|
|
2364
|
+
- value: option1
|
|
2365
|
+
label: Option 1
|
|
2366
|
+
- value: option2
|
|
2367
|
+
label: Option 2
|
|
2368
|
+
- value: option3
|
|
2369
|
+
label: Option 3
|
|
2370
|
+
default_value: option1
|
|
2371
|
+
display:
|
|
2372
|
+
filter:
|
|
2373
|
+
control: select
|
|
2374
|
+
placeholder: Select...
|
|
2375
|
+
label:
|
|
2376
|
+
text: ${widgetName}
|
|
2377
|
+
tooltip: ""
|
|
2378
|
+
tooltip_icon: info
|
|
2379
|
+
`;
|
|
2380
|
+
}
|
|
2381
|
+
}
|
|
2382
|
+
function widgetTemplate(component, options = {}) {
|
|
2383
|
+
const widgetName = options.name ?? `New ${COMPONENT_LABELS[component]}`;
|
|
2384
|
+
const id = `${sanitizeId(widgetName)}_${options.suffix ?? randomSuffix()}`;
|
|
2385
|
+
const head = `version: "1.0"
|
|
2386
|
+
kind: widget
|
|
2387
|
+
metadata:
|
|
2388
|
+
id: ${id}
|
|
2389
|
+
name: ${widgetName}
|
|
2390
|
+
description: ""
|
|
2391
|
+
platform:
|
|
2392
|
+
scope: dashboard
|
|
2393
|
+
roles: []
|
|
2394
|
+
spec:
|
|
2395
|
+
component: ${component}
|
|
2396
|
+
layout:
|
|
2397
|
+
${LAYOUT}`;
|
|
2398
|
+
switch (component) {
|
|
2399
|
+
case "kpi":
|
|
2400
|
+
return `${head}
|
|
2401
|
+
data:
|
|
2402
|
+
kind: dataset
|
|
2403
|
+
dataset: ""
|
|
2404
|
+
dimensions: []
|
|
2405
|
+
metrics: []
|
|
2406
|
+
filters: []
|
|
2407
|
+
params: {}
|
|
2408
|
+
mappings:
|
|
2409
|
+
value: ""
|
|
2410
|
+
display:
|
|
2411
|
+
kpi:
|
|
2412
|
+
format: number
|
|
2413
|
+
label:
|
|
2414
|
+
text: ${widgetName}
|
|
2415
|
+
tooltip: ""
|
|
2416
|
+
tooltip_icon: info
|
|
2417
|
+
`;
|
|
2418
|
+
case "filter":
|
|
2419
|
+
return filterTemplate(id, widgetName, options.filterControl ?? "select");
|
|
2420
|
+
case "text":
|
|
2421
|
+
return `${head}
|
|
2422
|
+
data:
|
|
2423
|
+
kind: static
|
|
2424
|
+
rows: []
|
|
2425
|
+
columns: []
|
|
2426
|
+
display:
|
|
2427
|
+
text:
|
|
2428
|
+
content: ${widgetName}
|
|
2429
|
+
markdown: false
|
|
2430
|
+
text_align: center
|
|
2431
|
+
vertical_align: middle
|
|
2432
|
+
label:
|
|
2433
|
+
text: ""
|
|
2434
|
+
tooltip: ""
|
|
2435
|
+
tooltip_icon: info
|
|
2436
|
+
`;
|
|
2437
|
+
case "table":
|
|
2438
|
+
return `${head}
|
|
2439
|
+
data:
|
|
2440
|
+
kind: dataset
|
|
2441
|
+
dataset: ""
|
|
2442
|
+
dimensions: []
|
|
2443
|
+
metrics: []
|
|
2444
|
+
filters: []
|
|
2445
|
+
params: {}
|
|
2446
|
+
display:
|
|
2447
|
+
table:
|
|
2448
|
+
page_size: 10
|
|
2449
|
+
export:
|
|
2450
|
+
csv: true
|
|
2451
|
+
excel: true
|
|
2452
|
+
label:
|
|
2453
|
+
text: ${widgetName}
|
|
2454
|
+
tooltip: ""
|
|
2455
|
+
tooltip_icon: info
|
|
2456
|
+
`;
|
|
2457
|
+
case "tabulator":
|
|
2458
|
+
return `${head}
|
|
2459
|
+
data:
|
|
2460
|
+
kind: dataset
|
|
2461
|
+
dataset: ""
|
|
2462
|
+
dimensions: []
|
|
2463
|
+
metrics: []
|
|
2464
|
+
filters: []
|
|
2465
|
+
params: {}
|
|
2466
|
+
display:
|
|
2467
|
+
tabulator:
|
|
2468
|
+
page_size: 25
|
|
2469
|
+
sortable: true
|
|
2470
|
+
layout: fitColumns
|
|
2471
|
+
export:
|
|
2472
|
+
csv: true
|
|
2473
|
+
excel: true
|
|
2474
|
+
label:
|
|
2475
|
+
text: ${widgetName}
|
|
2476
|
+
tooltip: ""
|
|
2477
|
+
tooltip_icon: info
|
|
2478
|
+
`;
|
|
2479
|
+
case "pivot_table":
|
|
2480
|
+
return `${head}
|
|
2481
|
+
data:
|
|
2482
|
+
kind: dataset
|
|
2483
|
+
dataset: ""
|
|
2484
|
+
dimensions: []
|
|
2485
|
+
metrics: []
|
|
2486
|
+
filters: []
|
|
2487
|
+
params: {}
|
|
2488
|
+
display:
|
|
2489
|
+
pivot_table:
|
|
2490
|
+
rows: []
|
|
2491
|
+
columns: []
|
|
2492
|
+
measures: []
|
|
2493
|
+
totals:
|
|
2494
|
+
row: true
|
|
2495
|
+
column: true
|
|
2496
|
+
grand: true
|
|
2497
|
+
subtotals: collapsed
|
|
2498
|
+
empty_value: "-"
|
|
2499
|
+
export:
|
|
2500
|
+
csv: true
|
|
2501
|
+
excel: true
|
|
2502
|
+
label:
|
|
2503
|
+
text: ${widgetName}
|
|
2504
|
+
tooltip: ""
|
|
2505
|
+
tooltip_icon: info
|
|
2506
|
+
`;
|
|
2507
|
+
case "echarts":
|
|
2508
|
+
return `${head}
|
|
2509
|
+
data:
|
|
2510
|
+
kind: dataset
|
|
2511
|
+
dataset: ""
|
|
2512
|
+
dimensions: []
|
|
2513
|
+
metrics: []
|
|
2514
|
+
filters: []
|
|
2515
|
+
params: {}
|
|
2516
|
+
display:
|
|
2517
|
+
echarts:
|
|
2518
|
+
option:
|
|
2519
|
+
series:
|
|
2520
|
+
- type: line
|
|
2521
|
+
label:
|
|
2522
|
+
text: ${widgetName}
|
|
2523
|
+
tooltip: ""
|
|
2524
|
+
tooltip_icon: info
|
|
2525
|
+
`;
|
|
2526
|
+
case "image":
|
|
2527
|
+
return `${head}
|
|
2528
|
+
data:
|
|
2529
|
+
kind: static
|
|
2530
|
+
rows: []
|
|
2531
|
+
columns: []
|
|
2532
|
+
display:
|
|
2533
|
+
image:
|
|
2534
|
+
url: ""
|
|
2535
|
+
alt: ${widgetName}
|
|
2536
|
+
object_fit: contain
|
|
2537
|
+
label:
|
|
2538
|
+
text: ${widgetName}
|
|
2539
|
+
tooltip: ""
|
|
2540
|
+
tooltip_icon: info
|
|
2541
|
+
`;
|
|
2542
|
+
case "markdown":
|
|
2543
|
+
return `${head}
|
|
2544
|
+
data:
|
|
2545
|
+
kind: static
|
|
2546
|
+
rows: []
|
|
2547
|
+
columns: []
|
|
2548
|
+
display:
|
|
2549
|
+
markdown:
|
|
2550
|
+
content: "# ${widgetName}"
|
|
2551
|
+
label:
|
|
2552
|
+
text: ${widgetName}
|
|
2553
|
+
tooltip: ""
|
|
2554
|
+
tooltip_icon: info
|
|
2555
|
+
`;
|
|
2556
|
+
}
|
|
2557
|
+
}
|
|
2558
|
+
function positionOf2(node, lineCounter) {
|
|
2559
|
+
if (!node?.range) return null;
|
|
2560
|
+
const [start, valueEnd] = node.range;
|
|
2561
|
+
const startPos = lineCounter.linePos(start);
|
|
2562
|
+
const endPos = lineCounter.linePos(valueEnd);
|
|
2563
|
+
return {
|
|
2564
|
+
line: startPos.line,
|
|
2565
|
+
column: startPos.col,
|
|
2566
|
+
endColumn: endPos.line === startPos.line ? endPos.col : Number.MAX_SAFE_INTEGER
|
|
2567
|
+
};
|
|
2568
|
+
}
|
|
2569
|
+
function anchorNode(specYaml, anchor) {
|
|
2570
|
+
const doc = parseDocument(specYaml);
|
|
2571
|
+
if (!isMap(doc.contents)) return null;
|
|
2572
|
+
const root = doc.contents;
|
|
2573
|
+
const spec = root.get("spec", true);
|
|
2574
|
+
const specMap = isMap(spec) ? spec : null;
|
|
2575
|
+
const data = specMap?.get("data", true);
|
|
2576
|
+
const dataMap = isMap(data) ? data : null;
|
|
2577
|
+
switch (anchor.kind) {
|
|
2578
|
+
case "id": {
|
|
2579
|
+
const metadata = root.get("metadata", true);
|
|
2580
|
+
return isMap(metadata) ? metadata.get("id", true) ?? metadata : null;
|
|
2581
|
+
}
|
|
2582
|
+
case "dataset":
|
|
2583
|
+
return dataMap ? dataMap.get("dataset", true) ?? dataMap : specMap;
|
|
2584
|
+
case "columns":
|
|
2585
|
+
return dataMap ? dataMap.get("columns", true) ?? dataMap : specMap;
|
|
2586
|
+
case "column": {
|
|
2587
|
+
const columns = dataMap?.get("columns", true);
|
|
2588
|
+
if (!isSeq(columns)) return dataMap;
|
|
2589
|
+
const item = columns.items.find(
|
|
2590
|
+
(entry) => isScalar(entry) && entry.value === anchor.key
|
|
2591
|
+
);
|
|
2592
|
+
return item ?? columns;
|
|
2593
|
+
}
|
|
2594
|
+
case "mapping": {
|
|
2595
|
+
const mappings = specMap?.get("mappings", true);
|
|
2596
|
+
if (!isMap(mappings)) return specMap;
|
|
2597
|
+
return mappings.get(anchor.slot, true) ?? mappings;
|
|
2598
|
+
}
|
|
2599
|
+
case "series": {
|
|
2600
|
+
const display = specMap?.get("display", true);
|
|
2601
|
+
const echarts = isMap(display) ? display.get("echarts", true) : null;
|
|
2602
|
+
const series = isMap(echarts) ? echarts.get("series", true) : null;
|
|
2603
|
+
if (!isSeq(series)) return specMap;
|
|
2604
|
+
return series.items[anchor.index] ?? series;
|
|
2605
|
+
}
|
|
2606
|
+
case "pivotRows":
|
|
2607
|
+
case "pivotColumns":
|
|
2608
|
+
case "pivotMeasures":
|
|
2609
|
+
case "pivotHeatmap":
|
|
2610
|
+
case "pivotSort":
|
|
2611
|
+
case "pivotTopRows": {
|
|
2612
|
+
const pivot = pivotBlock(specMap);
|
|
2613
|
+
if (!pivot) return specMap;
|
|
2614
|
+
const key = PIVOT_ANCHOR_KEYS[anchor.kind];
|
|
2615
|
+
return pivot.get(key, true) ?? pivot;
|
|
2616
|
+
}
|
|
2617
|
+
case "pivotMeasure": {
|
|
2618
|
+
const pivot = pivotBlock(specMap);
|
|
2619
|
+
const measures = pivot?.get("measures", true);
|
|
2620
|
+
if (!isSeq(measures)) return pivot ?? specMap;
|
|
2621
|
+
return measures.items[anchor.index] ?? measures;
|
|
2622
|
+
}
|
|
2623
|
+
default:
|
|
2624
|
+
return null;
|
|
2625
|
+
}
|
|
2626
|
+
}
|
|
2627
|
+
var PIVOT_ANCHOR_KEYS = {
|
|
2628
|
+
pivotRows: "rows",
|
|
2629
|
+
pivotColumns: "columns",
|
|
2630
|
+
pivotMeasures: "measures",
|
|
2631
|
+
pivotHeatmap: "heatmap",
|
|
2632
|
+
pivotSort: "sort",
|
|
2633
|
+
pivotTopRows: "top_rows"
|
|
2634
|
+
};
|
|
2635
|
+
function pivotBlock(specMap) {
|
|
2636
|
+
if (!isMap(specMap)) return null;
|
|
2637
|
+
const display = specMap.get("display", true);
|
|
2638
|
+
const pivot = isMap(display) ? display.get("pivot_table", true) : null;
|
|
2639
|
+
return isMap(pivot) ? pivot : null;
|
|
2640
|
+
}
|
|
2641
|
+
function widgetMarkers(specYaml, model) {
|
|
2642
|
+
const lineCounter = new LineCounter();
|
|
2643
|
+
const doc = parseDocument(specYaml, { lineCounter });
|
|
2644
|
+
if (doc.errors.length > 0) {
|
|
2645
|
+
return doc.errors.map((err) => {
|
|
2646
|
+
const start = lineCounter.linePos(err.pos[0]);
|
|
2647
|
+
const end = lineCounter.linePos(err.pos[1]);
|
|
2648
|
+
return {
|
|
2649
|
+
startLineNumber: start.line,
|
|
2650
|
+
startColumn: start.col,
|
|
2651
|
+
endLineNumber: end.line,
|
|
2652
|
+
endColumn: end.col,
|
|
2653
|
+
message: err.message,
|
|
2654
|
+
severity: "error"
|
|
2655
|
+
};
|
|
2656
|
+
});
|
|
2657
|
+
}
|
|
2658
|
+
const outline = readWidgetOutline(specYaml);
|
|
2659
|
+
if (!outline.widget) return [];
|
|
2660
|
+
return widgetFindings(outline.widget, model).map((finding) => {
|
|
2661
|
+
const at = positionOf2(anchorNode(specYaml, finding.at), lineCounter) ?? {
|
|
2662
|
+
line: 1,
|
|
2663
|
+
column: 1,
|
|
2664
|
+
endColumn: Number.MAX_SAFE_INTEGER
|
|
2665
|
+
};
|
|
2666
|
+
return {
|
|
2667
|
+
startLineNumber: at.line,
|
|
2668
|
+
startColumn: at.column,
|
|
2669
|
+
endLineNumber: at.line,
|
|
2670
|
+
endColumn: at.endColumn,
|
|
2671
|
+
message: finding.message,
|
|
2672
|
+
severity: "warning"
|
|
2673
|
+
};
|
|
2674
|
+
});
|
|
2675
|
+
}
|
|
2676
|
+
|
|
2677
|
+
// ../dashboard-renderers/src/dashboard-tabs.ts
|
|
2678
|
+
function tabWidgetIds(tab) {
|
|
2679
|
+
const entries = tab.widgets ?? [];
|
|
2680
|
+
const ids = [];
|
|
2681
|
+
for (const entry of entries) {
|
|
2682
|
+
if (typeof entry === "string") {
|
|
2683
|
+
ids.push(entry);
|
|
2684
|
+
continue;
|
|
2685
|
+
}
|
|
2686
|
+
const ref = entry["ref"];
|
|
2687
|
+
if (typeof ref === "string") {
|
|
2688
|
+
const base = ref.split("/").pop() ?? ref;
|
|
2689
|
+
ids.push(base.replace(/\.ya?ml$/u, ""));
|
|
2690
|
+
continue;
|
|
2691
|
+
}
|
|
2692
|
+
const metadata = entry["metadata"];
|
|
2693
|
+
if (typeof metadata === "object" && metadata !== null) {
|
|
2694
|
+
const id = metadata.id;
|
|
2695
|
+
if (typeof id === "string") ids.push(id);
|
|
2696
|
+
}
|
|
2697
|
+
}
|
|
2698
|
+
return ids;
|
|
2699
|
+
}
|
|
2700
|
+
function dashboardTabs(dashboard) {
|
|
2701
|
+
return dashboard.spec.tabs ?? [];
|
|
2702
|
+
}
|
|
2703
|
+
function defaultTabId(tabs) {
|
|
2704
|
+
const chosen = tabs.find((tab) => tab.default === true) ?? tabs[0];
|
|
2705
|
+
return chosen?.id;
|
|
2706
|
+
}
|
|
2707
|
+
|
|
2708
|
+
// ../dashboard-renderers/src/responsive.ts
|
|
2709
|
+
var LAYOUT_BREAKPOINT_WIDTHS = {
|
|
2710
|
+
lg: 1200,
|
|
2711
|
+
md: 960,
|
|
2712
|
+
sm: 720,
|
|
2713
|
+
xs: 480,
|
|
2714
|
+
xxs: 0
|
|
2715
|
+
};
|
|
2716
|
+
var DEFAULTS = {
|
|
2717
|
+
wide: { cols: 24, row_height: 30, margin: [8, 8] },
|
|
2718
|
+
desktop: { cols: 24, row_height: 30, margin: [8, 8] },
|
|
2719
|
+
tablet: { cols: 24, row_height: 30, margin: [10, 10] },
|
|
2720
|
+
mobile: { cols: 24, row_height: 30, margin: [10, 10] }
|
|
2721
|
+
};
|
|
2722
|
+
function layoutKeyForWidth(width) {
|
|
2723
|
+
const keys = ["lg", "md", "sm", "xs", "xxs"];
|
|
2724
|
+
return keys.find((key) => width >= LAYOUT_BREAKPOINT_WIDTHS[key]) ?? "xxs";
|
|
2725
|
+
}
|
|
2726
|
+
function resolveResponsive(dashboard) {
|
|
2727
|
+
const configured = dashboard.spec.responsive;
|
|
2728
|
+
return {
|
|
2729
|
+
wide: { ...DEFAULTS.wide, ...configured?.wide ?? {} },
|
|
2730
|
+
desktop: { ...DEFAULTS.desktop, ...configured?.desktop ?? {} },
|
|
2731
|
+
tablet: { ...DEFAULTS.tablet, ...configured?.tablet ?? {} },
|
|
2732
|
+
mobile: { ...DEFAULTS.mobile, ...configured?.mobile ?? {} }
|
|
2733
|
+
};
|
|
2734
|
+
}
|
|
2735
|
+
|
|
2736
|
+
// ../dashboard-renderers/src/reader-grid.ts
|
|
2737
|
+
function buildReaderGrid(input) {
|
|
2738
|
+
const { dashboard, widgets, width } = input;
|
|
2739
|
+
const responsive = resolveResponsive(dashboard);
|
|
2740
|
+
const gridConfig = {
|
|
2741
|
+
...DEFAULT_GRID_CONFIG,
|
|
2742
|
+
...dashboard.spec.config?.grid ?? {}
|
|
2743
|
+
};
|
|
2744
|
+
const cols = {
|
|
2745
|
+
lg: responsive.wide.cols ?? 24,
|
|
2746
|
+
md: responsive.desktop.cols ?? 24,
|
|
2747
|
+
sm: responsive.tablet.cols ?? 24,
|
|
2748
|
+
xs: responsive.mobile.cols ?? 24,
|
|
2749
|
+
xxs: responsive.mobile.cols ?? 24
|
|
2750
|
+
};
|
|
2751
|
+
const layoutKey = layoutKeyForWidth(width);
|
|
2752
|
+
const breakpoint = BREAKPOINT_MAP[layoutKey] ?? "desktop";
|
|
2753
|
+
const gridWidgets = Object.values(widgets).filter(
|
|
2754
|
+
(widget) => !isPanelledControl(widget, dashboard)
|
|
2755
|
+
);
|
|
2756
|
+
const configuredMaxRows = gridConfig.maxRows ?? void 0;
|
|
2757
|
+
const declared = buildBaseLayouts(
|
|
2758
|
+
widgets,
|
|
2759
|
+
dashboard,
|
|
2760
|
+
{},
|
|
2761
|
+
[],
|
|
2762
|
+
configuredMaxRows
|
|
2763
|
+
);
|
|
2764
|
+
const safe = sanitizeLayouts(declared, cols, configuredMaxRows);
|
|
2765
|
+
const covered = ensureLayoutCoverage(
|
|
2766
|
+
safe,
|
|
2767
|
+
widgets,
|
|
2768
|
+
dashboard,
|
|
2769
|
+
{},
|
|
2770
|
+
[],
|
|
2771
|
+
configuredMaxRows
|
|
2772
|
+
);
|
|
2773
|
+
const placed = filterLayoutsByIds(
|
|
2774
|
+
covered,
|
|
2775
|
+
new Set(gridWidgets.map((widget) => widget.metadata.id))
|
|
2776
|
+
);
|
|
2777
|
+
const layouts = stackNarrowLayouts(placed, {
|
|
2778
|
+
xs: cols.xs,
|
|
2779
|
+
xxs: cols.xxs
|
|
2780
|
+
});
|
|
2781
|
+
const breakpointConfig = responsive[breakpoint];
|
|
2782
|
+
const margin = breakpointConfig.margin ?? [8, 8];
|
|
2783
|
+
const containerPadding = breakpointConfig.containerPadding ?? gridConfig.containerPadding ?? margin;
|
|
2784
|
+
const columnCount = cols[layoutKey];
|
|
2785
|
+
const marginX = margin[0] ?? 8;
|
|
2786
|
+
const marginY = margin[1] ?? 8;
|
|
2787
|
+
const paddingX = containerPadding[0] ?? marginX;
|
|
2788
|
+
const paddingY = containerPadding[1] ?? marginY;
|
|
2789
|
+
const columnWidth = (width - 2 * paddingX - marginX * (columnCount - 1)) / columnCount;
|
|
2790
|
+
const smallBreakpoint = breakpoint === "mobile" || breakpoint === "tablet";
|
|
2791
|
+
const rowHeightFloor = breakpointConfig.row_height ?? 30;
|
|
2792
|
+
const squareHeight = smallBreakpoint ? Math.max(columnWidth, rowHeightFloor, 1) : Math.max(columnWidth, 1);
|
|
2793
|
+
const rowHeight = Math.max(
|
|
2794
|
+
1,
|
|
2795
|
+
gridConfig.squareCells === false ? rowHeightFloor : squareHeight
|
|
2796
|
+
);
|
|
2797
|
+
const items = layouts[layoutKey] ?? [];
|
|
2798
|
+
const gridBottom = items.reduce(
|
|
2799
|
+
(lowest, item) => Math.max(lowest, item.y + item.h),
|
|
2800
|
+
0
|
|
2801
|
+
);
|
|
2802
|
+
return {
|
|
2803
|
+
layoutKey,
|
|
2804
|
+
breakpoint,
|
|
2805
|
+
height: gridBottom * rowHeight + Math.max(0, gridBottom - 1) * marginY + 2 * paddingY,
|
|
2806
|
+
columnCount,
|
|
2807
|
+
columnWidth,
|
|
2808
|
+
rowHeight,
|
|
2809
|
+
tiles: items.filter((item) => widgets[item.i] !== void 0).map((item) => ({
|
|
2810
|
+
widgetId: item.i,
|
|
2811
|
+
left: paddingX + item.x * (columnWidth + marginX),
|
|
2812
|
+
top: paddingY + item.y * (rowHeight + marginY),
|
|
2813
|
+
width: item.w * columnWidth + Math.max(0, item.w - 1) * marginX,
|
|
2814
|
+
height: item.h * rowHeight + Math.max(0, item.h - 1) * marginY
|
|
2815
|
+
}))
|
|
2816
|
+
};
|
|
2817
|
+
}
|
|
2818
|
+
function readerRendererContext(dashboard) {
|
|
2819
|
+
const widgetDefaults = dashboard.spec.config?.widgetDefaults;
|
|
2820
|
+
return widgetDefaults === void 0 || widgetDefaults === null ? {} : {
|
|
2821
|
+
widgetDefaults
|
|
2822
|
+
};
|
|
2823
|
+
}
|
|
2824
|
+
|
|
2825
|
+
// ../dashboard-renderers/src/result-table.ts
|
|
2826
|
+
function resultRows(result) {
|
|
2827
|
+
const rows = result?.rows;
|
|
2828
|
+
if (!Array.isArray(rows)) return [];
|
|
2829
|
+
return rows.filter(
|
|
2830
|
+
(row) => typeof row === "object" && row !== null && !Array.isArray(row)
|
|
2831
|
+
);
|
|
2832
|
+
}
|
|
2833
|
+
function resultColumns(result, rows) {
|
|
2834
|
+
const columns = result?.columns;
|
|
2835
|
+
if (Array.isArray(columns)) {
|
|
2836
|
+
const named = columns.map(
|
|
2837
|
+
(column) => typeof column === "string" ? column : typeof column === "object" && column !== null ? String(column.name ?? "") : ""
|
|
2838
|
+
).filter((column) => column !== "");
|
|
2839
|
+
if (named.length > 0) return named;
|
|
2840
|
+
}
|
|
2841
|
+
const first = rows[0];
|
|
2842
|
+
return first === void 0 ? [] : Object.keys(first);
|
|
2843
|
+
}
|
|
2844
|
+
function cellText(value) {
|
|
2845
|
+
if (value === null || value === void 0 || value === "") return "-";
|
|
2846
|
+
if (typeof value === "number") return value.toLocaleString();
|
|
2847
|
+
if (typeof value === "boolean") return value ? "true" : "false";
|
|
2848
|
+
return String(value);
|
|
2849
|
+
}
|
|
2850
|
+
function csvCell(value) {
|
|
2851
|
+
const text2 = value === null || value === void 0 ? "" : String(value);
|
|
2852
|
+
return /[",\n]/u.test(text2) ? `"${text2.replaceAll('"', '""')}"` : text2;
|
|
2853
|
+
}
|
|
2854
|
+
function formatTableCsv(table) {
|
|
2855
|
+
const header = table.columns.map(csvCell).join(",");
|
|
2856
|
+
const body = table.rows.map(
|
|
2857
|
+
(row) => table.columns.map((column) => csvCell(row[column])).join(",")
|
|
2858
|
+
);
|
|
2859
|
+
return [header, ...body].join("\r\n");
|
|
2860
|
+
}
|
|
2861
|
+
|
|
2862
|
+
export { AGGREGATIONS, ALL_COMPONENTS, BREAKPOINT_MAP, BUILDER_COMPONENTS, CHART_TYPES, CONTROL_COMPONENTS, DEFAULT_GRID_CONFIG, DELTA_FORMATS, FILTER_CONTROLS, LAYOUT_BREAKPOINT_WIDTHS, MAX_LAYOUT_WIDTHS, MODEL_TEMPLATES, NUMBER_FORMATS, PIVOT_AGGREGATIONS, PIVOT_DIRECTIONS, PIVOT_SUBTOTALS, SORT_ORDERS, TEMPLATE_COMPONENTS, addWidgetToLayouts, aggregationExpression, blockingKeys, buildBaseLayouts, buildReaderGrid, cellText, computeRequiredMaxRows, countModelIssues, dashboardTabs, dashboardTemplate, dataStoreFragment, datasetFragment, datasetsAreJoined, datasetsInExpression, defaultTabId, diagnosticsToMarkers, dimensionTypeFor, ensureLayoutCoverage, filterLayoutsByIds, filterPanelConfig, firstFreeSlot, formatTableCsv, humanizeColumn, indexModelYaml, insertModelTemplate, isBuilderComponent, isControlComponent, isDoubleScaledPercent, isPanelledControl, itemsCollide, labelColumns, layoutForBreakpoint, layoutKeyForWidth, layoutsForSave, mergeLayouts, modelMetricFragment, modelMetricIssues, modelText, movedIds, numericColumns, overlappingIds, parseAggregation, parseModelDocument, parseRatioExpression, preservedKeys, ratioExpression, readDashboardOutline, readDataStores, readDatasets, readModelMetrics, readModelOutline, readRelationships, readWidgetOutline, readerRendererContext, relationshipFragment, relationshipIssues, removeDataStore, removeDataset, removeModelMetric, removeRelationship, requiredRows, rescaleLayoutColumns, resolveCollisions, resolveResponsive, resolveSnap, resultColumns, resultRows, sanitizeLayouts, stackNarrowLayouts, storeOf, stripPanelledControls, syncActiveLayoutToDesktopWide, syncDesktopWideLayouts, tabWidgetIds, templateComponentLabel, upsertDataStore, upsertDataset, upsertModelMetric, upsertRelationship, widgetColumns, widgetFindings, widgetFragment, widgetIssues, widgetMarkers, widgetTemplate, writeComponent, writeIdentity, writePivotLayout, writeWidget, yamlSyntaxMarkers };
|
|
2863
|
+
//# sourceMappingURL=chunk-IMNUQGHE.js.map
|
|
2864
|
+
//# sourceMappingURL=chunk-IMNUQGHE.js.map
|