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

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/admin/AdminUtils.ts +5 -0
  2. package/admin/AppModel.ts +17 -5
  3. package/admin/tabs/activity/tracking/ActivityTracking.scss +18 -0
  4. package/admin/tabs/activity/tracking/ActivityTrackingModel.ts +296 -199
  5. package/admin/tabs/activity/tracking/ActivityTrackingPanel.ts +81 -51
  6. package/admin/tabs/activity/tracking/chart/AggChartModel.ts +218 -0
  7. package/admin/tabs/activity/tracking/chart/AggChartPanel.ts +61 -0
  8. package/admin/tabs/activity/tracking/datafields/DataFieldsEditor.ts +147 -0
  9. package/admin/tabs/activity/tracking/datafields/DataFieldsEditorModel.ts +133 -0
  10. package/admin/tabs/activity/tracking/detail/ActivityDetailModel.ts +114 -59
  11. package/admin/tabs/activity/tracking/detail/ActivityDetailView.ts +61 -30
  12. package/admin/tabs/cluster/instances/memory/MemoryMonitorModel.ts +1 -2
  13. package/build/types/admin/AdminUtils.d.ts +2 -0
  14. package/build/types/admin/AppModel.d.ts +4 -1
  15. package/build/types/admin/tabs/activity/tracking/ActivityTrackingModel.d.ts +30 -26
  16. package/build/types/admin/tabs/activity/tracking/chart/AggChartModel.d.ts +33 -0
  17. package/build/types/admin/tabs/activity/tracking/chart/AggChartPanel.d.ts +2 -0
  18. package/build/types/admin/tabs/activity/tracking/datafields/DataFieldsEditor.d.ts +2 -0
  19. package/build/types/admin/tabs/activity/tracking/datafields/DataFieldsEditorModel.d.ts +46 -0
  20. package/build/types/admin/tabs/activity/tracking/detail/ActivityDetailModel.d.ts +13 -1
  21. package/build/types/cmp/form/FormModel.d.ts +15 -27
  22. package/build/types/cmp/form/field/SubformsFieldModel.d.ts +20 -18
  23. package/build/types/core/HoistBase.d.ts +2 -2
  24. package/build/types/data/cube/CubeField.d.ts +4 -5
  25. package/build/types/desktop/cmp/appOption/AutoRefreshAppOption.d.ts +3 -3
  26. package/build/types/desktop/cmp/appOption/ThemeAppOption.d.ts +3 -3
  27. package/cmp/error/ErrorBoundaryModel.ts +1 -1
  28. package/cmp/form/FormModel.ts +18 -28
  29. package/cmp/form/field/SubformsFieldModel.ts +28 -22
  30. package/cmp/grid/impl/GridHScrollbar.ts +1 -2
  31. package/core/HoistBase.ts +12 -12
  32. package/data/cube/CubeField.ts +17 -18
  33. package/package.json +1 -1
  34. package/promise/Promise.ts +8 -6
  35. package/tsconfig.tsbuildinfo +1 -1
  36. package/admin/tabs/activity/tracking/charts/ChartsModel.ts +0 -218
  37. package/admin/tabs/activity/tracking/charts/ChartsPanel.ts +0 -76
  38. package/build/types/admin/tabs/activity/tracking/charts/ChartsModel.d.ts +0 -34
  39. package/build/types/admin/tabs/activity/tracking/charts/ChartsPanel.d.ts +0 -2
@@ -4,6 +4,7 @@
4
4
  *
5
5
  * Copyright © 2025 Extremely Heavy Industries Inc.
6
6
  */
7
+ import {AppModel} from '@xh/hoist/admin/AppModel';
7
8
  import {XH} from '@xh/hoist/core';
8
9
  import {LocalDate} from '@xh/hoist/utils/datetime';
9
10
 
