@xh/hoist 73.0.0-SNAPSHOT.1746025071597 → 73.0.0-SNAPSHOT.1746050507413
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 +9 -3
- package/admin/AdminUtils.ts +5 -0
- package/admin/AppModel.ts +19 -7
- package/admin/columns/Rest.ts +8 -0
- package/admin/columns/Tracking.ts +72 -0
- package/admin/tabs/activity/tracking/ActivityTracking.scss +19 -0
- package/admin/tabs/activity/tracking/ActivityTrackingModel.ts +309 -216
- 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 +148 -0
- package/admin/tabs/activity/tracking/datafields/DataFieldsEditorModel.ts +133 -0
- package/admin/tabs/activity/tracking/detail/ActivityDetailModel.ts +123 -60
- package/admin/tabs/activity/tracking/detail/ActivityDetailView.ts +106 -58
- package/admin/tabs/client/ClientTab.ts +2 -4
- 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/columns/Rest.d.ts +1 -0
- package/build/types/admin/columns/Tracking.d.ts +6 -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/desktop/cmp/button/DashCanvasAddViewButton.ts +1 -0
- 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/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,49 +47,14 @@ export class ActivityDetailModel extends HoistModel {
|
|
|
30
47
|
}
|
|
31
48
|
|
|
32
49
|
override onLinked() {
|
|
33
|
-
|
|
34
|
-
this.gridModel = new GridModel({
|
|
35
|
-
sortBy: 'dateCreated|desc',
|
|
36
|
-
colChooserModel: true,
|
|
37
|
-
enableExport: true,
|
|
38
|
-
filterModel: false,
|
|
39
|
-
exportOptions: {
|
|
40
|
-
columns: 'ALL',
|
|
41
|
-
filename: exportFilename('activity-detail')
|
|
42
|
-
},
|
|
43
|
-
emptyText: 'Select a group on the left to see detailed tracking logs.',
|
|
44
|
-
columns: [
|
|
45
|
-
{...Col.impersonatingFlag},
|
|
46
|
-
{...Col.entryId, hidden},
|
|
47
|
-
{...Col.username},
|
|
48
|
-
{...Col.impersonating, hidden},
|
|
49
|
-
{...Col.category},
|
|
50
|
-
{...Col.msg},
|
|
51
|
-
{...Col.browser},
|
|
52
|
-
{...Col.device},
|
|
53
|
-
{...Col.userAgent, hidden},
|
|
54
|
-
{...Col.appVersion},
|
|
55
|
-
{...Col.loadId},
|
|
56
|
-
{...Col.tabId},
|
|
57
|
-
{...Col.appEnvironment, hidden},
|
|
58
|
-
{...Col.data, hidden},
|
|
59
|
-
{...Col.url},
|
|
60
|
-
{...Col.instance, hidden},
|
|
61
|
-
{...Col.correlationId},
|
|
62
|
-
{...Col.severity, hidden},
|
|
63
|
-
{...Col.elapsed},
|
|
64
|
-
{...Col.dateCreatedWithSec, displayName: 'Timestamp'}
|
|
65
|
-
]
|
|
66
|
-
});
|
|
67
|
-
|
|
68
|
-
this.formModel = new FormModel({
|
|
69
|
-
readonly: true,
|
|
70
|
-
fields: this.gridModel
|
|
71
|
-
.getLeafColumns()
|
|
72
|
-
.map(it => ({name: it.field, displayName: it.headerName as string}))
|
|
73
|
-
});
|
|
50
|
+
this.markPersist('formattedDataFilterPath', this.activityTrackingModel.persistWith);
|
|
74
51
|
|
|
75
52
|
this.addReaction(
|
|
53
|
+
{
|
|
54
|
+
track: () => this.activityTrackingModel.dataFields,
|
|
55
|
+
run: () => this.createAndSetCoreModels(),
|
|
56
|
+
fireImmediately: true
|
|
57
|
+
},
|
|
76
58
|
{
|
|
77
59
|
track: () => this.activityTrackingModel.gridModel.selectedRecord,
|
|
78
60
|
run: aggRec => this.showActivityEntriesAsync(aggRec)
|
|
@@ -80,11 +62,18 @@ export class ActivityDetailModel extends HoistModel {
|
|
|
80
62
|
{
|
|
81
63
|
track: () => this.gridModel.selectedRecord,
|
|
82
64
|
run: detailRec => this.showEntryDetail(detailRec)
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
track: () => this.formattedDataFilterPath,
|
|
68
|
+
run: () => this.updateFormattedData()
|
|
83
69
|
}
|
|
84
70
|
);
|
|
85
71
|
}
|
|
86
72
|
|
|
87
|
-
|
|
73
|
+
//------------------
|
|
74
|
+
// Implementation
|
|
75
|
+
//------------------
|
|
76
|
+
private async showActivityEntriesAsync(aggRec: StoreRecord) {
|
|
88
77
|
const {gridModel} = this,
|
|
89
78
|
leaves = this.getAllLeafRows(aggRec);
|
|
90
79
|
|
|
@@ -93,7 +82,7 @@ export class ActivityDetailModel extends HoistModel {
|
|
|
93
82
|
}
|
|
94
83
|
|
|
95
84
|
// Extract all leaf, track-entry-level rows from an aggregate record (at any level).
|
|
96
|
-
private getAllLeafRows(aggRec, ret = []) {
|
|
85
|
+
private getAllLeafRows(aggRec: StoreRecord, ret = []) {
|
|
97
86
|
if (!aggRec) return [];
|
|
98
87
|
|
|
99
88
|
if (aggRec.children.length) {
|
|
@@ -107,22 +96,96 @@ export class ActivityDetailModel extends HoistModel {
|
|
|
107
96
|
return ret;
|
|
108
97
|
}
|
|
109
98
|
|
|
110
|
-
|
|
111
|
-
// Also parse/format any additional data (as JSON) if provided.
|
|
99
|
+
/** Extract data from a (detail) grid record and flush it into our form for display. */
|
|
112
100
|
@action
|
|
113
|
-
private showEntryDetail(detailRec) {
|
|
114
|
-
|
|
115
|
-
|
|
101
|
+
private showEntryDetail(detailRec: StoreRecord) {
|
|
102
|
+
this.formModel.init(detailRec?.data ?? {});
|
|
103
|
+
this.updateFormattedData();
|
|
104
|
+
}
|
|
116
105
|
|
|
117
|
-
|
|
106
|
+
@action
|
|
107
|
+
private updateFormattedData() {
|
|
108
|
+
const {gridModel, formattedDataFilterPath} = this,
|
|
109
|
+
trackData = gridModel.selectedRecord?.data.data;
|
|
118
110
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
formattedTrackData = fmtJson(trackData, {replacer: timestampReplacer()});
|
|
123
|
-
} catch (ignored) {}
|
|
111
|
+
if (!trackData) {
|
|
112
|
+
this.formattedData = '';
|
|
113
|
+
return;
|
|
124
114
|
}
|
|
125
115
|
|
|
126
|
-
|
|
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
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
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
|
+
});
|
|
127
190
|
}
|
|
128
191
|
}
|
|
@@ -7,16 +7,18 @@
|
|
|
7
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';
|
|
10
|
+
import {a, br, div, filler, h3, hframe, placeholder, span} from '@xh/hoist/cmp/layout';
|
|
11
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,68 +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
|
-
}
|
|
97
|
-
|
|
98
|
-
formField({
|
|
99
|
-
field: 'loadId',
|
|
100
|
-
readonlyRenderer: badgeRenderer
|
|
98
|
+
field: 'elapsed',
|
|
99
|
+
readonlyRenderer: numberRenderer({
|
|
100
|
+
label: 'ms',
|
|
101
|
+
nullDisplay: '-',
|
|
102
|
+
formatConfig: {thousandSeparated: false, mantissa: 0}
|
|
103
|
+
}),
|
|
104
|
+
omit: isNil(formModel.values.elapsed)
|
|
101
105
|
}),
|
|
102
106
|
formField({
|
|
103
|
-
field: '
|
|
104
|
-
readonlyRenderer:
|
|
107
|
+
field: 'dateCreated',
|
|
108
|
+
readonlyRenderer: dateTimeSecRenderer({})
|
|
105
109
|
}),
|
|
106
110
|
formField({
|
|
107
|
-
field: '
|
|
108
|
-
readonlyRenderer:
|
|
111
|
+
field: 'correlationId',
|
|
112
|
+
readonlyRenderer: badgeRenderer,
|
|
113
|
+
omit: !formModel.values.correlationId
|
|
109
114
|
}),
|
|
115
|
+
h3(Icon.idBadge(), 'Session IDs'),
|
|
110
116
|
formField({
|
|
111
|
-
field: '
|
|
112
|
-
readonlyRenderer: badgeRenderer
|
|
117
|
+
field: 'loadId',
|
|
118
|
+
readonlyRenderer: badgeRenderer,
|
|
119
|
+
omit: !formModel.values.loadId
|
|
113
120
|
}),
|
|
114
121
|
formField({
|
|
115
|
-
field: '
|
|
116
|
-
readonlyRenderer: badgeRenderer
|
|
122
|
+
field: 'tabId',
|
|
123
|
+
readonlyRenderer: badgeRenderer,
|
|
124
|
+
omit: !formModel.values.tabId
|
|
117
125
|
}),
|
|
118
126
|
formField({
|
|
119
|
-
field: '
|
|
120
|
-
readonlyRenderer:
|
|
121
|
-
|
|
122
|
-
nullDisplay: '-',
|
|
123
|
-
formatConfig: {thousandSeparated: false, mantissa: 0}
|
|
124
|
-
})
|
|
127
|
+
field: 'instance',
|
|
128
|
+
readonlyRenderer: badgeRenderer,
|
|
129
|
+
omit: !formModel.values.instance
|
|
125
130
|
}),
|
|
131
|
+
|
|
132
|
+
h3(Icon.desktop(), 'Client App / Browser'),
|
|
126
133
|
formField({
|
|
127
|
-
field: '
|
|
128
|
-
readonlyRenderer:
|
|
134
|
+
field: 'appVersion',
|
|
135
|
+
readonlyRenderer: appVersion => {
|
|
136
|
+
if (!appVersion) return naSpan();
|
|
137
|
+
const {appEnvironment} = formModel.values;
|
|
138
|
+
return `${appVersion} (${appEnvironment})`;
|
|
139
|
+
}
|
|
129
140
|
}),
|
|
130
|
-
|
|
131
|
-
formField({field: 'device'}),
|
|
141
|
+
formField({field: 'device', label: 'Device'}),
|
|
132
142
|
formField({field: 'browser'}),
|
|
133
|
-
formField({field: 'userAgent'})
|
|
143
|
+
formField({field: 'userAgent'}),
|
|
144
|
+
formField({
|
|
145
|
+
field: 'url',
|
|
146
|
+
readonlyRenderer: hyperlinkVal
|
|
147
|
+
})
|
|
134
148
|
]
|
|
135
149
|
}),
|
|
136
|
-
|
|
137
|
-
flex: 1,
|
|
138
|
-
className: 'xh-border-left',
|
|
139
|
-
items: [h3(Icon.json(), 'Additional Data'), additionalDataJsonInput()]
|
|
140
|
-
})
|
|
150
|
+
additionalDataPanel()
|
|
141
151
|
)
|
|
142
152
|
})
|
|
143
|
-
: placeholder('Select an activity tracking record to view details.');
|
|
153
|
+
: placeholder(Icon.detail(), 'Select an activity tracking record to view details.');
|
|
144
154
|
});
|
|
145
155
|
|
|
146
|
-
const
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
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
|
+
]
|
|
153
201
|
});
|
|
154
202
|
});
|
|
155
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
|
})
|
|
@@ -13,9 +13,8 @@ import {LoadSpec, managed, XH} from '@xh/hoist/core';
|
|
|
13
13
|
import {lengthIs, required} from '@xh/hoist/data';
|
|
14
14
|
import {fmtTime, numberRenderer} from '@xh/hoist/format';
|
|
15
15
|
import {Icon} from '@xh/hoist/icon';
|
|
16
|
-
import {bindable, makeObservable} from '@xh/hoist/mobx';
|
|
16
|
+
import {bindable, makeObservable, observable, runInAction} from '@xh/hoist/mobx';
|
|
17
17
|
import {forOwn, orderBy, sortBy} from 'lodash';
|
|
18
|
-
import {observable, runInAction} from 'mobx';
|
|
19
18
|
|
|
20
19
|
export interface PastInstance {
|
|
21
20
|
name: string;
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
* Copyright © 2025 Extremely Heavy Industries Inc.
|
|
6
6
|
*/
|
|
7
7
|
import {configPanel} from '@xh/hoist/admin/tabs/general/config/ConfigPanel';
|
|
8
|
+
import {feedbackPanel} from '@xh/hoist/admin/tabs/general/feedback/FeedbackPanel';
|
|
8
9
|
import {tabContainer} from '@xh/hoist/cmp/tab';
|
|
9
10
|
import {hoistCmp} from '@xh/hoist/core';
|
|
10
11
|
import {Icon} from '@xh/hoist/icon';
|
|
@@ -19,6 +20,7 @@ export const generalTab = hoistCmp.factory(() =>
|
|
|
19
20
|
tabs: [
|
|
20
21
|
{id: 'about', icon: Icon.info(), content: aboutPanel},
|
|
21
22
|
{id: 'config', icon: Icon.settings(), content: configPanel},
|
|
23
|
+
{id: 'feedback', icon: Icon.comment(), content: feedbackPanel},
|
|
22
24
|
{id: 'alertBanner', icon: Icon.bullhorn(), content: alertBannerPanel}
|
|
23
25
|
]
|
|
24
26
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { AppModel } from '@xh/hoist/admin/AppModel';
|
|
1
2
|
/**
|
|
2
3
|
* Generate a standardized filename for an Admin module grid export, without datestamp.
|
|
3
4
|
*/
|
|
@@ -7,3 +8,4 @@ export declare function exportFilename(moduleName: string): string;
|
|
|
7
8
|
* Returned as a closure to ensure current date is evaluated at export time.
|
|
8
9
|
*/
|
|
9
10
|
export declare function exportFilenameWithDate(moduleName: string): () => string;
|
|
11
|
+
export declare function getAppModel<T extends AppModel>(): T;
|