@xh/hoist 73.0.0-SNAPSHOT.1745973083869 → 73.0.0-SNAPSHOT.1745976013413

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/CHANGELOG.md +2 -0
  2. package/admin/AdminUtils.ts +0 -5
  3. package/admin/AppModel.ts +5 -17
  4. package/admin/tabs/activity/tracking/ActivityTracking.scss +0 -18
  5. package/admin/tabs/activity/tracking/ActivityTrackingModel.ts +199 -296
  6. package/admin/tabs/activity/tracking/ActivityTrackingPanel.ts +51 -81
  7. package/admin/tabs/activity/tracking/charts/ChartsModel.ts +218 -0
  8. package/admin/tabs/activity/tracking/charts/ChartsPanel.ts +76 -0
  9. package/admin/tabs/activity/tracking/detail/ActivityDetailModel.ts +59 -114
  10. package/admin/tabs/activity/tracking/detail/ActivityDetailView.ts +30 -61
  11. package/admin/tabs/cluster/instances/memory/MemoryMonitorModel.ts +2 -1
  12. package/build/types/admin/AdminUtils.d.ts +0 -2
  13. package/build/types/admin/AppModel.d.ts +1 -4
  14. package/build/types/admin/tabs/activity/tracking/ActivityTrackingModel.d.ts +26 -30
  15. package/build/types/admin/tabs/activity/tracking/charts/ChartsModel.d.ts +34 -0
  16. package/build/types/admin/tabs/activity/tracking/charts/ChartsPanel.d.ts +2 -0
  17. package/build/types/admin/tabs/activity/tracking/detail/ActivityDetailModel.d.ts +1 -13
  18. package/build/types/cmp/form/FormModel.d.ts +40 -17
  19. package/build/types/cmp/form/field/SubformsFieldModel.d.ts +18 -20
  20. package/build/types/core/HoistBase.d.ts +2 -2
  21. package/build/types/data/cube/CubeField.d.ts +5 -4
  22. package/build/types/desktop/cmp/appOption/AutoRefreshAppOption.d.ts +3 -3
  23. package/build/types/desktop/cmp/appOption/ThemeAppOption.d.ts +3 -3
  24. package/cmp/error/ErrorBoundaryModel.ts +1 -1
  25. package/cmp/form/FormModel.ts +112 -20
  26. package/cmp/form/field/SubformsFieldModel.ts +22 -28
  27. package/cmp/grid/impl/GridHScrollbar.ts +2 -1
  28. package/core/HoistBase.ts +12 -12
  29. package/data/cube/CubeField.ts +18 -17
  30. package/package.json +1 -1
  31. package/tsconfig.tsbuildinfo +1 -1
  32. package/admin/tabs/activity/tracking/chart/AggChartModel.ts +0 -218
  33. package/admin/tabs/activity/tracking/chart/AggChartPanel.ts +0 -61
  34. package/admin/tabs/activity/tracking/datafields/DataFieldsEditor.ts +0 -147
  35. package/admin/tabs/activity/tracking/datafields/DataFieldsEditorModel.ts +0 -133
  36. package/build/types/admin/tabs/activity/tracking/chart/AggChartModel.d.ts +0 -33
  37. package/build/types/admin/tabs/activity/tracking/chart/AggChartPanel.d.ts +0 -2
  38. package/build/types/admin/tabs/activity/tracking/datafields/DataFieldsEditor.d.ts +0 -2
  39. package/build/types/admin/tabs/activity/tracking/datafields/DataFieldsEditorModel.d.ts +0 -46
@@ -4,24 +4,22 @@
4
4
  *
5
5
  * Copyright © 2025 Extremely Heavy Industries Inc.
6
6
  */
7
- import {dataFieldsEditor} from '@xh/hoist/admin/tabs/activity/tracking/datafields/DataFieldsEditor';
8
- import {errorMessage} from '@xh/hoist/cmp/error';
9
7
  import {form} from '@xh/hoist/cmp/form';
