Skip to content

Commit 68c17de

Browse files
committed
STAC-22568: Adding dashboard command
1 parent 3034723 commit 68c17de

File tree

9 files changed

+666
-0
lines changed

9 files changed

+666
-0
lines changed

cmd/dashboard.go

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
package cmd
2+
3+
import (
4+
"github.com/spf13/cobra"
5+
"github.com/stackvista/stackstate-cli/cmd/dashboard"
6+
"github.com/stackvista/stackstate-cli/internal/di"
7+
)
8+
9+
func DashboardCommand(cli *di.Deps) *cobra.Command {
10+
cmd := &cobra.Command{
11+
Use: "dashboard",
12+
Short: "Manage dashboards",
13+
Long: "Manage, test and develop dashboards.",
14+
}
15+
cmd.AddCommand(dashboard.DashboardListCommand(cli))
16+
cmd.AddCommand(dashboard.DashboardDescribeCommand(cli))
17+
cmd.AddCommand(dashboard.DashboardCloneCommand(cli))
18+
cmd.AddCommand(dashboard.DashboardDeleteCommand(cli))
19+
cmd.AddCommand(dashboard.DashboardApplyCommand(cli))
20+
cmd.AddCommand(dashboard.DashboardEditCommand(cli))
21+
22+
return cmd
23+
}

cmd/dashboard/common.go

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
package dashboard
2+
3+
import (
4+
"fmt"
5+
)
6+
7+
// ResolveDashboardIdOrUrn resolves ID or identifier to a string that can be used with the API
8+
// Returns the resolved identifier string or an error if neither is provided
9+
func ResolveDashboardIdOrUrn(id int64, identifier string) (string, error) {
10+
switch {
11+
case id != 0:
12+
return fmt.Sprintf("%d", id), nil
13+
case identifier != "":
14+
return identifier, nil
15+
default:
16+
return "", fmt.Errorf("either --id or --identifier must be provided")
17+
}
18+
}

