@wibi-global/sdk 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/README.md +18 -0
- package/dist/builder.d.ts +19 -0
- package/dist/builder.d.ts.map +1 -0
- package/dist/builder.js +34 -0
- package/dist/builder.js.map +1 -0
- package/dist/context.d.ts +12 -0
- package/dist/context.d.ts.map +1 -0
- package/dist/context.js +11 -0
- package/dist/context.js.map +1 -0
- package/dist/dashboard-context.d.ts +46 -0
- package/dist/dashboard-context.d.ts.map +1 -0
- package/dist/dashboard-context.js +140 -0
- package/dist/dashboard-context.js.map +1 -0
- package/dist/dashboard-spec.d.ts +22 -0
- package/dist/dashboard-spec.d.ts.map +1 -0
- package/dist/dashboard-spec.js +754 -0
- package/dist/dashboard-spec.js.map +1 -0
- package/dist/data/index.d.ts +7 -0
- package/dist/data/index.d.ts.map +1 -0
- package/dist/data/index.js +12 -0
- package/dist/data/index.js.map +1 -0
- package/dist/data/serialization/dashboard-serialization.types.d.ts +357 -0
- package/dist/data/serialization/dashboard-serialization.types.d.ts.map +1 -0
- package/dist/data/serialization/dashboard-serialization.types.js +2 -0
- package/dist/data/serialization/dashboard-serialization.types.js.map +1 -0
- package/dist/data/serialization/index.d.ts +2 -0
- package/dist/data/serialization/index.d.ts.map +1 -0
- package/dist/data/serialization/index.js +2 -0
- package/dist/data/serialization/index.js.map +1 -0
- package/dist/data.d.ts +26 -0
- package/dist/data.d.ts.map +1 -0
- package/dist/data.js +106 -0
- package/dist/data.js.map +1 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +7 -0
- package/dist/index.js.map +1 -0
- package/package.json +34 -0
|
@@ -0,0 +1,754 @@
|
|
|
1
|
+
import widgetSchemaJson from './data/widgets.wbx' with { type: 'json' };
|
|
2
|
+
export const WIBI_DASHBOARD_SPEC_VERSION = '1.0.0';
|
|
3
|
+
export const WIBI_DASHBOARD_SPEC_EXPORT_NAME = 'dashboardSpec';
|
|
4
|
+
export const WIBI_DASHBOARD_TEMPLATE_ALLOWED_IMPORTS = ['@wibi-global/sdk'];
|
|
5
|
+
export const WIBI_DASHBOARD_OPTIONAL_DEFAULT_EXPORT = true;
|
|
6
|
+
export const WIBI_DASHBOARD_REQUIRED_INITIAL_FIELDS = [
|
|
7
|
+
'metadata.name',
|
|
8
|
+
'metadata.description',
|
|
9
|
+
'metadata.schema_version',
|
|
10
|
+
'metadata.generated_at',
|
|
11
|
+
'metadata.category',
|
|
12
|
+
'metadata.is_public',
|
|
13
|
+
'filters',
|
|
14
|
+
'layout.mode',
|
|
15
|
+
'layout.sections',
|
|
16
|
+
'widgets',
|
|
17
|
+
'queries',
|
|
18
|
+
];
|
|
19
|
+
const STABLE_ID_PATTERN = /^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/;
|
|
20
|
+
const FILTER_ID_PATTERN = /^filter-[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
21
|
+
const SECTION_ID_PATTERN = /^section-[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
22
|
+
const QUERY_ID_PATTERN = /^query-[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
23
|
+
const ISO_TIMESTAMP_PATTERN = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{3})?Z$/;
|
|
24
|
+
const WIDGET_TYPES = new Set(['kpi', 'chart', 'grid', 'progress-list', 'panel', 'date', 'companies', 'heading', 'text', 'image', 'divider', 'spacer']);
|
|
25
|
+
/** Widget types that are decorative — no data query required */
|
|
26
|
+
const DECORATIVE_WIDGET_TYPES = new Set(['date', 'companies', 'heading', 'text', 'image', 'divider', 'spacer']);
|
|
27
|
+
const OFFICIAL_DASHBOARD_ITEM_TYPES = new Set((widgetSchemaJson.dashboardItemType?.fields?.type?.values ?? []).filter(Boolean));
|
|
28
|
+
const OFFICIAL_WIDGET_CATALOG = (widgetSchemaJson.widgets ?? {});
|
|
29
|
+
const OFFICIAL_CHART_VARIANTS = new Set(Object.entries(OFFICIAL_WIDGET_CATALOG)
|
|
30
|
+
.filter(([, definition]) => definition.render === 'EChartsChart')
|
|
31
|
+
.map(([type]) => type));
|
|
32
|
+
const SERIALIZED_WIDGET_TO_OFFICIAL_TYPES = {
|
|
33
|
+
chart: ['chart'],
|
|
34
|
+
grid: ['grid'],
|
|
35
|
+
kpi: ['kpi'],
|
|
36
|
+
'progress-list': ['progress-list'],
|
|
37
|
+
panel: ['card'],
|
|
38
|
+
date: ['date'],
|
|
39
|
+
companies: ['companies'],
|
|
40
|
+
heading: ['text'],
|
|
41
|
+
text: ['text'],
|
|
42
|
+
image: ['image'],
|
|
43
|
+
divider: ['text'],
|
|
44
|
+
spacer: ['text'],
|
|
45
|
+
};
|
|
46
|
+
const KPI_ALLOWED_PROPERTY_KEYS = new Set([
|
|
47
|
+
'widget_variant',
|
|
48
|
+
'label',
|
|
49
|
+
'value_field',
|
|
50
|
+
'value_format',
|
|
51
|
+
'currency',
|
|
52
|
+
'decimals',
|
|
53
|
+
'caption',
|
|
54
|
+
'icon',
|
|
55
|
+
'tone',
|
|
56
|
+
'thresholds',
|
|
57
|
+
'card_properties',
|
|
58
|
+
]);
|
|
59
|
+
const CHART_ALLOWED_PROPERTY_KEYS = new Set([
|
|
60
|
+
'chart_type',
|
|
61
|
+
'x_field',
|
|
62
|
+
'y_field',
|
|
63
|
+
'y_fields',
|
|
64
|
+
'group_by_field',
|
|
65
|
+
'aggregation',
|
|
66
|
+
'top_n',
|
|
67
|
+
'sort_order',
|
|
68
|
+
'sort_by',
|
|
69
|
+
'title',
|
|
70
|
+
'description',
|
|
71
|
+
'show_legend',
|
|
72
|
+
'show_data_labels',
|
|
73
|
+
'empty_state',
|
|
74
|
+
'alternate_colors',
|
|
75
|
+
'orientation',
|
|
76
|
+
'bar_width',
|
|
77
|
+
'rotate_labels',
|
|
78
|
+
'enable_zoom',
|
|
79
|
+
'smooth',
|
|
80
|
+
'show_symbol',
|
|
81
|
+
'symbol_size',
|
|
82
|
+
'line_style',
|
|
83
|
+
'area_opacity',
|
|
84
|
+
'gradient',
|
|
85
|
+
'radius',
|
|
86
|
+
'inner_radius',
|
|
87
|
+
'rose_type',
|
|
88
|
+
'start_angle',
|
|
89
|
+
'min_angle',
|
|
90
|
+
'show_percentage',
|
|
91
|
+
'label_position',
|
|
92
|
+
'max_categories',
|
|
93
|
+
]);
|
|
94
|
+
const GRID_ALLOWED_PROPERTY_KEYS = new Set([
|
|
95
|
+
'title',
|
|
96
|
+
'page_size',
|
|
97
|
+
'columns',
|
|
98
|
+
'column_labels',
|
|
99
|
+
'enable_filters',
|
|
100
|
+
'enable_export',
|
|
101
|
+
'enable_pagination',
|
|
102
|
+
'enable_column_settings',
|
|
103
|
+
'enable_fullscreen',
|
|
104
|
+
'enable_aggregation',
|
|
105
|
+
'column_defs',
|
|
106
|
+
'default_sort',
|
|
107
|
+
'use_grid_v2',
|
|
108
|
+
'empty_message',
|
|
109
|
+
'loading_message',
|
|
110
|
+
]);
|
|
111
|
+
const PANEL_ALLOWED_PROPERTY_KEYS = new Set([
|
|
112
|
+
'panel_type',
|
|
113
|
+
'title',
|
|
114
|
+
'score_field',
|
|
115
|
+
'score_format',
|
|
116
|
+
'score_thresholds',
|
|
117
|
+
'diagnostic_fields',
|
|
118
|
+
'actions',
|
|
119
|
+
'sources',
|
|
120
|
+
]);
|
|
121
|
+
const PROGRESS_LIST_ALLOWED_PROPERTY_KEYS = new Set([
|
|
122
|
+
'labelField',
|
|
123
|
+
'valueField',
|
|
124
|
+
'aggregation',
|
|
125
|
+
'maxItems',
|
|
126
|
+
'showValues',
|
|
127
|
+
'showPercentage',
|
|
128
|
+
'valueFormat',
|
|
129
|
+
'barHeight',
|
|
130
|
+
'colorScheme',
|
|
131
|
+
'primaryColor',
|
|
132
|
+
'sortBy',
|
|
133
|
+
'sortOrder',
|
|
134
|
+
]);
|
|
135
|
+
const DATE_WIDGET_ALLOWED_PROPERTY_KEYS = new Set([
|
|
136
|
+
'label',
|
|
137
|
+
'default_period_type',
|
|
138
|
+
'fallback_days',
|
|
139
|
+
]);
|
|
140
|
+
const COMPANIES_WIDGET_ALLOWED_PROPERTY_KEYS = new Set([
|
|
141
|
+
'placeholder',
|
|
142
|
+
'show_icon',
|
|
143
|
+
'auto_select_first',
|
|
144
|
+
'default_value',
|
|
145
|
+
]);
|
|
146
|
+
const HEADING_ALLOWED_PROPERTY_KEYS = new Set([
|
|
147
|
+
'level',
|
|
148
|
+
'content',
|
|
149
|
+
'align',
|
|
150
|
+
'color',
|
|
151
|
+
'subtitle',
|
|
152
|
+
'subtitle_color',
|
|
153
|
+
'icon',
|
|
154
|
+
]);
|
|
155
|
+
const TEXT_ALLOWED_PROPERTY_KEYS = new Set([
|
|
156
|
+
'content',
|
|
157
|
+
'align',
|
|
158
|
+
'font_size',
|
|
159
|
+
'color',
|
|
160
|
+
'background_color',
|
|
161
|
+
'padding',
|
|
162
|
+
'border_radius',
|
|
163
|
+
'border_color',
|
|
164
|
+
]);
|
|
165
|
+
const IMAGE_ALLOWED_PROPERTY_KEYS = new Set([
|
|
166
|
+
'src',
|
|
167
|
+
'alt',
|
|
168
|
+
'object_fit',
|
|
169
|
+
'height',
|
|
170
|
+
'border_radius',
|
|
171
|
+
'href',
|
|
172
|
+
'caption',
|
|
173
|
+
]);
|
|
174
|
+
const DIVIDER_ALLOWED_PROPERTY_KEYS = new Set([
|
|
175
|
+
'style',
|
|
176
|
+
'color',
|
|
177
|
+
'thickness',
|
|
178
|
+
'margin',
|
|
179
|
+
'label',
|
|
180
|
+
]);
|
|
181
|
+
const SPACER_ALLOWED_PROPERTY_KEYS = new Set([
|
|
182
|
+
'height',
|
|
183
|
+
]);
|
|
184
|
+
const DATE_FILTER_PLACEHOLDERS = ['startDate', 'endDate'];
|
|
185
|
+
const EMPRESA_FILTER_PLACEHOLDERS = ['empresaId', 'companyId', 'companies', 'companyIds'];
|
|
186
|
+
const ACCEPTED_QUERY_PLACEHOLDER_PATTERNS = [
|
|
187
|
+
/^\$\{startDate\}$/,
|
|
188
|
+
/^\$\{endDate\}$/,
|
|
189
|
+
/^\$\{empresaId\}$/,
|
|
190
|
+
/^\$\{companyId\}$/,
|
|
191
|
+
/^\$\{companyIds\}$/,
|
|
192
|
+
/^\$\{companyFilter:[a-zA-Z_][a-zA-Z0-9_]*\}$/,
|
|
193
|
+
/^\{\{start_date\}\}$/,
|
|
194
|
+
/^\{\{end_date\}\}$/,
|
|
195
|
+
/^\{\{year\}\}$/,
|
|
196
|
+
/^\{\{date_limit_2y\}\}$/,
|
|
197
|
+
];
|
|
198
|
+
const ALLOWED_MANUAL_QUERY_SOURCES = new Set(['manual-bootstrap']);
|
|
199
|
+
const pushIssue = (issues, path, message) => {
|
|
200
|
+
issues.push({ path, message });
|
|
201
|
+
};
|
|
202
|
+
const isNonEmptyString = (value) => value.trim().length > 0;
|
|
203
|
+
const validateNonEmptyString = (issues, path, value, label) => {
|
|
204
|
+
if (!isNonEmptyString(value)) {
|
|
205
|
+
pushIssue(issues, path, `${label} must be a non-empty string.`);
|
|
206
|
+
}
|
|
207
|
+
};
|
|
208
|
+
const validateUniqueStrings = (issues, values, path, label) => {
|
|
209
|
+
const seen = new Set();
|
|
210
|
+
values.forEach((value, index) => {
|
|
211
|
+
if (seen.has(value)) {
|
|
212
|
+
pushIssue(issues, `${path}[${index}]`, `${label} "${value}" is duplicated.`);
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
seen.add(value);
|
|
216
|
+
});
|
|
217
|
+
};
|
|
218
|
+
const validateStableId = (issues, path, value, pattern, label) => {
|
|
219
|
+
if (!pattern.test(value)) {
|
|
220
|
+
pushIssue(issues, path, `${label} must use lowercase kebab-case ASCII and follow the official naming rule.`);
|
|
221
|
+
}
|
|
222
|
+
};
|
|
223
|
+
const validateUniqueIds = (issues, values, label) => {
|
|
224
|
+
const seen = new Map();
|
|
225
|
+
for (const value of values) {
|
|
226
|
+
const firstPath = seen.get(value.id);
|
|
227
|
+
if (firstPath) {
|
|
228
|
+
pushIssue(issues, value.path, `${label} "${value.id}" is duplicated. First declared at ${firstPath}.`);
|
|
229
|
+
continue;
|
|
230
|
+
}
|
|
231
|
+
seen.set(value.id, value.path);
|
|
232
|
+
}
|
|
233
|
+
};
|
|
234
|
+
const validateOfficialWidgetType = (issues, path, widgetType) => {
|
|
235
|
+
// Decorative widgets don't require widgets.wbx mapping
|
|
236
|
+
if (DECORATIVE_WIDGET_TYPES.has(widgetType)) {
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
const officialTypes = SERIALIZED_WIDGET_TO_OFFICIAL_TYPES[widgetType] ?? [];
|
|
240
|
+
const hasOfficialType = officialTypes.some((type) => OFFICIAL_DASHBOARD_ITEM_TYPES.has(type));
|
|
241
|
+
if (!hasOfficialType) {
|
|
242
|
+
pushIssue(issues, path, `Widget type "${widgetType}" has no compatible mapping in widgets.wbx dashboardItemType.`);
|
|
243
|
+
}
|
|
244
|
+
};
|
|
245
|
+
const validateAllowedPropertyKeys = (issues, path, properties, allowedKeys, widgetType) => {
|
|
246
|
+
const record = properties;
|
|
247
|
+
Object.keys(record).forEach((key) => {
|
|
248
|
+
if (!allowedKeys.has(key)) {
|
|
249
|
+
pushIssue(issues, `${path}.${key}`, `Property "${key}" is not supported for widget type "${widgetType}" in the current TSX contract.`);
|
|
250
|
+
}
|
|
251
|
+
});
|
|
252
|
+
};
|
|
253
|
+
const validateKpiProperties = (properties, issues, path) => {
|
|
254
|
+
validateAllowedPropertyKeys(issues, path, properties, KPI_ALLOWED_PROPERTY_KEYS, 'kpi');
|
|
255
|
+
validateNonEmptyString(issues, `${path}.label`, properties.label, 'KPI label');
|
|
256
|
+
validateNonEmptyString(issues, `${path}.value_field`, properties.value_field, 'KPI value_field');
|
|
257
|
+
validateNonEmptyString(issues, `${path}.caption`, properties.caption, 'KPI caption');
|
|
258
|
+
validateNonEmptyString(issues, `${path}.icon`, properties.icon, 'KPI icon');
|
|
259
|
+
if (properties.decimals !== undefined && (!Number.isInteger(properties.decimals) || properties.decimals < 0)) {
|
|
260
|
+
pushIssue(issues, `${path}.decimals`, 'KPI decimals must be a non-negative integer when provided.');
|
|
261
|
+
}
|
|
262
|
+
const officialKpi = OFFICIAL_WIDGET_CATALOG.kpi;
|
|
263
|
+
if (!officialKpi) {
|
|
264
|
+
pushIssue(issues, path, 'Official KPI definition is missing from widgets.wbx.');
|
|
265
|
+
}
|
|
266
|
+
};
|
|
267
|
+
const validateChartProperties = (properties, issues, path) => {
|
|
268
|
+
validateAllowedPropertyKeys(issues, path, properties, CHART_ALLOWED_PROPERTY_KEYS, 'chart');
|
|
269
|
+
validateNonEmptyString(issues, `${path}.x_field`, properties.x_field, 'Chart x_field');
|
|
270
|
+
if (properties.title !== undefined) {
|
|
271
|
+
validateNonEmptyString(issues, `${path}.title`, properties.title, 'Chart title');
|
|
272
|
+
}
|
|
273
|
+
const yFields = properties.y_fields ?? [];
|
|
274
|
+
if (yFields.length === 0 && !properties.y_field) {
|
|
275
|
+
pushIssue(issues, `${path}.y_fields`, 'Chart must have y_field or y_fields with at least one field.');
|
|
276
|
+
}
|
|
277
|
+
validateUniqueStrings(issues, yFields, `${path}.y_fields`, 'Chart y_field');
|
|
278
|
+
yFields.forEach((field, index) => {
|
|
279
|
+
validateNonEmptyString(issues, `${path}.y_fields[${index}]`, field, 'Chart y_field');
|
|
280
|
+
});
|
|
281
|
+
if (!OFFICIAL_CHART_VARIANTS.has(properties.chart_type)) {
|
|
282
|
+
pushIssue(issues, `${path}.chart_type`, `Chart type "${properties.chart_type}" is not registered in widgets.wbx.`);
|
|
283
|
+
}
|
|
284
|
+
const officialChart = OFFICIAL_WIDGET_CATALOG.chart;
|
|
285
|
+
if (!officialChart) {
|
|
286
|
+
pushIssue(issues, path, 'Official chart definition is missing from widgets.wbx.');
|
|
287
|
+
}
|
|
288
|
+
};
|
|
289
|
+
const validateGridProperties = (properties, issues, path) => {
|
|
290
|
+
validateAllowedPropertyKeys(issues, path, properties, GRID_ALLOWED_PROPERTY_KEYS, 'grid');
|
|
291
|
+
validateNonEmptyString(issues, `${path}.title`, properties.title, 'Grid title');
|
|
292
|
+
if (!Number.isInteger(properties.page_size) || properties.page_size <= 0) {
|
|
293
|
+
pushIssue(issues, `${path}.page_size`, 'Grid page_size must be a positive integer.');
|
|
294
|
+
}
|
|
295
|
+
// When column_defs is provided, columns + column_labels become optional
|
|
296
|
+
const hasColumnDefs = Array.isArray(properties.column_defs) && properties.column_defs.length > 0;
|
|
297
|
+
if (!hasColumnDefs) {
|
|
298
|
+
if (properties.columns.length === 0) {
|
|
299
|
+
pushIssue(issues, `${path}.columns`, 'Grid columns must contain at least one field (or use column_defs).');
|
|
300
|
+
}
|
|
301
|
+
validateUniqueStrings(issues, properties.columns, `${path}.columns`, 'Grid column');
|
|
302
|
+
properties.columns.forEach((column, index) => {
|
|
303
|
+
validateNonEmptyString(issues, `${path}.columns[${index}]`, column, 'Grid column');
|
|
304
|
+
if (!Object.prototype.hasOwnProperty.call(properties.column_labels, column)) {
|
|
305
|
+
pushIssue(issues, `${path}.column_labels`, `Grid column_labels must include a label for column "${column}".`);
|
|
306
|
+
}
|
|
307
|
+
});
|
|
308
|
+
}
|
|
309
|
+
if (hasColumnDefs) {
|
|
310
|
+
const columnDefIds = properties.column_defs.map((def) => def.id);
|
|
311
|
+
validateUniqueStrings(issues, columnDefIds, `${path}.column_defs`, 'Grid column_defs id');
|
|
312
|
+
properties.column_defs.forEach((def, index) => {
|
|
313
|
+
validateNonEmptyString(issues, `${path}.column_defs[${index}].id`, def.id, 'column_defs id');
|
|
314
|
+
validateNonEmptyString(issues, `${path}.column_defs[${index}].header`, def.header, 'column_defs header');
|
|
315
|
+
if (def.format?.type !== undefined) {
|
|
316
|
+
const validTypes = new Set(['text', 'number', 'currency', 'percentage', 'date', 'datetime', 'boolean']);
|
|
317
|
+
if (!validTypes.has(def.format.type)) {
|
|
318
|
+
pushIssue(issues, `${path}.column_defs[${index}].format.type`, `column_defs format.type must be one of: ${[...validTypes].join(', ')}.`);
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
});
|
|
322
|
+
}
|
|
323
|
+
if (properties.default_sort) {
|
|
324
|
+
validateNonEmptyString(issues, `${path}.default_sort.column_id`, properties.default_sort.column_id, 'Grid default_sort.column_id');
|
|
325
|
+
const validDirections = new Set(['asc', 'desc']);
|
|
326
|
+
if (!validDirections.has(properties.default_sort.direction)) {
|
|
327
|
+
pushIssue(issues, `${path}.default_sort.direction`, 'Grid default_sort.direction must be asc or desc.');
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
const officialGrid = OFFICIAL_WIDGET_CATALOG.grid;
|
|
331
|
+
if (!officialGrid) {
|
|
332
|
+
pushIssue(issues, path, 'Official grid definition is missing from widgets.wbx.');
|
|
333
|
+
}
|
|
334
|
+
};
|
|
335
|
+
const validatePanelProperties = (properties, issues, path) => {
|
|
336
|
+
validateAllowedPropertyKeys(issues, path, properties, PANEL_ALLOWED_PROPERTY_KEYS, 'panel');
|
|
337
|
+
validateNonEmptyString(issues, `${path}.title`, properties.title, 'Panel title');
|
|
338
|
+
if (properties.diagnostic_fields) {
|
|
339
|
+
validateUniqueStrings(issues, properties.diagnostic_fields, `${path}.diagnostic_fields`, 'Panel diagnostic field');
|
|
340
|
+
properties.diagnostic_fields.forEach((field, index) => {
|
|
341
|
+
validateNonEmptyString(issues, `${path}.diagnostic_fields[${index}]`, field, 'Panel diagnostic field');
|
|
342
|
+
});
|
|
343
|
+
}
|
|
344
|
+
if (properties.sources) {
|
|
345
|
+
validateUniqueStrings(issues, properties.sources, `${path}.sources`, 'Panel source');
|
|
346
|
+
properties.sources.forEach((source, index) => {
|
|
347
|
+
validateNonEmptyString(issues, `${path}.sources[${index}]`, source, 'Panel source');
|
|
348
|
+
});
|
|
349
|
+
}
|
|
350
|
+
if (properties.actions) {
|
|
351
|
+
properties.actions.forEach((action, index) => {
|
|
352
|
+
validateNonEmptyString(issues, `${path}.actions[${index}].path`, action.path, 'Panel action path');
|
|
353
|
+
validateNonEmptyString(issues, `${path}.actions[${index}].title`, action.title, 'Panel action title');
|
|
354
|
+
validateNonEmptyString(issues, `${path}.actions[${index}].description`, action.description, 'Panel action description');
|
|
355
|
+
});
|
|
356
|
+
}
|
|
357
|
+
};
|
|
358
|
+
const validateProgressListProperties = (properties, issues, path) => {
|
|
359
|
+
validateAllowedPropertyKeys(issues, path, properties, PROGRESS_LIST_ALLOWED_PROPERTY_KEYS, 'progress-list');
|
|
360
|
+
validateNonEmptyString(issues, `${path}.labelField`, properties.labelField, 'ProgressList labelField');
|
|
361
|
+
validateNonEmptyString(issues, `${path}.valueField`, properties.valueField, 'ProgressList valueField');
|
|
362
|
+
if (properties.maxItems !== undefined && (!Number.isInteger(properties.maxItems) || properties.maxItems <= 0)) {
|
|
363
|
+
pushIssue(issues, `${path}.maxItems`, 'ProgressList maxItems must be a positive integer when provided.');
|
|
364
|
+
}
|
|
365
|
+
if (properties.valueFormat !== undefined) {
|
|
366
|
+
const validFormats = new Set(['number', 'currency', 'compact', 'percentage']);
|
|
367
|
+
if (!validFormats.has(properties.valueFormat)) {
|
|
368
|
+
pushIssue(issues, `${path}.valueFormat`, 'ProgressList valueFormat must be one of: number, currency, compact, percentage.');
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
if (properties.colorScheme !== undefined) {
|
|
372
|
+
const validSchemes = new Set(['gradient', 'single', 'custom']);
|
|
373
|
+
if (!validSchemes.has(properties.colorScheme)) {
|
|
374
|
+
pushIssue(issues, `${path}.colorScheme`, 'ProgressList colorScheme must be one of: gradient, single, custom.');
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
if (properties.sortBy !== undefined) {
|
|
378
|
+
const validSortBy = new Set(['value', 'label', 'none']);
|
|
379
|
+
if (!validSortBy.has(properties.sortBy)) {
|
|
380
|
+
pushIssue(issues, `${path}.sortBy`, 'ProgressList sortBy must be one of: value, label, none.');
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
if (properties.sortOrder !== undefined) {
|
|
384
|
+
const validSortOrder = new Set(['asc', 'desc']);
|
|
385
|
+
if (!validSortOrder.has(properties.sortOrder)) {
|
|
386
|
+
pushIssue(issues, `${path}.sortOrder`, 'ProgressList sortOrder must be one of: asc, desc.');
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
};
|
|
390
|
+
const validateDateWidgetProperties = (properties, issues, path) => {
|
|
391
|
+
validateAllowedPropertyKeys(issues, path, properties, DATE_WIDGET_ALLOWED_PROPERTY_KEYS, 'date');
|
|
392
|
+
if (properties.label !== undefined) {
|
|
393
|
+
validateNonEmptyString(issues, `${path}.label`, properties.label, 'Date widget label');
|
|
394
|
+
}
|
|
395
|
+
if (properties.default_period_type !== undefined) {
|
|
396
|
+
const validPeriods = new Set(['D', 'S', 'M', 'T', 'SE', 'A', 'W', 'Q', 'Y', 'SP', 'CUSTOM']);
|
|
397
|
+
if (!validPeriods.has(properties.default_period_type)) {
|
|
398
|
+
pushIssue(issues, `${path}.default_period_type`, 'Date widget default_period_type must be one of D, S, M, T, SE, A, W, Q, Y, SP or CUSTOM.');
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
if (properties.fallback_days !== undefined && (!Number.isInteger(properties.fallback_days) || properties.fallback_days < 0)) {
|
|
402
|
+
pushIssue(issues, `${path}.fallback_days`, 'Date widget fallback_days must be a non-negative integer when provided.');
|
|
403
|
+
}
|
|
404
|
+
};
|
|
405
|
+
const validateCompaniesWidgetProperties = (properties, issues, path) => {
|
|
406
|
+
validateAllowedPropertyKeys(issues, path, properties, COMPANIES_WIDGET_ALLOWED_PROPERTY_KEYS, 'companies');
|
|
407
|
+
if (properties.placeholder !== undefined) {
|
|
408
|
+
validateNonEmptyString(issues, `${path}.placeholder`, properties.placeholder, 'Companies widget placeholder');
|
|
409
|
+
}
|
|
410
|
+
if (properties.default_value !== undefined && typeof properties.default_value !== 'number') {
|
|
411
|
+
pushIssue(issues, `${path}.default_value`, 'Companies widget default_value must be a number when provided.');
|
|
412
|
+
}
|
|
413
|
+
};
|
|
414
|
+
const validateHeadingProperties = (properties, issues, path) => {
|
|
415
|
+
validateAllowedPropertyKeys(issues, path, properties, HEADING_ALLOWED_PROPERTY_KEYS, 'heading');
|
|
416
|
+
validateNonEmptyString(issues, `${path}.content`, properties.content, 'Heading content');
|
|
417
|
+
const validLevels = new Set(['h1', 'h2', 'h3', 'h4']);
|
|
418
|
+
if (!validLevels.has(properties.level)) {
|
|
419
|
+
pushIssue(issues, `${path}.level`, 'Heading level must be one of h1, h2, h3, h4.');
|
|
420
|
+
}
|
|
421
|
+
if (properties.align !== undefined) {
|
|
422
|
+
const validAligns = new Set(['left', 'center', 'right']);
|
|
423
|
+
if (!validAligns.has(properties.align)) {
|
|
424
|
+
pushIssue(issues, `${path}.align`, 'Heading align must be one of left, center, right.');
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
};
|
|
428
|
+
const validateTextProperties = (properties, issues, path) => {
|
|
429
|
+
validateAllowedPropertyKeys(issues, path, properties, TEXT_ALLOWED_PROPERTY_KEYS, 'text');
|
|
430
|
+
validateNonEmptyString(issues, `${path}.content`, properties.content, 'Text content');
|
|
431
|
+
if (properties.align !== undefined) {
|
|
432
|
+
const validAligns = new Set(['left', 'center', 'right', 'justify']);
|
|
433
|
+
if (!validAligns.has(properties.align)) {
|
|
434
|
+
pushIssue(issues, `${path}.align`, 'Text align must be one of left, center, right, justify.');
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
if (properties.font_size !== undefined) {
|
|
438
|
+
const validSizes = new Set(['xs', 'sm', 'base', 'lg', 'xl']);
|
|
439
|
+
if (!validSizes.has(properties.font_size)) {
|
|
440
|
+
pushIssue(issues, `${path}.font_size`, 'Text font_size must be one of xs, sm, base, lg, xl.');
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
};
|
|
444
|
+
const validateImageProperties = (properties, issues, path) => {
|
|
445
|
+
validateAllowedPropertyKeys(issues, path, properties, IMAGE_ALLOWED_PROPERTY_KEYS, 'image');
|
|
446
|
+
validateNonEmptyString(issues, `${path}.src`, properties.src, 'Image src');
|
|
447
|
+
validateNonEmptyString(issues, `${path}.alt`, properties.alt, 'Image alt');
|
|
448
|
+
if (properties.object_fit !== undefined) {
|
|
449
|
+
const validFits = new Set(['cover', 'contain', 'fill', 'none']);
|
|
450
|
+
if (!validFits.has(properties.object_fit)) {
|
|
451
|
+
pushIssue(issues, `${path}.object_fit`, 'Image object_fit must be one of cover, contain, fill, none.');
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
if (properties.height !== undefined && (!Number.isInteger(properties.height) || properties.height <= 0)) {
|
|
455
|
+
pushIssue(issues, `${path}.height`, 'Image height must be a positive integer when provided.');
|
|
456
|
+
}
|
|
457
|
+
};
|
|
458
|
+
const validateDividerProperties = (properties, issues, path) => {
|
|
459
|
+
validateAllowedPropertyKeys(issues, path, properties, DIVIDER_ALLOWED_PROPERTY_KEYS, 'divider');
|
|
460
|
+
if (properties.style !== undefined) {
|
|
461
|
+
const validStyles = new Set(['solid', 'dashed', 'dotted']);
|
|
462
|
+
if (!validStyles.has(properties.style)) {
|
|
463
|
+
pushIssue(issues, `${path}.style`, 'Divider style must be one of solid, dashed, dotted.');
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
if (properties.thickness !== undefined && (!Number.isInteger(properties.thickness) || properties.thickness <= 0)) {
|
|
467
|
+
pushIssue(issues, `${path}.thickness`, 'Divider thickness must be a positive integer when provided.');
|
|
468
|
+
}
|
|
469
|
+
};
|
|
470
|
+
const validateSpacerProperties = (properties, issues, path) => {
|
|
471
|
+
validateAllowedPropertyKeys(issues, path, properties, SPACER_ALLOWED_PROPERTY_KEYS, 'spacer');
|
|
472
|
+
if (!Number.isInteger(properties.height) || properties.height <= 0) {
|
|
473
|
+
pushIssue(issues, `${path}.height`, 'Spacer height must be a positive integer.');
|
|
474
|
+
}
|
|
475
|
+
};
|
|
476
|
+
const validateWidgetProperties = (widget, issues, path) => {
|
|
477
|
+
validateOfficialWidgetType(issues, `${path}.widget_type`, widget.widget_type);
|
|
478
|
+
switch (widget.widget_type) {
|
|
479
|
+
case 'kpi':
|
|
480
|
+
validateKpiProperties(widget.properties, issues, `${path}.properties`);
|
|
481
|
+
break;
|
|
482
|
+
case 'chart':
|
|
483
|
+
validateChartProperties(widget.properties, issues, `${path}.properties`);
|
|
484
|
+
break;
|
|
485
|
+
case 'grid':
|
|
486
|
+
validateGridProperties(widget.properties, issues, `${path}.properties`);
|
|
487
|
+
break;
|
|
488
|
+
case 'progress-list':
|
|
489
|
+
validateProgressListProperties(widget.properties, issues, `${path}.properties`);
|
|
490
|
+
break;
|
|
491
|
+
case 'panel':
|
|
492
|
+
validatePanelProperties(widget.properties, issues, `${path}.properties`);
|
|
493
|
+
break;
|
|
494
|
+
case 'date':
|
|
495
|
+
validateDateWidgetProperties(widget.properties, issues, `${path}.properties`);
|
|
496
|
+
break;
|
|
497
|
+
case 'companies':
|
|
498
|
+
validateCompaniesWidgetProperties(widget.properties, issues, `${path}.properties`);
|
|
499
|
+
break;
|
|
500
|
+
case 'heading':
|
|
501
|
+
validateHeadingProperties(widget.properties, issues, `${path}.properties`);
|
|
502
|
+
break;
|
|
503
|
+
case 'text':
|
|
504
|
+
validateTextProperties(widget.properties, issues, `${path}.properties`);
|
|
505
|
+
break;
|
|
506
|
+
case 'image':
|
|
507
|
+
validateImageProperties(widget.properties, issues, `${path}.properties`);
|
|
508
|
+
break;
|
|
509
|
+
case 'divider':
|
|
510
|
+
validateDividerProperties(widget.properties, issues, `${path}.properties`);
|
|
511
|
+
break;
|
|
512
|
+
case 'spacer':
|
|
513
|
+
validateSpacerProperties(widget.properties, issues, `${path}.properties`);
|
|
514
|
+
break;
|
|
515
|
+
default:
|
|
516
|
+
break;
|
|
517
|
+
}
|
|
518
|
+
};
|
|
519
|
+
const validateRequiredFilters = (spec, issues) => {
|
|
520
|
+
const hasDateFilter = spec.filters.some((filter) => filter.type === 'date');
|
|
521
|
+
const hasEmpresaFilter = spec.filters.some((filter) => filter.type === 'empresa');
|
|
522
|
+
const sqlTemplates = spec.queries.map((query) => query.sql_template.toLowerCase());
|
|
523
|
+
const dependsOnDate = sqlTemplates.some((sql) => DATE_FILTER_PLACEHOLDERS.some((placeholder) => sql.includes(placeholder.toLowerCase())));
|
|
524
|
+
const dependsOnEmpresa = sqlTemplates.some((sql) => EMPRESA_FILTER_PLACEHOLDERS.some((placeholder) => sql.includes(placeholder.toLowerCase())));
|
|
525
|
+
if (dependsOnDate && !hasDateFilter) {
|
|
526
|
+
pushIssue(issues, 'filters', 'Dashboard queries use date placeholders, so at least one filter of type "date" is required.');
|
|
527
|
+
}
|
|
528
|
+
if (dependsOnEmpresa && !hasEmpresaFilter) {
|
|
529
|
+
pushIssue(issues, 'filters', 'Dashboard queries use company placeholders, so at least one filter of type "empresa" is required.');
|
|
530
|
+
}
|
|
531
|
+
};
|
|
532
|
+
const validateFilters = (filters, issues) => {
|
|
533
|
+
validateUniqueIds(issues, filters.map((filter, index) => ({ id: filter.id, path: `filters[${index}].id` })), 'Filter id');
|
|
534
|
+
filters.forEach((filter, index) => {
|
|
535
|
+
validateStableId(issues, `filters[${index}].id`, filter.id, FILTER_ID_PATTERN, 'Filter id');
|
|
536
|
+
validateNonEmptyString(issues, `filters[${index}].label`, filter.label, 'Filter label');
|
|
537
|
+
if (filter.type === 'date' && filter.binds_to.length === 0) {
|
|
538
|
+
pushIssue(issues, `filters[${index}].binds_to`, 'Date filters must bind at least one runtime parameter.');
|
|
539
|
+
}
|
|
540
|
+
if (filter.type === 'date') {
|
|
541
|
+
validateUniqueStrings(issues, filter.binds_to, `filters[${index}].binds_to`, 'Date filter binding');
|
|
542
|
+
filter.binds_to.forEach((binding, bindingIndex) => {
|
|
543
|
+
validateNonEmptyString(issues, `filters[${index}].binds_to[${bindingIndex}]`, binding, 'Date filter binding');
|
|
544
|
+
});
|
|
545
|
+
}
|
|
546
|
+
if (filter.type === 'empresa') {
|
|
547
|
+
validateNonEmptyString(issues, `filters[${index}].placeholder`, filter.placeholder, 'Empresa filter placeholder');
|
|
548
|
+
validateNonEmptyString(issues, `filters[${index}].sql_template`, filter.sql_template, 'Empresa filter sql_template');
|
|
549
|
+
}
|
|
550
|
+
});
|
|
551
|
+
};
|
|
552
|
+
const validateSections = (sections, issues) => {
|
|
553
|
+
if (sections.length === 0) {
|
|
554
|
+
pushIssue(issues, 'layout.sections', 'At least one section is required in the official TSX signature.');
|
|
555
|
+
}
|
|
556
|
+
validateUniqueIds(issues, sections.map((section, index) => ({ id: section.id, path: `layout.sections[${index}].id` })), 'Section id');
|
|
557
|
+
sections.forEach((section, index) => {
|
|
558
|
+
validateStableId(issues, `layout.sections[${index}].id`, section.id, SECTION_ID_PATTERN, 'Section id');
|
|
559
|
+
validateNonEmptyString(issues, `layout.sections[${index}].title`, section.title, 'Section title');
|
|
560
|
+
if (!Number.isInteger(section.columns) || section.columns <= 0) {
|
|
561
|
+
pushIssue(issues, `layout.sections[${index}].columns`, 'Section columns must be a positive integer.');
|
|
562
|
+
}
|
|
563
|
+
const responsiveEntries = Object.entries(section.responsive ?? {});
|
|
564
|
+
responsiveEntries.forEach(([breakpoint, value]) => {
|
|
565
|
+
if (value !== undefined && (!Number.isInteger(value) || value <= 0)) {
|
|
566
|
+
pushIssue(issues, `layout.sections[${index}].responsive.${breakpoint}`, 'Responsive section columns must be positive integers.');
|
|
567
|
+
}
|
|
568
|
+
});
|
|
569
|
+
validateUniqueStrings(issues, section.items, `layout.sections[${index}].items`, 'Section item');
|
|
570
|
+
});
|
|
571
|
+
};
|
|
572
|
+
const validateQueries = (queries, issues) => {
|
|
573
|
+
validateUniqueIds(issues, queries.map((query, index) => ({ id: query.id, path: `queries[${index}].id` })), 'Query id');
|
|
574
|
+
queries.forEach((query, index) => {
|
|
575
|
+
validateStableId(issues, `queries[${index}].id`, query.id, QUERY_ID_PATTERN, 'Query id');
|
|
576
|
+
validateNonEmptyString(issues, `queries[${index}].description`, query.description, 'Query description');
|
|
577
|
+
validateNonEmptyString(issues, `queries[${index}].sql_template`, query.sql_template, 'Query sql_template');
|
|
578
|
+
if (query.sources.length === 0) {
|
|
579
|
+
pushIssue(issues, `queries[${index}].sources`, 'Queries must declare at least one normative source.');
|
|
580
|
+
}
|
|
581
|
+
if (query.output_fields.length === 0) {
|
|
582
|
+
pushIssue(issues, `queries[${index}].output_fields`, 'Queries must declare at least one output field.');
|
|
583
|
+
}
|
|
584
|
+
validateUniqueStrings(issues, query.sources, `queries[${index}].sources`, 'Query source');
|
|
585
|
+
validateUniqueStrings(issues, query.output_fields, `queries[${index}].output_fields`, 'Query output field');
|
|
586
|
+
query.sources.forEach((source, sourceIndex) => {
|
|
587
|
+
validateNonEmptyString(issues, `queries[${index}].sources[${sourceIndex}]`, source, 'Query source');
|
|
588
|
+
});
|
|
589
|
+
query.output_fields.forEach((field, fieldIndex) => {
|
|
590
|
+
validateNonEmptyString(issues, `queries[${index}].output_fields[${fieldIndex}]`, field, 'Query output field');
|
|
591
|
+
});
|
|
592
|
+
});
|
|
593
|
+
};
|
|
594
|
+
const validateQueryPlaceholders = (queries, issues) => {
|
|
595
|
+
queries.forEach((query, index) => {
|
|
596
|
+
const placeholders = query.sql_template.match(/\$\{[^}]+\}|\{\{[^}]+\}\}/g) ?? [];
|
|
597
|
+
placeholders.forEach((placeholder, placeholderIndex) => {
|
|
598
|
+
const isAccepted = ACCEPTED_QUERY_PLACEHOLDER_PATTERNS.some((pattern) => pattern.test(placeholder));
|
|
599
|
+
if (!isAccepted) {
|
|
600
|
+
pushIssue(issues, `queries[${index}].sql_template`, `Placeholder "${placeholder}" is not allowed by the current WiBi SQL conventions (match #${placeholderIndex + 1}).`);
|
|
601
|
+
}
|
|
602
|
+
});
|
|
603
|
+
});
|
|
604
|
+
};
|
|
605
|
+
const validateSerializedDashboardShape = (spec, issues) => {
|
|
606
|
+
if (typeof spec.metadata.is_public !== 'boolean') {
|
|
607
|
+
pushIssue(issues, 'metadata.is_public', 'metadata.is_public must be a boolean.');
|
|
608
|
+
}
|
|
609
|
+
spec.filters.forEach((filter, index) => {
|
|
610
|
+
if (filter.type === 'date') {
|
|
611
|
+
if (!['D', 'S', 'M', 'T', 'SE', 'A', 'W', 'Q', 'Y', 'SP', 'CUSTOM'].includes(filter.default_period_type)) {
|
|
612
|
+
pushIssue(issues, `filters[${index}].default_period_type`, 'Date filter default_period_type must be one of D, S, M, T, SE, A, W, Q, Y, SP or CUSTOM.');
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
if (filter.type === 'empresa' && typeof filter.default_value !== 'number') {
|
|
616
|
+
pushIssue(issues, `filters[${index}].default_value`, 'Empresa filter default_value must be a number.');
|
|
617
|
+
}
|
|
618
|
+
});
|
|
619
|
+
spec.layout.sections.forEach((section, index) => {
|
|
620
|
+
if (!Array.isArray(section.items)) {
|
|
621
|
+
pushIssue(issues, `layout.sections[${index}].items`, 'Section items must be an array.');
|
|
622
|
+
}
|
|
623
|
+
});
|
|
624
|
+
spec.widgets.forEach((widget, index) => {
|
|
625
|
+
if (widget.query_id !== null && typeof widget.query_id !== 'string') {
|
|
626
|
+
pushIssue(issues, `widgets[${index}].query_id`, 'Widget query_id must be a string or null.');
|
|
627
|
+
}
|
|
628
|
+
if (typeof widget.properties !== 'object' || widget.properties === null || Array.isArray(widget.properties)) {
|
|
629
|
+
pushIssue(issues, `widgets[${index}].properties`, 'Widget properties must be an object.');
|
|
630
|
+
}
|
|
631
|
+
});
|
|
632
|
+
spec.queries.forEach((query, index) => {
|
|
633
|
+
if (!Array.isArray(query.sources)) {
|
|
634
|
+
pushIssue(issues, `queries[${index}].sources`, 'Query sources must be an array.');
|
|
635
|
+
}
|
|
636
|
+
if (!Array.isArray(query.output_fields)) {
|
|
637
|
+
pushIssue(issues, `queries[${index}].output_fields`, 'Query output_fields must be an array.');
|
|
638
|
+
}
|
|
639
|
+
});
|
|
640
|
+
};
|
|
641
|
+
const validateWidgets = (sections, widgets, issues, sectionIds, queryIds) => {
|
|
642
|
+
validateUniqueIds(issues, widgets.map((widget, index) => ({ id: widget.id, path: `widgets[${index}].id` })), 'Widget id');
|
|
643
|
+
const sectionOrders = new Map();
|
|
644
|
+
const sectionColumns = new Map(sections.map((section) => [section.id, section.columns]));
|
|
645
|
+
widgets.forEach((widget, index) => {
|
|
646
|
+
validateStableId(issues, `widgets[${index}].id`, widget.id, STABLE_ID_PATTERN, 'Widget id');
|
|
647
|
+
validateNonEmptyString(issues, `widgets[${index}].title`, widget.title, 'Widget title');
|
|
648
|
+
if (!WIDGET_TYPES.has(widget.widget_type)) {
|
|
649
|
+
pushIssue(issues, `widgets[${index}].widget_type`, `Widget type "${widget.widget_type}" is not supported by the official TSX signature.`);
|
|
650
|
+
}
|
|
651
|
+
if (!sectionIds.has(widget.section_id)) {
|
|
652
|
+
pushIssue(issues, `widgets[${index}].section_id`, `Widget section_id "${widget.section_id}" does not match any declared layout section.`);
|
|
653
|
+
}
|
|
654
|
+
if (widget.query_id && !queryIds.has(widget.query_id)) {
|
|
655
|
+
pushIssue(issues, `widgets[${index}].query_id`, `Widget query_id "${widget.query_id}" does not match any declared query.`);
|
|
656
|
+
}
|
|
657
|
+
if (!DECORATIVE_WIDGET_TYPES.has(widget.widget_type) && widget.widget_type !== 'panel' && !widget.query_id) {
|
|
658
|
+
pushIssue(issues, `widgets[${index}].query_id`, `Widget type "${widget.widget_type}" must declare a query_id.`);
|
|
659
|
+
}
|
|
660
|
+
if (!Number.isInteger(widget.order) || widget.order < 0) {
|
|
661
|
+
pushIssue(issues, `widgets[${index}].order`, 'Widget order must be a non-negative integer.');
|
|
662
|
+
}
|
|
663
|
+
if (!Number.isInteger(widget.col_span) || widget.col_span <= 0) {
|
|
664
|
+
pushIssue(issues, `widgets[${index}].col_span`, 'Widget col_span must be a positive integer.');
|
|
665
|
+
}
|
|
666
|
+
const maxColumns = sectionColumns.get(widget.section_id);
|
|
667
|
+
if (maxColumns !== undefined && widget.col_span > maxColumns) {
|
|
668
|
+
pushIssue(issues, `widgets[${index}].col_span`, `Widget col_span ${widget.col_span} exceeds section "${widget.section_id}" columns (${maxColumns}).`);
|
|
669
|
+
}
|
|
670
|
+
if (widget.height !== undefined && (!Number.isInteger(widget.height) || widget.height <= 0)) {
|
|
671
|
+
pushIssue(issues, `widgets[${index}].height`, 'Widget height must be a positive integer when provided.');
|
|
672
|
+
}
|
|
673
|
+
validateWidgetProperties(widget, issues, `widgets[${index}]`);
|
|
674
|
+
const existingOrders = sectionOrders.get(widget.section_id) ?? new Set();
|
|
675
|
+
if (existingOrders.has(widget.order)) {
|
|
676
|
+
pushIssue(issues, `widgets[${index}].order`, `Widget order ${widget.order} is duplicated inside section "${widget.section_id}".`);
|
|
677
|
+
}
|
|
678
|
+
existingOrders.add(widget.order);
|
|
679
|
+
sectionOrders.set(widget.section_id, existingOrders);
|
|
680
|
+
});
|
|
681
|
+
};
|
|
682
|
+
const validateLayoutItems = (sections, widgets, issues) => {
|
|
683
|
+
const widgetIds = new Set(widgets.map((widget) => widget.id));
|
|
684
|
+
const widgetsBySection = new Map();
|
|
685
|
+
widgets.forEach((widget) => {
|
|
686
|
+
const ids = widgetsBySection.get(widget.section_id) ?? new Set();
|
|
687
|
+
ids.add(widget.id);
|
|
688
|
+
widgetsBySection.set(widget.section_id, ids);
|
|
689
|
+
});
|
|
690
|
+
sections.forEach((section, sectionIndex) => {
|
|
691
|
+
const declaredItems = new Set(section.items);
|
|
692
|
+
const expectedItems = widgetsBySection.get(section.id) ?? new Set();
|
|
693
|
+
if (section.items.length === 0 && expectedItems.size > 0) {
|
|
694
|
+
pushIssue(issues, `layout.sections[${sectionIndex}].items`, 'Section items must reference at least one widget.');
|
|
695
|
+
}
|
|
696
|
+
section.items.forEach((itemId, itemIndex) => {
|
|
697
|
+
if (!widgetIds.has(itemId)) {
|
|
698
|
+
pushIssue(issues, `layout.sections[${sectionIndex}].items[${itemIndex}]`, `Section item "${itemId}" does not match any declared widget.`);
|
|
699
|
+
}
|
|
700
|
+
});
|
|
701
|
+
for (const widgetId of expectedItems) {
|
|
702
|
+
if (!declaredItems.has(widgetId)) {
|
|
703
|
+
pushIssue(issues, `layout.sections[${sectionIndex}].items`, `Section "${section.id}" must include widget "${widgetId}" in layout.items.`);
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
});
|
|
707
|
+
};
|
|
708
|
+
export const validateDashboardSpec = (spec) => {
|
|
709
|
+
const issues = [];
|
|
710
|
+
validateNonEmptyString(issues, 'metadata.name', spec.metadata.name, 'metadata.name');
|
|
711
|
+
validateNonEmptyString(issues, 'metadata.description', spec.metadata.description, 'metadata.description');
|
|
712
|
+
validateNonEmptyString(issues, 'metadata.category', spec.metadata.category, 'metadata.category');
|
|
713
|
+
if (spec.layout.mode !== 'sections') {
|
|
714
|
+
pushIssue(issues, 'layout.mode', 'layout.mode must be "sections" for the current TSX signature.');
|
|
715
|
+
}
|
|
716
|
+
if (spec.metadata.schema_version !== WIBI_DASHBOARD_SPEC_VERSION) {
|
|
717
|
+
pushIssue(issues, 'metadata.schema_version', `metadata.schema_version must be "${WIBI_DASHBOARD_SPEC_VERSION}" for the current TSX signature.`);
|
|
718
|
+
}
|
|
719
|
+
if (!ISO_TIMESTAMP_PATTERN.test(spec.metadata.generated_at)) {
|
|
720
|
+
pushIssue(issues, 'metadata.generated_at', 'metadata.generated_at must be an ISO-8601 UTC timestamp, for example 2026-03-12T00:00:00.000Z.');
|
|
721
|
+
}
|
|
722
|
+
validateFilters(spec.filters, issues);
|
|
723
|
+
validateSections(spec.layout.sections, issues);
|
|
724
|
+
validateQueries(spec.queries, issues);
|
|
725
|
+
validateRequiredFilters(spec, issues);
|
|
726
|
+
validateQueryPlaceholders(spec.queries, issues);
|
|
727
|
+
validateSerializedDashboardShape(spec, issues);
|
|
728
|
+
const sectionIds = new Set(spec.layout.sections.map((section) => section.id));
|
|
729
|
+
const queryIds = new Set(spec.queries.map((query) => query.id));
|
|
730
|
+
validateWidgets(spec.layout.sections, spec.widgets, issues, sectionIds, queryIds);
|
|
731
|
+
validateLayoutItems(spec.layout.sections, spec.widgets, issues);
|
|
732
|
+
if (issues.length > 0) {
|
|
733
|
+
return {
|
|
734
|
+
ok: false,
|
|
735
|
+
issues,
|
|
736
|
+
};
|
|
737
|
+
}
|
|
738
|
+
return {
|
|
739
|
+
ok: true,
|
|
740
|
+
issues: [],
|
|
741
|
+
};
|
|
742
|
+
};
|
|
743
|
+
export const assertValidDashboardSpec = (spec) => {
|
|
744
|
+
const result = validateDashboardSpec(spec);
|
|
745
|
+
if (!result.ok) {
|
|
746
|
+
const details = result.issues.map((issue) => `${issue.path}: ${issue.message}`).join('\n');
|
|
747
|
+
throw new Error(`Invalid WiBi dashboardSpec.\n${details}`);
|
|
748
|
+
}
|
|
749
|
+
return spec;
|
|
750
|
+
};
|
|
751
|
+
export const defineDashboardSpec = (spec) => {
|
|
752
|
+
return assertValidDashboardSpec(spec);
|
|
753
|
+
};
|
|
754
|
+
//# sourceMappingURL=dashboard-spec.js.map
|