Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions cli/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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" +
Expand Down Expand Up @@ -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.
Expand Down
54 changes: 54 additions & 0 deletions cmd/dashboard/archive.go
Original file line number Diff line number Diff line change
@@ -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 <dashboard_id>",
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
}
}
100 changes: 100 additions & 0 deletions cmd/dashboard/create.go
Original file line number Diff line number Diff line change
@@ -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
}
}
24 changes: 24 additions & 0 deletions cmd/dashboard/dashboard.go
Original file line number Diff line number Diff line change
@@ -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
}
97 changes: 97 additions & 0 deletions cmd/dashboard/get.go
Original file line number Diff line number Diff line change
@@ -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
}
}
Loading
Loading