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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -23,3 +23,4 @@ release-notes.md
release-notes.json

.localdev/
stackstate-cli
32 changes: 16 additions & 16 deletions cmd/dashboard/dashboard_apply.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package dashboard

import (
"encoding/json"
"fmt"
"os"
"path/filepath"
Expand All @@ -11,6 +10,7 @@ import (
"github.com/stackvista/stackstate-cli/generated/stackstate_api"
"github.com/stackvista/stackstate-cli/internal/common"
"github.com/stackvista/stackstate-cli/internal/di"
"sigs.k8s.io/kustomize/kyaml/yaml"
)

// ApplyArgs contains arguments for dashboard apply command
Expand All @@ -22,12 +22,12 @@ func DashboardApplyCommand(cli *di.Deps) *cobra.Command {
args := &ApplyArgs{}
cmd := &cobra.Command{
Use: "apply",
Short: "Create or edit a dashboard from JSON",
Long: "Create or edit a dashboard from JSON file.",
Short: "Create or edit a dashboard from YAML",
Long: "Create or edit a dashboard from YAML file.",
RunE: cli.CmdRunEWithApi(RunDashboardApplyCommand(args)),
}

common.AddRequiredFileFlagVar(cmd, &args.File, "Path to a .json file with the dashboard definition")
common.AddRequiredFileFlagVar(cmd, &args.File, "Path to a .yaml file with the dashboard definition")

return cmd
}
Expand All @@ -41,26 +41,26 @@ func RunDashboardApplyCommand(args *ApplyArgs) di.CmdWithApiFn {

// Determine file type by extension
ext := strings.ToLower(filepath.Ext(args.File))
if ext != ".json" {
return common.NewCLIArgParseError(fmt.Errorf("unsupported file type: %s. Only .json files are supported", ext))
if ext != ".yaml" {
return common.NewCLIArgParseError(fmt.Errorf("unsupported file type: %s. Only .yaml files are supported", ext))
}

return applyJSONDashboard(cli, api, fileBytes)
return applyYAMLDashboard(cli, api, fileBytes)
}
}

// applyJSONDashboard processes JSON dashboard file and determines create vs update operation
func applyJSONDashboard(cli *di.Deps, api *stackstate_api.APIClient, fileBytes []byte) common.CLIError {
// applyYAMLDashboard processes JSON dashboard file and determines create vs update operation
func applyYAMLDashboard(cli *di.Deps, api *stackstate_api.APIClient, fileBytes []byte) common.CLIError {
// Parse the JSON to determine if it's a create or update operation
var dashboardData map[string]interface{}
if err := json.Unmarshal(fileBytes, &dashboardData); err != nil {
return common.NewCLIArgParseError(fmt.Errorf("failed to parse JSON: %v", err))
if err := yaml.Unmarshal(fileBytes, &dashboardData); err != nil {
return common.NewCLIArgParseError(fmt.Errorf("failed to parse YAML: %v", err))
}

// Check if it has an ID field (indicates update operation)
if idField, hasId := dashboardData["id"]; hasId {
// Update existing dashboard
dashboardId := fmt.Sprintf("%.0f", idField.(float64))
dashboardId := fmt.Sprintf("%.0d", idField.(int))
return updateDashboard(cli, api, dashboardId, dashboardData)
} else {
// Create new dashboard
Expand All @@ -71,8 +71,8 @@ func applyJSONDashboard(cli *di.Deps, api *stackstate_api.APIClient, fileBytes [
// createDashboard creates a new dashboard from JSON schema
func createDashboard(cli *di.Deps, api *stackstate_api.APIClient, fileBytes []byte) common.CLIError {
var writeSchema stackstate_api.DashboardWriteSchema
if err := json.Unmarshal(fileBytes, &writeSchema); err != nil {
return common.NewCLIArgParseError(fmt.Errorf("failed to parse JSON as DashboardWriteSchema: %v", err))
if err := yaml.Unmarshal(fileBytes, &writeSchema); err != nil {
return common.NewCLIArgParseError(fmt.Errorf("failed to parse YAML as DashboardWriteSchema: %v", err))
}

// Validate required fields
Expand Down Expand Up @@ -115,10 +115,10 @@ func updateDashboard(cli *di.Deps, api *stackstate_api.APIClient, dashboardId st
}
if dashboardContent, ok := dashboardData["dashboard"]; ok {
// Convert dashboard content to PersesDashboard
dashboardBytes, err := json.Marshal(dashboardContent)
dashboardBytes, err := yaml.Marshal(dashboardContent)
if err == nil {
var persesDashboard stackstate_api.PersesDashboard
if err := json.Unmarshal(dashboardBytes, &persesDashboard); err == nil {
if err := yaml.Unmarshal(dashboardBytes, &persesDashboard); err == nil {
patchSchema.SetDashboard(persesDashboard)
}
}
Expand Down
98 changes: 44 additions & 54 deletions cmd/dashboard/dashboard_apply_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,32 +41,25 @@ func createTestApplyResult() sts.DashboardReadFullSchema {

func TestShouldApplyDashboardCreate(t *testing.T) {
// Create a temporary file with dashboard JSON
file, err := os.CreateTemp(os.TempDir(), "test_dashboard_*.json")
file, err := os.CreateTemp(os.TempDir(), "test_dashboard_*.yaml")
if err != nil {
panic(err)
}
defer os.Remove(file.Name())

dashboardJSON := `{
"name": "applied-dashboard",
"description": "Dashboard created via apply",
"scope": "publicDashboard",
"dashboard": {
"spec": {
"layouts": [
{
"kind": "Grid",
"spec": {
"items": []
}
}
],
"panels": {}
}
}
}`

_, err = file.WriteString(dashboardJSON)
dashboardYAML := `name: applied-dashboard
description: Dashboard created via apply
scope: publicDashboard
dashboard:
spec:
layouts:
- kind: Grid
spec:
items: []
panels: {}
`

_, err = file.WriteString(dashboardYAML)
assert.Nil(t, err)
file.Close()

Expand All @@ -86,20 +79,19 @@ func TestShouldApplyDashboardCreate(t *testing.T) {

func TestShouldApplyDashboardUpdate(t *testing.T) {
// Create a temporary file with dashboard update JSON (includes ID)
file, err := os.CreateTemp(os.TempDir(), "test_dashboard_*.json")
file, err := os.CreateTemp(os.TempDir(), "test_dashboard_*.yaml")
if err != nil {
panic(err)
}
defer os.Remove(file.Name())

updateJSON := `{
"id": 1234,
"name": "updated-dashboard",
"description": "Updated dashboard description",
"scope": "privateDashboard"
}`
updateYAML := `id: 1234
name: updated-dashboard
description: Updated dashboard description
scope: privateDashboard
`

_, err = file.WriteString(updateJSON)
_, err = file.WriteString(updateYAML)
assert.Nil(t, err)
file.Close()

Expand All @@ -125,25 +117,22 @@ func TestShouldApplyDashboardUpdate(t *testing.T) {
}

func TestShouldApplyDashboardWithJson(t *testing.T) {
file, err := os.CreateTemp(os.TempDir(), "test_dashboard_*.json")
file, err := os.CreateTemp(os.TempDir(), "test_dashboard_*.yaml")
if err != nil {
panic(err)
}
defer os.Remove(file.Name())

dashboardJSON := `{
"name": "json-output-dashboard",
"description": "Dashboard for JSON output test",
"scope": "publicDashboard",
"dashboard": {
"spec": {
"layouts": [],
"panels": {}
}
}
}`

_, err = file.WriteString(dashboardJSON)
dashboardYAML := `name: yaml-output-dashboard
description: Dashboard for JSON output test
scope: publicDashboard
dashboard:
spec:
layouts: []
panels: {}
`

_, err = file.WriteString(dashboardYAML)
assert.Nil(t, err)
file.Close()

Expand Down Expand Up @@ -174,28 +163,30 @@ func TestApplyDashboardInvalidFileType(t *testing.T) {
_, err = di.ExecuteCommandWithContext(&cli.Deps, cmd, "--file", file.Name())

assert.NotNil(t, err)
assert.Contains(t, err.Error(), "unsupported file type: .txt. Only .json files are supported")
assert.Contains(t, err.Error(), "unsupported file type: .txt. Only .yaml files are supported")
}

func TestApplyDashboardMissingFile(t *testing.T) {
cli, cmd := setDashboardApplyCmd(t)

_, err := di.ExecuteCommandWithContext(&cli.Deps, cmd, "--file", "/nonexistent/file.json")
_, err := di.ExecuteCommandWithContext(&cli.Deps, cmd, "--file", "/nonexistent/file.yaml")

assert.NotNil(t, err)
assert.Contains(t, err.Error(), "cannot read file")
}

func TestApplyDashboardInvalidJSON(t *testing.T) {
file, err := os.CreateTemp(os.TempDir(), "test_dashboard_*.json")
file, err := os.CreateTemp(os.TempDir(), "test_dashboard_*.yaml")
if err != nil {
panic(err)
}
defer os.Remove(file.Name())

invalidJSON := `{"name": "test", "invalid": json}`
invalidYAML := `name: test
invalid yaml
`

_, err = file.WriteString(invalidJSON)
_, err = file.WriteString(invalidYAML)
assert.Nil(t, err)
file.Close()

Expand All @@ -204,20 +195,19 @@ func TestApplyDashboardInvalidJSON(t *testing.T) {
_, err = di.ExecuteCommandWithContext(&cli.Deps, cmd, "--file", file.Name())

assert.NotNil(t, err)
assert.Contains(t, err.Error(), "failed to parse JSON")
assert.Contains(t, err.Error(), "failed to parse YAML")
}

func TestApplyDashboardMissingName(t *testing.T) {
file, err := os.CreateTemp(os.TempDir(), "test_dashboard_*.json")
file, err := os.CreateTemp(os.TempDir(), "test_dashboard_*.yaml")
if err != nil {
panic(err)
}
defer os.Remove(file.Name())

invalidDashboard := `{
"description": "Dashboard without name",
"scope": "publicDashboard"
}`
invalidDashboard := `description: Dashboard without name
scope: publicDashboard
`

_, err = file.WriteString(invalidDashboard)
assert.Nil(t, err)
Expand Down
8 changes: 4 additions & 4 deletions cmd/dashboard/dashboard_describe.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package dashboard

import (
"encoding/json"
"fmt"

"github.com/spf13/cobra"
Expand All @@ -10,6 +9,7 @@ import (
"github.com/stackvista/stackstate-cli/internal/common"
"github.com/stackvista/stackstate-cli/internal/di"
"github.com/stackvista/stackstate-cli/internal/util"
"sigs.k8s.io/kustomize/kyaml/yaml"
)

type DescribeArgs struct {
Expand All @@ -22,7 +22,7 @@ func DashboardDescribeCommand(cli *di.Deps) *cobra.Command {
args := &DescribeArgs{}
cmd := &cobra.Command{
Use: "describe",
Short: "Describe a dashboard in STY format",
Short: "Describe a dashboard in YAML format",
Long: "Describe a dashboard in StackState Templated YAML.",
RunE: cli.CmdRunEWithApi(RunDashboardDescribeCommand(args)),
}
Expand All @@ -48,11 +48,11 @@ func RunDashboardDescribeCommand(args *DescribeArgs) di.CmdWithApiFn {
return common.NewResponseError(err, resp)
}

jsonData, err := json.MarshalIndent(dashboard, "", " ")
yamlData, err := yaml.Marshal(dashboard)
if err != nil {
return common.NewExecutionError(fmt.Errorf("failed to marshal dashboard: %v", err))
}
data := string(jsonData)
data := string(yamlData)

if args.FilePath != "" {
if err := util.WriteFile(args.FilePath, []byte(data)); err != nil {
Expand Down
12 changes: 6 additions & 6 deletions cmd/dashboard/dashboard_describe_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,12 +48,12 @@ func TestDashboardDescribe(t *testing.T) {
_, err := di.ExecuteCommandWithContext(&cli.Deps, cmd, "--id", "1")
assert.Nil(t, err)

// Verify that the command printed the dashboard JSON
// Verify that the command printed the dashboard YAML
assert.Len(t, *cli.MockPrinter.PrintLnCalls, 1)
printedOutput := (*cli.MockPrinter.PrintLnCalls)[0]
assert.Contains(t, printedOutput, `"id": 1`)
assert.Contains(t, printedOutput, `"name": "aDashboard"`)
assert.Contains(t, printedOutput, `"identifier": "urn:custom:dashboard:aDashboard"`)
assert.Contains(t, printedOutput, `id: 1`)
assert.Contains(t, printedOutput, `name: aDashboard`)
assert.Contains(t, printedOutput, `identifier: urn:custom:dashboard:aDashboard`)
}

func TestDashboardDescribeWithIdentifier(t *testing.T) {
Expand Down Expand Up @@ -106,8 +106,8 @@ func TestDashboardDescribeToFile(t *testing.T) {
// Verify file contents
body, err := os.ReadFile(file.Name())
assert.Nil(t, err)
assert.Contains(t, string(body), `"id": 1`)
assert.Contains(t, string(body), `"name": "aDashboard"`)
assert.Contains(t, string(body), `id: 1`)
assert.Contains(t, string(body), `name: aDashboard`)
}

func TestDashboardDescribeToFileJson(t *testing.T) {
Expand Down
17 changes: 9 additions & 8 deletions cmd/dashboard/dashboard_edit.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
stscobra "github.com/stackvista/stackstate-cli/internal/cobra"
"github.com/stackvista/stackstate-cli/internal/common"
"github.com/stackvista/stackstate-cli/internal/di"
"sigs.k8s.io/kustomize/kyaml/yaml"
)

const LongDescription = `Edit a dashboard.
Expand Down Expand Up @@ -60,19 +61,19 @@ func RunDashboardEditCommand(args *EditArgs) di.CmdWithApiFn {
}

// Convert dashboard to pretty JSON for editing
originalJSON, err := json.MarshalIndent(dashboard, "", " ")
originalYAML, err := yaml.Marshal(dashboard)
if err != nil {
return common.NewExecutionError(fmt.Errorf("failed to marshal dashboard to JSON: %v", err))
return common.NewExecutionError(fmt.Errorf("failed to marshal dashboard to YAML: %v", err))
}

// Open editor with the dashboard JSON
editedContent, err := cli.Editor.Edit("dashboard-", ".json", strings.NewReader(string(originalJSON)))
editedContent, err := cli.Editor.Edit("dashboard-", ".yaml", strings.NewReader(string(originalYAML)))
if err != nil {
return common.NewExecutionError(fmt.Errorf("failed to open editor: %v", err))
}

// Check if any changes were made
if strings.Compare(string(originalJSON), string(editedContent)) == 0 {
if strings.Compare(string(originalYAML), string(editedContent)) == 0 {
if cli.IsJson() {
cli.Printer.PrintJson(map[string]interface{}{"message": "No changes made"})
} else {
Expand All @@ -81,13 +82,13 @@ func RunDashboardEditCommand(args *EditArgs) di.CmdWithApiFn {
return nil
}

// Parse the edited JSON
// Parse the edited YAML
var editedDashboard map[string]interface{}
if err := json.Unmarshal(editedContent, &editedDashboard); err != nil {
return common.NewExecutionError(fmt.Errorf("failed to parse edited JSON: %v", err))
if err := yaml.Unmarshal(editedContent, &editedDashboard); err != nil {
return common.NewExecutionError(fmt.Errorf("failed to parse edited YAML: %v", err))
}

// Create patch schema from the edited JSON
// Create patch schema from the edited YAML
patchSchema := stackstate_api.NewDashboardPatchSchema()

if name, ok := editedDashboard["name"].(string); ok && name != "" {
Expand Down
Loading