Skip to main content

Widget layout storage

Use this integration after deciding that the product must persist a WidgetLayoutState. Widget layout storage is a separate boundary from ChartStorageAdapter, even when both boundaries use the same authenticated backend.

Implement the storage contract

WidgetLayoutStorageAdapter owns one complete widget panel arrangement at a time:

MethodContract
load(context)Resolve one WidgetLayoutState, or null only when that widget panel arrangement does not exist
save(request)Replace the complete widget panel arrangement with request.state
delete(context)Delete only the selected widget panel arrangement

Every operation receives a stable arrangementId. Optional userId and workspaceId values partition the record within the host's storage model. They are routing context, not proof that the caller may access the record.

Use createRestWidgetLayoutStorageAdapter when the backend can implement the HTTP contract on this page. Implement WidgetLayoutStorageAdapter directly when the application uses GraphQL, RPC, different routes, revision tokens, or another transport.

Configure the REST adapter

createRestWidgetLayoutStorageAdapter implements the widget panel arrangement HTTP transport:

panelStorage.ts
import { createRestWidgetLayoutStorageAdapter } from '@tradescript/pro/sdk';

declare function getAccessToken(): Promise<string>;

export const panelStorage = createRestWidgetLayoutStorageAdapter({
baseUrl: 'https://api.example.com/widget-layout-storage',
apiVersion: 'v1',
clientId: 'pro-terminal',
headers: async () => ({
Authorization: `Bearer ${await getAccessToken()}`,
}),
});

The default endpoint is panel_arrangements. Set path when the backend uses a different route. The adapter resolves headers() for each operation, so short-lived credentials can be refreshed without replacing the adapter.

Load before mounting and save later changes

Load the widget panel arrangement before mounting the composed surface. Pass the result as defaultLayout, then save later snapshots from onLayoutChange:

persistentWorkspace.ts
import type {
TradeScriptSdkProducts,
WidgetLayoutStorageAdapter,
WidgetLayoutSurfaceWidgetDefinition,
} from '@tradescript/pro/sdk';

declare const sdk: TradeScriptSdkProducts;
declare const panelStorage: WidgetLayoutStorageAdapter;
declare const widgets: readonly WidgetLayoutSurfaceWidgetDefinition[];

const context = {
arrangementId: 'primary-terminal',
userId: 'user-123',
workspaceId: 'main-desk',
};

const defaultLayout = (await panelStorage.load(context)) ?? undefined;
let saveQueue = Promise.resolve();

export const mounted = sdk.layout.mount({
mount: '#workspace',
widgets,
defaultLayout,
onLayoutChange(state) {
saveQueue = saveQueue
.then(() => panelStorage.save({ ...context, state }))
.catch((error: unknown) => {
console.error('Panel arrangement save failed', error);
});
},
});

onLayoutChange is debounced by the widget panel container. The explicit queue preserves save order when a previous request is still in flight. In production, surface failed saves to the user instead of relying only on logging.

The same defaultLayout and onLayoutChange options are available on sdk.tradingTerminal.mount().

terminalId identifies the live terminal. It does not save anything by itself and is not the persisted record identity. Choose an explicit arrangementId even when both values are derived from the same product surface.

Save or restore imperatively

Use the canonical widget panel arrangement methods when a host control performs a manual save or restore:

manualPanelSave.ts
import type {
WidgetLayoutContainerHandle,
WidgetLayoutStorageAdapter,
} from '@tradescript/pro/sdk';

declare const handle: WidgetLayoutContainerHandle;
declare const panelStorage: WidgetLayoutStorageAdapter;

const context = { arrangementId: 'analysis-workspace' };
const current = handle.savePanelArrangement();
if (current !== undefined) {
await panelStorage.save({ ...context, state: current });
}

const stored = await panelStorage.load(context);
if (stored !== null && !handle.loadPanelArrangement(stored)) {
throw new Error('The stored panel arrangement could not be applied');
}

saveLayout() and loadLayout() remain deprecated compatibility aliases. They save and restore widget panel topology, not ChartLayout.

Implement the HTTP resource

The REST adapter calls one widget panel arrangement resource:

OperationRequest
LoadGET /v1/panel_arrangements?arrangement={arrangementId}
SavePUT /v1/panel_arrangements?arrangement={arrangementId}
DeleteDELETE /v1/panel_arrangements?arrangement={arrangementId}

Configured clientId, userId, and workspaceId become client, user, and workspace query parameters. Values supplied to an operation override the configured user and workspace defaults.

Save receives exactly one public state object:

