@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
package/CHANGELOG.md CHANGED
@@ -10,6 +10,8 @@ Requires `hoist-core >= 30.0` with new APIs to support the consolidated Admin Co
10
10
 
11
11
  * Added a new "Clients" Admin Console tab- a consolidated view of all websocket-connected clients
12
12
  across all instances in the cluster.
13
+ * Updated `FormModel` to support `persistWith` for storing and recalling its values, including
14
+ developer options to persist all or a provided subset of fields.
13
15
 
14
16
  ### 🐞 Bug Fixes
15
17
 
@@ -4,7 +4,6 @@
4
4
  *
5
5
  * Copyright © 2025 Extremely Heavy Industries Inc.
6
6
  */
7
- import {AppModel} from '@xh/hoist/admin/AppModel';
8
7
  import {XH} from '@xh/hoist/core';
9
8
  import {LocalDate} from '@xh/hoist/utils/datetime';
10
9
 
@@ -22,7 +21,3 @@ export function exportFilename(moduleName: string): string {
22
21
  export function exportFilenameWithDate(moduleName: string): () => string {
23
22
  return () => `${XH.appCode}-${moduleName}-${LocalDate.today()}`;
24
23
  }
25
-
26
- export function getAppModel<T extends AppModel>() {
27
- return XH.appModel as T;
28
- }
package/admin/AppModel.ts CHANGED
@@ -7,21 +7,21 @@
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 {ViewManagerModel} from '@xh/hoist/cmp/viewmanager';
11
- import {HoistAppModel, XH} from '@xh/hoist/core';
10
+ import {HoistAppModel, managed, XH} from '@xh/hoist/core';
12
11
  import {Icon} from '@xh/hoist/icon';
13
12
  import {without} from 'lodash';
14
13
  import {Route} from 'router5';
15
14
  import {activityTrackingPanel} from './tabs/activity/tracking/ActivityTrackingPanel';
16
- import {clientTab} from './tabs/client/ClientTab';
17
15
  import {generalTab} from './tabs/general/GeneralTab';
18
16
  import {monitorTab} from './tabs/monitor/MonitorTab';
19
17
  import {userDataTab} from './tabs/userData/UserDataTab';
18
+ import {clientTab} from './tabs/client/ClientTab';
20
19
 
21
20
  export class AppModel extends HoistAppModel {
22
- tabModel: TabContainerModel;
21
+ static instance: AppModel;
23
22
 
24
- viewManagerModels: Record<string, ViewManagerModel> = {};
23
+ @managed
24
+ tabModel: TabContainerModel;
25
25
 
26
26
  static get readonly() {
27
27
  return !XH.getUser().isHoistAdmin;
@@ -40,11 +40,6 @@ 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
-
48
43
  override getRoutes(): Route[] {
49
44
  return [
50
45
  {
@@ -166,11 +161,4 @@ export class AppModel extends HoistAppModel {
166
161
  const appCodes = without(XH.clientApps, XH.clientAppCode, 'mobile');
167
162
  return appCodes.find(it => it === 'app') ?? appCodes[0];
168
163
  }
169
-
170
- async initViewManagerModelsAsync() {
171
- this.viewManagerModels.activityTracking = await ViewManagerModel.createAsync({
172
- type: 'xhAdminActivityTrackingView',
173
- typeDisplayName: 'View'
174
- });
175
- }
176
164
  }
@@ -31,24 +31,6 @@
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
- }
52
34
  }
53
35
 
54
36
  .xh-admin-activity-detail {
@@ -4,42 +4,30 @@
4
4
  *
5
5
  * Copyright © 2025 Extremely Heavy Industries Inc.
6
6
  */
7
- import {exportFilename, getAppModel} from '@xh/hoist/admin/AdminUtils';
7
+ import {exportFilename} 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';
13
9
  import {FilterChooserModel} from '@xh/hoist/cmp/filter';
14
10
  import {FormModel} from '@xh/hoist/cmp/form';
15
- import {ColumnRenderer, ColumnSpec, GridModel, TreeStyle} from '@xh/hoist/cmp/grid';
11
+ import {GridModel, TreeStyle} from '@xh/hoist/cmp/grid';
16
12
  import {GroupingChooserModel} from '@xh/hoist/cmp/grouping';
17
- import {HoistModel, LoadSpec, managed, PlainObject, XH} from '@xh/hoist/core';
13
+ import {HoistModel, LoadSpec, managed, XH} from '@xh/hoist/core';
18
14
  import {Cube, CubeFieldSpec, FieldSpec} from '@xh/hoist/data';
19
- import {dateRenderer, dateTimeSecRenderer, fmtNumber, numberRenderer} from '@xh/hoist/format';
20
- import {action, computed, makeObservable, observable} from '@xh/hoist/mobx';
15
+ import {fmtNumber} from '@xh/hoist/format';
16
+ import {action, computed, makeObservable} from '@xh/hoist/mobx';
21
17
  import {LocalDate} from '@xh/hoist/utils/datetime';
22
- import {compact, get, isEmpty, isEqual, round} from 'lodash';
18
+ import {compact, isEmpty, round} from 'lodash';
23
19
  import moment from 'moment';
24
20
 
25
- export class ActivityTrackingModel extends HoistModel {
26
- /** FormModel for server-side querying controls. */
27
- @managed formModel: FormModel;
21
+ export const PERSIST_ACTIVITY = {localStorageKey: 'xhAdminActivityState'};
28
22
 
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[] = [];
23
+ export class ActivityTrackingModel extends HoistModel {
24
+ override persistWith = PERSIST_ACTIVITY;
41
25
 
42
- @observable showFilterChooser: boolean = false;
26
+ @managed formModel: FormModel;
27
+ @managed groupingChooserModel: GroupingChooserModel;
28
+ @managed cube: Cube;
29
+ @managed filterChooserModel: FilterChooserModel;
30
+ @managed gridModel: GridModel;
43
31
 
44
32
  get enabled(): boolean {
45
33
  return XH.trackService.enabled;
@@ -49,13 +37,17 @@ export class ActivityTrackingModel extends HoistModel {
49
37
  return this.groupingChooserModel.value;
50
38
  }
51
39
 
52
- get endDay(): LocalDate {
53
- return this.formModel.values.endDay;
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`;
54
47
  }
55
48
 
56
- @computed
57
- get hasFilter(): boolean {
58
- return !!this.filterChooserModel.value;
49
+ get endDay(): LocalDate {
50
+ return this.formModel.values.endDay;
59
51
  }
60
52
 
61
53
  get maxRowOptions() {
@@ -77,31 +69,12 @@ export class ActivityTrackingModel extends HoistModel {
77
69
  return this.maxRows === this.cube.store.allCount;
78
70
  }
79
71
 
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
-
95
72
  private _monthFormat = 'MMM YYYY';
73
+ private _defaultDims = ['username'];
96
74
 
97
75
  constructor() {
98
76
  super();
99
77
  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)
105
78
  this.formModel = new FormModel({
106
79
  fields: [
107
80
  {name: 'startDay', initialValue: () => this.defaultStartDay},
@@ -110,40 +83,140 @@ export class ActivityTrackingModel extends HoistModel {
110
83
  ]
111
84
  });
112
85
 
113
- this.dataFieldsEditorModel = new DataFieldsEditorModel(this);
114
- this.markPersist('dataFields');
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
+ });
115
111
 
116
- this.addReaction(
117
- {
118
- track: () => this.dataFields,
119
- run: () => this.createAndSetCoreModels(),
120
- fireImmediately: true
121
- },
122
- {
123
- track: () => this.query,
124
- run: () => this.loadAsync(),
125
- debounce: 100
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
126
156
  },
127
- {
128
- track: () => [this.cube.records, this.dimensions],
129
- run: () => this.loadGridAsync(),
130
- debounce: 100
131
- }
132
- );
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
+ });
133
202
  }
134
203
 
135
204
  override async doLoadAsync(loadSpec: LoadSpec) {
136
- const {enabled, cube, query} = this;
205
+ const {enabled, cube} = this;
137
206
  if (!enabled) return;
138
207
 
139
208
  try {
140
- const data = await XH.postJson({
209
+ const data = await XH.fetchService.postJson({
141
210
  url: 'trackLogAdmin',
142
- body: query,
211
+ body: this.query,
143
212
  loadSpec
144
213
  });
145
214
 
146
- data.forEach(it => this.processRawTrackLog(it));
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
+ });
147
220
 
148
221
  await cube.loadDataAsync(data);
149
222
  } catch (e) {
@@ -152,23 +225,47 @@ export class ActivityTrackingModel extends HoistModel {
152
225
  }
153
226
  }
154
227
 
155
- @action
156
- setDataFields(dataFields: ActivityTrackingDataFieldSpec[]) {
157
- if (!isEqual(dataFields, this.dataFields)) {
158
- this.dataFields = dataFields ?? [];
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));
159
253
  }
160
254
  }
161
255
 
162
256
  @action
163
- toggleFilterChooser() {
164
- this.showFilterChooser = !this.showFilterChooser;
257
+ resetQuery() {
258
+ const {formModel, filterChooserModel, groupingChooserModel, _defaultDims} = this;
259
+ formModel.init();
260
+ filterChooserModel.setValue(null);
261
+ groupingChooserModel.setValue(_defaultDims);
165
262
  }
166
263
 
167
- adjustDates(dir: 'add' | 'subtract') {
264
+ adjustDates(dir) {
168
265
  const {startDay, endDay} = this.formModel.fields,
169
266
  appDay = LocalDate.currentAppDay(),
170
- start: LocalDate = startDay.value,
171
- end: LocalDate = endDay.value,
267
+ start = startDay.value,
268
+ end = endDay.value,
172
269
  diff = end.diff(start),
173
270
  incr = diff + 1;
174
271
 
@@ -197,42 +294,7 @@ export class ActivityTrackingModel extends HoistModel {
197
294
  return startDay === endDay.subtract(value, unit).nextDay();
198
295
  }
199
296
 
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}) {
297
+ cubeLabelComparator(valA, valB, sortDir, abs, {recordA, recordB, defaultComparator}) {
236
298
  const rawA = recordA?.raw,
237
299
  rawB = recordB?.raw,
238
300
  sortValA = this.getComparableValForDim(rawA, rawA?.cubeDimension),
@@ -241,7 +303,7 @@ export class ActivityTrackingModel extends HoistModel {
241
303
  return defaultComparator(sortValA, sortValB);
242
304
  }
243
305
 
244
- private getComparableValForDim(raw, dim) {
306
+ getComparableValForDim(raw, dim) {
245
307
  const rawVal = raw ? raw[dim] : null;
246
308
  if (rawVal == null) return null;
247
309
 
@@ -266,6 +328,24 @@ export class ActivityTrackingModel extends HoistModel {
266
328
  return LocalDate.currentAppDay();
267
329
  }
268
330
 
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
+
269
349
  @computed
270
350
  private get query() {
271
351
  const {values} = this.formModel;
@@ -276,181 +356,4 @@ export class ActivityTrackingModel extends HoistModel {
276
356
  filters: this.filterChooserModel.value
277
357
  };
278
358
  }
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
- }
456
359
  }