Skip to content

Latest commit

Β 

History

History
909 lines (738 loc) Β· 36.1 KB

File metadata and controls

909 lines (738 loc) Β· 36.1 KB

Dashboard

Section Description
Overview Dashboard system intro, DashContainer vs DashCanvas comparison
Architecture Model hierarchy, ViewSpec/ViewModel lifecycle
DashContainer Space-filling tabbed/tiled layout via GoldenLayout
DashCanvas Scrollable grid-based widget layout via react-grid-layout
DashCanvasWidgetChooser UI for adding widgets to a DashCanvas
Common Features ViewSpecs, state persistence, publishing, refresh
Widget Content Patterns Implementing widget views and their models
Common Patterns Initialization, programmatic layout, view management
Common Pitfalls Frequent issues and how to avoid them
Key Source Files Primary source files for the dash package
Related Packages Links to related hoist-react packages

Overview

The /desktop/cmp/dash/ package provides Hoist's dashboard system β€” configurable layouts where users can add, remove, resize, and rearrange views (widgets). Two implementations are available:

  • DashContainer β€” Space-filling, tabbed/tiled layout powered by GoldenLayout v1.5. Views are organized in rows, columns, and stacks (tabs) that users can drag, resize, and rearrange. Note: Hoist uses the 1.x branch of GoldenLayout, which provides first-class React support. The newer 2.x branch dropped this support, so do not reference 2.x documentation or APIs.
  • DashCanvas β€” Scrollable, grid-based widget layout powered by react-grid-layout. Views are placed on a configurable grid and can be dragged and resized freely.

Both share a common architecture of ViewSpecs (templates), ViewModels (instances), and state persistence, allowing users to customize their workspace.

Choosing Between DashContainer and DashCanvas

The two implementations have different layout models and are suited to different use cases:

DashContainer is fully space-filling β€” it does not scroll. Widgets divide the available space using relative sizing in both dimensions, and can be stacked into tabsets. It devotes minimal space to UI chrome (no padding between widgets, tabs instead of individual headers), making it a good fit for space-constrained layouts such as detail panels docked to the bottom or side of a screen.

DashCanvas is space-filling horizontally but widgets have explicit heights (defaulted by the developer, then driven by user resizing), with configurable snapping points for width (columns) and height (rowHeight in pixels). The canvas scrolls vertically, allowing users to see more widgets at once without tabbing between them. This makes it well-suited for full-screen reporting dashboards and similar use cases where users benefit from viewing multiple widgets simultaneously.

DashContainer DashCanvas
Scrolling No β€” fills available space Vertical scroll
Widget sizing Relative in both dimensions Grid-snapped width, fixed-height rows
Stacking/tabs Yes β€” widgets can share a stack No β€” all widgets visible
Chrome overhead Minimal β€” no inter-widget padding Per-widget panel headers and margins
Best for Detail panels, constrained layouts Primary dashboards, reporting tools

When in doubt, favor DashCanvas β€” it is the more modern library and generally the more flexible tool. Ultimately, the choice depends on the specific use case and developer preference β€” these are suggestions, not rules.

Architecture

DashModel (abstract base)
β”œβ”€β”€ viewSpecs: ViewSpec[]         # Available view templates
β”œβ”€β”€ state: ViewState[]            # Current layout state
β”œβ”€β”€ viewModels: ViewModel[]       # Active view instances
β”œβ”€β”€ layoutLocked: boolean         # Prevent drag/resize
β”œβ”€β”€ contentLocked: boolean        # Prevent adding/removing views
β”œβ”€β”€ renameLocked: boolean         # Prevent renaming views
β”œβ”€β”€ emptyText: string             # Shown when container is empty
β”œβ”€β”€ addViewButtonText: string     # Button label in empty overlay
β”œβ”€β”€ extraMenuItems: MenuItemLike[]# Additional context menu items
└── refreshContextModel           # For coordinated refresh

