Skip to content

Commit 2f78cdb

Browse files
rohanraaroraclaude
andcommitted
Add dashboard filter/parameter commands
Add 6 new dashboard subcommands for managing filters and parameter mappings: - list-params: list all filters on a dashboard - add-param: add a filter with optional values source config - remove-param: remove a filter with cascading mapping cleanup - map-param: connect a filter to a card's template tag - unmap-param: remove a filter-to-card mapping - setup-filters: bulk setup from a JSON file Also extracts serializeDashcard() helper to reduce duplication in add-card/remove-card, and enriches Parameter/DashCard types with ParameterMapping and ValuesSourceConfig interfaces. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 9be42f9 commit 2f78cdb

File tree

3 files changed

+414
-25
lines changed

3 files changed

+414
-25
lines changed

src/commands/dashboard.ts

Lines changed: 325 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,28 @@
1+
import { readFileSync } from "node:fs";
2+
import { randomBytes } from "node:crypto";
13
import { Command } from "commander";
24
import { DashboardApi } from "../api/dashboard.js";
35
import { SafetyGuard } from "../safety/guard.js";
46
import { formatEntityTable, formatJson } from "../utils/output.js";
57
import { resolveClient, isUnsafe } from "./helpers.js";
8+
import type { DashCard, Parameter, ParameterMapping } from "../types.js";
9+
10+
function generateParamId(): string {
11+
return randomBytes(4).toString("hex");
12+
}
13+
14+
function serializeDashcard(dc: DashCard) {
15+
return {
16+
id: dc.id,
17+
card_id: dc.card_id,
18+
row: dc.row,
19+
col: dc.col,
20+
size_x: dc.size_x,
21+
size_y: dc.size_y,
22+
parameter_mappings: dc.parameter_mappings,
23+
visualization_settings: dc.visualization_settings,
24+
};
25+
}
626

