@xh/hoist 73.0.0-SNAPSHOT.1746025071597 → 73.0.0-SNAPSHOT.1746050068813

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 (48) hide show
  1. package/CHANGELOG.md +8 -3
  2. package/admin/AdminUtils.ts +5 -0
  3. package/admin/AppModel.ts +19 -7
  4. package/admin/columns/Rest.ts +8 -0
  5. package/admin/columns/Tracking.ts +72 -0
  6. package/admin/tabs/activity/tracking/ActivityTracking.scss +18 -0
  7. package/admin/tabs/activity/tracking/ActivityTrackingModel.ts +309 -216
  8. package/admin/tabs/activity/tracking/ActivityTrackingPanel.ts +81 -51
  9. package/admin/tabs/activity/tracking/chart/AggChartModel.ts +218 -0
  10. package/admin/tabs/activity/tracking/chart/AggChartPanel.ts +61 -0
  11. package/admin/tabs/activity/tracking/datafields/DataFieldsEditor.ts +147 -0
  12. package/admin/tabs/activity/tracking/datafields/DataFieldsEditorModel.ts +133 -0
  13. package/admin/tabs/activity/tracking/detail/ActivityDetailModel.ts +123 -60
  14. package/admin/tabs/activity/tracking/detail/ActivityDetailView.ts +106 -58
  15. package/admin/tabs/client/ClientTab.ts +2 -4
  16. package/admin/tabs/cluster/instances/memory/MemoryMonitorModel.ts +1 -2
  17. package/admin/tabs/general/GeneralTab.ts +2 -0
  18. package/build/types/admin/AdminUtils.d.ts +2 -0
  19. package/build/types/admin/AppModel.d.ts +4 -1
  20. package/build/types/admin/columns/Rest.d.ts +1 -0
  21. package/build/types/admin/columns/Tracking.d.ts +6 -0
  22. package/build/types/admin/tabs/activity/tracking/ActivityTrackingModel.d.ts +31 -28
  23. package/build/types/admin/tabs/activity/tracking/chart/AggChartModel.d.ts +33 -0
  24. package/build/types/admin/tabs/activity/tracking/chart/AggChartPanel.d.ts +2 -0
  25. package/build/types/admin/tabs/activity/tracking/datafields/DataFieldsEditor.d.ts +2 -0
  26. package/build/types/admin/tabs/activity/tracking/datafields/DataFieldsEditorModel.d.ts +46 -0
  27. package/build/types/admin/tabs/activity/tracking/detail/ActivityDetailModel.d.ts +14 -1
  28. package/build/types/cmp/form/FormModel.d.ts +19 -30
  29. package/build/types/cmp/form/field/SubformsFieldModel.d.ts +25 -22
  30. package/build/types/core/HoistBase.d.ts +2 -2
  31. package/build/types/data/cube/CubeField.d.ts +4 -5
  32. package/build/types/desktop/cmp/appOption/AutoRefreshAppOption.d.ts +3 -3
  33. package/build/types/desktop/cmp/appOption/ThemeAppOption.d.ts +3 -3
  34. package/cmp/error/ErrorBoundaryModel.ts +1 -1
  35. package/cmp/form/FormModel.ts +20 -28
  36. package/cmp/form/field/SubformsFieldModel.ts +28 -22
  37. package/cmp/grid/columns/DatesTimes.ts +1 -2
  38. package/cmp/grid/impl/GridHScrollbar.ts +1 -2
  39. package/core/HoistBase.ts +12 -12
  40. package/data/cube/CubeField.ts +17 -18
  41. package/package.json +1 -1
  42. package/tsconfig.tsbuildinfo +1 -1
  43. package/admin/tabs/activity/tracking/charts/ChartsModel.ts +0 -218
  44. package/admin/tabs/activity/tracking/charts/ChartsPanel.ts +0 -76
  45. package/build/types/admin/tabs/activity/tracking/charts/ChartsModel.d.ts +0 -34
  46. package/build/types/admin/tabs/activity/tracking/charts/ChartsPanel.d.ts +0 -2
  47. /package/admin/tabs/{client → general}/feedback/FeedbackPanel.ts +0 -0
  48. /package/build/types/admin/tabs/{client → general}/feedback/FeedbackPanel.d.ts +0 -0
