@xh/hoist 73.0.0-SNAPSHOT.1746482507483 → 73.0.0-SNAPSHOT.1746483592964

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 (26) hide show
  1. package/CHANGELOG.md +0 -4
  2. package/admin/columns/Rest.ts +1 -0
  3. package/admin/columns/Tracking.ts +9 -6
  4. package/admin/tabs/activity/tracking/ActivityTrackingModel.ts +47 -14
  5. package/admin/tabs/activity/tracking/ActivityTrackingPanel.ts +1 -1
  6. package/admin/tabs/activity/tracking/detail/ActivityDetailModel.ts +51 -35
  7. package/admin/tabs/activity/tracking/detail/ActivityDetailView.ts +7 -5
  8. package/admin/tabs/client/clients/ClientsModel.ts +7 -2
  9. package/admin/tabs/client/clients/ClientsPanel.ts +3 -2
  10. package/admin/tabs/client/clients/activity/ClientDetail.scss +24 -0
  11. package/admin/tabs/client/clients/activity/ClientDetailModel.ts +83 -0
  12. package/admin/tabs/client/clients/activity/ClientDetailPanel.ts +63 -0
  13. package/build/types/admin/tabs/activity/tracking/ActivityTrackingModel.d.ts +7 -2
  14. package/build/types/admin/tabs/activity/tracking/detail/ActivityDetailModel.d.ts +18 -6
  15. package/build/types/admin/tabs/client/clients/activity/ClientDetailModel.d.ts +21 -0
  16. package/build/types/admin/tabs/client/clients/activity/ClientDetailPanel.d.ts +3 -0
  17. package/build/types/cmp/grid/Types.d.ts +1 -4
  18. package/build/types/desktop/cmp/grid/impl/filter/headerfilter/values/ValuesTabModel.d.ts +0 -2
  19. package/cmp/grid/Types.ts +1 -4
  20. package/cmp/grid/filter/GridFilterModel.ts +1 -1
  21. package/desktop/cmp/grid/impl/filter/headerfilter/values/ValuesTab.scss +0 -13
  22. package/desktop/cmp/grid/impl/filter/headerfilter/values/ValuesTab.ts +2 -29
  23. package/desktop/cmp/grid/impl/filter/headerfilter/values/ValuesTabModel.ts +15 -37
  24. package/package.json +1 -1
  25. package/promise/Promise.ts +2 -2
  26. package/tsconfig.tsbuildinfo +1 -1
package/CHANGELOG.md CHANGED
@@ -127,10 +127,6 @@
127
127
 
128
128
  ### 🎁 New Features
129
129
 
130
- * Improvements to Grid columns `HeaderFilter` component:
131
- * `GridFilterModel` `commitOnChage` now set to `false` by default
132
- * Addition of ability to append terms to active filter **only** when `commitOnChage:false`
133
- * Column header filtering functionality now similar to Excel on Windows
134
130
  * Introduced a new "JSON Search" feature to the Hoist Admin Console, accessible from the Config,
135
131
  User Preference, and JSON Blob tabs. Supports searching JSON values stored within these objects
136
132
  to filter and match data using JSON Path expressions.
@@ -17,6 +17,7 @@ export const dateCreatedNoYear: ColumnSpec = {
17
17
  ...Col.dateTimeSec,
18
18
  field: {name: 'dateCreated', type: 'date'},
19
19
  tooltip: true,
20
+ align: 'right',
20
21
  renderer: dateTimeRenderer({fmt: 'MMM DD HH:mm:ss'})
21
22
  };
22
23
 
@@ -13,6 +13,8 @@ import {fmtDate, fmtSpan, numberRenderer} from '@xh/hoist/format';
13
13
  import {Icon} from '@xh/hoist/icon';
14
14
  import {ReactElement} from 'react';
15
15
 
