Skip to main content

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=settings opens 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_url
  • workspaces.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.ts
  • src/features/dashboards/custom-workspace-studio-store.ts
  • src/features/dashboards/workspace-api.ts
  • src/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.md
  • docs/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 Workspaces app 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.

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.tsx
  • src/features/dashboards/CustomDashboardStudioPage.tsx
  • src/features/dashboards/CustomWorkspaceSettingsPage.tsx
  • src/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.