{
"state": {
"version": 1,
"dockview": {
"grid": {
"root": {
"type": "branch",
"data": [
{
"type": "leaf",
"data": {
"views": ["chart"],
"activeView": "chart",
"id": "group-1"
},
"size": 1280
}
],
"size": 720
},
"width": 1280,
"height": 720,
"orientation": "HORIZONTAL"
},
"panels": {
"chart": {
"id": "chart",
"contentComponent": "ts-widget",
"tabComponent": "ts-widget-tab",
"params": {
"widgetId": "chart",
"widgetParams": { "symbol": "AAPL" }
},
"title": "Chart"
}
},
"activeGroup": "group-1"
},
"containerHeight": 720
}
}

This is a representative one-panel snapshot emitted by the container. The exact dockview value changes with the user's panels and must be stored as an opaque object, not constructed or normalized by the backend.

Load may return WidgetLayoutState directly, under state, or inside the standard response envelope:

{
"status": "ok",
"data": {
"state": {
"version": 1,
"dockview": {
"grid": {
"root": {
"type": "branch",
"data": [
{
"type": "leaf",
"data": {
"views": ["chart"],
"activeView": "chart",
"id": "group-1"
},
"size": 1280
}
],
"size": 720
},
"width": 1280,
"height": 720,
"orientation": "HORIZONTAL"
},
"panels": {
"chart": {
"id": "chart",
"contentComponent": "ts-widget",
"tabComponent": "ts-widget-tab",
"params": {
"widgetId": "chart",
"widgetParams": { "symbol": "AAPL" }
},
"title": "Chart"
}
},
"activeGroup": "group-1"
},
"containerHeight": 720
}
}
}

Save and delete accept an empty successful response, including 204. A 404 load and a successful empty load resolve to null. A 404 save or delete rejects because only a missing load represents absence.

The adapter rejects malformed successful JSON, an invalid WidgetLayoutState, non-success HTTP responses, and a successful envelope with status: "error". Authentication, permission, transport, and server failures must remain errors; do not convert them to null.

An empty or whitespace-only arrangementId rejects before a network request. For saves and loaded responses, the REST adapter also requires version: 1, an object-valued dockview, and, when present, a finite numeric containerHeight.

Enforce authentication and tenancy

  • Authenticate and authorize every load, save, and delete operation.
  • Derive the caller and permitted tenant scope from credentials. Treat client, user, workspace, and arrangement query values only as routing context.
  • Partition records with an explicit tenant key and a stable arrangementId. Never let a visible terminal title become record identity.
  • Configure CORS for the application origin, the three HTTP methods, and the required authorization headers.
  • Runtime-validate requests and responses, and redact credentials and full saved payloads from logs.
  • Version the HTTP route independently from WidgetLayoutState.version.

Define errors and concurrency

The built-in REST adapter does not add retries, timeouts, cancellation, an offline queue, revision tokens, or compare-and-swap behavior. If a request may have reached the server before failing, reconcile the saved widget panel arrangement before retrying an uncertain write.

onLayoutChange debounces event emission, but it does not guarantee that network requests finish in order. Queue saves in the host, as in the example above, or serialize them on the backend. Without a stronger custom contract, concurrent saves are last-write-wins.

Use a custom transport

Implement WidgetLayoutStorageAdapter directly when the REST convention does not match the host backend. Keep these invariants:

  • load returns null only for an absent widget panel arrangement and rejects every permission, validation, or transport failure.
  • save replaces one complete WidgetLayoutState; it does not partially merge the opaque dockview payload.
  • delete affects only the addressed widget panel arrangement and never cascades to chart layouts.
  • The adapter closure owns the authenticated API client and tenant scope.
  • If the custom backend adds revisions or subscriptions, the custom adapter owns their conflict and teardown behavior; they are not part of the public WidgetLayoutStorageAdapter contract.

Test the widget panel integration

  • Load a missing arrangementId before mount, confirm null, and confirm the product starts from its default widget panel arrangement.
  • Add, move, resize, activate, regroup, and close panels. Save every debounced snapshot in request order, reload, and verify panel parameters and optional container height return.
  • Delay two saves so the first finishes last. Confirm the integration still persists the newest widget panel arrangement.
  • Change chart content without moving panels and confirm widget panel storage does not change. Then move panels without changing chart content and confirm chart workspace storage does not change.
  • Restore with every persisted WidgetLayoutSurfaceWidgetDefinition.id registered, then repeat with one definition missing and verify the unknown-widget placeholder.
  • Repeat load, save, and delete with another user and workspace. Confirm no record crosses the authenticated tenant boundary.
  • Exercise malformed JSON, invalid state, 401, 403, 404, 429, 5xx, connection loss, and an empty successful delete.
  • Delete the widget panel arrangement and confirm chart layouts remain available. Destroy the mounted surface and confirm no debounced save fires afterward.

Next steps