727
export function dashboardCommand(): Command {
828
const cmd = new Command("dashboard").description("Manage dashboards").addHelpText(
@@ -206,19 +226,7 @@ Examples:
206226
size_y: opts.height,
207227
};
208228

209-
const updatedCards = [
210-
...dashboard.dashcards.map((dc: any) => ({
211-
id: dc.id,
212-
card_id: dc.card_id,
213-
row: dc.row,
214-
col: dc.col,
215-
size_x: dc.size_x,
216-
size_y: dc.size_y,
217-
parameter_mappings: dc.parameter_mappings,
218-
visualization_settings: dc.visualization_settings,
219-
})),
220-
newCard,
221-
];
229+
const updatedCards = [...dashboard.dashcards.map(serializeDashcard), newCard];
222230

223231
await api.update(dashId, { dashcards: updatedCards });
224232
console.log(
@@ -242,25 +250,318 @@ Examples:
242250
const dashId = parseInt(dashboardId);
243251
const dashboard = await api.get(dashId);
244252

245-
const filtered = dashboard.dashcards.filter((dc: any) => dc.card_id !== opts.card);
253+
const filtered = dashboard.dashcards.filter((dc) => dc.card_id !== opts.card);
246254
if (filtered.length === dashboard.dashcards.length) {
247255
console.error(`Card #${opts.card} not found on dashboard #${dashId}.`);
248256
process.exit(1);
249257
}
250258

251-
const updatedCards = filtered.map((dc: any) => ({
252-
id: dc.id,
253-
card_id: dc.card_id,
254-
row: dc.row,
255-
col: dc.col,
256-
size_x: dc.size_x,
257-
size_y: dc.size_y,
258-
parameter_mappings: dc.parameter_mappings,
259-
visualization_settings: dc.visualization_settings,
259+
await api.update(dashId, { dashcards: filtered.map(serializeDashcard) });
260+
console.log(`Card #${opts.card} removed from dashboard #${dashId}.`);
261+
});
262+
263+
// ─── Parameter/Filter Commands ──────────────────────────────────────────────
264+
265+
cmd
266+
.command("list-params <dashboard-id>")
267+
.description("List filters/parameters on a dashboard")
268+
.option("--format <format>", "Output format: table, json", "table")
269+
.addHelpText(
270+
"after",
271+
`
272+
Examples:
273+
$ metabase-cli dashboard list-params 7
274+
$ metabase-cli dashboard list-params 7 --format json`,
275+
)
276+
.action(async (dashboardId: string, opts) => {
277+
const client = await resolveClient();
278+
const api = new DashboardApi(client);
279+
const dashboard = await api.get(parseInt(dashboardId));
280+
281+
if (opts.format === "json") {
282+
console.log(formatJson(dashboard.parameters));
283+
return;
284+
}
285+
286+
if (dashboard.parameters.length === 0) {
287+
console.log("No parameters on this dashboard.");
288+
return;
289+
}
290+
291+
console.log(
292+
formatEntityTable(dashboard.parameters as any[], [
293+
{ key: "id", header: "ID" },
294+
{ key: "name", header: "Name" },
295+
{ key: "slug", header: "Slug" },
296+
{ key: "type", header: "Type" },
297+
{ key: "default", header: "Default" },
298+
{ key: "values_source_type", header: "Source" },
299+
]),
300+
);
301+
});
302+
303+
cmd
304+
.command("add-param <dashboard-id>")
305+
.description("Add a filter/parameter to a dashboard")
306+
.requiredOption("--type <type>", "Parameter type (e.g. date/single, string/=, number/=)")
307+
.requiredOption("--name <name>", "Display name")
308+
.requiredOption("--slug <slug>", "URL slug")
309+
.option("--id <id>", "Parameter ID (auto-generated if omitted)")
310+
.option("--default <value>", "Default value")
311+
.option("--source-card <id>", "Values source card ID (for dropdown filters)", parseInt)
312+
.option("--source-value-field <json>", "Value field ref as JSON")
313+
.option("--source-label-field <json>", "Label field ref as JSON")
314+
.addHelpText(
315+
"after",
316+
`
317+
Parameter types: date/single, date/range, string/=, string/contains, number/=, number/between, id
318+
319+
Examples:
320+
$ metabase-cli dashboard add-param 7 --type "date/single" --name "Start Date" --slug start_date --default "2026-01-01"
321+
$ metabase-cli dashboard add-param 7 --type "string/=" --name "Channel" --slug channel \\
322+
--source-card 99 --source-value-field '["field", "channel", {"base-type": "type/Text"}]'`,
323+
)
324+
.action(async (dashboardId: string, opts) => {
325+
const client = await resolveClient();
326+
const api = new DashboardApi(client);
327+
const dashId = parseInt(dashboardId);
328+
const dashboard = await api.get(dashId);
329+
330+
const param: Parameter = {
331+
id: opts.id || generateParamId(),
332+
type: opts.type,
333+
name: opts.name,
334+
slug: opts.slug,
335+
};
336+
337+
if (opts.default !== undefined) param.default = opts.default;
338+
339+
if (opts.sourceCard) {
340+
param.values_source_type = "card";
341+
param.values_source_config = { card_id: opts.sourceCard };
342+
if (opts.sourceValueField) {
343+
param.values_source_config.value_field = JSON.parse(opts.sourceValueField);
344+
}
345+
if (opts.sourceLabelField) {
346+
param.values_source_config.label_field = JSON.parse(opts.sourceLabelField);
347+
}
348+
}
349+
350+
await api.update(dashId, { parameters: [...dashboard.parameters, param] });
351+
console.log(`Parameter "${param.name}" (${param.id}) added to dashboard #${dashId}.`);
352+
});
353+
354+
cmd
355+
.command("remove-param <dashboard-id>")
356+
.description("Remove a filter/parameter from a dashboard")
357+
.requiredOption("--param <id-or-slug>", "Parameter ID or slug to remove")
358+
.addHelpText(
359+
"after",
360+
`
361+
Also removes all parameter mappings referencing this parameter from dashcards.
362+
363+
Examples:
364+
$ metabase-cli dashboard remove-param 7 --param start_date
365+
$ metabase-cli dashboard remove-param 7 --param f1a2b3c4`,
366+
)
367+
.action(async (dashboardId: string, opts) => {
368+
const client = await resolveClient();
369+
const api = new DashboardApi(client);
370+
const dashId = parseInt(dashboardId);
371+
const dashboard = await api.get(dashId);
372+
373+
const paramMatch = dashboard.parameters.find(
374+
(p) => p.id === opts.param || p.slug === opts.param,
375+
);
376+
if (!paramMatch) {
377+
console.error(`Parameter "${opts.param}" not found on dashboard #${dashId}.`);
378+
process.exit(1);
379+
}
380+
381+
const updatedParams = dashboard.parameters.filter((p) => p.id !== paramMatch.id);
382+
const updatedCards = dashboard.dashcards.map((dc) => ({
383+
...serializeDashcard(dc),
384+
parameter_mappings: dc.parameter_mappings.filter(
385+
(m) => m.parameter_id !== paramMatch.id,
386+
),
260387
}));
261388

389+
await api.update(dashId, { parameters: updatedParams, dashcards: updatedCards });
390+
console.log(`Parameter "${paramMatch.name}" removed from dashboard #${dashId}.`);
391+
});
392+
393+
cmd
394+
.command("map-param <dashboard-id>")
395+
.description("Map a dashboard filter to a card's template tag")
396+
.requiredOption("--param <id>", "Parameter ID on the dashboard")
397+
.requiredOption("--card <id>", "Card/question ID on the dashboard", parseInt)
398+
.requiredOption(
399+
"--target <json>",
400+
'Mapping target as JSON (e.g. \'["variable", ["template-tag", "start_date"]]\')',
401+
)
402+
.addHelpText(
403+
"after",
404+
`
405+
Examples:
406+
$ metabase-cli dashboard map-param 7 --param f1a2b3c4 --card 42 \\
407+
--target '["variable", ["template-tag", "start_date"]]'`,
408+
)
409+
.action(async (dashboardId: string, opts) => {
410+
const client = await resolveClient();
411+
const api = new DashboardApi(client);
412+
const dashId = parseInt(dashboardId);
413+
const dashboard = await api.get(dashId);
414+
415+
const paramExists = dashboard.parameters.some((p) => p.id === opts.param);
416+
if (!paramExists) {
417+
console.error(`Parameter "${opts.param}" not found on dashboard #${dashId}.`);
418+
process.exit(1);
419+
}
420+
421+
const dcIndex = dashboard.dashcards.findIndex((dc) => dc.card_id === opts.card);
422+
if (dcIndex === -1) {
423+
console.error(`Card #${opts.card} not found on dashboard #${dashId}.`);
424+
process.exit(1);
425+
}
426+
427+
const mapping: ParameterMapping = {
428+
parameter_id: opts.param,
429+
card_id: opts.card,
430+
target: JSON.parse(opts.target),
431+
};
432+
433+
const updatedCards = dashboard.dashcards.map((dc, i) => {
434+
const serialized = serializeDashcard(dc);
435+
if (i !== dcIndex) return serialized;
436+
437+
// Replace existing mapping for same param+card, or append
438+
const filtered = dc.parameter_mappings.filter(
439+
(m) => !(m.parameter_id === opts.param && m.card_id === opts.card),
440+
);
441+
return { ...serialized, parameter_mappings: [...filtered, mapping] };
442+
});
443+
262444
await api.update(dashId, { dashcards: updatedCards });
263-
console.log(`Card #${opts.card} removed from dashboard #${dashId}.`);
445+
console.log(
446+
`Parameter "${opts.param}" mapped to card #${opts.card} on dashboard #${dashId}.`,
447+
);
448+
});
449+
450+
cmd
451+
.command("unmap-param <dashboard-id>")
452+
.description("Remove a parameter mapping from a card")
453+
.requiredOption("--param <id>", "Parameter ID")
454+
.requiredOption("--card <id>", "Card/question ID", parseInt)
455+
.addHelpText(
456+
"after",
457+
`
458+
Examples:
459+
$ metabase-cli dashboard unmap-param 7 --param f1a2b3c4 --card 42`,
460+
)
461+
.action(async (dashboardId: string, opts) => {
462+
const client = await resolveClient();
463+
const api = new DashboardApi(client);
464+
const dashId = parseInt(dashboardId);
465+
const dashboard = await api.get(dashId);
466+
467+
const updatedCards = dashboard.dashcards.map((dc) => {
468+
const serialized = serializeDashcard(dc);
469+
if (dc.card_id !== opts.card) return serialized;
470+
return {
471+
...serialized,
472+
parameter_mappings: dc.parameter_mappings.filter(
473+
(m) => m.parameter_id !== opts.param,
474+
),
475+
};
476+
});
477+
478+
await api.update(dashId, { dashcards: updatedCards });
479+
console.log(
480+
`Parameter "${opts.param}" unmapped from card #${opts.card} on dashboard #${dashId}.`,
481+
);
482+
});
483+
484+
cmd
485+
.command("setup-filters <dashboard-id>")
486+
.description("Bulk setup filters and mappings from a JSON file")
487+
.requiredOption("--from-json <file>", "Path to JSON file with parameters and mappings")
488+
.addHelpText(
489+
"after",
490+
`
491+
JSON file format:
492+
{
493+
"parameters": [
494+
{ "type": "date/single", "name": "Start Date", "slug": "start_date", "default": "2026-01-01" },
495+
{ "type": "string/=", "name": "Channel", "slug": "channel",
496+
"values_source_type": "card",
497+
"values_source_config": { "card_id": 99, "value_field": ["field", "channel", {"base-type": "type/Text"}] }
498+
}
499+
],
500+
"mappings": [
501+
{ "param_slug": "start_date", "card_id": 42, "target": ["variable", ["template-tag", "start_date"]] }
502+
]
503+
}
504+
505+
Examples:
506+
$ metabase-cli dashboard setup-filters 7 --from-json filters.json`,
507+
)
508+
.action(async (dashboardId: string, opts) => {
509+
const client = await resolveClient();
510+
const api = new DashboardApi(client);
511+
const dashId = parseInt(dashboardId);
512+
const dashboard = await api.get(dashId);
513+
514+
const config = JSON.parse(readFileSync(opts.fromJson, "utf-8"));
515+
516+
// Build parameters with generated IDs
517+
const slugToId: Record<string, string> = {};
518+
const newParams: Parameter[] = (config.parameters || []).map(
519+
(p: Record<string, unknown>) => {
520+
const id = (p.id as string) || generateParamId();
521+
slugToId[p.slug as string] = id;
522+
return { ...p, id };
523+
},
524+
);
525+
526+
const allParams = [...dashboard.parameters, ...newParams];
527+
528+
// Validate and build mappings
529+
const cardIdsOnDash = new Set(dashboard.dashcards.map((dc) => dc.card_id));
530+
const allParamIds = new Set(allParams.map((p) => p.id));
531+
const mappingsByCard = new Map<number, ParameterMapping[]>();
532+
for (const m of config.mappings || []) {
533+
if (!cardIdsOnDash.has(m.card_id)) {
534+
console.error(`Card #${m.card_id} not found on dashboard #${dashId}.`);
535+
process.exit(1);
536+
}
537+
const paramId = slugToId[m.param_slug] || m.param_slug;
538+
if (!allParamIds.has(paramId)) {
539+
console.error(`Parameter "${m.param_slug}" not found in dashboard or JSON parameters.`);
540+
process.exit(1);
541+
}
542+
const mapping: ParameterMapping = {
543+
parameter_id: paramId,
544+
card_id: m.card_id,
545+
target: m.target,
546+
};
547+
const existing = mappingsByCard.get(m.card_id) || [];
548+
existing.push(mapping);
549+
mappingsByCard.set(m.card_id, existing);
550+
}
551+
552+
const updatedCards = dashboard.dashcards.map((dc) => {
553+
const serialized = serializeDashcard(dc);
554+
const newMappings = mappingsByCard.get(dc.card_id!) || [];
555+
return {
556+
...serialized,
557+
parameter_mappings: [...dc.parameter_mappings, ...newMappings],
558+
};
559+
});
560+
561+
await api.update(dashId, { parameters: allParams, dashcards: updatedCards });
562+
console.log(
563+
`Setup ${newParams.length} parameter(s) and ${(config.mappings || []).length} mapping(s) on dashboard #${dashId}.`,
564+
);
264565
});
265566

266567
return cmd;

0 commit comments

Comments
 (0)