DashViewSpec (base interface)
β”œβ”€β”€ id: string                    # Unique identifier
β”œβ”€β”€ content: Content              # Rendered content
β”œβ”€β”€ title: string                 # Default title (defaults to startCase(id))
β”œβ”€β”€ icon: ReactElement            # Default icon
β”œβ”€β”€ groupName: string             # Menu grouping
β”œβ”€β”€ unique: boolean               # Allow multiple instances?
β”œβ”€β”€ allowAdd/Remove/Rename        # Permission flags
└── omit: Thunkable<boolean>      # Exclude from dashboard

DashViewModel (base class)
β”œβ”€β”€ id: string                    # Instance identifier
β”œβ”€β”€ viewSpec: ViewSpec            # Template reference
β”œβ”€β”€ containerModel                # Parent DashContainerModel or DashCanvasModel
β”œβ”€β”€ title: string                 # Display title (customizable, persisted)
β”œβ”€β”€ titleDetails: string          # Dynamic suffix (not persisted)
β”œβ”€β”€ fullTitle: string             # Computed: title + titleDetails
β”œβ”€β”€ icon: ReactElement            # Display icon (customizable)
β”œβ”€β”€ isActive: boolean             # Currently visible?
β”œβ”€β”€ viewState: PlainObject        # Custom state for the view (persisted)
β”œβ”€β”€ extraMenuItems: MenuItemLike[]# Extra context menu items for this view
└── refreshContextModel           # For coordinated refresh

DashContainer

Basic Usage

import {dashContainer, DashContainerModel} from '@xh/hoist/desktop/cmp/dash';

const dashModel = new DashContainerModel({
    viewSpecs: [
        {id: 'grid', title: 'Data Grid', icon: Icon.grid(), content: () => gridPanel()},
        {id: 'chart', title: 'Chart', icon: Icon.chartLine(), content: () => chartPanel()},
        {id: 'summary', title: 'Summary', content: () => summaryPanel()}
    ],
    initialState: [
        {type: 'row', content: [
            {type: 'stack', width: 70, content: [
                {type: 'view', id: 'grid'},
                {type: 'view', id: 'chart'}
            ]},
            {type: 'view', id: 'summary', width: 30}
        ]}
    ]
});

dashContainer({model: dashModel})

DashContainerModel Config

Option Type Description
viewSpecs DashContainerViewSpec[] Available view templates. Required.
viewSpecDefaults Partial<ViewSpec> Properties to merge deeply into all viewSpecs.
initialState DashContainerViewState[] Initial layout. Default [].
renderMode RenderMode When to render views. Default 'lazy'. Can be overridden per-view.
refreshMode RefreshMode When to refresh views. Default 'onShowLazy'. Can be overridden per-view.
layoutLocked boolean Prevent drag/resize. Default false.
contentLocked boolean Prevent adding/removing views. Default false.
renameLocked boolean Prevent renaming views. Default false.
showMenuButton boolean Show context menu button in stack headers. Default false.
margin number Gap between items in pixels. Default 6.
persistWith PersistOptions Persistence configuration. Default persistence path is 'dashContainer'.
emptyText string Placeholder text when empty. Default 'No views have been added to the container.'
addViewButtonText string Button label in empty overlay. Default 'Add View'.
extraMenuItems MenuItemLike[] Additional context menu items shown below the 'Add' action.
goldenLayoutSettings PlainObject Passthrough config for the GoldenLayout instance.

DashContainerModel.defaults exposes margin and showMenuButton for app-wide override β€” see DashContainerModelDefaults for details.

State Structure

State is nested arrays of containers and views:

// Container types: 'row', 'column', 'stack'
// View type: 'view' (references viewSpec.id)

[{
    type: 'row',
    content: [
        {
            type: 'stack',
            width: '200px',  // Fixed pixel width
            content: [
                {type: 'view', id: 'navigation'},
                {type: 'view', id: 'filters'}
            ]
        },
        {
            type: 'column',
            width: 80,  // Relative width (percentage)
            content: [
                {type: 'view', id: 'grid', height: 60},
                {type: 'view', id: 'details', height: 40}
            ]
        }
    ]
}]

Container types:

  • row β€” lays out children horizontally
  • column β€” lays out children vertically
  • stack β€” lays out children as tabs (can only contain view items)

