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
69 changes: 66 additions & 3 deletions api/swagger/superplane.swagger.json
Original file line number Diff line number Diff line change
Expand Up @@ -1258,7 +1258,7 @@
},
"put": {
"summary": "Update canvas version",
"description": "Updates a user-owned canvas version; if version_id is omitted, updates the live canvas in sandbox mode",
"description": "Updates a user-owned canvas version; if version_id is omitted, updates the live canvas when versioning is disabled",
"operationId": "Canvases_UpdateCanvasVersion2",
"responses": {
"200": {
Expand Down Expand Up @@ -1334,7 +1334,7 @@
},
"put": {
"summary": "Update canvas version",
"description": "Updates a user-owned canvas version; if version_id is omitted, updates the live canvas in sandbox mode",
"description": "Updates a user-owned canvas version; if version_id is omitted, updates the live canvas when versioning is disabled",
"operationId": "Canvases_UpdateCanvasVersion",
"responses": {
"200": {
Expand Down Expand Up @@ -1437,6 +1437,44 @@
"tags": [
"Canvas"
]
},
"put": {
"summary": "Update canvas",
"description": "Updates canvas metadata",
"operationId": "Canvases_UpdateCanvas",
"responses": {
"200": {
"description": "A successful response.",
"schema": {
"$ref": "#/definitions/CanvasesUpdateCanvasResponse"
}
},
"default": {
"description": "An unexpected error response.",
"schema": {
"$ref": "#/definitions/googlerpcStatus"
}
}
},
"parameters": [
{
"name": "id",
"in": "path",
"required": true,
"type": "string"
},
{
"name": "body",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/CanvasesUpdateCanvasBody"
}
}
],
"tags": [
"Canvas"
]
}
},
"/api/v1/components": {
Expand Down Expand Up @@ -4221,6 +4259,9 @@
},
"isTemplate": {
"type": "boolean"
},
"canvasVersioningEnabled": {
"type": "boolean"
}
}
},
Expand Down Expand Up @@ -4754,6 +4795,28 @@
}
}
},
"CanvasesUpdateCanvasBody": {
"type": "object",
"properties": {
"name": {
"type": "string"
},
"description": {
"type": "string"
},
"canvasVersioningEnabled": {
"type": "boolean"
}
}
},
"CanvasesUpdateCanvasResponse": {
"type": "object",
"properties": {
"canvas": {
"$ref": "#/definitions/CanvasesCanvas"
}
}
},
"CanvasesUpdateCanvasVersionBody": {
"type": "object",
"properties": {
Expand Down Expand Up @@ -5938,7 +6001,7 @@
"type": "string",
"format": "date-time"
},
"canvasSandboxModeEnabled": {
"canvasVersioningEnabled": {
"type": "boolean"
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
BEGIN;

ALTER TABLE public.organizations
RENAME COLUMN canvas_sandbox_mode_enabled TO canvas_versioning_enabled;

UPDATE public.organizations
SET canvas_versioning_enabled = NOT canvas_versioning_enabled;

ALTER TABLE public.organizations
ALTER COLUMN canvas_versioning_enabled SET DEFAULT false;

COMMIT;
Empty file.
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
ALTER TABLE public.workflows
ADD COLUMN canvas_versioning_enabled boolean NOT NULL DEFAULT false;
7 changes: 4 additions & 3 deletions db/structure.sql
Original file line number Diff line number Diff line change
Expand Up @@ -342,7 +342,7 @@ CREATE TABLE public.organizations (
updated_at timestamp without time zone DEFAULT CURRENT_TIMESTAMP,
deleted_at timestamp without time zone,
description text DEFAULT ''::text,
canvas_sandbox_mode_enabled boolean DEFAULT true NOT NULL
canvas_versioning_enabled boolean DEFAULT false NOT NULL
);


Expand Down Expand Up @@ -604,7 +604,8 @@ CREATE TABLE public.workflows (
created_by uuid,
deleted_at timestamp without time zone,
is_template boolean DEFAULT false NOT NULL,
live_version_id uuid NOT NULL
live_version_id uuid NOT NULL,
canvas_versioning_enabled boolean DEFAULT false NOT NULL
);


Expand Down Expand Up @@ -1846,7 +1847,7 @@ SET row_security = off;
--

COPY public.schema_migrations (version, dirty) FROM stdin;
20260304110100 f
20260306201921 f
\.


Expand Down
6 changes: 6 additions & 0 deletions docs/contributing/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,13 +90,19 @@ The database model follows a hierarchical structure that enables multi-tenancy a

- Top-level tenant boundary providing complete data isolation
- All resources (canvases, integrations, secrets) are scoped to an organization
- Organization metadata `canvas_versioning_enabled` (API field `canvasVersioningEnabled`) acts as a global override for canvas versioning when enabled

**Canvas:**

- Workspace for building and managing workflows
- Belongs to an organization
- Contains multiple workflows with their nodes and edges
- Stores workflow graph structure, node configurations, and metadata
- Canvas editing behavior is controlled by effective canvas versioning: organization metadata `canvas_versioning_enabled` OR canvas metadata `canvas_versioning_enabled` (API field `canvasVersioningEnabled`)
- Effective behavior:
- Organization versioning enabled: all canvases are effectively versioned
- Organization versioning disabled: each canvas uses its own `canvas_versioning_enabled` value
- When effective canvas versioning is enabled, users edit draft versions and publish via change requests; when disabled, users edit the live canvas directly

**Integration:**

Expand Down
1 change: 1 addition & 0 deletions pkg/authorization/interceptor.go
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,7 @@ func NewAuthorizationInterceptor(authService Authorization) *AuthorizationInterc
pbCanvases.Canvases_ListCanvases_FullMethodName: {Resource: "canvases", Action: "read", DomainType: models.DomainTypeOrganization},
pbCanvases.Canvases_DescribeCanvas_FullMethodName: {Resource: "canvases", Action: "read", DomainType: models.DomainTypeOrganization},
pbCanvases.Canvases_CreateCanvas_FullMethodName: {Resource: "canvases", Action: "create", DomainType: models.DomainTypeOrganization},
pbCanvases.Canvases_UpdateCanvas_FullMethodName: {Resource: "canvases", Action: "update", DomainType: models.DomainTypeOrganization},
pbCanvases.Canvases_CreateCanvasVersion_FullMethodName: {Resource: "canvases", Action: "update", DomainType: models.DomainTypeOrganization},
pbCanvases.Canvases_ListCanvasVersions_FullMethodName: {Resource: "canvases", Action: "read", DomainType: models.DomainTypeOrganization},
pbCanvases.Canvases_DescribeCanvasVersion_FullMethodName: {Resource: "canvases", Action: "read", DomainType: models.DomainTypeOrganization},
Expand Down
4 changes: 4 additions & 0 deletions pkg/cli/commands/canvases/models/canvas.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,10 @@ func ParseCanvas(raw []byte) (*Canvas, error) {
return nil, fmt.Errorf("canvas apiVersion is required")
}

if resource.Metadata == nil {
return nil, fmt.Errorf("canvas metadata is required")
}

if resource.Metadata.Name == nil {
return nil, fmt.Errorf("canvas metadata.name is required")
}
Expand Down
6 changes: 3 additions & 3 deletions pkg/cli/commands/canvases/publish.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,12 @@ func (c *publishCommand) Execute(ctx core.CommandContext) error {
return err
}

versioningContext, err := resolveCanvasVersioningContext(ctx)
versioningContext, err := resolveCanvasVersioningContext(ctx, canvasID)
if err != nil {
return err
}
if versioningContext.sandboxModeEnabled {
return fmt.Errorf("canvas versioning is disabled while sandbox mode is enabled")
if !versioningContext.versioningEnabled {
return fmt.Errorf("effective canvas versioning is disabled for this canvas")
}

draftVersionID, err := findCurrentUserDraftVersionID(ctx, canvasID)
Expand Down
2 changes: 1 addition & 1 deletion pkg/cli/commands/canvases/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ func NewCommand(options core.BindOptions) *cobra.Command {
Args: cobra.MaximumNArgs(1),
}
updateCmd.Flags().StringVarP(&updateFile, "file", "f", "", "filename, directory, or URL to files to use to update the resource")
updateCmd.Flags().BoolVar(&updateDraft, "draft", false, "update your draft version (required when versioning is enabled)")
updateCmd.Flags().BoolVar(&updateDraft, "draft", false, "update your draft version (required when effective canvas versioning is enabled)")
updateCmd.Flags().StringVar(&updateAutoLayout, "auto-layout", "", "automatically arrange the canvas (supported: horizontal)")
updateCmd.Flags().StringVar(&updateAutoLayoutScope, "auto-layout-scope", "", "scope for auto layout (full-canvas, connected-component, exact-set)")
updateCmd.Flags().StringArrayVar(&updateAutoLayoutNodes, "auto-layout-node", nil, "node id seed for auto layout (repeatable)")
Expand Down
8 changes: 4 additions & 4 deletions pkg/cli/commands/canvases/update.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,19 +59,19 @@ func (c *updateCommand) Execute(ctx core.CommandContext) error {
current = &canvas
}

versioningContext, err := resolveCanvasVersioningContext(ctx)
versioningContext, err := resolveCanvasVersioningContext(ctx, canvasID)
if err != nil {
return err
}

targetVersionID := ""
if versioningContext.sandboxModeEnabled {
if !versioningContext.versioningEnabled {
if draftMode {
return fmt.Errorf("--draft cannot be used when canvas sandbox mode is enabled")
return fmt.Errorf("--draft cannot be used when effective canvas versioning is disabled")
}
} else {
if !draftMode {
return fmt.Errorf("canvas versioning is enabled for this organization; use --draft to update your edit version")
return fmt.Errorf("effective canvas versioning is enabled for this canvas; use --draft")
}

targetVersionID, err = ensureCurrentUserDraftVersionID(ctx, canvasID)
Expand Down
31 changes: 7 additions & 24 deletions pkg/cli/commands/canvases/versioning_helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,39 +9,22 @@ import (
)

type canvasVersioningContext struct {
currentUserID string
sandboxModeEnabled bool
versioningEnabled bool
}

func resolveCanvasVersioningContext(ctx core.CommandContext) (*canvasVersioningContext, error) {
me, _, err := ctx.API.MeAPI.MeMe(ctx.Context).Execute()
if err != nil {
return nil, err
}

currentUserID := strings.TrimSpace(me.GetId())
if currentUserID == "" {
return nil, fmt.Errorf("user id not found for authenticated user")
}

organizationID := strings.TrimSpace(me.GetOrganizationId())
if organizationID == "" {
return nil, fmt.Errorf("organization id not found for authenticated user")
}

organizationResponse, _, err := ctx.API.OrganizationAPI.
OrganizationsDescribeOrganization(ctx.Context, organizationID).
func resolveCanvasVersioningContext(ctx core.CommandContext, canvasID string) (*canvasVersioningContext, error) {
canvasResponse, _, err := ctx.API.CanvasAPI.
CanvasesDescribeCanvas(ctx.Context, canvasID).
Execute()
if err != nil {
return nil, err
}
if organizationResponse.Organization == nil || organizationResponse.Organization.Metadata == nil {
return nil, fmt.Errorf("organization metadata not found")
if canvasResponse.Canvas == nil || canvasResponse.Canvas.Metadata == nil {
return nil, fmt.Errorf("canvas metadata not found")
}

return &canvasVersioningContext{
currentUserID: currentUserID,
sandboxModeEnabled: organizationResponse.Organization.Metadata.GetCanvasSandboxModeEnabled(),
versioningEnabled: canvasResponse.Canvas.Metadata.GetCanvasVersioningEnabled(),
}, nil
}

Expand Down
28 changes: 14 additions & 14 deletions pkg/cli/whoami.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ func (w *whoamiCommand) Execute(ctx core.CommandContext) error {
}

organizationLabel := response.GetOrganizationId()
var canvasSandboxModeEnabled *bool
var canvasVersioningEnabled *bool
if response.HasOrganizationId() && response.GetOrganizationId() != "" {
orgResponse, _, err := ctx.API.OrganizationAPI.
OrganizationsDescribeOrganization(ctx.Context, response.GetOrganizationId()).
Expand All @@ -29,38 +29,38 @@ func (w *whoamiCommand) Execute(ctx core.CommandContext) error {
organizationLabel = *metadata.Name
}

if enabled, ok := metadata.GetCanvasSandboxModeEnabledOk(); ok {
canvasSandboxModeEnabled = enabled
if enabled, ok := metadata.GetCanvasVersioningEnabledOk(); ok {
canvasVersioningEnabled = enabled
}
}
}

if ctx.Renderer.IsText() {
return ctx.Renderer.RenderText(func(stdout io.Writer) error {
sandboxModeLabel := "unknown"
if canvasSandboxModeEnabled != nil {
if *canvasSandboxModeEnabled {
sandboxModeLabel = "enabled"
versioningLabel := "unknown"
if canvasVersioningEnabled != nil {
if *canvasVersioningEnabled {
versioningLabel = "enabled"
} else {
sandboxModeLabel = "disabled"
versioningLabel = "disabled"
}
}

_, _ = fmt.Fprintf(stdout, "ID: %s\n", response.GetId())
_, _ = fmt.Fprintf(stdout, "Email: %s\n", response.GetEmail())
_, _ = fmt.Fprintf(stdout, "Organization ID: %s\n", response.GetOrganizationId())
_, _ = fmt.Fprintf(stdout, "Organization: %s\n", organizationLabel)
_, _ = fmt.Fprintf(stdout, "Canvas Sandbox Mode: %s\n", sandboxModeLabel)
_, _ = fmt.Fprintf(stdout, "Canvas Versioning: %s\n", versioningLabel)
return nil
})
}

return ctx.Renderer.Render(map[string]any{
"id": response.GetId(),
"email": response.GetEmail(),
"organizationId": response.GetOrganizationId(),
"organizationName": organizationLabel,
"canvasSandboxModeEnabled": canvasSandboxModeEnabled,
"id": response.GetId(),
"email": response.GetEmail(),
"organizationId": response.GetOrganizationId(),
"organizationName": organizationLabel,
"canvasVersioningEnabled": canvasVersioningEnabled,
})
}

Expand Down
26 changes: 17 additions & 9 deletions pkg/grpc/actions/canvases/create_canvas.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,18 +44,26 @@ func CreateCanvas(ctx context.Context, registry *registry.Registry, organization
if isTemplate {
targetOrganizationID = models.TemplateOrganizationID
}
canvasVersioningEnabled := false
if !isTemplate {
canvasVersioningEnabled, err = models.IsCanvasVersioningEnabled(targetOrganizationID)
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to load organization canvas versioning: %v", err)
}
}
liveVersionID := uuid.New()

canvas := models.Canvas{
ID: uuid.New(),
OrganizationID: targetOrganizationID,
LiveVersionID: &liveVersionID,
IsTemplate: isTemplate,
Name: pbCanvas.Metadata.Name,
Description: pbCanvas.Metadata.Description,
CreatedBy: &createdBy,
CreatedAt: &now,
UpdatedAt: &now,
ID: uuid.New(),
OrganizationID: targetOrganizationID,
LiveVersionID: &liveVersionID,
IsTemplate: isTemplate,
CanvasVersioningEnabled: canvasVersioningEnabled,
Name: pbCanvas.Metadata.Name,
Description: pbCanvas.Metadata.Description,
CreatedBy: &createdBy,
CreatedAt: &now,
UpdatedAt: &now,
}

err = database.Conn().Transaction(func(tx *gorm.DB) error {
Expand Down
Loading