@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.
Files changed (39) hide show
  1. package/CHANGELOG.md +15 -5
  2. package/admin/AppModel.ts +19 -12
  3. package/admin/columns/Core.ts +29 -0
  4. package/admin/columns/Tracking.ts +3 -23
  5. package/admin/tabs/BaseAdminTabModel.ts +17 -0
  6. package/admin/tabs/activity/{ActivityTab.scss → tracking/ActivityTracking.scss} +0 -8
  7. package/admin/tabs/activity/tracking/ActivityTrackingPanel.ts +1 -0
  8. package/admin/tabs/activity/tracking/detail/ActivityDetailView.ts +5 -1
  9. package/admin/tabs/{activity/ActivityTab.ts → client/ClientTab.ts} +7 -8
  10. package/admin/tabs/{cluster/instances/websocket/WebSocketColumns.ts → client/clients/ClientsColumns.ts} +12 -6
  11. package/admin/tabs/{cluster/instances/websocket/WebSocketModel.ts → client/clients/ClientsModel.ts} +99 -57
  12. package/admin/tabs/{cluster/instances/websocket/WebSocketPanel.ts → client/clients/ClientsPanel.ts} +32 -26
  13. package/admin/tabs/{activity/clienterrors → client/errors}/ClientErrorDetail.ts +9 -5
  14. package/admin/tabs/client/errors/ClientErrors.scss +52 -0
  15. package/admin/tabs/{activity/clienterrors → client/errors}/ClientErrorsPanel.ts +2 -1
  16. package/admin/tabs/cluster/instances/BaseInstanceModel.ts +3 -10
  17. package/admin/tabs/cluster/instances/InstancesTabModel.ts +1 -3
  18. package/admin/tabs/monitor/MonitorTabModel.ts +4 -3
  19. package/admin/tabs/userData/roles/RoleModel.ts +1 -0
  20. package/admin/tabs/userData/roles/RolePanel.ts +4 -2
  21. package/build/types/admin/columns/Core.d.ts +3 -0
  22. package/build/types/admin/tabs/BaseAdminTabModel.d.ts +6 -0
  23. package/build/types/admin/tabs/activity/tracking/ActivityTrackingPanel.d.ts +1 -0
  24. package/build/types/admin/tabs/client/ClientTab.d.ts +1 -0
  25. package/build/types/admin/tabs/{cluster/instances/websocket/WebSocketModel.d.ts → client/clients/ClientsModel.d.ts} +5 -2
  26. package/build/types/admin/tabs/client/clients/ClientsPanel.d.ts +2 -0
  27. package/build/types/admin/tabs/{activity/clienterrors → client/errors}/ClientErrorsPanel.d.ts +1 -0
  28. package/build/types/admin/tabs/cluster/instances/BaseInstanceModel.d.ts +3 -5
  29. package/build/types/admin/tabs/monitor/MonitorTabModel.d.ts +3 -2
  30. package/package.json +1 -1
  31. package/tsconfig.tsbuildinfo +1 -1
  32. package/build/types/admin/tabs/activity/ActivityTab.d.ts +0 -2
  33. package/build/types/admin/tabs/cluster/instances/websocket/WebSocketPanel.d.ts +0 -2
  34. /package/admin/tabs/{activity/clienterrors → client/errors}/ClientErrorsModel.ts +0 -0
  35. /package/admin/tabs/{activity → client}/feedback/FeedbackPanel.ts +0 -0
  36. /package/build/types/admin/tabs/{cluster/instances/websocket/WebSocketColumns.d.ts → client/clients/ClientsColumns.d.ts} +0 -0
  37. /package/build/types/admin/tabs/{activity/clienterrors → client/errors}/ClientErrorDetail.d.ts +0 -0
  38. /package/build/types/admin/tabs/{activity/clienterrors → client/errors}/ClientErrorsModel.d.ts +0 -0
  39. /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
