1+ import { readFileSync } from "node:fs" ;
2+ import { randomBytes } from "node:crypto" ;
13import { Command } from "commander" ;
24import { DashboardApi } from "../api/dashboard.js" ;
35import { SafetyGuard } from "../safety/guard.js" ;
46import { formatEntityTable , formatJson } from "../utils/output.js" ;
57import { 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
727export 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