16
+ const autosizeMaxWidth = 400;
17
+
16
18
  export const appBuild: ColumnSpec = {
17
19
  field: {
18
20
  name: 'appBuild',
@@ -74,7 +76,7 @@ export const correlationId: ColumnSpec = {
74
76
  export const data: ColumnSpec = {
75
77
  field: {name: 'data', type: 'json'},
76
78
  width: 250,
77
- autosizeMaxWidth: 400
79
+ autosizeMaxWidth
78
80
  };
79
81
 
80
82
  export const day: ColumnSpec = {
@@ -179,7 +181,7 @@ export const error: ColumnSpec = {
179
181
  type: 'string'
180
182
  },
181
183
  width: 250,
182
- autosizeMaxWidth: 400,
184
+ autosizeMaxWidth,
183
185
  renderer: e => fmtSpan(e, {className: 'xh-font-family-mono xh-font-size-small'})
184
186
  };
185
187
 
@@ -211,7 +213,7 @@ export const msg: ColumnSpec = {
211
213
  aggregator: 'UNIQUE'
212
214
  },
213
215
  width: 250,
214
- autosizeMaxWidth: 400
216
+ autosizeMaxWidth
215
217
  };
216
218
 
217
219
  export const severity: ColumnSpec = {
@@ -267,13 +269,13 @@ export const url: ColumnSpec = {
267
269
  displayName: 'URL'
268
270
  },
269
271
  width: 250,
270
- autosizeMaxWidth: 400
272
+ autosizeMaxWidth
271
273
  };
272
274
 
273
275
  export const urlPathOnly: ColumnSpec = {
274
276
  field: url.field,
275
277
  width: 250,
276
- autosizeMaxWidth: 400,
278
+ autosizeMaxWidth,
277
279
  tooltip: true,
278
280
  renderer: v => {
279
281
  if (!v) return null;
@@ -293,7 +295,8 @@ export const userAgent: ColumnSpec = {
293
295
  isDimension: true,
294
296
  aggregator: 'UNIQUE'
295
297
  },
296
- width: 130
298
+ width: 130,
299
+ autosizeMaxWidth
297
300
  };
298
301
 
299
302
  export const userAlertedFlag: ColumnSpec = {
@@ -15,14 +15,15 @@ import {FormModel} from '@xh/hoist/cmp/form';
15
15
  import {ColumnRenderer, ColumnSpec, GridModel, TreeStyle} from '@xh/hoist/cmp/grid';
16
16
  import {GroupingChooserModel} from '@xh/hoist/cmp/grouping';
17
17
  import {HoistModel, LoadSpec, managed, PlainObject, XH} from '@xh/hoist/core';
18
- import {Cube, CubeFieldSpec, FieldSpec} from '@xh/hoist/data';
18
+ import {Cube, CubeFieldSpec, FieldSpec, StoreRecord} from '@xh/hoist/data';
19
19
  import {dateRenderer, dateTimeSecRenderer, fmtNumber, numberRenderer} from '@xh/hoist/format';
20
20
  import {action, computed, makeObservable, observable} from '@xh/hoist/mobx';
21
21
  import {LocalDate} from '@xh/hoist/utils/datetime';
22
22
  import {compact, get, isEmpty, isEqual, round} from 'lodash';
23
23
  import moment from 'moment';
24
+ import {ActivityDetailProvider} from './detail/ActivityDetailModel';
24
25
 
25
- export class ActivityTrackingModel extends HoistModel {
26
+ export class ActivityTrackingModel extends HoistModel implements ActivityDetailProvider {
26
27
  /** FormModel for server-side querying controls. */
27
28
  @managed formModel: FormModel;
28
29
 
@@ -39,6 +40,17 @@ export class ActivityTrackingModel extends HoistModel {
39
40
  */
40
41
  @observable.ref dataFields: ActivityTrackingDataFieldSpec[] = [];
41
42
 
43
+ // TODO - process two collections - one for agg grid with _agg fields left as-is, another for
44
+ // detail grid and filter that replaces (potentially multiple) agg fields with a single
45
+ // underlying field.
46
+ get dataFieldCols(): ColumnSpec[] {
47
+ return this.dataFields.map(df => ({
48
+ field: df,
49
+ renderer: this.getDfRenderer(df),
50
+ appData: {showInAggGrid: !!df.aggregator}
51
+ }));
52
+ }
53
+
42
54
  @observable showFilterChooser: boolean = false;
43
55
 
44
56
  get enabled(): boolean {
@@ -77,21 +89,18 @@ export class ActivityTrackingModel extends HoistModel {
77
89
  return this.maxRows === this.cube.store.allCount;
78
90
  }
79
91
 
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
92
  get viewManagerModel() {
92
93
  return getAppModel().viewManagerModels.activityTracking;
93
94
  }
94
95
 
96
+ //-----------------------
97
+ // ActivityDetailProvider
98
+ //-----------------------
99
+ readonly isActivityDetailProvider = true;
100
+
101
+ /** Raw leaf-level log entries for the selected aggregate record, for detail. */
102
+ @observable.ref trackLogs: PlainObject[] = [];
103
+
95
104
  private _monthFormat = 'MMM YYYY';
96
105
 
97
106
  constructor() {
@@ -121,6 +130,11 @@ export class ActivityTrackingModel extends HoistModel {
121
130
  track: () => [this.cube.records, this.dimensions],
122
131
  run: () => this.loadGridAsync(),
123
132
  debounce: 100
133
+ },
134
+ {
135
+ track: () => this.gridModel.selectedRecords,
136
+ run: recs => (this.trackLogs = this.getAllLeafRows(recs)),
137
+ debounce: 100
124
138
  }
125
139
  );
126
140
  }
@@ -273,6 +287,23 @@ export class ActivityTrackingModel extends HoistModel {
273
287
  };
274
288
  }
275
289
 
290
+ // Extract all leaf, track-entry-level rows from an aggregate record (at any level).
291
+ private getAllLeafRows(aggRecs: StoreRecord[], ret = []): PlainObject[] {
292
+ if (isEmpty(aggRecs)) return [];
293
+
294
+ aggRecs.forEach(aggRec => {
295
+ if (aggRec.children.length) {
296
+ this.getAllLeafRows(aggRec.children, ret);
297
+ } else if (aggRec.raw.leafRows) {
298
+ aggRec.raw.leafRows.forEach(leaf => {
299
+ ret.push({...leaf});
300
+ });
301
+ }
302
+ });
303
+
304
+ return ret;
305
+ }
306
+
276
307
  //------------------------
277
308
  // Impl - core data models
278
309
  //------------------------
@@ -382,11 +413,12 @@ export class ActivityTrackingModel extends HoistModel {
382
413
  const hidden = true;
383
414
  return new GridModel({
384
415
  persistWith: {...this.persistWith, path: 'aggGrid'},
416
+ selModel: 'multiple',
385
417
  enableExport: true,
386
418
  colChooserModel: true,
387
419
  treeMode: true,
388
420
  treeStyle: TreeStyle.HIGHLIGHTS_AND_BORDERS,
389
- autosizeOptions: {mode: 'managed'},
421
+ autosizeOptions: {mode: 'managed', includeCollapsedChildren: true},
390
422
  exportOptions: {filename: exportFilename('activity-summary')},
391
423
  emptyText: 'No activity reported...',
392
424
  sortBy: ['cubeLabel'],
@@ -398,6 +430,7 @@ export class ActivityTrackingModel extends HoistModel {
398
430
  displayName: 'Group'
399
431
  },
400
432
  minWidth: 100,
433
+ autosizeMaxWidth: 400,
401
434
  isTreeColumn: true,
402
435
  comparator: this.cubeLabelComparator.bind(this)
403
436
  },
@@ -155,7 +155,7 @@ const aggregateView = hoistCmp.factory<ActivityTrackingModel>(({model}) => {
155
155
  persistWith: {...model.persistWith, path: 'aggPanel'}
156
156
  },
157
157
  tbar: toolbar({
158
- // compact: true,
158
+ compact: true,
159
159
  items: [
160
160
  groupingChooser({flex: 10, maxWidth: 300}),
161
161
  filler(),
@@ -6,17 +6,32 @@
6
6
  */
7
7
  import {exportFilename} from '@xh/hoist/admin/AdminUtils';
8
8
  import * as Col from '@xh/hoist/admin/columns';
9
+ import {ActivityTrackingDataFieldSpec} from '@xh/hoist/admin/tabs/activity/tracking/datafields/DataFieldsEditorModel';
9
10
  import {FormModel} from '@xh/hoist/cmp/form';
10
- import {GridModel} from '@xh/hoist/cmp/grid';
11
- import {HoistModel, lookup, managed} from '@xh/hoist/core';
11
+ import {ColumnSpec, GridModel} from '@xh/hoist/cmp/grid';
12
+ import {HoistModel, lookup, managed, PersistOptions, PlainObject} from '@xh/hoist/core';
12
13
  import {StoreRecord} from '@xh/hoist/data';
13
14
  import {timestampReplacer} from '@xh/hoist/format';
14
15
  import {action, bindable, computed, makeObservable, observable} from '@xh/hoist/mobx';
15
- import {get} from 'lodash';
16
- import {ActivityTrackingModel} from '../ActivityTrackingModel';
16
+ import {get, isString} from 'lodash';
17
+
18
+ /**
19
+ * Interface to cover the two usages of this component - {@link ActivityTrackingModel} and {@link ClientDetailModel}
20
+ */
21
+ export interface ActivityDetailProvider {
22
+ isActivityDetailProvider: true;
23
+ trackLogs: PlainObject[];
24
+ persistWith?: PersistOptions;
25
+ colDefaults?: Record<string, Partial<ColumnSpec>>;
26
+ dataFields?: ActivityTrackingDataFieldSpec[];
27
+ dataFieldCols?: ColumnSpec[];
28
+ }
17
29
 
18
30
  export class ActivityDetailModel extends HoistModel {
19
- @lookup(ActivityTrackingModel) activityTrackingModel: ActivityTrackingModel;
31
+ @lookup(model => {
32
+ return model.isActivityDetailProvider ?? false;
33
+ })
34
+ parentModel: ActivityDetailProvider;
20
35
 
21
36
  @managed @observable.ref gridModel: GridModel;
22
37
  @managed @observable.ref formModel: FormModel;
@@ -31,14 +46,22 @@ export class ActivityDetailModel extends HoistModel {
31
46
  /** Stringified, pretty-printed, optionally path-filtered `data` payload. */
32
47
  @observable formattedData: string;
33
48
 
49
+ get dataFields(): ActivityTrackingDataFieldSpec[] {
50
+ return this.parentModel?.dataFields ?? [];
51
+ }
52
+
53
+ get dataFieldCols(): ColumnSpec[] {
54
+ return this.parentModel?.dataFieldCols ?? [];
55
+ }
56
+
34
57
  @computed
35
58
  get hasExtraTrackData(): boolean {
36
- return this.gridModel.selectedRecord?.data.data != null;
59
+ return this.gridModel?.selectedRecord?.data.data != null;
37
60
  }
38
61
 
39
62
  @computed
40
63
  get hasSelection() {
41
- return this.gridModel.selectedRecord != null;
64
+ return this.gridModel?.selectedRecord != null;
42
65
  }
43
66
 
44
67
  constructor() {
@@ -47,17 +70,22 @@ export class ActivityDetailModel extends HoistModel {
47
70
  }
48
71
 
49
72
  override onLinked() {
50
- this.markPersist('formattedDataFilterPath', this.activityTrackingModel.persistWith);
73
+ if (this.parentModel.persistWith) {
74
+ this.persistWith = {...this.parentModel.persistWith, path: 'activityDetail'};
75
+ this.markPersist('formattedDataFilterPath', {
76
+ path: `${this.persistWith.path}.formattedDataFilterPath`
77
+ });
78
+ }
51
79
 
52
80
  this.addReaction(
53
81
  {
54
- track: () => this.activityTrackingModel.dataFields,
82
+ track: () => this.dataFields,
55
83
  run: () => this.createAndSetCoreModels(),
56
84
  fireImmediately: true
57
85
  },
58
86
  {
59
- track: () => this.activityTrackingModel.gridModel.selectedRecord,
60
- run: aggRec => this.showActivityEntriesAsync(aggRec)
87
+ track: () => this.parentModel.trackLogs,
88
+ run: trackLogs => this.showTrackLogsAsync(trackLogs)
61
89
  },
62
90
  {
63
91
  track: () => this.gridModel.selectedRecord,
@@ -73,29 +101,12 @@ export class ActivityDetailModel extends HoistModel {
73
101
  //------------------
74
102
  // Implementation
75
103
  //------------------
76
- private async showActivityEntriesAsync(aggRec: StoreRecord) {
77
- const {gridModel} = this,
78
- leaves = this.getAllLeafRows(aggRec);
79
-
80
- gridModel.loadData(leaves);
104
+ private async showTrackLogsAsync(trackLogs: PlainObject[]) {
105
+ const {gridModel} = this;
106
+ gridModel.loadData(trackLogs);
81
107
  await gridModel.preSelectFirstAsync();
82
108
  }
83
109
 
84
- // Extract all leaf, track-entry-level rows from an aggregate record (at any level).
85
- private getAllLeafRows(aggRec: StoreRecord, ret = []) {
86
- if (!aggRec) return [];
87
-
88
- if (aggRec.children.length) {
89
- aggRec.children.forEach(childRec => this.getAllLeafRows(childRec, ret));
90
- } else if (aggRec.raw.leafRows) {
91
- aggRec.raw.leafRows.forEach(leaf => {
92
- ret.push({...leaf});
93
- });
94
- }
95
-
96
- return ret;
97
- }
98
-
99
110
  /** Extract data from a (detail) grid record and flush it into our form for display. */
100
111
  @action
101
112
  private showEntryDetail(detailRec: StoreRecord) {
@@ -139,11 +150,13 @@ export class ActivityDetailModel extends HoistModel {
139
150
  }
140
151
 
141
152
  private createGridModel(): GridModel {
142
- const hidden = true,
153
+ const {persistWith, parentModel, dataFieldCols} = this,
154
+ colDefaults = parentModel.colDefaults ?? {},
155
+ hidden = true,
143
156
  pinned = true;
144
157
 
145
158
  return new GridModel({
146
- persistWith: {...this.activityTrackingModel.persistWith, path: 'detailGrid'},
159
+ persistWith: persistWith ? {...persistWith, path: `${persistWith.path}.grid`} : null,
147
160
  sortBy: 'dateCreated|desc',
148
161
  colChooserModel: true,
149
162
  enableExport: true,
@@ -174,8 +187,11 @@ export class ActivityDetailModel extends HoistModel {
174
187
  {...Col.urlPathOnly},
175
188
  {...Col.data, hidden},
176
189
  {...Col.dateCreatedNoYear, displayName: 'Timestamp'},
177
- ...this.activityTrackingModel.dataFieldCols
178
- ]
190
+ ...dataFieldCols
191
+ ].map(it => {
192
+ const fieldName = isString(it.field) ? it.field : it.field.name;
193
+ return {...it, ...colDefaults[fieldName]};
194
+ })
179
195
  });
180
196
  }
181
197
 
@@ -37,11 +37,12 @@ export const activityDetailView = hoistCmp.factory({
37
37
  const tbar = hoistCmp.factory<ActivityDetailModel>(({model}) => {
38
38
  const {gridModel} = model;
39
39
  return toolbar({
40
+ compact: true,
40
41
  items: [
41
42
  filler(),
42
43
  gridCountLabel({unit: 'entry'}),
43
44
  '-',
44
- gridFindField({gridModel, key: gridModel.xhId}),
45
+ gridFindField({gridModel, key: gridModel.xhId, width: 250}),
45
46
  colChooserButton({gridModel}),
46
47
  exportButton()
47
48
  ]
@@ -50,6 +51,8 @@ const tbar = hoistCmp.factory<ActivityDetailModel>(({model}) => {
50
51
 
51
52
  // Discrete outer panel to retain sizing across master/detail selection changes.
52
53
  const detailRecPanel = hoistCmp.factory<ActivityDetailModel>(({model}) => {
54
+ const {persistWith} = model;
55
+
53
56
  return panel({
54
57
  collapsedTitle: 'Activity Details',
55
58
  collapsedIcon: Icon.info(),
@@ -57,10 +60,9 @@ const detailRecPanel = hoistCmp.factory<ActivityDetailModel>(({model}) => {
57
60
  modelConfig: {
58
61
  side: 'bottom',
59
62
  defaultSize: 400,
60
- persistWith: {
61
- ...model.activityTrackingModel.persistWith,
62
- path: 'singleActivityDetailPanel'
63
- }
63
+ persistWith: persistWith
64
+ ? {...persistWith, path: `${persistWith.path}.singleActivityDetailPanel`}
65
+ : null
64
66
  },
65
67
  item: detailRecForm()
66
68
  });
@@ -73,6 +73,8 @@ export class ClientsModel extends BaseAdminTabModel {
73
73
  }
74
74
 
75
75
  override async doLoadAsync(loadSpec: LoadSpec) {
76
+ const {gridModel} = this;
77
+
76
78
  try {
77
79
  const data = await XH.fetchJson({
78
80
  url: 'clientAdmin/allClients',
@@ -80,12 +82,15 @@ export class ClientsModel extends BaseAdminTabModel {
80
82
  });
81
83
  if (loadSpec.isStale) return;
82
84
 
83
- this.gridModel.loadData(data);
85
+ gridModel.loadData(data);
86
+ gridModel.preSelectFirstAsync();
84
87
  runInAction(() => {
85
88
  this.lastRefresh = Date.now();
86
89
  });
87
90
  } catch (e) {
88
- if (loadSpec.isStale) return;
91
+ if (loadSpec.isStale || loadSpec.isAutoRefresh) return;
92
+
93
+ gridModel.clear();
89
94
  XH.handleException(e, {alertType: 'toast'});
90
95
  }
91
96
  }
@@ -4,9 +4,10 @@
4
4
  *
5
5
  * Copyright © 2025 Extremely Heavy Industries Inc.
6
6
  */
7
+ import {clientDetailPanel} from '@xh/hoist/admin/tabs/client/clients/activity/ClientDetailPanel';
7
8
  import {errorMessage} from '@xh/hoist/cmp/error';
8
9
  import {grid, gridCountLabel} from '@xh/hoist/cmp/grid';
9
- import {filler, fragment, p} from '@xh/hoist/cmp/layout';
10
+ import {filler, fragment, hframe, p} from '@xh/hoist/cmp/layout';
10
11
  import {relativeTimestamp} from '@xh/hoist/cmp/relativetimestamp';
11
12
  import {storeFilterField} from '@xh/hoist/cmp/store';
12
13
  import {creates, hoistCmp, XH} from '@xh/hoist/core';
@@ -48,7 +49,7 @@ export const clientsPanel = hoistCmp.factory<ClientsModel>({
48
49
  colChooserButton(),
49
50
  exportButton()
50
51
  ],
51
- item: grid(),
52
+ items: hframe(grid(), clientDetailPanel()),
52
53
  mask: 'onLoad',
53
54
  ref: model.viewRef
54
55
  });
@@ -0,0 +1,24 @@
1
+ .xh-admin-client-detail {
2
+ &__header {
3
+ align-items: center;
4
+ background-color: var(--xh-grid-bg-odd);
5
+ border-bottom: var(--xh-border-solid);
6
+ padding: var(--xh-pad-px);
7
+
8
+ h2 {
9
+ flex: 1;
10
+ font-weight: normal;
11
+ margin: 0;
12
+
13
+ .xh-icon {
14
+ font-size: 0.8em;
15
+ margin-right: 0.3em;
16
+ }
17
+ }
18
+
19
+ &__meta {
20
+ flex: none;
21
+ align-items: flex-end;
22
+ }
23
+ }
24
+ }
@@ -0,0 +1,83 @@
1
+ import {ClientsModel} from '@xh/hoist/admin/tabs/client/clients/ClientsModel';
2
+ import {ColumnSpec} from '@xh/hoist/cmp/grid';
3
+ import {HoistModel, LoadSpec, lookup, PlainObject, XH} from '@xh/hoist/core';
4
+ import {StoreRecord} from '@xh/hoist/data';
5
+ import {bindable, computed, makeObservable} from '@xh/hoist/mobx';
6
+ import {ReactNode} from 'react';
7
+ import {ActivityDetailProvider} from '../../../activity/tracking/detail/ActivityDetailModel';
8
+
9
+ export class ClientDetailModel extends HoistModel implements ActivityDetailProvider {
10
+ @lookup(ClientsModel) clientsModel: ClientsModel;
11
+
12
+ readonly isActivityDetailProvider = true;
13
+
14
+ /** Client tabID for which to load and show activity. */
15
+ @bindable tabId: string;
16
+ @bindable.ref trackLogs: PlainObject[] = [];
17
+
18
+ get selectedRec(): StoreRecord {
19
+ return this.clientsModel?.gridModel.selectedRecord;
20
+ }
21
+
22
+ @computed
23
+ get hasSelection(): boolean {
24
+ return !!this.selectedRec;
25
+ }
26
+
27
+ get title(): ReactNode {
28
+ return this.selectedRec?.data.user ?? 'Client Activity';
29
+ }
30
+
31
+ /** For child {@link ActivityDetailModel}. */
32
+ readonly colDefaults: Record<string, Partial<ColumnSpec>> = {
33
+ username: {hidden: true},
34
+ impersonatingFlag: {hidden: true},
35
+ tabId: {hidden: true},
36
+ loadId: {hidden: false},
37
+ device: {hidden: true}
38
+ };
39
+
40
+ constructor() {
41
+ super();
42
+ makeObservable(this);
43
+ }
44
+
45
+ override onLinked() {
46
+ super.onLinked();
47
+
48
+ this.addReaction(
49
+ {
50
+ track: () => this.clientsModel.gridModel.selectedRecord,
51
+ run: rec => (this.tabId = rec?.get('tabId'))
52
+ },
53
+ {
54
+ track: () => this.tabId,
55
+ run: () => this.loadAsync(),
56
+ debounce: 300
57
+ }
58
+ );
59
+ }
60
+
61
+ override async doLoadAsync(loadSpec: LoadSpec) {
62
+ const {tabId} = this;
63
+
64
+ if (!tabId) {
65
+ this.trackLogs = [];
66
+ return;
67
+ }
68
+
69
+ try {
70
+ this.trackLogs = await XH.postJson({
71
+ url: 'trackLogAdmin',
72
+ body: {
73
+ filters: {field: 'tabId', op: '=', value: tabId}
74
+ }
75
+ });
76
+ } catch (e) {
77
+ if (loadSpec.isStale || !loadSpec.isAutoRefresh) return;
78
+
79
+ XH.handleException(e, {alertType: 'toast'});
80
+ this.trackLogs = [];
81
+ }
82
+ }
83
+ }
@@ -0,0 +1,63 @@
1
+ import {isOpen} from '@xh/hoist/admin/columns';
2
+ import {activityDetailView} from '@xh/hoist/admin/tabs/activity/tracking/detail/ActivityDetailView';
3
+ import {ClientDetailModel} from '@xh/hoist/admin/tabs/client/clients/activity/ClientDetailModel';
4
+ import {h2, hbox, placeholder, vbox} from '@xh/hoist/cmp/layout';
5
+ import {mask} from '@xh/hoist/cmp/mask';
6
+ import {relativeTimestamp} from '@xh/hoist/cmp/relativetimestamp';
7
+ import {creates, hoistCmp} from '@xh/hoist/core';
8
+ import {panel} from '@xh/hoist/desktop/cmp/panel';
9
+ import {Icon} from '@xh/hoist/icon';
10
+ import './ClientDetail.scss';
11
+
12
+ export const clientDetailPanel = hoistCmp.factory({
13
+ displayName: 'ClientDetailPanel',
14
+ model: creates(ClientDetailModel),
15
+
16
+ render({model}) {
17
+ return panel({
18
+ className: 'xh-admin-client-detail',
19
+ collapsedTitle: model.title,
20
+ collapsedIcon: Icon.analytics(),
21
+ compactHeader: true,
22
+ modelConfig: {side: 'right', defaultSize: '40%'},
23
+ item: model.hasSelection
24
+ ? clientDetail()
25
+ : placeholder(Icon.analytics(), 'Select a client to view activity...')
26
+ });
27
+ }
28
+ });
29
+
30
+ const clientDetail = hoistCmp.factory<ClientDetailModel>(({model}) => {
31
+ const {data} = model.selectedRec;
32
+ return panel({
33
+ items: [
34
+ hbox({
35
+ className: 'xh-admin-client-detail__header',
36
+ items: [
37
+ h2(isOpen.renderer(data.isOpen, null), data.user),
38
+ vbox({
39
+ className: 'xh-admin-client-detail__header__meta',
40
+ items: [
41
+ relativeTimestamp({
42
+ timestamp: data.createdTime,
43
+ options: {prefix: 'Session established'}
44
+ }),
45
+ relativeTimestamp({
46
+ timestamp: data.lastReceivedTime,
47
+ options: {prefix: 'Last heartbeat', emptyResult: 'No heartbeat yet'}
48
+ })
49
+ ]
50
+ })
51
+ ]
52
+ }),
53
+ panel({
54
+ item: activityDetailView(),
55
+ mask: mask({
56
+ bind: model.loadModel,
57
+ spinner: true,
58
+ message: 'Loading activity...'
59
+ })
60
+ })
61
+ ]
62
+ });
63
+ });
@@ -6,7 +6,8 @@ import { GroupingChooserModel } from '@xh/hoist/cmp/grouping';
6
6
  import { HoistModel, LoadSpec, PlainObject } from '@xh/hoist/core';
7
7
  import { Cube } from '@xh/hoist/data';
8
8
  import { LocalDate } from '@xh/hoist/utils/datetime';
9
- export declare class ActivityTrackingModel extends HoistModel {
9
+ import { ActivityDetailProvider } from './detail/ActivityDetailModel';
10
+ export declare class ActivityTrackingModel extends HoistModel implements ActivityDetailProvider {
10
11
  /** FormModel for server-side querying controls. */
11
12
  formModel: FormModel;
12
13
  /** Models for data-handling components - can be rebuilt due to change in dataFields. */
@@ -20,6 +21,7 @@ export declare class ActivityTrackingModel extends HoistModel {
20
21
  * and promoted to top-level columns in the grids. Supports dot-delimited paths as names.
21
22
  */
22
23
  dataFields: ActivityTrackingDataFieldSpec[];
24
+ get dataFieldCols(): ColumnSpec[];
23
25
  showFilterChooser: boolean;
24
26
  get enabled(): boolean;
25
27
  get dimensions(): string[];
@@ -32,8 +34,10 @@ export declare class ActivityTrackingModel extends HoistModel {
32
34
  get maxRows(): number;
33
35
  /** True if data loaded from the server has been topped by maxRows. */
34
36
  get maxRowsReached(): boolean;
35
- get dataFieldCols(): ColumnSpec[];
36
37
  get viewManagerModel(): import("../../../../cmp/viewmanager").ViewManagerModel<PlainObject>;
38
+ readonly isActivityDetailProvider = true;
39
+ /** Raw leaf-level log entries for the selected aggregate record, for detail. */
40
+ trackLogs: PlainObject[];
37
41
  private _monthFormat;
38
42
  constructor();
39
43
  doLoadAsync(loadSpec: LoadSpec): Promise<void>;
@@ -49,6 +53,7 @@ export declare class ActivityTrackingModel extends HoistModel {
49
53
  private cubeLabelComparator;
50
54
  private getComparableValForDim;
51
55
  private get query();
56
+ private getAllLeafRows;
52
57
  private createAndSetCoreModels;
53
58
  private createCube;
54
59
  private createFilterChooserModel;