- * The two versions *should* be the same, but in cases where a browser "restores" a tab and
20
- re-inits an app without reloading the code itself, the upgrade check would miss the fact that
21
- the client remained on an older version.
22
- * Note that a misconfigured build - where the client build version is not set to the same value as
23
- the server - would result in a false positive for an upgrade. The two should always match.
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 {activityTab} from './tabs/activity/ActivityTab';
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: activityTab
150
+ content: activityTrackingPanel
144
151
  }
145
152
  ];
146
153
  }
@@ -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 copy from 'clipboard-copy';
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: 370
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 './ActivityTab.scss';
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 {activityTrackingPanel} from './tracking/ActivityTrackingPanel';
12
+ import {clientsPanel} from './clients/ClientsPanel';
14
13
 
15
- export const activityTab = hoistCmp.factory(() =>
14
+ export const clientTab = hoistCmp.factory(() =>
16
15
  tabContainer({
17
16
  modelConfig: {
18
- route: 'default.activity',
19
- switcher: {orientation: 'left', testId: 'activity-tab-switcher'},
17
+ route: 'default.clients',
18
+ switcher: {orientation: 'left', testId: 'client-tab-switcher'},
20
19
  tabs: [
21
- {id: 'tracking', icon: Icon.analytics(), content: activityTrackingPanel},
22
- {id: 'clientErrors', icon: Icon.warning(), content: clientErrorsPanel},
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 {Icon} from '@xh/hoist/icon';
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: {name: 'key', type: 'string'},
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
- type: 'string',
80
- displayName: 'Client Version'
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
- width: 120
103
+ ...badgeCol
98
104
  };
99
105
  export const tabId: ColumnSpec = {
100
106
  field: {
101
107
  name: 'tabId',
102
108
  type: 'string'
103
109
  },
104
- width: 120
110
+ ...badgeCol
105
111
  };
@@ -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 './WebSocketColumns';
21
+ import * as WSCol from './ClientsColumns';
22
+ import {BaseAdminTabModel} from '@xh/hoist/admin/tabs/BaseAdminTabModel';
23
23
 
24
- export class WebSocketModel extends BaseInstanceModel {
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 = new 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) this.autoRefreshAsync();
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: 'webSocketAdmin/allChannels',
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
- this.handleLoadException(e, loadSpec);
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: 'webSocketAdmin/pushToChannel',
227
+ url: 'clientAdmin/pushToClient',
191
228
  params: {
192
229
  channelKey: rec.data.key,
193
- instance: this.instanceName,
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
  }
@@ -4,36 +4,48 @@
4
4
  *
5
5
  * Copyright © 2025 Extremely Heavy Industries Inc.
6
6
  */
7
- import {WebSocketModel} from '@xh/hoist/admin/tabs/cluster/instances/websocket/WebSocketModel';
7
+ import {errorMessage} from '@xh/hoist/cmp/error';
8
8
  import {grid, gridCountLabel} from '@xh/hoist/cmp/grid';
9
- import {box, filler, fragment, p} from '@xh/hoist/cmp/layout';
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 {errorMessage} from '@xh/hoist/cmp/error';
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 {toolbarSep} from '@xh/hoist/desktop/cmp/toolbar';
17
+ import {ClientsModel} from './ClientsModel';
18
18
 
19
- export const webSocketPanel = hoistCmp.factory<WebSocketModel>({
20
- model: creates(WebSocketModel),
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
- bbar: [
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', options: {prefix: 'Refreshed'}}),
33
- toolbarSep(),
43
+ relativeTimestamp({bind: 'lastRefresh'}),
44
+ '-',
34
45
  gridCountLabel({unit: 'client'}),
35
- toolbarSep(),
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
- box({
48
- height: 200,
49
- width: 1000,
50
- items: [
51
- errorMessage({
52
- error: {
53
- message: fragment(
54
- p('WebSockets are not enabled in this application.'),
55
- p(
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
  );