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

Draft
wants to merge 11 commits into
base: develop
Choose a base branch
from
2 changes: 2 additions & 0 deletions client-app/src/Bootstrap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
// App Services -- Import and Register
//-----------------------------------------------------------------
import {XH} from '@xh/hoist/core';
import {PersistenceManagerModel} from '@xh/hoist/core/persist/persistenceManager';
import {when} from '@xh/hoist/mobx';

import {ContactService} from './examples/contact/svc/ContactService';
Expand All @@ -24,6 +25,7 @@ declare module '@xh/hoist/core' {
gitHubService: GitHubService;
portfolioService: PortfolioService;
taskService: TaskService;
persistenceManagerModel: PersistenceManagerModel;
}
// @ts-ignore - Help IntelliJ recognize uses of injected service methods on the `XH` singleton.
export const XH: XHApi;
Expand Down
16 changes: 15 additions & 1 deletion client-app/src/examples/portfolio/AppModel.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import {XH} from '@xh/hoist/core';
import {managed, XH} from '@xh/hoist/core';
import {PersistenceManagerModel} from '@xh/hoist/core/persist/persistenceManager';
import {themeAppOption, sizingModeAppOption} from '@xh/hoist/desktop/cmp/appOption';
import {Icon} from '@xh/hoist/icon';
import {PortfolioService} from '../../core/svc/PortfolioService';
Expand All @@ -10,10 +11,23 @@ export const PERSIST_DETAIL = {localStorageKey: 'portfolioAppDetailState'};
export class AppModel extends BaseAppModel {
static instance: AppModel;

@managed persistenceManagerModel;

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

this.persistenceManagerModel = await PersistenceManagerModel.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
3 changes: 2 additions & 1 deletion client-app/src/examples/portfolio/GridPanel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ 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 {persistenceManager} from '@xh/hoist/desktop/cmp/persistenceManager';
import {Icon} from '@xh/hoist/icon';
import {GridPanelModel} from './GridPanelModel';
import {PERSIST_MAIN} from './AppModel';
Expand All @@ -24,7 +25,7 @@ export const gridPanel = hoistCmp.factory({
collapsedTitle,
collapsedIcon: Icon.treeList(),
compactHeader: true,
tbar: [groupingChooser({flex: 1, icon: Icon.treeList()})],
tbar: [persistenceManager(), groupingChooser({flex: 1, icon: Icon.treeList()})],
item: grid({agOptions: {groupDefaultExpanded: 1}}),
bbar: [
gridCountLabel({unit: 'position'}),
Expand Down
32 changes: 24 additions & 8 deletions client-app/src/examples/portfolio/GridPanelModel.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,14 @@
import {HoistModel, managed} from '@xh/hoist/core';
import {bindable, makeObservable} from '@xh/hoist/mobx';
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;

parentModel: PortfolioPanelModel;

Expand All @@ -22,16 +20,34 @@ 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);
}

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

gridPm.updateGridColumns();
gridPm.updateGridSort();
}

async clearStateAsync() {
await this.gridModel.restoreDefaultsAsync({skipWarning: true});
}

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

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.persistenceManagerModel
? [hframe(gridPanel(), mapPanel()), detailPanel()]
: placeholder()
});
}
});
89 changes: 82 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,67 @@
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 {isNil, 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 {PersistenceManagerModel} from '@xh/hoist/core/persist/persistenceManager';
import {AppModel} from './AppModel';

export class PortfolioPanelModel extends HoistModel {
@managed session;

@managed groupingChooserModel = this.createGroupingChooserModel();
@managed @observable.ref persistenceManagerModel: PersistenceManagerModel =
AppModel.instance.persistenceManagerModel;
@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.persistenceManagerModel.value,
run: value => this.onViewChangeAsync(value)
});
}

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

const wsService = XH.webSocketService,
{store, groupingChooserModel, gridPanelModel} = this,
dims = groupingChooserModel.value;
Expand All @@ -41,6 +75,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 +111,50 @@ export class PortfolioPanelModel extends HoistModel {
}

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

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

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

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

const start = Date.now();

await wait(); // allow masking to start

if (isNil(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 ?? []);
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