Sizing:

  • Numeric values = relative percentages of available space
  • String values (e.g., '200px') = fixed pixels (converted to relative at parse time)
  • Unspecified = remaining space divided equally among unsized children

DashContainerViewSpec

Extends DashViewSpec with:

Option Type Description
renderMode RenderMode Per-view render strategy. Defaults to the model's renderMode.
refreshMode RefreshMode Per-view refresh strategy. Defaults to the model's refreshMode.

Key Methods

// Add a view
dashModel.addView('chartId');                    // Add to root
dashModel.addView('chartId', stackContainer);    // Add to specific stack
dashModel.addView('chartId', container, 2);      // Add at index

// Remove a view
dashModel.removeView(viewModelId);

// Rename a view (opens inline editor on the tab)
dashModel.renameView(viewModelId);

// Get view spec or model
dashModel.getViewSpec('chartId');
dashModel.getViewModel(viewModelId);

// Restore initial state (async β€” destroys and recreates GoldenLayout)
await dashModel.restoreDefaultsAsync();

// Load specific state (async)
await dashModel.loadStateAsync(newState);

DashCanvas

Basic Usage

import {dashCanvas, DashCanvasModel} from '@xh/hoist/desktop/cmp/dash';

const canvasModel = new DashCanvasModel({
    viewSpecs: [
        {
            id: 'kpi',
            title: 'KPI Card',
            content: () => kpiCard(),
            width: 3,
            height: 2,
            minWidth: 2,
            minHeight: 1
        },
        {
            id: 'chart',
            title: 'Chart',
            content: () => chartWidget(),
            width: 6,
            height: 4
        }
    ],
    initialState: [
        {viewSpecId: 'kpi', layout: {x: 0, y: 0, w: 3, h: 2}, title: 'Revenue'},
        {viewSpecId: 'kpi', layout: {x: 3, y: 0, w: 3, h: 2}, title: 'Orders'},
        {viewSpecId: 'chart', layout: {x: 0, y: 2, w: 6, h: 4}}
    ],
    columns: 12,
    rowHeight: 50
});

dashCanvas({model: canvasModel})

DashCanvasModel Config

Option Type Description
viewSpecs DashCanvasViewSpec[] Available view templates. Required.
viewSpecDefaults Partial<ViewSpec> Properties to merge deeply into all viewSpecs.
initialState DashCanvasItemState[] Initial layout. Default [].
columns number Grid columns. Default 12.
rowHeight number Row height in pixels. Default 50.
compact boolean | 'vertical' | 'horizontal' | 'wrap' Compaction mode. true defaults to 'vertical'. Use 'wrap' with caution β€” it only works well if all items are 1 row high. Default 'vertical'.
margin [x, y] Gap between items in pixels. Default [10, 10].
containerPadding [x, y] Outer padding in pixels. Defaults to same as margin.
maxRows number Maximum row count. Default Infinity.
showGridBackground boolean Show grid lines behind widgets. Default false.
allowsDrop boolean Accept external drag-and-drop from a DashCanvasWidgetChooser or similar component. Default false.
onDropDone (vm: DashCanvasViewModel) => void Callback after a view is successfully dropped onto the canvas.
onDropDragOver (e: DragEvent) => OnDropDragOverResult Customize the dropping placeholder size. Return false to prevent drop.
showAddViewButtonWhenEmpty boolean Show an overlay with an Add View button when the canvas is empty. Default true.
layoutLocked boolean Prevent drag/resize. Default false.
contentLocked boolean Prevent adding/removing views. Default false.
renameLocked boolean Prevent renaming views. Default false.
persistWith PersistOptions Persistence configuration. Default persistence path is 'dashCanvas'.
emptyText string Placeholder text when empty. Default 'No widgets have been added.'
addViewButtonText string Button label in empty overlay. Default 'Add Widget'.
extraMenuItems MenuItemLike[] Additional context menu items shown below the 'Add' action.

DashCanvasModel.defaults exposes layout properties (columns, rowHeight, margin, containerPadding, maxRows, showGridBackground) for app-wide override β€” see DashCanvasModelDefaults for details.