10
8
  import {grid} from '@xh/hoist/cmp/grid';
11
- import {div, filler, hframe} from '@xh/hoist/cmp/layout';
9
+ import {div, hframe} from '@xh/hoist/cmp/layout';
12
10
  import {creates, hoistCmp} from '@xh/hoist/core';
13
11
  import {button, buttonGroup, colChooserButton, exportButton} from '@xh/hoist/desktop/cmp/button';
12
+ import {errorMessage} from '@xh/hoist/cmp/error';
14
13
  import {filterChooser} from '@xh/hoist/desktop/cmp/filter';
15
14
  import {formField} from '@xh/hoist/desktop/cmp/form';
16
15
  import {groupingChooser} from '@xh/hoist/desktop/cmp/grouping';
17
16
  import {dateInput, DateInputProps, select} from '@xh/hoist/desktop/cmp/input';
18
17
  import {panel} from '@xh/hoist/desktop/cmp/panel';
19
18
  import {toolbar, toolbarSep} from '@xh/hoist/desktop/cmp/toolbar';
20
- import {viewManager} from '@xh/hoist/desktop/cmp/viewmanager';
21
19
  import {Icon} from '@xh/hoist/icon';
22
20
  import {LocalDate} from '@xh/hoist/utils/datetime';
23
21
  import {ActivityTrackingModel} from './ActivityTrackingModel';
24
- import {aggChartPanel} from '@xh/hoist/admin/tabs/activity/tracking/chart/AggChartPanel';
22
+ import {chartsPanel} from './charts/ChartsPanel';
25
23
  import {activityDetailView} from './detail/ActivityDetailView';
26
24
  import './ActivityTracking.scss';
27
25
 
@@ -38,20 +36,14 @@ export const activityTrackingPanel = hoistCmp.factory({
38
36
  return panel({
39
37
  className: 'xh-admin-activity-panel',
40
38
  tbar: tbar(),
41
- items: [filterBar(), hframe(aggregateView(), activityDetailView({flex: 1}))],
39
+ item: hframe(aggregateView(), activityDetailView({flex: 1})),
42
40
  mask: 'onLoad'
43
41
  });
44
42
  }
45
43
  });
46
44
 
