@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.
- package/CHANGELOG.md +0 -4
- package/admin/columns/Rest.ts +1 -0
- package/admin/columns/Tracking.ts +9 -6
- package/admin/tabs/activity/tracking/ActivityTrackingModel.ts +47 -14
- package/admin/tabs/activity/tracking/ActivityTrackingPanel.ts +1 -1
- package/admin/tabs/activity/tracking/detail/ActivityDetailModel.ts +51 -35
- package/admin/tabs/activity/tracking/detail/ActivityDetailView.ts +7 -5
- package/admin/tabs/client/clients/ClientsModel.ts +7 -2
- package/admin/tabs/client/clients/ClientsPanel.ts +3 -2
- package/admin/tabs/client/clients/activity/ClientDetail.scss +24 -0
- package/admin/tabs/client/clients/activity/ClientDetailModel.ts +83 -0
- package/admin/tabs/client/clients/activity/ClientDetailPanel.ts +63 -0
- package/build/types/admin/tabs/activity/tracking/ActivityTrackingModel.d.ts +7 -2
- package/build/types/admin/tabs/activity/tracking/detail/ActivityDetailModel.d.ts +18 -6
- package/build/types/admin/tabs/client/clients/activity/ClientDetailModel.d.ts +21 -0
- package/build/types/admin/tabs/client/clients/activity/ClientDetailPanel.d.ts +3 -0
- package/build/types/cmp/grid/Types.d.ts +1 -4
- package/build/types/desktop/cmp/grid/impl/filter/headerfilter/values/ValuesTabModel.d.ts +0 -2
- package/cmp/grid/Types.ts +1 -4
- package/cmp/grid/filter/GridFilterModel.ts +1 -1
- package/desktop/cmp/grid/impl/filter/headerfilter/values/ValuesTab.scss +0 -13
- package/desktop/cmp/grid/impl/filter/headerfilter/values/ValuesTab.ts +2 -29
- package/desktop/cmp/grid/impl/filter/headerfilter/values/ValuesTabModel.ts +15 -37
- package/package.json +1 -1
- package/promise/Promise.ts +2 -2
- 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.
|
package/admin/columns/Rest.ts
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
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
|
|
272
|
+
autosizeMaxWidth
|
|
271
273
|
};
|
|
272
274
|
|
|
273
275
|
export const urlPathOnly: ColumnSpec = {
|
|
274
276
|
field: url.field,
|
|
275
277
|
width: 250,
|
|
276
|
-
autosizeMaxWidth
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
|
59
|
+
return this.gridModel?.selectedRecord?.data.data != null;
|
|
37
60
|
}
|
|
38
61
|
|
|
39
62
|
@computed
|
|
40
63
|
get hasSelection() {
|
|
41
|
-
return this.gridModel
|
|
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
|
-
|
|
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.
|
|
82
|
+
track: () => this.dataFields,
|
|
55
83
|
run: () => this.createAndSetCoreModels(),
|
|
56
84
|
fireImmediately: true
|
|
57
85
|
},
|
|
58
86
|
{
|
|
59
|
-
track: () => this.
|
|
60
|
-
run:
|
|
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
|
|
77
|
-
const {gridModel} = this
|
|
78
|
-
|
|
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
|
|
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: {...
|
|
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
|
-
...
|
|
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
|
-
...
|
|
62
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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;
|