@xh/hoist 73.0.0-SNAPSHOT.1745976013413 → 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.
- package/CHANGELOG.md +10 -1
- package/admin/AdminUtils.ts +5 -0
- package/admin/App.scss +6 -0
- package/admin/AppModel.ts +19 -7
- package/admin/{tabs/client/clients/ClientsColumns.ts → columns/Clients.ts} +20 -53
- package/admin/columns/Core.ts +34 -35
- package/admin/columns/Rest.ts +8 -0
- package/admin/columns/Tracking.ts +144 -42
- package/admin/columns/index.ts +1 -0
- package/admin/tabs/activity/tracking/ActivityTracking.scss +18 -0
- package/admin/tabs/activity/tracking/ActivityTrackingModel.ts +309 -210
- 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 +123 -59
- package/admin/tabs/activity/tracking/detail/ActivityDetailView.ts +110 -54
- package/admin/tabs/client/ClientTab.ts +2 -4
- package/admin/tabs/client/clients/ClientsModel.ts +10 -11
- package/admin/tabs/cluster/instances/memory/MemoryMonitorModel.ts +1 -2
- package/admin/tabs/general/GeneralTab.ts +2 -0
- package/build/types/admin/AdminUtils.d.ts +2 -0
- package/build/types/admin/AppModel.d.ts +4 -1
- package/build/types/admin/{tabs/client/clients/ClientsColumns.d.ts → columns/Clients.d.ts} +3 -7
- package/build/types/admin/columns/Core.d.ts +5 -5
- package/build/types/admin/columns/Rest.d.ts +1 -0
- package/build/types/admin/columns/Tracking.d.ts +13 -4
- package/build/types/admin/columns/index.d.ts +1 -0
- package/build/types/admin/tabs/activity/tracking/ActivityTrackingModel.d.ts +31 -28
- 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 +14 -1
- package/build/types/cmp/form/FormModel.d.ts +19 -30
- package/build/types/cmp/form/field/SubformsFieldModel.d.ts +25 -22
- 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 +20 -28
- package/cmp/form/field/SubformsFieldModel.ts +28 -22
- package/cmp/grid/columns/DatesTimes.ts +1 -2
- 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/svc/TrackService.ts +2 -0
- 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/tabs/{client → general}/feedback/FeedbackPanel.ts +0 -0
- /package/build/types/admin/tabs/{client → general}/feedback/FeedbackPanel.d.ts +0 -0
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import {ActivityTrackingModel} from '@xh/hoist/admin/tabs/activity/tracking/ActivityTrackingModel';
|
|
2
|
+
import {FormModel, SubformsFieldModel} from '@xh/hoist/cmp/form';
|
|
3
|
+
import {HoistModel, managed} from '@xh/hoist/core';
|
|
4
|
+
import {AggregatorToken, FieldType, genDisplayName, required} from '@xh/hoist/data';
|
|
5
|
+
import {action, observable, makeObservable} from '@xh/hoist/mobx';
|
|
6
|
+
import {computed} from '@xh/hoist/mobx';
|
|
7
|
+
import {last, uniqBy} from 'lodash';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Slimmed down {@link CubeFieldSpec} for persisted specs of fields to be extracted from the `data`
|
|
11
|
+
* block of loaded track statements and promoted to top-level columns in the grids. These are the
|
|
12
|
+
* entities (stored on parent `ActivityTrackingModel`) that are edited by the this component.
|
|
13
|
+
*/
|
|
14
|
+
export interface ActivityTrackingDataFieldSpec {
|
|
15
|
+
/**
|
|
16
|
+
* Path to field data within the `data` block of each track log entry. Can be dot-delimited for
|
|
17
|
+
* nested data (e.g. `timings.preAuth`). See {@link ActivityTrackingModel.processRawTrackLog}.
|
|
18
|
+
*/
|
|
19
|
+
path: string;
|
|
20
|
+
/**
|
|
21
|
+
* Normalized name for the field for use in Cube/Grid - adds `df_` prefix to avoid conflicts
|
|
22
|
+
* and strips out dot-delimiters. See {@link ActivityTrackingModel.setDataFields}.
|
|
23
|
+
*/
|
|
24
|
+
name: string;
|
|
25
|
+
displayName?: string;
|
|
26
|
+
type?: FieldType;
|
|
27
|
+
isDimension?: boolean;
|
|
28
|
+
aggregator?: AggregatorToken;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export class DataFieldsEditorModel extends HoistModel {
|
|
32
|
+
@observable showEditor = false;
|
|
33
|
+
|
|
34
|
+
@managed formModel: FormModel;
|
|
35
|
+
private parentModel: ActivityTrackingModel;
|
|
36
|
+
|
|
37
|
+
aggTokens: AggregatorToken[] = ['AVG', 'MAX', 'MIN', 'SINGLE', 'SUM', 'UNIQUE'];
|
|
38
|
+
|
|
39
|
+
get dataFields(): SubformsFieldModel {
|
|
40
|
+
return this.formModel.fields.dataFields as SubformsFieldModel;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
@computed
|
|
44
|
+
get appliedDataFieldCount(): number {
|
|
45
|
+
return this.parentModel.dataFields.length;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
get hasAppliedDataFields(): boolean {
|
|
49
|
+
return this.appliedDataFieldCount > 0;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
constructor(parentModel: ActivityTrackingModel) {
|
|
53
|
+
super();
|
|
54
|
+
makeObservable(this);
|
|
55
|
+
|
|
56
|
+
this.parentModel = parentModel;
|
|
57
|
+
|
|
58
|
+
this.formModel = new FormModel({
|
|
59
|
+
fields: [
|
|
60
|
+
{
|
|
61
|
+
name: 'dataFields',
|
|
62
|
+
subforms: {
|
|
63
|
+
fields: [
|
|
64
|
+
{name: 'path', rules: [required]},
|
|
65
|
+
{name: 'displayName'},
|
|
66
|
+
{name: 'type', initialValue: 'auto'},
|
|
67
|
+
{name: 'isDimension'},
|
|
68
|
+
{name: 'aggregator'}
|
|
69
|
+
]
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
]
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
this.addReaction({
|
|
76
|
+
track: () => this.parentModel.dataFields,
|
|
77
|
+
run: () => this.syncFromParent(),
|
|
78
|
+
fireImmediately: true
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
@action
|
|
83
|
+
show() {
|
|
84
|
+
this.syncFromParent();
|
|
85
|
+
this.showEditor = true;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
@action
|
|
89
|
+
applyAndClose() {
|
|
90
|
+
this.syncToParent();
|
|
91
|
+
this.showEditor = false;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
@action
|
|
95
|
+
close() {
|
|
96
|
+
this.showEditor = false;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
addField() {
|
|
100
|
+
this.dataFields.add();
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
cloneField(formModel: FormModel) {
|
|
104
|
+
const {dataFields} = this,
|
|
105
|
+
srcIdx = dataFields.value.indexOf(formModel);
|
|
106
|
+
|
|
107
|
+
dataFields.add({initialValues: formModel.getData(), index: srcIdx + 1});
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
private syncFromParent() {
|
|
111
|
+
this.formModel.init({
|
|
112
|
+
dataFields: this.parentModel.dataFields
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Normalize field specs and set onto parent.
|
|
118
|
+
* Note, will de-dupe fields by name w/o any alert to user.
|
|
119
|
+
*/
|
|
120
|
+
private syncToParent() {
|
|
121
|
+
const raw = this.formModel.getData().dataFields,
|
|
122
|
+
specs: ActivityTrackingDataFieldSpec[] = raw.map(it => {
|
|
123
|
+
const {displayName, path, aggregator: agg} = it;
|
|
124
|
+
return {
|
|
125
|
+
...it,
|
|
126
|
+
name: 'df_' + path.replaceAll('.', '') + (agg ? `_${agg}` : ''),
|
|
127
|
+
displayName: displayName || genDisplayName(last(path.split('.')))
|
|
128
|
+
};
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
this.parentModel.setDataFields(uniqBy(specs, 'name'));
|
|
132
|
+
}
|
|
133
|
+
}
|
|
@@ -9,15 +9,32 @@ import * as Col from '@xh/hoist/admin/columns';
|
|
|
9
9
|
import {FormModel} from '@xh/hoist/cmp/form';
|
|
10
10
|
import {GridModel} from '@xh/hoist/cmp/grid';
|
|
11
11
|
import {HoistModel, lookup, managed} from '@xh/hoist/core';
|
|
12
|
-
import {
|
|
12
|
+
import {StoreRecord} from '@xh/hoist/data';
|
|
13
|
+
import {timestampReplacer} from '@xh/hoist/format';
|
|
14
|
+
import {action, bindable, computed, makeObservable, observable} from '@xh/hoist/mobx';
|
|
15
|
+
import {get} from 'lodash';
|
|
13
16
|
import {ActivityTrackingModel} from '../ActivityTrackingModel';
|
|
14
|
-
import {fmtJson, timestampReplacer} from '@xh/hoist/format';
|
|
15
17
|
|
|
16
18
|
export class ActivityDetailModel extends HoistModel {
|
|
17
19
|
@lookup(ActivityTrackingModel) activityTrackingModel: ActivityTrackingModel;
|
|
18
|
-
|
|
19
|
-
@managed
|
|
20
|
-
@observable
|
|
20
|
+
|
|
21
|
+
@managed @observable.ref gridModel: GridModel;
|
|
22
|
+
@managed @observable.ref formModel: FormModel;
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Optional dot-delimited path(s) to filter the displayed `data` payload down to a particular
|
|
26
|
+
* node or nodes, for easier browsing of records with a large data payload. Multiple paths
|
|
27
|
+
* can be separated with `|`.
|
|
28
|
+
*/
|
|
29
|
+
@bindable formattedDataFilterPath: string;
|
|
30
|
+
|
|
31
|
+
/** Stringified, pretty-printed, optionally path-filtered `data` payload. */
|
|
32
|
+
@observable formattedData: string;
|
|
33
|
+
|
|
34
|
+
@computed
|
|
35
|
+
get hasExtraTrackData(): boolean {
|
|
36
|
+
return this.gridModel.selectedRecord?.data.data != null;
|
|
37
|
+
}
|
|
21
38
|
|
|
22
39
|
@computed
|
|
23
40
|
get hasSelection() {
|
|
@@ -30,48 +47,14 @@ export class ActivityDetailModel extends HoistModel {
|
|
|
30
47
|
}
|
|
31
48
|
|
|
32
49
|
override onLinked() {
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
this.gridModel = new GridModel({
|
|
36
|
-
sortBy: 'dateCreated|desc',
|
|
37
|
-
colChooserModel: true,
|
|
38
|
-
enableExport: true,
|
|
39
|
-
filterModel: false,
|
|
40
|
-
exportOptions: {
|
|
41
|
-
columns: 'ALL',
|
|
42
|
-
filename: exportFilename('activity-detail')
|
|
43
|
-
},
|
|
44
|
-
emptyText: 'Select a group on the left to see detailed tracking logs.',
|
|
45
|
-
columns: [
|
|
46
|
-
{...Col.impersonatingFlag},
|
|
47
|
-
{...Col.entryId, hidden},
|
|
48
|
-
{...Col.username},
|
|
49
|
-
{...Col.impersonating, hidden},
|
|
50
|
-
{...Col.category},
|
|
51
|
-
{...Col.msg},
|
|
52
|
-
{...Col.browser},
|
|
53
|
-
{...Col.device},
|
|
54
|
-
{...Col.userAgent, hidden},
|
|
55
|
-
{...Col.appVersion},
|
|
56
|
-
{...Col.appEnvironment, hidden},
|
|
57
|
-
{...Col.data, hidden},
|
|
58
|
-
{...Col.url},
|
|
59
|
-
{...Col.correlationId},
|
|
60
|
-
{...Col.instance, hidden},
|
|
61
|
-
{...Col.severity, hidden},
|
|
62
|
-
{...Col.elapsed},
|
|
63
|
-
{...Col.dateCreatedWithSec, displayName: 'Timestamp'}
|
|
64
|
-
]
|
|
65
|
-
});
|
|
66
|
-
|
|
67
|
-
this.formModel = new FormModel({
|
|
68
|
-
readonly: true,
|
|
69
|
-
fields: this.gridModel
|
|
70
|
-
.getLeafColumns()
|
|
71
|
-
.map(it => ({name: it.field, displayName: it.headerName as string}))
|
|
72
|
-
});
|
|
50
|
+
this.markPersist('formattedDataFilterPath', this.activityTrackingModel.persistWith);
|
|
73
51
|
|
|
74
52
|
this.addReaction(
|
|
53
|
+
{
|
|
54
|
+
track: () => this.activityTrackingModel.dataFields,
|
|
55
|
+
run: () => this.createAndSetCoreModels(),
|
|
56
|
+
fireImmediately: true
|
|
57
|
+
},
|
|
75
58
|
{
|
|
76
59
|
track: () => this.activityTrackingModel.gridModel.selectedRecord,
|
|
77
60
|
run: aggRec => this.showActivityEntriesAsync(aggRec)
|
|
@@ -79,11 +62,18 @@ export class ActivityDetailModel extends HoistModel {
|
|
|
79
62
|
{
|
|
80
63
|
track: () => this.gridModel.selectedRecord,
|
|
81
64
|
run: detailRec => this.showEntryDetail(detailRec)
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
track: () => this.formattedDataFilterPath,
|
|
68
|
+
run: () => this.updateFormattedData()
|
|
82
69
|
}
|
|
83
70
|
);
|
|
84
71
|
}
|
|
85
72
|
|
|
86
|
-
|
|
73
|
+
//------------------
|
|
74
|
+
// Implementation
|
|
75
|
+
//------------------
|
|
76
|
+
private async showActivityEntriesAsync(aggRec: StoreRecord) {
|
|
87
77
|
const {gridModel} = this,
|
|
88
78
|
leaves = this.getAllLeafRows(aggRec);
|
|
89
79
|
|
|
@@ -92,7 +82,7 @@ export class ActivityDetailModel extends HoistModel {
|
|
|
92
82
|
}
|
|
93
83
|
|
|
94
84
|
// Extract all leaf, track-entry-level rows from an aggregate record (at any level).
|
|
95
|
-
private getAllLeafRows(aggRec, ret = []) {
|
|
85
|
+
private getAllLeafRows(aggRec: StoreRecord, ret = []) {
|
|
96
86
|
if (!aggRec) return [];
|
|
97
87
|
|
|
98
88
|
if (aggRec.children.length) {
|
|
@@ -106,22 +96,96 @@ export class ActivityDetailModel extends HoistModel {
|
|
|
106
96
|
return ret;
|
|
107
97
|
}
|
|
108
98
|
|
|
109
|
-
|
|
110
|
-
|
|
99
|
+
/** Extract data from a (detail) grid record and flush it into our form for display. */
|
|
100
|
+
@action
|
|
101
|
+
private showEntryDetail(detailRec: StoreRecord) {
|
|
102
|
+
this.formModel.init(detailRec?.data ?? {});
|
|
103
|
+
this.updateFormattedData();
|
|
104
|
+
}
|
|
105
|
+
|
|
111
106
|
@action
|
|
112
|
-
private
|
|
113
|
-
const
|
|
114
|
-
trackData =
|
|
107
|
+
private updateFormattedData() {
|
|
108
|
+
const {gridModel, formattedDataFilterPath} = this,
|
|
109
|
+
trackData = gridModel.selectedRecord?.data.data;
|
|
115
110
|
|
|
116
|
-
|
|
111
|
+
if (!trackData) {
|
|
112
|
+
this.formattedData = '';
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
117
115
|
|
|
118
|
-
let
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
116
|
+
let parsed = JSON.parse(trackData),
|
|
117
|
+
toFormat = parsed;
|
|
118
|
+
|
|
119
|
+
if (formattedDataFilterPath) {
|
|
120
|
+
const paths = formattedDataFilterPath.split('|');
|
|
121
|
+
if (paths.length > 1) {
|
|
122
|
+
toFormat = {};
|
|
123
|
+
paths.forEach(path => (toFormat[path.trim()] = get(parsed, path.trim())));
|
|
124
|
+
} else {
|
|
125
|
+
toFormat = get(parsed, formattedDataFilterPath.trim());
|
|
126
|
+
}
|
|
123
127
|
}
|
|
124
128
|
|
|
125
|
-
this.formattedData =
|
|
129
|
+
this.formattedData = JSON.stringify(toFormat, timestampReplacer(), 2);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
//------------------------
|
|
133
|
+
// Core data-handling models
|
|
134
|
+
//------------------------
|
|
135
|
+
@action
|
|
136
|
+
private createAndSetCoreModels() {
|
|
137
|
+
this.gridModel = this.createGridModel();
|
|
138
|
+
this.formModel = this.createSingleEntryFormModel();
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
private createGridModel(): GridModel {
|
|
142
|
+
const hidden = true,
|
|
143
|
+
pinned = true;
|
|
144
|
+
|
|
145
|
+
return new GridModel({
|
|
146
|
+
persistWith: {...this.activityTrackingModel.persistWith, path: 'detailGrid'},
|
|
147
|
+
sortBy: 'dateCreated|desc',
|
|
148
|
+
colChooserModel: true,
|
|
149
|
+
enableExport: true,
|
|
150
|
+
filterModel: false,
|
|
151
|
+
exportOptions: {
|
|
152
|
+
columns: 'ALL',
|
|
153
|
+
filename: exportFilename('activity-detail')
|
|
154
|
+
},
|
|
155
|
+
emptyText: 'Select a group on the left to see detailed tracking logs.',
|
|
156
|
+
columns: [
|
|
157
|
+
{...Col.entryId, hidden, pinned},
|
|
158
|
+
{...Col.severityIcon, pinned},
|
|
159
|
+
{...Col.impersonatingFlag, pinned},
|
|
160
|
+
{...Col.username, pinned},
|
|
161
|
+
{...Col.impersonating, hidden},
|
|
162
|
+
{...Col.category},
|
|
163
|
+
{...Col.msg},
|
|
164
|
+
{...Col.elapsed},
|
|
165
|
+
{...Col.deviceIcon},
|
|
166
|
+
{...Col.browser, hidden},
|
|
167
|
+
{...Col.userAgent, hidden},
|
|
168
|
+
{...Col.appVersion, hidden},
|
|
169
|
+
{...Col.loadId, hidden},
|
|
170
|
+
{...Col.tabId},
|
|
171
|
+
{...Col.correlationId, hidden},
|
|
172
|
+
{...Col.appEnvironment, hidden},
|
|
173
|
+
{...Col.instance, hidden},
|
|
174
|
+
{...Col.urlPathOnly},
|
|
175
|
+
{...Col.data, hidden},
|
|
176
|
+
{...Col.dateCreatedNoYear, displayName: 'Timestamp'},
|
|
177
|
+
...this.activityTrackingModel.dataFieldCols
|
|
178
|
+
]
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// TODO - don't base on grid cols
|
|
183
|
+
private createSingleEntryFormModel(): FormModel {
|
|
184
|
+
return new FormModel({
|
|
185
|
+
readonly: true,
|
|
186
|
+
fields: this.gridModel
|
|
187
|
+
.getLeafColumns()
|
|
188
|
+
.map(it => ({name: it.field, displayName: it.headerName as string}))
|
|
189
|
+
});
|
|
126
190
|
}
|
|
127
191
|
}
|
|
@@ -4,19 +4,21 @@
|
|
|
4
4
|
*
|
|
5
5
|
* Copyright © 2025 Extremely Heavy Industries Inc.
|
|
6
6
|
*/
|
|
7
|
-
import {
|
|
7
|
+
import {badgeRenderer} from '@xh/hoist/admin/columns';
|
|
8
8
|
import {form} from '@xh/hoist/cmp/form';
|
|
9
9
|
import {grid, gridCountLabel} from '@xh/hoist/cmp/grid';
|
|
10
|
-
import {a, div, filler, h3, hframe, placeholder, span} from '@xh/hoist/cmp/layout';
|
|
11
|
-
import {
|
|
10
|
+
import {a, br, div, filler, h3, hframe, placeholder, span} from '@xh/hoist/cmp/layout';
|
|
11
|
+
import {creates, hoistCmp} from '@xh/hoist/core';
|
|
12
12
|
import {colChooserButton, exportButton} from '@xh/hoist/desktop/cmp/button';
|
|
13
13
|
import {formField} from '@xh/hoist/desktop/cmp/form';
|
|
14
14
|
import {gridFindField} from '@xh/hoist/desktop/cmp/grid';
|
|
15
|
-
import {jsonInput} from '@xh/hoist/desktop/cmp/input';
|
|
15
|
+
import {jsonInput, textInput} from '@xh/hoist/desktop/cmp/input';
|
|
16
16
|
import {panel} from '@xh/hoist/desktop/cmp/panel';
|
|
17
17
|
import {toolbar} from '@xh/hoist/desktop/cmp/toolbar';
|
|
18
18
|
import {dateTimeSecRenderer, numberRenderer} from '@xh/hoist/format';
|
|
19
19
|
import {Icon} from '@xh/hoist/icon/Icon';
|
|
20
|
+
import {tooltip} from '@xh/hoist/kit/blueprint';
|
|
21
|
+
import {isNil} from 'lodash';
|
|
20
22
|
import {ActivityDetailModel} from './ActivityDetailModel';
|
|
21
23
|
|
|
22
24
|
export const activityDetailView = hoistCmp.factory({
|
|
@@ -24,26 +26,26 @@ export const activityDetailView = hoistCmp.factory({
|
|
|
24
26
|
|
|
25
27
|
render({model, ...props}) {
|
|
26
28
|
return panel({
|
|
27
|
-
title: 'Track Log Entries',
|
|
28
|
-
icon: Icon.list(),
|
|
29
29
|
className: 'xh-admin-activity-detail',
|
|
30
|
-
compactHeader: true,
|
|
31
|
-
items: [grid({flex: 1}), detailRecPanel()],
|
|
32
30
|
tbar: tbar(),
|
|
31
|
+
items: [grid({flex: 1}), detailRecPanel()],
|
|
33
32
|
...props
|
|
34
33
|
});
|
|
35
34
|
}
|
|
36
35
|
});
|
|
37
36
|
|
|
38
|
-
const tbar = hoistCmp.factory(({model}) => {
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
37
|
+
const tbar = hoistCmp.factory<ActivityDetailModel>(({model}) => {
|
|
38
|
+
const {gridModel} = model;
|
|
39
|
+
return toolbar({
|
|
40
|
+
items: [
|
|
41
|
+
filler(),
|
|
42
|
+
gridCountLabel({unit: 'entry'}),
|
|
43
|
+
'-',
|
|
44
|
+
gridFindField({gridModel, key: gridModel.xhId}),
|
|
45
|
+
colChooserButton({gridModel}),
|
|
46
|
+
exportButton()
|
|
47
|
+
]
|
|
48
|
+
});
|
|
47
49
|
});
|
|
48
50
|
|
|
49
51
|
// Discrete outer panel to retain sizing across master/detail selection changes.
|
|
@@ -54,7 +56,11 @@ const detailRecPanel = hoistCmp.factory<ActivityDetailModel>(({model}) => {
|
|
|
54
56
|
compactHeader: true,
|
|
55
57
|
modelConfig: {
|
|
56
58
|
side: 'bottom',
|
|
57
|
-
defaultSize: 400
|
|
59
|
+
defaultSize: 400,
|
|
60
|
+
persistWith: {
|
|
61
|
+
...model.activityTrackingModel.persistWith,
|
|
62
|
+
path: 'singleActivityDetailPanel'
|
|
63
|
+
}
|
|
58
64
|
},
|
|
59
65
|
item: detailRecForm()
|
|
60
66
|
});
|
|
@@ -71,6 +77,7 @@ const detailRecForm = hoistCmp.factory<ActivityDetailModel>(({model}) => {
|
|
|
71
77
|
style: {flex: 1},
|
|
72
78
|
items: [
|
|
73
79
|
h3(Icon.info(), 'Activity'),
|
|
80
|
+
formField({field: 'severity', label: 'Severity'}),
|
|
74
81
|
formField({
|
|
75
82
|
field: 'username',
|
|
76
83
|
readonlyRenderer: username => {
|
|
@@ -88,60 +95,109 @@ const detailRecForm = hoistCmp.factory<ActivityDetailModel>(({model}) => {
|
|
|
88
95
|
formField({field: 'category'}),
|
|
89
96
|
formField({field: 'msg'}),
|
|
90
97
|
formField({
|
|
91
|
-
field: '
|
|
92
|
-
readonlyRenderer:
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
}
|
|
98
|
+
field: 'elapsed',
|
|
99
|
+
readonlyRenderer: numberRenderer({
|
|
100
|
+
label: 'ms',
|
|
101
|
+
nullDisplay: '-',
|
|
102
|
+
formatConfig: {thousandSeparated: false, mantissa: 0}
|
|
103
|
+
}),
|
|
104
|
+
omit: isNil(formModel.values.elapsed)
|
|
97
105
|
}),
|
|
98
106
|
formField({
|
|
99
|
-
field: '
|
|
100
|
-
readonlyRenderer:
|
|
107
|
+
field: 'dateCreated',
|
|
108
|
+
readonlyRenderer: dateTimeSecRenderer({})
|
|
101
109
|
}),
|
|
102
110
|
formField({
|
|
103
|
-
field: '
|
|
104
|
-
readonlyRenderer:
|
|
111
|
+
field: 'correlationId',
|
|
112
|
+
readonlyRenderer: badgeRenderer,
|
|
113
|
+
omit: !formModel.values.correlationId
|
|
105
114
|
}),
|
|
115
|
+
h3(Icon.idBadge(), 'Session IDs'),
|
|
106
116
|
formField({
|
|
107
|
-
field: '
|
|
108
|
-
readonlyRenderer:
|
|
117
|
+
field: 'loadId',
|
|
118
|
+
readonlyRenderer: badgeRenderer,
|
|
119
|
+
omit: !formModel.values.loadId
|
|
109
120
|
}),
|
|
110
121
|
formField({
|
|
111
|
-
field: '
|
|
112
|
-
readonlyRenderer:
|
|
113
|
-
|
|
114
|
-
nullDisplay: '-',
|
|
115
|
-
formatConfig: {thousandSeparated: false, mantissa: 0}
|
|
116
|
-
})
|
|
122
|
+
field: 'tabId',
|
|
123
|
+
readonlyRenderer: badgeRenderer,
|
|
124
|
+
omit: !formModel.values.tabId
|
|
117
125
|
}),
|
|
118
126
|
formField({
|
|
119
|
-
field: '
|
|
120
|
-
readonlyRenderer:
|
|
127
|
+
field: 'instance',
|
|
128
|
+
readonlyRenderer: badgeRenderer,
|
|
129
|
+
omit: !formModel.values.instance
|
|
130
|
+
}),
|
|
131
|
+
|
|
132
|
+
h3(Icon.desktop(), 'Client App / Browser'),
|
|
133
|
+
formField({
|
|
134
|
+
field: 'appVersion',
|
|
135
|
+
readonlyRenderer: appVersion => {
|
|
136
|
+
if (!appVersion) return naSpan();
|
|
137
|
+
const {appEnvironment} = formModel.values;
|
|
138
|
+
return `${appVersion} (${appEnvironment})`;
|
|
139
|
+
}
|
|
121
140
|
}),
|
|
122
|
-
|
|
123
|
-
formField({field: 'device'}),
|
|
141
|
+
formField({field: 'device', label: 'Device'}),
|
|
124
142
|
formField({field: 'browser'}),
|
|
125
|
-
formField({field: 'userAgent'})
|
|
143
|
+
formField({field: 'userAgent'}),
|
|
144
|
+
formField({
|
|
145
|
+
field: 'url',
|
|
146
|
+
readonlyRenderer: hyperlinkVal
|
|
147
|
+
})
|
|
126
148
|
]
|
|
127
149
|
}),
|
|
128
|
-
|
|
129
|
-
flex: 1,
|
|
130
|
-
className: 'xh-border-left',
|
|
131
|
-
items: [h3(Icon.json(), 'Additional Data'), additionalDataJsonInput()]
|
|
132
|
-
})
|
|
150
|
+
additionalDataPanel()
|
|
133
151
|
)
|
|
134
152
|
})
|
|
135
|
-
: placeholder('Select an activity tracking record to view details.');
|
|
153
|
+
: placeholder(Icon.detail(), 'Select an activity tracking record to view details.');
|
|
136
154
|
});
|
|
137
155
|
|
|
138
|
-
const
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
156
|
+
const additionalDataPanel = hoistCmp.factory<ActivityDetailModel>(({model}) => {
|
|
157
|
+
const item = model.formattedData
|
|
158
|
+
? jsonInput({
|
|
159
|
+
readonly: true,
|
|
160
|
+
width: '100%',
|
|
161
|
+
height: '100%',
|
|
162
|
+
showCopyButton: true,
|
|
163
|
+
value: model.formattedData
|
|
164
|
+
})
|
|
165
|
+
: placeholder({
|
|
166
|
+
items: [
|
|
167
|
+
model.hasExtraTrackData ? Icon.filter() : null,
|
|
168
|
+
model.hasExtraTrackData
|
|
169
|
+
? 'Additional data available, but hidden by path filter below.'
|
|
170
|
+
: 'No additional data available.'
|
|
171
|
+
]
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
return panel({
|
|
175
|
+
flex: 1,
|
|
176
|
+
className: 'xh-border-left',
|
|
177
|
+
items: [
|
|
178
|
+
h3(Icon.json(), 'Additional Data'),
|
|
179
|
+
item,
|
|
180
|
+
toolbar(
|
|
181
|
+
textInput({
|
|
182
|
+
placeholder: 'Path filter(s)...',
|
|
183
|
+
leftIcon: Icon.filter({
|
|
184
|
+
intent: model.formattedDataFilterPath ? 'warning' : null
|
|
185
|
+
}),
|
|
186
|
+
commitOnChange: true,
|
|
187
|
+
enableClear: true,
|
|
188
|
+
flex: 1,
|
|
189
|
+
bind: 'formattedDataFilterPath'
|
|
190
|
+
}),
|
|
191
|
+
tooltip({
|
|
192
|
+
item: Icon.questionCircle({className: 'xh-margin-right'}),
|
|
193
|
+
content: span(
|
|
194
|
+
'Specify one or more dot-delimited paths to filter the JSON data displayed above.',
|
|
195
|
+
br(),
|
|
196
|
+
'Separate multiple paths that you wish to include with a | character.'
|
|
197
|
+
)
|
|
198
|
+
})
|
|
199
|
+
)
|
|
200
|
+
]
|
|
145
201
|
});
|
|
146
202
|
});
|
|
147
203
|
|
|
@@ -4,11 +4,10 @@
|
|
|
4
4
|
*
|
|
5
5
|
* Copyright © 2025 Extremely Heavy Industries Inc.
|
|
6
6
|
*/
|
|
7
|
+
import {clientErrorsPanel} from '@xh/hoist/admin/tabs/client/errors/ClientErrorsPanel';
|
|
7
8
|
import {tabContainer} from '@xh/hoist/cmp/tab';
|
|
8
9
|
import {hoistCmp} from '@xh/hoist/core';
|
|
9
10
|
import {Icon} from '@xh/hoist/icon';
|
|
10
|
-
import {clientErrorsPanel} from '@xh/hoist/admin/tabs/client/errors/ClientErrorsPanel';
|
|
11
|
-
import {feedbackPanel} from './feedback/FeedbackPanel';
|
|
12
11
|
import {clientsPanel} from './clients/ClientsPanel';
|
|
13
12
|
|
|
14
13
|
export const clientTab = hoistCmp.factory(() =>
|
|
@@ -18,8 +17,7 @@ export const clientTab = hoistCmp.factory(() =>
|
|
|
18
17
|
switcher: {orientation: 'left', testId: 'client-tab-switcher'},
|
|
19
18
|
tabs: [
|
|
20
19
|
{id: 'connections', icon: Icon.diff(), content: clientsPanel},
|
|
21
|
-
{id: 'errors', icon: Icon.warning(), content: clientErrorsPanel}
|
|
22
|
-
{id: 'feedback', icon: Icon.comment(), content: feedbackPanel}
|
|
20
|
+
{id: 'errors', icon: Icon.warning(), content: clientErrorsPanel}
|
|
23
21
|
]
|
|
24
22
|
}
|
|
25
23
|
})
|
|
@@ -18,7 +18,6 @@ import {Timer} from '@xh/hoist/utils/async';
|
|
|
18
18
|
import {SECONDS} from '@xh/hoist/utils/datetime';
|
|
19
19
|
import {pluralize} from '@xh/hoist/utils/js';
|
|
20
20
|
import {isEmpty} from 'lodash';
|
|
21
|
-
import * as WSCol from './ClientsColumns';
|
|
22
21
|
import {BaseAdminTabModel} from '@xh/hoist/admin/tabs/BaseAdminTabModel';
|
|
23
22
|
|
|
24
23
|
export class ClientsModel extends BaseAdminTabModel {
|
|
@@ -177,32 +176,32 @@ export class ClientsModel extends BaseAdminTabModel {
|
|
|
177
176
|
},
|
|
178
177
|
sortBy: ['user'],
|
|
179
178
|
columns: [
|
|
180
|
-
|
|
179
|
+
Col.isOpen,
|
|
181
180
|
Col.user,
|
|
182
181
|
{
|
|
183
182
|
headerName: 'Session',
|
|
184
183
|
headerAlign: 'center',
|
|
185
184
|
children: [
|
|
186
|
-
|
|
187
|
-
{...
|
|
185
|
+
Col.createdTime,
|
|
186
|
+
{...Col.key, hidden},
|
|
188
187
|
Col.instance,
|
|
189
|
-
|
|
190
|
-
|
|
188
|
+
Col.loadId,
|
|
189
|
+
Col.tabId
|
|
191
190
|
]
|
|
192
191
|
},
|
|
193
192
|
{
|
|
194
193
|
headerName: 'Client App',
|
|
195
194
|
headerAlign: 'center',
|
|
196
|
-
children: [
|
|
195
|
+
children: [Col.appVersion, Col.appBuild]
|
|
197
196
|
},
|
|
198
197
|
{
|
|
199
198
|
headerName: 'Send/Receive',
|
|
200
199
|
headerAlign: 'center',
|
|
201
200
|
children: [
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
201
|
+
Col.sentMessageCount,
|
|
202
|
+
Col.lastSentTime,
|
|
203
|
+
Col.receivedMessageCount,
|
|
204
|
+
Col.lastReceivedTime
|
|
206
205
|
]
|
|
207
206
|
}
|
|
208
207
|
]
|