State Structure

Canvas state is a flat array of widget positions:

[
    {
        viewSpecId: 'kpi',
        layout: {x: 0, y: 0, w: 3, h: 2},  // Grid coordinates
        title: 'Revenue KPI'                 // Optional custom title
    },
    {
        viewSpecId: 'chart',
        layout: {x: 3, y: 0, w: 6, h: 4},
        state: {chartType: 'line'}           // Optional view-specific state
    }
]

DashCanvasViewSpec

Extends DashViewSpec with:

Option Type Description
width number Default width in columns. Default 5.
height number Default height in rows. Default 5.
minWidth number Minimum width in columns.
maxWidth number Maximum width in columns.
minHeight number Minimum height in rows.
maxHeight number Maximum height in rows.
autoHeight boolean Auto-size height to content. Default false.
hidePanelHeader boolean Hide the widget's panel header. Default false.
hideMenuButton boolean Hide the menu button in the widget header. Default false.
allowDuplicate boolean Allow duplicating this widget via the context menu. Default true.

Key Methods

// Add a view (returns the new DashCanvasViewModel)
canvasModel.addView('chartId');
canvasModel.addView('chartId', {position: 'first'});         // At top of canvas
canvasModel.addView('chartId', {position: 'last'});          // At bottom of canvas
canvasModel.addView('chartId', {position: existingViewId});  // At another view's position
canvasModel.addView('chartId', {title: 'Custom', width: 8, height: 4, state: {type: 'bar'}});

// Remove or replace a view
canvasModel.removeView(viewModelId);
canvasModel.replaceView(viewModelId, 'newSpecId');  // Keeps existing layout position

// Clear all views
canvasModel.clear();

// Rename a view (shows prompt dialog)
canvasModel.renameView(viewModelId);

// Scroll a view into the viewport
canvasModel.ensureViewVisible(viewModelId);

// Restore initial state (sync β€” unlike DashContainer's async version)
canvasModel.restoreDefaults();

// Grid info
canvasModel.columns;  // Number of columns
canvasModel.rows;     // Current row count
canvasModel.isEmpty;  // No widgets?

DashCanvasWidgetChooser

DashCanvasWidgetChooser is a companion component that renders a draggable list of available view specs for a DashCanvas. Users can drag widgets from the chooser and drop them onto the canvas. It is typically placed inside a collapsible panel alongside the canvas.

Available view specs are listed in their defined order, grouped by their groupName property if present. Specs with unique: true that already have an instance on the canvas are automatically hidden from the chooser.

Requirements

The target DashCanvasModel must be configured with allowsDrop: true to accept drops.

Usage

import {
    dashCanvas, DashCanvasModel,
    dashCanvasWidgetChooser
} from '@xh/hoist/desktop/cmp/dash';

const canvasModel = new DashCanvasModel({
    allowsDrop: true,
    onDropDone: vm => console.log('Dropped:', vm.viewSpec.id),
    viewSpecs: [...],
    ...
});

// Render the chooser alongside the canvas, passing the model as a prop
hbox(
    panel({
        title: 'Widgets',
        modelConfig: {side: 'left', defaultSize: 200, collapsible: true},
        item: dashCanvasWidgetChooser({dashCanvasModel: canvasModel})
    }),
    dashCanvas({model: canvasModel})
);

Common Features

ViewSpec Base Config

Both dash types share these base properties on their ViewSpecs:

Option Type Description
id string Unique identifier. Required.
content Content View content to render. Required.
title string Default display title. Defaults to startCase(id).
icon ReactElement Display icon.
groupName string Group name for the "Add" menu β€” specs with the same group are nested into a submenu.
omit Thunkable<boolean> Skip this spec entirely. References in state are quietly dropped. Useful for role-based exclusion. Default false.
unique boolean Only one instance allowed at a time. Default false.
allowAdd boolean Can add new instances via the menu. Existing instances in state are still loaded. Default true.
allowRemove boolean Can remove instances. Default true.
allowRename boolean Can rename instances. Default true.

DashViewModel

