Workspaces
Overview
Workspaces is the core extension's user-scoped workspace builder.
It is intentionally modeled as an app, not a loose dashboard utility. The app owns one surface,
Workspaces, and that surface handles three states:
- workspace index
- workspace canvas
- workspace settings
The canvas and settings are not separate app destinations. They are views over a selected workspace instance.
Route Model
The primary route is:
/app/workspace-studio/workspaces
The entire app is gated by VITE_INCLUDE_WORKSPACES=true. When that env flag is set to false, the
runtime registry does not expose workspace-studio, and direct app routes fall back through the
normal app-resolution redirect path.
State is selected through query params:
?workspace=<id>opens the selected workspace canvas?workspace=<id>&view=settingsopens the settings view for that same workspace
This keeps the app model simple while still supporting instance-specific editing flows.
Persistence Model
Workspaces are stored in browser localStorage during development by default.
The current implementation is:
- local to the browser
- scoped by signed-in user id
- draft-aware, with explicit save/reset behavior
- migration-capable for older grid formats
If both workspace backend URLs are configured in config/command-center.yaml, the studio switches
to backend persistence instead:
workspaces.list_urlworkspaces.detail_url
Blank, null, or None values keep the browser-local fallback active.
The main persistence code lives in:
src/features/dashboards/custom-dashboard-storage.tssrc/features/dashboards/custom-workspace-studio-store.tssrc/features/dashboards/workspace-api.tssrc/features/dashboards/workspace-persistence.ts
When the backend uses numeric workspace ids, the frontend normalizes them to strings in the
workspace model and URL state.
In backend mode, workspace creation waits for the backend response and adopts the backend-assigned
id instead of persisting a client-generated id.
The workspace index page uses the backend-backed saved collection as its source of truth in backend
mode rather than rendering the current draft collection.
In backend mode, the editor keeps a local draft and only persists changes when the user explicitly
saves.
In backend mode, delete reloads the workspace collection from the backend after success instead of
computing the next list locally.
In backend mode, workspace settings also expose a Permissions tab backed by the standard
object-sharing endpoints for the workspace object itself. Browser-local workspaces cannot be shared
through RBAC and show an explanatory backend-only message instead.
For the recommended production backend model for shared workspaces, see:
docs/workspace-backend-model.mddocs/adr-shared-workspace-state.md
Workspace Model
Each workspace is a dashboard-like model with:
- id
- title
- description
- labels
- grid config
- shared dashboard controls state
- widget instances and widget geometry
- widget bindings when a widget instance declares first-class inputs
The canvas uses a fine-grained grid and stores widget placement directly in the workspace model.
Custom workspaces currently normalize onto one canonical dense manual grid: 48 columns,
15px row units, and 8px visual gutters. Older 12-, 24-, and 96-column custom layouts are
migrated into that model on load so the studio no longer inherits legacy resize steps.
The studio now uses the root react-grid-layout v2 API directly through grouped
gridConfig / dragConfig / resizeConfig props instead of the old flat prop surface.
In custom edit mode, the editor intentionally uses only the bottom-right se resize handle so
width and height resize together from one corner.
Labels are edited in workspace settings as pill chips with enter-to-add behavior.
Widget runtime state can also be stored per instance when the widget reports it through the shared
widget contract.
Widget instances can also persist canonical bindings separately from props. Binding changes clear
that widget's runtime state by default so stale upstream-derived caches do not survive rebinding.
In canvas edit mode, widget cards keep the same header height as normal viewing, use the existing
header area as the drag target, and reveal edit actions on hover instead of adding a separate move
control.
Widget instances may hide their header during normal viewing, but the canvas forces headers visible
again in edit mode so widget controls remain reachable.
Non-row widgets in custom can resize down to a single column and a single row; only row widgets
keep the fixed full-width structural sizing rules.
Freshly inserted non-row widgets now all start from one shared workspace footprint instead of
deriving their starting geometry from each widget definition's defaultSize.
The canvas Components browser supports large widget catalogs with search, category/kind/source
filters, favorites, recent widgets, and grouped category browse when search is empty.
If a workspace still contains a widget id that is no longer available in the current client build,
the canvas explains that the widget is a legacy/unavailable instance and offers a direct delete
action.
Workspace deletion from settings uses the app's destructive confirmation dialog. In backend mode,
the workspace remains in the UI until the backend confirms the delete.
The dedicated widget settings page now also hosts a Bindings tab for widgets that declare
dependency inputs, so inter-widget edges are edited separately from normal settings rather than
hidden in raw props JSON. The binding UI is explicit per input: users select both the upstream
widget and the upstream output port, then see the final output -> input mapping directly.
JSON Snapshots
Workspaces can now be exported and recovered through a versioned JSON snapshot from workspace settings.
The snapshot includes:
- workspace metadata
- labels
- controls state
- grid configuration
- widget layout and props
- widget bindings
- widget runtime state when the widget supports it
The current envelope is mainsequence.workspace version 1.
Import supports two recovery paths:
- create a new workspace from the snapshot
- replace the current workspace draft with the snapshot
Import changes the draft model first. Users still save explicitly if they want the recovered workspace written to the active persistence target.
In a production backend, this snapshot shape should map to revision/export transport rather than
the primary Workspace row itself.
Favorites
Workspaces support instance-level favorites in addition to the shell's existing surface favorites.
That means users can:
- favorite the
Workspacesapp surface like any other surface - favorite a specific workspace instance from the workspace table
- open those favorited workspace instances from the global favorites menu
Workspace favorites are stored in shell-local persisted state, then resolved against the current workspace collection.
Copying Workspaces
The workspace index also exposes a direct Copy action for each workspace row.
Copy works by cloning the current workspace document into a fresh workspace instance and then routing through the normal create flow:
- local mode creates a new browser-local workspace
- backend mode issues a normal workspace create request, so the copy is stored as a new backend workspace rather than updating the original one
The copied workspace gets a fresh workspace id and a default title prefixed with Copy of.
Recommended Production Split
For shared workspaces, the recommended backend split is:
- shared workspace content in
Workspace - direct and team access in object-level grant tables
- per-user temporary viewing state in
WorkspaceUserState - immutable snapshots in
WorkspaceRevision
That split exists because a shared editable workspace should still avoid collaboration conflicts on temporary interactions such as:
- zoom / pan
- selected date range
- selected refresh interval
- selected graph node
Main Entry Points
src/features/dashboards/WorkspacesPage.tsxsrc/features/dashboards/CustomDashboardStudioPage.tsxsrc/features/dashboards/CustomWorkspaceSettingsPage.tsxsrc/features/dashboards/useCustomWorkspaceStudio.ts
The app registration lives in:
src/extensions/core/index.ts
Maintenance Notes
Update this page whenever any of the following change:
- route/query-param model
- local-storage schema or migration behavior
- JSON snapshot schema or import/export behavior
- favorites behavior
- workspace list, canvas, or settings ownership boundaries
- saved control state or widget layout behavior
Keep src/features/dashboards/README.md aligned with this page so code-local docs and product docs
do not drift.