@@ -21,3 +22,7 @@ export function exportFilename(moduleName: string): string {
21
22
  export function exportFilenameWithDate(moduleName: string): () => string {
22
23
  return () => `${XH.appCode}-${moduleName}-${LocalDate.today()}`;
23
24
  }
25
+
26
+ export function getAppModel<T extends AppModel>() {
27
+ return XH.appModel as T;
28
+ }
package/admin/AppModel.ts CHANGED
@@ -7,22 +7,22 @@
7
7
  import {clusterTab} from '@xh/hoist/admin/tabs/cluster/ClusterTab';
8
8
  import {GridModel} from '@xh/hoist/cmp/grid';
9
9
  import {TabConfig, TabContainerModel} from '@xh/hoist/cmp/tab';
10
- import {HoistAppModel, managed, XH} from '@xh/hoist/core';
10
+ import {ViewManagerModel} from '@xh/hoist/cmp/viewmanager';
11
+ import {HoistAppModel, XH} from '@xh/hoist/core';
11
12
  import {Icon} from '@xh/hoist/icon';
12
13
  import {without} from 'lodash';
13
14
  import {Route} from 'router5';
14
15
  import {activityTrackingPanel} from './tabs/activity/tracking/ActivityTrackingPanel';
16
+ import {clientTab} from './tabs/client/ClientTab';
15
17
  import {generalTab} from './tabs/general/GeneralTab';
16
18
  import {monitorTab} from './tabs/monitor/MonitorTab';
17
19
  import {userDataTab} from './tabs/userData/UserDataTab';
18
- import {clientTab} from './tabs/client/ClientTab';
19
20
 
20
21
  export class AppModel extends HoistAppModel {
21
- static instance: AppModel;
22
-
23
- @managed
24
22
  tabModel: TabContainerModel;
25
23
 
24
+ viewManagerModels: Record<string, ViewManagerModel> = {};
25
+
26
26
  static get readonly() {
27
27
  return !XH.getUser().isHoistAdmin;
28
28
  }
@@ -40,6 +40,11 @@ export class AppModel extends HoistAppModel {
40
40
  GridModel.DEFAULT_AUTOSIZE_MODE = 'managed';
41
41
  }
42
42
 
43
+ override async initAsync() {
44
+ await this.initViewManagerModelsAsync();
45
+ await super.initAsync();
46
+ }
47
+
43
48
  override getRoutes(): Route[] {
44
49
  return [
45
50
  {
@@ -161,4 +166,11 @@ export class AppModel extends HoistAppModel {
161
166
  const appCodes = without(XH.clientApps, XH.clientAppCode, 'mobile');
162
167
  return appCodes.find(it => it === 'app') ?? appCodes[0];
163
168
  }
169
+
170
+ async initViewManagerModelsAsync() {
171
+ this.viewManagerModels.activityTracking = await ViewManagerModel.createAsync({
172
+ type: 'xhAdminActivityTrackingView',
173
+ typeDisplayName: 'View'
174
+ });
175
+ }
164
176
  }
@@ -31,6 +31,24 @@
31
31
  margin-right: var(--xh-pad-half-px);
32
32
  }
33
33
  }
34
+
35
+ &__data-fields-editor {
36
+ width: 50vw;
37
+ min-width: 700px;
38
+ max-width: 1000px;
39
+ min-height: 200px;
40
+
41
+ &__row {
42
+ align-items: flex-start;
43
+ flex: none;
44
+ margin-left: 5px;
45
+ margin-right: 5px;
46
+
47
+ &:first-child {
48
+ margin-top: 5px;
49
+ }
50
+ }
51
+ }
34
52
  }
35
53
 
36
54
  .xh-admin-activity-detail {
@@ -4,30 +4,42 @@
4
4
  *
5
5
  * Copyright © 2025 Extremely Heavy Industries Inc.
6
6
  */
7
- import {exportFilename} from '@xh/hoist/admin/AdminUtils';
7
+ import {exportFilename, getAppModel} from '@xh/hoist/admin/AdminUtils';
8
8
  import * as Col from '@xh/hoist/admin/columns';
9
+ import {
10
+ ActivityTrackingDataFieldSpec,
11
+ DataFieldsEditorModel
12
+ } from '@xh/hoist/admin/tabs/activity/tracking/datafields/DataFieldsEditorModel';
9
13
  import {FilterChooserModel} from '@xh/hoist/cmp/filter';
10
14
  import {FormModel} from '@xh/hoist/cmp/form';
11
- import {GridModel, TreeStyle} from '@xh/hoist/cmp/grid';
15
+ import {ColumnRenderer, ColumnSpec, GridModel, TreeStyle} from '@xh/hoist/cmp/grid';
12
16
  import {GroupingChooserModel} from '@xh/hoist/cmp/grouping';
13
- import {HoistModel, LoadSpec, managed, XH} from '@xh/hoist/core';
17
+ import {HoistModel, LoadSpec, managed, PlainObject, XH} from '@xh/hoist/core';
14
18
  import {Cube, CubeFieldSpec, FieldSpec} from '@xh/hoist/data';
15
- import {fmtNumber} from '@xh/hoist/format';
16
- import {action, computed, makeObservable} from '@xh/hoist/mobx';
19
+ import {dateRenderer, dateTimeSecRenderer, fmtNumber, numberRenderer} from '@xh/hoist/format';
20
+ import {action, computed, makeObservable, observable} from '@xh/hoist/mobx';
17
21
  import {LocalDate} from '@xh/hoist/utils/datetime';
18
- import {compact, isEmpty, round} from 'lodash';
22
+ import {compact, get, isEmpty, isEqual, round} from 'lodash';
19
23
  import moment from 'moment';
20
24
 
21
- export const PERSIST_ACTIVITY = {localStorageKey: 'xhAdminActivityState'};
22
-
23
25
  export class ActivityTrackingModel extends HoistModel {
24
- override persistWith = PERSIST_ACTIVITY;
25
-
26
+ /** FormModel for server-side querying controls. */
26
27
  @managed formModel: FormModel;
27
- @managed groupingChooserModel: GroupingChooserModel;
28
- @managed cube: Cube;
29
- @managed filterChooserModel: FilterChooserModel;
30
- @managed gridModel: GridModel;
28
+
29
+ /** Models for data-handling components - can be rebuilt due to change in dataFields. */
30
+ @managed @observable.ref groupingChooserModel: GroupingChooserModel;
31
+ @managed @observable.ref cube: Cube;
32
+ @managed @observable.ref filterChooserModel: FilterChooserModel;
33
+ @managed @observable.ref gridModel: GridModel;
34
+ @managed dataFieldsEditorModel: DataFieldsEditorModel;
35
+
36
+ /**
37
+ * Optional spec for fields to be extracted from additional `data` returned by track entries
38
+ * and promoted to top-level columns in the grids. Supports dot-delimited paths as names.
39
+ */
40
+ @observable.ref dataFields: ActivityTrackingDataFieldSpec[] = [];
41
+
42
+ @observable showFilterChooser: boolean = false;
31
43
 
32
44
  get enabled(): boolean {
33
45
  return XH.trackService.enabled;
@@ -37,19 +49,15 @@ export class ActivityTrackingModel extends HoistModel {
37
49
  return this.groupingChooserModel.value;
38
50
  }
39
51
 
40
- /**
41
- * Summary of currently active query / filters.
42
- * TODO - include new local filters if feasible, or drop this altogether.
43
- * Formerly summarized server-side filters, but was misleading w/new filtering.
44
- */
45
- get queryDisplayString(): string {
46
- return `${XH.appName} Activity`;
47
- }
48
-
49
52
  get endDay(): LocalDate {
50
53
  return this.formModel.values.endDay;
51
54
  }
52
55
 
56
+ @computed
57
+ get hasFilter(): boolean {
58
+ return !!this.filterChooserModel.value;
59
+ }
60
+
53
61
  get maxRowOptions() {
54
62
  return (
55
63
  XH.trackService.conf.maxRows?.options?.map(rowCount => ({
@@ -69,12 +77,31 @@ export class ActivityTrackingModel extends HoistModel {
69
77
  return this.maxRows === this.cube.store.allCount;
70
78
  }
71
79
 
80
+ // TODO - process two collections - one for agg grid with _agg fields left as-is, another for
81
+ // detail grid and filter that replaces (potentially multiple) agg fields with a single
82
+ // underlying field.
83
+ get dataFieldCols(): ColumnSpec[] {
84
+ return this.dataFields.map(df => ({
85
+ field: df,
86
+ renderer: this.getDfRenderer(df),
87
+ appData: {showInAggGrid: !!df.aggregator}
88
+ }));
89
+ }
90
+
91
+ get viewManagerModel() {
92
+ return getAppModel().viewManagerModels.activityTracking;
93
+ }
94
+
72
95
  private _monthFormat = 'MMM YYYY';
73
- private _defaultDims = ['username'];
74
96
 
75
97
  constructor() {
76
98
  super();
77
99
  makeObservable(this);
100
+
101
+ this.persistWith = {viewManagerModel: this.viewManagerModel};
102
+ this.markPersist('showFilterChooser');
103
+
104
+ // TODO - persist maxRows via FM persistence (to be merged shortly)
78
105
  this.formModel = new FormModel({
79
106
  fields: [
80
107
  {name: 'startDay', initialValue: () => this.defaultStartDay},
@@ -83,140 +110,40 @@ export class ActivityTrackingModel extends HoistModel {
83
110
  ]
84
111
  });
85
112
 
86
- this.cube = new Cube({
87
- fields: [
88
- Col.browser.field,
89
- Col.category.field,
90
- Col.severity.field,
91
- Col.correlationId.field,
92
- Col.data.field,
93
- {...(Col.dateCreated.field as FieldSpec), displayName: 'Timestamp'},
94
- Col.day.field,
95
- Col.dayRange.field,
96
- Col.device.field,
97
- Col.elapsed.field,
98
- Col.entryCount.field,
99
- Col.impersonating.field,
100
- Col.msg.field,
101
- Col.userAgent.field,
102
- Col.username.field,
103
- {name: 'count', type: 'int', aggregator: 'CHILD_COUNT'},
104
- {name: 'month', type: 'string', isDimension: true, aggregator: 'UNIQUE'},
105
- Col.url.field,
106
- Col.instance.field,
107
- Col.appVersion.field,
108
- Col.appEnvironment.field
109
- ] as CubeFieldSpec[]
110
- });
113
+ this.dataFieldsEditorModel = new DataFieldsEditorModel(this);
114
+ this.markPersist('dataFields');
111
115
 
112
- this.filterChooserModel = new FilterChooserModel({
113
- fieldSpecs: [
114
- {field: 'category'},
115
- {field: 'correlationId'},
116
- {field: 'username', displayName: 'User'},
117
- {field: 'device'},
118
- {field: 'browser'},
119
- {
120
- field: 'elapsed',
121
- valueRenderer: v => {
122
- return fmtNumber(v, {
123
- label: 'ms',
124
- formatConfig: {thousandSeparated: false, mantissa: 0}
125
- });
126
- },
127
- fieldType: 'number'
128
- },
129
- {field: 'msg', displayName: 'Message'},
130
- {field: 'data'},
131
- {field: 'userAgent'},
132
- {field: 'url', displayName: 'URL'},
133
- {field: 'instance'},
134
- {field: 'severity'},
135
- {field: 'appVersion'},
136
- {field: 'appEnvironment', displayName: 'Environment'}
137
- ]
138
- });
139
-
140
- this.loadLookupsAsync();
141
-
142
- this.groupingChooserModel = new GroupingChooserModel({
143
- dimensions: this.cube.dimensions,
144
- persistWith: this.persistWith,
145
- initialValue: this._defaultDims
146
- });
147
-
148
- const hidden = true;
149
- this.gridModel = new GridModel({
150
- treeMode: true,
151
- treeStyle: TreeStyle.HIGHLIGHTS_AND_BORDERS,
152
- persistWith: {
153
- ...this.persistWith,
154
- path: 'aggGridModel',
155
- persistSort: false
116
+ this.addReaction(
117
+ {
118
+ track: () => this.dataFields,
119
+ run: () => this.createAndSetCoreModels(),
120
+ fireImmediately: true
156
121
  },
157
- colChooserModel: true,
158
- enableExport: true,
159
- exportOptions: {filename: exportFilename('activity-summary')},
160
- emptyText: 'No activity reported...',
161
- sortBy: ['cubeLabel'],
162
- columns: [
163
- {
164
- field: {
165
- name: 'cubeLabel',
166
- type: 'string',
167
- displayName: 'Tracked Activity'
168
- },
169
- flex: 1,
170
- minWidth: 100,
171
- isTreeColumn: true,
172
- comparator: this.cubeLabelComparator.bind(this)
173
- },
174
- {...Col.username, hidden},
175
- {...Col.category, hidden},
176
- {...Col.device, hidden},
177
- {...Col.browser, hidden},
178
- {...Col.userAgent, hidden},
179
- {...Col.impersonating, hidden},
180
- {...Col.elapsed, headerName: 'Elapsed (avg)', hidden},
181
- {...Col.dayRange, hidden},
182
- {...Col.entryCount},
183
- {field: 'count', hidden},
184
- {...Col.appEnvironment, hidden},
185
- {...Col.appVersion, hidden},
186
- {...Col.url, hidden},
187
- {...Col.instance, hidden}
188
- ]
189
- });
190
-
191
- this.addReaction({
192
- track: () => this.query,
193
- run: () => this.loadAsync(),
194
- debounce: 100
195
- });
196
-
197
- this.addReaction({
198
- track: () => [this.cube.records, this.dimensions],
199
- run: () => this.loadGridAsync(),
200
- debounce: 100
201
- });
122
+ {
123
+ track: () => this.query,
124
+ run: () => this.loadAsync(),
125
+ debounce: 100
126
+ },
127
+ {
128
+ track: () => [this.cube.records, this.dimensions],
129
+ run: () => this.loadGridAsync(),
130
+ debounce: 100
131
+ }
132
+ );
202
133
  }
203
134
 
204
135
  override async doLoadAsync(loadSpec: LoadSpec) {
205
- const {enabled, cube} = this;
136
+ const {enabled, cube, query} = this;
206
137
  if (!enabled) return;
207
138
 
208
139
  try {
209
- const data = await XH.fetchService.postJson({
140
+ const data = await XH.postJson({
210
141
  url: 'trackLogAdmin',
211
- body: this.query,
142
+ body: query,
212
143
  loadSpec
213
144
  });
214
145
 
215
- data.forEach(it => {
216
- it.day = LocalDate.from(it.day);
217
- it.month = it.day.format(this._monthFormat);
218
- it.dayRange = {min: it.day, max: it.day};
219
- });
146
+ data.forEach(it => this.processRawTrackLog(it));
220
147
 
221
148
  await cube.loadDataAsync(data);
222
149
  } catch (e) {
@@ -225,47 +152,23 @@ export class ActivityTrackingModel extends HoistModel {
225
152
  }
226
153
  }
227
154
 
228
- async loadGridAsync() {
229
- const {cube, gridModel, dimensions} = this,
230
- data = cube.executeQuery({
231
- dimensions,
232
- includeRoot: true,
233
- includeLeaves: true
234
- });
235
-
236
- data.forEach(node => this.separateLeafRows(node));
237
- gridModel.loadData(data);
238
- await gridModel.preSelectFirstAsync();
239
- }
240
-
241
- // Cube emits leaves in "children" collection - rename that collection to "leafRows" so we can
242
- // carry the leaves with the record, but deliberately not show them in the tree grid. We only
243
- // want the tree grid to show aggregate records.
244
- separateLeafRows(node) {
245
- if (isEmpty(node.children)) return;
246
-
247
- const childrenAreLeaves = !node.children[0].children;
248
- if (childrenAreLeaves) {
249
- node.leafRows = node.children;
250
- delete node.children;
251
- } else {
252
- node.children.forEach(child => this.separateLeafRows(child));
155
+ @action
156
+ setDataFields(dataFields: ActivityTrackingDataFieldSpec[]) {
157
+ if (!isEqual(dataFields, this.dataFields)) {
158
+ this.dataFields = dataFields ?? [];
253
159
  }
254
160
  }
255
161
 
256
162
  @action
257
- resetQuery() {
258
- const {formModel, filterChooserModel, groupingChooserModel, _defaultDims} = this;
259
- formModel.init();
260
- filterChooserModel.setValue(null);
261
- groupingChooserModel.setValue(_defaultDims);
163
+ toggleFilterChooser() {
164
+ this.showFilterChooser = !this.showFilterChooser;
262
165
  }
263
166
 
264
- adjustDates(dir) {
167
+ adjustDates(dir: 'add' | 'subtract') {
265
168
  const {startDay, endDay} = this.formModel.fields,
266
169
  appDay = LocalDate.currentAppDay(),
267
- start = startDay.value,
268
- end = endDay.value,
170
+ start: LocalDate = startDay.value,
171
+ end: LocalDate = endDay.value,
269
172
  diff = end.diff(start),
270
173
  incr = diff + 1;
271
174
 
@@ -294,7 +197,42 @@ export class ActivityTrackingModel extends HoistModel {
294
197
  return startDay === endDay.subtract(value, unit).nextDay();
295
198
  }
296
199
 
297
- cubeLabelComparator(valA, valB, sortDir, abs, {recordA, recordB, defaultComparator}) {
200
+ getDisplayName(fieldName: string) {
201
+ return fieldName ? (this.cube.store.getField(fieldName)?.displayName ?? fieldName) : null;
202
+ }
203
+
204
+ //------------------
205
+ // Implementation
206
+ //------------------
207
+ private async loadGridAsync() {
208
+ const {cube, gridModel, dimensions} = this,
209
+ data = cube.executeQuery({
210
+ dimensions,
211
+ includeRoot: true,
212
+ includeLeaves: true
213
+ });
214
+
215
+ data.forEach(node => this.separateLeafRows(node));
216
+ gridModel.loadData(data);
217
+ await gridModel.preSelectFirstAsync();
218
+ }
219
+
220
+ // Cube emits leaves in "children" collection - rename that collection to "leafRows" so we can
221
+ // carry the leaves with the record, but deliberately not show them in the tree grid. We only
222
+ // want the tree grid to show aggregate records.
223
+ private separateLeafRows(node) {
224
+ if (isEmpty(node.children)) return;
225
+
226
+ const childrenAreLeaves = !node.children[0].children;
227
+ if (childrenAreLeaves) {
228
+ node.leafRows = node.children;
229
+ delete node.children;
230
+ } else {
231
+ node.children.forEach(child => this.separateLeafRows(child));
232
+ }
233
+ }
234
+
235
+ private cubeLabelComparator(valA, valB, sortDir, abs, {recordA, recordB, defaultComparator}) {
298
236
  const rawA = recordA?.raw,
299
237
  rawB = recordB?.raw,
300
238
  sortValA = this.getComparableValForDim(rawA, rawA?.cubeDimension),
@@ -303,7 +241,7 @@ export class ActivityTrackingModel extends HoistModel {
303
241
  return defaultComparator(sortValA, sortValB);
304
242
  }
305
243
 
306
- getComparableValForDim(raw, dim) {
244
+ private getComparableValForDim(raw, dim) {
307
245
  const rawVal = raw ? raw[dim] : null;
308
246
  if (rawVal == null) return null;
309
247
 
@@ -328,24 +266,6 @@ export class ActivityTrackingModel extends HoistModel {
328
266
  return LocalDate.currentAppDay();
329
267
  }
330
268
 
331
- private async loadLookupsAsync() {
332
- try {
333
- const lookups = await XH.fetchJson({url: 'trackLogAdmin/lookups'});
334
- this.filterChooserModel.fieldSpecs.forEach(spec => {
335
- const {field} = spec,
336
- lookup = lookups[field] ? compact(lookups[field]) : null;
337
-
338
- if (!isEmpty(lookup)) {
339
- spec.values = lookup;
340
- spec.enableValues = true;
341
- spec.hasExplicitValues = true;
342
- }
343
- });
344
- } catch (e) {
345
- XH.handleException(e, {title: 'Error loading lookups for filtering'});
346
- }
347
- }
348
-
349
269
  @computed
350
270
  private get query() {
351
271
  const {values} = this.formModel;
@@ -356,4 +276,181 @@ export class ActivityTrackingModel extends HoistModel {
356
276
  filters: this.filterChooserModel.value
357
277
  };
358
278
  }
279
+
280
+ //------------------------
281
+ // Impl - core data models
282
+ //------------------------
283
+ @action
284
+ private createAndSetCoreModels() {
285
+ this.cube = this.createCube();
286
+ this.filterChooserModel = this.createFilterChooserModel();
287
+ this.groupingChooserModel = this.createGroupingChooserModel();
288
+ this.gridModel = this.createGridModel();
289
+ }
290
+
291
+ private createCube(): Cube {
292
+ const fields = [
293
+ Col.browser.field,
294
+ Col.category.field,
295
+ Col.severity.field,
296
+ Col.correlationId.field,
297
+ Col.data.field,
298
+ {...(Col.dateCreated.field as FieldSpec), displayName: 'Timestamp'},
299
+ Col.day.field,
300
+ Col.dayRange.field,
301
+ Col.device.field,
302
+ Col.elapsed.field,
303
+ Col.entryCount.field,
304
+ Col.impersonating.field,
305
+ Col.msg.field,
306
+ Col.userAgent.field,
307
+ Col.username.field,
308
+ {name: 'count', type: 'int', aggregator: 'CHILD_COUNT'},
309
+ {name: 'month', type: 'string', isDimension: true, aggregator: 'UNIQUE'},
310
+ Col.url.field,
311
+ Col.instance.field,
312
+ Col.appVersion.field,
313
+ Col.appEnvironment.field,
314
+ ...this.dataFields
315
+ ] as CubeFieldSpec[];
316
+
317
+ return new Cube({fields});
318
+ }
319
+
320
+ private createFilterChooserModel(): FilterChooserModel {
321
+ // TODO - data fields?
322
+ const ret = new FilterChooserModel({
323
+ persistWith: {...this.persistWith, persistFavorites: false},
324
+ fieldSpecs: [
325
+ {field: 'category'},
326
+ {field: 'correlationId'},
327
+ {field: 'username', displayName: 'User'},
328
+ {field: 'device'},
329
+ {field: 'browser'},
330
+ {
331
+ field: 'elapsed',
332
+ valueRenderer: v => {
333
+ return fmtNumber(v, {
334
+ label: 'ms',
335
+ formatConfig: {thousandSeparated: false, mantissa: 0}
336
+ });
337
+ },
338
+ fieldType: 'number'
339
+ },
340
+ {field: 'msg', displayName: 'Message'},
341
+ {field: 'data'},
342
+ {field: 'userAgent'},
343
+ {field: 'url', displayName: 'URL'},
344
+ {field: 'instance'},
345
+ {field: 'severity'},
346
+ {field: 'appVersion'},
347
+ {field: 'appEnvironment', displayName: 'Environment'}
348
+ ]
349
+ });
350
+
351
+ // Load lookups - not awaited
352
+ try {
353
+ XH.fetchJson({url: 'trackLogAdmin/lookups'}).then(lookups => {
354
+ if (ret !== this.filterChooserModel) return;
355
+ ret.fieldSpecs.forEach(spec => {
356
+ const {field} = spec,
357
+ lookup = lookups[field] ? compact(lookups[field]) : null;
358
+
359
+ if (!isEmpty(lookup)) {
360
+ spec.values = lookup;
361
+ spec.enableValues = true;
362
+ spec.hasExplicitValues = true;
363
+ }
364
+ });
365
+ });
366
+ } catch (e) {
367
+ XH.handleException(e, {title: 'Error loading lookups for filtering'});
368
+ }
369
+
370
+ return ret;
371
+ }
372
+
373
+ private createGroupingChooserModel(): GroupingChooserModel {
374
+ return new GroupingChooserModel({
375
+ persistWith: {...this.persistWith, persistFavorites: false},
376
+ dimensions: this.cube.dimensions,
377
+ initialValue: ['username', 'category']
378
+ });
379
+ }
380
+
381
+ private createGridModel(): GridModel {
382
+ const hidden = true;
383
+ return new GridModel({
384
+ persistWith: {...this.persistWith, path: 'aggGrid'},
385
+ enableExport: true,
386
+ colChooserModel: true,
387
+ treeMode: true,
388
+ treeStyle: TreeStyle.HIGHLIGHTS_AND_BORDERS,
389
+ autosizeOptions: {mode: 'managed'},
390
+ exportOptions: {filename: exportFilename('activity-summary')},
391
+ emptyText: 'No activity reported...',
392
+ sortBy: ['cubeLabel'],
393
+ columns: [
394
+ {
395
+ field: {
396
+ name: 'cubeLabel',
397
+ type: 'string',
398
+ displayName: 'Group'
399
+ },
400
+ minWidth: 100,
401
+ isTreeColumn: true,
402
+ comparator: this.cubeLabelComparator.bind(this)
403
+ },
404
+ {...Col.username, hidden},
405
+ {...Col.category, hidden},
406
+ {...Col.device, hidden},
407
+ {...Col.browser, hidden},
408
+ {...Col.userAgent, hidden},
409
+ {...Col.impersonating, hidden},
410
+ {...Col.elapsed, headerName: 'Elapsed (avg)', hidden},
411
+ {...Col.dayRange, hidden},
412
+ {...Col.entryCount},
413
+ {field: 'count', hidden},
414
+ {...Col.appEnvironment, hidden},
415
+ {...Col.appVersion, hidden},
416
+ {...Col.url, hidden},
417
+ {...Col.instance, hidden},
418
+ ...this.dataFieldCols.map(it => ({...it, hidden: !it.appData.showInAggGrid}))
419
+ ]
420
+ });
421
+ }
422
+
423
+ //------------------------------
424
+ // Impl - data fields processing
425
+ //------------------------------
426
+ private processRawTrackLog(raw: PlainObject) {
427
+ try {
428
+ raw.day = LocalDate.from(raw.day);
429
+ raw.month = raw.day.format(this._monthFormat);
430
+ raw.dayRange = {min: raw.day, max: raw.day};
431
+
432
+ const data = JSON.parse(raw.data);
433
+ if (isEmpty(data)) return;
434
+
435
+ this.dataFields.forEach(df => {
436
+ const path = df.path;
437
+ raw[df.name] = get(data, path);
438
+ });
439
+ } catch (e) {
440
+ this.logError(`Error processing raw track log`, e);
441
+ }
442
+ }
443
+
444
+ private getDfRenderer(df: ActivityTrackingDataFieldSpec): ColumnRenderer {
445
+ switch (df.type) {
446
+ case 'number':
447
+ return numberRenderer();
448
+ case 'date':
449
+ return dateTimeSecRenderer();
450
+ case 'localDate':
451
+ return dateRenderer();
452
+ default:
453
+ return v => v ?? '-';
454
+ }
455
+ }
359
456
  }