Skip to content
Closed
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
10 changes: 10 additions & 0 deletions pkg/agents/anthropic/resources.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")),
Expand Down
5 changes: 5 additions & 0 deletions pkg/agents/anthropic/resources_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
130 changes: 130 additions & 0 deletions pkg/cli/commands/canvases/dashboard.go
Original file line number Diff line number Diff line change
@@ -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
}
113 changes: 113 additions & 0 deletions pkg/cli/commands/canvases/dashboard_test.go
Original file line number Diff line number Diff line change
@@ -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
}
88 changes: 88 additions & 0 deletions pkg/cli/commands/canvases/models/dashboard.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
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 {
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,
Metadata: DashboardMetadata{
CanvasID: dashboard.GetCanvasId(),
Name: canvasName,
},
Spec: DashboardSpec{
Panels: panels,
Layout: layout,
},
}
}
Comment thread
cursor[bot] marked this conversation as resolved.

func UpdateDashboardRequestFromDashboard(resource Dashboard) openapi_client.CanvasesUpdateCanvasDashboardBody {
body := openapi_client.CanvasesUpdateCanvasDashboardBody{}
body.SetPanels(resource.Spec.Panels)
body.SetLayout(resource.Spec.Layout)
return body
}
Loading
Loading