@xh/hoist 72.3.0 → 72.5.0
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 +25 -6
- package/admin/tabs/activity/tracking/ActivityTrackingModel.ts +22 -14
- package/admin/tabs/activity/tracking/detail/ActivityDetailModel.ts +2 -2
- package/admin/tabs/cluster/instances/BaseInstanceModel.ts +1 -29
- package/admin/tabs/cluster/instances/services/DetailsPanel.ts +2 -1
- package/admin/tabs/cluster/instances/websocket/WebSocketColumns.ts +24 -2
- package/admin/tabs/cluster/instances/websocket/WebSocketModel.ts +76 -25
- package/admin/tabs/cluster/instances/websocket/WebSocketPanel.ts +2 -2
- package/admin/tabs/cluster/objects/DetailModel.ts +4 -40
- package/admin/tabs/cluster/objects/DetailPanel.ts +2 -1
- package/appcontainer/AppContainerModel.ts +2 -0
- package/appcontainer/AppStateModel.ts +40 -8
- package/build/types/admin/tabs/activity/tracking/ActivityTrackingModel.d.ts +1 -1
- package/build/types/admin/tabs/cluster/instances/BaseInstanceModel.d.ts +1 -3
- package/build/types/admin/tabs/cluster/instances/websocket/WebSocketColumns.d.ts +4 -1
- package/build/types/admin/tabs/cluster/instances/websocket/WebSocketModel.d.ts +5 -2
- package/build/types/admin/tabs/cluster/objects/DetailModel.d.ts +1 -3
- package/build/types/appcontainer/AppStateModel.d.ts +2 -0
- package/build/types/cmp/viewmanager/ViewManagerModel.d.ts +7 -0
- package/build/types/core/XH.d.ts +11 -1
- package/build/types/core/types/Interfaces.d.ts +2 -2
- package/build/types/format/FormatDate.d.ts +22 -1
- package/build/types/format/FormatMisc.d.ts +3 -2
- package/build/types/security/Types.d.ts +0 -25
- package/build/types/security/msal/MsalClient.d.ts +42 -4
- package/build/types/svc/ClientHealthService.d.ts +64 -0
- package/build/types/svc/TrackService.d.ts +3 -11
- package/build/types/svc/WebSocketService.d.ts +38 -15
- package/build/types/svc/index.d.ts +1 -0
- package/build/types/utils/js/index.d.ts +0 -1
- package/cmp/viewmanager/ViewManagerModel.ts +10 -1
- package/core/XH.ts +26 -1
- package/core/types/Interfaces.ts +2 -2
- package/data/filter/BaseFilterFieldSpec.ts +6 -2
- package/desktop/appcontainer/AboutDialog.ts +14 -0
- package/desktop/cmp/button/AppMenuButton.ts +1 -1
- package/desktop/cmp/contextmenu/ContextMenu.ts +1 -1
- package/desktop/cmp/viewmanager/ViewMenu.ts +11 -9
- package/format/FormatDate.ts +45 -3
- package/format/FormatMisc.ts +6 -4
- package/kit/onsen/theme.scss +5 -0
- package/mobile/appcontainer/AboutDialog.scss +1 -1
- package/mobile/appcontainer/AboutDialog.ts +24 -1
- package/mobile/cmp/menu/impl/Menu.ts +2 -2
- package/package.json +1 -1
- package/security/Types.ts +0 -27
- package/security/msal/MsalClient.ts +77 -25
- package/svc/ClientHealthService.ts +179 -0
- package/svc/TrackService.ts +9 -69
- package/svc/WebSocketService.ts +74 -33
- package/svc/index.ts +1 -0
- package/tsconfig.tsbuildinfo +1 -1
- package/utils/js/index.ts +0 -1
- package/build/types/utils/js/BrowserUtils.d.ts +0 -41
- package/utils/js/BrowserUtils.ts +0 -103
package/CHANGELOG.md
CHANGED
|
@@ -1,12 +1,36 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## v72.5.0 - 2025-04-14
|
|
4
|
+
|
|
5
|
+
### 🎁 New Features
|
|
6
|
+
|
|
7
|
+
* Added option from the Admin Console > Websockets tab to request a client health report from any
|
|
8
|
+
connected clients.
|
|
9
|
+
* Enabled telemetry reporting from `WebSocketService`.
|
|
10
|
+
* Updated `MenuItem.actionFn()` to receive the click event as an additional argument.
|
|
11
|
+
* Support for reporting App Build, Tab Id, and Load Id in websocket admin page.
|
|
12
|
+
|
|
13
|
+
## v72.4.0 - 2025-04-09
|
|
14
|
+
|
|
15
|
+
### 🎁 New Features
|
|
16
|
+
|
|
17
|
+
* Added new methods for formatting timestamps within JSON objects. See `withFormattedTimestamps`
|
|
18
|
+
and `timestampReplacer` in the `@xh/hoist/format` package.
|
|
19
|
+
* Added new `ViewManagerConfig.viewMenuItemFn` option to support custom rendering of pinned views in
|
|
20
|
+
the drop-down menu.
|
|
21
|
+
|
|
22
|
+
### ⚙️ Technical
|
|
23
|
+
|
|
24
|
+
* Added dedicated `ClientHealthService` for managing client health report. Additional enhancements
|
|
25
|
+
to health report to include information about web sockets, idle time, and page state.
|
|
26
|
+
|
|
3
27
|
## v72.3.0 - 2025-04-08
|
|
4
28
|
|
|
5
29
|
### 🎁 New Features
|
|
6
30
|
|
|
7
31
|
* Added support for posting a "Client Health Report" track message on a configurable interval. This
|
|
8
32
|
message will include basic client information, and can be extended to include any other desired
|
|
9
|
-
data via `XH.
|
|
33
|
+
data via `XH.clientHealthService.addSource()`. Enable by updating your app's
|
|
10
34
|
`xhActivityTrackingConfig` to include `clientHealthReport: {intervalMins: XXXX}`.
|
|
11
35
|
* Enabled opt-in support for telemetry in `MsalClient`, leveraging hooks built-in to MSAL to collect
|
|
12
36
|
timing and success/failure count for all events emitted by the library.
|
|
@@ -16,11 +40,6 @@
|
|
|
16
40
|
|
|
17
41
|
* Improved fetch request tracking to include time spent loading headers as specified by application.
|
|
18
42
|
|
|
19
|
-
### ⚙️ Technical
|
|
20
|
-
|
|
21
|
-
* Update shape of returned `BrowserUtils.getClientDeviceInfo()` to nest several properties under new
|
|
22
|
-
top-level `window` key and report JS heap size / usage values under the `memory` block in MB.
|
|
23
|
-
|
|
24
43
|
### 📚 Libraries
|
|
25
44
|
|
|
26
45
|
* @azure/msal-browser `3.28 → 4.8.0`
|
|
@@ -5,17 +5,17 @@
|
|
|
5
5
|
* Copyright © 2025 Extremely Heavy Industries Inc.
|
|
6
6
|
*/
|
|
7
7
|
import {exportFilename} from '@xh/hoist/admin/AdminUtils';
|
|
8
|
-
import
|
|
8
|
+
import * as Col from '@xh/hoist/admin/columns';
|
|
9
9
|
import {FilterChooserModel} from '@xh/hoist/cmp/filter';
|
|
10
10
|
import {FormModel} from '@xh/hoist/cmp/form';
|
|
11
11
|
import {GridModel, TreeStyle} from '@xh/hoist/cmp/grid';
|
|
12
|
+
import {GroupingChooserModel} from '@xh/hoist/cmp/grouping';
|
|
12
13
|
import {HoistModel, LoadSpec, managed, XH} from '@xh/hoist/core';
|
|
13
14
|
import {Cube, CubeFieldSpec, FieldSpec} from '@xh/hoist/data';
|
|
14
15
|
import {fmtNumber} from '@xh/hoist/format';
|
|
15
16
|
import {action, computed, makeObservable} from '@xh/hoist/mobx';
|
|
16
17
|
import {LocalDate} from '@xh/hoist/utils/datetime';
|
|
17
|
-
import
|
|
18
|
-
import {isEmpty, round} from 'lodash';
|
|
18
|
+
import {compact, isEmpty, round} from 'lodash';
|
|
19
19
|
import moment from 'moment';
|
|
20
20
|
|
|
21
21
|
export const PERSIST_ACTIVITY = {localStorageKey: 'xhAdminActivityState'};
|
|
@@ -109,14 +109,13 @@ export class ActivityTrackingModel extends HoistModel {
|
|
|
109
109
|
] as CubeFieldSpec[]
|
|
110
110
|
});
|
|
111
111
|
|
|
112
|
-
const enableValues = true;
|
|
113
112
|
this.filterChooserModel = new FilterChooserModel({
|
|
114
113
|
fieldSpecs: [
|
|
115
|
-
{field: 'category'
|
|
114
|
+
{field: 'category'},
|
|
116
115
|
{field: 'correlationId'},
|
|
117
|
-
{field: 'username', displayName: 'User'
|
|
118
|
-
{field: 'device'
|
|
119
|
-
{field: 'browser'
|
|
116
|
+
{field: 'username', displayName: 'User'},
|
|
117
|
+
{field: 'device'},
|
|
118
|
+
{field: 'browser'},
|
|
120
119
|
{
|
|
121
120
|
field: 'elapsed',
|
|
122
121
|
valueRenderer: v => {
|
|
@@ -330,12 +329,21 @@ export class ActivityTrackingModel extends HoistModel {
|
|
|
330
329
|
}
|
|
331
330
|
|
|
332
331
|
private async loadLookupsAsync() {
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
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
|
+
}
|
|
339
347
|
}
|
|
340
348
|
|
|
341
349
|
@computed
|
|
@@ -9,9 +9,9 @@ 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 {fmtJson} from '@xh/hoist/format';
|
|
13
12
|
import {action, computed, makeObservable, observable} from '@xh/hoist/mobx';
|
|
14
13
|
import {ActivityTrackingModel} from '../ActivityTrackingModel';
|
|
14
|
+
import {fmtJson, timestampReplacer} from '@xh/hoist/format';
|
|
15
15
|
|
|
16
16
|
export class ActivityDetailModel extends HoistModel {
|
|
17
17
|
@lookup(ActivityTrackingModel) activityTrackingModel: ActivityTrackingModel;
|
|
@@ -118,7 +118,7 @@ export class ActivityDetailModel extends HoistModel {
|
|
|
118
118
|
let formattedTrackData = trackData;
|
|
119
119
|
if (formattedTrackData) {
|
|
120
120
|
try {
|
|
121
|
-
formattedTrackData = fmtJson(trackData);
|
|
121
|
+
formattedTrackData = fmtJson(trackData, {replacer: timestampReplacer()});
|
|
122
122
|
} catch (ignored) {}
|
|
123
123
|
}
|
|
124
124
|
|
|
@@ -5,10 +5,7 @@
|
|
|
5
5
|
* Copyright © 2025 Extremely Heavy Industries Inc.
|
|
6
6
|
*/
|
|
7
7
|
import {InstancesTabModel} from '@xh/hoist/admin/tabs/cluster/instances/InstancesTabModel';
|
|
8
|
-
import {HoistModel, LoadSpec, lookup,
|
|
9
|
-
import {fmtDateTimeSec, fmtJson} from '@xh/hoist/format';
|
|
10
|
-
import {DAYS} from '@xh/hoist/utils/datetime';
|
|
11
|
-
import {cloneDeep, forOwn, isArray, isNumber, isPlainObject} from 'lodash';
|
|
8
|
+
import {HoistModel, LoadSpec, lookup, XH} from '@xh/hoist/core';
|
|
12
9
|
import {createRef} from 'react';
|
|
13
10
|
import {isDisplayed} from '@xh/hoist/utils/js';
|
|
14
11
|
|
|
@@ -21,12 +18,6 @@ export class BaseInstanceModel extends HoistModel {
|
|
|
21
18
|
return this.parent.instanceName;
|
|
22
19
|
}
|
|
23
20
|
|
|
24
|
-
fmtStats(stats: PlainObject): string {
|
|
25
|
-
stats = cloneDeep(stats);
|
|
26
|
-
this.processTimestamps(stats);
|
|
27
|
-
return fmtJson(JSON.stringify(stats));
|
|
28
|
-
}
|
|
29
|
-
|
|
30
21
|
handleLoadException(e: unknown, loadSpec: LoadSpec) {
|
|
31
22
|
const instanceNotFound = this.isInstanceNotFound(e),
|
|
32
23
|
connDown = this.parent.lastLoadException,
|
|
@@ -49,23 +40,4 @@ export class BaseInstanceModel extends HoistModel {
|
|
|
49
40
|
private isInstanceNotFound(e: unknown): boolean {
|
|
50
41
|
return e['name'] == 'InstanceNotFoundException';
|
|
51
42
|
}
|
|
52
|
-
|
|
53
|
-
private processTimestamps(stats: PlainObject) {
|
|
54
|
-
forOwn(stats, (v, k) => {
|
|
55
|
-
// Convert numbers that look like recent timestamps to date values.
|
|
56
|
-
if (
|
|
57
|
-
(k.endsWith('Time') ||
|
|
58
|
-
k.endsWith('Date') ||
|
|
59
|
-
k.endsWith('Timestamp') ||
|
|
60
|
-
k == 'timestamp') &&
|
|
61
|
-
isNumber(v) &&
|
|
62
|
-
v > Date.now() - 365 * DAYS
|
|
63
|
-
) {
|
|
64
|
-
stats[k] = v ? fmtDateTimeSec(v, {fmt: 'MMM DD HH:mm:ss.SSS'}) : null;
|
|
65
|
-
}
|
|
66
|
-
if (isPlainObject(v) || isArray(v)) {
|
|
67
|
-
this.processTimestamps(v);
|
|
68
|
-
}
|
|
69
|
-
});
|
|
70
|
-
}
|
|
71
43
|
}
|
|
@@ -12,6 +12,7 @@ import {panel} from '@xh/hoist/desktop/cmp/panel';
|
|
|
12
12
|
import {jsonInput} from '@xh/hoist/desktop/cmp/input';
|
|
13
13
|
import {Icon} from '@xh/hoist/icon';
|
|
14
14
|
import {isEmpty} from 'lodash';
|
|
15
|
+
import {fmtJson, timestampReplacer} from '@xh/hoist/format';
|
|
15
16
|
|
|
16
17
|
export const detailsPanel = hoistCmp.factory({
|
|
17
18
|
model: creates(DetailsModel),
|
|
@@ -57,7 +58,7 @@ const stats = hoistCmp.factory<DetailsModel>({
|
|
|
57
58
|
enableSearch: true,
|
|
58
59
|
showFullscreenButton: false,
|
|
59
60
|
editorProps: {lineNumbers: false},
|
|
60
|
-
value:
|
|
61
|
+
value: fmtJson(stats, {replacer: timestampReplacer()})
|
|
61
62
|
})
|
|
62
63
|
);
|
|
63
64
|
}
|
|
@@ -73,11 +73,33 @@ export const lastReceivedTime: ColumnSpec = {
|
|
|
73
73
|
width: 140
|
|
74
74
|
};
|
|
75
75
|
|
|
76
|
-
export const
|
|
76
|
+
export const appVersion: ColumnSpec = {
|
|
77
77
|
field: {
|
|
78
|
-
name: '
|
|
78
|
+
name: 'appVersion',
|
|
79
79
|
type: 'string',
|
|
80
80
|
displayName: 'Client Version'
|
|
81
81
|
},
|
|
82
82
|
width: 120
|
|
83
83
|
};
|
|
84
|
+
|
|
85
|
+
export const appBuild: ColumnSpec = {
|
|
86
|
+
field: {
|
|
87
|
+
name: 'appBuild',
|
|
88
|
+
type: 'string'
|
|
89
|
+
},
|
|
90
|
+
width: 120
|
|
91
|
+
};
|
|
92
|
+
export const loadId: ColumnSpec = {
|
|
93
|
+
field: {
|
|
94
|
+
name: 'loadId',
|
|
95
|
+
type: 'string'
|
|
96
|
+
},
|
|
97
|
+
width: 120
|
|
98
|
+
};
|
|
99
|
+
export const tabId: ColumnSpec = {
|
|
100
|
+
field: {
|
|
101
|
+
name: 'tabId',
|
|
102
|
+
type: 'string'
|
|
103
|
+
},
|
|
104
|
+
width: 120
|
|
105
|
+
};
|
|
@@ -5,20 +5,21 @@
|
|
|
5
5
|
* Copyright © 2025 Extremely Heavy Industries Inc.
|
|
6
6
|
*/
|
|
7
7
|
import {exportFilenameWithDate} from '@xh/hoist/admin/AdminUtils';
|
|
8
|
+
import {AppModel} from '@xh/hoist/admin/AppModel';
|
|
8
9
|
import * as Col from '@xh/hoist/admin/columns';
|
|
9
10
|
import {BaseInstanceModel} from '@xh/hoist/admin/tabs/cluster/instances/BaseInstanceModel';
|
|
10
11
|
import {GridModel} from '@xh/hoist/cmp/grid';
|
|
11
12
|
import {div, p} from '@xh/hoist/cmp/layout';
|
|
12
13
|
import {LoadSpec, managed, XH} from '@xh/hoist/core';
|
|
14
|
+
import {RecordActionSpec, StoreRecord} from '@xh/hoist/data';
|
|
13
15
|
import {textInput} from '@xh/hoist/desktop/cmp/input';
|
|
14
16
|
import {Icon} from '@xh/hoist/icon';
|
|
15
17
|
import {makeObservable, observable, runInAction} from '@xh/hoist/mobx';
|
|
16
18
|
import {Timer} from '@xh/hoist/utils/async';
|
|
17
19
|
import {SECONDS} from '@xh/hoist/utils/datetime';
|
|
20
|
+
import {pluralize} from '@xh/hoist/utils/js';
|
|
18
21
|
import {isEmpty} from 'lodash';
|
|
19
22
|
import * as WSCol from './WebSocketColumns';
|
|
20
|
-
import {RecordActionSpec} from '@xh/hoist/data';
|
|
21
|
-
import {AppModel} from '@xh/hoist/admin/AppModel';
|
|
22
23
|
|
|
23
24
|
export class WebSocketModel extends BaseInstanceModel {
|
|
24
25
|
@observable
|
|
@@ -34,11 +35,19 @@ export class WebSocketModel extends BaseInstanceModel {
|
|
|
34
35
|
text: 'Force suspend',
|
|
35
36
|
icon: Icon.stopCircle(),
|
|
36
37
|
intent: 'danger',
|
|
37
|
-
actionFn: () => this.forceSuspendAsync(),
|
|
38
|
+
actionFn: ({selectedRecords}) => this.forceSuspendAsync(selectedRecords),
|
|
38
39
|
displayFn: () => ({hidden: AppModel.readonly}),
|
|
39
40
|
recordsRequired: true
|
|
40
41
|
};
|
|
41
42
|
|
|
43
|
+
reqHealthReportAction: RecordActionSpec = {
|
|
44
|
+
text: 'Request Health Report',
|
|
45
|
+
icon: Icon.health(),
|
|
46
|
+
actionFn: ({selectedRecords}) => this.requestHealthReportAsync(selectedRecords),
|
|
47
|
+
recordsRequired: true,
|
|
48
|
+
hidden: !XH.trackService.enabled
|
|
49
|
+
};
|
|
50
|
+
|
|
42
51
|
constructor() {
|
|
43
52
|
super();
|
|
44
53
|
makeObservable(this);
|
|
@@ -48,7 +57,12 @@ export class WebSocketModel extends BaseInstanceModel {
|
|
|
48
57
|
enableExport: true,
|
|
49
58
|
exportOptions: {filename: exportFilenameWithDate('ws-connections')},
|
|
50
59
|
selModel: 'multiple',
|
|
51
|
-
contextMenu: [
|
|
60
|
+
contextMenu: [
|
|
61
|
+
this.forceSuspendAction,
|
|
62
|
+
this.reqHealthReportAction,
|
|
63
|
+
'-',
|
|
64
|
+
...GridModel.defaultContextMenu
|
|
65
|
+
],
|
|
52
66
|
store: {
|
|
53
67
|
idSpec: 'key',
|
|
54
68
|
processRawData: row => {
|
|
@@ -78,7 +92,10 @@ export class WebSocketModel extends BaseInstanceModel {
|
|
|
78
92
|
WSCol.lastSentTime,
|
|
79
93
|
WSCol.receivedMessageCount,
|
|
80
94
|
WSCol.lastReceivedTime,
|
|
81
|
-
WSCol.
|
|
95
|
+
WSCol.appVersion,
|
|
96
|
+
WSCol.appBuild,
|
|
97
|
+
WSCol.loadId,
|
|
98
|
+
WSCol.tabId
|
|
82
99
|
]
|
|
83
100
|
});
|
|
84
101
|
|
|
@@ -107,9 +124,8 @@ export class WebSocketModel extends BaseInstanceModel {
|
|
|
107
124
|
}
|
|
108
125
|
}
|
|
109
126
|
|
|
110
|
-
async forceSuspendAsync() {
|
|
111
|
-
|
|
112
|
-
if (isEmpty(selectedRecords)) return;
|
|
127
|
+
async forceSuspendAsync(toRecs: StoreRecord[]) {
|
|
128
|
+
if (isEmpty(toRecs)) return;
|
|
113
129
|
|
|
114
130
|
const message = await XH.prompt<string>({
|
|
115
131
|
title: 'Please confirm...',
|
|
@@ -123,7 +139,7 @@ export class WebSocketModel extends BaseInstanceModel {
|
|
|
123
139
|
},
|
|
124
140
|
message: div(
|
|
125
141
|
p(
|
|
126
|
-
`This action will force ${
|
|
142
|
+
`This action will force ${toRecs.length} connected client(s) into suspended mode, halting all background refreshes and other activity, masking the UI, and requiring users to reload the app to continue.`
|
|
127
143
|
),
|
|
128
144
|
p('Enter an optional message below to display within the suspended app.')
|
|
129
145
|
),
|
|
@@ -134,23 +150,58 @@ export class WebSocketModel extends BaseInstanceModel {
|
|
|
134
150
|
});
|
|
135
151
|
|
|
136
152
|
if (message !== false) {
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
topic: XH.webSocketService.FORCE_APP_SUSPEND_TOPIC,
|
|
143
|
-
instance: this.instanceName,
|
|
144
|
-
message
|
|
145
|
-
}
|
|
146
|
-
})
|
|
147
|
-
);
|
|
148
|
-
|
|
149
|
-
await Promise.allSettled(tasks).track({
|
|
150
|
-
category: 'Audit',
|
|
151
|
-
message: 'Suspended clients via WebSocket',
|
|
152
|
-
data: {users: selectedRecords.map(it => it.data.user).sort()}
|
|
153
|
+
await this.bulkPush({
|
|
154
|
+
toRecs,
|
|
155
|
+
topic: XH.webSocketService.FORCE_APP_SUSPEND_TOPIC,
|
|
156
|
+
message,
|
|
157
|
+
trackMessage: 'Suspended clients via WebSocket'
|
|
153
158
|
});
|
|
154
159
|
}
|
|
155
160
|
}
|
|
161
|
+
|
|
162
|
+
async requestHealthReportAsync(toRecs: StoreRecord[]) {
|
|
163
|
+
await this.bulkPush({
|
|
164
|
+
toRecs,
|
|
165
|
+
topic: XH.webSocketService.REQ_CLIENT_HEALTH_RPT_TOPIC
|
|
166
|
+
});
|
|
167
|
+
XH.successToast(
|
|
168
|
+
`Client health report requested for ${pluralize('client', toRecs.length, true)} - available in User Activity shortly...`
|
|
169
|
+
);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
//------------------
|
|
173
|
+
// Implementation
|
|
174
|
+
//------------------
|
|
175
|
+
private async bulkPush({
|
|
176
|
+
toRecs,
|
|
177
|
+
topic,
|
|
178
|
+
message,
|
|
179
|
+
trackMessage
|
|
180
|
+
}: {
|
|
181
|
+
toRecs?: StoreRecord[];
|
|
182
|
+
topic: string;
|
|
183
|
+
message?: string;
|
|
184
|
+
trackMessage?: string;
|
|
185
|
+
}) {
|
|
186
|
+
if (isEmpty(toRecs)) return;
|
|
187
|
+
|
|
188
|
+
const tasks = toRecs.map(rec =>
|
|
189
|
+
XH.fetchJson({
|
|
190
|
+
url: 'webSocketAdmin/pushToChannel',
|
|
191
|
+
params: {
|
|
192
|
+
channelKey: rec.data.key,
|
|
193
|
+
instance: this.instanceName,
|
|
194
|
+
topic,
|
|
195
|
+
message
|
|
196
|
+
}
|
|
197
|
+
})
|
|
198
|
+
);
|
|
199
|
+
|
|
200
|
+
await Promise.allSettled(tasks).track({
|
|
201
|
+
category: 'Audit',
|
|
202
|
+
message: trackMessage,
|
|
203
|
+
data: {users: toRecs.map(it => it.data.user).sort()},
|
|
204
|
+
omit: !trackMessage
|
|
205
|
+
});
|
|
206
|
+
}
|
|
156
207
|
}
|
|
@@ -16,7 +16,7 @@ import {panel} from '@xh/hoist/desktop/cmp/panel';
|
|
|
16
16
|
import {recordActionBar} from '@xh/hoist/desktop/cmp/record';
|
|
17
17
|
import {toolbarSep} from '@xh/hoist/desktop/cmp/toolbar';
|
|
18
18
|
|
|
19
|
-
export const webSocketPanel = hoistCmp.factory({
|
|
19
|
+
export const webSocketPanel = hoistCmp.factory<WebSocketModel>({
|
|
20
20
|
model: creates(WebSocketModel),
|
|
21
21
|
|
|
22
22
|
render({model}) {
|
|
@@ -26,7 +26,7 @@ export const webSocketPanel = hoistCmp.factory({
|
|
|
26
26
|
bbar: [
|
|
27
27
|
recordActionBar({
|
|
28
28
|
selModel: model.gridModel.selModel,
|
|
29
|
-
actions: [model.forceSuspendAction]
|
|
29
|
+
actions: [model.forceSuspendAction, model.reqHealthReportAction]
|
|
30
30
|
}),
|
|
31
31
|
filler(),
|
|
32
32
|
relativeTimestamp({bind: 'lastRefresh', options: {prefix: 'Refreshed'}}),
|
|
@@ -6,21 +6,11 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import {ClusterObjectsModel} from '@xh/hoist/admin/tabs/cluster/objects/ClusterObjectsModel';
|
|
8
8
|
import {ColumnSpec, GridModel} from '@xh/hoist/cmp/grid';
|
|
9
|
-
import {HoistModel, lookup, managed,
|
|
9
|
+
import {HoistModel, lookup, managed, XH} from '@xh/hoist/core';
|
|
10
10
|
import {StoreRecord} from '@xh/hoist/data';
|
|
11
|
-
import {fmtDateTimeSec, fmtJson} from '@xh/hoist/format';
|
|
12
11
|
import {action, makeObservable, observable} from '@xh/hoist/mobx';
|
|
13
|
-
import {
|
|
14
|
-
import {
|
|
15
|
-
cloneDeep,
|
|
16
|
-
forOwn,
|
|
17
|
-
isArray,
|
|
18
|
-
isEmpty,
|
|
19
|
-
isEqual,
|
|
20
|
-
isNumber,
|
|
21
|
-
isPlainObject,
|
|
22
|
-
without
|
|
23
|
-
} from 'lodash';
|
|
12
|
+
import {isEmpty, isEqual, without} from 'lodash';
|
|
13
|
+
import {withFormattedTimestamps} from '@xh/hoist/format';
|
|
24
14
|
|
|
25
15
|
export class DetailModel extends HoistModel {
|
|
26
16
|
@lookup(ClusterObjectsModel)
|
|
@@ -66,12 +56,6 @@ export class DetailModel extends HoistModel {
|
|
|
66
56
|
});
|
|
67
57
|
}
|
|
68
58
|
|
|
69
|
-
fmtStats(stats: PlainObject): string {
|
|
70
|
-
stats = cloneDeep(stats);
|
|
71
|
-
this.processTimestamps(stats);
|
|
72
|
-
return fmtJson(JSON.stringify(stats));
|
|
73
|
-
}
|
|
74
|
-
|
|
75
59
|
//----------------------
|
|
76
60
|
// Implementation
|
|
77
61
|
//----------------------
|
|
@@ -95,8 +79,7 @@ export class DetailModel extends HoistModel {
|
|
|
95
79
|
const gridModel = this.createGridModel(diffFields, otherFields);
|
|
96
80
|
gridModel.loadData(
|
|
97
81
|
instanceNames.map(instanceName => {
|
|
98
|
-
const data =
|
|
99
|
-
this.processTimestamps(data);
|
|
82
|
+
const data = withFormattedTimestamps(adminStatsByInstance[instanceName] ?? {});
|
|
100
83
|
return {instanceName, ...data};
|
|
101
84
|
})
|
|
102
85
|
);
|
|
@@ -136,23 +119,4 @@ export class DetailModel extends HoistModel {
|
|
|
136
119
|
}
|
|
137
120
|
return ret;
|
|
138
121
|
}
|
|
139
|
-
|
|
140
|
-
private processTimestamps(stats: PlainObject) {
|
|
141
|
-
forOwn(stats, (v, k) => {
|
|
142
|
-
// Convert numbers that look like recent timestamps to date values.
|
|
143
|
-
if (
|
|
144
|
-
(k.endsWith('Time') ||
|
|
145
|
-
k.endsWith('Date') ||
|
|
146
|
-
k.endsWith('Timestamp') ||
|
|
147
|
-
k == 'timestamp') &&
|
|
148
|
-
isNumber(v) &&
|
|
149
|
-
v > Date.now() - 365 * DAYS
|
|
150
|
-
) {
|
|
151
|
-
stats[k] = v ? fmtDateTimeSec(v, {fmt: 'MMM DD HH:mm:ss.SSS'}) : null;
|
|
152
|
-
}
|
|
153
|
-
if (isPlainObject(v) || isArray(v)) {
|
|
154
|
-
this.processTimestamps(v);
|
|
155
|
-
}
|
|
156
|
-
});
|
|
157
|
-
}
|
|
158
122
|
}
|
|
@@ -12,6 +12,7 @@ import {panel} from '@xh/hoist/desktop/cmp/panel';
|
|
|
12
12
|
import {Icon} from '@xh/hoist/icon';
|
|
13
13
|
import {DetailModel} from './DetailModel';
|
|
14
14
|
import './ClusterObjects.scss';
|
|
15
|
+
import {fmtJson, timestampReplacer} from '@xh/hoist/format';
|
|
15
16
|
|
|
16
17
|
export const detailPanel = hoistCmp.factory({
|
|
17
18
|
model: creates(DetailModel),
|
|
@@ -42,7 +43,7 @@ export const detailPanel = hoistCmp.factory({
|
|
|
42
43
|
height: '100%',
|
|
43
44
|
showFullscreenButton: false,
|
|
44
45
|
editorProps: {lineNumbers: false},
|
|
45
|
-
value:
|
|
46
|
+
value: fmtJson(selectedAdminStats, {replacer: timestampReplacer()})
|
|
46
47
|
})
|
|
47
48
|
})
|
|
48
49
|
]
|
|
@@ -25,6 +25,7 @@ import {
|
|
|
25
25
|
AlertBannerService,
|
|
26
26
|
AutoRefreshService,
|
|
27
27
|
ChangelogService,
|
|
28
|
+
ClientHealthService,
|
|
28
29
|
ConfigService,
|
|
29
30
|
EnvironmentService,
|
|
30
31
|
FetchService,
|
|
@@ -237,6 +238,7 @@ export class AppContainerModel extends HoistModel {
|
|
|
237
238
|
AlertBannerService,
|
|
238
239
|
AutoRefreshService,
|
|
239
240
|
ChangelogService,
|
|
241
|
+
ClientHealthService,
|
|
240
242
|
IdleService,
|
|
241
243
|
InspectorService,
|
|
242
244
|
GridAutosizeService,
|
|
@@ -4,11 +4,10 @@
|
|
|
4
4
|
*
|
|
5
5
|
* Copyright © 2025 Extremely Heavy Industries Inc.
|
|
6
6
|
*/
|
|
7
|
-
import {AppState, AppSuspendData, HoistModel, XH} from '@xh/hoist/core';
|
|
7
|
+
import {AppState, AppSuspendData, HoistModel, PlainObject, XH} from '@xh/hoist/core';
|
|
8
8
|
import {action, makeObservable, observable} from '@xh/hoist/mobx';
|
|
9
9
|
import {Timer} from '@xh/hoist/utils/async';
|
|
10
|
-
import {
|
|
11
|
-
import {camelCase, isBoolean, isString, mapKeys} from 'lodash';
|
|
10
|
+
import {camelCase, isBoolean, isString, mapKeys, pick} from 'lodash';
|
|
12
11
|
|
|
13
12
|
/**
|
|
14
13
|
* Support for Core Hoist Application state and loading.
|
|
@@ -93,13 +92,14 @@ export class AppStateModel extends HoistModel {
|
|
|
93
92
|
timestamp: loadStarted,
|
|
94
93
|
elapsed: Date.now() - loadStarted - (timings.LOGIN_REQUIRED ?? 0),
|
|
95
94
|
data: {
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
locationHref: window.location.href,
|
|
95
|
+
loadId: XH.loadId,
|
|
96
|
+
tabId: XH.tabId,
|
|
99
97
|
timings: mapKeys(timings, (v, k) => camelCase(k)),
|
|
100
|
-
|
|
98
|
+
clientHealth: XH.clientHealthService.getReport(),
|
|
99
|
+
window: this.getWindowData(),
|
|
100
|
+
screen: this.getScreenData()
|
|
101
101
|
},
|
|
102
|
-
logData: ['
|
|
102
|
+
logData: ['loadId', 'tabId'],
|
|
103
103
|
omit: !XH.appSpec.trackAppLoad
|
|
104
104
|
})
|
|
105
105
|
});
|
|
@@ -115,4 +115,36 @@ export class AppStateModel extends HoistModel {
|
|
|
115
115
|
});
|
|
116
116
|
});
|
|
117
117
|
}
|
|
118
|
+
|
|
119
|
+
private getScreenData(): PlainObject {
|
|
120
|
+
const screen = window.screen as any;
|
|
121
|
+
if (!screen) return null;
|
|
122
|
+
|
|
123
|
+
const ret: PlainObject = pick(screen, [
|
|
124
|
+
'availWidth',
|
|
125
|
+
'availHeight',
|
|
126
|
+
'width',
|
|
127
|
+
'height',
|
|
128
|
+
'colorDepth',
|
|
129
|
+
'pixelDepth',
|
|
130
|
+
'availLeft',
|
|
131
|
+
'availTop'
|
|
132
|
+
]);
|
|
133
|
+
if (screen.orientation) {
|
|
134
|
+
ret.orientation = pick(screen.orientation, ['angle', 'type']);
|
|
135
|
+
}
|
|
136
|
+
return ret;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
private getWindowData(): PlainObject {
|
|
140
|
+
return pick(window, [
|
|
141
|
+
'devicePixelRatio',
|
|
142
|
+
'screenX',
|
|
143
|
+
'screenY',
|
|
144
|
+
'innerWidth',
|
|
145
|
+
'innerHeight',
|
|
146
|
+
'outerWidth',
|
|
147
|
+
'outerHeight'
|
|
148
|
+
]);
|
|
149
|
+
}
|
|
118
150
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { GroupingChooserModel } from '@xh/hoist/cmp/grouping';
|
|
2
1
|
import { FilterChooserModel } from '@xh/hoist/cmp/filter';
|
|
3
2
|
import { FormModel } from '@xh/hoist/cmp/form';
|
|
4
3
|
import { GridModel } from '@xh/hoist/cmp/grid';
|
|
4
|
+
import { GroupingChooserModel } from '@xh/hoist/cmp/grouping';
|
|
5
5
|
import { HoistModel, LoadSpec } from '@xh/hoist/core';
|
|
6
6
|
import { Cube } from '@xh/hoist/data';
|
|
7
7
|
import { LocalDate } from '@xh/hoist/utils/datetime';
|
|
@@ -1,13 +1,11 @@
|
|
|
1
1
|
/// <reference types="react" />
|
|
2
2
|
import { InstancesTabModel } from '@xh/hoist/admin/tabs/cluster/instances/InstancesTabModel';
|
|
3
|
-
import { HoistModel, LoadSpec
|
|
3
|
+
import { HoistModel, LoadSpec } from '@xh/hoist/core';
|
|
4
4
|
export declare class BaseInstanceModel extends HoistModel {
|
|
5
5
|
viewRef: import("react").RefObject<HTMLElement>;
|
|
6
6
|
parent: InstancesTabModel;
|
|
7
7
|
get instanceName(): string;
|
|
8
|
-
fmtStats(stats: PlainObject): string;
|
|
9
8
|
handleLoadException(e: unknown, loadSpec: LoadSpec): void;
|
|
10
9
|
get isVisible(): boolean;
|
|
11
10
|
private isInstanceNotFound;
|
|
12
|
-
private processTimestamps;
|
|
13
11
|
}
|
|
@@ -6,4 +6,7 @@ export declare const sentMessageCount: ColumnSpec;
|
|
|
6
6
|
export declare const lastSentTime: ColumnSpec;
|
|
7
7
|
export declare const receivedMessageCount: ColumnSpec;
|
|
8
8
|
export declare const lastReceivedTime: ColumnSpec;
|
|
9
|
-
export declare const
|
|
9
|
+
export declare const appVersion: ColumnSpec;
|
|
10
|
+
export declare const appBuild: ColumnSpec;
|
|
11
|
+
export declare const loadId: ColumnSpec;
|
|
12
|
+
export declare const tabId: ColumnSpec;
|
|
@@ -1,13 +1,16 @@
|
|
|
1
1
|
import { BaseInstanceModel } from '@xh/hoist/admin/tabs/cluster/instances/BaseInstanceModel';
|
|
2
2
|
import { GridModel } from '@xh/hoist/cmp/grid';
|
|
3
3
|
import { LoadSpec } from '@xh/hoist/core';
|
|
4
|
-
import { RecordActionSpec } from '@xh/hoist/data';
|
|
4
|
+
import { RecordActionSpec, StoreRecord } from '@xh/hoist/data';
|
|
5
5
|
export declare class WebSocketModel extends BaseInstanceModel {
|
|
6
6
|
lastRefresh: number;
|
|
7
7
|
gridModel: GridModel;
|
|
8
8
|
private _timer;
|
|
9
9
|
forceSuspendAction: RecordActionSpec;
|
|
10
|
+
reqHealthReportAction: RecordActionSpec;
|
|
10
11
|
constructor();
|
|
11
12
|
doLoadAsync(loadSpec: LoadSpec): Promise<void>;
|
|
12
|
-
forceSuspendAsync(): Promise<void>;
|
|
13
|
+
forceSuspendAsync(toRecs: StoreRecord[]): Promise<void>;
|
|
14
|
+
requestHealthReportAsync(toRecs: StoreRecord[]): Promise<void>;
|
|
15
|
+
private bulkPush;
|
|
13
16
|
}
|