Skip to content

Commit 8e18559

Browse files
authored
Merge pull request #30 from andreagrandi/dashboard-card-parameter-workflows
Improve dashboard and card parameter workflows
2 parents 892d089 + b27d904 commit 8e18559

11 files changed

Lines changed: 711 additions & 14 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
## [Unreleased]
44

5+
- Add `card params` and `dashboard params list` commands to discover the parameters a saved question or dashboard accepts before running it; failed parameterized runs now point to the exact discovery command (#14)
56
- Add `collection` commands (`list`, `get`, `items`) to browse collections and discover the cards, dashboards, and nested collections inside them, with `--models` filtering and `root` collection support (#13)
67
- Classify `--error-format json` errors from typed errors instead of message substrings, making structured error output reliable when wording changes (#12)
78
- Add `--timeout` flag and propagate request contexts so commands abort cleanly on Ctrl+C, with clear timeout and cancellation errors (#11)

internal/cli/card.go

Lines changed: 64 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,21 @@ var cardRunCmd = &cobra.Command{
4040
RunE: runCardRun,
4141
}
4242

43+
var cardParamsCmd = &cobra.Command{
44+
Use: "params <id>",
45+
Short: "List the parameters a saved question accepts",
46+
Long: "Lists the parameters a saved question accepts so they can be supplied to 'card run --param key=value'.",
47+
Args: cobra.ExactArgs(1),
48+
RunE: runCardParams,
49+
}
50+
51+
type cardParamsResult struct {
52+
CardID int `json:"card_id"`
53+
CardName string `json:"card_name"`
54+
QueryType string `json:"query_type,omitempty"`
55+
Parameters []client.CardParameter `json:"parameters"`
56+
}
57+
4358
type cardSummary struct {
4459
ID int `json:"id"`
4560
Name string `json:"name"`
@@ -57,6 +72,7 @@ func init() {
5772
cardCmd.AddCommand(cardListCmd)
5873
cardCmd.AddCommand(cardGetCmd)
5974
cardCmd.AddCommand(cardRunCmd)
75+
cardCmd.AddCommand(cardParamsCmd)
6076

6177
cardGetCmd.Flags().Bool("full", false, "Include the full query definition and card metadata")
6278
cardRunCmd.Flags().String("fields", "", "Comma-separated list of columns to include in output")
@@ -128,12 +144,44 @@ func runCardRun(cmd *cobra.Command, args []string) error {
128144

129145
result, err := c.RunCardWithParams(ctx, id, params)
130146
if err != nil {
131-
return wrapParameterizedRunError(err)
147+
return wrapParameterizedRunError(err, len(params) > 0, fmt.Sprintf("mb-cli card params %d", id))
132148
}
133149

134150
return formatQueryResultOutput(cmd, result)
135151
}
136152

153+
func runCardParams(cmd *cobra.Command, args []string) error {
154+
id, err := strconv.Atoi(args[0])
155+
if err != nil {
156+
return err
157+
}
158+
159+
c, err := newClient(cmd)
160+
if err != nil {
161+
return err
162+
}
163+
164+
ctx, cancel := requestContext(cmd)
165+
defer cancel()
166+
167+
card, err := c.GetCard(ctx, id)
168+
if err != nil {
169+
return err
170+
}
171+
172+
format, _ := cmd.Flags().GetString("format")
173+
if format == "json" {
174+
return formatter.Output(cmd, cardParamsResult{
175+
CardID: card.ID,
176+
CardName: card.Name,
177+
QueryType: card.QueryType,
178+
Parameters: card.CardParameters(),
179+
})
180+
}
181+
182+
return formatter.FormatCardParametersTable(card, os.Stdout)
183+
}
184+
137185
func summarizeCard(card *client.Card) cardSummary {
138186
return cardSummary{
139187
ID: card.ID,
@@ -181,15 +229,22 @@ func formatQueryResultOutput(cmd *cobra.Command, result *client.QueryResult) err
181229
return formatter.FormatQueryResults(format, columns, rows, os.Stdout)
182230
}
183231

184-
func wrapParameterizedRunError(err error) error {
232+
// wrapParameterizedRunError classifies a failed card or dashboard run. When the
233+
// caller supplied parameters, an API rejection is surfaced as a
234+
// ParameterizedQueryError carrying inspect, the exact command that lists the
235+
// valid parameters for the query. Metabase rejects invalid parameters with a
236+
// 500, so the supplied-parameters context, not the status code, drives this.
237+
func wrapParameterizedRunError(err error, hadParams bool, inspect string) error {
185238
var apiErr *mberr.APIError
186-
if errors.As(err, &apiErr) {
187-
switch apiErr.StatusCode {
188-
case http.StatusBadRequest:
189-
return &mberr.ParameterizedQueryError{Err: err}
190-
case http.StatusNotFound:
191-
return fmt.Errorf("query target was not found (%w)", err)
192-
}
239+
if !errors.As(err, &apiErr) {
240+
return err
241+
}
242+
243+
if apiErr.StatusCode == http.StatusNotFound {
244+
return fmt.Errorf("query target was not found (%w)", err)
245+
}
246+
if hadParams {
247+
return &mberr.ParameterizedQueryError{Err: err, Inspect: inspect}
193248
}
194249
return err
195250
}

internal/cli/context_embed.md

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,12 +30,14 @@ Set both environment variables (required):
3030
| `query filter` | Run a structured query with field filters | none | `--db`, `--table`, `--where` |
3131
| `card list` | List saved questions | none | none |
3232
| `card get <id>` | Get card details | id (positional) | none |
33+
| `card params <id>` | List the parameters a saved question accepts | id (positional) | none |
3334
| `card run <id>` | Execute a saved question | id (positional) | none |
3435
| `dashboard list` | List dashboards | none | none |
3536
| `dashboard get <id>` | Get dashboard details | id (positional) | none |
3637
| `dashboard cards <id>` | List cards used by a dashboard | id (positional) | none |
3738
| `dashboard analyze <id>` | Summarize dashboard dependencies | id (positional) | none |
3839
| `dashboard run-card <dashboard-id> <dashcard-id> <card-id>` | Execute a dashboard card | dashboard-id, dashcard-id, card-id (positional) | none |
40+
| `dashboard params list <dashboard-id>` | List a dashboard's parameters and the cards they filter | dashboard-id (positional) | none |
3941
| `dashboard params values <dashboard-id> <param-key>` | List valid dashboard parameter values | dashboard-id, param-key (positional) | none |
4042
| `dashboard params search <dashboard-id> <param-key> <query>` | Search dashboard parameter values | dashboard-id, param-key, query (positional) | none |
4143
| `collection list` | List collections | none | none |
@@ -148,6 +150,22 @@ Query result commands (`query sql`, `query filter`, `card run`, `table data`) fo
148150

149151
Dashboard inspection commands default to concise summaries in table mode. Use `--format json` for full raw dashboard or analysis payloads.
150152

153+
## Parameterized Cards and Dashboards
154+
155+
Some saved questions and dashboard cards take parameters. Discover them before
156+
running, instead of guessing parameter names or values:
157+
158+
- `card params <id>` lists the parameters a saved question accepts. Pass each
159+
listed `name` to `card run` as `--param <name>=<value>`.
160+
- `dashboard params list <dashboard-id>` lists a dashboard's parameters and how
161+
many cards each one filters.
162+
- `dashboard params values <dashboard-id> <param-key>` lists the valid values
163+
for a parameter; `dashboard params search` filters those values by a query.
164+
165+
When a parameterized run fails because a parameter key or value is invalid, the
166+
error's suggestion names the exact discovery command to run next (for example,
167+
`mb-cli card params 5` or `mb-cli dashboard params list 298`).
168+
151169
## Structured Error Output
152170

153171
Use `--error-format json` to get machine-readable errors on stderr:
@@ -213,12 +231,20 @@ mb-cli query filter --db 1 --table products --where "id=prod_1234" --export csv
213231
mb-cli card list
214232
mb-cli card run 5
215233

234+
# Discover a card's parameters before running it, then pass them
235+
mb-cli card params 5
236+
mb-cli card run 5 --param timeframe_days=14
237+
216238
# Inspect dashboard structure and dependencies
217239
mb-cli dashboard get 298
218240
mb-cli dashboard cards 298
219-
mb-cli dashboard params values 298 merchant_name
220241
mb-cli dashboard analyze 298
221242

243+
# Discover dashboard parameters, inspect their values, then run a card
244+
mb-cli dashboard params list 298
245+
mb-cli dashboard params values 298 merchant_name
246+
mb-cli dashboard run-card 298 1234 50 --param merchant_name=acme
247+
222248
# Browse collections and discover their contents
223249
mb-cli collection list
224250
mb-cli collection get root

internal/cli/dashboard.go

Lines changed: 65 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,13 @@ var dashboardParamsCmd = &cobra.Command{
5050
Short: "Dashboard parameter commands",
5151
}
5252

53+
var dashboardParamsListCmd = &cobra.Command{
54+
Use: "list <dashboard-id>",
55+
Short: "List a dashboard's parameters and the cards they filter",
56+
Args: cobra.ExactArgs(1),
57+
RunE: runDashboardParamsList,
58+
}
59+
5360
var dashboardParamsValuesCmd = &cobra.Command{
5461
Use: "values <dashboard-id> <param-key>",
5562
Short: "List valid values for a dashboard parameter",
@@ -98,6 +105,20 @@ type dashboardParamMappingRow struct {
98105
Target string `json:"target,omitempty"`
99106
}
100107

108+
type dashboardParamsListResult struct {
109+
DashboardID int `json:"dashboard_id"`
110+
DashboardName string `json:"dashboard_name"`
111+
Parameters []dashboardParamListRow `json:"parameters"`
112+
}
113+
114+
type dashboardParamListRow struct {
115+
ID string `json:"id"`
116+
Name string `json:"name"`
117+
Slug string `json:"slug"`
118+
Type string `json:"type"`
119+
MappedCards []dashboardParamMappingRow `json:"mapped_cards"`
120+
}
121+
101122
func init() {
102123
rootCmd.AddCommand(dashboardCmd)
103124

@@ -107,6 +128,7 @@ func init() {
107128
dashboardCmd.AddCommand(dashboardRunCardCmd)
108129
dashboardCmd.AddCommand(dashboardParamsCmd)
109130

131+
dashboardParamsCmd.AddCommand(dashboardParamsListCmd)
110132
dashboardParamsCmd.AddCommand(dashboardParamsValuesCmd)
111133
dashboardParamsCmd.AddCommand(dashboardParamsSearchCmd)
112134

@@ -225,6 +247,48 @@ func buildDashboardCardRows(dashboard *client.Dashboard) []dashboardCardRow {
225247
return rows
226248
}
227249

250+
func runDashboardParamsList(cmd *cobra.Command, args []string) error {
251+
dashboardID, err := strconv.Atoi(args[0])
252+
if err != nil {
253+
return err
254+
}
255+
256+
c, err := newClient(cmd)
257+
if err != nil {
258+
return err
259+
}
260+
261+
ctx, cancel := requestContext(cmd)
262+
defer cancel()
263+
264+
dashboard, err := c.GetDashboard(ctx, dashboardID)
265+
if err != nil {
266+
return err
267+
}
268+
269+
format, _ := cmd.Flags().GetString("format")
270+
if format == "json" {
271+
rows := make([]dashboardParamListRow, 0, len(dashboard.Parameters))
272+
for i := range dashboard.Parameters {
273+
parameter := &dashboard.Parameters[i]
274+
rows = append(rows, dashboardParamListRow{
275+
ID: parameter.ID,
276+
Name: parameter.Name,
277+
Slug: parameter.Slug,
278+
Type: parameter.Type,
279+
MappedCards: buildDashboardParamMappingRows(dashboard, parameter),
280+
})
281+
}
282+
return formatter.Output(cmd, dashboardParamsListResult{
283+
DashboardID: dashboard.ID,
284+
DashboardName: dashboard.Name,
285+
Parameters: rows,
286+
})
287+
}
288+
289+
return formatter.FormatDashboardParametersListTable(dashboard, os.Stdout)
290+
}
291+
228292
func runDashboardParamValues(cmd *cobra.Command, args []string) error {
229293
return runDashboardParamLookup(cmd, args[0], args[1], "", false)
230294
}
@@ -262,7 +326,7 @@ func runDashboardRunCard(cmd *cobra.Command, args []string) error {
262326

263327
result, err := c.RunDashboardCard(ctx, dashboardID, dashcardID, cardID, params)
264328
if err != nil {
265-
return wrapParameterizedRunError(err)
329+
return wrapParameterizedRunError(err, len(params) > 0, fmt.Sprintf("mb-cli dashboard params list %d", dashboardID))
266330
}
267331

268332
return formatQueryResultOutput(cmd, result)

internal/cli/root.go

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,10 @@ func classifyError(err error) (errorType, suggestion string) {
112112

113113
var paramErr *mberr.ParameterizedQueryError
114114
if errors.As(err, &paramErr) {
115-
return "API_ERROR", "Check parameter IDs with 'mb-cli dashboard get <id>' or 'mb-cli card get <id> --full'"
115+
if paramErr.Inspect != "" {
116+
return "API_ERROR", fmt.Sprintf("Run '%s' to list valid parameter names, types, and default values", paramErr.Inspect)
117+
}
118+
return "API_ERROR", "List valid parameters with 'mb-cli card params <id>' or 'mb-cli dashboard params list <id>'"
116119
}
117120

118121
var resolutionErr *mberr.ResolutionError

internal/client/parameters.go

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,40 @@ package client
22

33
import (
44
"encoding/json"
5+
"sort"
56
"strconv"
67
"strings"
78
)
89

10+
// CardParameters returns the parameters a saved question accepts, derived from
11+
// its native query template tags and sorted by name for stable output. Card
12+
// and snippet template tags are excluded because they reference other queries
13+
// rather than accepting a user-supplied value.
14+
func (c *Card) CardParameters() []CardParameter {
15+
if c == nil || c.DatasetQuery == nil || c.DatasetQuery.Native == nil {
16+
return []CardParameter{}
17+
}
18+
19+
tags := c.DatasetQuery.Native.TemplateTags
20+
params := make([]CardParameter, 0, len(tags))
21+
for key, tag := range tags {
22+
if tag.Type == "card" || tag.Type == "snippet" {
23+
continue
24+
}
25+
params = append(params, CardParameter{
26+
Name: key,
27+
DisplayName: tag.DisplayName,
28+
Type: tag.Type,
29+
WidgetType: tag.WidgetType,
30+
Required: tag.Required,
31+
Default: tag.Default,
32+
})
33+
}
34+
35+
sort.Slice(params, func(i, j int) bool { return params[i].Name < params[j].Name })
36+
return params
37+
}
38+
939
func buildCardQueryParameters(card *Card, params map[string]string) []QueryParameter {
1040
if len(params) == 0 {
1141
return nil

internal/client/types.go

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,7 @@ type TemplateTag struct {
115115
Type string `json:"type,omitempty"`
116116
WidgetType string `json:"widget-type,omitempty"`
117117
Required bool `json:"required,omitempty"`
118+
Default any `json:"default,omitempty"`
118119
}
119120

120121
// StructuredQuery represents an MBQL structured query.
@@ -140,6 +141,18 @@ type Card struct {
140141
VisualizationSettings map[string]any `json:"visualization_settings,omitempty"`
141142
}
142143

144+
// CardParameter describes a parameter a saved question accepts. It is derived
145+
// from a native query template tag and names the value passed to
146+
// `card run --param <name>=<value>`.
147+
type CardParameter struct {
148+
Name string `json:"name"`
149+
DisplayName string `json:"display_name,omitempty"`
150+
Type string `json:"type,omitempty"`
151+
WidgetType string `json:"widget_type,omitempty"`
152+
Required bool `json:"required"`
153+
Default any `json:"default,omitempty"`
154+
}
155+
143156
// Dashboard represents a Metabase dashboard.
144157
type Dashboard struct {
145158
ID int `json:"id"`

0 commit comments

Comments
 (0)