@sentio/cli 3.6.0-rc.2 → 3.6.0-rc.3
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 +82 -22
- package/package.json +1 -1
- package/src/commands/alert.ts +9 -3
- package/src/commands/dashboard.ts +84 -21
package/lib/index.js
CHANGED
|
@@ -143006,7 +143006,10 @@ function createAlertGetCommand() {
|
|
|
143006
143006
|
function createAlertCreateCommand() {
|
|
143007
143007
|
return withOutputOptions4(
|
|
143008
143008
|
withSharedProjectOptions4(withAuthOptions4(new Command("create").description("Create an alert rule")))
|
|
143009
|
-
).showHelpAfterError().option("--file <path>", "Read request JSON or YAML from file. Use --doc to show the full alert request format").option("--stdin", "Read request JSON or YAML from stdin. Use --doc to show the full alert request format").option("--doc", "Print the full alert request file format and exit").option("--type <type>", "Alert type: METRIC, LOG, or SQL").option("--subject <text>", "Alert subject/title").option("--message <text>", "Optional alert message template").option(
|
|
143009
|
+
).showHelpAfterError().option("--file <path>", "Read request JSON or YAML from file. Use --doc to show the full alert request format").option("--stdin", "Read request JSON or YAML from stdin. Use --doc to show the full alert request format").option("--doc", "Print the full alert request file format and exit").option("--type <type>", "Alert type: METRIC, LOG, or SQL").option("--subject <text>", "Alert subject/title").option("--message <text>", "Optional alert message template").option(
|
|
143010
|
+
"--query <text>",
|
|
143011
|
+
"Inline query. LOG: Elasticsearch query-string syntax (e.g. amount:>1000, status:error). SQL: full SQL statement."
|
|
143012
|
+
).option("--event <name>", "Inline event query for METRIC alerts").option("--metric <name>", "Inline metric query for METRIC alerts").option("--alias <alias>", "Alias for the inline METRIC query").option("--source-name <name>", "Optional source name for the inline METRIC query").option("--filter <selector>", "Inline METRIC query filter like amount>0 or meta.chain=1", collectOption2, []).option("--group-by <field>", "Inline METRIC query group-by field", collectOption2, []).option(
|
|
143010
143013
|
"--aggr <aggregation>",
|
|
143011
143014
|
"Inline aggregation. METRIC: avg|sum|min|max|count. EVENTS: total|unique|AAU|DAU|WAU|MAU"
|
|
143012
143015
|
).option("--func <function>", "Inline function like topk(1), bottomk(1), delta(1m)", collectOption2, []).option("--op <operator>", "Condition operator like >, >=, ==, !=, <, <=, between").option("--threshold <value>", "Condition threshold", parseNumber).option("--threshold2 <value>", "Second threshold for between", parseNumber).option("--for <duration>", "Evaluate over the last duration, for example 5m or 1h").option("--interval <duration>", "Alert evaluation interval, for example 1m or 5m").option("--time-column <column>", "SQL alert time column for column-based conditions").option("--value-column <column>", "SQL alert value column for column-based conditions").option("--sql-aggr <aggregation>", "SQL aggregation: COUNT, SUM, AVG, MAX, MIN, LAST").addHelpText(
|
|
@@ -143014,7 +143017,7 @@ function createAlertCreateCommand() {
|
|
|
143014
143017
|
`
|
|
143015
143018
|
|
|
143016
143019
|
Examples:
|
|
143017
|
-
$ sentio alert create --project sentio/coinbase --type LOG --subject "Large transfer logs" --query 'amount
|
|
143020
|
+
$ sentio alert create --project sentio/coinbase --type LOG --subject "Large transfer logs" --query 'amount:>1000' --op '>' --threshold 0
|
|
143018
143021
|
$ sentio alert create --project sentio/coinbase --type SQL --subject "Large transfer(SQL demo)" --query 'select timestamp, amount from transfer where amount > 1000' --time-column timestamp --value-column amount --sql-aggr MAX --op '>' --threshold 1000
|
|
143019
143022
|
$ sentio alert create --project sentio/coinbase --type METRIC --subject "Burn spike" --metric burn --filter meta.chain=1 --aggr avg --group-by meta.address --op '>' --threshold 100
|
|
143020
143023
|
$ sentio alert create --project sentio/coinbase --type METRIC --subject "Transfer anomaly" --event Transfer --filter amount>0 --aggr total --func 'delta(1m)' --op '>' --threshold 100
|
|
@@ -143637,11 +143640,14 @@ Metric alert example:
|
|
|
143637
143640
|
disabled: false
|
|
143638
143641
|
|
|
143639
143642
|
Log alert example:
|
|
143643
|
+
NOTE: logCondition.query uses Elasticsearch query-string syntax.
|
|
143644
|
+
Ranges MUST use field:>value form \u2014 C-style "field > value" is rejected at evaluation time.
|
|
143645
|
+
Examples: amount:>1000000 timestamp:>=2024-01-01 amount:[1000 TO 9999] status:error
|
|
143640
143646
|
rule:
|
|
143641
143647
|
alertType: LOG
|
|
143642
143648
|
subject: large transfer logs
|
|
143643
143649
|
logCondition:
|
|
143644
|
-
query: amount
|
|
143650
|
+
query: amount:>1000
|
|
143645
143651
|
comparisonOp: ">"
|
|
143646
143652
|
threshold: 0
|
|
143647
143653
|
|
|
@@ -144874,7 +144880,13 @@ function createDashboardAddPanelCommand() {
|
|
|
144874
144880
|
"Event filter or metric label selector like field:value or amount>0",
|
|
144875
144881
|
collectOption3,
|
|
144876
144882
|
[]
|
|
144877
|
-
).option("--group-by <field>", "Group by event property or metric label", collectOption3, []).option("--aggr <aggregation>", "Event: total|unique|AAU|DAU|WAU|MAU. Metric: avg|sum|min|max|count").option("--func <function>", "Function like topk(1), bottomk(1)", collectOption3, []).
|
|
144883
|
+
).option("--group-by <field>", "Group by event property or metric label", collectOption3, []).option("--aggr <aggregation>", "Event: total|unique|AAU|DAU|WAU|MAU. Metric: avg|sum|min|max|count").option("--func <function>", "Function like topk(1), bottomk(1)", collectOption3, []).option(
|
|
144884
|
+
"--time-range-start <value>",
|
|
144885
|
+
"Panel time range start: relative (e.g. -24h, -7d, -30m) or ISO date (e.g. 2024-01-01T00:00:00Z)"
|
|
144886
|
+
).option(
|
|
144887
|
+
"--time-range-end <value>",
|
|
144888
|
+
"Panel time range end: relative (e.g. now, -1h) or ISO date. Defaults to now when --time-range-start is set."
|
|
144889
|
+
).option("--time-range-step <seconds>", "Panel time range step in seconds (e.g. 3600)").addHelpText(
|
|
144878
144890
|
"after",
|
|
144879
144891
|
`
|
|
144880
144892
|
|
|
@@ -144911,7 +144923,17 @@ Metric insights panel examples:
|
|
|
144911
144923
|
--metric burn --filter meta.chain=1 --aggr avg --group-by meta.address
|
|
144912
144924
|
$ sentio dashboard add-panel abc123 --project owner/slug \\
|
|
144913
144925
|
--panel-name "Burn Rate Delta" --type LINE \\
|
|
144914
|
-
--metric burn --aggr sum
|
|
144926
|
+
--metric burn --aggr sum
|
|
144927
|
+
|
|
144928
|
+
Panel time range override examples:
|
|
144929
|
+
$ sentio dashboard add-panel abc123 --project owner/slug \\
|
|
144930
|
+
--panel-name "Last 24h Transfers" --type LINE \\
|
|
144931
|
+
--event Transfer --aggr total \\
|
|
144932
|
+
--time-range-start -24h --time-range-end now --time-range-step 3600
|
|
144933
|
+
$ sentio dashboard add-panel abc123 --project owner/slug \\
|
|
144934
|
+
--panel-name "Jan 2024 Volume" --type BAR \\
|
|
144935
|
+
--sql "SELECT date, sum(amount) FROM Transfer GROUP BY date" \\
|
|
144936
|
+
--time-range-start 2024-01-01T00:00:00Z --time-range-end 2024-02-01T00:00:00Z
|
|
144915
144937
|
`
|
|
144916
144938
|
).action(async (dashboardId, options, command) => {
|
|
144917
144939
|
try {
|
|
@@ -144979,14 +145001,18 @@ function buildDashboardCreateBody(options, project) {
|
|
|
144979
145001
|
};
|
|
144980
145002
|
}
|
|
144981
145003
|
function normalizeDashboardInit(input) {
|
|
145004
|
+
const emptyLayouts = {
|
|
145005
|
+
responsiveLayouts: {
|
|
145006
|
+
lg: { layouts: [] },
|
|
145007
|
+
md: { layouts: [] },
|
|
145008
|
+
sm: { layouts: [] },
|
|
145009
|
+
xs: { layouts: [] }
|
|
145010
|
+
}
|
|
145011
|
+
};
|
|
144982
145012
|
if (input === void 0) {
|
|
144983
145013
|
return {
|
|
144984
145014
|
panels: {},
|
|
144985
|
-
layouts:
|
|
144986
|
-
responsiveLayouts: {
|
|
144987
|
-
lg: { layouts: [] }
|
|
144988
|
-
}
|
|
144989
|
-
}
|
|
145015
|
+
layouts: emptyLayouts
|
|
144990
145016
|
};
|
|
144991
145017
|
}
|
|
144992
145018
|
if (!input || typeof input !== "object" || Array.isArray(input)) {
|
|
@@ -144995,11 +145021,7 @@ function normalizeDashboardInit(input) {
|
|
|
144995
145021
|
const dashboard = input;
|
|
144996
145022
|
return {
|
|
144997
145023
|
panels: isRecord(dashboard.panels) ? dashboard.panels : {},
|
|
144998
|
-
layouts: isRecord(dashboard.layouts) ? dashboard.layouts :
|
|
144999
|
-
responsiveLayouts: {
|
|
145000
|
-
lg: { layouts: [] }
|
|
145001
|
-
}
|
|
145002
|
-
}
|
|
145024
|
+
layouts: isRecord(dashboard.layouts) ? dashboard.layouts : emptyLayouts
|
|
145003
145025
|
};
|
|
145004
145026
|
}
|
|
145005
145027
|
async function runDashboardAddPanel(dashboardId, options) {
|
|
@@ -145023,11 +145045,13 @@ async function runDashboardAddPanel(dashboardId, options) {
|
|
|
145023
145045
|
const chartType = normalizeChartType(options.type);
|
|
145024
145046
|
const panelId = generatePanelId();
|
|
145025
145047
|
const chart = buildPanelChart(chartType, options);
|
|
145048
|
+
const timeRangeOverride = buildTimeRangeOverride(options);
|
|
145026
145049
|
const newPanel = {
|
|
145027
145050
|
id: panelId,
|
|
145028
145051
|
name: options.panelName,
|
|
145029
145052
|
dashboardId,
|
|
145030
|
-
chart
|
|
145053
|
+
chart,
|
|
145054
|
+
...timeRangeOverride ? { timeRangeOverride } : {}
|
|
145031
145055
|
};
|
|
145032
145056
|
const existingLayouts = dashboard.layouts?.responsiveLayouts?.lg?.layouts ?? [];
|
|
145033
145057
|
let maxBottom = 0;
|
|
@@ -145046,15 +145070,17 @@ async function runDashboardAddPanel(dashboardId, options) {
|
|
|
145046
145070
|
};
|
|
145047
145071
|
const panels = { ...dashboard.panels ?? {} };
|
|
145048
145072
|
panels[panelId] = newPanel;
|
|
145049
|
-
const
|
|
145073
|
+
const existingResponsive = dashboard.layouts?.responsiveLayouts ?? {};
|
|
145074
|
+
const updatedResponsive = { ...existingResponsive };
|
|
145075
|
+
for (const bp2 of ["lg", "md", "sm", "xs"]) {
|
|
145076
|
+
const existing = existingResponsive[bp2]?.layouts ?? [];
|
|
145077
|
+
updatedResponsive[bp2] = { layouts: [...existing, newLayout] };
|
|
145078
|
+
}
|
|
145050
145079
|
const dashboardJson = {
|
|
145051
145080
|
...dashboard,
|
|
145052
145081
|
panels,
|
|
145053
145082
|
layouts: {
|
|
145054
|
-
responsiveLayouts:
|
|
145055
|
-
...dashboard.layouts?.responsiveLayouts ?? {},
|
|
145056
|
-
lg: { layouts: updatedLayouts }
|
|
145057
|
-
}
|
|
145083
|
+
responsiveLayouts: updatedResponsive
|
|
145058
145084
|
}
|
|
145059
145085
|
};
|
|
145060
145086
|
const importResponse = await import_api14.WebService.importDashboard({
|
|
@@ -145072,6 +145098,40 @@ async function runDashboardAddPanel(dashboardId, options) {
|
|
|
145072
145098
|
dashboard: importData.dashboard
|
|
145073
145099
|
});
|
|
145074
145100
|
}
|
|
145101
|
+
function buildTimeRangeLike(value) {
|
|
145102
|
+
const relMatch = value.match(/^(-?\d+)\s*([smhdwMy])$/);
|
|
145103
|
+
if (relMatch) {
|
|
145104
|
+
return { relativeTime: { unit: relMatch[2], value: Number(relMatch[1]) } };
|
|
145105
|
+
}
|
|
145106
|
+
if (value === "now" || value === "0") {
|
|
145107
|
+
return { relativeTime: { unit: "h", value: 0 } };
|
|
145108
|
+
}
|
|
145109
|
+
const ts2 = Date.parse(value);
|
|
145110
|
+
if (!Number.isNaN(ts2)) {
|
|
145111
|
+
return { absoluteTime: String(ts2) };
|
|
145112
|
+
}
|
|
145113
|
+
throw new CliError(
|
|
145114
|
+
`Invalid time range value "${value}". Use a relative offset (e.g. -24h, -7d, -30m, now) or an ISO date string.`
|
|
145115
|
+
);
|
|
145116
|
+
}
|
|
145117
|
+
function buildTimeRangeOverride(options) {
|
|
145118
|
+
if (!options.timeRangeStart && !options.timeRangeEnd) {
|
|
145119
|
+
return void 0;
|
|
145120
|
+
}
|
|
145121
|
+
const timeRange = {};
|
|
145122
|
+
if (options.timeRangeStart) {
|
|
145123
|
+
timeRange.start = buildTimeRangeLike(options.timeRangeStart);
|
|
145124
|
+
}
|
|
145125
|
+
if (options.timeRangeEnd) {
|
|
145126
|
+
timeRange.end = buildTimeRangeLike(options.timeRangeEnd);
|
|
145127
|
+
} else {
|
|
145128
|
+
timeRange.end = { relativeTime: { unit: "h", value: 0 } };
|
|
145129
|
+
}
|
|
145130
|
+
if (options.timeRangeStep) {
|
|
145131
|
+
timeRange.step = options.timeRangeStep;
|
|
145132
|
+
}
|
|
145133
|
+
return { enabled: true, timeRange };
|
|
145134
|
+
}
|
|
145075
145135
|
function buildPanelChart(chartType, options) {
|
|
145076
145136
|
if (options.sql) {
|
|
145077
145137
|
const sqlSize = Number.parseInt(String(options.size ?? "100"), 10) || 100;
|
|
@@ -145141,7 +145201,7 @@ function withOutputOptions8(command) {
|
|
|
145141
145201
|
return command.option("--json", "Print raw JSON response").option("--yaml", "Print raw YAML response");
|
|
145142
145202
|
}
|
|
145143
145203
|
function handleDashboardCommandError(error, command) {
|
|
145144
|
-
if (error instanceof CliError && (error.message.startsWith("Project is required.") || error.message.startsWith("Invalid project ") || error.message.startsWith("Dashboard ") || error.message.startsWith("Provide --file or --stdin") || error.message.startsWith("Use either --file or --stdin") || error.message.startsWith("Expected JSON or YAML") || error.message.startsWith("Invalid JSON or YAML") || error.message.startsWith("Dashboard initialization data") || error.message.startsWith("Provide exactly one data source") || error.message.startsWith("Use exactly one of --sql") || error.message.startsWith("Invalid chart type") || error.message.startsWith("Invalid aggregation") || error.message.startsWith("Invalid metric aggregation") || error.message.startsWith("Invalid filter") || error.message.startsWith("Invalid metric selector"))) {
|
|
145204
|
+
if (error instanceof CliError && (error.message.startsWith("Project is required.") || error.message.startsWith("Invalid project ") || error.message.startsWith("Dashboard ") || error.message.startsWith("Provide --file or --stdin") || error.message.startsWith("Use either --file or --stdin") || error.message.startsWith("Expected JSON or YAML") || error.message.startsWith("Invalid JSON or YAML") || error.message.startsWith("Dashboard initialization data") || error.message.startsWith("Provide exactly one data source") || error.message.startsWith("Use exactly one of --sql") || error.message.startsWith("Invalid chart type") || error.message.startsWith("Invalid aggregation") || error.message.startsWith("Invalid metric aggregation") || error.message.startsWith("Invalid filter") || error.message.startsWith("Invalid metric selector") || error.message.startsWith("Invalid time range value"))) {
|
|
145145
145205
|
console.error(error.message);
|
|
145146
145206
|
if (command) {
|
|
145147
145207
|
console.error();
|
package/package.json
CHANGED
package/src/commands/alert.ts
CHANGED
|
@@ -136,7 +136,10 @@ function createAlertCreateCommand() {
|
|
|
136
136
|
.option('--type <type>', 'Alert type: METRIC, LOG, or SQL')
|
|
137
137
|
.option('--subject <text>', 'Alert subject/title')
|
|
138
138
|
.option('--message <text>', 'Optional alert message template')
|
|
139
|
-
.option(
|
|
139
|
+
.option(
|
|
140
|
+
'--query <text>',
|
|
141
|
+
'Inline query. LOG: Elasticsearch query-string syntax (e.g. amount:>1000, status:error). SQL: full SQL statement.'
|
|
142
|
+
)
|
|
140
143
|
.option('--event <name>', 'Inline event query for METRIC alerts')
|
|
141
144
|
.option('--metric <name>', 'Inline metric query for METRIC alerts')
|
|
142
145
|
.option('--alias <alias>', 'Alias for the inline METRIC query')
|
|
@@ -161,7 +164,7 @@ function createAlertCreateCommand() {
|
|
|
161
164
|
`
|
|
162
165
|
|
|
163
166
|
Examples:
|
|
164
|
-
$ sentio alert create --project sentio/coinbase --type LOG --subject "Large transfer logs" --query 'amount
|
|
167
|
+
$ sentio alert create --project sentio/coinbase --type LOG --subject "Large transfer logs" --query 'amount:>1000' --op '>' --threshold 0
|
|
165
168
|
$ sentio alert create --project sentio/coinbase --type SQL --subject "Large transfer(SQL demo)" --query 'select timestamp, amount from transfer where amount > 1000' --time-column timestamp --value-column amount --sql-aggr MAX --op '>' --threshold 1000
|
|
166
169
|
$ sentio alert create --project sentio/coinbase --type METRIC --subject "Burn spike" --metric burn --filter meta.chain=1 --aggr avg --group-by meta.address --op '>' --threshold 100
|
|
167
170
|
$ sentio alert create --project sentio/coinbase --type METRIC --subject "Transfer anomaly" --event Transfer --filter amount>0 --aggr total --func 'delta(1m)' --op '>' --threshold 100
|
|
@@ -916,11 +919,14 @@ Metric alert example:
|
|
|
916
919
|
disabled: false
|
|
917
920
|
|
|
918
921
|
Log alert example:
|
|
922
|
+
NOTE: logCondition.query uses Elasticsearch query-string syntax.
|
|
923
|
+
Ranges MUST use field:>value form — C-style "field > value" is rejected at evaluation time.
|
|
924
|
+
Examples: amount:>1000000 timestamp:>=2024-01-01 amount:[1000 TO 9999] status:error
|
|
919
925
|
rule:
|
|
920
926
|
alertType: LOG
|
|
921
927
|
subject: large transfer logs
|
|
922
928
|
logCondition:
|
|
923
|
-
query: amount
|
|
929
|
+
query: amount:>1000
|
|
924
930
|
comparisonOp: ">"
|
|
925
931
|
threshold: 0
|
|
926
932
|
|
|
@@ -50,6 +50,9 @@ interface AddPanelOptions extends DashboardOptions {
|
|
|
50
50
|
groupBy?: string[]
|
|
51
51
|
aggr?: string
|
|
52
52
|
func?: string[]
|
|
53
|
+
timeRangeStart?: string
|
|
54
|
+
timeRangeEnd?: string
|
|
55
|
+
timeRangeStep?: string
|
|
53
56
|
}
|
|
54
57
|
|
|
55
58
|
export function createDashboardCommand() {
|
|
@@ -162,6 +165,15 @@ function createDashboardAddPanelCommand() {
|
|
|
162
165
|
.option('--group-by <field>', 'Group by event property or metric label', collectOption, [])
|
|
163
166
|
.option('--aggr <aggregation>', 'Event: total|unique|AAU|DAU|WAU|MAU. Metric: avg|sum|min|max|count')
|
|
164
167
|
.option('--func <function>', 'Function like topk(1), bottomk(1)', collectOption, [])
|
|
168
|
+
.option(
|
|
169
|
+
'--time-range-start <value>',
|
|
170
|
+
'Panel time range start: relative (e.g. -24h, -7d, -30m) or ISO date (e.g. 2024-01-01T00:00:00Z)'
|
|
171
|
+
)
|
|
172
|
+
.option(
|
|
173
|
+
'--time-range-end <value>',
|
|
174
|
+
'Panel time range end: relative (e.g. now, -1h) or ISO date. Defaults to now when --time-range-start is set.'
|
|
175
|
+
)
|
|
176
|
+
.option('--time-range-step <seconds>', 'Panel time range step in seconds (e.g. 3600)')
|
|
165
177
|
.addHelpText(
|
|
166
178
|
'after',
|
|
167
179
|
`
|
|
@@ -199,7 +211,17 @@ Metric insights panel examples:
|
|
|
199
211
|
--metric burn --filter meta.chain=1 --aggr avg --group-by meta.address
|
|
200
212
|
$ sentio dashboard add-panel abc123 --project owner/slug \\
|
|
201
213
|
--panel-name "Burn Rate Delta" --type LINE \\
|
|
202
|
-
--metric burn --aggr sum
|
|
214
|
+
--metric burn --aggr sum
|
|
215
|
+
|
|
216
|
+
Panel time range override examples:
|
|
217
|
+
$ sentio dashboard add-panel abc123 --project owner/slug \\
|
|
218
|
+
--panel-name "Last 24h Transfers" --type LINE \\
|
|
219
|
+
--event Transfer --aggr total \\
|
|
220
|
+
--time-range-start -24h --time-range-end now --time-range-step 3600
|
|
221
|
+
$ sentio dashboard add-panel abc123 --project owner/slug \\
|
|
222
|
+
--panel-name "Jan 2024 Volume" --type BAR \\
|
|
223
|
+
--sql "SELECT date, sum(amount) FROM Transfer GROUP BY date" \\
|
|
224
|
+
--time-range-start 2024-01-01T00:00:00Z --time-range-end 2024-02-01T00:00:00Z
|
|
203
225
|
`
|
|
204
226
|
)
|
|
205
227
|
.action(async (dashboardId, options, command) => {
|
|
@@ -279,14 +301,19 @@ function buildDashboardCreateBody(options: DashboardCreateOptions, project: { ow
|
|
|
279
301
|
}
|
|
280
302
|
|
|
281
303
|
function normalizeDashboardInit(input: unknown) {
|
|
304
|
+
const emptyLayouts = {
|
|
305
|
+
responsiveLayouts: {
|
|
306
|
+
lg: { layouts: [] },
|
|
307
|
+
md: { layouts: [] },
|
|
308
|
+
sm: { layouts: [] },
|
|
309
|
+
xs: { layouts: [] }
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
282
313
|
if (input === undefined) {
|
|
283
314
|
return {
|
|
284
315
|
panels: {},
|
|
285
|
-
layouts:
|
|
286
|
-
responsiveLayouts: {
|
|
287
|
-
lg: { layouts: [] }
|
|
288
|
-
}
|
|
289
|
-
}
|
|
316
|
+
layouts: emptyLayouts
|
|
290
317
|
}
|
|
291
318
|
}
|
|
292
319
|
|
|
@@ -297,13 +324,7 @@ function normalizeDashboardInit(input: unknown) {
|
|
|
297
324
|
const dashboard = input as Record<string, unknown>
|
|
298
325
|
return {
|
|
299
326
|
panels: isRecord(dashboard.panels) ? dashboard.panels : {},
|
|
300
|
-
layouts: isRecord(dashboard.layouts)
|
|
301
|
-
? dashboard.layouts
|
|
302
|
-
: {
|
|
303
|
-
responsiveLayouts: {
|
|
304
|
-
lg: { layouts: [] }
|
|
305
|
-
}
|
|
306
|
-
}
|
|
327
|
+
layouts: isRecord(dashboard.layouts) ? dashboard.layouts : emptyLayouts
|
|
307
328
|
}
|
|
308
329
|
}
|
|
309
330
|
|
|
@@ -335,11 +356,13 @@ async function runDashboardAddPanel(dashboardId: string, options: AddPanelOption
|
|
|
335
356
|
const panelId = generatePanelId()
|
|
336
357
|
const chart = buildPanelChart(chartType, options)
|
|
337
358
|
|
|
338
|
-
const
|
|
359
|
+
const timeRangeOverride = buildTimeRangeOverride(options)
|
|
360
|
+
const newPanel: Record<string, unknown> = {
|
|
339
361
|
id: panelId,
|
|
340
362
|
name: options.panelName,
|
|
341
363
|
dashboardId,
|
|
342
|
-
chart
|
|
364
|
+
chart,
|
|
365
|
+
...(timeRangeOverride ? { timeRangeOverride } : {})
|
|
343
366
|
}
|
|
344
367
|
|
|
345
368
|
// 3. Compute layout position: place below all existing panels
|
|
@@ -364,16 +387,18 @@ async function runDashboardAddPanel(dashboardId: string, options: AddPanelOption
|
|
|
364
387
|
const panels = { ...(dashboard.panels ?? {}) }
|
|
365
388
|
panels[panelId] = newPanel as never
|
|
366
389
|
|
|
367
|
-
const
|
|
390
|
+
const existingResponsive = dashboard.layouts?.responsiveLayouts ?? {}
|
|
391
|
+
const updatedResponsive: Record<string, unknown> = { ...existingResponsive }
|
|
392
|
+
for (const bp of ['lg', 'md', 'sm', 'xs'] as const) {
|
|
393
|
+
const existing = (existingResponsive as Record<string, { layouts?: unknown[] } | undefined>)[bp]?.layouts ?? []
|
|
394
|
+
updatedResponsive[bp] = { layouts: [...existing, newLayout] }
|
|
395
|
+
}
|
|
368
396
|
|
|
369
397
|
const dashboardJson: Record<string, unknown> = {
|
|
370
398
|
...dashboard,
|
|
371
399
|
panels,
|
|
372
400
|
layouts: {
|
|
373
|
-
responsiveLayouts:
|
|
374
|
-
...(dashboard.layouts?.responsiveLayouts ?? {}),
|
|
375
|
-
lg: { layouts: updatedLayouts }
|
|
376
|
-
}
|
|
401
|
+
responsiveLayouts: updatedResponsive
|
|
377
402
|
}
|
|
378
403
|
}
|
|
379
404
|
|
|
@@ -393,6 +418,43 @@ async function runDashboardAddPanel(dashboardId: string, options: AddPanelOption
|
|
|
393
418
|
})
|
|
394
419
|
}
|
|
395
420
|
|
|
421
|
+
function buildTimeRangeLike(value: string): Record<string, unknown> {
|
|
422
|
+
const relMatch = value.match(/^(-?\d+)\s*([smhdwMy])$/)
|
|
423
|
+
if (relMatch) {
|
|
424
|
+
return { relativeTime: { unit: relMatch[2], value: Number(relMatch[1]) } }
|
|
425
|
+
}
|
|
426
|
+
if (value === 'now' || value === '0') {
|
|
427
|
+
return { relativeTime: { unit: 'h', value: 0 } }
|
|
428
|
+
}
|
|
429
|
+
const ts = Date.parse(value)
|
|
430
|
+
if (!Number.isNaN(ts)) {
|
|
431
|
+
return { absoluteTime: String(ts) }
|
|
432
|
+
}
|
|
433
|
+
throw new CliError(
|
|
434
|
+
`Invalid time range value "${value}". Use a relative offset (e.g. -24h, -7d, -30m, now) or an ISO date string.`
|
|
435
|
+
)
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
function buildTimeRangeOverride(options: AddPanelOptions): Record<string, unknown> | undefined {
|
|
439
|
+
if (!options.timeRangeStart && !options.timeRangeEnd) {
|
|
440
|
+
return undefined
|
|
441
|
+
}
|
|
442
|
+
const timeRange: Record<string, unknown> = {}
|
|
443
|
+
if (options.timeRangeStart) {
|
|
444
|
+
timeRange.start = buildTimeRangeLike(options.timeRangeStart)
|
|
445
|
+
}
|
|
446
|
+
if (options.timeRangeEnd) {
|
|
447
|
+
timeRange.end = buildTimeRangeLike(options.timeRangeEnd)
|
|
448
|
+
} else {
|
|
449
|
+
// default end to "now" when start is provided
|
|
450
|
+
timeRange.end = { relativeTime: { unit: 'h', value: 0 } }
|
|
451
|
+
}
|
|
452
|
+
if (options.timeRangeStep) {
|
|
453
|
+
timeRange.step = options.timeRangeStep
|
|
454
|
+
}
|
|
455
|
+
return { enabled: true, timeRange }
|
|
456
|
+
}
|
|
457
|
+
|
|
396
458
|
function buildPanelChart(chartType: string, options: AddPanelOptions) {
|
|
397
459
|
if (options.sql) {
|
|
398
460
|
const sqlSize = Number.parseInt(String(options.size ?? '100'), 10) || 100
|
|
@@ -496,7 +558,8 @@ function handleDashboardCommandError(error: unknown, command?: Command) {
|
|
|
496
558
|
error.message.startsWith('Invalid aggregation') ||
|
|
497
559
|
error.message.startsWith('Invalid metric aggregation') ||
|
|
498
560
|
error.message.startsWith('Invalid filter') ||
|
|
499
|
-
error.message.startsWith('Invalid metric selector')
|
|
561
|
+
error.message.startsWith('Invalid metric selector') ||
|
|
562
|
+
error.message.startsWith('Invalid time range value'))
|
|
500
563
|
) {
|
|
501
564
|
console.error(error.message)
|
|
502
565
|
if (command) {
|