DashViewModel is the model for each active view instance. Content rendered within a dash view can look up this model to access and modify view-level state. It is not created directly β€” instances are produced automatically from ViewSpecs and state.

A primary interaction with DashViewModel within application code is to use it to configure persistence.

Key properties:

Property Description
id Unique instance identifier.
viewSpec The DashViewSpec used to create this view.
containerModel The parent DashContainerModel or DashCanvasModel.
title Display title β€” @bindable, persisted. Initialized from viewSpec.
titleDetails Dynamic suffix appended after title. Not persisted.
fullTitle Computed: title + titleDetails, space-separated.
icon Display icon β€” @bindable. Initialized from viewSpec.
viewState PlainObject for custom view-specific state β€” @bindable, persisted.
isActive Whether the view is currently visible (e.g. the active tab in a stack).
extraMenuItems MenuItemLike[] for additional context menu items specific to this view.
renderMode Resolved from viewSpec, falling back to the container model.
refreshMode Resolved from viewSpec, falling back to the container model.

Key methods:

// Update a single key within viewState (immutable update)
viewModel.setViewStateKey('chartType', 'bar');

DashCanvasViewModel

DashCanvasViewModel extends DashViewModel with additional canvas-specific properties:

Property Description
headerItems ReactNode[] β€” additional items to display in the widget's panel header. Useful for injecting controls like column chooser buttons.
hidePanelHeader Whether to hide this widget's panel header. Initialized from viewSpec.
hideMenuButton Whether to hide this widget's header menu button. Initialized from viewSpec.
autoHeight Whether the widget resizes its height to fit content. Initialized from viewSpec.
allowDuplicate Whether the widget can be duplicated via the context menu. Initialized from viewSpec.
// Scroll a canvas view into the viewport
viewModel.ensureVisible();

Locking

Control user interactions at the model level:

const dashModel = new DashContainerModel({
    layoutLocked: false,   // Can drag/resize?
    contentLocked: false,  // Can add/remove views?
    renameLocked: false,   // Can rename views?
    ...
});

// Dynamic locking β€” all three properties are @bindable
dashModel.layoutLocked = true;
dashModel.contentLocked = true;

Persistence

Dashboards support two complementary levels of persistence:

  1. Layout persistence β€” The DashModel persists which views are present and how they are arranged (positions, sizes, tabs). This is the "outer" state.
  2. Widget state persistence β€” Individual widget models persist their own internal settings (selected metric, filter values, sort order, column visibility) through their DashViewModel. This is the "inner" state that makes each view instance unique.

Both levels work together: when a user adds two instances of the same widget side by side and configures them differently (e.g. one showing Revenue, the other showing Orders), both the layout and each widget's settings are persisted. This is a core reason dashboards are useful β€” they give users a customizable workspace with persistent, per-widget configuration.

See Persistence for full details on Hoist's persistence system, including the DashViewProvider and how it fits alongside other persistence providers.

Layout Persistence

Enable layout persistence by providing persistWith on the model config:

const dashModel = new DashContainerModel({
    persistWith: {localStorageKey: 'dashboard'},
    ...
});

The default persistence path is 'dashContainer' for DashContainerModel and 'dashCanvas' for DashCanvasModel. Override with {...persistWith, path: 'custom'} when multiple dash models share a persistence provider.

Widget State Persistence

Widget models persist their state by targeting their DashViewModel as the persistence provider. Under the hood, Hoist's DashViewProvider reads and writes to DashViewModel.viewState β€” a PlainObject that the parent DashModel rolls into its own persisted state automatically. This creates a hierarchical persistence chain:

Widget model β†’ DashViewModel.viewState β†’ DashModel.state β†’ localStorage / pref / ViewManager

This wiring must happen in onLinked() because the DashViewModel is resolved via @lookup, which requires the model to be linked into the component tree first. This is an example of deferred persistence setup β€” persistence cannot be configured at construction time because the required context (the view model) is not yet available.

class MetricWidgetModel extends HoistModel {
    @lookup(() => DashViewModel) viewModel: DashViewModel;

    @bindable metric: string = 'hours';
    @bindable showPrior: boolean = true;
    @managed filterModel: FilterChooserModel;

