From fa6027b3fe3903006050fe1be06405d3c5ab4f3f Mon Sep 17 00:00:00 2001 From: Ivan Pusic <450140+ivpusic@users.noreply.github.com> Date: Sat, 11 Apr 2026 19:13:06 +0200 Subject: [PATCH 1/3] Dashboard management via cli --- cli/root.go | 3 + cmd/dashboard/archive.go | 54 ++++++++++++++ cmd/dashboard/create.go | 100 ++++++++++++++++++++++++++ cmd/dashboard/dashboard.go | 24 +++++++ cmd/dashboard/get.go | 97 +++++++++++++++++++++++++ cmd/dashboard/update.go | 142 +++++++++++++++++++++++++++++++++++++ go.mod | 2 + go.sum | 2 - 8 files changed, 422 insertions(+), 2 deletions(-) create mode 100644 cmd/dashboard/archive.go create mode 100644 cmd/dashboard/create.go create mode 100644 cmd/dashboard/dashboard.go create mode 100644 cmd/dashboard/get.go create mode 100644 cmd/dashboard/update.go diff --git a/cli/root.go b/cli/root.go index c74964f..dba8280 100644 --- a/cli/root.go +++ b/cli/root.go @@ -16,6 +16,7 @@ import ( "github.com/duneanalytics/cli/authconfig" "github.com/duneanalytics/cli/cmd/auth" duneconfig "github.com/duneanalytics/cli/cmd/config" + "github.com/duneanalytics/cli/cmd/dashboard" "github.com/duneanalytics/cli/cmd/dataset" "github.com/duneanalytics/cli/cmd/docs" "github.com/duneanalytics/cli/cmd/execution" @@ -41,6 +42,7 @@ var rootCmd = &cobra.Command{ " - Create, update, archive, and retrieve saved DuneSQL queries\n" + " - Execute saved queries or raw DuneSQL and display results\n" + " - Create and manage visualizations (charts, tables, counters) on query results\n" + + " - Create and manage dashboards with visualizations and text widgets\n" + " - Browse Dune documentation for DuneSQL syntax, API references, and guides\n" + " - Query real-time wallet and token data via the Sim API\n" + " - Monitor credit usage, storage consumption, and billing periods\n\n" + @@ -125,6 +127,7 @@ func init() { rootCmd.AddCommand(whoami.NewWhoAmICmd()) rootCmd.AddCommand(sim.NewSimCmd()) rootCmd.AddCommand(visualization.NewVisualizationCmd()) + rootCmd.AddCommand(dashboard.NewDashboardCmd()) } // Execute runs the root command via Fang. diff --git a/cmd/dashboard/archive.go b/cmd/dashboard/archive.go new file mode 100644 index 0000000..6b21a74 --- /dev/null +++ b/cmd/dashboard/archive.go @@ -0,0 +1,54 @@ +package dashboard + +import ( + "fmt" + "strconv" + + "github.com/duneanalytics/cli/cmdutil" + "github.com/duneanalytics/cli/output" + "github.com/spf13/cobra" +) + +func newArchiveCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "archive ", + Short: "Archive a dashboard by its ID", + Long: `Archive a dashboard by its ID. Archived dashboards are hidden from public view +but can be restored later. + +This also deletes any scheduled refresh jobs associated with the dashboard. + +Examples: + dune dashboard archive 12345 + dune dashboard archive 12345 -o json`, + Args: cobra.ExactArgs(1), + RunE: runArchive, + } + + output.AddFormatFlag(cmd, "text") + + return cmd +} + +func runArchive(cmd *cobra.Command, args []string) error { + dashboardID, err := strconv.Atoi(args[0]) + if err != nil { + return fmt.Errorf("invalid dashboard ID %q: must be an integer", args[0]) + } + + client := cmdutil.ClientFromCmd(cmd) + + resp, err := client.ArchiveDashboard(dashboardID) + if err != nil { + return err + } + + w := cmd.OutOrStdout() + switch output.FormatFromCmd(cmd) { + case output.FormatJSON: + return output.PrintJSON(w, resp) + default: + fmt.Fprintf(w, "Archived dashboard %d\n", dashboardID) + return nil + } +} diff --git a/cmd/dashboard/create.go b/cmd/dashboard/create.go new file mode 100644 index 0000000..785ad68 --- /dev/null +++ b/cmd/dashboard/create.go @@ -0,0 +1,100 @@ +package dashboard + +import ( + "encoding/json" + "fmt" + + "github.com/duneanalytics/cli/cmdutil" + "github.com/duneanalytics/cli/output" + "github.com/duneanalytics/duneapi-client-go/models" + "github.com/spf13/cobra" +) + +func newCreateCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "create", + Short: "Create a new dashboard", + Long: `Create a new dashboard with optional visualization and text widgets. + +Visualizations are placed in a grid layout controlled by --columns-per-row: + 1 = full-width charts, 2 = half-width (default), 3 = compact overview. +Text widgets always span the full width above visualizations. + +The --visualization-ids flag accepts a comma-separated list of visualization IDs +(from 'dune viz create' or 'dune viz list' output). + +The --text-widgets flag accepts a JSON array of text widget objects: + --text-widgets '[{"text":"# Dashboard Title"},{"text":"Description here"}]' + +Examples: + # Empty dashboard + dune dashboard create --name "My Dashboard" -o json + + # Dashboard with visualizations + dune dashboard create --name "DEX Overview" --visualization-ids 111,222,333 -o json + + # Dashboard with text header and visualizations + dune dashboard create --name "ETH Analysis" \ + --text-widgets '[{"text":"# Ethereum Analysis\nDaily metrics"}]' \ + --visualization-ids 111,222 --columns-per-row 1 -o json + + # Private dashboard + dune dashboard create --name "Internal Metrics" --private -o json`, + RunE: runCreate, + } + + cmd.Flags().String("name", "", "dashboard name (required)") + cmd.Flags().Bool("private", false, "make the dashboard private") + cmd.Flags().Int64Slice("visualization-ids", nil, "visualization IDs to add (comma-separated)") + cmd.Flags().String("text-widgets", "", `text widgets JSON array, e.g. '[{"text":"# Title"}]'`) + cmd.Flags().Int32("columns-per-row", 2, "visualizations per row: 1, 2, or 3") + _ = cmd.MarkFlagRequired("name") + output.AddFormatFlag(cmd, "text") + + return cmd +} + +func runCreate(cmd *cobra.Command, _ []string) error { + client := cmdutil.ClientFromCmd(cmd) + + name, _ := cmd.Flags().GetString("name") + private, _ := cmd.Flags().GetBool("private") + vizIDs, _ := cmd.Flags().GetInt64Slice("visualization-ids") + textWidgetsStr, _ := cmd.Flags().GetString("text-widgets") + columnsPerRow, _ := cmd.Flags().GetInt32("columns-per-row") + + req := models.CreateDashboardRequest{ + Name: name, + } + + if cmd.Flags().Changed("private") { + req.IsPrivate = &private + } + if len(vizIDs) > 0 { + req.VisualizationIDs = vizIDs + } + if textWidgetsStr != "" { + var textWidgets []models.TextWidgetInput + if err := json.Unmarshal([]byte(textWidgetsStr), &textWidgets); err != nil { + return fmt.Errorf("invalid --text-widgets JSON: %w", err) + } + req.TextWidgets = textWidgets + } + if cmd.Flags().Changed("columns-per-row") { + req.ColumnsPerRow = &columnsPerRow + } + + resp, err := client.CreateDashboard(req) + if err != nil { + return err + } + + w := cmd.OutOrStdout() + switch output.FormatFromCmd(cmd) { + case output.FormatJSON: + return output.PrintJSON(w, resp) + default: + fmt.Fprintf(w, "Created dashboard %d\n%s\n", resp.DashboardID, resp.DashboardURL) + return nil + } +} diff --git a/cmd/dashboard/dashboard.go b/cmd/dashboard/dashboard.go new file mode 100644 index 0000000..2687b2d --- /dev/null +++ b/cmd/dashboard/dashboard.go @@ -0,0 +1,24 @@ +package dashboard + +import "github.com/spf13/cobra" + +func NewDashboardCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "dashboard", + Aliases: []string{"dash"}, + Short: "Create and manage Dune dashboards", + Long: "Create and manage dashboards on Dune.\n\n" + + "Dashboards are collections of visualizations and text widgets that display\n" + + "blockchain and crypto data. Each dashboard has a unique URL and can be\n" + + "public or private.\n\n" + + "Visualizations are arranged in a 6-column grid. Use --columns-per-row to\n" + + "control layout: 1 for full-width, 2 for half-width (default), 3 for compact.", + } + + cmd.AddCommand(newCreateCmd()) + cmd.AddCommand(newGetCmd()) + cmd.AddCommand(newUpdateCmd()) + cmd.AddCommand(newArchiveCmd()) + + return cmd +} diff --git a/cmd/dashboard/get.go b/cmd/dashboard/get.go new file mode 100644 index 0000000..e6d2fd0 --- /dev/null +++ b/cmd/dashboard/get.go @@ -0,0 +1,97 @@ +package dashboard + +import ( + "fmt" + "strconv" + "strings" + + "github.com/duneanalytics/cli/cmdutil" + "github.com/duneanalytics/cli/output" + "github.com/duneanalytics/duneapi-client-go/models" + "github.com/spf13/cobra" +) + +func newGetCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "get [dashboard_id]", + Short: "Get details of a dashboard", + Long: `Retrieve the full state of a dashboard including metadata and widgets. + +Lookup by ID (positional argument): + dune dashboard get 12345 + +Lookup by owner and slug: + dune dashboard get --owner duneanalytics --slug ethereum-overview + +Use -o json to get the full response including all widget details. + +Examples: + dune dashboard get 12345 -o json + dune dashboard get --owner alice --slug my-dashboard -o json`, + Args: cobra.MaximumNArgs(1), + RunE: runGet, + } + + cmd.Flags().String("owner", "", "owner handle (username or team handle)") + cmd.Flags().String("slug", "", "dashboard URL slug") + output.AddFormatFlag(cmd, "text") + + return cmd +} + +func runGet(cmd *cobra.Command, args []string) error { + client := cmdutil.ClientFromCmd(cmd) + owner, _ := cmd.Flags().GetString("owner") + slug, _ := cmd.Flags().GetString("slug") + + hasID := len(args) > 0 + hasSlug := owner != "" && slug != "" + + if !hasID && !hasSlug { + return fmt.Errorf("provide either a dashboard ID or both --owner and --slug") + } + if hasID && hasSlug { + return fmt.Errorf("provide either a dashboard ID or --owner/--slug, not both") + } + + var resp *models.DashboardResponse + var err error + + if hasID { + dashboardID, parseErr := strconv.Atoi(args[0]) + if parseErr != nil { + return fmt.Errorf("invalid dashboard ID %q: must be an integer", args[0]) + } + resp, err = client.GetDashboard(dashboardID) + } else { + resp, err = client.GetDashboardBySlug(owner, slug) + } + if err != nil { + return err + } + + w := cmd.OutOrStdout() + switch output.FormatFromCmd(cmd) { + case output.FormatJSON: + return output.PrintJSON(w, resp) + default: + tags := "" + if len(resp.Tags) > 0 { + tags = strings.Join(resp.Tags, ", ") + } + output.PrintTable(w, + []string{"Field", "Value"}, + [][]string{ + {"ID", fmt.Sprintf("%d", resp.DashboardID)}, + {"Name", resp.Name}, + {"Slug", resp.Slug}, + {"Private", fmt.Sprintf("%t", resp.IsPrivate)}, + {"Tags", tags}, + {"URL", resp.DashboardURL}, + {"Visualizations", fmt.Sprintf("%d", len(resp.VisualizationWidgets))}, + {"Text Widgets", fmt.Sprintf("%d", len(resp.TextWidgets))}, + }, + ) + return nil + } +} diff --git a/cmd/dashboard/update.go b/cmd/dashboard/update.go new file mode 100644 index 0000000..777f8a0 --- /dev/null +++ b/cmd/dashboard/update.go @@ -0,0 +1,142 @@ +package dashboard + +import ( + "encoding/json" + "fmt" + "strconv" + "strings" + + "github.com/duneanalytics/cli/cmdutil" + "github.com/duneanalytics/cli/output" + "github.com/duneanalytics/duneapi-client-go/models" + "github.com/spf13/cobra" +) + +func newUpdateCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "update ", + Short: "Update an existing dashboard", + Long: `Update dashboard metadata or replace widgets. + +Only supply the flags you want to change; omitted fields are preserved. +At least one flag must be provided. + +IMPORTANT: Widget updates use all-or-nothing replacement. When you provide +--visualization-widgets or --text-widgets, ALL existing widgets are replaced. +To preserve widgets you want to keep, first fetch the current state with +'dune dashboard get -o json', modify what you need, and pass the complete +widget state back. + +The --visualization-widgets flag accepts a JSON array: + --visualization-widgets '[{"visualization_id":111},{"visualization_id":222}]' + +Each widget can include an optional position from the get output: + --visualization-widgets '[{"visualization_id":111,"position":{"row":0,"col":0,"size_x":3,"size_y":8}}]' + +Examples: + dune dashboard update 12345 --name "New Name" -o json + dune dashboard update 12345 --tags blockchain,defi,ethereum -o json + dune dashboard update 12345 --private -o json + dune dashboard update 12345 --visualization-widgets '[{"visualization_id":111}]' -o json`, + Args: cobra.ExactArgs(1), + RunE: runUpdate, + } + + cmd.Flags().String("name", "", "new dashboard name") + cmd.Flags().String("slug", "", "new URL slug") + cmd.Flags().Bool("private", false, "set dashboard privacy") + cmd.Flags().StringSlice("tags", nil, "replace all tags (comma-separated)") + cmd.Flags().String("visualization-widgets", "", "visualization widgets JSON array (replaces all)") + cmd.Flags().String("text-widgets", "", "text widgets JSON array (replaces all)") + cmd.Flags().String("param-widgets", "", "param widgets JSON array (replaces all, from get output)") + cmd.Flags().Int32("columns-per-row", 2, "visualizations per row: 1, 2, or 3") + output.AddFormatFlag(cmd, "text") + + return cmd +} + +func runUpdate(cmd *cobra.Command, args []string) error { + dashboardID, err := strconv.Atoi(args[0]) + if err != nil { + return fmt.Errorf("invalid dashboard ID %q: must be an integer", args[0]) + } + + changedFlags := []string{"name", "slug", "private", "tags", "visualization-widgets", "text-widgets", "param-widgets", "columns-per-row"} + hasChange := false + for _, f := range changedFlags { + if cmd.Flags().Changed(f) { + hasChange = true + break + } + } + if !hasChange { + return fmt.Errorf("at least one flag must be provided (--name, --slug, --private, --tags, --visualization-widgets, --text-widgets, or --columns-per-row)") + } + + client := cmdutil.ClientFromCmd(cmd) + + var req models.UpdateDashboardRequest + + if cmd.Flags().Changed("name") { + v, _ := cmd.Flags().GetString("name") + req.Name = &v + } + if cmd.Flags().Changed("slug") { + v, _ := cmd.Flags().GetString("slug") + req.Slug = &v + } + if cmd.Flags().Changed("private") { + v, _ := cmd.Flags().GetBool("private") + req.IsPrivate = &v + } + if cmd.Flags().Changed("tags") { + v, _ := cmd.Flags().GetStringSlice("tags") + // Normalize: trim whitespace from each tag + for i := range v { + v[i] = strings.TrimSpace(v[i]) + } + req.Tags = &v + } + if cmd.Flags().Changed("visualization-widgets") { + v, _ := cmd.Flags().GetString("visualization-widgets") + var widgets []models.VisualizationWidgetInput + if err := json.Unmarshal([]byte(v), &widgets); err != nil { + return fmt.Errorf("invalid --visualization-widgets JSON: %w", err) + } + req.VisualizationWidgets = &widgets + } + if cmd.Flags().Changed("text-widgets") { + v, _ := cmd.Flags().GetString("text-widgets") + var widgets []models.TextWidgetInput + if err := json.Unmarshal([]byte(v), &widgets); err != nil { + return fmt.Errorf("invalid --text-widgets JSON: %w", err) + } + req.TextWidgets = &widgets + } + if cmd.Flags().Changed("param-widgets") { + v, _ := cmd.Flags().GetString("param-widgets") + var widgets []models.ParamWidgetInput + if err := json.Unmarshal([]byte(v), &widgets); err != nil { + return fmt.Errorf("invalid --param-widgets JSON: %w", err) + } + req.ParamWidgets = &widgets + } + if cmd.Flags().Changed("columns-per-row") { + v, _ := cmd.Flags().GetInt32("columns-per-row") + req.ColumnsPerRow = &v + } + + resp, err := client.UpdateDashboard(dashboardID, req) + if err != nil { + return err + } + + w := cmd.OutOrStdout() + switch output.FormatFromCmd(cmd) { + case output.FormatJSON: + return output.PrintJSON(w, resp) + default: + fmt.Fprintf(w, "Updated dashboard %d\n", resp.DashboardID) + return nil + } +} diff --git a/go.mod b/go.mod index 65356d1..5ad192f 100644 --- a/go.mod +++ b/go.mod @@ -48,3 +48,5 @@ require ( golang.org/x/sys v0.41.0 // indirect golang.org/x/text v0.24.0 // indirect ) + +replace github.com/duneanalytics/duneapi-client-go => /Users/ivpusic/github/dune/nlq-migration/duneapi-client-go diff --git a/go.sum b/go.sum index 0756595..1bf9b1a 100644 --- a/go.sum +++ b/go.sum @@ -31,8 +31,6 @@ github.com/clipperhouse/uax29/v2 v2.3.0/go.mod h1:Wn1g7MK6OoeDT0vL+Q0SQLDz/KpfsV github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/duneanalytics/duneapi-client-go v0.4.7 h1:bsyMlKbTZnU3m7aXfNLjJNXuk0xKtucK5vEwEAHtELk= -github.com/duneanalytics/duneapi-client-go v0.4.7/go.mod h1:7pXXufWvR/Mh2KOehdyBaunJXmHI+pzjUmyQTQhJjdE= github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo= github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= From 329afd15cc13685ca999bd91e5f81694cce96be1 Mon Sep 17 00:00:00 2001 From: Ivan Pusic <450140+ivpusic@users.noreply.github.com> Date: Mon, 13 Apr 2026 11:53:41 +0200 Subject: [PATCH 2/3] use most recent dune go version --- go.mod | 4 +--- go.sum | 2 ++ 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 5ad192f..260db2a 100644 --- a/go.mod +++ b/go.mod @@ -5,7 +5,7 @@ go 1.25.6 require ( github.com/amplitude/analytics-go v1.3.0 github.com/charmbracelet/fang v0.4.4 - github.com/duneanalytics/duneapi-client-go v0.4.7 + github.com/duneanalytics/duneapi-client-go v0.4.8 github.com/modelcontextprotocol/go-sdk v1.4.0 github.com/spf13/cobra v1.10.2 github.com/stretchr/testify v1.11.1 @@ -48,5 +48,3 @@ require ( golang.org/x/sys v0.41.0 // indirect golang.org/x/text v0.24.0 // indirect ) - -replace github.com/duneanalytics/duneapi-client-go => /Users/ivpusic/github/dune/nlq-migration/duneapi-client-go diff --git a/go.sum b/go.sum index 1bf9b1a..031fa54 100644 --- a/go.sum +++ b/go.sum @@ -31,6 +31,8 @@ github.com/clipperhouse/uax29/v2 v2.3.0/go.mod h1:Wn1g7MK6OoeDT0vL+Q0SQLDz/KpfsV github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/duneanalytics/duneapi-client-go v0.4.8 h1:zKdX+ib5oZ3zZsiOcwhoa48xquffLOevrIUnDgI+jYg= +github.com/duneanalytics/duneapi-client-go v0.4.8/go.mod h1:7pXXufWvR/Mh2KOehdyBaunJXmHI+pzjUmyQTQhJjdE= github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo= github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= From 4af716988578baa0a12383f4d318fcf8c6e64734 Mon Sep 17 00:00:00 2001 From: Ivan Pusic <450140+ivpusic@users.noreply.github.com> Date: Mon, 13 Apr 2026 11:59:21 +0200 Subject: [PATCH 3/3] added missing flag --- cmd/dashboard/update.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/dashboard/update.go b/cmd/dashboard/update.go index 777f8a0..b9d75db 100644 --- a/cmd/dashboard/update.go +++ b/cmd/dashboard/update.go @@ -70,7 +70,7 @@ func runUpdate(cmd *cobra.Command, args []string) error { } } if !hasChange { - return fmt.Errorf("at least one flag must be provided (--name, --slug, --private, --tags, --visualization-widgets, --text-widgets, or --columns-per-row)") + return fmt.Errorf("at least one flag must be provided (--name, --slug, --private, --tags, --visualization-widgets, --text-widgets, --param-widgets, or --columns-per-row)") } client := cmdutil.ClientFromCmd(cmd)