@xh/hoist 73.0.0-SNAPSHOT.1745446674015 → 73.0.0-SNAPSHOT.1745457790188
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 +15 -5
- package/admin/AppModel.ts +19 -12
- package/admin/columns/Core.ts +29 -0
- package/admin/columns/Tracking.ts +3 -23
- package/admin/tabs/BaseAdminTabModel.ts +17 -0
- package/admin/tabs/activity/{ActivityTab.scss → tracking/ActivityTracking.scss} +0 -8
- package/admin/tabs/activity/tracking/ActivityTrackingPanel.ts +1 -0
- package/admin/tabs/activity/tracking/detail/ActivityDetailView.ts +5 -1
- package/admin/tabs/{activity/ActivityTab.ts → client/ClientTab.ts} +7 -8
- package/admin/tabs/{cluster/instances/websocket/WebSocketColumns.ts → client/clients/ClientsColumns.ts} +12 -6
- package/admin/tabs/{cluster/instances/websocket/WebSocketModel.ts → client/clients/ClientsModel.ts} +99 -57
- package/admin/tabs/{cluster/instances/websocket/WebSocketPanel.ts → client/clients/ClientsPanel.ts} +32 -26
- package/admin/tabs/{activity/clienterrors → client/errors}/ClientErrorDetail.ts +9 -5
- package/admin/tabs/client/errors/ClientErrors.scss +52 -0
- package/admin/tabs/{activity/clienterrors → client/errors}/ClientErrorsPanel.ts +2 -1
- package/admin/tabs/cluster/instances/BaseInstanceModel.ts +3 -10
- package/admin/tabs/cluster/instances/InstancesTabModel.ts +1 -3
- package/admin/tabs/monitor/MonitorTabModel.ts +4 -3
- package/admin/tabs/userData/roles/RoleModel.ts +1 -0
- package/admin/tabs/userData/roles/RolePanel.ts +4 -2
- package/build/types/admin/columns/Core.d.ts +3 -0
- package/build/types/admin/tabs/BaseAdminTabModel.d.ts +6 -0
- package/build/types/admin/tabs/activity/tracking/ActivityTrackingPanel.d.ts +1 -0
- package/build/types/admin/tabs/client/ClientTab.d.ts +1 -0
- package/build/types/admin/tabs/{cluster/instances/websocket/WebSocketModel.d.ts → client/clients/ClientsModel.d.ts} +5 -2
- package/build/types/admin/tabs/client/clients/ClientsPanel.d.ts +2 -0
- package/build/types/admin/tabs/{activity/clienterrors → client/errors}/ClientErrorsPanel.d.ts +1 -0
- package/build/types/admin/tabs/cluster/instances/BaseInstanceModel.d.ts +3 -5
- package/build/types/admin/tabs/monitor/MonitorTabModel.d.ts +3 -2
- package/package.json +1 -1
- package/tsconfig.tsbuildinfo +1 -1
- package/build/types/admin/tabs/activity/ActivityTab.d.ts +0 -2
- package/build/types/admin/tabs/cluster/instances/websocket/WebSocketPanel.d.ts +0 -2
- /package/admin/tabs/{activity/clienterrors → client/errors}/ClientErrorsModel.ts +0 -0
- /package/admin/tabs/{activity → client}/feedback/FeedbackPanel.ts +0 -0
- /package/build/types/admin/tabs/{cluster/instances/websocket/WebSocketColumns.d.ts → client/clients/ClientsColumns.d.ts} +0 -0
- /package/build/types/admin/tabs/{activity/clienterrors → client/errors}/ClientErrorDetail.d.ts +0 -0
- /package/build/types/admin/tabs/{activity/clienterrors → client/errors}/ClientErrorsModel.d.ts +0 -0
- /package/build/types/admin/tabs/{activity → client}/feedback/FeedbackPanel.d.ts +0 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,7 +2,17 @@
|
|
|
2
2
|
|
|
3
3
|
## v73.0.0-SNAPSHOT - unreleased
|
|
4
4
|
|
|
5
|
+
### 💥 Breaking Changes (upgrade difficulty: 🟢 TRIVIAL - minor upgrade to Hoist Core)
|
|
6
|
+
|
|
7
|
+
Requires `hoist-core >= 30.0` with new APIs to support the consolidated Admin Console "Clients" tab.
|
|
8
|
+
|
|
9
|
+
### 🎁 New Features
|
|
10
|
+
|
|
11
|
+
* Added a new "Clients" Admin Console tab- a consolidated view of all websocket-connected clients
|
|
12
|
+
across all instances in the cluster.
|
|
13
|
+
|
|
5
14
|
### 🐞 Bug Fixes
|
|
15
|
+
|
|
6
16
|
* Fixed drag-and-drop usability issues with the mobile `ColChooser`.
|
|
7
17
|
* Made `GridModel.defaultGroupSortFn` null-safe and improved type signature.
|
|
8
18
|
|
|
@@ -16,11 +26,11 @@
|
|
|
16
26
|
* Updated the background version checking performed by `EnvironmentService` to use the app version
|
|
17
27
|
and build information baked into the client build when comparing against the latest values from
|
|
18
28
|
the server. Previously the versions loaded from the server on init were used as the baseline.
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
29
|
+
* The two versions *should* be the same, but in cases where a browser "restores" a tab and
|
|
30
|
+
re-inits an app without reloading the code itself, the upgrade check would miss the fact that
|
|
31
|
+
the client remained on an older version.
|
|
32
|
+
* Note that a misconfigured build - where the client build version is not set to the same value
|
|
33
|
+
as the server - would result in a false positive for an upgrade. The two should always match.
|
|
24
34
|
|
|
25
35
|
## v72.5.1 - 2025-04-15
|
|
26
36
|
|
package/admin/AppModel.ts
CHANGED
|
@@ -11,10 +11,11 @@ import {HoistAppModel, managed, XH} from '@xh/hoist/core';
|
|
|
11
11
|
import {Icon} from '@xh/hoist/icon';
|
|
12
12
|
import {without} from 'lodash';
|
|
13
13
|
import {Route} from 'router5';
|
|
14
|
-
import {
|
|
14
|
+
import {activityTrackingPanel} from './tabs/activity/tracking/ActivityTrackingPanel';
|
|
15
15
|
import {generalTab} from './tabs/general/GeneralTab';
|
|
16
16
|
import {monitorTab} from './tabs/monitor/MonitorTab';
|
|
17
17
|
import {userDataTab} from './tabs/userData/UserDataTab';
|
|
18
|
+
import {clientTab} from './tabs/client/ClientTab';
|
|
18
19
|
|
|
19
20
|
export class AppModel extends HoistAppModel {
|
|
20
21
|
static instance: AppModel;
|
|
@@ -79,27 +80,28 @@ export class AppModel extends HoistAppModel {
|
|
|
79
80
|
{name: 'memory', path: '/memory'},
|
|
80
81
|
{name: 'jdbcPool', path: '/jdbcPool'},
|
|
81
82
|
{name: 'environment', path: '/environment'},
|
|
82
|
-
{name: 'services', path: '/services'}
|
|
83
|
-
{name: 'hibernate', path: '/hibernate'},
|
|
84
|
-
{name: 'consistency', path: '/consistency'},
|
|
85
|
-
{name: 'webSockets', path: '/webSockets'}
|
|
83
|
+
{name: 'services', path: '/services'}
|
|
86
84
|
]
|
|
87
85
|
},
|
|
88
86
|
{name: 'objects', path: '/objects'}
|
|
89
87
|
]
|
|
90
88
|
},
|
|
89
|
+
{
|
|
90
|
+
name: 'clients',
|
|
91
|
+
path: '/clients',
|
|
92
|
+
children: [
|
|
93
|
+
{name: 'connections', path: '/connections'},
|
|
94
|
+
{name: 'errors', path: '/errors'},
|
|
95
|
+
{name: 'feedback', path: '/feedback'}
|
|
96
|
+
]
|
|
97
|
+
},
|
|
91
98
|
{
|
|
92
99
|
name: 'monitors',
|
|
93
100
|
path: '/monitors'
|
|
94
101
|
},
|
|
95
102
|
{
|
|
96
103
|
name: 'activity',
|
|
97
|
-
path: '/activity'
|
|
98
|
-
children: [
|
|
99
|
-
{name: 'tracking', path: '/tracking'},
|
|
100
|
-
{name: 'clientErrors', path: '/clientErrors'},
|
|
101
|
-
{name: 'feedback', path: '/feedback'}
|
|
102
|
-
]
|
|
104
|
+
path: '/activity'
|
|
103
105
|
},
|
|
104
106
|
{
|
|
105
107
|
name: 'userData',
|
|
@@ -126,6 +128,11 @@ export class AppModel extends HoistAppModel {
|
|
|
126
128
|
icon: Icon.server(),
|
|
127
129
|
content: clusterTab
|
|
128
130
|
},
|
|
131
|
+
{
|
|
132
|
+
id: 'clients',
|
|
133
|
+
icon: Icon.desktop(),
|
|
134
|
+
content: clientTab
|
|
135
|
+
},
|
|
129
136
|
{
|
|
130
137
|
id: 'monitors',
|
|
131
138
|
icon: Icon.shieldCheck(),
|
|
@@ -140,7 +147,7 @@ export class AppModel extends HoistAppModel {
|
|
|
140
147
|
id: 'activity',
|
|
141
148
|
title: 'User Activity',
|
|
142
149
|
icon: Icon.analytics(),
|
|
143
|
-
content:
|
|
150
|
+
content: activityTrackingPanel
|
|
144
151
|
}
|
|
145
152
|
];
|
|
146
153
|
}
|
package/admin/columns/Core.ts
CHANGED
|
@@ -4,8 +4,12 @@
|
|
|
4
4
|
*
|
|
5
5
|
* Copyright © 2025 Extremely Heavy Industries Inc.
|
|
6
6
|
*/
|
|
7
|
+
import {badge} from '@xh/hoist/cmp/badge';
|
|
7
8
|
import {ColumnSpec, dateTimeSec} from '@xh/hoist/cmp/grid';
|
|
9
|
+
import {XH} from '@xh/hoist/core';
|
|
8
10
|
import {dateTimeRenderer} from '@xh/hoist/format';
|
|
11
|
+
import {Icon} from '@xh/hoist/icon';
|
|
12
|
+
import copy from 'clipboard-copy';
|
|
9
13
|
|
|
10
14
|
export const name: ColumnSpec = {
|
|
11
15
|
field: {name: 'name', type: 'string'},
|
|
@@ -46,3 +50,28 @@ export const timestampNoYear: ColumnSpec = {
|
|
|
46
50
|
...dateTimeSec,
|
|
47
51
|
renderer: dateTimeRenderer({fmt: 'MMM DD HH:mm:ss.SSS'})
|
|
48
52
|
};
|
|
53
|
+
|
|
54
|
+
export function badgeRenderer(v) {
|
|
55
|
+
return v
|
|
56
|
+
? badge({
|
|
57
|
+
item: v,
|
|
58
|
+
className: 'xh-font-family-mono',
|
|
59
|
+
style: {cursor: 'copy'},
|
|
60
|
+
intent: 'primary',
|
|
61
|
+
title: 'Double-click to copy',
|
|
62
|
+
onDoubleClick: () => {
|
|
63
|
+
copy(v);
|
|
64
|
+
XH.toast({
|
|
65
|
+
icon: Icon.copy(),
|
|
66
|
+
message: `Copied ${v}`
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
})
|
|
70
|
+
: '-';
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export const badgeCol: ColumnSpec = {
|
|
74
|
+
autosizable: false,
|
|
75
|
+
width: 90,
|
|
76
|
+
renderer: badgeRenderer
|
|
77
|
+
};
|
|
@@ -4,14 +4,12 @@
|
|
|
4
4
|
*
|
|
5
5
|
* Copyright © 2025 Extremely Heavy Industries Inc.
|
|
6
6
|
*/
|
|
7
|
+
import {badgeRenderer} from '@xh/hoist/admin/columns';
|
|
7
8
|
import {RangeAggregator} from '@xh/hoist/admin/tabs/activity/aggregators/RangeAggregator';
|
|
8
|
-
import {badge} from '@xh/hoist/cmp/badge';
|
|
9
|
-
import {XH} from '@xh/hoist/core';
|
|
10
|
-
import {Icon} from '@xh/hoist/icon';
|
|
11
|
-
import {fmtDate, fmtSpan, numberRenderer} from '@xh/hoist/format';
|
|
12
9
|
import * as Col from '@xh/hoist/cmp/grid/columns';
|
|
13
10
|
import {ColumnSpec} from '@xh/hoist/cmp/grid/columns';
|
|
14
|
-
import
|
|
11
|
+
import {fmtDate, fmtSpan, numberRenderer} from '@xh/hoist/format';
|
|
12
|
+
import {Icon} from '@xh/hoist/icon';
|
|
15
13
|
|
|
16
14
|
export const appEnvironment: ColumnSpec = {
|
|
17
15
|
field: {
|
|
@@ -243,21 +241,3 @@ function dayRangeComparator(rangeA, rangeB, sortDir, abs, {defaultComparator}) {
|
|
|
243
241
|
|
|
244
242
|
return defaultComparator(maxA, maxB);
|
|
245
243
|
}
|
|
246
|
-
|
|
247
|
-
function badgeRenderer(v) {
|
|
248
|
-
return v
|
|
249
|
-
? badge({
|
|
250
|
-
item: v,
|
|
251
|
-
className: 'xh-font-family-mono',
|
|
252
|
-
style: {cursor: 'copy'},
|
|
253
|
-
title: 'Double-click to copy',
|
|
254
|
-
onDoubleClick: () => {
|
|
255
|
-
copy(v);
|
|
256
|
-
XH.toast({
|
|
257
|
-
icon: Icon.copy(),
|
|
258
|
-
message: `Copied ${v}`
|
|
259
|
-
});
|
|
260
|
-
}
|
|
261
|
-
})
|
|
262
|
-
: '-';
|
|
263
|
-
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* This file belongs to Hoist, an application development toolkit
|
|
3
|
+
* developed by Extremely Heavy Industries (www.xh.io | info@xh.io)
|
|
4
|
+
*
|
|
5
|
+
* Copyright © 2025 Extremely Heavy Industries Inc.
|
|
6
|
+
*/
|
|
7
|
+
import {HoistModel, XH} from '@xh/hoist/core';
|
|
8
|
+
import {createRef} from 'react';
|
|
9
|
+
import {isDisplayed} from '@xh/hoist/utils/js';
|
|
10
|
+
|
|
11
|
+
export class BaseAdminTabModel extends HoistModel {
|
|
12
|
+
viewRef = createRef<HTMLElement>();
|
|
13
|
+
|
|
14
|
+
get isVisible() {
|
|
15
|
+
return XH.pageIsVisible && isDisplayed(this.viewRef.current);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
@@ -56,14 +56,6 @@
|
|
|
56
56
|
}
|
|
57
57
|
}
|
|
58
58
|
|
|
59
|
-
&__message {
|
|
60
|
-
background-color: var(--xh-grid-bg-odd);
|
|
61
|
-
padding: var(--xh-pad-px);
|
|
62
|
-
overflow: auto;
|
|
63
|
-
width: 100%;
|
|
64
|
-
height: 100%;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
59
|
h3 {
|
|
68
60
|
color: var(--xh-text-color-headings);
|
|
69
61
|
background-color: var(--xh-blue-gray-light);
|
|
@@ -21,6 +21,7 @@ import {LocalDate} from '@xh/hoist/utils/datetime';
|
|
|
21
21
|
import {ActivityTrackingModel} from './ActivityTrackingModel';
|
|
22
22
|
import {chartsPanel} from './charts/ChartsPanel';
|
|
23
23
|
import {activityDetailView} from './detail/ActivityDetailView';
|
|
24
|
+
import './ActivityTracking.scss';
|
|
24
25
|
|
|
25
26
|
export const activityTrackingPanel = hoistCmp.factory({
|
|
26
27
|
model: creates(ActivityTrackingModel),
|
|
@@ -39,6 +39,7 @@ const tbar = hoistCmp.factory(({model}) => {
|
|
|
39
39
|
return toolbar(
|
|
40
40
|
filler(),
|
|
41
41
|
gridCountLabel({unit: 'entry'}),
|
|
42
|
+
'-',
|
|
42
43
|
gridFindField(),
|
|
43
44
|
colChooserButton(),
|
|
44
45
|
exportButton()
|
|
@@ -48,9 +49,12 @@ const tbar = hoistCmp.factory(({model}) => {
|
|
|
48
49
|
// Discrete outer panel to retain sizing across master/detail selection changes.
|
|
49
50
|
const detailRecPanel = hoistCmp.factory<ActivityDetailModel>(({model}) => {
|
|
50
51
|
return panel({
|
|
52
|
+
collapsedTitle: 'Activity Details',
|
|
53
|
+
collapsedIcon: Icon.info(),
|
|
54
|
+
compactHeader: true,
|
|
51
55
|
modelConfig: {
|
|
52
56
|
side: 'bottom',
|
|
53
|
-
defaultSize:
|
|
57
|
+
defaultSize: 400
|
|
54
58
|
},
|
|
55
59
|
item: detailRecForm()
|
|
56
60
|
});
|
|
@@ -7,19 +7,18 @@
|
|
|
7
7
|
import {tabContainer} from '@xh/hoist/cmp/tab';
|
|
8
8
|
import {hoistCmp} from '@xh/hoist/core';
|
|
9
9
|
import {Icon} from '@xh/hoist/icon';
|
|
10
|
-
import '
|
|
11
|
-
import {clientErrorsPanel} from './clienterrors/ClientErrorsPanel';
|
|
10
|
+
import {clientErrorsPanel} from '@xh/hoist/admin/tabs/client/errors/ClientErrorsPanel';
|
|
12
11
|
import {feedbackPanel} from './feedback/FeedbackPanel';
|
|
13
|
-
import {
|
|
12
|
+
import {clientsPanel} from './clients/ClientsPanel';
|
|
14
13
|
|
|
15
|
-
export const
|
|
14
|
+
export const clientTab = hoistCmp.factory(() =>
|
|
16
15
|
tabContainer({
|
|
17
16
|
modelConfig: {
|
|
18
|
-
route: 'default.
|
|
19
|
-
switcher: {orientation: 'left', testId: '
|
|
17
|
+
route: 'default.clients',
|
|
18
|
+
switcher: {orientation: 'left', testId: 'client-tab-switcher'},
|
|
20
19
|
tabs: [
|
|
21
|
-
{id: '
|
|
22
|
-
{id: '
|
|
20
|
+
{id: 'connections', icon: Icon.diff(), content: clientsPanel},
|
|
21
|
+
{id: 'errors', icon: Icon.warning(), content: clientErrorsPanel},
|
|
23
22
|
{id: 'feedback', icon: Icon.comment(), content: feedbackPanel}
|
|
24
23
|
]
|
|
25
24
|
}
|
|
@@ -4,9 +4,10 @@
|
|
|
4
4
|
*
|
|
5
5
|
* Copyright © 2025 Extremely Heavy Industries Inc.
|
|
6
6
|
*/
|
|
7
|
-
import {
|
|
7
|
+
import {badgeCol} from '@xh/hoist/admin/columns';
|
|
8
8
|
import * as Col from '@xh/hoist/cmp/grid/columns';
|
|
9
9
|
import {ColumnSpec} from '@xh/hoist/cmp/grid/columns';
|
|
10
|
+
import {Icon} from '@xh/hoist/icon';
|
|
10
11
|
|
|
11
12
|
export const isOpen: ColumnSpec = {
|
|
12
13
|
field: {name: 'isOpen', type: 'bool'},
|
|
@@ -20,7 +21,11 @@ export const isOpen: ColumnSpec = {
|
|
|
20
21
|
};
|
|
21
22
|
|
|
22
23
|
export const key: ColumnSpec = {
|
|
23
|
-
field: {
|
|
24
|
+
field: {
|
|
25
|
+
name: 'key',
|
|
26
|
+
type: 'string',
|
|
27
|
+
displayName: 'Channel Key'
|
|
28
|
+
},
|
|
24
29
|
width: 160
|
|
25
30
|
};
|
|
26
31
|
|
|
@@ -76,8 +81,8 @@ export const lastReceivedTime: ColumnSpec = {
|
|
|
76
81
|
export const appVersion: ColumnSpec = {
|
|
77
82
|
field: {
|
|
78
83
|
name: 'appVersion',
|
|
79
|
-
|
|
80
|
-
|
|
84
|
+
displayName: 'Version',
|
|
85
|
+
type: 'string'
|
|
81
86
|
},
|
|
82
87
|
width: 120
|
|
83
88
|
};
|
|
@@ -85,6 +90,7 @@ export const appVersion: ColumnSpec = {
|
|
|
85
90
|
export const appBuild: ColumnSpec = {
|
|
86
91
|
field: {
|
|
87
92
|
name: 'appBuild',
|
|
93
|
+
displayName: 'Build',
|
|
88
94
|
type: 'string'
|
|
89
95
|
},
|
|
90
96
|
width: 120
|
|
@@ -94,12 +100,12 @@ export const loadId: ColumnSpec = {
|
|
|
94
100
|
name: 'loadId',
|
|
95
101
|
type: 'string'
|
|
96
102
|
},
|
|
97
|
-
|
|
103
|
+
...badgeCol
|
|
98
104
|
};
|
|
99
105
|
export const tabId: ColumnSpec = {
|
|
100
106
|
field: {
|
|
101
107
|
name: 'tabId',
|
|
102
108
|
type: 'string'
|
|
103
109
|
},
|
|
104
|
-
|
|
110
|
+
...badgeCol
|
|
105
111
|
};
|
package/admin/tabs/{cluster/instances/websocket/WebSocketModel.ts → client/clients/ClientsModel.ts}
RENAMED
|
@@ -7,27 +7,30 @@
|
|
|
7
7
|
import {exportFilenameWithDate} from '@xh/hoist/admin/AdminUtils';
|
|
8
8
|
import {AppModel} from '@xh/hoist/admin/AppModel';
|
|
9
9
|
import * as Col from '@xh/hoist/admin/columns';
|
|
10
|
-
import {BaseInstanceModel} from '@xh/hoist/admin/tabs/cluster/instances/BaseInstanceModel';
|
|
11
10
|
import {GridModel} from '@xh/hoist/cmp/grid';
|
|
12
11
|
import {div, p} from '@xh/hoist/cmp/layout';
|
|
13
12
|
import {LoadSpec, managed, XH} from '@xh/hoist/core';
|
|
14
13
|
import {RecordActionSpec, StoreRecord} from '@xh/hoist/data';
|
|
15
14
|
import {textInput} from '@xh/hoist/desktop/cmp/input';
|
|
16
15
|
import {Icon} from '@xh/hoist/icon';
|
|
17
|
-
import {makeObservable, observable, runInAction} from '@xh/hoist/mobx';
|
|
16
|
+
import {bindable, makeObservable, observable, runInAction} from '@xh/hoist/mobx';
|
|
18
17
|
import {Timer} from '@xh/hoist/utils/async';
|
|
19
18
|
import {SECONDS} from '@xh/hoist/utils/datetime';
|
|
20
19
|
import {pluralize} from '@xh/hoist/utils/js';
|
|
21
20
|
import {isEmpty} from 'lodash';
|
|
22
|
-
import * as WSCol from './
|
|
21
|
+
import * as WSCol from './ClientsColumns';
|
|
22
|
+
import {BaseAdminTabModel} from '@xh/hoist/admin/tabs/BaseAdminTabModel';
|
|
23
23
|
|
|
24
|
-
export class
|
|
24
|
+
export class ClientsModel extends BaseAdminTabModel {
|
|
25
25
|
@observable
|
|
26
26
|
lastRefresh: number;
|
|
27
27
|
|
|
28
28
|
@managed
|
|
29
29
|
gridModel: GridModel;
|
|
30
30
|
|
|
31
|
+
@bindable
|
|
32
|
+
groupBy: 'user' | 'instance' = null;
|
|
33
|
+
|
|
31
34
|
@managed
|
|
32
35
|
private _timer: Timer;
|
|
33
36
|
|
|
@@ -52,75 +55,39 @@ export class WebSocketModel extends BaseInstanceModel {
|
|
|
52
55
|
super();
|
|
53
56
|
makeObservable(this);
|
|
54
57
|
|
|
55
|
-
this.gridModel =
|
|
56
|
-
emptyText: 'No clients connected.',
|
|
57
|
-
enableExport: true,
|
|
58
|
-
exportOptions: {filename: exportFilenameWithDate('ws-connections')},
|
|
59
|
-
selModel: 'multiple',
|
|
60
|
-
contextMenu: [
|
|
61
|
-
this.forceSuspendAction,
|
|
62
|
-
this.reqHealthReportAction,
|
|
63
|
-
'-',
|
|
64
|
-
...GridModel.defaultContextMenu
|
|
65
|
-
],
|
|
66
|
-
store: {
|
|
67
|
-
idSpec: 'key',
|
|
68
|
-
processRawData: row => {
|
|
69
|
-
const authUser = row.authUser.username,
|
|
70
|
-
apparentUser = row.apparentUser.username,
|
|
71
|
-
impersonating = authUser !== apparentUser;
|
|
72
|
-
|
|
73
|
-
return {
|
|
74
|
-
...row,
|
|
75
|
-
authUser,
|
|
76
|
-
apparentUser,
|
|
77
|
-
user: impersonating ? `${authUser} (as ${apparentUser})` : authUser
|
|
78
|
-
};
|
|
79
|
-
},
|
|
80
|
-
fields: [
|
|
81
|
-
{name: 'authUser', type: 'string'},
|
|
82
|
-
{name: 'apparentUser', type: 'string'}
|
|
83
|
-
]
|
|
84
|
-
},
|
|
85
|
-
sortBy: ['key'],
|
|
86
|
-
columns: [
|
|
87
|
-
WSCol.isOpen,
|
|
88
|
-
WSCol.key,
|
|
89
|
-
Col.user,
|
|
90
|
-
WSCol.createdTime,
|
|
91
|
-
WSCol.sentMessageCount,
|
|
92
|
-
WSCol.lastSentTime,
|
|
93
|
-
WSCol.receivedMessageCount,
|
|
94
|
-
WSCol.lastReceivedTime,
|
|
95
|
-
WSCol.appVersion,
|
|
96
|
-
WSCol.appBuild,
|
|
97
|
-
WSCol.loadId,
|
|
98
|
-
WSCol.tabId
|
|
99
|
-
]
|
|
100
|
-
});
|
|
58
|
+
this.gridModel = this.createGridModel();
|
|
101
59
|
|
|
102
60
|
this._timer = Timer.create({
|
|
103
|
-
runFn: () => {
|
|
104
|
-
if (this.isVisible)
|
|
61
|
+
runFn: async () => {
|
|
62
|
+
if (this.isVisible) {
|
|
63
|
+
await this.autoRefreshAsync();
|
|
64
|
+
}
|
|
105
65
|
},
|
|
106
66
|
interval: 5 * SECONDS,
|
|
107
67
|
delay: true
|
|
108
68
|
});
|
|
69
|
+
|
|
70
|
+
this.addReaction({
|
|
71
|
+
track: () => this.groupBy,
|
|
72
|
+
run: () => this.applyGroupBy()
|
|
73
|
+
});
|
|
109
74
|
}
|
|
110
75
|
|
|
111
76
|
override async doLoadAsync(loadSpec: LoadSpec) {
|
|
112
77
|
try {
|
|
113
78
|
const data = await XH.fetchJson({
|
|
114
|
-
url: '
|
|
115
|
-
params: {instance: this.instanceName},
|
|
79
|
+
url: 'clientAdmin/allClients',
|
|
116
80
|
loadSpec
|
|
117
81
|
});
|
|
82
|
+
if (loadSpec.isStale) return;
|
|
83
|
+
|
|
118
84
|
this.gridModel.loadData(data);
|
|
119
85
|
runInAction(() => {
|
|
120
86
|
this.lastRefresh = Date.now();
|
|
121
87
|
});
|
|
122
88
|
} catch (e) {
|
|
123
|
-
|
|
89
|
+
if (loadSpec.isStale) return;
|
|
90
|
+
XH.handleException(e, {alertType: 'toast'});
|
|
124
91
|
}
|
|
125
92
|
}
|
|
126
93
|
|
|
@@ -172,6 +139,76 @@ export class WebSocketModel extends BaseInstanceModel {
|
|
|
172
139
|
//------------------
|
|
173
140
|
// Implementation
|
|
174
141
|
//------------------
|
|
142
|
+
private createGridModel(): GridModel {
|
|
143
|
+
const hidden = true;
|
|
144
|
+
|
|
145
|
+
return new GridModel({
|
|
146
|
+
emptyText: 'No clients connected.',
|
|
147
|
+
groupBy: this.groupBy,
|
|
148
|
+
colChooserModel: true,
|
|
149
|
+
enableExport: true,
|
|
150
|
+
selModel: 'multiple',
|
|
151
|
+
exportOptions: {filename: exportFilenameWithDate('clients')},
|
|
152
|
+
restoreDefaultsFn: async () => this.applyGroupBy(),
|
|
153
|
+
contextMenu: [
|
|
154
|
+
this.forceSuspendAction,
|
|
155
|
+
this.reqHealthReportAction,
|
|
156
|
+
'-',
|
|
157
|
+
...GridModel.defaultContextMenu
|
|
158
|
+
],
|
|
159
|
+
store: {
|
|
160
|
+
idSpec: 'key',
|
|
161
|
+
processRawData: row => {
|
|
162
|
+
const authUser = row.authUser.username,
|
|
163
|
+
apparentUser = row.apparentUser.username,
|
|
164
|
+
impersonating = authUser !== apparentUser;
|
|
165
|
+
|
|
166
|
+
return {
|
|
167
|
+
...row,
|
|
168
|
+
authUser,
|
|
169
|
+
apparentUser,
|
|
170
|
+
user: impersonating ? `${authUser} (as ${apparentUser})` : authUser
|
|
171
|
+
};
|
|
172
|
+
},
|
|
173
|
+
fields: [
|
|
174
|
+
{name: 'authUser', type: 'string'},
|
|
175
|
+
{name: 'apparentUser', type: 'string'}
|
|
176
|
+
]
|
|
177
|
+
},
|
|
178
|
+
sortBy: ['user'],
|
|
179
|
+
columns: [
|
|
180
|
+
WSCol.isOpen,
|
|
181
|
+
Col.user,
|
|
182
|
+
{
|
|
183
|
+
headerName: 'Session',
|
|
184
|
+
headerAlign: 'center',
|
|
185
|
+
children: [
|
|
186
|
+
WSCol.createdTime,
|
|
187
|
+
{...WSCol.key, hidden},
|
|
188
|
+
Col.instance,
|
|
189
|
+
WSCol.loadId,
|
|
190
|
+
WSCol.tabId
|
|
191
|
+
]
|
|
192
|
+
},
|
|
193
|
+
{
|
|
194
|
+
headerName: 'Client App',
|
|
195
|
+
headerAlign: 'center',
|
|
196
|
+
children: [WSCol.appVersion, WSCol.appBuild]
|
|
197
|
+
},
|
|
198
|
+
{
|
|
199
|
+
headerName: 'Send/Receive',
|
|
200
|
+
headerAlign: 'center',
|
|
201
|
+
children: [
|
|
202
|
+
WSCol.sentMessageCount,
|
|
203
|
+
WSCol.lastSentTime,
|
|
204
|
+
WSCol.receivedMessageCount,
|
|
205
|
+
WSCol.lastReceivedTime
|
|
206
|
+
]
|
|
207
|
+
}
|
|
208
|
+
]
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
|
|
175
212
|
private async bulkPush({
|
|
176
213
|
toRecs,
|
|
177
214
|
topic,
|
|
@@ -187,10 +224,10 @@ export class WebSocketModel extends BaseInstanceModel {
|
|
|
187
224
|
|
|
188
225
|
const tasks = toRecs.map(rec =>
|
|
189
226
|
XH.fetchJson({
|
|
190
|
-
url: '
|
|
227
|
+
url: 'clientAdmin/pushToClient',
|
|
191
228
|
params: {
|
|
192
229
|
channelKey: rec.data.key,
|
|
193
|
-
instance:
|
|
230
|
+
instance: rec.data.instance,
|
|
194
231
|
topic,
|
|
195
232
|
message
|
|
196
233
|
}
|
|
@@ -204,4 +241,9 @@ export class WebSocketModel extends BaseInstanceModel {
|
|
|
204
241
|
omit: !trackMessage
|
|
205
242
|
});
|
|
206
243
|
}
|
|
244
|
+
|
|
245
|
+
private applyGroupBy() {
|
|
246
|
+
const {groupBy, gridModel} = this;
|
|
247
|
+
gridModel.setGroupBy(groupBy);
|
|
248
|
+
}
|
|
207
249
|
}
|
package/admin/tabs/{cluster/instances/websocket/WebSocketPanel.ts → client/clients/ClientsPanel.ts}
RENAMED
|
@@ -4,36 +4,48 @@
|
|
|
4
4
|
*
|
|
5
5
|
* Copyright © 2025 Extremely Heavy Industries Inc.
|
|
6
6
|
*/
|
|
7
|
-
import {
|
|
7
|
+
import {errorMessage} from '@xh/hoist/cmp/error';
|
|
8
8
|
import {grid, gridCountLabel} from '@xh/hoist/cmp/grid';
|
|
9
|
-
import {
|
|
9
|
+
import {filler, fragment, p} from '@xh/hoist/cmp/layout';
|
|
10
10
|
import {relativeTimestamp} from '@xh/hoist/cmp/relativetimestamp';
|
|
11
11
|
import {storeFilterField} from '@xh/hoist/cmp/store';
|
|
12
12
|
import {creates, hoistCmp, XH} from '@xh/hoist/core';
|
|
13
|
-
import {exportButton} from '@xh/hoist/desktop/cmp/button';
|
|
14
|
-
import {
|
|
13
|
+
import {colChooserButton, exportButton} from '@xh/hoist/desktop/cmp/button';
|
|
14
|
+
import {select} from '@xh/hoist/desktop/cmp/input';
|
|
15
15
|
import {panel} from '@xh/hoist/desktop/cmp/panel';
|
|
16
16
|
import {recordActionBar} from '@xh/hoist/desktop/cmp/record';
|
|
17
|
-
import {
|
|
17
|
+
import {ClientsModel} from './ClientsModel';
|
|
18
18
|
|
|
19
|
-
export const
|
|
20
|
-
model: creates(
|
|
19
|
+
export const clientsPanel = hoistCmp.factory<ClientsModel>({
|
|
20
|
+
model: creates(ClientsModel),
|
|
21
21
|
|
|
22
22
|
render({model}) {
|
|
23
23
|
if (!XH.webSocketService.enabled) return notPresentMessage();
|
|
24
24
|
|
|
25
25
|
return panel({
|
|
26
|
-
|
|
26
|
+
tbar: [
|
|
27
|
+
select({
|
|
28
|
+
bind: 'groupBy',
|
|
29
|
+
placeholder: 'Ungrouped',
|
|
30
|
+
options: [
|
|
31
|
+
{value: 'user', label: 'By User'},
|
|
32
|
+
{value: 'instance', label: 'By Instance'}
|
|
33
|
+
],
|
|
34
|
+
enableClear: true,
|
|
35
|
+
enableFilter: false
|
|
36
|
+
}),
|
|
37
|
+
'-',
|
|
27
38
|
recordActionBar({
|
|
28
39
|
selModel: model.gridModel.selModel,
|
|
29
40
|
actions: [model.forceSuspendAction, model.reqHealthReportAction]
|
|
30
41
|
}),
|
|
31
42
|
filler(),
|
|
32
|
-
relativeTimestamp({bind: 'lastRefresh'
|
|
33
|
-
|
|
43
|
+
relativeTimestamp({bind: 'lastRefresh'}),
|
|
44
|
+
'-',
|
|
34
45
|
gridCountLabel({unit: 'client'}),
|
|
35
|
-
|
|
46
|
+
'-',
|
|
36
47
|
storeFilterField(),
|
|
48
|
+
colChooserButton(),
|
|
37
49
|
exportButton()
|
|
38
50
|
],
|
|
39
51
|
item: grid(),
|
|
@@ -44,20 +56,14 @@ export const webSocketPanel = hoistCmp.factory<WebSocketModel>({
|
|
|
44
56
|
});
|
|
45
57
|
|
|
46
58
|
const notPresentMessage = hoistCmp.factory(() =>
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
'Please ensure that you have enabled web sockets in your server and client application configuration.'
|
|
57
|
-
)
|
|
58
|
-
)
|
|
59
|
-
}
|
|
60
|
-
})
|
|
61
|
-
]
|
|
59
|
+
errorMessage({
|
|
60
|
+
error: {
|
|
61
|
+
message: fragment(
|
|
62
|
+
p('WebSockets are not enabled in this application.'),
|
|
63
|
+
p(
|
|
64
|
+
'Please ensure that you have enabled WebSockets in your server and client application configuration.'
|
|
65
|
+
)
|
|
66
|
+
)
|
|
67
|
+
}
|
|
62
68
|
})
|
|
63
69
|
);
|