    override onLinked() {
        super.onLinked();

        // 1. Point persistence at this widget's DashViewModel
        this.persistWith = {dashViewModel: this.viewModel};

        // 2. Mark individual @bindable properties for persistence
        this.markPersist('metric');
        this.markPersist('showPrior');

        // 3. Pass the same persistWith to child models that also need persistence.
        //    Here the FilterChooserModel will use the DashViewModel to persist its *primary value*.
        //    It has been further customized to save *user favorite filters* into a user preference,
        //    a common pattern to allow a user to access their favorites from any instance.
        this.filterModel = new FilterChooserModel({
            persistWith: {...this.persistWith, persistFavorites: {prefKey: 'metricFilters'}}
            // ...other configs
        });
    }
}

Key points:

  • Call super.onLinked() before setting up persistence.
  • Set this.persistWith before calling markPersist() β€” markPersist reads from it.
  • Child models (GridModel, FilterChooserModel, PanelModel, etc.) can share the same persistWith, using path to namespace their state within the view's state object.

Context Menus

Right-click on tabs (DashContainer) or widget headers (DashCanvas) shows a context menu for adding, removing, and renaming views. The menu respects all locking and permission flags.

Custom menu items can be added at two levels:

  • Model-level: extraMenuItems in the model config β€” shown in all context menus.
  • View-level: Set viewModel.extraMenuItems β€” shown only in that view's menu.

A common pattern is adding a "Restore Default Layout" item:

const dashModel = new DashCanvasModel({
    extraMenuItems: [
        {
            text: 'Restore Default Layout',
            icon: Icon.reset(),
            actionFn: () => dashModel.restoreDefaults()
        }
    ],
    ...
});

Render and Refresh Modes

DashContainer supports render/refresh modes like TabContainer. These control when views are mounted and refreshed as the user switches between tabs:

const dashModel = new DashContainerModel({
    renderMode: 'lazy',        // Mount on first show (default)
    refreshMode: 'onShowLazy', // Refresh when shown if stale (default)
    viewSpecs: [
        {
            id: 'heavy',
            renderMode: 'unmountOnHide',  // Per-view override
            content: heavyPanel
        }
    ]
});

Widget Content Patterns

Accessing the DashViewModel

Widget content rendered inside a dash view can look up its DashViewModel (or DashCanvasViewModel) via @lookup. This is how widgets access their title, viewState, persistence, and other view-level configuration:

class MyWidgetModel extends HoistModel {
    @lookup(() => DashViewModel) viewModel: DashViewModel;

    // For DashCanvas-specific features (headerItems, etc.)
    @lookup(() => DashCanvasViewModel) canvasViewModel: DashCanvasViewModel;
}

Because @lookup resolves from the component tree, the view model is not available at construction time β€” only after onLinked(). This is why persistence setup, child model creation, and other initialization that depends on the view model must happen in onLinked(), not in the constructor. See the Widget State Persistence section for the full pattern.

Dynamic Titles

Widgets often update their title to reflect current state β€” e.g. showing the selected metric or time period:

override onLinked() {
    super.onLinked();
    this.addReaction({
        track: () => [this.metric, this.period],
        run: () => {
            this.viewModel.title = `${this.displayMetric} ${this.period.toUpperCase()}`;
        }
    });
}

Injecting Header Items (DashCanvas)

DashCanvas widgets can inject buttons or controls into their panel header via DashCanvasViewModel.headerItems:

override onLinked() {
    super.onLinked();
    if (this.viewModel instanceof DashCanvasViewModel) {
        this.viewModel.headerItems = [
            colChooserButton({gridModel: this.gridModel}),
            modalToggleButton({panelModel: this.panelModel})
        ];
    }
}

Conditional ViewSpecs

Use omit to conditionally include viewSpecs based on user roles or other runtime conditions. omit is Thunkable, so it can be a static boolean or a function evaluated at render time:

viewSpecs: [
    {id: 'info', title: 'Basic Info', content: infoWidget},
    {id: 'admin', title: 'Admin Tools', content: adminWidget, omit: !XH.getUser().isHoistAdmin},
    {id: 'forecast', title: 'Forecast', content: forecastWidget, omit: !XH.getConf('enableForecasts')}
]

