@sentio/cli 3.5.0-rc.1 → 3.5.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 +385 -38
- package/package.json +1 -1
- package/src/commands/dashboard.ts +472 -0
- package/src/commands/project.ts +10 -4
- package/src/index.ts +2 -0
package/lib/index.js
CHANGED
|
@@ -1220,7 +1220,7 @@ var require_command = __commonJS({
|
|
|
1220
1220
|
var childProcess4 = __require("node:child_process");
|
|
1221
1221
|
var path16 = __require("node:path");
|
|
1222
1222
|
var fs20 = __require("node:fs");
|
|
1223
|
-
var
|
|
1223
|
+
var process25 = __require("node:process");
|
|
1224
1224
|
var { Argument: Argument7, humanReadableArgName } = require_argument();
|
|
1225
1225
|
var { CommanderError: CommanderError2 } = require_error();
|
|
1226
1226
|
var { Help: Help2, stripColor } = require_help();
|
|
@@ -1267,13 +1267,13 @@ var require_command = __commonJS({
|
|
|
1267
1267
|
this._showSuggestionAfterError = true;
|
|
1268
1268
|
this._savedState = null;
|
|
1269
1269
|
this._outputConfiguration = {
|
|
1270
|
-
writeOut: (str) =>
|
|
1271
|
-
writeErr: (str) =>
|
|
1270
|
+
writeOut: (str) => process25.stdout.write(str),
|
|
1271
|
+
writeErr: (str) => process25.stderr.write(str),
|
|
1272
1272
|
outputError: (str, write) => write(str),
|
|
1273
|
-
getOutHelpWidth: () =>
|
|
1274
|
-
getErrHelpWidth: () =>
|
|
1275
|
-
getOutHasColors: () => useColor() ?? (
|
|
1276
|
-
getErrHasColors: () => useColor() ?? (
|
|
1273
|
+
getOutHelpWidth: () => process25.stdout.isTTY ? process25.stdout.columns : void 0,
|
|
1274
|
+
getErrHelpWidth: () => process25.stderr.isTTY ? process25.stderr.columns : void 0,
|
|
1275
|
+
getOutHasColors: () => useColor() ?? (process25.stdout.isTTY && process25.stdout.hasColors?.()),
|
|
1276
|
+
getErrHasColors: () => useColor() ?? (process25.stderr.isTTY && process25.stderr.hasColors?.()),
|
|
1277
1277
|
stripColor: (str) => stripColor(str)
|
|
1278
1278
|
};
|
|
1279
1279
|
this._hidden = false;
|
|
@@ -1668,7 +1668,7 @@ Expecting one of '${allowedValues.join("', '")}'`);
|
|
|
1668
1668
|
if (this._exitCallback) {
|
|
1669
1669
|
this._exitCallback(new CommanderError2(exitCode, code, message));
|
|
1670
1670
|
}
|
|
1671
|
-
|
|
1671
|
+
process25.exit(exitCode);
|
|
1672
1672
|
}
|
|
1673
1673
|
/**
|
|
1674
1674
|
* Register callback `fn` for the command.
|
|
@@ -2068,16 +2068,16 @@ Expecting one of '${allowedValues.join("', '")}'`);
|
|
|
2068
2068
|
}
|
|
2069
2069
|
parseOptions = parseOptions || {};
|
|
2070
2070
|
if (argv === void 0 && parseOptions.from === void 0) {
|
|
2071
|
-
if (
|
|
2071
|
+
if (process25.versions?.electron) {
|
|
2072
2072
|
parseOptions.from = "electron";
|
|
2073
2073
|
}
|
|
2074
|
-
const execArgv =
|
|
2074
|
+
const execArgv = process25.execArgv ?? [];
|
|
2075
2075
|
if (execArgv.includes("-e") || execArgv.includes("--eval") || execArgv.includes("-p") || execArgv.includes("--print")) {
|
|
2076
2076
|
parseOptions.from = "eval";
|
|
2077
2077
|
}
|
|
2078
2078
|
}
|
|
2079
2079
|
if (argv === void 0) {
|
|
2080
|
-
argv =
|
|
2080
|
+
argv = process25.argv;
|
|
2081
2081
|
}
|
|
2082
2082
|
this.rawArgs = argv.slice();
|
|
2083
2083
|
let userArgs;
|
|
@@ -2088,7 +2088,7 @@ Expecting one of '${allowedValues.join("', '")}'`);
|
|
|
2088
2088
|
userArgs = argv.slice(2);
|
|
2089
2089
|
break;
|
|
2090
2090
|
case "electron":
|
|
2091
|
-
if (
|
|
2091
|
+
if (process25.defaultApp) {
|
|
2092
2092
|
this._scriptPath = argv[1];
|
|
2093
2093
|
userArgs = argv.slice(2);
|
|
2094
2094
|
} else {
|
|
@@ -2275,11 +2275,11 @@ Expecting one of '${allowedValues.join("', '")}'`);
|
|
|
2275
2275
|
}
|
|
2276
2276
|
launchWithNode = sourceExt.includes(path16.extname(executableFile));
|
|
2277
2277
|
let proc;
|
|
2278
|
-
if (
|
|
2278
|
+
if (process25.platform !== "win32") {
|
|
2279
2279
|
if (launchWithNode) {
|
|
2280
2280
|
args.unshift(executableFile);
|
|
2281
|
-
args = incrementNodeInspectorPort(
|
|
2282
|
-
proc = childProcess4.spawn(
|
|
2281
|
+
args = incrementNodeInspectorPort(process25.execArgv).concat(args);
|
|
2282
|
+
proc = childProcess4.spawn(process25.argv[0], args, { stdio: "inherit" });
|
|
2283
2283
|
} else {
|
|
2284
2284
|
proc = childProcess4.spawn(executableFile, args, { stdio: "inherit" });
|
|
2285
2285
|
}
|
|
@@ -2290,13 +2290,13 @@ Expecting one of '${allowedValues.join("', '")}'`);
|
|
|
2290
2290
|
subcommand._name
|
|
2291
2291
|
);
|
|
2292
2292
|
args.unshift(executableFile);
|
|
2293
|
-
args = incrementNodeInspectorPort(
|
|
2294
|
-
proc = childProcess4.spawn(
|
|
2293
|
+
args = incrementNodeInspectorPort(process25.execArgv).concat(args);
|
|
2294
|
+
proc = childProcess4.spawn(process25.execPath, args, { stdio: "inherit" });
|
|
2295
2295
|
}
|
|
2296
2296
|
if (!proc.killed) {
|
|
2297
2297
|
const signals = ["SIGUSR1", "SIGUSR2", "SIGTERM", "SIGINT", "SIGHUP"];
|
|
2298
2298
|
signals.forEach((signal) => {
|
|
2299
|
-
|
|
2299
|
+
process25.on(signal, () => {
|
|
2300
2300
|
if (proc.killed === false && proc.exitCode === null) {
|
|
2301
2301
|
proc.kill(signal);
|
|
2302
2302
|
}
|
|
@@ -2307,7 +2307,7 @@ Expecting one of '${allowedValues.join("', '")}'`);
|
|
|
2307
2307
|
proc.on("close", (code) => {
|
|
2308
2308
|
code = code ?? 1;
|
|
2309
2309
|
if (!exitCallback) {
|
|
2310
|
-
|
|
2310
|
+
process25.exit(code);
|
|
2311
2311
|
} else {
|
|
2312
2312
|
exitCallback(
|
|
2313
2313
|
new CommanderError2(
|
|
@@ -2329,7 +2329,7 @@ Expecting one of '${allowedValues.join("', '")}'`);
|
|
|
2329
2329
|
throw new Error(`'${executableFile}' not executable`);
|
|
2330
2330
|
}
|
|
2331
2331
|
if (!exitCallback) {
|
|
2332
|
-
|
|
2332
|
+
process25.exit(1);
|
|
2333
2333
|
} else {
|
|
2334
2334
|
const wrappedError = new CommanderError2(
|
|
2335
2335
|
1,
|
|
@@ -2830,13 +2830,13 @@ Expecting one of '${allowedValues.join("', '")}'`);
|
|
|
2830
2830
|
*/
|
|
2831
2831
|
_parseOptionsEnv() {
|
|
2832
2832
|
this.options.forEach((option2) => {
|
|
2833
|
-
if (option2.envVar && option2.envVar in
|
|
2833
|
+
if (option2.envVar && option2.envVar in process25.env) {
|
|
2834
2834
|
const optionKey = option2.attributeName();
|
|
2835
2835
|
if (this.getOptionValue(optionKey) === void 0 || ["default", "config", "env"].includes(
|
|
2836
2836
|
this.getOptionValueSource(optionKey)
|
|
2837
2837
|
)) {
|
|
2838
2838
|
if (option2.required || option2.optional) {
|
|
2839
|
-
this.emit(`optionEnv:${option2.name()}`,
|
|
2839
|
+
this.emit(`optionEnv:${option2.name()}`, process25.env[option2.envVar]);
|
|
2840
2840
|
} else {
|
|
2841
2841
|
this.emit(`optionEnv:${option2.name()}`);
|
|
2842
2842
|
}
|
|
@@ -3360,7 +3360,7 @@ Expecting one of '${allowedValues.join("', '")}'`);
|
|
|
3360
3360
|
*/
|
|
3361
3361
|
help(contextOptions) {
|
|
3362
3362
|
this.outputHelp(contextOptions);
|
|
3363
|
-
let exitCode = Number(
|
|
3363
|
+
let exitCode = Number(process25.exitCode ?? 0);
|
|
3364
3364
|
if (exitCode === 0 && contextOptions && typeof contextOptions !== "function" && contextOptions.error) {
|
|
3365
3365
|
exitCode = 1;
|
|
3366
3366
|
}
|
|
@@ -3450,9 +3450,9 @@ Expecting one of '${allowedValues.join("', '")}'`);
|
|
|
3450
3450
|
});
|
|
3451
3451
|
}
|
|
3452
3452
|
function useColor() {
|
|
3453
|
-
if (
|
|
3453
|
+
if (process25.env.NO_COLOR || process25.env.FORCE_COLOR === "0" || process25.env.FORCE_COLOR === "false")
|
|
3454
3454
|
return false;
|
|
3455
|
-
if (
|
|
3455
|
+
if (process25.env.FORCE_COLOR || process25.env.CLICOLOR_FORCE !== void 0)
|
|
3456
3456
|
return true;
|
|
3457
3457
|
return void 0;
|
|
3458
3458
|
}
|
|
@@ -5903,10 +5903,10 @@ var require_directives = __commonJS({
|
|
|
5903
5903
|
};
|
|
5904
5904
|
var escapeTagName = (tn2) => tn2.replace(/[!,[\]{}]/g, (ch) => escapeChars[ch]);
|
|
5905
5905
|
var Directives = class _Directives {
|
|
5906
|
-
constructor(
|
|
5906
|
+
constructor(yaml13, tags) {
|
|
5907
5907
|
this.docStart = null;
|
|
5908
5908
|
this.docEnd = false;
|
|
5909
|
-
this.yaml = Object.assign({}, _Directives.defaultYaml,
|
|
5909
|
+
this.yaml = Object.assign({}, _Directives.defaultYaml, yaml13);
|
|
5910
5910
|
this.tags = Object.assign({}, _Directives.defaultTags, tags);
|
|
5911
5911
|
}
|
|
5912
5912
|
clone() {
|
|
@@ -41614,15 +41614,15 @@ var require_streams2 = __commonJS({
|
|
|
41614
41614
|
var POOL_SIZE2 = 65536;
|
|
41615
41615
|
if (!globalThis.ReadableStream) {
|
|
41616
41616
|
try {
|
|
41617
|
-
const
|
|
41618
|
-
const { emitWarning } =
|
|
41617
|
+
const process25 = __require("node:process");
|
|
41618
|
+
const { emitWarning } = process25;
|
|
41619
41619
|
try {
|
|
41620
|
-
|
|
41620
|
+
process25.emitWarning = () => {
|
|
41621
41621
|
};
|
|
41622
41622
|
Object.assign(globalThis, __require("node:stream/web"));
|
|
41623
|
-
|
|
41623
|
+
process25.emitWarning = emitWarning;
|
|
41624
41624
|
} catch (error) {
|
|
41625
|
-
|
|
41625
|
+
process25.emitWarning = emitWarning;
|
|
41626
41626
|
throw error;
|
|
41627
41627
|
}
|
|
41628
41628
|
} catch (error) {
|
|
@@ -141911,7 +141911,7 @@ var require_sdk_gen = __commonJS({
|
|
|
141911
141911
|
}
|
|
141912
141912
|
};
|
|
141913
141913
|
exports.DataService = DataService2;
|
|
141914
|
-
var
|
|
141914
|
+
var WebService2 = class {
|
|
141915
141915
|
/**
|
|
141916
141916
|
* List all dashboards in a project
|
|
141917
141917
|
*/
|
|
@@ -142094,7 +142094,7 @@ var require_sdk_gen = __commonJS({
|
|
|
142094
142094
|
});
|
|
142095
142095
|
}
|
|
142096
142096
|
};
|
|
142097
|
-
exports.WebService =
|
|
142097
|
+
exports.WebService = WebService2;
|
|
142098
142098
|
var MoveService = class {
|
|
142099
142099
|
/**
|
|
142100
142100
|
* Get Aptos transaction call trace
|
|
@@ -151775,7 +151775,7 @@ function formatOutput(data) {
|
|
|
151775
151775
|
for (const project2 of [...projects].sort((left, right) => (left.slug ?? "").localeCompare(right.slug ?? ""))) {
|
|
151776
151776
|
const owner2 = getOwnerName(project2) ?? "<owner>";
|
|
151777
151777
|
const slug2 = project2.slug ?? "<slug>";
|
|
151778
|
-
const attrs =
|
|
151778
|
+
const attrs = formatProjectListAttrs(project2);
|
|
151779
151779
|
const updatedAt = formatTimestamp2(project2.updatedAt);
|
|
151780
151780
|
lines3.push(`- ${owner2}/${slug2}${attrs ? ` [${attrs}]` : ""}${updatedAt ? ` updated ${updatedAt}` : ""}`);
|
|
151781
151781
|
}
|
|
@@ -151787,7 +151787,7 @@ function formatOutput(data) {
|
|
|
151787
151787
|
lines2.push(`${group.owner} (${group.projects.length})`);
|
|
151788
151788
|
for (const project2 of group.projects) {
|
|
151789
151789
|
const slug2 = project2.slug ?? "<slug>";
|
|
151790
|
-
const attrs =
|
|
151790
|
+
const attrs = formatProjectListAttrs(project2);
|
|
151791
151791
|
const updatedAt = formatTimestamp2(project2.updatedAt);
|
|
151792
151792
|
lines2.push(`- ${slug2}${attrs ? ` [${attrs}]` : ""}${updatedAt ? ` updated ${updatedAt}` : ""}`);
|
|
151793
151793
|
}
|
|
@@ -151827,6 +151827,10 @@ function formatOutput(data) {
|
|
|
151827
151827
|
}
|
|
151828
151828
|
return lines.join("\n");
|
|
151829
151829
|
}
|
|
151830
|
+
function formatProjectListAttrs(project) {
|
|
151831
|
+
const attrs = [project.type === "SENTIO" ? void 0 : project.type, project.visibility].filter(Boolean);
|
|
151832
|
+
return attrs.join(", ");
|
|
151833
|
+
}
|
|
151830
151834
|
function normalizeProjectList(data) {
|
|
151831
151835
|
if (Array.isArray(data)) {
|
|
151832
151836
|
return data;
|
|
@@ -151911,7 +151915,8 @@ async function getProjectsByOwner(ownerName, context) {
|
|
|
151911
151915
|
const organizationResponse = await fetchApiJson("/api/v1/organizations", context, { orgIdOrName: ownerName });
|
|
151912
151916
|
const organization = organizationResponse.organizations?.find((entry) => entry.name === ownerName);
|
|
151913
151917
|
if (organization) {
|
|
151914
|
-
|
|
151918
|
+
const data = await fetchApiJson("/api/v1/projects", context, { owner: ownerName });
|
|
151919
|
+
return normalizeProjectList(data);
|
|
151915
151920
|
}
|
|
151916
151921
|
} catch {
|
|
151917
151922
|
}
|
|
@@ -151920,7 +151925,7 @@ async function getProjectsByOwner(ownerName, context) {
|
|
|
151920
151925
|
userName: ownerName
|
|
151921
151926
|
});
|
|
151922
151927
|
if (userInfo.id) {
|
|
151923
|
-
const data = await fetchApiJson("/api/v1/projects", context, {
|
|
151928
|
+
const data = await fetchApiJson("/api/v1/projects", context, { owner: ownerName });
|
|
151924
151929
|
return normalizeProjectList(data);
|
|
151925
151930
|
}
|
|
151926
151931
|
} catch {
|
|
@@ -154267,6 +154272,347 @@ function parseInteger6(value) {
|
|
|
154267
154272
|
return parsedValue;
|
|
154268
154273
|
}
|
|
154269
154274
|
|
|
154275
|
+
// src/commands/dashboard.ts
|
|
154276
|
+
init_cjs_shim();
|
|
154277
|
+
var import_api14 = __toESM(require_src5(), 1);
|
|
154278
|
+
var import_yaml12 = __toESM(require_dist(), 1);
|
|
154279
|
+
import process24 from "process";
|
|
154280
|
+
function createDashboardCommand() {
|
|
154281
|
+
const dashboardCommand = new Command("dashboard").description("Manage Sentio dashboards");
|
|
154282
|
+
dashboardCommand.addCommand(createDashboardListCommand());
|
|
154283
|
+
dashboardCommand.addCommand(createDashboardExportCommand());
|
|
154284
|
+
dashboardCommand.addCommand(createDashboardImportCommand());
|
|
154285
|
+
dashboardCommand.addCommand(createDashboardAddPanelCommand());
|
|
154286
|
+
return dashboardCommand;
|
|
154287
|
+
}
|
|
154288
|
+
function createDashboardListCommand() {
|
|
154289
|
+
return withOutputOptions8(
|
|
154290
|
+
withSharedProjectOptions7(withAuthOptions8(new Command("list").description("List dashboards for a project")))
|
|
154291
|
+
).showHelpAfterError().action(async (options, command) => {
|
|
154292
|
+
try {
|
|
154293
|
+
await runDashboardList(options);
|
|
154294
|
+
} catch (error) {
|
|
154295
|
+
handleDashboardCommandError(error, command);
|
|
154296
|
+
}
|
|
154297
|
+
});
|
|
154298
|
+
}
|
|
154299
|
+
function createDashboardExportCommand() {
|
|
154300
|
+
return withOutputOptions8(
|
|
154301
|
+
withSharedProjectOptions7(
|
|
154302
|
+
withAuthOptions8(
|
|
154303
|
+
new Command("export").description("Export a dashboard as JSON").argument("<dashboardId>", "Dashboard ID")
|
|
154304
|
+
)
|
|
154305
|
+
)
|
|
154306
|
+
).showHelpAfterError().action(async (dashboardId, options, command) => {
|
|
154307
|
+
try {
|
|
154308
|
+
await runDashboardExport(dashboardId, options);
|
|
154309
|
+
} catch (error) {
|
|
154310
|
+
handleDashboardCommandError(error, command);
|
|
154311
|
+
}
|
|
154312
|
+
});
|
|
154313
|
+
}
|
|
154314
|
+
function createDashboardImportCommand() {
|
|
154315
|
+
return withOutputOptions8(
|
|
154316
|
+
withSharedProjectOptions7(
|
|
154317
|
+
withAuthOptions8(
|
|
154318
|
+
new Command("import").description("Import dashboard data from a JSON file into an existing dashboard").argument("<dashboardId>", "Target dashboard ID to import into")
|
|
154319
|
+
)
|
|
154320
|
+
)
|
|
154321
|
+
).showHelpAfterError().option("--file <path>", "Read dashboard JSON from file").option("--stdin", "Read dashboard JSON from stdin").option("--override-layouts", "Override the layout of the target dashboard").action(async (dashboardId, options, command) => {
|
|
154322
|
+
try {
|
|
154323
|
+
await runDashboardImport(dashboardId, options);
|
|
154324
|
+
} catch (error) {
|
|
154325
|
+
handleDashboardCommandError(error, command);
|
|
154326
|
+
}
|
|
154327
|
+
});
|
|
154328
|
+
}
|
|
154329
|
+
function createDashboardAddPanelCommand() {
|
|
154330
|
+
return withOutputOptions8(
|
|
154331
|
+
withSharedProjectOptions7(
|
|
154332
|
+
withAuthOptions8(
|
|
154333
|
+
new Command("add-panel").description("Add a panel to a dashboard (SQL or insights query)").argument("<dashboardId>", "Dashboard ID")
|
|
154334
|
+
)
|
|
154335
|
+
)
|
|
154336
|
+
).showHelpAfterError().requiredOption("--panel-name <name>", "Panel name").requiredOption("--type <type>", "Chart type: TABLE, LINE, BAR, PIE, QUERY_VALUE, AREA, BAR_GAUGE, SCATTER").option("--sql <query>", "SQL query (plain SQL \u2014 automatically wrapped into the required format)").option("--size <count>", "SQL query result size limit (default: 100)").option("--event <name>", "Event name for an insights panel (mutually exclusive with --sql and --metric)").option("--metric <name>", "Metric name for an insights panel (mutually exclusive with --sql and --event)").option("--alias <alias>", "Alias for the insights query").option("--source-name <name>", "Source name for the insights query").option(
|
|
154337
|
+
"--filter <selector>",
|
|
154338
|
+
"Event filter or metric label selector like field:value or amount>0",
|
|
154339
|
+
collectOption3,
|
|
154340
|
+
[]
|
|
154341
|
+
).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, []).addHelpText(
|
|
154342
|
+
"after",
|
|
154343
|
+
`
|
|
154344
|
+
|
|
154345
|
+
Data source: use exactly one of --sql, --event, or --metric.
|
|
154346
|
+
|
|
154347
|
+
SQL panel examples:
|
|
154348
|
+
$ sentio dashboard add-panel abc123 --project owner/slug \\
|
|
154349
|
+
--panel-name "Top Holders" --type TABLE \\
|
|
154350
|
+
--sql "SELECT * FROM CoinBalance ORDER BY balance DESC LIMIT 50"
|
|
154351
|
+
$ sentio dashboard add-panel abc123 --project owner/slug \\
|
|
154352
|
+
--panel-name "Daily Volume" --type LINE \\
|
|
154353
|
+
--sql "SELECT toStartOfDay(timestamp) as date, sum(amount) as volume FROM Transfer GROUP BY date ORDER BY date"
|
|
154354
|
+
|
|
154355
|
+
Event insights panel examples:
|
|
154356
|
+
$ sentio dashboard add-panel abc123 --project owner/slug \\
|
|
154357
|
+
--panel-name "Transfer Count" --type LINE \\
|
|
154358
|
+
--event Transfer --aggr total
|
|
154359
|
+
$ sentio dashboard add-panel abc123 --project owner/slug \\
|
|
154360
|
+
--panel-name "Large Transfers" --type TABLE \\
|
|
154361
|
+
--event Transfer --filter amount>1000 --aggr total --group-by meta.address
|
|
154362
|
+
$ sentio dashboard add-panel abc123 --project owner/slug \\
|
|
154363
|
+
--panel-name "Top 5 Senders" --type BAR \\
|
|
154364
|
+
--event Transfer --aggr unique --group-by from --func 'topk(5)'
|
|
154365
|
+
$ sentio dashboard add-panel abc123 --project owner/slug \\
|
|
154366
|
+
--panel-name "DAU" --type LINE \\
|
|
154367
|
+
--event Transfer --aggr DAU
|
|
154368
|
+
|
|
154369
|
+
Metric insights panel examples:
|
|
154370
|
+
$ sentio dashboard add-panel abc123 --project owner/slug \\
|
|
154371
|
+
--panel-name "ETH Price" --type LINE \\
|
|
154372
|
+
--metric cbETH_price
|
|
154373
|
+
$ sentio dashboard add-panel abc123 --project owner/slug \\
|
|
154374
|
+
--panel-name "Avg Burn by Chain" --type BAR \\
|
|
154375
|
+
--metric burn --filter meta.chain=1 --aggr avg --group-by meta.address
|
|
154376
|
+
$ sentio dashboard add-panel abc123 --project owner/slug \\
|
|
154377
|
+
--panel-name "Burn Rate Delta" --type LINE \\
|
|
154378
|
+
--metric burn --aggr sum
|
|
154379
|
+
`
|
|
154380
|
+
).action(async (dashboardId, options, command) => {
|
|
154381
|
+
try {
|
|
154382
|
+
await runDashboardAddPanel(dashboardId, options);
|
|
154383
|
+
} catch (error) {
|
|
154384
|
+
handleDashboardCommandError(error, command);
|
|
154385
|
+
}
|
|
154386
|
+
});
|
|
154387
|
+
}
|
|
154388
|
+
async function runDashboardList(options) {
|
|
154389
|
+
const context = createApiContext(options);
|
|
154390
|
+
const project = await resolveProjectRef(options, context, { ownerSlug: true });
|
|
154391
|
+
const response = await import_api14.WebService.listDashboards2({
|
|
154392
|
+
path: { owner: project.owner, slug: project.slug },
|
|
154393
|
+
headers: context.headers
|
|
154394
|
+
});
|
|
154395
|
+
const data = unwrapApiResult(response);
|
|
154396
|
+
printOutput8(options, data);
|
|
154397
|
+
}
|
|
154398
|
+
async function runDashboardExport(dashboardId, options) {
|
|
154399
|
+
const context = createApiContext(options);
|
|
154400
|
+
const response = await import_api14.WebService.exportDashboard({
|
|
154401
|
+
path: { dashboardId },
|
|
154402
|
+
headers: context.headers
|
|
154403
|
+
});
|
|
154404
|
+
const data = unwrapApiResult(response);
|
|
154405
|
+
console.log(JSON.stringify(data.dashboardJson ?? data, null, 2));
|
|
154406
|
+
}
|
|
154407
|
+
async function runDashboardImport(dashboardId, options) {
|
|
154408
|
+
const context = createApiContext(options);
|
|
154409
|
+
const input = loadJsonInput(options);
|
|
154410
|
+
if (!input) {
|
|
154411
|
+
throw new CliError("Provide --file or --stdin with the dashboard JSON to import.");
|
|
154412
|
+
}
|
|
154413
|
+
const dashboardJson = typeof input === "object" ? input : {};
|
|
154414
|
+
const response = await import_api14.WebService.importDashboard({
|
|
154415
|
+
body: {
|
|
154416
|
+
dashboardId,
|
|
154417
|
+
dashboardJson,
|
|
154418
|
+
overrideLayouts: options.overrideLayouts
|
|
154419
|
+
},
|
|
154420
|
+
headers: context.headers
|
|
154421
|
+
});
|
|
154422
|
+
const data = unwrapApiResult(response);
|
|
154423
|
+
printOutput8(options, { message: `Dashboard imported into ${dashboardId}`, dashboard: data.dashboard });
|
|
154424
|
+
}
|
|
154425
|
+
async function runDashboardAddPanel(dashboardId, options) {
|
|
154426
|
+
const context = createApiContext(options);
|
|
154427
|
+
const selectedSources = [Boolean(options.sql), Boolean(options.event), Boolean(options.metric)].filter(Boolean).length;
|
|
154428
|
+
if (selectedSources === 0) {
|
|
154429
|
+
throw new CliError("Provide exactly one data source: --sql, --event, or --metric.");
|
|
154430
|
+
}
|
|
154431
|
+
if (selectedSources > 1) {
|
|
154432
|
+
throw new CliError("Use exactly one of --sql, --event, or --metric.");
|
|
154433
|
+
}
|
|
154434
|
+
const getResponse = await import_api14.WebService.getDashboard({
|
|
154435
|
+
path: { dashboardId },
|
|
154436
|
+
headers: context.headers
|
|
154437
|
+
});
|
|
154438
|
+
const dashboardData = unwrapApiResult(getResponse);
|
|
154439
|
+
const dashboard = dashboardData.dashboards?.[0];
|
|
154440
|
+
if (!dashboard) {
|
|
154441
|
+
throw new CliError(`Dashboard ${dashboardId} not found.`);
|
|
154442
|
+
}
|
|
154443
|
+
const chartType = normalizeChartType(options.type);
|
|
154444
|
+
const panelId = generatePanelId();
|
|
154445
|
+
const chart = buildPanelChart(chartType, options);
|
|
154446
|
+
const newPanel = {
|
|
154447
|
+
id: panelId,
|
|
154448
|
+
name: options.panelName,
|
|
154449
|
+
dashboardId,
|
|
154450
|
+
chart
|
|
154451
|
+
};
|
|
154452
|
+
const existingLayouts = dashboard.layouts?.responsiveLayouts?.lg?.layouts ?? [];
|
|
154453
|
+
let maxBottom = 0;
|
|
154454
|
+
for (const layout of existingLayouts) {
|
|
154455
|
+
const bottom = (layout.y ?? 0) + (layout.h ?? 0);
|
|
154456
|
+
if (bottom > maxBottom) {
|
|
154457
|
+
maxBottom = bottom;
|
|
154458
|
+
}
|
|
154459
|
+
}
|
|
154460
|
+
const newLayout = {
|
|
154461
|
+
i: panelId,
|
|
154462
|
+
x: 0,
|
|
154463
|
+
y: maxBottom,
|
|
154464
|
+
w: 12,
|
|
154465
|
+
h: 6
|
|
154466
|
+
};
|
|
154467
|
+
const panels = { ...dashboard.panels ?? {} };
|
|
154468
|
+
panels[panelId] = newPanel;
|
|
154469
|
+
const updatedLayouts = [...existingLayouts, newLayout];
|
|
154470
|
+
const dashboardJson = {
|
|
154471
|
+
...dashboard,
|
|
154472
|
+
panels,
|
|
154473
|
+
layouts: {
|
|
154474
|
+
responsiveLayouts: {
|
|
154475
|
+
...dashboard.layouts?.responsiveLayouts ?? {},
|
|
154476
|
+
lg: { layouts: updatedLayouts }
|
|
154477
|
+
}
|
|
154478
|
+
}
|
|
154479
|
+
};
|
|
154480
|
+
const importResponse = await import_api14.WebService.importDashboard({
|
|
154481
|
+
body: {
|
|
154482
|
+
dashboardId,
|
|
154483
|
+
dashboardJson,
|
|
154484
|
+
overrideLayouts: true
|
|
154485
|
+
},
|
|
154486
|
+
headers: context.headers
|
|
154487
|
+
});
|
|
154488
|
+
const importData = unwrapApiResult(importResponse);
|
|
154489
|
+
printOutput8(options, {
|
|
154490
|
+
message: `Panel "${options.panelName}" added to dashboard ${dashboardId}`,
|
|
154491
|
+
panelId,
|
|
154492
|
+
dashboard: importData.dashboard
|
|
154493
|
+
});
|
|
154494
|
+
}
|
|
154495
|
+
function buildPanelChart(chartType, options) {
|
|
154496
|
+
if (options.sql) {
|
|
154497
|
+
const sqlSize = Number.parseInt(String(options.size ?? "100"), 10) || 100;
|
|
154498
|
+
return {
|
|
154499
|
+
type: chartType,
|
|
154500
|
+
datasourceType: "SQL",
|
|
154501
|
+
sqlQuery: JSON.stringify({ sql: options.sql, size: sqlSize })
|
|
154502
|
+
};
|
|
154503
|
+
}
|
|
154504
|
+
if (options.event) {
|
|
154505
|
+
const queryBody = buildEventsInsightQueryBody(options.event, {
|
|
154506
|
+
alias: options.alias,
|
|
154507
|
+
sourceName: options.sourceName,
|
|
154508
|
+
filter: options.filter,
|
|
154509
|
+
groupBy: options.groupBy,
|
|
154510
|
+
aggr: options.aggr,
|
|
154511
|
+
func: options.func
|
|
154512
|
+
});
|
|
154513
|
+
return {
|
|
154514
|
+
type: chartType,
|
|
154515
|
+
datasourceType: "INSIGHTS",
|
|
154516
|
+
insightsQueries: queryBody.queries
|
|
154517
|
+
};
|
|
154518
|
+
}
|
|
154519
|
+
if (options.metric) {
|
|
154520
|
+
const queryBody = buildMetricsInsightQueryBody(options.metric, {
|
|
154521
|
+
alias: options.alias,
|
|
154522
|
+
sourceName: options.sourceName,
|
|
154523
|
+
filter: options.filter,
|
|
154524
|
+
groupBy: options.groupBy,
|
|
154525
|
+
aggr: options.aggr,
|
|
154526
|
+
func: options.func
|
|
154527
|
+
});
|
|
154528
|
+
return {
|
|
154529
|
+
type: chartType,
|
|
154530
|
+
datasourceType: "INSIGHTS",
|
|
154531
|
+
insightsQueries: queryBody.queries
|
|
154532
|
+
};
|
|
154533
|
+
}
|
|
154534
|
+
throw new CliError("Provide exactly one data source: --sql, --event, or --metric.");
|
|
154535
|
+
}
|
|
154536
|
+
function normalizeChartType(value) {
|
|
154537
|
+
const normalized = value.toUpperCase();
|
|
154538
|
+
const valid = ["LINE", "AREA", "BAR", "BAR_GAUGE", "TABLE", "QUERY_VALUE", "PIE", "NOTE", "SCATTER"];
|
|
154539
|
+
if (valid.includes(normalized)) {
|
|
154540
|
+
return normalized;
|
|
154541
|
+
}
|
|
154542
|
+
throw new CliError(`Invalid chart type "${value}". Use one of: ${valid.join(", ")}`);
|
|
154543
|
+
}
|
|
154544
|
+
function generatePanelId() {
|
|
154545
|
+
return `panel_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
|
|
154546
|
+
}
|
|
154547
|
+
function collectOption3(value, previous = []) {
|
|
154548
|
+
previous.push(value);
|
|
154549
|
+
return previous;
|
|
154550
|
+
}
|
|
154551
|
+
function withAuthOptions8(command) {
|
|
154552
|
+
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");
|
|
154553
|
+
}
|
|
154554
|
+
function withSharedProjectOptions7(command) {
|
|
154555
|
+
return command.option("--project <project>", "Sentio project as <owner>/<slug> or <slug>").option("--owner <owner>", "Sentio project owner").option("--name <name>", "Sentio project name").option("--project-id <id>", "Sentio project id");
|
|
154556
|
+
}
|
|
154557
|
+
function withOutputOptions8(command) {
|
|
154558
|
+
return command.option("--json", "Print raw JSON response").option("--yaml", "Print raw YAML response");
|
|
154559
|
+
}
|
|
154560
|
+
function handleDashboardCommandError(error, command) {
|
|
154561
|
+
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"))) {
|
|
154562
|
+
console.error(error.message);
|
|
154563
|
+
if (command) {
|
|
154564
|
+
console.error();
|
|
154565
|
+
command.outputHelp();
|
|
154566
|
+
}
|
|
154567
|
+
process24.exit(1);
|
|
154568
|
+
}
|
|
154569
|
+
handleCommandError(error);
|
|
154570
|
+
}
|
|
154571
|
+
function printOutput8(options, data) {
|
|
154572
|
+
if (options.json && options.yaml) {
|
|
154573
|
+
throw new CliError("Choose only one structured output format: --json or --yaml.");
|
|
154574
|
+
}
|
|
154575
|
+
if (options.json) {
|
|
154576
|
+
console.log(JSON.stringify(data, null, 2));
|
|
154577
|
+
return;
|
|
154578
|
+
}
|
|
154579
|
+
if (options.yaml) {
|
|
154580
|
+
console.log(import_yaml12.default.stringify(data).trimEnd());
|
|
154581
|
+
return;
|
|
154582
|
+
}
|
|
154583
|
+
console.log(formatOutput7(data));
|
|
154584
|
+
}
|
|
154585
|
+
function formatOutput7(data) {
|
|
154586
|
+
if (data && typeof data === "object" && "message" in data) {
|
|
154587
|
+
return String(data.message ?? "");
|
|
154588
|
+
}
|
|
154589
|
+
if (data && typeof data === "object" && "dashboards" in data) {
|
|
154590
|
+
const dashboards = data.dashboards ?? [];
|
|
154591
|
+
const lines = [`Dashboards (${dashboards.length})`];
|
|
154592
|
+
for (const db of dashboards) {
|
|
154593
|
+
const id = db.id ?? "<id>";
|
|
154594
|
+
const name = db.name ?? "<unnamed>";
|
|
154595
|
+
const panelCount = db.panels ? Object.keys(db.panels).length : 0;
|
|
154596
|
+
const visibility = db.visibility ?? "";
|
|
154597
|
+
const updated = formatTimestamp3(db.updatedAt);
|
|
154598
|
+
lines.push(
|
|
154599
|
+
`- ${id} "${name}" [${panelCount} panels]${visibility ? ` ${visibility}` : ""}${updated ? ` updated ${updated}` : ""}`
|
|
154600
|
+
);
|
|
154601
|
+
}
|
|
154602
|
+
return lines.join("\n");
|
|
154603
|
+
}
|
|
154604
|
+
return JSON.stringify(data, null, 2);
|
|
154605
|
+
}
|
|
154606
|
+
function formatTimestamp3(value) {
|
|
154607
|
+
if (!value) {
|
|
154608
|
+
return "";
|
|
154609
|
+
}
|
|
154610
|
+
if (/^\d{13}$/.test(value)) {
|
|
154611
|
+
return new Date(Number.parseInt(value, 10)).toISOString();
|
|
154612
|
+
}
|
|
154613
|
+
return value;
|
|
154614
|
+
}
|
|
154615
|
+
|
|
154270
154616
|
// src/index.ts
|
|
154271
154617
|
var program2 = new Command();
|
|
154272
154618
|
program2.name("sentio").description("Login & Manage your project files to Sentio.");
|
|
@@ -154293,6 +154639,7 @@ program2.addCommand(createAlertCommand());
|
|
|
154293
154639
|
program2.addCommand(createPriceCommand());
|
|
154294
154640
|
program2.addCommand(createSimulationCommand());
|
|
154295
154641
|
program2.addCommand(createEndpointCommand());
|
|
154642
|
+
program2.addCommand(createDashboardCommand());
|
|
154296
154643
|
program2.parse();
|
|
154297
154644
|
/*! Bundled license information:
|
|
154298
154645
|
|
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
|
+
}
|
package/src/commands/project.ts
CHANGED
|
@@ -327,7 +327,7 @@ function formatOutput(data: unknown) {
|
|
|
327
327
|
for (const project of [...projects].sort((left, right) => (left.slug ?? '').localeCompare(right.slug ?? ''))) {
|
|
328
328
|
const owner = getOwnerName(project) ?? '<owner>'
|
|
329
329
|
const slug = project.slug ?? '<slug>'
|
|
330
|
-
const attrs =
|
|
330
|
+
const attrs = formatProjectListAttrs(project)
|
|
331
331
|
const updatedAt = formatTimestamp(project.updatedAt)
|
|
332
332
|
lines.push(`- ${owner}/${slug}${attrs ? ` [${attrs}]` : ''}${updatedAt ? ` updated ${updatedAt}` : ''}`)
|
|
333
333
|
}
|
|
@@ -340,7 +340,7 @@ function formatOutput(data: unknown) {
|
|
|
340
340
|
lines.push(`${group.owner} (${group.projects.length})`)
|
|
341
341
|
for (const project of group.projects) {
|
|
342
342
|
const slug = project.slug ?? '<slug>'
|
|
343
|
-
const attrs =
|
|
343
|
+
const attrs = formatProjectListAttrs(project)
|
|
344
344
|
const updatedAt = formatTimestamp(project.updatedAt)
|
|
345
345
|
lines.push(`- ${slug}${attrs ? ` [${attrs}]` : ''}${updatedAt ? ` updated ${updatedAt}` : ''}`)
|
|
346
346
|
}
|
|
@@ -383,6 +383,11 @@ function formatOutput(data: unknown) {
|
|
|
383
383
|
return lines.join('\n')
|
|
384
384
|
}
|
|
385
385
|
|
|
386
|
+
function formatProjectListAttrs(project: ProjectSummary) {
|
|
387
|
+
const attrs = [project.type === 'SENTIO' ? undefined : project.type, project.visibility].filter(Boolean)
|
|
388
|
+
return attrs.join(', ')
|
|
389
|
+
}
|
|
390
|
+
|
|
386
391
|
function normalizeProjectList(data: unknown): ProjectSummary[] {
|
|
387
392
|
if (Array.isArray(data)) {
|
|
388
393
|
return data as ProjectSummary[]
|
|
@@ -486,7 +491,8 @@ async function getProjectsByOwner(ownerName: string, context: ReturnType<typeof
|
|
|
486
491
|
}>('/api/v1/organizations', context, { orgIdOrName: ownerName })
|
|
487
492
|
const organization = organizationResponse.organizations?.find((entry) => entry.name === ownerName)
|
|
488
493
|
if (organization) {
|
|
489
|
-
|
|
494
|
+
const data = await fetchApiJson<unknown>('/api/v1/projects', context, { owner: ownerName })
|
|
495
|
+
return normalizeProjectList(data)
|
|
490
496
|
}
|
|
491
497
|
} catch {}
|
|
492
498
|
try {
|
|
@@ -494,7 +500,7 @@ async function getProjectsByOwner(ownerName: string, context: ReturnType<typeof
|
|
|
494
500
|
userName: ownerName
|
|
495
501
|
})
|
|
496
502
|
if (userInfo.id) {
|
|
497
|
-
const data = await fetchApiJson<unknown>('/api/v1/projects', context, {
|
|
503
|
+
const data = await fetchApiJson<unknown>('/api/v1/projects', context, { owner: ownerName })
|
|
498
504
|
return normalizeProjectList(data)
|
|
499
505
|
}
|
|
500
506
|
} catch {}
|
package/src/index.ts
CHANGED
|
@@ -18,6 +18,7 @@ import { createAlertCommand } from './commands/alert.js'
|
|
|
18
18
|
import { createPriceCommand } from './commands/price.js'
|
|
19
19
|
import { createSimulationCommand } from './commands/simulation.js'
|
|
20
20
|
import { createEndpointCommand } from './commands/endpoint.js'
|
|
21
|
+
import { createDashboardCommand } from './commands/dashboard.js'
|
|
21
22
|
import { enableApiDebug } from './api.js'
|
|
22
23
|
import { printVersions } from './utils.js'
|
|
23
24
|
|
|
@@ -50,5 +51,6 @@ program.addCommand(createAlertCommand())
|
|
|
50
51
|
program.addCommand(createPriceCommand())
|
|
51
52
|
program.addCommand(createSimulationCommand())
|
|
52
53
|
program.addCommand(createEndpointCommand())
|
|
54
|
+
program.addCommand(createDashboardCommand())
|
|
53
55
|
|
|
54
56
|
program.parse()
|