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