From 471c271f30be102a4dfab6604494ca3c479959d2 Mon Sep 17 00:00:00 2001 From: "Pedro F. Leao" Date: Wed, 20 May 2026 19:34:03 -0300 Subject: [PATCH 1/3] feat: add canvas dashboard CLI commands Signed-off-by: Pedro F. Leao --- pkg/agents/anthropic/resources.go | 10 ++ pkg/agents/anthropic/resources_test.go | 5 + pkg/cli/commands/canvases/dashboard.go | 130 ++++++++++++++++++ pkg/cli/commands/canvases/dashboard_test.go | 113 +++++++++++++++ pkg/cli/commands/canvases/models/dashboard.go | 78 +++++++++++ .../canvases/models/dashboard_test.go | 77 +++++++++++ pkg/cli/commands/canvases/root.go | 42 ++++++ 7 files changed, 455 insertions(+) create mode 100644 pkg/cli/commands/canvases/dashboard.go create mode 100644 pkg/cli/commands/canvases/dashboard_test.go create mode 100644 pkg/cli/commands/canvases/models/dashboard.go create mode 100644 pkg/cli/commands/canvases/models/dashboard_test.go diff --git a/pkg/agents/anthropic/resources.go b/pkg/agents/anthropic/resources.go index 955a9f38fd..db1457b1f9 100644 --- a/pkg/agents/anthropic/resources.go +++ b/pkg/agents/anthropic/resources.go @@ -115,6 +115,16 @@ func defaultResourceSourcesForSkillsBaseURL(skillsBaseURL string) ([]resourceSou "canvas-yaml-spec.md", ), }, + { + MountPath: "ref/skills/superplane-dashboard-and-widgets/SKILL.md", + SourceKey: filepath.ToSlash(filepath.Join("skills", "superplane-dashboard-and-widgets", "SKILL.md")), + SourceURL: skillsRawURL( + skillsBaseURL, + "skills", + "superplane-dashboard-and-widgets", + "SKILL.md", + ), + }, { MountPath: "ref/skills/superplane-monitor/SKILL.md", SourceKey: filepath.ToSlash(filepath.Join("skills", "superplane-monitor", "SKILL.md")), diff --git a/pkg/agents/anthropic/resources_test.go b/pkg/agents/anthropic/resources_test.go index 97becd4300..b09a5fde3b 100644 --- a/pkg/agents/anthropic/resources_test.go +++ b/pkg/agents/anthropic/resources_test.go @@ -41,6 +41,11 @@ func TestDefaultResourceSourcesForSkillsBaseURL(t *testing.T) { "https://example.test/root/skills/superplane-cli/references/canvas-yaml-spec.md", byMountPath["ref/skills/superplane-cli/references/canvas-yaml-spec.md"].SourceURL, ) + assert.Equal( + t, + "https://example.test/root/skills/superplane-dashboard-and-widgets/SKILL.md", + byMountPath["ref/skills/superplane-dashboard-and-widgets/SKILL.md"].SourceURL, + ) assert.Equal( t, "https://example.test/root/skills/superplane-monitor/SKILL.md", diff --git a/pkg/cli/commands/canvases/dashboard.go b/pkg/cli/commands/canvases/dashboard.go new file mode 100644 index 0000000000..c71c69735f --- /dev/null +++ b/pkg/cli/commands/canvases/dashboard.go @@ -0,0 +1,130 @@ +package canvases + +import ( + "fmt" + "io" + "os" + + "github.com/superplanehq/superplane/pkg/cli/commands/canvases/models" + "github.com/superplanehq/superplane/pkg/cli/core" + "github.com/superplanehq/superplane/pkg/openapi_client" +) + +type dashboardGetCommand struct{} + +type dashboardUpdateCommand struct { + file *string +} + +func (c *dashboardGetCommand) Execute(ctx core.CommandContext) error { + canvasID, err := findCanvasID(ctx, ctx.API, ctx.Args[0]) + if err != nil { + return err + } + + canvas, err := describeCanvas(ctx, canvasID) + if err != nil { + return err + } + + response, _, err := ctx.API.CanvasAPI.CanvasesGetCanvasDashboard(ctx.Context, canvasID).Execute() + if err != nil { + return err + } + if response == nil || response.Dashboard == nil { + return fmt.Errorf("dashboard for canvas %q not found", canvasID) + } + + canvasName := "" + if canvas.Metadata != nil { + canvasName = canvas.Metadata.GetName() + } + + resource := models.DashboardResourceFromDashboard(*response.Dashboard, canvasName) + if !ctx.Renderer.IsText() { + return ctx.Renderer.Render(resource) + } + + return ctx.Renderer.RenderText(func(stdout io.Writer) error { + _, _ = fmt.Fprintf(stdout, "Canvas ID: %s\n", resource.Metadata.CanvasID) + if resource.Metadata.Name != "" { + _, _ = fmt.Fprintf(stdout, "Canvas Name: %s\n", resource.Metadata.Name) + } + _, _ = fmt.Fprintf(stdout, "Panels: %d\n", len(resource.Spec.Panels)) + _, err := fmt.Fprintf(stdout, "Layout Items: %d\n", len(resource.Spec.Layout)) + return err + }) +} + +func (c *dashboardUpdateCommand) Execute(ctx core.CommandContext) error { + canvasID, err := findCanvasID(ctx, ctx.API, ctx.Args[0]) + if err != nil { + return err + } + + filePath := "" + if c.file != nil { + filePath = *c.file + } + resource, err := parseDashboardResourceFromFile(filePath) + if err != nil { + return err + } + + body := models.UpdateDashboardRequestFromDashboard(*resource) + response, _, err := ctx.API.CanvasAPI. + CanvasesUpdateCanvasDashboard(ctx.Context, canvasID). + Body(body). + Execute() + if err != nil { + return err + } + if response == nil || response.Dashboard == nil { + return fmt.Errorf("failed to update dashboard: the server returned an empty response") + } + + rendered := models.DashboardResourceFromDashboard(*response.Dashboard, resource.Metadata.Name) + if !ctx.Renderer.IsText() { + return ctx.Renderer.Render(rendered) + } + + return ctx.Renderer.RenderText(func(stdout io.Writer) error { + _, _ = fmt.Fprintf(stdout, "Dashboard updated for canvas %s\n", rendered.Metadata.CanvasID) + _, _ = fmt.Fprintf(stdout, "Panels: %d\n", len(rendered.Spec.Panels)) + _, err := fmt.Fprintf(stdout, "Layout Items: %d\n", len(rendered.Spec.Layout)) + return err + }) +} + +func parseDashboardResourceFromFile(filePath string) (*models.Dashboard, error) { + if filePath == "" { + return nil, fmt.Errorf("dashboard file is required") + } + + data, err := os.ReadFile(filePath) + if err != nil { + return nil, fmt.Errorf("failed to read dashboard file: %w", err) + } + + _, kind, err := core.ParseYamlResourceHeaders(data) + if err != nil { + return nil, err + } + if kind != models.DashboardKind { + return nil, fmt.Errorf("unsupported resource kind %q for dashboard update", kind) + } + + return models.ParseDashboard(data) +} + +func describeCanvas(ctx core.CommandContext, canvasID string) (openapi_client.CanvasesCanvas, error) { + response, _, err := ctx.API.CanvasAPI.CanvasesDescribeCanvas(ctx.Context, canvasID).Execute() + if err != nil { + return openapi_client.CanvasesCanvas{}, err + } + if response == nil || response.Canvas == nil { + return openapi_client.CanvasesCanvas{}, fmt.Errorf("canvas %q not found", canvasID) + } + + return *response.Canvas, nil +} diff --git a/pkg/cli/commands/canvases/dashboard_test.go b/pkg/cli/commands/canvases/dashboard_test.go new file mode 100644 index 0000000000..541e821b2c --- /dev/null +++ b/pkg/cli/commands/canvases/dashboard_test.go @@ -0,0 +1,113 @@ +package canvases + +import ( + "encoding/json" + "net/http" + "os" + "path/filepath" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestDashboardGetCommandReturnsYAML(t *testing.T) { + canvasID := "4e9ae08d-0363-40d2-ba2c-5f6389a418d8" + server := newAPITestServer( + t, + requestExpectation{ + method: http.MethodGet, + path: "/api/v1/canvases/" + canvasID, + handle: func(t *testing.T, w http.ResponseWriter, _ *http.Request) { + w.Header().Set("Content-Type", "application/json") + _, _ = w.Write([]byte(`{"canvas":{"metadata":{"id":"` + canvasID + `","name":"deploy"}}}`)) + }, + }, + requestExpectation{ + method: http.MethodGet, + path: "/api/v1/canvases/" + canvasID + "/dashboard", + handle: func(t *testing.T, w http.ResponseWriter, _ *http.Request) { + w.Header().Set("Content-Type", "application/json") + _, _ = w.Write([]byte(`{"dashboard":{"canvasId":"` + canvasID + `","panels":[{"id":"p1","type":"markdown","content":{"body":"ok"}}],"layout":[{"i":"p1","x":0,"y":0,"w":4,"h":3}]}}`)) + }, + }, + ) + + ctx, stdout := newCreateCommandContextForTest(t, server.server, "yaml") + ctx.Args = []string{canvasID} + + err := (&dashboardGetCommand{}).Execute(ctx) + require.NoError(t, err) + require.Contains(t, stdout.String(), "kind: Dashboard") + require.Contains(t, stdout.String(), "canvasId: "+canvasID) + require.Contains(t, stdout.String(), "name: deploy") + require.Contains(t, stdout.String(), "type: markdown") +} + +func TestDashboardUpdateCommandSendsPanelsAndLayout(t *testing.T) { + canvasID := "4e9ae08d-0363-40d2-ba2c-5f6389a418d8" + server := newAPITestServer( + t, + requestExpectation{ + method: http.MethodPut, + path: "/api/v1/canvases/" + canvasID + "/dashboard", + handle: func(t *testing.T, w http.ResponseWriter, r *http.Request) { + var body map[string]any + require.NoError(t, json.NewDecoder(r.Body).Decode(&body)) + require.Len(t, body["panels"], 1) + require.Len(t, body["layout"], 1) + + w.Header().Set("Content-Type", "application/json") + _, _ = w.Write([]byte(`{"dashboard":{"canvasId":"` + canvasID + `","panels":[{"id":"p1","type":"markdown","content":{"title":"Deploy"}}],"layout":[{"i":"p1","x":0,"y":0,"w":4,"h":3}]}}`)) + }, + }, + ) + + filePath := writeTestDashboardFile(t) + ctx, stdout := newCreateCommandContextForTest(t, server.server, "text") + ctx.Args = []string{canvasID} + + err := (&dashboardUpdateCommand{file: &filePath}).Execute(ctx) + require.NoError(t, err) + require.Contains(t, stdout.String(), "Dashboard updated for canvas "+canvasID) + require.Contains(t, stdout.String(), "Panels: 1") +} + +func TestDashboardUpdateCommandRequiresDashboardKind(t *testing.T) { + dir := t.TempDir() + filePath := filepath.Join(dir, "canvas.yaml") + require.NoError(t, os.WriteFile(filePath, []byte("apiVersion: v1\nkind: Canvas\nmetadata: {}\n"), 0o644)) + + ctx, _ := newCreateCommandContextForTest(t, nil, "text") + ctx.Args = []string{"4e9ae08d-0363-40d2-ba2c-5f6389a418d8"} + + err := (&dashboardUpdateCommand{file: &filePath}).Execute(ctx) + require.Error(t, err) + require.Contains(t, err.Error(), `unsupported resource kind "Canvas"`) +} + +func writeTestDashboardFile(t *testing.T) string { + t.Helper() + + dir := t.TempDir() + filePath := filepath.Join(dir, "dashboard.yaml") + content := []byte(` +apiVersion: v1 +kind: Dashboard +metadata: + name: Deploy +spec: + panels: + - id: p1 + type: markdown + content: + title: Deploy + layout: + - i: p1 + x: 0 + y: 0 + w: 4 + h: 3 +`) + require.NoError(t, os.WriteFile(filePath, content, 0o644)) + return filePath +} diff --git a/pkg/cli/commands/canvases/models/dashboard.go b/pkg/cli/commands/canvases/models/dashboard.go new file mode 100644 index 0000000000..b375e0d1e4 --- /dev/null +++ b/pkg/cli/commands/canvases/models/dashboard.go @@ -0,0 +1,78 @@ +package models + +import ( + "fmt" + + "github.com/superplanehq/superplane/pkg/cli/core" + "github.com/superplanehq/superplane/pkg/openapi_client" +) + +const ( + DashboardKind = "Dashboard" + DashboardAPIVersion = "v1" +) + +type DashboardMetadata struct { + CanvasID string `json:"canvasId,omitempty" yaml:"canvasId,omitempty"` + Name string `json:"name,omitempty" yaml:"name,omitempty"` +} + +type DashboardSpec struct { + Panels []openapi_client.CanvasesDashboardPanel `json:"panels" yaml:"panels"` + Layout []openapi_client.CanvasesDashboardLayoutItem `json:"layout" yaml:"layout"` +} + +type Dashboard struct { + APIVersion string `json:"apiVersion" yaml:"apiVersion"` + Kind string `json:"kind" yaml:"kind"` + Metadata DashboardMetadata `json:"metadata" yaml:"metadata"` + Spec DashboardSpec `json:"spec" yaml:"spec"` +} + +func ParseDashboard(raw []byte) (*Dashboard, error) { + var resource Dashboard + if err := core.NewDecoder(raw).DecodeYAML(&resource); err != nil { + return nil, fmt.Errorf("failed to parse dashboard yaml: %w", err) + } + + if resource.APIVersion == "" { + return nil, fmt.Errorf("dashboard apiVersion is required") + } + if resource.APIVersion != DashboardAPIVersion { + return nil, fmt.Errorf("unsupported dashboard apiVersion %q", resource.APIVersion) + } + if resource.Kind != DashboardKind { + return nil, fmt.Errorf("unsupported resource kind %q", resource.Kind) + } + + if resource.Spec.Panels == nil { + resource.Spec.Panels = []openapi_client.CanvasesDashboardPanel{} + } + if resource.Spec.Layout == nil { + resource.Spec.Layout = []openapi_client.CanvasesDashboardLayoutItem{} + } + + return &resource, nil +} + +func DashboardResourceFromDashboard(dashboard openapi_client.CanvasesCanvasDashboard, canvasName string) Dashboard { + return Dashboard{ + APIVersion: DashboardAPIVersion, + Kind: DashboardKind, + Metadata: DashboardMetadata{ + CanvasID: dashboard.GetCanvasId(), + Name: canvasName, + }, + Spec: DashboardSpec{ + Panels: dashboard.GetPanels(), + Layout: dashboard.GetLayout(), + }, + } +} + +func UpdateDashboardRequestFromDashboard(resource Dashboard) openapi_client.CanvasesUpdateCanvasDashboardBody { + body := openapi_client.CanvasesUpdateCanvasDashboardBody{} + body.SetPanels(resource.Spec.Panels) + body.SetLayout(resource.Spec.Layout) + return body +} diff --git a/pkg/cli/commands/canvases/models/dashboard_test.go b/pkg/cli/commands/canvases/models/dashboard_test.go new file mode 100644 index 0000000000..a7d77e9448 --- /dev/null +++ b/pkg/cli/commands/canvases/models/dashboard_test.go @@ -0,0 +1,77 @@ +package models + +import ( + "testing" + + "github.com/stretchr/testify/require" + "github.com/superplanehq/superplane/pkg/openapi_client" +) + +func TestParseDashboard(t *testing.T) { + raw := []byte(` +apiVersion: v1 +kind: Dashboard +metadata: + name: Deploy health +spec: + panels: + - id: status + type: markdown + content: + title: Status + body: Ready + layout: + - i: status + x: 0 + y: 0 + w: 4 + h: 3 +`) + + resource, err := ParseDashboard(raw) + require.NoError(t, err) + require.Equal(t, DashboardKind, resource.Kind) + require.Equal(t, "Deploy health", resource.Metadata.Name) + require.Len(t, resource.Spec.Panels, 1) + require.Equal(t, "status", resource.Spec.Panels[0].GetId()) + require.Equal(t, "Status", resource.Spec.Panels[0].GetContent()["title"]) + require.Len(t, resource.Spec.Layout, 1) + require.Equal(t, int32(4), resource.Spec.Layout[0].GetW()) +} + +func TestParseDashboardRejectsUnknownFields(t *testing.T) { + raw := []byte(` +apiVersion: v1 +kind: Dashboard +spec: + panels: [] + layout: [] +unexpected: true +`) + + _, err := ParseDashboard(raw) + require.Error(t, err) + require.Contains(t, err.Error(), `unknown field "unexpected"`) +} + +func TestDashboardConversions(t *testing.T) { + dashboard := openapi_client.CanvasesCanvasDashboard{} + dashboard.SetCanvasId("canvas-123") + dashboard.SetPanels([]openapi_client.CanvasesDashboardPanel{ + {Id: openapi_client.PtrString("p1"), Type: openapi_client.PtrString("markdown")}, + }) + dashboard.SetLayout([]openapi_client.CanvasesDashboardLayoutItem{ + {I: openapi_client.PtrString("p1"), W: openapi_client.PtrInt32(4), H: openapi_client.PtrInt32(2)}, + }) + + resource := DashboardResourceFromDashboard(dashboard, "My Canvas") + require.Equal(t, DashboardAPIVersion, resource.APIVersion) + require.Equal(t, DashboardKind, resource.Kind) + require.Equal(t, "canvas-123", resource.Metadata.CanvasID) + require.Equal(t, "My Canvas", resource.Metadata.Name) + require.Len(t, resource.Spec.Panels, 1) + + body := UpdateDashboardRequestFromDashboard(resource) + require.Len(t, body.GetPanels(), 1) + require.Len(t, body.GetLayout(), 1) +} diff --git a/pkg/cli/commands/canvases/root.go b/pkg/cli/commands/canvases/root.go index 54932cbdfa..34990ef8ba 100644 --- a/pkg/cli/commands/canvases/root.go +++ b/pkg/cli/commands/canvases/root.go @@ -93,6 +93,47 @@ AI agents: for canonical canvas YAML shapes and wiring rules, install skills: autoLayoutNodes: &updateAutoLayoutNodes, }, options) + dashboardCmd := &cobra.Command{ + Use: "dashboard", + Short: "Manage canvas dashboards", + Long: `Manage the per-canvas dashboard attached to a canvas. + +Dashboard files use this shape: + +apiVersion: v1 +kind: Dashboard +metadata: + name: Optional display name +spec: + panels: [] + layout: [] + +AI agents: for dashboard YAML examples and workflow, install skill: +- ` + core.SkillsInstallCommand("superplane-dashboard-and-widgets"), + } + + dashboardGetCmd := &cobra.Command{ + Use: "get ", + Short: "Get a canvas dashboard", + Args: cobra.ExactArgs(1), + } + core.Bind(dashboardGetCmd, &dashboardGetCommand{}, options) + + var dashboardUpdateFile string + dashboardUpdateCmd := &cobra.Command{ + Use: "update ", + Aliases: []string{"create", "apply"}, + Short: "Create or replace a canvas dashboard from a YAML file", + Long: "Creates or replaces the dashboard attached to a canvas using --file. Dashboard import is replace-all.", + Args: cobra.ExactArgs(1), + } + dashboardUpdateCmd.Flags().StringVarP(&dashboardUpdateFile, "file", "f", "", "dashboard YAML file") + _ = dashboardUpdateCmd.MarkFlagRequired("file") + core.Bind(dashboardUpdateCmd, &dashboardUpdateCommand{file: &dashboardUpdateFile}, options) + + dashboardCmd.AddCommand(dashboardGetCmd) + dashboardCmd.AddCommand(dashboardUpdateCmd) + var changeRequestsListStatusFilter string var changeRequestsListOnlyMine bool var changeRequestsListQuery string @@ -253,6 +294,7 @@ AI agents: for canonical canvas YAML shapes and wiring rules, install skills: root.AddCommand(initCmd) root.AddCommand(createCmd) root.AddCommand(updateCmd) + root.AddCommand(dashboardCmd) root.AddCommand(deleteCmd) root.AddCommand(changeRequestsCmd) From 39cfc0b945e4da1c28e4de834a1db40f54e5a67b Mon Sep 17 00:00:00 2001 From: "Pedro F. Leao" Date: Wed, 20 May 2026 20:00:36 -0300 Subject: [PATCH 2/3] test: cover dashboard resource parsing Signed-off-by: Pedro F. Leao --- .../canvases/models/dashboard_test.go | 64 +++++++++++++++++++ 1 file changed, 64 insertions(+) diff --git a/pkg/cli/commands/canvases/models/dashboard_test.go b/pkg/cli/commands/canvases/models/dashboard_test.go index a7d77e9448..34c5bf968c 100644 --- a/pkg/cli/commands/canvases/models/dashboard_test.go +++ b/pkg/cli/commands/canvases/models/dashboard_test.go @@ -54,6 +54,70 @@ unexpected: true require.Contains(t, err.Error(), `unknown field "unexpected"`) } +func TestParseDashboardValidatesResourceIdentity(t *testing.T) { + testCases := []struct { + name string + raw string + errorContains string + }{ + { + name: "missing apiVersion", + raw: ` +kind: Dashboard +spec: + panels: [] + layout: [] +`, + errorContains: "dashboard apiVersion is required", + }, + { + name: "unsupported apiVersion", + raw: ` +apiVersion: v2 +kind: Dashboard +spec: + panels: [] + layout: [] +`, + errorContains: `unsupported dashboard apiVersion "v2"`, + }, + { + name: "unsupported kind", + raw: ` +apiVersion: v1 +kind: Canvas +spec: + panels: [] + layout: [] +`, + errorContains: `unsupported resource kind "Canvas"`, + }, + } + + for _, testCase := range testCases { + t.Run(testCase.name, func(t *testing.T) { + _, err := ParseDashboard([]byte(testCase.raw)) + require.Error(t, err) + require.Contains(t, err.Error(), testCase.errorContains) + }) + } +} + +func TestParseDashboardDefaultsEmptyCollections(t *testing.T) { + raw := []byte(` +apiVersion: v1 +kind: Dashboard +spec: {} +`) + + resource, err := ParseDashboard(raw) + require.NoError(t, err) + require.NotNil(t, resource.Spec.Panels) + require.Empty(t, resource.Spec.Panels) + require.NotNil(t, resource.Spec.Layout) + require.Empty(t, resource.Spec.Layout) +} + func TestDashboardConversions(t *testing.T) { dashboard := openapi_client.CanvasesCanvasDashboard{} dashboard.SetCanvasId("canvas-123") From c6753de04e2271a5018d7eda2545b6a5ff6063c3 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Thu, 21 May 2026 23:58:17 +0000 Subject: [PATCH 3/3] fix: normalize dashboard panels/layout in exports MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Pedro Leão --- pkg/cli/commands/canvases/models/dashboard.go | 14 +++++++++-- .../canvases/models/dashboard_test.go | 23 +++++++++++++++++++ 2 files changed, 35 insertions(+), 2 deletions(-) diff --git a/pkg/cli/commands/canvases/models/dashboard.go b/pkg/cli/commands/canvases/models/dashboard.go index b375e0d1e4..8fd1d512b9 100644 --- a/pkg/cli/commands/canvases/models/dashboard.go +++ b/pkg/cli/commands/canvases/models/dashboard.go @@ -56,6 +56,16 @@ func ParseDashboard(raw []byte) (*Dashboard, error) { } func DashboardResourceFromDashboard(dashboard openapi_client.CanvasesCanvasDashboard, canvasName string) Dashboard { + panels := dashboard.GetPanels() + if panels == nil { + panels = []openapi_client.CanvasesDashboardPanel{} + } + + layout := dashboard.GetLayout() + if layout == nil { + layout = []openapi_client.CanvasesDashboardLayoutItem{} + } + return Dashboard{ APIVersion: DashboardAPIVersion, Kind: DashboardKind, @@ -64,8 +74,8 @@ func DashboardResourceFromDashboard(dashboard openapi_client.CanvasesCanvasDashb Name: canvasName, }, Spec: DashboardSpec{ - Panels: dashboard.GetPanels(), - Layout: dashboard.GetLayout(), + Panels: panels, + Layout: layout, }, } } diff --git a/pkg/cli/commands/canvases/models/dashboard_test.go b/pkg/cli/commands/canvases/models/dashboard_test.go index 34c5bf968c..39992127eb 100644 --- a/pkg/cli/commands/canvases/models/dashboard_test.go +++ b/pkg/cli/commands/canvases/models/dashboard_test.go @@ -1,8 +1,10 @@ package models import ( + "encoding/json" "testing" + "github.com/ghodss/yaml" "github.com/stretchr/testify/require" "github.com/superplanehq/superplane/pkg/openapi_client" ) @@ -139,3 +141,24 @@ func TestDashboardConversions(t *testing.T) { require.Len(t, body.GetPanels(), 1) require.Len(t, body.GetLayout(), 1) } + +func TestDashboardResourceFromDashboardDefaultsEmptyCollections(t *testing.T) { + dashboard := openapi_client.CanvasesCanvasDashboard{} + dashboard.SetCanvasId("canvas-123") + + resource := DashboardResourceFromDashboard(dashboard, "My Canvas") + + jsonPayload, err := json.Marshal(resource) + require.NoError(t, err) + require.Contains(t, string(jsonPayload), `"panels":[]`) + require.Contains(t, string(jsonPayload), `"layout":[]`) + require.NotContains(t, string(jsonPayload), `"panels":null`) + require.NotContains(t, string(jsonPayload), `"layout":null`) + + yamlPayload, err := yaml.Marshal(resource) + require.NoError(t, err) + require.Contains(t, string(yamlPayload), "panels: []") + require.Contains(t, string(yamlPayload), "layout: []") + require.NotContains(t, string(yamlPayload), "panels: null") + require.NotContains(t, string(yamlPayload), "layout: null") +}