cmd/dashboard/dashboard_apply.go

Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
package dashboard
2+
3+
import (
4+
"encoding/json"
5+
"fmt"
6+
"os"
7+
"path/filepath"
8+
"strings"
9+
10+
"github.com/spf13/cobra"
11+
"github.com/stackvista/stackstate-cli/generated/stackstate_api"
12+
"github.com/stackvista/stackstate-cli/internal/common"
13+
"github.com/stackvista/stackstate-cli/internal/di"
14+
)
15+
16+
type ApplyArgs struct {
17+
File string
18+
}
19+
20+
func DashboardApplyCommand(cli *di.Deps) *cobra.Command {
21+
args := &ApplyArgs{}
22+
cmd := &cobra.Command{
23+
Use: "apply",
24+
Short: "Create or edit a dashboard from JSON",
25+
Long: "Create or edit a dashboard from JSON file.",
26+
RunE: cli.CmdRunEWithApi(RunDashboardApplyCommand(args)),
27+
}
28+
29+
common.AddRequiredFileFlagVar(cmd, &args.File, "Path to a .json file with the dashboard definition")
30+
31+
return cmd
32+
}
33+
34+
func RunDashboardApplyCommand(args *ApplyArgs) di.CmdWithApiFn {
35+
return func(cmd *cobra.Command, cli *di.Deps, api *stackstate_api.APIClient, serverInfo *stackstate_api.ServerInfo) common.CLIError {
36+
fileBytes, err := os.ReadFile(args.File)
37+
if err != nil {
38+
return common.NewReadFileError(err, args.File)
39+
}
40+
41+
// Determine file type by extension
42+
ext := strings.ToLower(filepath.Ext(args.File))
43+
if ext != ".json" {
44+
return common.NewCLIArgParseError(fmt.Errorf("unsupported file type: %s. Only .json files are supported", ext))
45+
}
46+
47+
return applyJSONDashboard(cli, api, fileBytes)
48+
}
49+
}
50+
51+
// Apply JSON format dashboard using the Dashboard API directly
52+
func applyJSONDashboard(cli *di.Deps, api *stackstate_api.APIClient, fileBytes []byte) common.CLIError {
53+
// Parse the JSON to determine if it's a create or update operation
54+
var dashboardData map[string]interface{}
55+
if err := json.Unmarshal(fileBytes, &dashboardData); err != nil {
56+
return common.NewCLIArgParseError(fmt.Errorf("failed to parse JSON: %v", err))
57+
}
58+
59+
// Check if it has an ID field (indicates update operation)
60+
if idField, hasId := dashboardData["id"]; hasId {
61+
// Update existing dashboard
62+
dashboardId := fmt.Sprintf("%.0f", idField.(float64))
63+
return updateDashboard(cli, api, dashboardId, dashboardData)
64+
} else {
65+
// Create new dashboard
66+
return createDashboard(cli, api, fileBytes)
67+
}
68+
}
69+
70+
func createDashboard(cli *di.Deps, api *stackstate_api.APIClient, fileBytes []byte) common.CLIError {
71+
var writeSchema stackstate_api.DashboardWriteSchema
72+
if err := json.Unmarshal(fileBytes, &writeSchema); err != nil {
73+
return common.NewCLIArgParseError(fmt.Errorf("failed to parse JSON as DashboardWriteSchema: %v", err))
74+
}
75+
76+
// Validate required fields
77+
if writeSchema.Name == "" {
78+
return common.NewCLIArgParseError(fmt.Errorf("dashboard name is required"))
79+
}
80+
81+
// Create new dashboard
82+
dashboard, resp, err := api.DashboardsApi.CreateDashboard(cli.Context).DashboardWriteSchema(writeSchema).Execute()
83+
if err != nil {
84+
return common.NewResponseError(err, resp)
85+
}
86+
87+
if cli.IsJson() {
88+
cli.Printer.PrintJson(map[string]interface{}{
89+
"dashboard": dashboard,
90+
})
91+
} else {
92+
cli.Printer.Success(fmt.Sprintf("Dashboard created successfully! ID: %d, Name: %s", dashboard.GetId(), dashboard.GetName()))
93+
}
94+
95+
return nil
96+
}
97+
98+
func updateDashboard(cli *di.Deps, api *stackstate_api.APIClient, dashboardId string, dashboardData map[string]interface{}) common.CLIError {
99+
// Create patch schema from the JSON data
100+
patchSchema := stackstate_api.NewDashboardPatchSchema()
101+
102+
if name, ok := dashboardData["name"].(string); ok && name != "" {
103+
patchSchema.SetName(name)
104+
}
105+
if description, ok := dashboardData["description"].(string); ok {
106+
patchSchema.SetDescription(description)
107+
}
108+
if scopeStr, ok := dashboardData["scope"].(string); ok {
109+
if scope, err := stackstate_api.NewDashboardScopeFromValue(scopeStr); err == nil {
110+
patchSchema.SetScope(*scope)
111+
}
112+
}
113+
if dashboardContent, ok := dashboardData["dashboard"]; ok {
114+
// Convert dashboard content to PersesDashboard
115+
dashboardBytes, err := json.Marshal(dashboardContent)
116+
if err == nil {
117+
var persesDashboard stackstate_api.PersesDashboard
118+
if err := json.Unmarshal(dashboardBytes, &persesDashboard); err == nil {
119+
patchSchema.SetDashboard(persesDashboard)
120+
}
121+
}
122+
}
123+
124+
// Update existing dashboard
125+
dashboard, resp, err := api.DashboardsApi.PatchDashboard(cli.Context, dashboardId).DashboardPatchSchema(*patchSchema).Execute()
126+
if err != nil {
127+
return common.NewResponseError(err, resp)
128+
}
129+
130+
if cli.IsJson() {
131+
cli.Printer.PrintJson(map[string]interface{}{
132+
"dashboard": dashboard,
133+
})
134+
} else {
135+
cli.Printer.Success(fmt.Sprintf("Dashboard updated successfully! ID: %d, Name: %s", dashboard.GetId(), dashboard.GetName()))
136+
}
137+
138+
return nil
139+
}

