@cloudcare/guance-front-tools 1.0.13 → 1.0.16

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.
@@ -0,0 +1,1770 @@
1
+ const PANEL_TYPE_MAP = {
2
+ stat: 'singlestat',
3
+ singlestat: 'singlestat',
4
+ timeseries: 'sequence',
5
+ graph: 'sequence',
6
+ trend: 'sequence',
7
+ bargauge: 'toplist',
8
+ gauge: 'gauge',
9
+ barchart: 'bar',
10
+ piechart: 'pie',
11
+ table: 'table',
12
+ text: 'text',
13
+ heatmap: 'heatmap',
14
+ histogram: 'histogram',
15
+ treemap: 'treemap',
16
+ geomap: 'worldmap',
17
+ logs: 'log',
18
+ }
19
+
20
+ const GRAFANA_BUILTIN_VARS = new Set([
21
+ '__interval',
22
+ '__interval_ms',
23
+ '__range',
24
+ '__range_s',
25
+ '__range_ms',
26
+ '__from',
27
+ '__to',
28
+ '__dashboard',
29
+ '__name',
30
+ '__org',
31
+ '__user',
32
+ ])
33
+
34
+ const PROMQL_RESERVED_WORDS = new Set([
35
+ 'and',
36
+ 'or',
37
+ 'unless',
38
+ 'by',
39
+ 'without',
40
+ 'on',
41
+ 'ignoring',
42
+ 'group_left',
43
+ 'group_right',
44
+ 'bool',
45
+ 'offset',
46
+ ])
47
+
48
+ const UNIT_MAP = {
49
+ percent: ['percent', 'percent'],
50
+ bytes: ['digital', 'B'],
51
+ decbytes: ['digital', 'B'],
52
+ bits: ['digital', 'b'],
53
+ deckbytes: ['digital', 'KB'],
54
+ decgbytes: ['digital', 'GB'],
55
+ ms: ['time', 'ms'],
56
+ s: ['time', 's'],
57
+ m: ['time', 'min'],
58
+ h: ['time', 'h'],
59
+ d: ['time', 'd'],
60
+ short: ['custom', 'short'],
61
+ none: ['custom', 'none'],
62
+ reqps: ['custom', 'reqps'],
63
+ ops: ['custom', 'ops'],
64
+ }
65
+
66
+ const COMPARE_OPTIONS = {
67
+ hourCompare: { label: '小时同比', value: 'hourCompare' },
68
+ dayCompare: { label: '日同比', value: 'dayCompare' },
69
+ weekCompare: { label: '周同比', value: 'weekCompare' },
70
+ monthCompare: { label: '月同比', value: 'monthCompare' },
71
+ circleCompare: { label: '环比', value: 'circleCompare' },
72
+ }
73
+
74
+ export function convertDashboard(grafanaDashboard, options = {}) {
75
+ const variableNames = new Set((grafanaDashboard.templating?.list || []).map((item) => item?.name).filter(Boolean))
76
+ const state = {
77
+ groups: [],
78
+ groupUnfoldStatus: {},
79
+ charts: [],
80
+ }
81
+
82
+ const sortedPanels = sortPanels(grafanaDashboard.panels || [])
83
+ collectPanels(sortedPanels, state, null, variableNames, options)
84
+
85
+ return pruneEmpty({
86
+ title: grafanaDashboard.title || '',
87
+ description: grafanaDashboard.description || undefined,
88
+ tags: grafanaDashboard.tags || undefined,
89
+ uid: grafanaDashboard.uid || undefined,
90
+ dashboardExtend: {
91
+ groupUnfoldStatus: state.groupUnfoldStatus,
92
+ },
93
+ main: {
94
+ vars: convertVariables(grafanaDashboard.templating?.list || [], variableNames),
95
+ charts: state.charts,
96
+ groups: state.groups,
97
+ type: 'template',
98
+ },
99
+ })
100
+ }
101
+
102
+ function collectPanels(panels, state, inheritedGroup = null, variableNames = new Set(), options = {}) {
103
+ let activeRow = inheritedGroup
104
+ let openRowPanel = null
105
+
106
+ for (const panel of panels) {
107
+ if (panel.type === 'row') {
108
+ const rowName = panel.title || ''
109
+ if (rowName) {
110
+ state.groups.push({ name: rowName })
111
+ state.groupUnfoldStatus[rowName] = !panel.collapsed
112
+ }
113
+
114
+ if (panel.collapsed) {
115
+ collectPanels(sortPanels(panel.panels || []), state, rowName || null, variableNames, options)
116
+ activeRow = inheritedGroup
117
+ openRowPanel = null
118
+ } else {
119
+ activeRow = rowName || null
120
+ openRowPanel = panel
121
+ }
122
+ continue
123
+ }
124
+
125
+ const chart = convertPanel(panel, activeRow, openRowPanel, variableNames, options)
126
+ if (chart) {
127
+ state.charts.push(chart)
128
+ }
129
+ }
130
+ }
131
+
132
+ function sortPanels(panels) {
133
+ return [...panels].sort((left, right) => {
134
+ const leftPos = left.gridPos || {}
135
+ const rightPos = right.gridPos || {}
136
+ const leftY = leftPos.y ?? Number.MAX_SAFE_INTEGER
137
+ const rightY = rightPos.y ?? Number.MAX_SAFE_INTEGER
138
+ if (leftY !== rightY) return leftY - rightY
139
+ const leftX = leftPos.x ?? Number.MAX_SAFE_INTEGER
140
+ const rightX = rightPos.x ?? Number.MAX_SAFE_INTEGER
141
+ if (leftX !== rightX) return leftX - rightX
142
+ return (left.id || 0) - (right.id || 0)
143
+ })
144
+ }
145
+
146
+ function convertVariables(variables, variableNames) {
147
+ return variables
148
+ .map((variable, index) => convertVariable(variable, index, variableNames))
149
+ .filter(Boolean)
150
+ }
151
+
152
+ function convertVariable(variable, index, variableNames) {
153
+ const variableType = String(variable.type || '')
154
+ const current = variable.current || {}
155
+ const currentText = stringifyCurrent(current.text)
156
+ const currentValue = stringifyCurrent(current.value)
157
+ const includeAll = Boolean(variable.includeAll)
158
+ const defaultVal = {
159
+ label: normalizeAllValue(currentText, variable.allValue),
160
+ value: normalizeAllValue(currentValue, variable.allValue, true),
161
+ }
162
+
163
+ const base = {
164
+ name: variable.label || variable.name || '',
165
+ seq: index,
166
+ code: variable.name || `var_${index}`,
167
+ hide: variable.hide && variable.hide !== 0 ? 1 : 0,
168
+ multiple: Boolean(variable.multi),
169
+ includeStar: includeAll,
170
+ valueSort: 'desc',
171
+ extend: pruneEmpty({
172
+ originalType: variableType,
173
+ description: variable.description || undefined,
174
+ starMeaning: includeAll ? '*' : undefined,
175
+ options: Array.isArray(variable.options) ? variable.options : undefined,
176
+ refresh: variable.refresh,
177
+ skipUrlSync: variable.skipUrlSync,
178
+ sort: variable.sort,
179
+ }),
180
+ }
181
+
182
+ if (variableType === 'textbox' || variableType === 'constant' || variableType === 'interval') {
183
+ return pruneEmpty({
184
+ ...base,
185
+ datasource: 'custom',
186
+ type: 'CUSTOM_LIST',
187
+ multiple: false,
188
+ includeStar: false,
189
+ definition: {
190
+ value: variable.query || currentValue || '',
191
+ defaultVal,
192
+ },
193
+ })
194
+ }
195
+
196
+ if (variableType === 'custom' || variableType === 'datasource') {
197
+ return pruneEmpty({
198
+ ...base,
199
+ datasource: 'custom',
200
+ type: 'CUSTOM_LIST',
201
+ definition: {
202
+ value: variable.query || extractCustomOptions(variable.options || []),
203
+ defaultVal,
204
+ },
205
+ })
206
+ }
207
+
208
+ if (variableType === 'query') {
209
+ const queryString = extractVariableQuery(variable)
210
+ const queryKind = inferVariableQueryType(variable, queryString)
211
+ return pruneEmpty({
212
+ ...base,
213
+ datasource: queryKind === 'FIELD' ? 'object' : 'dataflux',
214
+ type: queryKind,
215
+ definition: {
216
+ tag: '',
217
+ field: queryKind === 'FIELD' ? extractFieldName(queryString) : '',
218
+ value: replaceVariables(queryString || '', variableNames),
219
+ metric: extractMetricName(queryString, variableNames),
220
+ object: queryKind === 'FIELD' ? 'HOST' : '',
221
+ defaultVal,
222
+ },
223
+ })
224
+ }
225
+
226
+ return pruneEmpty({
227
+ ...base,
228
+ datasource: 'custom',
229
+ type: 'CUSTOM_LIST',
230
+ multiple: false,
231
+ includeStar: false,
232
+ definition: {
233
+ value: variable.query || currentValue || '',
234
+ defaultVal,
235
+ },
236
+ })
237
+ }
238
+
239
+ function convertPanel(panel, groupName, rowPanel, variableNames, options) {
240
+ const chartType = inferChartType(panel)
241
+ if (!chartType || !panel.gridPos) {
242
+ return null
243
+ }
244
+
245
+ const queries = buildQueries(panel, chartType, variableNames, options)
246
+ const settings = buildSettings(panel, chartType, queries, variableNames)
247
+ const links = extractPanelLinks(panel, variableNames)
248
+ const group = groupName ?? null
249
+ const position = buildPosition(panel, rowPanel)
250
+
251
+ return pruneEmpty({
252
+ name: replaceVariables(panel.title || '', variableNames),
253
+ type: chartType,
254
+ group: { name: group },
255
+ pos: position,
256
+ extend: {
257
+ settings,
258
+ links: links.length ? links : undefined,
259
+ sourcePanelType: options.keepGrafanaMeta ? panel.type : undefined,
260
+ sourcePanelId: options.keepGrafanaMeta ? panel.id : undefined,
261
+ pluginVersion: options.keepGrafanaMeta ? panel.pluginVersion || undefined : undefined,
262
+ grafana: options.keepGrafanaMeta
263
+ ? pruneEmpty({
264
+ fieldConfig: panel.fieldConfig,
265
+ options: panel.options,
266
+ transformations: panel.transformations,
267
+ transparent: panel.transparent,
268
+ repeat: panel.repeat,
269
+ datasource: panel.datasource,
270
+ })
271
+ : undefined,
272
+ },
273
+ queries,
274
+ })
275
+ }
276
+
277
+ function buildPosition(panel, rowPanel) {
278
+ const gridPos = panel.gridPos || {}
279
+ const rowOffset = rowPanel?.gridPos?.y ?? 0
280
+ const rawY = typeof gridPos.y === 'number' ? gridPos.y - rowOffset : 0
281
+
282
+ return {
283
+ x: numberOr(gridPos.x, 0),
284
+ y: round1(rawY * 1.9 + 0.5),
285
+ w: numberOr(gridPos.w, 12),
286
+ h: round1(numberOr(gridPos.h, 8) * 1.9 + 0.1),
287
+ }
288
+ }
289
+
290
+ function buildQueries(panel, chartType, variableNames, options = {}) {
291
+ const queries = []
292
+ const targets = Array.isArray(panel.targets) ? panel.targets : []
293
+
294
+ for (let index = 0; index < targets.length; index++) {
295
+ const target = targets[index]
296
+ const queryText = extractTargetQuery(target)
297
+ if (!queryText) continue
298
+
299
+ const qtype = inferQueryLanguage(target, queryText)
300
+ const normalizedQueryText = normalizeTargetQuery(queryText, qtype, options)
301
+ queries.push(
302
+ pruneEmpty({
303
+ name: target.legendFormat || target.alias || '',
304
+ type: chartType,
305
+ qtype,
306
+ datasource: 'dataflux',
307
+ disabled: Boolean(target.hide),
308
+ query: {
309
+ q: replaceVariables(normalizedQueryText, variableNames),
310
+ code: normalizeQueryCode(target.refId, index),
311
+ type: qtype,
312
+ promqlCode: qtype === 'promql' ? index + 1 : undefined,
313
+ alias: target.legendFormat || target.alias || '',
314
+ field: target.field || undefined,
315
+ },
316
+ extend: pruneEmpty({
317
+ refId: target.refId || undefined,
318
+ datasource: target.datasource || undefined,
319
+ editorMode: target.editorMode || undefined,
320
+ queryMode: target.queryMode || undefined,
321
+ }),
322
+ })
323
+ )
324
+ }
325
+
326
+ if (chartType === 'text' && panel.options?.content) {
327
+ queries.push({
328
+ query: {
329
+ content: replaceVariables(panel.options.content, variableNames),
330
+ },
331
+ })
332
+ }
333
+
334
+ return queries
335
+ }
336
+
337
+ function buildSettings(panel, chartType, queries, variableNames) {
338
+ const defaults = panel.fieldConfig?.defaults || {}
339
+ const custom = defaults.custom || {}
340
+ const options = panel.options || {}
341
+ const legend = options.legend || panel.legend || {}
342
+ const transformationInfo = parseTransformations(panel.transformations || [])
343
+ const aliases = buildAliases(queries)
344
+ const tableColumns = buildTableColumns(panel.fieldConfig, transformationInfo.organize, variableNames)
345
+ const fieldOverrides = buildFieldOverrides(panel.fieldConfig, variableNames)
346
+ const legacyGauge = panel.gauge || {}
347
+ const valueMappings = buildLegacyValueMappings(panel.valueMaps)
348
+ const rangeMappings = buildLegacyRangeMappings(panel.rangeMaps)
349
+ const mappingItems = [...buildMappings(defaults.mappings), ...valueMappings, ...rangeMappings]
350
+ const explicitUnit = firstDefined(defaults.unit, panel.format, panel.yaxes?.[0]?.format)
351
+ const unit = explicitUnit || inferUnitFromQueries(queries, chartType)
352
+ const precision = firstDefinedNumber(defaults.decimals, panel.decimals)
353
+ const min = firstDefinedNumber(defaults.min, legacyGauge.minValue, panel.yaxes?.[0]?.min)
354
+ const max = firstDefinedNumber(defaults.max, legacyGauge.maxValue, panel.yaxes?.[0]?.max)
355
+ const lineWidth = firstDefinedNumber(custom.lineWidth, panel.linewidth)
356
+ const fillOpacity = firstDefinedNumber(custom.fillOpacity, normalizeLegacyFill(panel.fill))
357
+ const reduceOptions = options.reduceOptions || {}
358
+ const legendValues = mapLegendCalcs(legend.calcs?.length ? legend.calcs : extractLegacyLegendCalcs(legend))
359
+ const connectNulls = normalizeConnectNulls(firstDefined(custom.spanNulls, panel.nullPointMode))
360
+ const pointMode = firstDefined(custom.showPoints, panel.points === true ? 'always' : panel.points === false ? 'never' : undefined)
361
+ const graphMode = options.graphMode || undefined
362
+ const legacyTextMode = panel.valueName || undefined
363
+ const workspaceInfo = extractWorkspaceInfo(panel.targets || [])
364
+ const tooltip = options.tooltip || panel.tooltip || {}
365
+ const statText = options.text || {}
366
+ const textInfo = chartType === 'text' ? analyzeTextPanel(options.content, options.mode) : null
367
+ const tableFooter = options.footer || {}
368
+ const tableSortBy = Array.isArray(options.sortBy) ? options.sortBy : []
369
+ const tableCustom = chartType === 'table' ? custom : {}
370
+ const compareInfo = inferCompareSettings(queries, chartType)
371
+ const sortInfo = inferSortSettings(chartType, legend, tableSortBy)
372
+ const customUnits = buildCustomUnits(panel.fieldConfig)
373
+ const customColors = buildCustomColors(panel.fieldConfig)
374
+ const colorMappings = buildColorMappings(panel.fieldConfig, chartType)
375
+ const valColorMappings = buildValColorMappings(panel.fieldConfig, transformationInfo.organize)
376
+ const effectiveUnitType = customUnits.length ? 'custom' : unit ? 'global' : undefined
377
+ const slimit = inferSeriesLimit(queries, options, chartType)
378
+ const settings = {
379
+ showTitle: true,
380
+ titleDesc: panel.description || '',
381
+ isSampling: true,
382
+ changeWorkspace: workspaceInfo.changeWorkspace,
383
+ workspaceUUID: workspaceInfo.workspaceUUID,
384
+ workspaceName: workspaceInfo.workspaceName,
385
+ showFieldMapping: false,
386
+ openThousandsSeparator: true,
387
+ precision: typeof precision === 'number' ? String(precision) : '2',
388
+ timeInterval: normalizeTimeInterval(firstDefined(panel.interval, panel.targets?.find((item) => item.interval)?.interval, 'auto')),
389
+ fixedTime: panel.timeFrom || '',
390
+ maxPointCount: panel.maxDataPoints ?? null,
391
+ showLegend: legend.showLegend,
392
+ legendPostion: mapLegendPlacement(legend.placement),
393
+ legendValues,
394
+ showLegend: firstDefined(legend.showLegend, legend.show),
395
+ showLine: chartType === 'sequence' ? inferShowLine(panel, custom) : undefined,
396
+ lineType: mapLineInterpolation(custom.lineInterpolation),
397
+ connectNulls,
398
+ openStack: inferOpenStack(panel, custom),
399
+ stackType: mapStackType(firstDefined(custom.stacking?.mode, panel.stack ? 'normal' : 'none')),
400
+ chartType: inferDisplayChartType(panel, chartType),
401
+ isTimeInterval: chartType === 'sequence' || chartType === 'bar' || chartType === 'heatmap' || chartType === 'histogram',
402
+ xAxisShowType: chartType === 'sequence' || chartType === 'bar' ? 'time' : undefined,
403
+ unitType: effectiveUnitType,
404
+ globalUnit: customUnits.length ? undefined : mapUnit(unit),
405
+ units: customUnits.length ? customUnits : undefined,
406
+ colors: customColors.length ? customColors : undefined,
407
+ colorMappings: colorMappings.length ? colorMappings : undefined,
408
+ levels: buildLevels(defaults.thresholds),
409
+ slimit,
410
+ mappings: mappingItems,
411
+ alias: aliases,
412
+ min,
413
+ max,
414
+ showPercent: Array.isArray(options.displayLabels) ? options.displayLabels.includes('percent') : undefined,
415
+ showLabel: chartType === 'pie' ? Array.isArray(options.displayLabels) && options.displayLabels.length > 0 : undefined,
416
+ showLabelValue: Array.isArray(options.displayLabels)
417
+ ? options.displayLabels.includes('value') || options.displayLabels.includes('name')
418
+ : undefined,
419
+ direction: options.orientation || undefined,
420
+ queryMode: chartType === 'table' ? 'toGroupColumn' : undefined,
421
+ showTableHead: chartType === 'table' ? options.showHeader !== false : undefined,
422
+ pageEnable: chartType === 'table' ? false : undefined,
423
+ pageSize: chartType === 'table' ? 20 : undefined,
424
+ showColumns: chartType === 'table' ? tableColumns.map((column) => column.title || column.field) : undefined,
425
+ valMappings: chartType === 'table' ? buildTableMappings(panel.fieldConfig, transformationInfo.organize) : undefined,
426
+ valColorMappings: chartType === 'table' && valColorMappings.length ? valColorMappings : undefined,
427
+ legendValueOpen: Array.isArray(legend.values) ? legend.values.includes('value') : undefined,
428
+ legendValuePercentOpen: Array.isArray(legend.values) ? legend.values.includes('percent') : undefined,
429
+ showTopSize: chartType === 'toplist' ? true : undefined,
430
+ topSize: chartType === 'toplist' ? extractReduceLimit(options) : undefined,
431
+ scientificNotation: unit === 'short' ? true : undefined,
432
+ mainMeasurementQueryCode: queries[0]?.query?.code || undefined,
433
+ mainMeasurementLimit: chartType === 'pie' ? extractReduceLimit(options) : undefined,
434
+ color: defaults.color?.fixedColor || undefined,
435
+ fontColor: options.colorMode === 'value' ? defaults.color?.fixedColor : undefined,
436
+ bgColor: options.colorMode === 'background' ? defaults.color?.fixedColor : undefined,
437
+ sequenceChartType: chartType === 'singlestat' && graphMode ? inferSequenceChartType(panel, graphMode) : undefined,
438
+ showLineAxis: chartType === 'singlestat' ? graphMode !== 'none' : undefined,
439
+ repeatChartVariable: typeof panel.repeat === 'string' && panel.repeat ? panel.repeat : undefined,
440
+ repeatChartRowLimit: typeof panel.maxPerRow === 'number' ? panel.maxPerRow : undefined,
441
+ compares: compareInfo.compares,
442
+ compareType: compareInfo.compareType,
443
+ openCompare: compareInfo.openCompare,
444
+ compareChartType: compareInfo.compareChartType,
445
+ mainMeasurementSort: sortInfo.mainMeasurementSort,
446
+ sorderByOrder: sortInfo.sorderByOrder,
447
+ }
448
+
449
+ const links = extractPanelLinks(panel)
450
+ if (links.length) {
451
+ settings.queryCodes = queries.map((query) => query.query?.code).filter(Boolean)
452
+ }
453
+
454
+ settings.extend = pruneEmpty({
455
+ appearance: pruneEmpty({
456
+ lineWidth,
457
+ fillOpacity,
458
+ gradientMode: custom.gradientMode || undefined,
459
+ pointMode,
460
+ pointSize: firstDefinedNumber(custom.pointSize, panel.pointradius),
461
+ axisPlacement: custom.axisPlacement || undefined,
462
+ axisLabel: custom.axisLabel || undefined,
463
+ axisColorMode: custom.axisColorMode || undefined,
464
+ axisCenteredZero: typeof custom.axisCenteredZero === 'boolean' ? custom.axisCenteredZero : undefined,
465
+ axisSoftMin: numberOrUndefined(custom.axisSoftMin),
466
+ axisSoftMax: numberOrUndefined(custom.axisSoftMax),
467
+ barAlignment: numberOrUndefined(custom.barAlignment),
468
+ scaleDistribution: custom.scaleDistribution || undefined,
469
+ drawStyle: custom.drawStyle || undefined,
470
+ lineStyle: custom.lineStyle || undefined,
471
+ spanNulls: custom.spanNulls,
472
+ stackingGroup: custom.stacking?.group || undefined,
473
+ graphMode,
474
+ colorMode: options.colorMode || undefined,
475
+ fieldColorMode: defaults.color?.mode || undefined,
476
+ fixedColor: defaults.color?.fixedColor || undefined,
477
+ thresholdsMode: defaults.thresholds?.mode || undefined,
478
+ thresholdsStyleMode: custom.thresholdsStyle?.mode || undefined,
479
+ textMode: options.textMode || legacyTextMode,
480
+ reduceCalcs: Array.isArray(reduceOptions.calcs) ? reduceOptions.calcs : undefined,
481
+ reduceFields: reduceOptions.fields || undefined,
482
+ reduceValues: typeof reduceOptions.values === 'boolean' ? reduceOptions.values : undefined,
483
+ pieType: options.pieType || undefined,
484
+ gaugeMode: chartType === 'gauge' || panel.type === 'singlestat' ? inferGaugeMode(panel, options, legacyGauge) : undefined,
485
+ thresholdMarkers: typeof legacyGauge.thresholdMarkers === 'boolean' ? legacyGauge.thresholdMarkers : undefined,
486
+ thresholdLabels: typeof legacyGauge.thresholdLabels === 'boolean' ? legacyGauge.thresholdLabels : undefined,
487
+ hideFrom: custom.hideFrom || undefined,
488
+ justifyMode: options.justifyMode || undefined,
489
+ titleSize: typeof statText.titleSize === 'number' ? statText.titleSize : undefined,
490
+ valueSize: typeof statText.valueSize === 'number' ? statText.valueSize : undefined,
491
+ }),
492
+ legend: pruneEmpty({
493
+ displayMode: legend.displayMode || undefined,
494
+ sortBy: legend.sortBy || legend.sort || undefined,
495
+ sortDesc: typeof legend.sortDesc === 'boolean' ? legend.sortDesc : undefined,
496
+ width: firstDefinedNumber(legend.width, legend.sideWidth),
497
+ }),
498
+ tooltip: pruneEmpty({
499
+ mode: tooltip.mode || tooltip.sharedMode || undefined,
500
+ sort: tooltip.sort || tooltip.value_type || undefined,
501
+ }),
502
+ table: chartType === 'table'
503
+ ? pruneEmpty({
504
+ align: tableCustom.align || undefined,
505
+ displayMode: tableCustom.displayMode || undefined,
506
+ sortBy: tableSortBy,
507
+ footer: pruneEmpty({
508
+ fields: tableFooter.fields || undefined,
509
+ reducer: Array.isArray(tableFooter.reducer) ? tableFooter.reducer : undefined,
510
+ show: typeof tableFooter.show === 'boolean' ? tableFooter.show : undefined,
511
+ }),
512
+ })
513
+ : undefined,
514
+ text: textInfo || undefined,
515
+ tableColumns: chartType === 'table' && tableColumns.length ? tableColumns : undefined,
516
+ fieldOverrides: fieldOverrides.length ? fieldOverrides : undefined,
517
+ transformations: transformationInfo.normalized.length ? transformationInfo.normalized : undefined,
518
+ fieldFilterPattern: transformationInfo.fieldFilterPattern || undefined,
519
+ valueFilters: transformationInfo.valueFilters.length ? transformationInfo.valueFilters : undefined,
520
+ layout: pruneEmpty({
521
+ repeatDirection: panel.repeatDirection || undefined,
522
+ }),
523
+ })
524
+
525
+ return pruneEmpty(settings)
526
+ }
527
+
528
+ function buildLevels(thresholds) {
529
+ const steps = Array.isArray(thresholds?.steps) ? thresholds.steps : []
530
+ return steps
531
+ .filter((step) => typeof step.value === 'number' || typeof step.color === 'string')
532
+ .map((step, index) => ({
533
+ title: `Level ${index + 1}`,
534
+ value: typeof step.value === 'number' ? step.value : 0,
535
+ bgColor: normalizeColor(step.color),
536
+ }))
537
+ }
538
+
539
+ function buildMappings(mappings) {
540
+ if (!Array.isArray(mappings)) return []
541
+ const result = []
542
+
543
+ for (const mapping of mappings) {
544
+ if (mapping.type === 'value' && mapping.options && typeof mapping.options === 'object') {
545
+ for (const [rawValue, item] of Object.entries(mapping.options)) {
546
+ result.push({
547
+ originalVal: [rawValue],
548
+ operation: '=',
549
+ mappingVal: item.text || rawValue,
550
+ })
551
+ }
552
+ }
553
+
554
+ if (mapping.type === 'range' && mapping.options) {
555
+ const from = mapping.options.from
556
+ const to = mapping.options.to
557
+ result.push({
558
+ originalVal: [String(from ?? ''), String(to ?? '')],
559
+ operation: 'between',
560
+ mappingVal: mapping.options.result?.text || '',
561
+ })
562
+ }
563
+ }
564
+
565
+ return result
566
+ }
567
+
568
+ function buildTableMappings(fieldConfig, organize) {
569
+ const overrideMappings = []
570
+ const overrides = Array.isArray(fieldConfig?.overrides) ? fieldConfig.overrides : []
571
+ const organizeMaps = createOrganizeMaps(organize)
572
+
573
+ for (const override of overrides) {
574
+ const field = override.matcher?.options
575
+ if (!field) continue
576
+ const displayField = resolveDisplayFieldName(field, organizeMaps)
577
+ const properties = Array.isArray(override.properties) ? override.properties : []
578
+ for (const property of properties) {
579
+ if (property.id !== 'mappings') continue
580
+ const mappings = buildMappings(property.value)
581
+ for (const mapping of mappings) {
582
+ overrideMappings.push({
583
+ field: displayField,
584
+ ...mapping,
585
+ })
586
+ }
587
+ }
588
+ }
589
+
590
+ return overrideMappings
591
+ }
592
+
593
+ function buildTableColumns(fieldConfig, organize, variableNames = new Set()) {
594
+ const columns = new Map()
595
+ const defaultCustom = fieldConfig?.defaults?.custom || {}
596
+ const overrides = Array.isArray(fieldConfig?.overrides) ? fieldConfig.overrides : []
597
+ const organizeMaps = createOrganizeMaps(organize)
598
+ const { renamedFields, excludedFields, indexedFields } = organizeMaps
599
+
600
+ for (const override of overrides) {
601
+ const rawField = override.matcher?.options
602
+ if (!rawField) continue
603
+ const field = resolveRawFieldName(rawField, organizeMaps)
604
+ if (excludedFields[field] === true || excludedFields[rawField] === true) continue
605
+ const columnKey = resolveDisplayFieldName(field, organizeMaps)
606
+ const currentColumn = columns.get(columnKey) || {
607
+ field,
608
+ title: columnKey,
609
+ order: typeof indexedFields[field] === 'number' ? indexedFields[field] : undefined,
610
+ align: defaultCustom.align || undefined,
611
+ displayMode: defaultCustom.displayMode || undefined,
612
+ }
613
+ for (const property of override.properties || []) {
614
+ if (property.id === 'custom.width') {
615
+ currentColumn.width = property.value
616
+ }
617
+ if (property.id === 'custom.align') {
618
+ currentColumn.align = property.value
619
+ }
620
+ if (property.id === 'custom.displayMode') {
621
+ currentColumn.displayMode = property.value
622
+ }
623
+ if (property.id === 'links') {
624
+ currentColumn.links = Array.isArray(property.value)
625
+ ? property.value.map((link) => normalizeGuanceLinkItem(link, variableNames))
626
+ : undefined
627
+ }
628
+ if (property.id === 'mappings') {
629
+ currentColumn.mappings = buildMappings(property.value)
630
+ }
631
+ }
632
+ columns.set(columnKey, pruneEmpty(currentColumn))
633
+ }
634
+
635
+ if (organize) {
636
+ for (const [field, order] of Object.entries(indexedFields)) {
637
+ if (excludedFields[field] === true) continue
638
+ const columnKey = resolveDisplayFieldName(field, organizeMaps)
639
+ if (columns.has(columnKey)) {
640
+ const currentColumn = columns.get(columnKey)
641
+ currentColumn.field = field
642
+ currentColumn.order = typeof order === 'number' ? order : currentColumn.order
643
+ currentColumn.title = columnKey
644
+ columns.set(columnKey, pruneEmpty(currentColumn))
645
+ continue
646
+ }
647
+ columns.set(
648
+ columnKey,
649
+ pruneEmpty({
650
+ field,
651
+ title: columnKey,
652
+ order: typeof order === 'number' ? order : undefined,
653
+ })
654
+ )
655
+ }
656
+ }
657
+
658
+ return finalizeTableColumns([...columns.values()])
659
+ }
660
+
661
+ function buildAliases(queries) {
662
+ return queries
663
+ .filter((query) => query.query?.alias)
664
+ .map((query) => ({
665
+ alias: query.query.alias,
666
+ key: query.query.code || query.name || '',
667
+ name: query.query.code || query.name || '',
668
+ }))
669
+ }
670
+
671
+ function buildCustomUnits(fieldConfig) {
672
+ const overrides = Array.isArray(fieldConfig?.overrides) ? fieldConfig.overrides : []
673
+ const units = []
674
+
675
+ for (const override of overrides) {
676
+ const rawKey = String(override?.matcher?.options || '').trim()
677
+ if (!rawKey) continue
678
+
679
+ const unitProperty = Array.isArray(override?.properties)
680
+ ? override.properties.find((property) => property?.id === 'unit')
681
+ : undefined
682
+ if (!unitProperty || typeof unitProperty.value !== 'string') continue
683
+
684
+ const unit = unitProperty.value
685
+ units.push(
686
+ pruneEmpty({
687
+ key: rawKey,
688
+ name: rawKey,
689
+ unit,
690
+ units: mapUnit(unit),
691
+ })
692
+ )
693
+ }
694
+
695
+ return units
696
+ }
697
+
698
+ function buildCustomColors(fieldConfig) {
699
+ const overrides = Array.isArray(fieldConfig?.overrides) ? fieldConfig.overrides : []
700
+ const colors = []
701
+
702
+ for (const override of overrides) {
703
+ const rawKey = String(override?.matcher?.options || '').trim()
704
+ if (!rawKey) continue
705
+
706
+ const colorProperty = Array.isArray(override?.properties)
707
+ ? override.properties.find((property) => property?.id === 'color')
708
+ : undefined
709
+ const fixedColor = colorProperty?.value?.fixedColor
710
+ const colorMode = colorProperty?.value?.mode
711
+ if (typeof fixedColor !== 'string' || !fixedColor) continue
712
+ if (colorMode && colorMode !== 'fixed') continue
713
+
714
+ colors.push(
715
+ pruneEmpty({
716
+ key: rawKey,
717
+ name: rawKey,
718
+ color: normalizeColor(fixedColor),
719
+ })
720
+ )
721
+ }
722
+
723
+ return colors
724
+ }
725
+
726
+ function buildColorMappings(fieldConfig, chartType) {
727
+ if (chartType !== 'toplist') return []
728
+
729
+ const steps = Array.isArray(fieldConfig?.defaults?.thresholds?.steps) ? fieldConfig.defaults.thresholds.steps : []
730
+ if (steps.length === 0) return []
731
+
732
+ const mappings = []
733
+ for (let index = 0; index < steps.length; index++) {
734
+ const current = steps[index]
735
+ const next = steps[index + 1]
736
+ const bgColor = normalizeColor(current?.color)
737
+ const start = current?.value
738
+ const end = next?.value
739
+
740
+ if (typeof start === 'number' && typeof end === 'number') {
741
+ mappings.push({
742
+ value: [start, end],
743
+ bgColor,
744
+ operation: 'between',
745
+ })
746
+ continue
747
+ }
748
+
749
+ if (typeof start === 'number') {
750
+ mappings.push({
751
+ value: [start],
752
+ bgColor,
753
+ operation: '>=',
754
+ })
755
+ continue
756
+ }
757
+
758
+ if (typeof end === 'number') {
759
+ mappings.push({
760
+ value: [end],
761
+ bgColor,
762
+ operation: '<',
763
+ })
764
+ }
765
+ }
766
+
767
+ return mappings
768
+ }
769
+
770
+ function buildValColorMappings(fieldConfig, organize) {
771
+ const overrides = Array.isArray(fieldConfig?.overrides) ? fieldConfig.overrides : []
772
+ const organizeMaps = createOrganizeMaps(organize)
773
+ const mappings = []
774
+
775
+ for (const override of overrides) {
776
+ const rawField = override?.matcher?.options
777
+ if (!rawField) continue
778
+
779
+ const field = resolveDisplayFieldName(resolveRawFieldName(rawField, organizeMaps), organizeMaps)
780
+ const properties = Array.isArray(override?.properties) ? override.properties : []
781
+ const mappingProperty = properties.find((property) => property?.id === 'mappings')
782
+ if (!mappingProperty) continue
783
+
784
+ const colorProperty = properties.find((property) => property?.id === 'color')
785
+ const fixedColor = typeof colorProperty?.value?.fixedColor === 'string' ? normalizeColor(colorProperty.value.fixedColor) : ''
786
+ const tableMappings = buildMappings(mappingProperty.value)
787
+
788
+ for (const mapping of tableMappings) {
789
+ mappings.push(
790
+ pruneEmpty({
791
+ field,
792
+ value: mapping.originalVal,
793
+ bgColor: '',
794
+ fontColor: fixedColor,
795
+ lineColor: '',
796
+ operation: mapping.operation,
797
+ })
798
+ )
799
+ }
800
+ }
801
+
802
+ return mappings
803
+ }
804
+
805
+ function buildFieldOverrides(fieldConfig, variableNames = new Set()) {
806
+ const overrides = Array.isArray(fieldConfig?.overrides) ? fieldConfig.overrides : []
807
+ const normalized = []
808
+
809
+ for (const override of overrides) {
810
+ const properties = Array.isArray(override.properties) ? override.properties : []
811
+ const normalizedProperties = properties
812
+ .map((property) => normalizeOverrideProperty(property, variableNames))
813
+ .filter(Boolean)
814
+
815
+ if (normalizedProperties.length === 0) continue
816
+
817
+ normalized.push(
818
+ pruneEmpty({
819
+ matcher: pruneEmpty({
820
+ id: override.matcher?.id || undefined,
821
+ options: override.matcher?.options,
822
+ }),
823
+ properties: normalizedProperties,
824
+ })
825
+ )
826
+ }
827
+
828
+ return normalized
829
+ }
830
+
831
+ function normalizeOverrideProperty(property, variableNames = new Set()) {
832
+ if (!property?.id) return undefined
833
+
834
+ if (property.id === 'links') {
835
+ return pruneEmpty({
836
+ id: property.id,
837
+ value: Array.isArray(property.value)
838
+ ? property.value.map((link) => normalizeGuanceLinkItem(link, variableNames))
839
+ : undefined,
840
+ })
841
+ }
842
+
843
+ return pruneEmpty({
844
+ id: property.id,
845
+ value: property.value,
846
+ })
847
+ }
848
+
849
+ function analyzeTextPanel(content, mode) {
850
+ if (typeof content !== 'string' || !content.trim()) return undefined
851
+
852
+ const normalizedMode = typeof mode === 'string' ? mode : undefined
853
+ const containsScript = /<script\b/i.test(content)
854
+ const containsHtml = /<[a-z][\s\S]*>/i.test(content)
855
+ const contentKind = containsScript ? 'interactive_html' : containsHtml ? 'html' : 'markdown'
856
+ const actions = extractTextActions(content)
857
+
858
+ return pruneEmpty({
859
+ mode: normalizedMode,
860
+ contentKind,
861
+ containsScript: containsScript || undefined,
862
+ actions: actions.length ? actions : undefined,
863
+ })
864
+ }
865
+
866
+ function extractTextActions(content) {
867
+ const actions = []
868
+ const seen = new Set()
869
+
870
+ const anchorRegex = /<a\b[^>]*href=(['"])(.*?)\1[^>]*>([\s\S]*?)<\/a>/gi
871
+ for (const match of content.matchAll(anchorRegex)) {
872
+ const url = (match[2] || '').trim()
873
+ const label = stripHtmlTags(match[3] || '').trim()
874
+ pushTextAction(actions, seen, {
875
+ title: label || undefined,
876
+ url: url || undefined,
877
+ open: url === '#' ? 'curWin' : 'newWin',
878
+ type: inferGuanceLinkType({ title: label, url }),
879
+ show: true,
880
+ showChanged: false,
881
+ })
882
+ }
883
+
884
+ const constUrlRegex = /\bconst\s+([A-Za-z_$][A-Za-z0-9_$]*)\s*=\s*([`'"])([\s\S]*?)\2\s*;/g
885
+ const constantUrls = new Map()
886
+ for (const match of content.matchAll(constUrlRegex)) {
887
+ constantUrls.set(match[1], match[3])
888
+ }
889
+
890
+ const windowOpenRegex = /window\.open\(\s*([A-Za-z_$][A-Za-z0-9_$]*|[`'"][\s\S]*?[`'"])\s*(?:,\s*([`'"][\s\S]*?[`'"]))?\s*\)/g
891
+ for (const match of content.matchAll(windowOpenRegex)) {
892
+ const rawTarget = match[1]?.trim() || ''
893
+ const rawBlank = match[2]?.trim() || ''
894
+ const directUrl = unwrapQuoted(rawTarget)
895
+ const variableUrl = constantUrls.get(rawTarget)
896
+ const url = directUrl || variableUrl
897
+ pushTextAction(actions, seen, {
898
+ title: inferActionTitle(content, url),
899
+ url: url || undefined,
900
+ open: rawBlank.includes('_blank') ? 'newWin' : 'curWin',
901
+ type: inferGuanceLinkType({ title: inferActionTitle(content, url), url }),
902
+ show: true,
903
+ showChanged: false,
904
+ })
905
+ }
906
+
907
+ return actions
908
+ }
909
+
910
+ function pushTextAction(actions, seen, action) {
911
+ const normalized = pruneEmpty(action)
912
+ const key = JSON.stringify(normalized)
913
+ if (!key || seen.has(key) || Object.keys(normalized).length === 0) return
914
+ seen.add(key)
915
+ actions.push(normalized)
916
+ }
917
+
918
+ function inferActionTitle(content, url) {
919
+ if (typeof url !== 'string' || !url) return undefined
920
+ if (content.includes('HotCall') || url.includes('hotcall')) return 'HotCall'
921
+ if (content.includes('业务大盘')) return '业务大盘'
922
+ if (content.includes('拓扑图') || content.includes('traceLink')) return '跳转观测云'
923
+ return undefined
924
+ }
925
+
926
+ function unwrapQuoted(value) {
927
+ if (typeof value !== 'string') return ''
928
+ const trimmed = value.trim()
929
+ if (trimmed.length < 2) return ''
930
+ const quote = trimmed[0]
931
+ if ((quote === '"' || quote === "'" || quote === '`') && trimmed.at(-1) === quote) {
932
+ return trimmed.slice(1, -1)
933
+ }
934
+ return ''
935
+ }
936
+
937
+ function stripHtmlTags(value) {
938
+ return String(value).replace(/<[^>]+>/g, ' ')
939
+ }
940
+
941
+ function normalizeColumnTitle(field, renamedField) {
942
+ if (typeof renamedField === 'string' && renamedField.trim()) return renamedField.trim()
943
+ return field
944
+ }
945
+
946
+ function finalizeTableColumns(columns) {
947
+ const mergedColumns = new Map()
948
+
949
+ for (const column of columns) {
950
+ const key = column.title || column.field
951
+ const existing = mergedColumns.get(key)
952
+ if (!existing) {
953
+ mergedColumns.set(key, { ...column })
954
+ continue
955
+ }
956
+
957
+ mergedColumns.set(
958
+ key,
959
+ pruneEmpty({
960
+ ...existing,
961
+ ...column,
962
+ field: existing.field || column.field,
963
+ title: key,
964
+ order: Math.min(numberOr(existing.order, Number.MAX_SAFE_INTEGER), numberOr(column.order, Number.MAX_SAFE_INTEGER)),
965
+ width: firstDefined(existing.width, column.width),
966
+ align: firstDefined(existing.align, column.align),
967
+ displayMode: firstDefined(existing.displayMode, column.displayMode),
968
+ links: existing.links || column.links,
969
+ mappings: existing.mappings || column.mappings,
970
+ })
971
+ )
972
+ }
973
+
974
+ return [...mergedColumns.values()].sort(
975
+ (left, right) => numberOr(left.order, Number.MAX_SAFE_INTEGER) - numberOr(right.order, Number.MAX_SAFE_INTEGER)
976
+ )
977
+ }
978
+
979
+ function createOrganizeMaps(organize) {
980
+ const renamedFields = organize?.renameByName || {}
981
+ const excludedFields = organize?.excludeByName || {}
982
+ const indexedFields = organize?.indexByName || {}
983
+ const displayToRaw = {}
984
+
985
+ for (const [rawField, renamedField] of Object.entries(renamedFields)) {
986
+ const title = normalizeColumnTitle(rawField, renamedField)
987
+ if (title && title !== rawField) {
988
+ displayToRaw[title] = rawField
989
+ }
990
+ }
991
+
992
+ return {
993
+ renamedFields,
994
+ excludedFields,
995
+ indexedFields,
996
+ displayToRaw,
997
+ }
998
+ }
999
+
1000
+ function resolveRawFieldName(field, organizeMaps) {
1001
+ return organizeMaps.displayToRaw[field] || field
1002
+ }
1003
+
1004
+ function resolveDisplayFieldName(field, organizeMaps) {
1005
+ return normalizeColumnTitle(field, organizeMaps.renamedFields[field])
1006
+ }
1007
+
1008
+ function parseTransformations(transformations) {
1009
+ const normalized = []
1010
+ let organize = null
1011
+ let fieldFilterPattern = ''
1012
+ const valueFilters = []
1013
+
1014
+ for (const transformation of Array.isArray(transformations) ? transformations : []) {
1015
+ if (!transformation?.id) continue
1016
+
1017
+ if (transformation.id === 'organize') {
1018
+ organize = transformation.options || {}
1019
+ normalized.push(
1020
+ pruneEmpty({
1021
+ type: 'organize',
1022
+ renameByName: organize.renameByName,
1023
+ excludeByName: organize.excludeByName,
1024
+ indexByName: organize.indexByName,
1025
+ })
1026
+ )
1027
+ continue
1028
+ }
1029
+
1030
+ if (transformation.id === 'filterFieldsByName') {
1031
+ fieldFilterPattern = transformation.options?.include?.pattern || transformation.options?.exclude?.pattern || ''
1032
+ normalized.push(
1033
+ pruneEmpty({
1034
+ type: 'filterFieldsByName',
1035
+ include: transformation.options?.include,
1036
+ exclude: transformation.options?.exclude,
1037
+ })
1038
+ )
1039
+ continue
1040
+ }
1041
+
1042
+ if (transformation.id === 'filterByValue') {
1043
+ const filters = Array.isArray(transformation.options?.filters) ? transformation.options.filters : []
1044
+ valueFilters.push(
1045
+ pruneEmpty({
1046
+ match: transformation.options?.match || undefined,
1047
+ type: transformation.options?.type || undefined,
1048
+ filters,
1049
+ })
1050
+ )
1051
+ normalized.push(
1052
+ pruneEmpty({
1053
+ type: 'filterByValue',
1054
+ match: transformation.options?.match || undefined,
1055
+ mode: transformation.options?.type || undefined,
1056
+ filters,
1057
+ })
1058
+ )
1059
+ continue
1060
+ }
1061
+
1062
+ normalized.push(
1063
+ pruneEmpty({
1064
+ type: transformation.id,
1065
+ options: transformation.options,
1066
+ })
1067
+ )
1068
+ }
1069
+
1070
+ return {
1071
+ organize,
1072
+ fieldFilterPattern,
1073
+ valueFilters,
1074
+ normalized,
1075
+ }
1076
+ }
1077
+
1078
+ function extractPanelLinks(panel, variableNames) {
1079
+ const links = []
1080
+ const defaults = panel.fieldConfig?.defaults || {}
1081
+ const overrideLinks = Array.isArray(panel.fieldConfig?.overrides)
1082
+ ? panel.fieldConfig.overrides.flatMap((override) =>
1083
+ (override.properties || [])
1084
+ .filter((property) => property.id === 'links')
1085
+ .flatMap((property) => property.value || [])
1086
+ )
1087
+ : []
1088
+
1089
+ const allLinks = [
1090
+ ...(Array.isArray(panel.links) ? panel.links : []),
1091
+ ...(Array.isArray(defaults.links) ? defaults.links : []),
1092
+ ...overrideLinks,
1093
+ ]
1094
+
1095
+ for (const link of allLinks) {
1096
+ if (!link || !link.url) continue
1097
+ links.push(normalizeGuanceLinkItem(link, variableNames))
1098
+ }
1099
+
1100
+ return links
1101
+ }
1102
+
1103
+ function normalizeGuanceLinkItem(link, variableNames = new Set()) {
1104
+ return pruneEmpty({
1105
+ url: replaceVariables(link.url || '', variableNames),
1106
+ open: Boolean(link.targetBlank) ? 'newWin' : 'curWin',
1107
+ show: true,
1108
+ type: inferGuanceLinkType(link),
1109
+ showChanged: false,
1110
+ })
1111
+ }
1112
+
1113
+ function inferGuanceLinkType(link) {
1114
+ const title = String(link?.title || '').toLowerCase()
1115
+ const url = String(link?.url || '').toLowerCase()
1116
+ const combined = `${title} ${url}`
1117
+
1118
+ if (url.includes('pipeline-log')) return 'custom'
1119
+ if (url.includes('/tracing/')) return 'tracing'
1120
+ if (url.includes('/logindi/') || url.includes('/log/') || url.includes('/logging/')) return 'logging'
1121
+ if (url.includes('/objectadmin/docker_containers') || url.includes('/container')) return 'container'
1122
+ if (url.includes('/objectadmin/host_processes') || url.includes('/process')) return 'processes'
1123
+ if (url.includes('/scene/builtinview/detail') || url.includes('/host')) return 'host'
1124
+ if (title.includes('trace') || title.includes('tracing')) return 'tracing'
1125
+ if (title.includes('日志')) return 'logging'
1126
+ return 'custom'
1127
+ }
1128
+
1129
+ function inferChartType(panel) {
1130
+ return PANEL_TYPE_MAP[panel.type] || null
1131
+ }
1132
+
1133
+ function inferDisplayChartType(panel, chartType) {
1134
+ if (chartType === 'pie') {
1135
+ return panel.options?.pieType === 'donut' ? 'donut' : 'pie'
1136
+ }
1137
+
1138
+ if (chartType === 'bar') return 'bar'
1139
+ if (chartType === 'toplist') return 'bar'
1140
+
1141
+ const drawStyle = panel.fieldConfig?.defaults?.custom?.drawStyle
1142
+ const fillOpacity = panel.fieldConfig?.defaults?.custom?.fillOpacity
1143
+ if (drawStyle === 'bars') return 'bar'
1144
+ if (fillOpacity && fillOpacity > 0) return 'areaLine'
1145
+ return 'line'
1146
+ }
1147
+
1148
+ function inferQueryLanguage(target, queryText) {
1149
+ const datasourceType = getDatasourceType(target.datasource)
1150
+ // Guance datasource defaults to DQL unless the Grafana target explicitly marks the query as PromQL.
1151
+ if (target.qtype === 'promql') return 'promql'
1152
+ if (target.qtype === 'dql') return 'dql'
1153
+ if (datasourceType.includes('guance-guance-datasource')) return 'dql'
1154
+ if (isPrometheusLikeDatasource(datasourceType)) return 'promql'
1155
+ if (isDqlLikeDatasource(datasourceType)) return 'dql'
1156
+ if (/^\s*(with|select)\b/i.test(queryText)) return 'dql'
1157
+ if (/^[A-Z]::/.test(queryText)) return 'dql'
1158
+ return 'promql'
1159
+ }
1160
+
1161
+ function inferVariableQueryType(variable, queryString) {
1162
+ const datasourceType = getDatasourceType(variable.datasource)
1163
+ const explicitQtype = String(variable.query?.qtype || '').toLowerCase()
1164
+ if (datasourceType.includes('object')) return 'FIELD'
1165
+ if (explicitQtype === 'promql') return 'PROMQL_QUERY'
1166
+ if (explicitQtype === 'dql') return 'QUERY'
1167
+ if (isDqlLikeDatasource(datasourceType) && /^\s*(with|select)\b/i.test(queryString)) return 'QUERY'
1168
+ if (/field_values\(/i.test(queryString) || /label_values\(/i.test(queryString)) return 'QUERY'
1169
+ if (/^[A-Z]::/.test(queryString) || /L\('/.test(queryString)) return 'QUERY'
1170
+ if (/^\s*(with|select)\b/i.test(queryString)) return 'QUERY'
1171
+ return 'PROMQL_QUERY'
1172
+ }
1173
+
1174
+ function extractVariableQuery(variable) {
1175
+ if (typeof variable.query === 'string') return variable.query
1176
+ if (variable.query && typeof variable.query === 'object') {
1177
+ return variable.query.rawQuery || variable.query.query || variable.query.expr || ''
1178
+ }
1179
+ if (typeof variable.definition === 'string') return variable.definition
1180
+ return ''
1181
+ }
1182
+
1183
+ function extractTargetQuery(target) {
1184
+ const candidates = [target.expr, target.query, target.queryText, target.expression, target.rawSql]
1185
+ for (const candidate of candidates) {
1186
+ if (typeof candidate === 'string' && candidate.trim()) return candidate
1187
+ }
1188
+ return ''
1189
+ }
1190
+
1191
+ function extractWorkspaceInfo(targets) {
1192
+ const workspaceUUIDs = []
1193
+ const workspaceNames = []
1194
+
1195
+ for (const target of Array.isArray(targets) ? targets : []) {
1196
+ for (const item of Array.isArray(target.workspaceUUIDs) ? target.workspaceUUIDs : []) {
1197
+ if (item?.value && !workspaceUUIDs.includes(item.value)) workspaceUUIDs.push(item.value)
1198
+ if (item?.label && !workspaceNames.includes(item.label)) workspaceNames.push(item.label)
1199
+ }
1200
+ }
1201
+
1202
+ return pruneEmpty({
1203
+ changeWorkspace: workspaceUUIDs.length > 0,
1204
+ workspaceUUID: workspaceUUIDs.length ? workspaceUUIDs.join(',') : undefined,
1205
+ workspaceName: workspaceNames.length ? workspaceNames : undefined,
1206
+ })
1207
+ }
1208
+
1209
+ function normalizeTargetQuery(queryText, qtype, options = {}) {
1210
+ if (qtype !== 'promql') return queryText
1211
+ if (!options.guancePromqlCompatible) return queryText
1212
+ return normalizePromqlForGuance(queryText)
1213
+ }
1214
+
1215
+ function extractMetricName(queryString, variableNames) {
1216
+ if (/^\s*(with|select)\b/i.test(queryString)) return ''
1217
+ const labelValuesMatch = queryString.match(/label_values\(([^,]+),\s*([^)]+)\)/i)
1218
+ if (labelValuesMatch) return replaceVariables(labelValuesMatch[1].trim(), variableNames)
1219
+ return ''
1220
+ }
1221
+
1222
+ function extractFieldName(queryString) {
1223
+ const fieldValuesMatch = queryString.match(/field_values\(`?([^`)\s]+)`?\)/i)
1224
+ if (fieldValuesMatch) return fieldValuesMatch[1].trim()
1225
+ const labelValuesMatch = queryString.match(/label_values\([^,]+,\s*([^)]+)\)/i)
1226
+ if (labelValuesMatch) return labelValuesMatch[1].replace(/[`'"]/g, '').trim()
1227
+ return ''
1228
+ }
1229
+
1230
+ function normalizeTimeInterval(value) {
1231
+ if (!value || typeof value !== 'string') return 'auto'
1232
+ const normalized = value.trim()
1233
+ if (!normalized) return 'auto'
1234
+ return normalized
1235
+ }
1236
+
1237
+ function normalizePromqlForGuance(queryText) {
1238
+ if (typeof queryText !== 'string' || !queryText.trim()) return queryText
1239
+ let result = ''
1240
+ let index = 0
1241
+ let braceDepth = 0
1242
+
1243
+ while (index < queryText.length) {
1244
+ const current = queryText[index]
1245
+
1246
+ if (current === '{') {
1247
+ braceDepth++
1248
+ result += current
1249
+ index++
1250
+ continue
1251
+ }
1252
+
1253
+ if (current === '}') {
1254
+ braceDepth = Math.max(0, braceDepth - 1)
1255
+ result += current
1256
+ index++
1257
+ continue
1258
+ }
1259
+
1260
+ if (braceDepth === 0 && /[A-Za-z_:]/.test(current)) {
1261
+ let end = index + 1
1262
+ while (end < queryText.length && /[A-Za-z0-9_:]/.test(queryText[end])) end++
1263
+ const token = queryText.slice(index, end)
1264
+ let lookahead = end
1265
+ while (lookahead < queryText.length && /\s/.test(queryText[lookahead])) lookahead++
1266
+ const next = queryText[lookahead]
1267
+ if (next === '{' || next === '[') {
1268
+ result += toGuancePromqlMetricName(token)
1269
+ index = end
1270
+ continue
1271
+ }
1272
+ }
1273
+
1274
+ result += current
1275
+ index++
1276
+ }
1277
+
1278
+ return result
1279
+ }
1280
+
1281
+ function inferUnitFromQueries(queries, chartType) {
1282
+ if (!Array.isArray(queries) || queries.length === 0) return undefined
1283
+ if (['text', 'table', 'topology', 'iframe', 'picture', 'video'].includes(chartType)) return undefined
1284
+
1285
+ const inferredUnits = queries
1286
+ .map((query) => inferUnitFromQueryText(query?.query?.q || ''))
1287
+ .filter(Boolean)
1288
+
1289
+ if (inferredUnits.length === 0) return undefined
1290
+
1291
+ const counts = new Map()
1292
+ for (const unit of inferredUnits) {
1293
+ counts.set(unit, (counts.get(unit) || 0) + 1)
1294
+ }
1295
+
1296
+ let bestUnit = inferredUnits[0]
1297
+ let bestCount = counts.get(bestUnit) || 0
1298
+ for (const unit of inferredUnits) {
1299
+ const currentCount = counts.get(unit) || 0
1300
+ if (currentCount > bestCount) {
1301
+ bestUnit = unit
1302
+ bestCount = currentCount
1303
+ }
1304
+ }
1305
+
1306
+ return bestUnit
1307
+ }
1308
+
1309
+ function inferCompareSettings(queries, chartType) {
1310
+ if (!Array.isArray(queries) || queries.length === 0) {
1311
+ return { compares: undefined, compareType: undefined, openCompare: undefined, compareChartType: undefined }
1312
+ }
1313
+
1314
+ if (!['sequence', 'singlestat'].includes(chartType)) {
1315
+ return { compares: undefined, compareType: undefined, openCompare: undefined, compareChartType: undefined }
1316
+ }
1317
+
1318
+ const compareTypes = []
1319
+ for (const query of queries) {
1320
+ const compareType = inferCompareTypeFromQuery(query?.query?.q || '')
1321
+ if (compareType && !compareTypes.includes(compareType)) {
1322
+ compareTypes.push(compareType)
1323
+ }
1324
+ }
1325
+
1326
+ if (compareTypes.length === 0) {
1327
+ return { compares: undefined, compareType: undefined, openCompare: undefined, compareChartType: undefined }
1328
+ }
1329
+
1330
+ return {
1331
+ compares: compareTypes.map((type) => COMPARE_OPTIONS[type]).filter(Boolean),
1332
+ compareType: compareTypes,
1333
+ openCompare: true,
1334
+ compareChartType: chartType,
1335
+ }
1336
+ }
1337
+
1338
+ function inferCompareTypeFromQuery(queryText) {
1339
+ if (typeof queryText !== 'string' || !queryText.trim()) return undefined
1340
+ const normalized = queryText.toLowerCase()
1341
+
1342
+ if (/\boffset\s+1h\b/.test(normalized)) return 'hourCompare'
1343
+ if (/\boffset\s+1d\b/.test(normalized)) return 'dayCompare'
1344
+ if (/\boffset\s+(7d|1w)\b/.test(normalized)) return 'weekCompare'
1345
+ if (/\boffset\s+(30d|4w)\b/.test(normalized)) return 'monthCompare'
1346
+
1347
+ return undefined
1348
+ }
1349
+
1350
+ function inferSortSettings(chartType, legend, tableSortBy) {
1351
+ const sequenceSort = inferSequenceSortOrder(legend)
1352
+ const mainMeasurementSort = inferMainMeasurementSort(chartType, legend, tableSortBy)
1353
+
1354
+ return {
1355
+ sorderByOrder: sequenceSort,
1356
+ mainMeasurementSort,
1357
+ }
1358
+ }
1359
+
1360
+ function inferSeriesLimit(queries, options, chartType) {
1361
+ const explicitLimit = extractReduceLimit(options)
1362
+ if (['pie', 'toplist', 'treemap'].includes(chartType)) {
1363
+ return extractTopkLimit(queries) || explicitLimit
1364
+ }
1365
+
1366
+ if (['sequence', 'table'].includes(chartType)) {
1367
+ return extractTopkLimit(queries) || undefined
1368
+ }
1369
+
1370
+ return undefined
1371
+ }
1372
+
1373
+ function extractTopkLimit(queries) {
1374
+ if (!Array.isArray(queries)) return undefined
1375
+
1376
+ for (const query of queries) {
1377
+ const queryText = String(query?.query?.q || '')
1378
+ const match = queryText.match(/\btopk\s*\(\s*(\d+)/i)
1379
+ if (!match) continue
1380
+ const limit = Number(match[1])
1381
+ if (Number.isFinite(limit)) return limit
1382
+ }
1383
+
1384
+ return undefined
1385
+ }
1386
+
1387
+ function inferSequenceSortOrder(legend) {
1388
+ if (typeof legend?.sortDesc === 'boolean') {
1389
+ return legend.sortDesc ? 'desc' : 'asc'
1390
+ }
1391
+ return undefined
1392
+ }
1393
+
1394
+ function inferMainMeasurementSort(chartType, legend, tableSortBy) {
1395
+ if (!['pie', 'toplist', 'table', 'treemap'].includes(chartType)) return undefined
1396
+
1397
+ if (chartType === 'table') {
1398
+ const primarySort = Array.isArray(tableSortBy) ? tableSortBy[0] : undefined
1399
+ if (primarySort && typeof primarySort.desc === 'boolean') {
1400
+ return primarySort.desc ? 'top' : 'bottom'
1401
+ }
1402
+ }
1403
+
1404
+ if (typeof legend?.sortDesc === 'boolean') {
1405
+ return legend.sortDesc ? 'top' : 'bottom'
1406
+ }
1407
+
1408
+ return undefined
1409
+ }
1410
+
1411
+ function inferUnitFromQueryText(queryText) {
1412
+ if (typeof queryText !== 'string' || !queryText.trim()) return undefined
1413
+ const normalized = queryText.toLowerCase()
1414
+ const isRateLike = /rate\(|irate\(|increase\(|delta\(|deriv\(/.test(normalized)
1415
+
1416
+ if (
1417
+ /cpu.*(?:usage|utili[sz]ation|used)|cpu_usage|cpu_used|usage_seconds_total/.test(normalized) &&
1418
+ /limit|quota|max|capacity|total|cores?|100/.test(normalized)
1419
+ ) {
1420
+ return 'percent'
1421
+ }
1422
+
1423
+ if (
1424
+ /memory.*(?:usage|used|utili[sz]ation)|heap.*used|rss|working_set|used_bytes|usage_bytes/.test(normalized)
1425
+ ) {
1426
+ return 'bytes'
1427
+ }
1428
+
1429
+ if (
1430
+ /disk.*(?:usage|used|utili[sz]ation)|filesystem.*(?:avail|free|size|used)|storage.*(?:used|usage)/.test(normalized)
1431
+ ) {
1432
+ return 'bytes'
1433
+ }
1434
+
1435
+ if (
1436
+ /error_rate|success_rate|failure_rate|biz_error_rate|_ratio\b|_percent\b|percent/.test(normalized) ||
1437
+ /container_cpu_usage_seconds_total/.test(normalized) && /kube_pod_container_resource_limits/.test(normalized) ||
1438
+ /\*\s*100\b/.test(normalized)
1439
+ ) {
1440
+ return 'percent'
1441
+ }
1442
+
1443
+ if (
1444
+ /p99|p95|p90|latency|duration|response_time|cost\b|elapsed|load_time|_ms\b|milliseconds?/.test(normalized) ||
1445
+ /performance_host_interface_p\d+/.test(normalized)
1446
+ ) {
1447
+ return 'ms'
1448
+ }
1449
+
1450
+ if (
1451
+ /_bytes\b|_bytes_total\b|memory|heap|rss|bandwidth|byte\b/.test(normalized)
1452
+ ) {
1453
+ return 'bytes'
1454
+ }
1455
+
1456
+ if (
1457
+ /gc_pause_seconds|gc.*(?:pause|time)|duration_seconds|latency_seconds|response_seconds/.test(normalized)
1458
+ ) {
1459
+ return 's'
1460
+ }
1461
+
1462
+ if (
1463
+ /\bqps\b|\brps\b|reqps|requests?_per_second|interface_qps|host_qps|requests?_total/.test(normalized) && isRateLike
1464
+ ) {
1465
+ return 'reqps'
1466
+ }
1467
+
1468
+ if (
1469
+ /\btps\b|\biops\b|\bops\b|operations?_per_second|ops_total/.test(normalized) && isRateLike
1470
+ ) {
1471
+ return 'ops'
1472
+ }
1473
+
1474
+ if (
1475
+ /cpu:load\d+s|load5s|load1s|load15s|system_load/.test(normalized)
1476
+ ) {
1477
+ return 'short'
1478
+ }
1479
+
1480
+ if (
1481
+ !isRateLike &&
1482
+ /goroutines?|threads?|connections?|conn_count|fd|file_descriptors?|queue(_size|_depth)?|pool(_size)?|inflight|pending|blocked|active_requests|jvm_.*_count|_count\b|_total\b|count_over_time\(/.test(normalized)
1483
+ ) {
1484
+ return 'none'
1485
+ }
1486
+
1487
+ if (
1488
+ /_seconds\b/.test(normalized) && !/rate\(|increase\(|irate\(/.test(normalized)
1489
+ ) {
1490
+ return 's'
1491
+ }
1492
+
1493
+ return undefined
1494
+ }
1495
+
1496
+ function toGuancePromqlMetricName(token) {
1497
+ if (!token) return token
1498
+ if (token.includes(':')) return token
1499
+ if (!token.includes('_')) return token
1500
+ if (token.startsWith('__')) return token
1501
+ if (PROMQL_RESERVED_WORDS.has(token)) return token
1502
+ const firstUnderscore = token.indexOf('_')
1503
+ if (firstUnderscore <= 0 || firstUnderscore === token.length - 1) return token
1504
+ return `${token.slice(0, firstUnderscore)}:${token.slice(firstUnderscore + 1)}`
1505
+ }
1506
+
1507
+ function getDatasourceType(datasource) {
1508
+ return String(datasource?.type || datasource || '').toLowerCase()
1509
+ }
1510
+
1511
+ function isPrometheusLikeDatasource(datasourceType) {
1512
+ return datasourceType.includes('prometheus') || datasourceType.includes('guance-guance-datasource')
1513
+ }
1514
+
1515
+ function isDqlLikeDatasource(datasourceType) {
1516
+ return (
1517
+ datasourceType.includes('mysql') ||
1518
+ datasourceType.includes('postgres') ||
1519
+ datasourceType.includes('mssql') ||
1520
+ datasourceType.includes('sql') ||
1521
+ datasourceType.includes('loki') ||
1522
+ datasourceType.includes('elasticsearch') ||
1523
+ datasourceType.includes('opensearch') ||
1524
+ datasourceType.includes('cloudwatch') ||
1525
+ datasourceType.includes('influx') ||
1526
+ datasourceType.includes('tempo') ||
1527
+ datasourceType.includes('jaeger') ||
1528
+ datasourceType.includes('zipkin')
1529
+ )
1530
+ }
1531
+
1532
+ function extractReduceLimit(options) {
1533
+ const limit = options.reduceOptions?.limit
1534
+ return typeof limit === 'number' ? limit : 10
1535
+ }
1536
+
1537
+ function extractCustomOptions(options) {
1538
+ if (!Array.isArray(options)) return ''
1539
+ return options
1540
+ .filter((item) => item && item.value !== '$__all')
1541
+ .map((item) => item.text || item.value || '')
1542
+ .filter(Boolean)
1543
+ .join(',')
1544
+ }
1545
+
1546
+ function mapLegendPlacement(value) {
1547
+ if (value === 'bottom') return 'bottom'
1548
+ if (value === 'right') return 'right'
1549
+ if (value === 'left') return 'left'
1550
+ if (value === 'top') return 'top'
1551
+ return 'none'
1552
+ }
1553
+
1554
+ function mapLegendCalcs(values) {
1555
+ if (!Array.isArray(values)) return []
1556
+ const allowed = new Set(['first', 'last', 'avg', 'min', 'max', 'sum', 'count'])
1557
+ return values
1558
+ .map((value) => String(value).toLowerCase())
1559
+ .filter((value) => allowed.has(value))
1560
+ }
1561
+
1562
+ function extractLegacyLegendCalcs(legend) {
1563
+ if (!legend || typeof legend !== 'object') return []
1564
+ const calcs = []
1565
+ if (legend.current) calcs.push('last')
1566
+ if (legend.avg) calcs.push('avg')
1567
+ if (legend.min) calcs.push('min')
1568
+ if (legend.max) calcs.push('max')
1569
+ if (legend.total) calcs.push('sum')
1570
+ return calcs
1571
+ }
1572
+
1573
+ function mapLineInterpolation(value) {
1574
+ if (value === 'smooth') return 'smooth'
1575
+ if (value === 'stepAfter') return 'stepAfter'
1576
+ if (value === 'stepBefore') return 'stepBefore'
1577
+ return 'linear'
1578
+ }
1579
+
1580
+ function mapStackType(value) {
1581
+ if (value === 'percent') return 'percent'
1582
+ if (value === 'normal') return 'time'
1583
+ return 'time'
1584
+ }
1585
+
1586
+ function inferShowLine(panel, custom) {
1587
+ if (typeof panel.lines === 'boolean') return panel.lines
1588
+ if (custom.drawStyle === 'bars') return false
1589
+ return true
1590
+ }
1591
+
1592
+ function inferOpenStack(panel, custom) {
1593
+ if (custom.stacking?.mode) return custom.stacking.mode !== 'none'
1594
+ if (typeof panel.stack === 'boolean') return panel.stack
1595
+ return undefined
1596
+ }
1597
+
1598
+ function inferSequenceChartType(panel, graphMode) {
1599
+ if (graphMode === 'area') return 'line'
1600
+ if (graphMode === 'none') return undefined
1601
+ return inferDisplayChartType(panel, 'sequence') === 'bar' ? 'bar' : 'line'
1602
+ }
1603
+
1604
+ function inferGaugeMode(panel, options, legacyGauge) {
1605
+ if (panel.type === 'gauge') return 'gauge'
1606
+ if (legacyGauge.show) return 'gauge'
1607
+ if (options.graphMode === 'none') return 'value'
1608
+ return 'value'
1609
+ }
1610
+
1611
+ function normalizeConnectNulls(value) {
1612
+ if (typeof value === 'boolean') return value
1613
+ if (typeof value === 'number') return value > 0
1614
+ if (value === 'connected') return true
1615
+ if (value === 'null' || value === 'null as zero') return false
1616
+ return undefined
1617
+ }
1618
+
1619
+ function mapUnit(unit) {
1620
+ if (!unit) return []
1621
+ const mapped = UNIT_MAP[String(unit).toLowerCase()]
1622
+ if (mapped) return mapped
1623
+ return ['custom', String(unit)]
1624
+ }
1625
+
1626
+ function buildLegacyValueMappings(valueMaps) {
1627
+ if (!Array.isArray(valueMaps)) return []
1628
+ return valueMaps
1629
+ .filter((item) => item && Object.prototype.hasOwnProperty.call(item, 'value'))
1630
+ .map((item) => ({
1631
+ originalVal: [String(item.value)],
1632
+ operation: normalizeLegacyMappingOperation(item.op),
1633
+ mappingVal: item.text || String(item.value),
1634
+ }))
1635
+ }
1636
+
1637
+ function buildLegacyRangeMappings(rangeMaps) {
1638
+ if (!Array.isArray(rangeMaps)) return []
1639
+ return rangeMaps.map((item) => ({
1640
+ originalVal: [String(item.from ?? ''), String(item.to ?? '')],
1641
+ operation: 'between',
1642
+ mappingVal: item.text || '',
1643
+ }))
1644
+ }
1645
+
1646
+ function normalizeLegacyMappingOperation(value) {
1647
+ const allowed = new Set(['>', '>=', '<', '<=', '=', '!=', 'between', '=~', '!=~', 'nodata'])
1648
+ if (allowed.has(value)) return value
1649
+ return '='
1650
+ }
1651
+
1652
+ function normalizeLegacyFill(value) {
1653
+ if (typeof value !== 'number') return undefined
1654
+ return Math.max(0, Math.min(100, value * 10))
1655
+ }
1656
+
1657
+ function firstDefinedNumber(...values) {
1658
+ for (const value of values) {
1659
+ if (typeof value === 'number' && Number.isFinite(value)) return value
1660
+ }
1661
+ return undefined
1662
+ }
1663
+
1664
+ function replaceVariables(input, variableNames = new Set()) {
1665
+ if (typeof input !== 'string') return input
1666
+ return input
1667
+ .replace(/\$\{([^}]+)\}/g, (match, expression) => {
1668
+ const variable = normalizeTemplateVariable(expression)
1669
+ if (!variable) return match
1670
+ if (GRAFANA_BUILTIN_VARS.has(variable)) return match
1671
+ if (!variableNames.has(variable)) return match
1672
+ return `#{${variable}}`
1673
+ })
1674
+ .replace(/(^|[^{])\$([A-Za-z0-9_]+)/g, (match, prefix, variable) => {
1675
+ if (GRAFANA_BUILTIN_VARS.has(variable)) return match
1676
+ if (!variableNames.has(variable)) return match
1677
+ return `${prefix}#{${variable}}`
1678
+ })
1679
+ }
1680
+
1681
+ function normalizeTemplateVariable(expression) {
1682
+ const trimmed = String(expression).trim()
1683
+ if (!trimmed) return ''
1684
+ const beforeFormat = trimmed.split(':')[0]
1685
+ if (!/^[A-Za-z0-9_.]+$/.test(beforeFormat)) return ''
1686
+ return beforeFormat
1687
+ }
1688
+
1689
+ function normalizeQueryCode(refId, index) {
1690
+ if (typeof refId === 'string' && refId.trim()) return refId.trim()
1691
+ const codePoint = 65 + index
1692
+ return String.fromCharCode(codePoint)
1693
+ }
1694
+
1695
+ function stringifyCurrent(value) {
1696
+ if (Array.isArray(value)) return value.join(',')
1697
+ if (typeof value === 'string') return value
1698
+ if (typeof value === 'number' || typeof value === 'boolean') return String(value)
1699
+ return ''
1700
+ }
1701
+
1702
+ function normalizeAllValue(value, allValue, isValue = false) {
1703
+ if (value === 'All') return allValue === '.*' ? '*' : 'all values'
1704
+ if (value === '$__all') return allValue === '.*' ? '*' : '__all__'
1705
+ if (isValue && value === '.*') return '*'
1706
+ return value
1707
+ }
1708
+
1709
+ function normalizeColor(color) {
1710
+ if (!color) return '#999999'
1711
+ return String(color)
1712
+ }
1713
+
1714
+ function numberOr(value, fallback) {
1715
+ return typeof value === 'number' && Number.isFinite(value) ? value : fallback
1716
+ }
1717
+
1718
+ function numberOrUndefined(value) {
1719
+ return typeof value === 'number' && Number.isFinite(value) ? value : undefined
1720
+ }
1721
+
1722
+ function round1(value) {
1723
+ return Number(value.toFixed(1))
1724
+ }
1725
+
1726
+ function firstDefined(...values) {
1727
+ for (const value of values) {
1728
+ if (value !== undefined && value !== null && value !== '') return value
1729
+ }
1730
+ return undefined
1731
+ }
1732
+
1733
+ function pruneEmpty(value) {
1734
+ if (Array.isArray(value)) {
1735
+ return value
1736
+ .map((item) => pruneEmpty(item))
1737
+ .filter((item) => item !== undefined)
1738
+ }
1739
+
1740
+ if (!value || typeof value !== 'object') {
1741
+ return value
1742
+ }
1743
+
1744
+ const entries = Object.entries(value)
1745
+ .map(([key, current]) => [key, pruneEmpty(current)])
1746
+ .filter(([, current]) => {
1747
+ if (current === undefined) return false
1748
+ if (Array.isArray(current) && current.length === 0) return false
1749
+ if (current && typeof current === 'object' && !Array.isArray(current) && Object.keys(current).length === 0)
1750
+ return false
1751
+ return true
1752
+ })
1753
+
1754
+ return Object.fromEntries(entries)
1755
+ }
1756
+
1757
+ function readJson(filePath) {
1758
+ return JSON.parse(fs.readFileSync(filePath, 'utf8'))
1759
+ }
1760
+
1761
+ function forEachFile(directoryPath, callback) {
1762
+ for (const entry of fs.readdirSync(directoryPath, { withFileTypes: true })) {
1763
+ const entryPath = path.join(directoryPath, entry.name)
1764
+ if (entry.isDirectory()) {
1765
+ forEachFile(entryPath, callback)
1766
+ } else {
1767
+ callback(entryPath)
1768
+ }
1769
+ }
1770
+ }