@sentio/cli 3.6.0-rc.1 → 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 +131 -12
- package/package.json +1 -1
- package/src/commands/alert.ts +9 -3
- package/src/commands/dashboard.ts +158 -9
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
|
|
|
@@ -144804,6 +144810,7 @@ import process24 from "process";
|
|
|
144804
144810
|
function createDashboardCommand() {
|
|
144805
144811
|
const dashboardCommand = new Command("dashboard").description("Manage Sentio dashboards");
|
|
144806
144812
|
dashboardCommand.addCommand(createDashboardListCommand());
|
|
144813
|
+
dashboardCommand.addCommand(createDashboardCreateCommand());
|
|
144807
144814
|
dashboardCommand.addCommand(createDashboardExportCommand());
|
|
144808
144815
|
dashboardCommand.addCommand(createDashboardImportCommand());
|
|
144809
144816
|
dashboardCommand.addCommand(createDashboardAddPanelCommand());
|
|
@@ -144820,6 +144827,17 @@ function createDashboardListCommand() {
|
|
|
144820
144827
|
}
|
|
144821
144828
|
});
|
|
144822
144829
|
}
|
|
144830
|
+
function createDashboardCreateCommand() {
|
|
144831
|
+
return withOutputOptions8(
|
|
144832
|
+
withSharedProjectOptions7(withAuthOptions8(new Command("create").description("Create a dashboard for a project")))
|
|
144833
|
+
).showHelpAfterError().requiredOption("--title <name>", "Dashboard title").option("--file <path>", "Read initial dashboard JSON or YAML from file").option("--stdin", "Read initial dashboard JSON or YAML from stdin").action(async (options, command) => {
|
|
144834
|
+
try {
|
|
144835
|
+
await runDashboardCreate(options);
|
|
144836
|
+
} catch (error) {
|
|
144837
|
+
handleDashboardCommandError(error, command);
|
|
144838
|
+
}
|
|
144839
|
+
});
|
|
144840
|
+
}
|
|
144823
144841
|
function createDashboardExportCommand() {
|
|
144824
144842
|
return withOutputOptions8(
|
|
144825
144843
|
withSharedProjectOptions7(
|
|
@@ -144862,7 +144880,13 @@ function createDashboardAddPanelCommand() {
|
|
|
144862
144880
|
"Event filter or metric label selector like field:value or amount>0",
|
|
144863
144881
|
collectOption3,
|
|
144864
144882
|
[]
|
|
144865
|
-
).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(
|
|
144866
144890
|
"after",
|
|
144867
144891
|
`
|
|
144868
144892
|
|
|
@@ -144899,7 +144923,17 @@ Metric insights panel examples:
|
|
|
144899
144923
|
--metric burn --filter meta.chain=1 --aggr avg --group-by meta.address
|
|
144900
144924
|
$ sentio dashboard add-panel abc123 --project owner/slug \\
|
|
144901
144925
|
--panel-name "Burn Rate Delta" --type LINE \\
|
|
144902
|
-
--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
|
|
144903
144937
|
`
|
|
144904
144938
|
).action(async (dashboardId, options, command) => {
|
|
144905
144939
|
try {
|
|
@@ -144919,6 +144953,16 @@ async function runDashboardList(options) {
|
|
|
144919
144953
|
const data4 = unwrapApiResult(response);
|
|
144920
144954
|
printOutput8(options, data4);
|
|
144921
144955
|
}
|
|
144956
|
+
async function runDashboardCreate(options) {
|
|
144957
|
+
const context = createApiContext(options);
|
|
144958
|
+
const project = await resolveProjectRef(options, context, { ownerSlug: true });
|
|
144959
|
+
const body = buildDashboardCreateBody(options, project);
|
|
144960
|
+
const data4 = await postApiJson("/v1/dashboards", context, body);
|
|
144961
|
+
printOutput8(options, {
|
|
144962
|
+
message: `Dashboard "${options.title}" created`,
|
|
144963
|
+
dashboard: data4.dashboard ?? data4
|
|
144964
|
+
});
|
|
144965
|
+
}
|
|
144922
144966
|
async function runDashboardExport(dashboardId, options) {
|
|
144923
144967
|
const context = createApiContext(options);
|
|
144924
144968
|
const response = await import_api14.WebService.exportDashboard({
|
|
@@ -144946,6 +144990,40 @@ async function runDashboardImport(dashboardId, options) {
|
|
|
144946
144990
|
const data4 = unwrapApiResult(response);
|
|
144947
144991
|
printOutput8(options, { message: `Dashboard imported into ${dashboardId}`, dashboard: data4.dashboard });
|
|
144948
144992
|
}
|
|
144993
|
+
function buildDashboardCreateBody(options, project) {
|
|
144994
|
+
const input = loadJsonInput(options);
|
|
144995
|
+
const initialDashboard = normalizeDashboardInit(input);
|
|
144996
|
+
return {
|
|
144997
|
+
name: options.title,
|
|
144998
|
+
projectOwner: project.owner,
|
|
144999
|
+
projectSlug: project.slug,
|
|
145000
|
+
...initialDashboard
|
|
145001
|
+
};
|
|
145002
|
+
}
|
|
145003
|
+
function normalizeDashboardInit(input) {
|
|
145004
|
+
const emptyLayouts = {
|
|
145005
|
+
responsiveLayouts: {
|
|
145006
|
+
lg: { layouts: [] },
|
|
145007
|
+
md: { layouts: [] },
|
|
145008
|
+
sm: { layouts: [] },
|
|
145009
|
+
xs: { layouts: [] }
|
|
145010
|
+
}
|
|
145011
|
+
};
|
|
145012
|
+
if (input === void 0) {
|
|
145013
|
+
return {
|
|
145014
|
+
panels: {},
|
|
145015
|
+
layouts: emptyLayouts
|
|
145016
|
+
};
|
|
145017
|
+
}
|
|
145018
|
+
if (!input || typeof input !== "object" || Array.isArray(input)) {
|
|
145019
|
+
throw new CliError("Dashboard initialization data must be a JSON or YAML object.");
|
|
145020
|
+
}
|
|
145021
|
+
const dashboard = input;
|
|
145022
|
+
return {
|
|
145023
|
+
panels: isRecord(dashboard.panels) ? dashboard.panels : {},
|
|
145024
|
+
layouts: isRecord(dashboard.layouts) ? dashboard.layouts : emptyLayouts
|
|
145025
|
+
};
|
|
145026
|
+
}
|
|
144949
145027
|
async function runDashboardAddPanel(dashboardId, options) {
|
|
144950
145028
|
const context = createApiContext(options);
|
|
144951
145029
|
const selectedSources = [Boolean(options.sql), Boolean(options.event), Boolean(options.metric)].filter(Boolean).length;
|
|
@@ -144967,11 +145045,13 @@ async function runDashboardAddPanel(dashboardId, options) {
|
|
|
144967
145045
|
const chartType = normalizeChartType(options.type);
|
|
144968
145046
|
const panelId = generatePanelId();
|
|
144969
145047
|
const chart = buildPanelChart(chartType, options);
|
|
145048
|
+
const timeRangeOverride = buildTimeRangeOverride(options);
|
|
144970
145049
|
const newPanel = {
|
|
144971
145050
|
id: panelId,
|
|
144972
145051
|
name: options.panelName,
|
|
144973
145052
|
dashboardId,
|
|
144974
|
-
chart
|
|
145053
|
+
chart,
|
|
145054
|
+
...timeRangeOverride ? { timeRangeOverride } : {}
|
|
144975
145055
|
};
|
|
144976
145056
|
const existingLayouts = dashboard.layouts?.responsiveLayouts?.lg?.layouts ?? [];
|
|
144977
145057
|
let maxBottom = 0;
|
|
@@ -144990,15 +145070,17 @@ async function runDashboardAddPanel(dashboardId, options) {
|
|
|
144990
145070
|
};
|
|
144991
145071
|
const panels = { ...dashboard.panels ?? {} };
|
|
144992
145072
|
panels[panelId] = newPanel;
|
|
144993
|
-
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
|
+
}
|
|
144994
145079
|
const dashboardJson = {
|
|
144995
145080
|
...dashboard,
|
|
144996
145081
|
panels,
|
|
144997
145082
|
layouts: {
|
|
144998
|
-
responsiveLayouts:
|
|
144999
|
-
...dashboard.layouts?.responsiveLayouts ?? {},
|
|
145000
|
-
lg: { layouts: updatedLayouts }
|
|
145001
|
-
}
|
|
145083
|
+
responsiveLayouts: updatedResponsive
|
|
145002
145084
|
}
|
|
145003
145085
|
};
|
|
145004
145086
|
const importResponse = await import_api14.WebService.importDashboard({
|
|
@@ -145016,6 +145098,40 @@ async function runDashboardAddPanel(dashboardId, options) {
|
|
|
145016
145098
|
dashboard: importData.dashboard
|
|
145017
145099
|
});
|
|
145018
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
|
+
}
|
|
145019
145135
|
function buildPanelChart(chartType, options) {
|
|
145020
145136
|
if (options.sql) {
|
|
145021
145137
|
const sqlSize = Number.parseInt(String(options.size ?? "100"), 10) || 100;
|
|
@@ -145072,6 +145188,9 @@ function collectOption3(value, previous = []) {
|
|
|
145072
145188
|
previous.push(value);
|
|
145073
145189
|
return previous;
|
|
145074
145190
|
}
|
|
145191
|
+
function isRecord(value) {
|
|
145192
|
+
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
145193
|
+
}
|
|
145075
145194
|
function withAuthOptions8(command) {
|
|
145076
145195
|
return command.option("--host <host>", "Override Sentio host").option("--api-key <key>", "Use an explicit API key instead of saved credentials").option("--token <token>", "Use an explicit bearer token instead of saved credentials");
|
|
145077
145196
|
}
|
|
@@ -145082,7 +145201,7 @@ function withOutputOptions8(command) {
|
|
|
145082
145201
|
return command.option("--json", "Print raw JSON response").option("--yaml", "Print raw YAML response");
|
|
145083
145202
|
}
|
|
145084
145203
|
function handleDashboardCommandError(error, command) {
|
|
145085
|
-
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("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"))) {
|
|
145086
145205
|
console.error(error.message);
|
|
145087
145206
|
if (command) {
|
|
145088
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
|
|
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
createApiContext,
|
|
8
8
|
handleCommandError,
|
|
9
9
|
loadJsonInput,
|
|
10
|
+
postApiJson,
|
|
10
11
|
resolveProjectRef,
|
|
11
12
|
unwrapApiResult
|
|
12
13
|
} from '../api.js'
|
|
@@ -30,6 +31,12 @@ interface DashboardImportOptions extends DashboardOptions {
|
|
|
30
31
|
overrideLayouts?: boolean
|
|
31
32
|
}
|
|
32
33
|
|
|
34
|
+
interface DashboardCreateOptions extends DashboardOptions {
|
|
35
|
+
title?: string
|
|
36
|
+
file?: string
|
|
37
|
+
stdin?: boolean
|
|
38
|
+
}
|
|
39
|
+
|
|
33
40
|
interface AddPanelOptions extends DashboardOptions {
|
|
34
41
|
panelName?: string
|
|
35
42
|
type?: string
|
|
@@ -43,11 +50,15 @@ interface AddPanelOptions extends DashboardOptions {
|
|
|
43
50
|
groupBy?: string[]
|
|
44
51
|
aggr?: string
|
|
45
52
|
func?: string[]
|
|
53
|
+
timeRangeStart?: string
|
|
54
|
+
timeRangeEnd?: string
|
|
55
|
+
timeRangeStep?: string
|
|
46
56
|
}
|
|
47
57
|
|
|
48
58
|
export function createDashboardCommand() {
|
|
49
59
|
const dashboardCommand = new Command('dashboard').description('Manage Sentio dashboards')
|
|
50
60
|
dashboardCommand.addCommand(createDashboardListCommand())
|
|
61
|
+
dashboardCommand.addCommand(createDashboardCreateCommand())
|
|
51
62
|
dashboardCommand.addCommand(createDashboardExportCommand())
|
|
52
63
|
dashboardCommand.addCommand(createDashboardImportCommand())
|
|
53
64
|
dashboardCommand.addCommand(createDashboardAddPanelCommand())
|
|
@@ -68,6 +79,23 @@ function createDashboardListCommand() {
|
|
|
68
79
|
})
|
|
69
80
|
}
|
|
70
81
|
|
|
82
|
+
function createDashboardCreateCommand() {
|
|
83
|
+
return withOutputOptions(
|
|
84
|
+
withSharedProjectOptions(withAuthOptions(new Command('create').description('Create a dashboard for a project')))
|
|
85
|
+
)
|
|
86
|
+
.showHelpAfterError()
|
|
87
|
+
.requiredOption('--title <name>', 'Dashboard title')
|
|
88
|
+
.option('--file <path>', 'Read initial dashboard JSON or YAML from file')
|
|
89
|
+
.option('--stdin', 'Read initial dashboard JSON or YAML from stdin')
|
|
90
|
+
.action(async (options, command) => {
|
|
91
|
+
try {
|
|
92
|
+
await runDashboardCreate(options)
|
|
93
|
+
} catch (error) {
|
|
94
|
+
handleDashboardCommandError(error, command)
|
|
95
|
+
}
|
|
96
|
+
})
|
|
97
|
+
}
|
|
98
|
+
|
|
71
99
|
function createDashboardExportCommand() {
|
|
72
100
|
return withOutputOptions(
|
|
73
101
|
withSharedProjectOptions(
|
|
@@ -137,6 +165,15 @@ function createDashboardAddPanelCommand() {
|
|
|
137
165
|
.option('--group-by <field>', 'Group by event property or metric label', collectOption, [])
|
|
138
166
|
.option('--aggr <aggregation>', 'Event: total|unique|AAU|DAU|WAU|MAU. Metric: avg|sum|min|max|count')
|
|
139
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)')
|
|
140
177
|
.addHelpText(
|
|
141
178
|
'after',
|
|
142
179
|
`
|
|
@@ -174,7 +211,17 @@ Metric insights panel examples:
|
|
|
174
211
|
--metric burn --filter meta.chain=1 --aggr avg --group-by meta.address
|
|
175
212
|
$ sentio dashboard add-panel abc123 --project owner/slug \\
|
|
176
213
|
--panel-name "Burn Rate Delta" --type LINE \\
|
|
177
|
-
--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
|
|
178
225
|
`
|
|
179
226
|
)
|
|
180
227
|
.action(async (dashboardId, options, command) => {
|
|
@@ -197,6 +244,18 @@ async function runDashboardList(options: DashboardOptions) {
|
|
|
197
244
|
printOutput(options, data)
|
|
198
245
|
}
|
|
199
246
|
|
|
247
|
+
async function runDashboardCreate(options: DashboardCreateOptions) {
|
|
248
|
+
const context = createApiContext(options)
|
|
249
|
+
const project = await resolveProjectRef(options, context, { ownerSlug: true })
|
|
250
|
+
const body = buildDashboardCreateBody(options, project)
|
|
251
|
+
const data = await postApiJson<{ dashboard?: Record<string, unknown> }>('/v1/dashboards', context, body)
|
|
252
|
+
|
|
253
|
+
printOutput(options, {
|
|
254
|
+
message: `Dashboard "${options.title}" created`,
|
|
255
|
+
dashboard: data.dashboard ?? data
|
|
256
|
+
})
|
|
257
|
+
}
|
|
258
|
+
|
|
200
259
|
async function runDashboardExport(dashboardId: string, options: DashboardOptions) {
|
|
201
260
|
const context = createApiContext(options)
|
|
202
261
|
const response = await WebService.exportDashboard({
|
|
@@ -229,6 +288,46 @@ async function runDashboardImport(dashboardId: string, options: DashboardImportO
|
|
|
229
288
|
printOutput(options, { message: `Dashboard imported into ${dashboardId}`, dashboard: data.dashboard })
|
|
230
289
|
}
|
|
231
290
|
|
|
291
|
+
function buildDashboardCreateBody(options: DashboardCreateOptions, project: { owner: string; slug: string }) {
|
|
292
|
+
const input = loadJsonInput(options)
|
|
293
|
+
const initialDashboard = normalizeDashboardInit(input)
|
|
294
|
+
|
|
295
|
+
return {
|
|
296
|
+
name: options.title,
|
|
297
|
+
projectOwner: project.owner,
|
|
298
|
+
projectSlug: project.slug,
|
|
299
|
+
...initialDashboard
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
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
|
+
|
|
313
|
+
if (input === undefined) {
|
|
314
|
+
return {
|
|
315
|
+
panels: {},
|
|
316
|
+
layouts: emptyLayouts
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
if (!input || typeof input !== 'object' || Array.isArray(input)) {
|
|
321
|
+
throw new CliError('Dashboard initialization data must be a JSON or YAML object.')
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
const dashboard = input as Record<string, unknown>
|
|
325
|
+
return {
|
|
326
|
+
panels: isRecord(dashboard.panels) ? dashboard.panels : {},
|
|
327
|
+
layouts: isRecord(dashboard.layouts) ? dashboard.layouts : emptyLayouts
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
232
331
|
async function runDashboardAddPanel(dashboardId: string, options: AddPanelOptions) {
|
|
233
332
|
const context = createApiContext(options)
|
|
234
333
|
|
|
@@ -257,11 +356,13 @@ async function runDashboardAddPanel(dashboardId: string, options: AddPanelOption
|
|
|
257
356
|
const panelId = generatePanelId()
|
|
258
357
|
const chart = buildPanelChart(chartType, options)
|
|
259
358
|
|
|
260
|
-
const
|
|
359
|
+
const timeRangeOverride = buildTimeRangeOverride(options)
|
|
360
|
+
const newPanel: Record<string, unknown> = {
|
|
261
361
|
id: panelId,
|
|
262
362
|
name: options.panelName,
|
|
263
363
|
dashboardId,
|
|
264
|
-
chart
|
|
364
|
+
chart,
|
|
365
|
+
...(timeRangeOverride ? { timeRangeOverride } : {})
|
|
265
366
|
}
|
|
266
367
|
|
|
267
368
|
// 3. Compute layout position: place below all existing panels
|
|
@@ -286,16 +387,18 @@ async function runDashboardAddPanel(dashboardId: string, options: AddPanelOption
|
|
|
286
387
|
const panels = { ...(dashboard.panels ?? {}) }
|
|
287
388
|
panels[panelId] = newPanel as never
|
|
288
389
|
|
|
289
|
-
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
|
+
}
|
|
290
396
|
|
|
291
397
|
const dashboardJson: Record<string, unknown> = {
|
|
292
398
|
...dashboard,
|
|
293
399
|
panels,
|
|
294
400
|
layouts: {
|
|
295
|
-
responsiveLayouts:
|
|
296
|
-
...(dashboard.layouts?.responsiveLayouts ?? {}),
|
|
297
|
-
lg: { layouts: updatedLayouts }
|
|
298
|
-
}
|
|
401
|
+
responsiveLayouts: updatedResponsive
|
|
299
402
|
}
|
|
300
403
|
}
|
|
301
404
|
|
|
@@ -315,6 +418,43 @@ async function runDashboardAddPanel(dashboardId: string, options: AddPanelOption
|
|
|
315
418
|
})
|
|
316
419
|
}
|
|
317
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
|
+
|
|
318
458
|
function buildPanelChart(chartType: string, options: AddPanelOptions) {
|
|
319
459
|
if (options.sql) {
|
|
320
460
|
const sqlSize = Number.parseInt(String(options.size ?? '100'), 10) || 100
|
|
@@ -378,6 +518,10 @@ function collectOption(value: string, previous: string[] = []) {
|
|
|
378
518
|
return previous
|
|
379
519
|
}
|
|
380
520
|
|
|
521
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
522
|
+
return Boolean(value) && typeof value === 'object' && !Array.isArray(value)
|
|
523
|
+
}
|
|
524
|
+
|
|
381
525
|
function withAuthOptions<T extends Command<any, any, any>>(command: T) {
|
|
382
526
|
return command
|
|
383
527
|
.option('--host <host>', 'Override Sentio host')
|
|
@@ -404,13 +548,18 @@ function handleDashboardCommandError(error: unknown, command?: Command) {
|
|
|
404
548
|
error.message.startsWith('Invalid project ') ||
|
|
405
549
|
error.message.startsWith('Dashboard ') ||
|
|
406
550
|
error.message.startsWith('Provide --file or --stdin') ||
|
|
551
|
+
error.message.startsWith('Use either --file or --stdin') ||
|
|
552
|
+
error.message.startsWith('Expected JSON or YAML') ||
|
|
553
|
+
error.message.startsWith('Invalid JSON or YAML') ||
|
|
554
|
+
error.message.startsWith('Dashboard initialization data') ||
|
|
407
555
|
error.message.startsWith('Provide exactly one data source') ||
|
|
408
556
|
error.message.startsWith('Use exactly one of --sql') ||
|
|
409
557
|
error.message.startsWith('Invalid chart type') ||
|
|
410
558
|
error.message.startsWith('Invalid aggregation') ||
|
|
411
559
|
error.message.startsWith('Invalid metric aggregation') ||
|
|
412
560
|
error.message.startsWith('Invalid filter') ||
|
|
413
|
-
error.message.startsWith('Invalid metric selector')
|
|
561
|
+
error.message.startsWith('Invalid metric selector') ||
|
|
562
|
+
error.message.startsWith('Invalid time range value'))
|
|
414
563
|
) {
|
|
415
564
|
console.error(error.message)
|
|
416
565
|
if (command) {
|