cmd/dashboard/dashboard_clone.go

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
package dashboard
2+
3+
import (
4+
"fmt"
5+
6+
"github.com/spf13/cobra"
7+
"github.com/stackvista/stackstate-cli/generated/stackstate_api"
8+
stscobra "github.com/stackvista/stackstate-cli/internal/cobra"
9+
"github.com/stackvista/stackstate-cli/internal/common"
10+
"github.com/stackvista/stackstate-cli/internal/di"
11+
)
12+
13+
type CloneArgs struct {
14+
ID int64
15+
Identifier string
16+
NewName string
17+
Description string
18+
Scope string
19+
}
20+
21+
func DashboardCloneCommand(cli *di.Deps) *cobra.Command {
22+
args := &CloneArgs{}
23+
cmd := &cobra.Command{
24+
Use: "clone",
25+
Short: "Clone a dashboard",
26+
Long: "Clone a dashboard.",
27+
RunE: cli.CmdRunEWithApi(RunDashboardCloneCommand(args)),
28+
}
29+
30+
common.AddIDFlagVar(cmd, &args.ID, "ID of the dashboard")
31+
common.AddIdentifierFlagVar(cmd, &args.Identifier, "Identifier (URN) of the dashboard")
32+
common.AddRequiredNameFlagVar(cmd, &args.NewName, "Name for the new dashboard")
33+
cmd.Flags().StringVar(&args.Description, "description", "", "Description for the new dashboard")
34+
cmd.Flags().StringVar(&args.Scope, "scope", "", "Scope for the new dashboard (publicDashboard or privateDashboard)")
35+
stscobra.MarkMutexFlags(cmd, []string{common.IDFlag, common.IdentifierFlag}, "identifier", true)
36+
37+
return cmd
38+
}
39+
40+
func RunDashboardCloneCommand(args *CloneArgs) di.CmdWithApiFn {
41+
return func(
42+
cmd *cobra.Command,
43+
cli *di.Deps,
44+
api *stackstate_api.APIClient,
45+
serverInfo *stackstate_api.ServerInfo,
46+
) common.CLIError {
47+
dashboardIdOrUrn, err := ResolveDashboardIdOrUrn(args.ID, args.Identifier)
48+
if err != nil {
49+
return common.NewCLIArgParseError(err)
50+
}
51+
52+
// Create the clone schema with required name
53+
cloneSchema := stackstate_api.NewDashboardCloneSchema(args.NewName)
54+
55+
// Add optional fields
56+
if args.Description != "" {
57+
cloneSchema.SetDescription(args.Description)
58+
}
59+
60+
if args.Scope != "" {
61+
switch args.Scope {
62+
case "publicDashboard":
63+
scope := stackstate_api.DASHBOARDSCOPE_PUBLIC_DASHBOARD
64+
cloneSchema.SetScope(scope)
65+
case "privateDashboard":
66+
scope := stackstate_api.DASHBOARDSCOPE_PRIVATE_DASHBOARD
67+
cloneSchema.SetScope(scope)
68+
default:
69+
return common.NewCLIArgParseError(fmt.Errorf("invalid scope: %s. Must be 'publicDashboard' or 'privateDashboard'", args.Scope))
70+
}
71+
}
72+
73+
// Execute the clone request
74+
clonedDashboard, resp, err := api.DashboardsApi.CloneDashboard(cli.Context, dashboardIdOrUrn).DashboardCloneSchema(*cloneSchema).Execute()
75+
if err != nil {
76+
return common.NewResponseError(err, resp)
77+
}
78+
79+
if cli.IsJson() {
80+
cli.Printer.PrintJson(map[string]interface{}{
81+
"dashboard": clonedDashboard,
82+
})
83+
} else {
84+
cli.Printer.Success(fmt.Sprintf("Dashboard cloned successfully! New dashboard ID: %d, Name: %s", clonedDashboard.GetId(), clonedDashboard.GetName()))
85+
}
86+
87+
return nil
88+
}
89+
}

cmd/dashboard/dashboard_delete.go

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
package dashboard
2+
3+
import (
4+
"fmt"
5+
6+
"github.com/spf13/cobra"
7+
"github.com/stackvista/stackstate-cli/generated/stackstate_api"
8+
stscobra "github.com/stackvista/stackstate-cli/internal/cobra"
9+
"github.com/stackvista/stackstate-cli/internal/common"
10+
"github.com/stackvista/stackstate-cli/internal/di"
11+
)
12+
13+
type DeleteArgs struct {
14+
ID int64
15+
Identifier string
16+
}
17+
18+
func DashboardDeleteCommand(cli *di.Deps) *cobra.Command {
19+
args := &DeleteArgs{}
20+
cmd := &cobra.Command{
21+
Use: "delete",
22+
Short: "Delete a dashboard",
23+
Long: "Delete a dashboard by its ID or identifier. Only user-owned dashboards can be deleted.",
24+
RunE: cli.CmdRunEWithApi(RunDashboardDeleteCommand(args)),
25+
}
26+
27+
common.AddIDFlagVar(cmd, &args.ID, "ID of the dashboard")
28+
common.AddIdentifierFlagVar(cmd, &args.Identifier, "Identifier (URN) of the dashboard")
29+
stscobra.MarkMutexFlags(cmd, []string{common.IDFlag, common.IdentifierFlag}, "identifier", true)
30+
31+
return cmd
32+
}
33+
34+
func RunDashboardDeleteCommand(args *DeleteArgs) di.CmdWithApiFn {
35+
return func(
36+
cmd *cobra.Command,
37+
cli *di.Deps,
38+
api *stackstate_api.APIClient,
39+
serverInfo *stackstate_api.ServerInfo,
40+
) common.CLIError {
41+
dashboardIdOrUrn, err := ResolveDashboardIdOrUrn(args.ID, args.Identifier)
42+
if err != nil {
43+
return common.NewCLIArgParseError(err)
44+
}
45+
46+
// Execute the delete request
47+
resp, err := api.DashboardsApi.DeleteDashboard(cli.Context, dashboardIdOrUrn).Execute()
48+
if err != nil {
49+
return common.NewResponseError(err, resp)
50+
}
51+
52+
if cli.IsJson() {
53+
cli.Printer.PrintJson(map[string]interface{}{
54+
"deleted-dashboard-identifier": dashboardIdOrUrn,
55+
})
56+
} else {
57+
cli.Printer.Success(fmt.Sprintf("Dashboard deleted: %s", dashboardIdOrUrn))
58+
}
59+
return nil
60+
}
61+
}

0 commit comments

Comments
 (0)