@@ -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,160 +77,66 @@ 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);
78
- this.formModel = new FormModel({
79
- fields: [
80
- {name: 'startDay', initialValue: () => this.defaultStartDay},
81
- {name: 'endDay', initialValue: () => this.defaultEndDay},
82
- {name: 'maxRows', initialValue: XH.trackService.conf.maxRows?.default}
83
- ]
84
- });
85
-
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
- Col.loadId.field,
110
- Col.tabId.field
111
- ] as CubeFieldSpec[]
112
- });
113
100
 
114
- this.filterChooserModel = new FilterChooserModel({
115
- fieldSpecs: [
116
- {field: 'category'},
117
- {field: 'correlationId'},
118
- {field: 'username', displayName: 'User'},
119
- {field: 'device'},
120
- {field: 'browser'},
121
- {
122
- field: 'elapsed',
123
- valueRenderer: v => {
124
- return fmtNumber(v, {
125
- label: 'ms',
126
- formatConfig: {thousandSeparated: false, mantissa: 0}
127
- });
128
- },
129
- fieldType: 'number'
130
- },
131
- {field: 'msg', displayName: 'Message'},
132
- {field: 'data'},
133
- {field: 'userAgent'},
134
- {field: 'url', displayName: 'URL'},
135
- {field: 'instance'},
136
- {field: 'severity'},
137
- {field: 'appVersion'},
138
- {field: 'loadId'},
139
- {field: 'tabId'},
140
- {field: 'appEnvironment', displayName: 'Environment'}
141
- ]
142
- });
101
+ this.persistWith = {viewManagerModel: this.viewManagerModel};
102
+ this.markPersist('showFilterChooser');
143
103
 
144
- this.loadLookupsAsync();
104
+ this.formModel = this.createQueryFormModel();
145
105
 
146
- this.groupingChooserModel = new GroupingChooserModel({
147
- dimensions: this.cube.dimensions,
148
- persistWith: this.persistWith,
149
- initialValue: this._defaultDims
150
- });
106
+ this.dataFieldsEditorModel = new DataFieldsEditorModel(this);
107
+ this.markPersist('dataFields');
151
108
 
152
- const hidden = true;
153
- this.gridModel = new GridModel({
154
- treeMode: true,
155
- treeStyle: TreeStyle.HIGHLIGHTS_AND_BORDERS,
156
- persistWith: {
157
- ...this.persistWith,
158
- path: 'aggGridModel',
159
- persistSort: false
109
+ this.addReaction(
110
+ {
111
+ track: () => this.dataFields,
112
+ run: () => this.createAndSetCoreModels(),
113
+ fireImmediately: true
160
114
  },
161
- colChooserModel: true,
162
- enableExport: true,
163
- exportOptions: {filename: exportFilename('activity-summary')},
164
- emptyText: 'No activity reported...',
165
- sortBy: ['cubeLabel'],
166
- columns: [
167
- {
168
- field: {
169
- name: 'cubeLabel',
170
- type: 'string',
171
- displayName: 'Tracked Activity'
172
- },
173
- flex: 1,
174
- minWidth: 100,
175
- isTreeColumn: true,
176
- comparator: this.cubeLabelComparator.bind(this)
177
- },
178
- {...Col.username, hidden},
179
- {...Col.category, hidden},
180
- {...Col.device, hidden},
181
- {...Col.browser, hidden},
182
- {...Col.userAgent, hidden},
183
- {...Col.impersonating, hidden},
184
- {...Col.elapsed, headerName: 'Elapsed (avg)', hidden},
185
- {...Col.dayRange, hidden},
186
- {...Col.entryCount},
187
- {field: 'count', hidden},
188
- {...Col.appEnvironment, hidden},
189
- {...Col.appVersion, hidden},
190
- {...Col.loadId, hidden},
191
- {...Col.tabId, hidden},
192
- {...Col.url, hidden},
193
- {...Col.instance, hidden}
194
- ]
195
- });
196
-
197
- this.addReaction({
198
- track: () => this.query,
199
- run: () => this.loadAsync(),
200
- debounce: 100
201
- });
202
-
203
- this.addReaction({
204
- track: () => [this.cube.records, this.dimensions],
205
- run: () => this.loadGridAsync(),
206
- debounce: 100
207
- });
115
+ {
116
+ track: () => this.query,
117
+ run: () => this.loadAsync(),
118
+ debounce: 100
119
+ },
120
+ {
121
+ track: () => [this.cube.records, this.dimensions],
122
+ run: () => this.loadGridAsync(),
123
+ debounce: 100
124
+ }
125
+ );
208
126
  }
