@cloudcare/guance-front-tools 1.0.11 → 1.0.13

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