47
45
  const tbar = hoistCmp.factory<ActivityTrackingModel>(({model}) => {
48
- const dateBtn = {outlined: true, width: 40} as const;
49
46
  return toolbar(
50
- viewManager({
51
- model: model.viewManagerModel,
52
- showSaveButton: 'always'
53
- }),
54
- '-',
55
47
  form({
56
48
  fieldDefaults: {label: null},
57
49
  items: [
@@ -73,96 +65,74 @@ const tbar = hoistCmp.factory<ActivityTrackingModel>(({model}) => {
73
65
  onClick: () => model.adjustDates('add'),
74
66
  disabled: model.endDay >= LocalDate.currentAppDay()
75
67
  }),
76
- buttonGroup({
77
- items: [
78
- button({
79
- text: '6m',
80
- onClick: () => model.adjustStartDate(6, 'months'),
81
- active: model.isInterval(6, 'months'),
82
- ...dateBtn
83
- }),
84
- button({
85
- text: '1m',
86
- onClick: () => model.adjustStartDate(1, 'months'),
87
- active: model.isInterval(1, 'months'),
88
- ...dateBtn
89
- }),
90
- button({
91
- text: '7d',
92
- onClick: () => model.adjustStartDate(7, 'days'),
93
- active: model.isInterval(7, 'days'),
94
- ...dateBtn
95
- }),
96
- button({
97
- text: '1d',
98
- onClick: () => model.adjustStartDate(1, 'days'),
99
- active: model.isInterval(1, 'days'),
100
- ...dateBtn
101
- })
102
- ]
103
- }),
68
+ buttonGroup(
69
+ button({
70
+ text: '6m',
71
+ outlined: true,
72
+ width: 40,
73
+ onClick: () => model.adjustStartDate(6, 'months'),
74
+ active: model.isInterval(6, 'months')
75
+ }),
76
+ button({
77
+ text: '1m',
78
+ outlined: true,
79
+ width: 40,
80
+ onClick: () => model.adjustStartDate(1, 'months'),
81
+ active: model.isInterval(1, 'months')
82
+ }),
83
+ button({
84
+ text: '7d',
85
+ outlined: true,
86
+ width: 40,
87
+ onClick: () => model.adjustStartDate(7, 'days'),
88
+ active: model.isInterval(7, 'days')
89
+ }),
90
+ button({
91
+ text: '1d',
92
+ outlined: true,
93
+ width: 40,
94
+ onClick: () => model.adjustStartDate(1, 'days'),
95
+ active: model.isInterval(1, 'days')
96
+ })
97
+ ),
104
98
  toolbarSep(),
105
- filterChooserToggleButton(),
99
+ filterChooser({
100
+ flex: 1,
101
+ enableClear: true
102
+ }),
106
103
  toolbarSep(),
107
- dataFieldsEditor(),
108
- filler(),
109
104
  formField({
110
105
  field: 'maxRows',
111
- label: 'Max rows',
106
+ label: 'Max rows:',
112
107
  width: 140,
113
108
  item: select({
114
109
  enableFilter: false,
115
110
  hideDropdownIndicator: true,
116
111
  options: model.maxRowOptions
117
112
  })
113
+ }),
114
+ toolbarSep(),
115
+ button({
116
+ icon: Icon.reset(),
117
+ intent: 'danger',
118
+ title: 'Reset query to defaults',
119
+ onClick: () => model.resetQuery()
118
120
  })
119
121
  ]
120
122
  })
121
123
  );
122
124
  });
123
125
 