209
127
 
210
128
  override async doLoadAsync(loadSpec: LoadSpec) {
211
- const {enabled, cube} = this;
129
+ const {enabled, cube, query} = this;
212
130
  if (!enabled) return;
213
131
 
214
132
  try {
215
- const data = await XH.fetchService.postJson({
133
+ const data = await XH.postJson({
216
134
  url: 'trackLogAdmin',
217
- body: this.query,
135
+ body: query,
218
136
  loadSpec
219
137
  });
220
138
 
221
- data.forEach(it => {
222
- it.day = LocalDate.from(it.day);
223
- it.month = it.day.format(this._monthFormat);
224
- it.dayRange = {min: it.day, max: it.day};
225
- });
139
+ data.forEach(it => this.processRawTrackLog(it));
226
140
 
227
141
  await cube.loadDataAsync(data);
228
142
  } catch (e) {
@@ -231,47 +145,23 @@ export class ActivityTrackingModel extends HoistModel {
231
145
  }
232
146
  }
233
147
 
234
- async loadGridAsync() {
235
- const {cube, gridModel, dimensions} = this,
236
- data = cube.executeQuery({
237
- dimensions,
238
- includeRoot: true,
239
- includeLeaves: true
240
- });
241
-
242
- data.forEach(node => this.separateLeafRows(node));
243
- gridModel.loadData(data);
244
- await gridModel.preSelectFirstAsync();
245
- }
246
-
247
- // Cube emits leaves in "children" collection - rename that collection to "leafRows" so we can
248
- // carry the leaves with the record, but deliberately not show them in the tree grid. We only
249
- // want the tree grid to show aggregate records.
250
- separateLeafRows(node) {
251
- if (isEmpty(node.children)) return;
252
-
253
- const childrenAreLeaves = !node.children[0].children;
254
- if (childrenAreLeaves) {
255
- node.leafRows = node.children;
256
- delete node.children;
257
- } else {
258
- node.children.forEach(child => this.separateLeafRows(child));
148
+ @action
149
+ setDataFields(dataFields: ActivityTrackingDataFieldSpec[]) {
150
+ if (!isEqual(dataFields, this.dataFields)) {
151
+ this.dataFields = dataFields ?? [];
259
152
  }
260
153
  }
261
154
 
262
155
  @action
263
- resetQuery() {
264
- const {formModel, filterChooserModel, groupingChooserModel, _defaultDims} = this;
265
- formModel.init();
266
- filterChooserModel.setValue(null);
267
- groupingChooserModel.setValue(_defaultDims);
156
+ toggleFilterChooser() {
157
+ this.showFilterChooser = !this.showFilterChooser;
268
158
  }
269
159
 
270
- adjustDates(dir) {
160
+ adjustDates(dir: 'add' | 'subtract') {
271
161
  const {startDay, endDay} = this.formModel.fields,
272
162
  appDay = LocalDate.currentAppDay(),
273
- start = startDay.value,
274
- end = endDay.value,
163
+ start: LocalDate = startDay.value,
164
+ end: LocalDate = endDay.value,
275
165
  diff = end.diff(start),
276
166
  incr = diff + 1;
277
167
 
@@ -300,7 +190,53 @@ export class ActivityTrackingModel extends HoistModel {
300
190
  return startDay === endDay.subtract(value, unit).nextDay();
301
191
  }
302
192
 
303
- cubeLabelComparator(valA, valB, sortDir, abs, {recordA, recordB, defaultComparator}) {
193
+ getDisplayName(fieldName: string) {
194
+ return fieldName ? (this.cube.store.getField(fieldName)?.displayName ?? fieldName) : null;
195
+ }
196
+
197
+ //------------------
198
+ // Implementation
199
+ //------------------
200
+ private createQueryFormModel(): FormModel {
201
+ return new FormModel({
202
+ persistWith: {...this.persistWith, path: 'queryFormValues', includeFields: ['maxRows']},
203
+ fields: [
204
+ {name: 'startDay', initialValue: () => LocalDate.currentAppDay()},
205
+ {name: 'endDay', initialValue: () => LocalDate.currentAppDay()},
206
+ {name: 'maxRows', initialValue: XH.trackService.conf.maxRows?.default}
207
+ ]
208
+ });
209
+ }
210
+
211
+ private async loadGridAsync() {
212
+ const {cube, gridModel, dimensions} = this,
213
+ data = cube.executeQuery({
214
+ dimensions,
215
+ includeRoot: true,
216
+ includeLeaves: true
217
+ });
218
+
219
+ data.forEach(node => this.separateLeafRows(node));
220
+ gridModel.loadData(data);
221
+ await gridModel.preSelectFirstAsync();
222
+ }
223
+
224
+ // Cube emits leaves in "children" collection - rename that collection to "leafRows" so we can
225
+ // carry the leaves with the record, but deliberately not show them in the tree grid. We only
226
+ // want the tree grid to show aggregate records.
227
+ private separateLeafRows(node) {
228
+ if (isEmpty(node.children)) return;
229
+
230
+ const childrenAreLeaves = !node.children[0].children;
231
+ if (childrenAreLeaves) {
232
+ node.leafRows = node.children;
233
+ delete node.children;
234
+ } else {
235
+ node.children.forEach(child => this.separateLeafRows(child));
236
+ }
237
+ }
238
+
239
+ private cubeLabelComparator(valA, valB, sortDir, abs, {recordA, recordB, defaultComparator}) {
304
240
  const rawA = recordA?.raw,
305
241
  rawB = recordB?.raw,
306
242
  sortValA = this.getComparableValForDim(rawA, rawA?.cubeDimension),
@@ -309,7 +245,7 @@ export class ActivityTrackingModel extends HoistModel {
309
245
  return defaultComparator(sortValA, sortValB);
310
246
  }
311
247
 
312
- getComparableValForDim(raw, dim) {
248
+ private getComparableValForDim(raw, dim) {
313
249
  const rawVal = raw ? raw[dim] : null;
314
250
  if (rawVal == null) return null;
315
251
 
@@ -326,40 +262,197 @@ export class ActivityTrackingModel extends HoistModel {
326
262
  }
327
263
  }
328
264
 
329
- private get defaultStartDay() {
330
- return LocalDate.currentAppDay();
265
+ @computed
266
+ private get query() {
267
+ const {values} = this.formModel;
268
+ return {
269
+ startDay: values.startDay,
270
+ endDay: values.endDay,
271
+ maxRows: values.maxRows,
272
+ filters: this.filterChooserModel.value
273
+ };
274
+ }
275
+
276
+ //------------------------
277
+ // Impl - core data models
278
+ //------------------------
279
+ @action
280
+ private createAndSetCoreModels() {
281
+ this.cube = this.createCube();
282
+ this.filterChooserModel = this.createFilterChooserModel();
283
+ this.groupingChooserModel = this.createGroupingChooserModel();
284
+ this.gridModel = this.createGridModel();
331
285
  }
332
286
 
333
- private get defaultEndDay() {
334
- return LocalDate.currentAppDay();
287
+ private createCube(): Cube {
288
+ const fields = [
289
+ Col.appEnvironment.field,
290
+ Col.appVersion.field,
291
+ Col.browser.field,
292
+ Col.category.field,
293
+ Col.correlationId.field,
294
+ {name: 'count', type: 'int', aggregator: 'CHILD_COUNT'},
295
+ Col.data.field,
296
+ {...(Col.dateCreated.field as FieldSpec), displayName: 'Timestamp'},
297
+ Col.day.field,
298
+ Col.dayRange.field,
299
+ Col.device.field,
300
+ Col.elapsed.field,
301
+ Col.entryCount.field,
302
+ Col.impersonating.field,
303
+ Col.instance.field,
304
+ Col.loadId.field,
305
+ Col.msg.field,
306
+ {name: 'month', type: 'string', isDimension: true, aggregator: 'UNIQUE'},
307
+ Col.severity.field,
308
+ Col.tabId.field,
309
+ Col.userAgent.field,
310
+ Col.username.field,
311
+ Col.url.field,
312
+ ...this.dataFields
313
+ ] as CubeFieldSpec[];
314
+
315
+ return new Cube({fields});
335
316
  }
336
317
 
337
- private async loadLookupsAsync() {
318
+ private createFilterChooserModel(): FilterChooserModel {
319
+ // TODO - data fields?
320
+ const ret = new FilterChooserModel({
321
+ persistWith: {...this.persistWith, persistFavorites: false},
322
+ fieldSpecs: [
323
+ {field: 'appEnvironment', displayName: 'Environment'},
324
+ {field: 'appVersion'},
325
+ {field: 'browser'},
326
+ {field: 'category'},
327
+ {field: 'correlationId'},
328
+ {field: 'data'},
329
+ {field: 'device'},
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: 'instance'},
341
+ {field: 'loadId'},
342
+ {field: 'msg', displayName: 'Message'},
343
+ {field: 'severity'},
344
+ {field: 'tabId'},
345
+ {field: 'userAgent'},
346
+ {field: 'username', displayName: 'User'},
347
+ {field: 'url', displayName: 'URL'}
348
+ ]
349
+ });
350
+
351
+ // Load lookups - not awaited
338
352
  try {
339
- const lookups = await XH.fetchJson({url: 'trackLogAdmin/lookups'});
340
- this.filterChooserModel.fieldSpecs.forEach(spec => {
341
- const {field} = spec,
342
- lookup = lookups[field] ? compact(lookups[field]) : null;
343
-
344
- if (!isEmpty(lookup)) {
345
- spec.values = lookup;
346
- spec.enableValues = true;
347
- spec.hasExplicitValues = true;
348
- }
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
+ });
349
365
  });
350
366
  } catch (e) {
351
367
  XH.handleException(e, {title: 'Error loading lookups for filtering'});
352
368
  }
369
+
370
+ return ret;
353
371
  }
354
372
 
355
- @computed
356
- private get query() {
357
- const {values} = this.formModel;
358
- return {
359
- startDay: values.startDay,
360
- endDay: values.endDay,
361
- maxRows: values.maxRows,
362
- filters: this.filterChooserModel.value
363
- };
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.loadId, hidden},
417
+ {...Col.tabId, hidden},
418
+ {...Col.url, hidden},
419
+ {...Col.instance, hidden},
420
+ ...this.dataFieldCols.map(it => ({...it, hidden: !it.appData.showInAggGrid}))
421
+ ]
422
+ });
423
+ }
424
+
425
+ //------------------------------
426
+ // Impl - data fields processing
427
+ //------------------------------
428
+ private processRawTrackLog(raw: PlainObject) {
429
+ try {
430
+ raw.day = LocalDate.from(raw.day);
431
+ raw.month = raw.day.format(this._monthFormat);
432
+ raw.dayRange = {min: raw.day, max: raw.day};
433
+
434
+ const data = JSON.parse(raw.data);
435
+ if (isEmpty(data)) return;
436
+
437
+ this.dataFields.forEach(df => {
438
+ const path = df.path;
439
+ raw[df.name] = get(data, path);
440
+ });
441
+ } catch (e) {
442
+ this.logError(`Error processing raw track log`, e);
443
+ }
444
+ }
445
+
446
+ private getDfRenderer(df: ActivityTrackingDataFieldSpec): ColumnRenderer {
447
+ switch (df.type) {
448
+ case 'number':
449
+ return numberRenderer();
450
+ case 'date':
451
+ return dateTimeSecRenderer();
452
+ case 'localDate':
453
+ return dateRenderer();
454
+ default:
455
+ return v => v ?? '-';
456
+ }
364
457
  }
365
458
  }