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:
| Method | Contract |
|---|---|
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:
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:
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:
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:
| Operation | Request |
|---|---|
| Load | GET /v1/panel_arrangements?arrangement={arrangementId} |
| Save | PUT /v1/panel_arrangements?arrangement={arrangementId} |
| Delete | DELETE /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, andarrangementquery 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:
loadreturnsnullonly for an absent widget panel arrangement and rejects every permission, validation, or transport failure.savereplaces one completeWidgetLayoutState; it does not partially merge the opaquedockviewpayload.deleteaffects 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
WidgetLayoutStorageAdaptercontract.
Test the widget panel integration
- Load a missing
arrangementIdbefore mount, confirmnull, 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.idregistered, 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
- Widget panel arrangements — review the state and lifecycle before implementing storage.
- Backend integration — keep chart workspace and widget panel storage boundaries separate.
- Trading Terminal — mount chart and widget panel persistence together.