@sentio/cli 3.4.2-rc.1 → 3.5.0-rc.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/index.js +502 -36
- package/package.json +1 -1
- package/src/commands/dashboard.ts +472 -0
- package/src/commands/processor.ts +178 -3
- package/src/index.ts +2 -0
package/package.json
CHANGED
|
@@ -0,0 +1,472 @@
|
|
|
1
|
+
import { WebService } from '@sentio/api'
|
|
2
|
+
import { Command } from '@commander-js/extra-typings'
|
|
3
|
+
import process from 'process'
|
|
4
|
+
import yaml from 'yaml'
|
|
5
|
+
import {
|
|
6
|
+
CliError,
|
|
7
|
+
createApiContext,
|
|
8
|
+
handleCommandError,
|
|
9
|
+
loadJsonInput,
|
|
10
|
+
resolveProjectRef,
|
|
11
|
+
unwrapApiResult
|
|
12
|
+
} from '../api.js'
|
|
13
|
+
import { buildEventsInsightQueryBody, buildMetricsInsightQueryBody } from './data.js'
|
|
14
|
+
|
|
15
|
+
interface DashboardOptions {
|
|
16
|
+
host?: string
|
|
17
|
+
apiKey?: string
|
|
18
|
+
token?: string
|
|
19
|
+
project?: string
|
|
20
|
+
owner?: string
|
|
21
|
+
name?: string
|
|
22
|
+
projectId?: string
|
|
23
|
+
json?: boolean
|
|
24
|
+
yaml?: boolean
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
interface DashboardImportOptions extends DashboardOptions {
|
|
28
|
+
file?: string
|
|
29
|
+
stdin?: boolean
|
|
30
|
+
overrideLayouts?: boolean
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
interface AddPanelOptions extends DashboardOptions {
|
|
34
|
+
panelName?: string
|
|
35
|
+
type?: string
|
|
36
|
+
sql?: string
|
|
37
|
+
size?: number
|
|
38
|
+
event?: string
|
|
39
|
+
metric?: string
|
|
40
|
+
alias?: string
|
|
41
|
+
sourceName?: string
|
|
42
|
+
filter?: string[]
|
|
43
|
+
groupBy?: string[]
|
|
44
|
+
aggr?: string
|
|
45
|
+
func?: string[]
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function createDashboardCommand() {
|
|
49
|
+
const dashboardCommand = new Command('dashboard').description('Manage Sentio dashboards')
|
|
50
|
+
dashboardCommand.addCommand(createDashboardListCommand())
|
|
51
|
+
dashboardCommand.addCommand(createDashboardExportCommand())
|
|
52
|
+
dashboardCommand.addCommand(createDashboardImportCommand())
|
|
53
|
+
dashboardCommand.addCommand(createDashboardAddPanelCommand())
|
|
54
|
+
return dashboardCommand
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function createDashboardListCommand() {
|
|
58
|
+
return withOutputOptions(
|
|
59
|
+
withSharedProjectOptions(withAuthOptions(new Command('list').description('List dashboards for a project')))
|
|
60
|
+
)
|
|
61
|
+
.showHelpAfterError()
|
|
62
|
+
.action(async (options, command) => {
|
|
63
|
+
try {
|
|
64
|
+
await runDashboardList(options)
|
|
65
|
+
} catch (error) {
|
|
66
|
+
handleDashboardCommandError(error, command)
|
|
67
|
+
}
|
|
68
|
+
})
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function createDashboardExportCommand() {
|
|
72
|
+
return withOutputOptions(
|
|
73
|
+
withSharedProjectOptions(
|
|
74
|
+
withAuthOptions(
|
|
75
|
+
new Command('export').description('Export a dashboard as JSON').argument('<dashboardId>', 'Dashboard ID')
|
|
76
|
+
)
|
|
77
|
+
)
|
|
78
|
+
)
|
|
79
|
+
.showHelpAfterError()
|
|
80
|
+
.action(async (dashboardId, options, command) => {
|
|
81
|
+
try {
|
|
82
|
+
await runDashboardExport(dashboardId, options)
|
|
83
|
+
} catch (error) {
|
|
84
|
+
handleDashboardCommandError(error, command)
|
|
85
|
+
}
|
|
86
|
+
})
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function createDashboardImportCommand() {
|
|
90
|
+
return withOutputOptions(
|
|
91
|
+
withSharedProjectOptions(
|
|
92
|
+
withAuthOptions(
|
|
93
|
+
new Command('import')
|
|
94
|
+
.description('Import dashboard data from a JSON file into an existing dashboard')
|
|
95
|
+
.argument('<dashboardId>', 'Target dashboard ID to import into')
|
|
96
|
+
)
|
|
97
|
+
)
|
|
98
|
+
)
|
|
99
|
+
.showHelpAfterError()
|
|
100
|
+
.option('--file <path>', 'Read dashboard JSON from file')
|
|
101
|
+
.option('--stdin', 'Read dashboard JSON from stdin')
|
|
102
|
+
.option('--override-layouts', 'Override the layout of the target dashboard')
|
|
103
|
+
.action(async (dashboardId, options, command) => {
|
|
104
|
+
try {
|
|
105
|
+
await runDashboardImport(dashboardId, options)
|
|
106
|
+
} catch (error) {
|
|
107
|
+
handleDashboardCommandError(error, command)
|
|
108
|
+
}
|
|
109
|
+
})
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function createDashboardAddPanelCommand() {
|
|
113
|
+
return withOutputOptions(
|
|
114
|
+
withSharedProjectOptions(
|
|
115
|
+
withAuthOptions(
|
|
116
|
+
new Command('add-panel')
|
|
117
|
+
.description('Add a panel to a dashboard (SQL or insights query)')
|
|
118
|
+
.argument('<dashboardId>', 'Dashboard ID')
|
|
119
|
+
)
|
|
120
|
+
)
|
|
121
|
+
)
|
|
122
|
+
.showHelpAfterError()
|
|
123
|
+
.requiredOption('--panel-name <name>', 'Panel name')
|
|
124
|
+
.requiredOption('--type <type>', 'Chart type: TABLE, LINE, BAR, PIE, QUERY_VALUE, AREA, BAR_GAUGE, SCATTER')
|
|
125
|
+
.option('--sql <query>', 'SQL query (plain SQL — automatically wrapped into the required format)')
|
|
126
|
+
.option('--size <count>', 'SQL query result size limit (default: 100)')
|
|
127
|
+
.option('--event <name>', 'Event name for an insights panel (mutually exclusive with --sql and --metric)')
|
|
128
|
+
.option('--metric <name>', 'Metric name for an insights panel (mutually exclusive with --sql and --event)')
|
|
129
|
+
.option('--alias <alias>', 'Alias for the insights query')
|
|
130
|
+
.option('--source-name <name>', 'Source name for the insights query')
|
|
131
|
+
.option(
|
|
132
|
+
'--filter <selector>',
|
|
133
|
+
'Event filter or metric label selector like field:value or amount>0',
|
|
134
|
+
collectOption,
|
|
135
|
+
[]
|
|
136
|
+
)
|
|
137
|
+
.option('--group-by <field>', 'Group by event property or metric label', collectOption, [])
|
|
138
|
+
.option('--aggr <aggregation>', 'Event: total|unique|AAU|DAU|WAU|MAU. Metric: avg|sum|min|max|count')
|
|
139
|
+
.option('--func <function>', 'Function like topk(1), bottomk(1)', collectOption, [])
|
|
140
|
+
.addHelpText(
|
|
141
|
+
'after',
|
|
142
|
+
`
|
|
143
|
+
|
|
144
|
+
Data source: use exactly one of --sql, --event, or --metric.
|
|
145
|
+
|
|
146
|
+
SQL panel examples:
|
|
147
|
+
$ sentio dashboard add-panel abc123 --project owner/slug \\
|
|
148
|
+
--panel-name "Top Holders" --type TABLE \\
|
|
149
|
+
--sql "SELECT * FROM CoinBalance ORDER BY balance DESC LIMIT 50"
|
|
150
|
+
$ sentio dashboard add-panel abc123 --project owner/slug \\
|
|
151
|
+
--panel-name "Daily Volume" --type LINE \\
|
|
152
|
+
--sql "SELECT toStartOfDay(timestamp) as date, sum(amount) as volume FROM Transfer GROUP BY date ORDER BY date"
|
|
153
|
+
|
|
154
|
+
Event insights panel examples:
|
|
155
|
+
$ sentio dashboard add-panel abc123 --project owner/slug \\
|
|
156
|
+
--panel-name "Transfer Count" --type LINE \\
|
|
157
|
+
--event Transfer --aggr total
|
|
158
|
+
$ sentio dashboard add-panel abc123 --project owner/slug \\
|
|
159
|
+
--panel-name "Large Transfers" --type TABLE \\
|
|
160
|
+
--event Transfer --filter amount>1000 --aggr total --group-by meta.address
|
|
161
|
+
$ sentio dashboard add-panel abc123 --project owner/slug \\
|
|
162
|
+
--panel-name "Top 5 Senders" --type BAR \\
|
|
163
|
+
--event Transfer --aggr unique --group-by from --func 'topk(5)'
|
|
164
|
+
$ sentio dashboard add-panel abc123 --project owner/slug \\
|
|
165
|
+
--panel-name "DAU" --type LINE \\
|
|
166
|
+
--event Transfer --aggr DAU
|
|
167
|
+
|
|
168
|
+
Metric insights panel examples:
|
|
169
|
+
$ sentio dashboard add-panel abc123 --project owner/slug \\
|
|
170
|
+
--panel-name "ETH Price" --type LINE \\
|
|
171
|
+
--metric cbETH_price
|
|
172
|
+
$ sentio dashboard add-panel abc123 --project owner/slug \\
|
|
173
|
+
--panel-name "Avg Burn by Chain" --type BAR \\
|
|
174
|
+
--metric burn --filter meta.chain=1 --aggr avg --group-by meta.address
|
|
175
|
+
$ sentio dashboard add-panel abc123 --project owner/slug \\
|
|
176
|
+
--panel-name "Burn Rate Delta" --type LINE \\
|
|
177
|
+
--metric burn --aggr sum
|
|
178
|
+
`
|
|
179
|
+
)
|
|
180
|
+
.action(async (dashboardId, options, command) => {
|
|
181
|
+
try {
|
|
182
|
+
await runDashboardAddPanel(dashboardId, options)
|
|
183
|
+
} catch (error) {
|
|
184
|
+
handleDashboardCommandError(error, command)
|
|
185
|
+
}
|
|
186
|
+
})
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
async function runDashboardList(options: DashboardOptions) {
|
|
190
|
+
const context = createApiContext(options)
|
|
191
|
+
const project = await resolveProjectRef(options, context, { ownerSlug: true })
|
|
192
|
+
const response = await WebService.listDashboards2({
|
|
193
|
+
path: { owner: project.owner, slug: project.slug },
|
|
194
|
+
headers: context.headers
|
|
195
|
+
})
|
|
196
|
+
const data = unwrapApiResult(response)
|
|
197
|
+
printOutput(options, data)
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
async function runDashboardExport(dashboardId: string, options: DashboardOptions) {
|
|
201
|
+
const context = createApiContext(options)
|
|
202
|
+
const response = await WebService.exportDashboard({
|
|
203
|
+
path: { dashboardId },
|
|
204
|
+
headers: context.headers
|
|
205
|
+
})
|
|
206
|
+
const data = unwrapApiResult(response)
|
|
207
|
+
// Export always outputs JSON regardless of --yaml flag, since the exported data is meant to be re-imported
|
|
208
|
+
console.log(JSON.stringify(data.dashboardJson ?? data, null, 2))
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
async function runDashboardImport(dashboardId: string, options: DashboardImportOptions) {
|
|
212
|
+
const context = createApiContext(options)
|
|
213
|
+
const input = loadJsonInput(options)
|
|
214
|
+
if (!input) {
|
|
215
|
+
throw new CliError('Provide --file or --stdin with the dashboard JSON to import.')
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
const dashboardJson = typeof input === 'object' ? (input as Record<string, unknown>) : {}
|
|
219
|
+
|
|
220
|
+
const response = await WebService.importDashboard({
|
|
221
|
+
body: {
|
|
222
|
+
dashboardId,
|
|
223
|
+
dashboardJson,
|
|
224
|
+
overrideLayouts: options.overrideLayouts
|
|
225
|
+
},
|
|
226
|
+
headers: context.headers
|
|
227
|
+
})
|
|
228
|
+
const data = unwrapApiResult(response)
|
|
229
|
+
printOutput(options, { message: `Dashboard imported into ${dashboardId}`, dashboard: data.dashboard })
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
async function runDashboardAddPanel(dashboardId: string, options: AddPanelOptions) {
|
|
233
|
+
const context = createApiContext(options)
|
|
234
|
+
|
|
235
|
+
// Validate: exactly one data source
|
|
236
|
+
const selectedSources = [Boolean(options.sql), Boolean(options.event), Boolean(options.metric)].filter(Boolean).length
|
|
237
|
+
if (selectedSources === 0) {
|
|
238
|
+
throw new CliError('Provide exactly one data source: --sql, --event, or --metric.')
|
|
239
|
+
}
|
|
240
|
+
if (selectedSources > 1) {
|
|
241
|
+
throw new CliError('Use exactly one of --sql, --event, or --metric.')
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// 1. Fetch current dashboard to determine layout positions
|
|
245
|
+
const getResponse = await WebService.getDashboard({
|
|
246
|
+
path: { dashboardId },
|
|
247
|
+
headers: context.headers
|
|
248
|
+
})
|
|
249
|
+
const dashboardData = unwrapApiResult(getResponse)
|
|
250
|
+
const dashboard = dashboardData.dashboards?.[0]
|
|
251
|
+
if (!dashboard) {
|
|
252
|
+
throw new CliError(`Dashboard ${dashboardId} not found.`)
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// 2. Build the new panel chart
|
|
256
|
+
const chartType = normalizeChartType(options.type!)
|
|
257
|
+
const panelId = generatePanelId()
|
|
258
|
+
const chart = buildPanelChart(chartType, options)
|
|
259
|
+
|
|
260
|
+
const newPanel = {
|
|
261
|
+
id: panelId,
|
|
262
|
+
name: options.panelName,
|
|
263
|
+
dashboardId,
|
|
264
|
+
chart
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// 3. Compute layout position: place below all existing panels
|
|
268
|
+
const existingLayouts = dashboard.layouts?.responsiveLayouts?.lg?.layouts ?? []
|
|
269
|
+
let maxBottom = 0
|
|
270
|
+
for (const layout of existingLayouts) {
|
|
271
|
+
const bottom = (layout.y ?? 0) + (layout.h ?? 0)
|
|
272
|
+
if (bottom > maxBottom) {
|
|
273
|
+
maxBottom = bottom
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
const newLayout = {
|
|
278
|
+
i: panelId,
|
|
279
|
+
x: 0,
|
|
280
|
+
y: maxBottom,
|
|
281
|
+
w: 12,
|
|
282
|
+
h: 6
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// 4. Build updated dashboard JSON and import it
|
|
286
|
+
const panels = { ...(dashboard.panels ?? {}) }
|
|
287
|
+
panels[panelId] = newPanel as never
|
|
288
|
+
|
|
289
|
+
const updatedLayouts = [...existingLayouts, newLayout]
|
|
290
|
+
|
|
291
|
+
const dashboardJson: Record<string, unknown> = {
|
|
292
|
+
...dashboard,
|
|
293
|
+
panels,
|
|
294
|
+
layouts: {
|
|
295
|
+
responsiveLayouts: {
|
|
296
|
+
...(dashboard.layouts?.responsiveLayouts ?? {}),
|
|
297
|
+
lg: { layouts: updatedLayouts }
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
const importResponse = await WebService.importDashboard({
|
|
303
|
+
body: {
|
|
304
|
+
dashboardId,
|
|
305
|
+
dashboardJson,
|
|
306
|
+
overrideLayouts: true
|
|
307
|
+
},
|
|
308
|
+
headers: context.headers
|
|
309
|
+
})
|
|
310
|
+
const importData = unwrapApiResult(importResponse)
|
|
311
|
+
printOutput(options, {
|
|
312
|
+
message: `Panel "${options.panelName}" added to dashboard ${dashboardId}`,
|
|
313
|
+
panelId,
|
|
314
|
+
dashboard: importData.dashboard
|
|
315
|
+
})
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
function buildPanelChart(chartType: string, options: AddPanelOptions) {
|
|
319
|
+
if (options.sql) {
|
|
320
|
+
const sqlSize = Number.parseInt(String(options.size ?? '100'), 10) || 100
|
|
321
|
+
return {
|
|
322
|
+
type: chartType,
|
|
323
|
+
datasourceType: 'SQL' as const,
|
|
324
|
+
sqlQuery: JSON.stringify({ sql: options.sql, size: sqlSize })
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
if (options.event) {
|
|
329
|
+
const queryBody = buildEventsInsightQueryBody(options.event, {
|
|
330
|
+
alias: options.alias,
|
|
331
|
+
sourceName: options.sourceName,
|
|
332
|
+
filter: options.filter,
|
|
333
|
+
groupBy: options.groupBy,
|
|
334
|
+
aggr: options.aggr,
|
|
335
|
+
func: options.func
|
|
336
|
+
})
|
|
337
|
+
return {
|
|
338
|
+
type: chartType,
|
|
339
|
+
datasourceType: 'INSIGHTS' as const,
|
|
340
|
+
insightsQueries: queryBody.queries
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
if (options.metric) {
|
|
345
|
+
const queryBody = buildMetricsInsightQueryBody(options.metric, {
|
|
346
|
+
alias: options.alias,
|
|
347
|
+
sourceName: options.sourceName,
|
|
348
|
+
filter: options.filter,
|
|
349
|
+
groupBy: options.groupBy,
|
|
350
|
+
aggr: options.aggr,
|
|
351
|
+
func: options.func
|
|
352
|
+
})
|
|
353
|
+
return {
|
|
354
|
+
type: chartType,
|
|
355
|
+
datasourceType: 'INSIGHTS' as const,
|
|
356
|
+
insightsQueries: queryBody.queries
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
throw new CliError('Provide exactly one data source: --sql, --event, or --metric.')
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
function normalizeChartType(value: string) {
|
|
364
|
+
const normalized = value.toUpperCase()
|
|
365
|
+
const valid = ['LINE', 'AREA', 'BAR', 'BAR_GAUGE', 'TABLE', 'QUERY_VALUE', 'PIE', 'NOTE', 'SCATTER']
|
|
366
|
+
if (valid.includes(normalized)) {
|
|
367
|
+
return normalized
|
|
368
|
+
}
|
|
369
|
+
throw new CliError(`Invalid chart type "${value}". Use one of: ${valid.join(', ')}`)
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
function generatePanelId() {
|
|
373
|
+
return `panel_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
function collectOption(value: string, previous: string[] = []) {
|
|
377
|
+
previous.push(value)
|
|
378
|
+
return previous
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
function withAuthOptions<T extends Command<any, any, any>>(command: T) {
|
|
382
|
+
return command
|
|
383
|
+
.option('--host <host>', 'Override Sentio host')
|
|
384
|
+
.option('--api-key <key>', 'Use an explicit API key instead of saved credentials')
|
|
385
|
+
.option('--token <token>', 'Use an explicit bearer token instead of saved credentials')
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
function withSharedProjectOptions<T extends Command<any, any, any>>(command: T) {
|
|
389
|
+
return command
|
|
390
|
+
.option('--project <project>', 'Sentio project as <owner>/<slug> or <slug>')
|
|
391
|
+
.option('--owner <owner>', 'Sentio project owner')
|
|
392
|
+
.option('--name <name>', 'Sentio project name')
|
|
393
|
+
.option('--project-id <id>', 'Sentio project id')
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
function withOutputOptions<T extends Command<any, any, any>>(command: T) {
|
|
397
|
+
return command.option('--json', 'Print raw JSON response').option('--yaml', 'Print raw YAML response')
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
function handleDashboardCommandError(error: unknown, command?: Command) {
|
|
401
|
+
if (
|
|
402
|
+
error instanceof CliError &&
|
|
403
|
+
(error.message.startsWith('Project is required.') ||
|
|
404
|
+
error.message.startsWith('Invalid project ') ||
|
|
405
|
+
error.message.startsWith('Dashboard ') ||
|
|
406
|
+
error.message.startsWith('Provide --file or --stdin') ||
|
|
407
|
+
error.message.startsWith('Provide exactly one data source') ||
|
|
408
|
+
error.message.startsWith('Use exactly one of --sql') ||
|
|
409
|
+
error.message.startsWith('Invalid chart type') ||
|
|
410
|
+
error.message.startsWith('Invalid aggregation') ||
|
|
411
|
+
error.message.startsWith('Invalid metric aggregation') ||
|
|
412
|
+
error.message.startsWith('Invalid filter') ||
|
|
413
|
+
error.message.startsWith('Invalid metric selector'))
|
|
414
|
+
) {
|
|
415
|
+
console.error(error.message)
|
|
416
|
+
if (command) {
|
|
417
|
+
console.error()
|
|
418
|
+
command.outputHelp()
|
|
419
|
+
}
|
|
420
|
+
process.exit(1)
|
|
421
|
+
}
|
|
422
|
+
handleCommandError(error)
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
function printOutput(options: DashboardOptions, data: unknown) {
|
|
426
|
+
if (options.json && options.yaml) {
|
|
427
|
+
throw new CliError('Choose only one structured output format: --json or --yaml.')
|
|
428
|
+
}
|
|
429
|
+
if (options.json) {
|
|
430
|
+
console.log(JSON.stringify(data, null, 2))
|
|
431
|
+
return
|
|
432
|
+
}
|
|
433
|
+
if (options.yaml) {
|
|
434
|
+
console.log(yaml.stringify(data).trimEnd())
|
|
435
|
+
return
|
|
436
|
+
}
|
|
437
|
+
console.log(formatOutput(data))
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
function formatOutput(data: unknown) {
|
|
441
|
+
if (data && typeof data === 'object' && 'message' in (data as Record<string, unknown>)) {
|
|
442
|
+
return String((data as { message?: string }).message ?? '')
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
if (data && typeof data === 'object' && 'dashboards' in (data as Record<string, unknown>)) {
|
|
446
|
+
const dashboards = ((data as { dashboards?: unknown[] }).dashboards ?? []) as Array<Record<string, unknown>>
|
|
447
|
+
const lines = [`Dashboards (${dashboards.length})`]
|
|
448
|
+
for (const db of dashboards) {
|
|
449
|
+
const id = db.id ?? '<id>'
|
|
450
|
+
const name = db.name ?? '<unnamed>'
|
|
451
|
+
const panelCount = db.panels ? Object.keys(db.panels as Record<string, unknown>).length : 0
|
|
452
|
+
const visibility = db.visibility ?? ''
|
|
453
|
+
const updated = formatTimestamp(db.updatedAt as string | undefined)
|
|
454
|
+
lines.push(
|
|
455
|
+
`- ${id} "${name}" [${panelCount} panels]${visibility ? ` ${visibility}` : ''}${updated ? ` updated ${updated}` : ''}`
|
|
456
|
+
)
|
|
457
|
+
}
|
|
458
|
+
return lines.join('\n')
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
return JSON.stringify(data, null, 2)
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
function formatTimestamp(value?: string) {
|
|
465
|
+
if (!value) {
|
|
466
|
+
return ''
|
|
467
|
+
}
|
|
468
|
+
if (/^\d{13}$/.test(value)) {
|
|
469
|
+
return new Date(Number.parseInt(value, 10)).toISOString()
|
|
470
|
+
}
|
|
471
|
+
return value
|
|
472
|
+
}
|