124
- const filterChooserToggleButton = hoistCmp.factory<ActivityTrackingModel>(({model}) => {
125
- const {hasFilter, showFilterChooser} = model;
126
-
127
- return button({
128
- text: 'Filter',
129
- icon: Icon.filter({prefix: hasFilter ? 'fas' : 'far'}),
130
- intent: hasFilter ? 'primary' : null,
131
- outlined: showFilterChooser,
132
- onClick: () => model.toggleFilterChooser()
133
- });
134
- });
135
-
136
- const filterBar = hoistCmp.factory<ActivityTrackingModel>(({model}) => {
137
- return model.showFilterChooser
138
- ? toolbar(
139
- filterChooser({
140
- flex: 1,
141
- enableClear: true
142
- })
143
- )
144
- : null;
145
- });
146
-
147
126
  const aggregateView = hoistCmp.factory<ActivityTrackingModel>(({model}) => {
148
127
  return panel({
149
- collapsedTitle: 'Aggregate Activity',
150
- collapsedIcon: Icon.users(),
128
+ title: 'Aggregate Activity Report',
129
+ icon: Icon.users(),
151
130
  compactHeader: true,
152
131
  modelConfig: {
153
132
  side: 'left',
154
- defaultSize: 500,
155
- persistWith: {...model.persistWith, path: 'aggPanel'}
133
+ defaultSize: 500
156
134
  },
157
- tbar: toolbar({
158
- compact: true,
159
- items: [
160
- groupingChooser({flex: 1, maxWidth: 300}),
161
- filler(),
162
- colChooserButton(),
163
- exportButton()
164
- ]
165
- }),
135
+ tbar: [groupingChooser({flex: 1}), colChooserButton(), exportButton()],
166
136
  items: [
167
137
  grid({
168
138
  flex: 1,
@@ -176,7 +146,7 @@ const aggregateView = hoistCmp.factory<ActivityTrackingModel>(({model}) => {
176
146
  ],
177
147
  omit: !model.maxRowsReached
178
148
  }),
179
- aggChartPanel()
149
+ chartsPanel()
180
150
  ]
181
151
  });
182
152
  });
@@ -0,0 +1,218 @@
1
+ /*
2
+ * This file belongs to Hoist, an application development toolkit
3
+ * developed by Extremely Heavy Industries (www.xh.io | info@xh.io)
4
+ *
5
+ * Copyright © 2025 Extremely Heavy Industries Inc.
6
+ */
7
+ import {ChartModel} from '@xh/hoist/cmp/chart';
8
+ import {br, fragment} from '@xh/hoist/cmp/layout';
9
+ import {HoistModel, managed, lookup} from '@xh/hoist/core';
10
+ import {capitalizeWords, fmtDate} from '@xh/hoist/format';
11
+ import {bindable, makeObservable} from '@xh/hoist/mobx';
12
+ import {LocalDate} from '@xh/hoist/utils/datetime';
13
+ import {filter, sortBy, isEmpty} from 'lodash';
14
+ import moment from 'moment';
15
+ import {PanelModel} from '@xh/hoist/desktop/cmp/panel';
16
+ import {ActivityTrackingModel} from '../ActivityTrackingModel';
17
+ import {ONE_DAY} from '@xh/hoist/utils/datetime/DateTimeUtils';
18
+
19
+ export class ChartsModel extends HoistModel {
20
+ @managed panelModel = new PanelModel({
21
+ modalSupport: {width: '90vw', height: '60vh'},
22
+ side: 'bottom',
23
+ defaultSize: 370
24
+ });
25
+
26
+ @lookup(ActivityTrackingModel)
27
+ activityTrackingModel: ActivityTrackingModel;
28
+
29
+ /** metric to chart on Y axis - one of:
30
+ * + entryCount - count of total track log entries within the primary dim group.
31
+ * + count - count of unique secondary dim values within the primary dim group.
32
+ * + elapsed - avg elapsed time in ms for the primary dim group.
33
+ */
34
+ @bindable
35
+ metric: 'entryCount' | 'count' | 'elapsed' = 'entryCount';
36
+
37
+ /** show weekends on the activity chart */
38
+ @bindable
39
+ incWeekends: boolean = false;
40
+
41
+ @managed
42
+ categoryChartModel: ChartModel = new ChartModel({
43
+ highchartsConfig: {
44
+ chart: {type: 'column', animation: false},
45
+ plotOptions: {column: {animation: false}},
46
+ legend: {enabled: false},
47
+ title: {text: null},
48
+ xAxis: {type: 'category', title: {}},
49
+ yAxis: [{title: {text: null}, allowDecimals: false}]
50
+ }
51
+ });
52
+
53
+ @managed
54
+ timeseriesChartModel: ChartModel = new ChartModel({
55
+ highchartsConfig: {
56
+ chart: {type: 'line', animation: false},
57
+ plotOptions: {
58
+ line: {
59
+ events: {
60
+ click: e => this.selectRow(e)
61
+ },
62
+ width: 1,
63
+ animation: false,
64
+ step: 'left'
65
+ }
66
+ },
67
+ legend: {enabled: false},
68
+ title: {text: null},
69
+ xAxis: {
70
+ type: 'datetime',
71
+ title: {},
72
+ units: [
73
+ ['day', [1]],
74
+ ['week', [2]],
75
+ ['month', [1]]
76
+ ],
77
+ labels: {
78
+ formatter: function () {
79
+ return fmtDate(this.value, 'D MMM');
80
+ }
81
+ }
82
+ },
83
+ yAxis: [{title: {text: null}, allowDecimals: false}]
84
+ }
85
+ });
86
+
87
+ get showAsTimeseries(): boolean {
88
+ return this.dimensions[0] === 'day';
89
+ }
90
+
91
+ get chartModel(): ChartModel {
92
+ return this.showAsTimeseries ? this.timeseriesChartModel : this.categoryChartModel;
93
+ }
94
+
95
+ get primaryDim(): string {
96
+ return this.dimensions[0];
97
+ }
98
+
99
+ get secondaryDim(): string {
100
+ const {dimensions} = this;
101
+ return dimensions.length >= 2 ? dimensions[1] : null;
102
+ }
103
+
104
+ get data() {
105
+ const roots = this.activityTrackingModel.gridModel.store.allRootRecords;
106
+ return roots.length ? roots[0].children : [];
107
+ }
108
+
109
+ get dimensions() {
110
+ return this.activityTrackingModel.dimensions;
111
+ }
112
+
113
+ constructor() {
114
+ super();
115
+ makeObservable(this);
116
+ }
117
+
118
+ getLabelForMetric(metric, multiline) {
119
+ switch (metric) {
120
+ case 'count':
121
+ return multiline
122
+ ? fragment(`Unique`, br(), `${this.getUnitsForDim(this.secondaryDim)} Count`)
123
+ : `Unique ${this.getUnitsForDim(this.secondaryDim)} Count`;
124
+ case 'entryCount':
125
+ return multiline ? fragment('Total', br(), 'Entry Count') : 'Total Entry Count';
126
+ case 'elapsed':
127
+ return 'Elapsed ms';
128
+ default:
129
+ return '???';
130
+ }
131
+ }
132
+
133
+ //-----------------
134
+ // Implementation
135
+ //-----------------
136
+
137
+ private selectRow(e) {
138
+ const date = moment(e.point.x).format('YYYY-MM-DD'),
139
+ id = `root>>day=[${date}]`;
140
+ this.activityTrackingModel.gridModel.selectAsync(id);
141
+ }
142
+
143
+ override onLinked() {
144
+ this.addReaction({
145
+ track: () => [this.data, this.metric, this.incWeekends],
146
+ run: () => this.loadChart()
147
+ });
148
+ }
149
+
150
+ private loadChart() {
151
+ const {showAsTimeseries, chartModel, primaryDim} = this,
152
+ series = this.getSeriesData();
153
+
154
+ if (!showAsTimeseries) {
155
+ chartModel.updateHighchartsConfig({
156
+ xAxis: {title: {text: this.getUnitsForDim(primaryDim)}}
157
+ });
158
+ }
159
+
160
+ chartModel.setSeries(series);
161
+ }
162
+
163
+ private getSeriesData() {
164
+ const {data, metric, primaryDim, showAsTimeseries} = this,
165
+ metricLabel = this.getLabelForMetric(metric, false);
166
+ let sortedData = sortBy(data, aggRow => {
167
+ const {cubeLabel} = aggRow.data;
168
+ switch (primaryDim) {
169
+ case 'day':
170
+ return LocalDate.from(cubeLabel).timestamp;
171
+ case 'month':
172
+ return moment(cubeLabel, 'MMM YYYY').valueOf();
173
+ default:
174
+ return cubeLabel;
175
+ }
176
+ }),
177
+ chartData = sortedData.map(aggRow => {
178
+ const {cubeLabel} = aggRow.data,
179
+ xVal = showAsTimeseries ? LocalDate.from(cubeLabel).timestamp : cubeLabel,
180
+ yVal = Math.round(aggRow.data[metric]);
181
+ return [xVal, yVal];
182
+ });
183
+
184
+ // Insert data where no activity was logged
185
+ if (showAsTimeseries) {
186
+ const fillData = [];
187
+ for (let i = 1; i < chartData.length; i++) {
188
+ const skippedDayCount = Math.floor(
189
+ (chartData[i][0] - chartData[i - 1][0]) / ONE_DAY - 1
190
+ );
191
+ if (skippedDayCount > 0) {
192
+ for (let j = 1; j <= skippedDayCount; j++) {
193
+ const skippedDate = chartData[i - 1][0] + j * ONE_DAY;
194
+ fillData.push([skippedDate, 0]);
195
+ }
196
+ }
197
+ }
198
+ if (!isEmpty(fillData)) {
199
+ chartData.push(...fillData);
200
+ chartData = sortBy(chartData, data => data[0]);
201
+ }
202
+
203
+ if (!this.incWeekends) {
204
+ chartData = filter(chartData, data => LocalDate.from(data[0]).isWeekday);
205
+ }
206
+ }
207
+ return [{name: metricLabel, data: chartData}];
208
+ }
209
+
210
+ private getUnitsForDim(dim) {
211
+ return (
212
+ {
213
+ username: 'User',
214
+ msg: 'Message'
215
+ }[dim] ?? capitalizeWords(dim)
216
+ );
217
+ }
218
+ }
@@ -0,0 +1,76 @@
1
+ /*
2
+ * This file belongs to Hoist, an application development toolkit
3
+ * developed by Extremely Heavy Industries (www.xh.io | info@xh.io)
4
+ *
5
+ * Copyright © 2025 Extremely Heavy Industries Inc.
6
+ */
7
+ import {chart} from '@xh/hoist/cmp/chart';
8
+ import {hoistCmp, creates} from '@xh/hoist/core';
9
+ import {button} from '@xh/hoist/desktop/cmp/button';
10
+ import {buttonGroupInput} from '@xh/hoist/desktop/cmp/input';
11
+ import {panel} from '@xh/hoist/desktop/cmp/panel';
12
+ import {Icon} from '@xh/hoist/icon/Icon';
13
+ import {toolbar} from '@xh/hoist/desktop/cmp/toolbar';
14
+ import {checkbox} from '@xh/hoist/desktop/cmp/input';
15
+ import {hspacer} from '@xh/hoist/cmp/layout';
16
+ import {ChartsModel} from './ChartsModel';
17
+
18
+ export const chartsPanel = hoistCmp.factory({
19
+ model: creates(ChartsModel),
20
+ render({model, ...props}) {
21
+ const {chartModel, activityTrackingModel, panelModel} = model,
22
+ {isModal} = panelModel;
23
+ return panel({
24
+ title: !isModal ? 'Aggregate Activity Chart' : activityTrackingModel.queryDisplayString,
25
+ icon: Icon.chartBar(),
26
+ model: panelModel,
27
+ compactHeader: !isModal,
28
+ item: chart({model: chartModel}),
29
+ bbar: bbar(),
30
+ height: '100%',
31
+ ...props
32
+ });
33
+ }
34
+ });
35
+
36
+ const bbar = hoistCmp.factory<ChartsModel>(() =>
37
+ toolbar(metricSwitcher({multiline: true}), hspacer(), incWeekendsCheckbox())
38
+ );
39
+
40
+ const incWeekendsCheckbox = hoistCmp.factory<ChartsModel>(({model}) =>
41
+ checkbox({
42
+ omit: !model.showAsTimeseries,
43
+ bind: 'incWeekends',
44
+ label: 'Inc Wknds'
45
+ })
46
+ );
47
+
48
+ const metricSwitcher = hoistCmp.factory<ChartsModel>(({model, multiline}) => {
49
+ return buttonGroupInput({
50
+ className: 'xh-admin-activity-panel__metric-switcher',
51
+ bind: 'metric',
52
+ outlined: true,
53
+ flex: 2,
54
+ items: [
55
+ button({
56
+ text: model.getLabelForMetric('entryCount', multiline),
57
+ value: 'entryCount',
58
+ outlined: true,
59
+ flex: 1
60
+ }),
61
+ button({
62
+ text: model.getLabelForMetric('count', multiline),
63
+ value: 'count',
64
+ outlined: true,
65
+ flex: 1,
66
+ omit: !model.secondaryDim
67
+ }),
68
+ button({
69
+ text: model.getLabelForMetric('elapsed', multiline),
70
+ value: 'elapsed',
71
+ outlined: true,
72
+ flex: 1
73
+ })
74
+ ]
75
+ });
76
+ });