Common Patterns

Dashboard in a Model

class WorkspaceModel extends HoistModel {
    @managed dashModel = new DashContainerModel({
        viewSpecs: this.buildViewSpecs(),
        initialState: this.defaultLayout,
        showMenuButton: true,
        persistWith: {localStorageKey: 'workspace.dash'}
    });

    private buildViewSpecs(): DashContainerViewSpec[] {
        return [
            {id: 'portfolio', title: 'Portfolio', icon: Icon.portfolio(), content: portfolioGrid},
            {id: 'positions', title: 'Positions', icon: Icon.list(), content: positionsGrid},
            {id: 'chart', title: 'Chart', icon: Icon.chartLine(), content: priceChart}
        ];
    }

    private get defaultLayout() {
        return [{
            type: 'row',
            content: [
                {type: 'stack', content: [
                    {type: 'view', id: 'portfolio'},
                    {type: 'view', id: 'positions'}
                ]},
                {type: 'view', id: 'chart'}
            ]
        }];
    }
}

Dashboard with ViewManager

For dashboards that support named, saveable, and shareable views, persist through a ViewManagerModel. The ViewManagerModel is typically created in AppModel and accessed via XH.appModel:

class WorkspaceModel extends HoistModel {
    @managed dashModel: DashContainerModel;

    constructor() {
        super();
        this.persistWith = {viewManagerModel: XH.appModel.workspaceViewManager};
        this.dashModel = new DashContainerModel({
            viewSpecs: this.buildViewSpecs(),
            initialState: this.defaultLayout,
            showMenuButton: true,
            persistWith: this.persistWith
        });
    }

    private buildViewSpecs(): DashContainerViewSpec[] {
        return [
            {id: 'portfolio', title: 'Portfolio', icon: Icon.portfolio(), content: portfolioGrid},
            {id: 'positions', title: 'Positions', icon: Icon.list(), content: positionsGrid},
            {id: 'chart', title: 'Chart', icon: Icon.chartLine(), content: priceChart}
        ];
    }

    private get defaultLayout() {
        return [{
            type: 'row',
            content: [
                {type: 'stack', content: [
                    {type: 'view', id: 'portfolio'},
                    {type: 'view', id: 'positions'}
                ]},
                {type: 'view', id: 'chart'}
            ]
        }];
    }
}

DashContainer in a Collapsible Panel

A common layout pattern: a primary grid with a collapsible detail panel containing a DashContainer for flexible detail views:

const detailPanel = hoistCmp.factory<ReportModel>(({model}) => {
    return panel({
        collapsedTitle: 'Details',
        collapsedIcon: Icon.detail(),
        compactHeader: true,
        modelConfig: {
            side: 'bottom',
            defaultSize: '30%',
            persistWith: {localStorageKey: 'detailPanelModel'}
        },
        item: dashContainer()
    });
});

Dynamic Locking

Lock dashboard editing based on runtime conditions such as a user preference or admin setting:

// Observbable flag on model to control editability
@bindable lockDashEditing: boolean = false;

// Then in constructor or onLinked()
this.addReaction({
    track: () => this.lockDashEditing,
    run: lockEditing => {
        const {dashModel} = this;
        dashModel.contentLocked = lockEditing;
        dashModel.layoutLocked = lockEditing;
    },
    fireImmediately: true
});

Widget Dashboard with Multiple Instances

DashCanvas supports multiple instances of the same viewSpec, each with their own title and state. This is useful for KPI dashboards or configurable widgets:

const widgetDash = new DashCanvasModel({
    viewSpecs: [
        {
            id: 'kpi',
            title: 'KPI',
            width: 3,
            height: 2,
            content: kpiWidget
        }
    ],
    initialState: [
        {viewSpecId: 'kpi', layout: {x: 0, y: 0, w: 3, h: 2}, title: 'Revenue',
            state: {metric: 'revenue', period: 'mtd'}},
        {viewSpecId: 'kpi', layout: {x: 3, y: 0, w: 3, h: 2}, title: 'Orders',
            state: {metric: 'orderCount', period: 'mtd'}},
        {viewSpecId: 'kpi', layout: {x: 6, y: 0, w: 3, h: 2}, title: 'Margin',
            state: {metric: 'margin', period: 'ytd'}}
    ],
    columns: 12,
    rowHeight: 80
});

