Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Persistence Manager #733

Closed
wants to merge 18 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 15 additions & 1 deletion client-app/src/examples/portfolio/AppModel.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,33 @@
import {XH} from '@xh/hoist/core';
import {managed, XH} from '@xh/hoist/core';
import {themeAppOption, sizingModeAppOption} from '@xh/hoist/desktop/cmp/appOption';
import {Icon} from '@xh/hoist/icon';
import {PortfolioService} from '../../core/svc/PortfolioService';
import {BaseAppModel} from '../../BaseAppModel';
import {ViewManagerModel} from '@xh/hoist/core/persist/viewManager';

export const PERSIST_MAIN = {localStorageKey: 'portfolioAppMainState'};
export const PERSIST_DETAIL = {localStorageKey: 'portfolioAppDetailState'};

export class AppModel extends BaseAppModel {
static instance: AppModel;

@managed viewManagerModel;

override async initAsync() {
await super.initAsync();
await XH.installServicesAsync(PortfolioService);

this.viewManagerModel = await ViewManagerModel.createAsync({
entity: {
name: 'PortfolioExample',
displayName: 'View'
},
canManageGlobal: () => XH.getUser().hasRole('GLOBAL_VIEW_MANAGER'),
persistWith: {prefKey: 'portfolioExample'},
enableDefault: true,
enableAutoSave: true
});

this.addReaction({
track: () => XH.webSocketService.connected,
run: () => this.updateWebsocketAlertBanner()
Expand Down
13 changes: 6 additions & 7 deletions client-app/src/examples/portfolio/GridPanel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@ import {relativeTimestamp} from '@xh/hoist/cmp/relativetimestamp';
import {refreshButton} from '@xh/hoist/desktop/cmp/button';
import {groupingChooser} from '@xh/hoist/desktop/cmp/grouping';
import {panel} from '@xh/hoist/desktop/cmp/panel';
import {viewManager} from '@xh/hoist/desktop/cmp/viewManager';
import {Icon} from '@xh/hoist/icon';
import {GridPanelModel} from './GridPanelModel';
import {PERSIST_MAIN} from './AppModel';

export const gridPanel = hoistCmp.factory({
model: uses(GridPanelModel),
Expand All @@ -16,15 +16,14 @@ export const gridPanel = hoistCmp.factory({
const {collapsedTitle} = model;

return panel({
modelConfig: {
defaultSize: 500,
side: 'left',
persistWith: {...PERSIST_MAIN, path: 'positionsPanel'}
},
model: model.panelModel,
collapsedTitle,
collapsedIcon: Icon.treeList(),
compactHeader: true,
tbar: [groupingChooser({flex: 1, icon: Icon.treeList()})],
tbar: [
viewManager({viewMenuProps: {}}),
groupingChooser({flex: 1, icon: Icon.treeList()})
],
item: grid({agOptions: {groupDefaultExpanded: 1}}),
bbar: [
gridCountLabel({unit: 'position'}),
Expand Down
46 changes: 38 additions & 8 deletions client-app/src/examples/portfolio/GridPanelModel.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
import {HoistModel, managed} from '@xh/hoist/core';
import {bindable, makeObservable} from '@xh/hoist/mobx';
import {PanelModel} from '@xh/hoist/desktop/cmp/panel';
import {action, bindable, makeObservable} from '@xh/hoist/mobx';
import {GridModel, TreeStyle} from '@xh/hoist/cmp/grid';
import {PERSIST_MAIN} from './AppModel';
import {mktValCol, nameCol, pnlCol} from '../../core/columns';
import {PortfolioPanelModel} from './PortfolioPanelModel';
import {capitalize} from 'lodash';

export class GridPanelModel extends HoistModel {
@bindable loadTimestamp: number;

@managed
gridModel: GridModel;
@managed gridModel: GridModel;
@managed panelModel: PanelModel;

parentModel: PortfolioPanelModel;

Expand All @@ -22,16 +22,46 @@ export class GridPanelModel extends HoistModel {
return this.parentModel.groupingChooserModel.value.map(it => capitalize(it)).join(' › ');
}

constructor({parentModel}) {
constructor({persistWith, parentModel}) {
super();
makeObservable(this);
this.parentModel = parentModel;
this.gridModel = this.createGridModel();
this.gridModel = this.createGridModel(persistWith);
this.panelModel = this.createPanelModel(persistWith);
}

private createGridModel() {
@action
updateState(newState) {
const {gridModel} = this;
const gridPm = gridModel.persistenceModel;
gridPm.patchState(newState.portfolioGrid);
gridPm.updateGridColumns();
gridPm.updateGridSort();

this.panelModel.size = newState.positionsPanel.size;
this.panelModel.collapsed = newState.positionsPanel.collapsed;
}

async clearStateAsync() {
await this.gridModel.restoreDefaultsAsync({skipWarning: true});
this.panelModel.size = this.panelModel.defaultSize;
}

//------------------
// Implementation
//------------------

private createPanelModel(persistWith) {
return new PanelModel({
defaultSize: 500,
side: 'left',
persistWith: {path: 'positionsPanel', ...persistWith}
});
}

private createGridModel(persistWith) {
return new GridModel({
persistWith: PERSIST_MAIN,
persistWith: {path: 'portfolioGrid', ...persistWith},
treeMode: true,
treeStyle: TreeStyle.HIGHLIGHTS_AND_BORDERS,
sortBy: 'pnl|desc|abs',
Expand Down
12 changes: 7 additions & 5 deletions client-app/src/examples/portfolio/PortfolioPanel.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,20 @@
import {hframe} from '@xh/hoist/cmp/layout';
import {hframe, placeholder} from '@xh/hoist/cmp/layout';
import {creates, hoistCmp} from '@xh/hoist/core';
import {panel} from '@xh/hoist/desktop/cmp/panel';
import {detailPanel} from './detail/DetailPanel';
import {gridPanel} from './GridPanel';
import {mapPanel} from './MapPanel';
import {PortfolioPanelModel} from './PortfolioPanelModel';

export const portfolioPanel = hoistCmp.factory({
export const portfolioPanel = hoistCmp.factory<PortfolioPanelModel>({
model: creates(PortfolioPanelModel),

render() {
render({model}) {
return panel({
mask: 'onLoad',
items: [hframe(gridPanel(), mapPanel()), detailPanel()]
mask: [model.loadModel, model.initTask],
items: model.viewManagerModel
? [hframe(gridPanel(), mapPanel()), detailPanel()]
: placeholder()
});
}
});
93 changes: 86 additions & 7 deletions client-app/src/examples/portfolio/PortfolioPanelModel.ts
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In general, this is an annoying amount of boilerplate you had to write! I wonder if there'd be a way to generalize this and build a higher order component / model to avoid some of this. Just brainstorming - what if the PersistenceManager component was a container that accepted a single child component that it would not render until it was done initializing. Big issue here is that ideally the child would create its model, so that it would not be constructed until the manager finished initializing. BUT we would need a reference to that model in order to wire it. Wonder if we could use modelRef to do that... but we wouldn't want whatever we do to be even more complicated / harder to reason about than what you have here...

Sorry for the flight of ideas - will think more on this, and maybe we can brainstorm together.

Original file line number Diff line number Diff line change
@@ -1,33 +1,69 @@
import {HoistModel, managed, XH} from '@xh/hoist/core';
import {HoistModel, managed, TaskObserver, XH} from '@xh/hoist/core';
import {Store} from '@xh/hoist/data';
import {bindable, makeObservable, observable} from '@xh/hoist/mobx';
import {logInfo} from '@xh/hoist/utils/js';
import {GridPanelModel} from './GridPanelModel';
import {round} from 'lodash';
import {isEmpty, round} from 'lodash';
import {GroupingChooserModel} from '@xh/hoist/cmp/grouping';
import {PERSIST_MAIN} from './AppModel';
import {waitFor} from '@xh/hoist/promise';
import {wait, waitFor} from '@xh/hoist/promise';
import {SECONDS} from '@xh/hoist/utils/datetime';
import {DetailPanelModel} from './detail/DetailPanelModel';
import {AppModel} from './AppModel';
import {ViewManagerModel} from '@xh/hoist/core/persist/viewManager';

export class PortfolioPanelModel extends HoistModel {
@managed session;

@managed groupingChooserModel = this.createGroupingChooserModel();
@managed @observable.ref viewManagerModel: ViewManagerModel =
AppModel.instance.viewManagerModel;
@managed groupingChooserModel: GroupingChooserModel;
@managed store = this.createStore();
@managed gridPanelModel = new GridPanelModel({parentModel: this});
@managed gridPanelModel: GridPanelModel;
@managed detailPanelModel: DetailPanelModel;

@bindable.ref initError: Error;

initTask = TaskObserver.trackAll();

get prefKey(): string {
return 'portfolioExample';
}

get selectedPosition() {
return this.gridPanelModel.selectedRecord;
}

constructor() {
super();
makeObservable(this);
const wsService = XH.webSocketService;

this.groupingChooserModel = this.createGroupingChooserModel();
this.detailPanelModel = this.createDetailPanelModel();
this.gridPanelModel = this.createGridPanelModel();

this.addReaction({
track: () => [this.groupingChooserModel.value, wsService.connected],
run: () => this.loadAsync()
});
this.addReaction({
track: () => this.selectedPosition,
run: position => {
this.detailPanelModel.positionId = position?.id ?? null;
},
debounce: 300
});
this.addReaction({
track: () => this.viewManagerModel.value,
run: value => this.onViewChangeAsync(value),
fireImmediately: true,
debounce: 1000
});
}

override async doLoadAsync(loadSpec) {
if (!this.groupingChooserModel) return;

const wsService = XH.webSocketService,
{store, groupingChooserModel, gridPanelModel} = this,
dims = groupingChooserModel.value;
Expand All @@ -41,6 +77,7 @@ export class PortfolioPanelModel extends HoistModel {
);
if (loadSpec.isStale) return;

if (!dims) return;
session = await XH.portfolioService.getLivePositionsAsync(dims, 'mainApp').catchDefault();

store.loadData([session.initialPositions.root]);
Expand Down Expand Up @@ -76,10 +113,52 @@ export class PortfolioPanelModel extends HoistModel {
}

private createGroupingChooserModel() {
const {viewManagerModel} = this;
return new GroupingChooserModel({
dimensions: ['fund', 'model', 'region', 'sector', 'symbol', 'trader'],
initialValue: ['region', 'sector', 'symbol'],
persistWith: PERSIST_MAIN
persistWith: {viewManagerModel},
allowEmpty: false
});
}

private createGridPanelModel() {
const {viewManagerModel} = this;
return new GridPanelModel({
persistWith: {viewManagerModel},
parentModel: this
});
}

private createDetailPanelModel() {
const {viewManagerModel} = this;
return new DetailPanelModel({
persistWith: {viewManagerModel},
parentModel: this
});
}

private async onViewChangeAsync(value) {
if (!this.groupingChooserModel) return;

const start = Date.now();

await wait(); // allow masking to start

if (isEmpty(value)) {
this.groupingChooserModel.setValue(['region', 'sector', 'symbol']);
await this.gridPanelModel.clearStateAsync();
await this.detailPanelModel.clearStateAsync();
return;
}

const {gridPanelModel, detailPanelModel, groupingChooserModel} = this;
groupingChooserModel.setValue(
value.groupingChooser?.value ?? ['region', 'sector', 'symbol']
);
gridPanelModel.updateState(value);
detailPanelModel.updateState(value);

logInfo(`Rebuilt view | took ${Date.now() - start}ms`, this);
}
}
4 changes: 2 additions & 2 deletions client-app/src/examples/portfolio/detail/DetailPanel.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import {hframe, placeholder} from '@xh/hoist/cmp/layout';
import {hoistCmp, creates} from '@xh/hoist/core';
import {hoistCmp, uses} from '@xh/hoist/core';
import {panel} from '@xh/hoist/desktop/cmp/panel';
import {Icon} from '@xh/hoist/icon/Icon';
import {chartsPanel} from './charts/ChartsPanel';
import {DetailPanelModel} from './DetailPanelModel';
import {ordersPanel} from './OrdersPanel';

export const detailPanel = hoistCmp.factory({
model: creates(DetailPanelModel),
model: uses(DetailPanelModel),

render({model}) {
const {panelSizingModel, positionId} = model;
Expand Down
27 changes: 15 additions & 12 deletions client-app/src/examples/portfolio/detail/DetailPanelModel.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
import {HoistModel, lookup, managed} from '@xh/hoist/core';
import {HoistModel, managed} from '@xh/hoist/core';
import {observable, makeObservable} from '@xh/hoist/mobx';
import {PanelModel} from '@xh/hoist/desktop/cmp/panel';
import {ChartsPanelModel} from './charts/ChartsPanelModel';
import {OrdersPanelModel} from './OrdersPanelModel';
import {PERSIST_DETAIL} from '../AppModel';
import {PortfolioPanelModel} from '../PortfolioPanelModel';

export class DetailPanelModel extends HoistModel {
@observable positionId = null;

@lookup(PortfolioPanelModel) parentModel;
@managed ordersPanelModel = new OrdersPanelModel(this);
@managed ordersPanelModel: OrdersPanelModel;
@managed chartsPanelModel: ChartsPanelModel;

@managed panelSizingModel = new PanelModel({
defaultSize: 400,
Expand All @@ -20,22 +21,24 @@ export class DetailPanelModel extends HoistModel {
persistWith: PERSIST_DETAIL
});

parentModel: PortfolioPanelModel;

get collapsed() {
return this.panelSizingModel.collapsed;
}

constructor() {
constructor({persistWith, parentModel}) {
super();
makeObservable(this);
this.parentModel = parentModel;
this.ordersPanelModel = new OrdersPanelModel({persistWith, parentModel: this});
}

updateState(newState) {
this.ordersPanelModel.updateState(newState);
}

override onLinked() {
this.addReaction({
track: () => this.parentModel.selectedPosition,
run: position => {
this.positionId = position?.id ?? null;
},
debounce: 300
});
async clearStateAsync() {
await this.ordersPanelModel.clearStateAsync();
}
}
1 change: 0 additions & 1 deletion client-app/src/examples/portfolio/detail/OrdersPanel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ export const ordersPanel = hoistCmp.factory({

render({model}) {
const {positionId, loadModel} = model;

return panel({
title: `Orders: ${formatPositionId(positionId)}`,
icon: Icon.edit(),
Expand Down
Loading
Loading