Layouts
Topology layouts store positions, zoom/pan, hidden nodes, and node scale. The canvas uses one shared library across networks: scope_type=workspace. The server supplies the fixed workspace ID 00000000-0000-0000-0000-000000000001; scope_id is optional and ignored for this scope. Existing area, protocol-instance, routing-domain and network layouts remain in the library with their original metadata. Users see shared layouts and their own private layouts. Admins can set a shared workspace default.
List Layouts
GET /api/v1/layouts?scope_type={scope_type}&scope_id={scope_id}
GET /api/v1/areas/{areaID}/layouts
List layouts for the requested library or legacy scope. Returns the authenticated user's own layouts plus any public layouts.
With scope_type=workspace, returns these layouts from all scopes. Deleted entries are excluded. The response includes owner_username and is_default; the latter identifies the workspace default. An empty list is []. Listing omits full layout_data.
Query parameters (first form):
scope_type (required) — One of: "workspace", "area", "protocol_instance", "routing_domain", "network"
scope_id — UUID of the scope; required only for non-workspace requests
Note: The second form (/areas/{areaID}/layouts) is backward-compatible and equivalent to ?scope_type=area&scope_id={areaID}.
Note: For parent scopes (network, routing_domain, protocol_instance), the response includes layouts from child scopes as well, so users can see layouts saved at lower levels.
Response: 200 OK
[
{
"id": "uuid",
"scope_type": "area",
"scope_id": "uuid",
"owner_id": "uuid",
"name": "My Layout",
"description": "Custom layout for area 0",
"is_public": false,
"created_at": "2024-01-01T00:00:00Z",
"updated_at": "2024-01-01T00:00:00Z"
}
]
Note: layout_data is omitted from list responses for performance.
Error: 400 Bad Request — Missing or invalid scope_type/scope_id
Create Layout
POST /api/v1/layouts?scope_type={scope_type}&scope_id={scope_id}
POST /api/v1/areas/{areaID}/layouts
Create a new layout for a scope.
Request body:
{
"name": "My Layout",
"description": "Optional description",
"is_public": false,
"layout_data": {
"positions": {"node-uuid": {"x": 100, "y": 200}},
"zoom": 1.5,
"pan": {"x": 0, "y": 0},
"hidden_nodes": ["router-id"],
"node_scale": 0.7
}
}
layout_data is stored as opaque JSONB; the frontend writes positions, viewport zoom/pan, hidden_nodes (router-id keys) and node_scale (global node/label scale, absent on layouts saved before the field existed).
Response: 201 Created
{
"id": "uuid",
"scope_type": "area",
"scope_id": "uuid",
"owner_id": "uuid",
"name": "My Layout",
"description": "Optional description",
"is_public": false,
"layout_data": { ... },
"created_at": "2024-01-01T00:00:00Z",
"updated_at": "2024-01-01T00:00:00Z"
}
Error: 400 Bad Request — Missing required fields
Get Layout
GET /api/v1/layouts/{layoutID}
GET /api/v1/areas/{areaID}/layouts/{layoutID}
Get a single layout by ID (includes full layout_data).
Access control: Owner, public layouts, or admin.
Response: 200 OK (full layout object with layout_data)
Error responses:
403 Forbidden — User does not own the layout and it is not public
404 Not Found — Layout does not exist
Update Layout
PUT /api/v1/layouts/{layoutID}
PUT /api/v1/areas/{areaID}/layouts/{layoutID}
Update a layout. All fields are optional (partial update).
Access control: Owner or admin only.
Request body:
{
"name": "Updated Name",
"description": "Updated description",
"is_public": true,
"layout_data": { ... }
}
Response: 200 OK (updated layout object)
Error responses:
403 Forbidden — User does not own the layout and is not an admin
404 Not Found — Layout does not exist
Delete Layout
DELETE /api/v1/layouts/{layoutID}
DELETE /api/v1/areas/{areaID}/layouts/{layoutID}
Move a layout to trash (deleted_at), preserving its data. Clears its shared and legacy default status. Repeated deletion is idempotent. Personal selections are retained but ignored while the layout is deleted. Layouts also survive deletion of their original hierarchy entity.
Access control: Owner or admin only.
Response: 204 No Content
Error responses:
403 Forbidden — User does not own the layout and is not an admin
404 Not Found — Layout does not exist
Deleted Layouts and Restore
GET /api/v1/layouts/trash
PUT /api/v1/layouts/{layoutID}/restore
The trash list returns metadata including deleted_at for the owner's deleted layouts; admins see all deleted layouts. Restore requires ownership or the admin role and returns 204 No Content. It restores the same ID and data. If the original name is now in use within the same owner/scope, it appends a distinct restored suffix. It never replaces the workspace default. A retained personal selection may become active again after restoration.
Restore returns 403 for another user's layout, 404 for a missing ID, or 409 if the entry is already live or a concurrent name conflict prevents restoration. Deleted layouts cannot be updated or activated (409) and their contents can only be read by the owner or an admin. No automatic purge or permanent-delete endpoint is provided.
Set Shared Default
PUT /api/v1/layouts/{layoutID}/set-default?scope_type=workspace
Admin only (403 otherwise). The layout must be shared and outside the trash (409 otherwise). Atomically replaces the workspace default and returns 200 OK with {"message":"default layout set"}. Explicit personal workspace selections keep precedence. Legacy scope parameters, or no scope to use the layout's own scope, remain supported.
Get Active Layout
GET /api/v1/layouts/active?scope_type={scope_type}&scope_id={scope_id}
GET /api/v1/areas/{areaID}/active-layout
Get the currently active layout. For scope_type=workspace, resolution is: the user's explicit workspace selection, then the shared workspace default, then a retained personal selection from an older scope. When several legacy selections exist, choose the most recently updated layout, breaking ties by ID. Only accessible, non-deleted layouts qualify. Returns 204 if none qualifies.
The hierarchy walk below applies only to legacy scope requests.
Query parameters (first form):
scope_type (required) — One of: "workspace", "area", "protocol_instance", "routing_domain", "network"
scope_id — UUID of the scope; required only for non-workspace requests
Hierarchy walk order (closest match wins):
The server checks the following scopes in order, stopping at the first match. At each scope level, a user's explicit active layout takes priority over the scope default.
| Starting scope |
Walk order |
area |
area → protocol_instance → routing_domain → network |
protocol_instance |
protocol_instance → routing_domain → network |
routing_domain |
routing_domain → network |
network |
network only |
Resolution at each scope level:
- User's explicit active layout for the scope (set via Activate Layout)
- Scope default layout (set by admin via Set Default Layout)
If no layout is found at any level, returns 204 No Content.
Response: 200 OK (full layout object) or 204 No Content
Note: The returned layout may belong to a different (ancestor) scope than the one requested. The response scope_type and scope_id fields reflect the layout's own scope, not the requested scope.
Error: 400 Bad Request — Missing or invalid scope parameters
Activate Layout
PUT /api/v1/layouts/{layoutID}/activate
PUT /api/v1/layouts/{layoutID}/activate?scope_type={scope_type}&scope_id={scope_id}
PUT /api/v1/areas/{areaID}/layouts/{layoutID}/activate
Set a layout as the authenticated user's active layout. By default, the activation is stored under the layout's own scope. When optional scope_type and scope_id query parameters are provided, the activation is stored under the specified viewing scope instead. This enables cross-scope layout reuse -- for example, activating an area-scoped layout while viewing at the protocol-instance or network level.
The canvas always sends scope_type=workspace. This stores one selection for the user regardless of the layout's original scope, and it persists across network/area changes.
Query parameters (optional, first and second forms):
scope_type — One of: "workspace", "area", "protocol_instance", "routing_domain", "network"
scope_id — UUID of the viewing scope to store the activation under
When scope_type/scope_id are omitted (or when using the backward-compatible /areas/{areaID}/... form), the activation is stored under the layout's own scope_id.
Access control: User must have access to the layout (owner, public, or admin).
Response: 200 OK
{"message": "layout activated"}
Error responses:
403 Forbidden — User does not have access to the layout
404 Not Found — Layout does not exist