Each instance receives its own viewState via DashViewModel, which the widget model reads to determine what metric to display. Users can then further customize each widget, with those changes persisted back through the same viewState.

Note: For production dashboards, consider using a ViewManagerModel instead of hard-coding a detailed initialState. Admins can design the desired widget layout and settings in a global view that is then set as the default for all users β€” providing the same starting point without baking it into code, and allowing it to be updated without a deploy. Use initialState for simpler or more constrained use cases, e.g. a detail dashboard with a small number of widgets.

viewSpecDefaults

Apply shared config to all viewSpecs. Merges deeply, so individual viewSpecs can override:

const dashModel = new DashContainerModel({
    viewSpecDefaults: {
        unique: true,
        allowRename: false,
        icon: Icon.grid()
    },
    viewSpecs: [
        {id: 'orders', title: 'Orders', content: ordersGrid},
        {id: 'chart', title: 'Chart', icon: Icon.chartLine(), content: priceChart}  // overrides icon
    ]
});

Common Pitfalls

Loading DashContainer state is destructive

Calling loadStateAsync destroys and recreates the entire GoldenLayout instance, including all view components. Avoid calling it frequently β€” use persistence and restoreDefaultsAsync instead of programmatically swapping layouts. If you need dynamic content within a view, use viewState rather than changing the layout structure.

Persisted state references obsolete viewSpecs

When removing a viewSpec from your code, any persisted state referencing it will log a warning and skip that view gracefully. However, users may see unexpected empty spaces in their layout. Consider providing a "Restore Default Layout" menu item via extraMenuItems so users can reset when their layout becomes stale.

restoreDefaults is sync for DashCanvas, async for DashContainer

DashCanvasModel's restoreDefaults() runs synchronously and returns void. DashContainerModel's restoreDefaultsAsync() is async and returns a Promise β€” it must destroy and recreate GoldenLayout. Await it if you need to take action after the layout is rebuilt.

Key Source Files

File Description
desktop/cmp/dash/DashModel.ts Abstract base β€” shared state, locking, refresh context.
desktop/cmp/dash/DashViewModel.ts Per-view model β€” title, icon, viewState, active state.
desktop/cmp/dash/DashViewSpec.ts Base ViewSpec interface β€” id, content, permissions.
desktop/cmp/dash/DashConfig.ts Base config interface β€” shared constructor options.
desktop/cmp/dash/container/DashContainerModel.ts GoldenLayout integration, state management, addView/removeView.
desktop/cmp/dash/container/DashContainer.ts Component factory for rendering a DashContainer.
desktop/cmp/dash/canvas/DashCanvasModel.ts react-grid-layout integration, grid positioning, addView/replaceView.
desktop/cmp/dash/canvas/DashCanvasViewModel.ts Canvas-specific view model β€” headerItems, autoHeight, ensureVisible.
desktop/cmp/dash/canvas/DashCanvas.ts Component factory for rendering a DashCanvas.
desktop/cmp/dash/canvas/widgetchooser/DashCanvasWidgetChooser.ts Draggable widget chooser for adding views to a DashCanvas via drag-and-drop.
desktop/cmp/dash/canvas/widgetchooser/DashCanvasWidgetChooserModel.ts Model for DashCanvasWidgetChooser β€” manages drag events and DashCanvasModel integration.
core/persist/provider/DashViewProvider.ts PersistenceProvider that reads/writes to DashViewModel.viewState.

Related Packages

  • /cmp/tab/ β€” TabContainerModel for similar render/refresh patterns
  • /cmp/viewmanager/ β€” ViewManagerModel for named view persistence
  • /core/ β€” Persistable interface, RenderMode, RefreshMode
  • /desktop/cmp/panel/ β€” Panel container used to wrap dashboard content