@xh/hoist 67.0.0-SNAPSHOT.1725133204871 → 67.0.0-SNAPSHOT.1725396437331
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 +2 -0
- package/admin/tabs/cluster/ClusterTabModel.ts +12 -7
- package/build/types/svc/EnvironmentService.d.ts +10 -3
- package/build/types/svc/WebSocketService.d.ts +1 -0
- package/package.json +1 -1
- package/svc/EnvironmentService.ts +84 -63
- package/svc/WebSocketService.ts +13 -1
- package/tsconfig.tsbuildinfo +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -37,6 +37,8 @@
|
|
|
37
37
|
|
|
38
38
|
* Fixed Admin Console bug where a role with a dot in its name could not be deleted.
|
|
39
39
|
* Fixed inline `SelectEditor` to ensure new value is flushed before grid editing stops.
|
|
40
|
+
* `WebSocketService` now attempts to establish a new connection when app's server instance changes.
|
|
41
|
+
|
|
40
42
|
|
|
41
43
|
### ✨ Styles
|
|
42
44
|
|
|
@@ -71,12 +71,18 @@ export class ClusterTabModel extends HoistModel {
|
|
|
71
71
|
constructor() {
|
|
72
72
|
super();
|
|
73
73
|
|
|
74
|
-
this.addReaction(
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
74
|
+
this.addReaction(
|
|
75
|
+
{
|
|
76
|
+
track: () => this.instanceName,
|
|
77
|
+
run: instName => {
|
|
78
|
+
if (instName) this.tabModel.refreshContextModel.refreshAsync();
|
|
79
|
+
}
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
track: () => XH.environmentService.serverInstance,
|
|
83
|
+
run: () => this.gridModel.agApi.refreshCells({force: true})
|
|
78
84
|
}
|
|
79
|
-
|
|
85
|
+
);
|
|
80
86
|
}
|
|
81
87
|
|
|
82
88
|
private createGridModel() {
|
|
@@ -86,7 +92,6 @@ export class ClusterTabModel extends HoistModel {
|
|
|
86
92
|
fields: [
|
|
87
93
|
{name: 'name', type: 'string'},
|
|
88
94
|
{name: 'isPrimary', type: 'bool'},
|
|
89
|
-
{name: 'isLocal', type: 'bool'},
|
|
90
95
|
{name: 'isReady', type: 'bool'},
|
|
91
96
|
{name: 'wsConnections', type: 'int'},
|
|
92
97
|
{name: 'startupTime', type: 'date'},
|
|
@@ -173,7 +178,7 @@ export class ClusterTabModel extends HoistModel {
|
|
|
173
178
|
formatInstance(instance: PlainObject): ReactNode {
|
|
174
179
|
const content = [instance.name];
|
|
175
180
|
if (instance.isPrimary) content.push(badge({item: 'primary', intent: 'primary'}));
|
|
176
|
-
if (instance.
|
|
181
|
+
if (instance.name === XH.environmentService.serverInstance) content.push(badge('local'));
|
|
177
182
|
return hbox(content);
|
|
178
183
|
}
|
|
179
184
|
|
|
@@ -1,4 +1,8 @@
|
|
|
1
1
|
import { HoistService } from '@xh/hoist/core';
|
|
2
|
+
/**
|
|
3
|
+
* Load and report on the client and server environment, including software versions, timezones, and
|
|
4
|
+
* and other technical information.
|
|
5
|
+
*/
|
|
2
6
|
export declare class EnvironmentService extends HoistService {
|
|
3
7
|
static instance: EnvironmentService;
|
|
4
8
|
/**
|
|
@@ -16,7 +20,9 @@ export declare class EnvironmentService extends HoistService {
|
|
|
16
20
|
* Unlike most other EnvironmentService state, this is refreshed on a timer and observable.
|
|
17
21
|
*/
|
|
18
22
|
serverInstance: string;
|
|
19
|
-
private
|
|
23
|
+
private data;
|
|
24
|
+
private pollConfig;
|
|
25
|
+
private pollTimer;
|
|
20
26
|
initAsync(): Promise<void>;
|
|
21
27
|
get(key: string): any;
|
|
22
28
|
get appEnvironment(): AppEnvironment;
|
|
@@ -25,9 +31,10 @@ export declare class EnvironmentService extends HoistService {
|
|
|
25
31
|
isMinHoistCoreVersion(version: string): boolean;
|
|
26
32
|
isMaxHoistCoreVersion(version: string): boolean;
|
|
27
33
|
constructor();
|
|
28
|
-
private
|
|
29
|
-
private
|
|
34
|
+
private startPolling;
|
|
35
|
+
private pollServerAsync;
|
|
30
36
|
private setServerInfo;
|
|
37
|
+
private get pollIntervalMs();
|
|
31
38
|
}
|
|
32
39
|
/**
|
|
33
40
|
* Available application deployment environments, as defined by Hoist Core `AppEnvironment.groovy`.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xh/hoist",
|
|
3
|
-
"version": "67.0.0-SNAPSHOT.
|
|
3
|
+
"version": "67.0.0-SNAPSHOT.1725396437331",
|
|
4
4
|
"description": "Hoist add-on for building and deploying React Applications.",
|
|
5
5
|
"repository": "github:xh/hoist-react",
|
|
6
6
|
"homepage": "https://xh.io",
|
|
@@ -7,15 +7,19 @@
|
|
|
7
7
|
import bpPkg from '@blueprintjs/core/package.json';
|
|
8
8
|
import {HoistService, XH} from '@xh/hoist/core';
|
|
9
9
|
import {agGridVersion} from '@xh/hoist/kit/ag-grid';
|
|
10
|
-
import {
|
|
10
|
+
import {action, makeObservable, observable} from '@xh/hoist/mobx';
|
|
11
11
|
import hoistPkg from '@xh/hoist/package.json';
|
|
12
12
|
import {Timer} from '@xh/hoist/utils/async';
|
|
13
13
|
import {MINUTES, SECONDS} from '@xh/hoist/utils/datetime';
|
|
14
14
|
import {checkMaxVersion, checkMinVersion, deepFreeze} from '@xh/hoist/utils/js';
|
|
15
|
-
import {defaults} from 'lodash';
|
|
15
|
+
import {defaults, isFinite} from 'lodash';
|
|
16
16
|
import mobxPkg from 'mobx/package.json';
|
|
17
17
|
import {version as reactVersion} from 'react';
|
|
18
18
|
|
|
19
|
+
/**
|
|
20
|
+
* Load and report on the client and server environment, including software versions, timezones, and
|
|
21
|
+
* and other technical information.
|
|
22
|
+
*/
|
|
19
23
|
export class EnvironmentService extends HoistService {
|
|
20
24
|
static instance: EnvironmentService;
|
|
21
25
|
|
|
@@ -40,47 +44,49 @@ export class EnvironmentService extends HoistService {
|
|
|
40
44
|
@observable
|
|
41
45
|
serverInstance: string;
|
|
42
46
|
|
|
43
|
-
private
|
|
47
|
+
private data = {};
|
|
48
|
+
private pollConfig: PollConfig;
|
|
49
|
+
private pollTimer: Timer;
|
|
44
50
|
|
|
45
51
|
override async initAsync() {
|
|
46
|
-
const serverEnv = await XH.fetchJson({
|
|
52
|
+
const {pollConfig, instanceName, ...serverEnv} = await XH.fetchJson({
|
|
53
|
+
url: 'xh/environment'
|
|
54
|
+
}),
|
|
47
55
|
clientTimeZone = Intl.DateTimeFormat().resolvedOptions().timeZone ?? 'Unknown',
|
|
48
56
|
clientTimeZoneOffset = new Date().getTimezoneOffset() * -1 * MINUTES;
|
|
49
57
|
|
|
50
58
|
// Favor client-side data injected via Webpack build or otherwise determined locally,
|
|
51
59
|
// then apply all other env data sourced from the server.
|
|
52
|
-
this.
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
60
|
+
this.data = deepFreeze(
|
|
61
|
+
defaults(
|
|
62
|
+
{
|
|
63
|
+
appCode: XH.appCode,
|
|
64
|
+
appName: XH.appName,
|
|
65
|
+
clientVersion: XH.appVersion,
|
|
66
|
+
clientBuild: XH.appBuild,
|
|
67
|
+
reactVersion,
|
|
68
|
+
hoistReactVersion: hoistPkg.version,
|
|
69
|
+
agGridVersion,
|
|
70
|
+
mobxVersion: mobxPkg.version,
|
|
71
|
+
blueprintCoreVersion: bpPkg.version,
|
|
72
|
+
clientTimeZone,
|
|
73
|
+
clientTimeZoneOffset
|
|
74
|
+
},
|
|
75
|
+
serverEnv
|
|
76
|
+
)
|
|
67
77
|
);
|
|
68
78
|
|
|
69
|
-
|
|
70
|
-
delete this._data['instanceName'];
|
|
71
|
-
|
|
72
|
-
deepFreeze(this._data);
|
|
73
|
-
|
|
74
|
-
this.setServerInfo(serverEnv.instanceName, serverEnv.appVersion, serverEnv.appBuild);
|
|
79
|
+
this.setServerInfo(instanceName, serverEnv.appVersion, serverEnv.appBuild);
|
|
75
80
|
|
|
81
|
+
this.pollConfig = pollConfig;
|
|
76
82
|
this.addReaction({
|
|
77
83
|
when: () => XH.appIsRunning,
|
|
78
|
-
run: this.
|
|
84
|
+
run: this.startPolling
|
|
79
85
|
});
|
|
80
86
|
}
|
|
81
87
|
|
|
82
88
|
get(key: string): any {
|
|
83
|
-
return this.
|
|
89
|
+
return this.data[key];
|
|
84
90
|
}
|
|
85
91
|
|
|
86
92
|
get appEnvironment(): AppEnvironment {
|
|
@@ -111,54 +117,64 @@ export class EnvironmentService extends HoistService {
|
|
|
111
117
|
makeObservable(this);
|
|
112
118
|
}
|
|
113
119
|
|
|
114
|
-
private
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
120
|
+
private startPolling() {
|
|
121
|
+
this.pollTimer = Timer.create({
|
|
122
|
+
runFn: () => this.pollServerAsync(),
|
|
123
|
+
interval: this.pollIntervalMs,
|
|
124
|
+
delay: true
|
|
119
125
|
});
|
|
120
126
|
}
|
|
121
127
|
|
|
122
|
-
private
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
// `mode` set in `xhAppVersionCheck`. Builds are checked here to trigger refresh prompts
|
|
130
|
-
// across SNAPSHOT updates for projects with active dev/QA users.
|
|
131
|
-
if (appVersion !== this.get('appVersion') || appBuild !== this.get('appBuild')) {
|
|
132
|
-
if (mode === 'promptReload') {
|
|
133
|
-
XH.appContainerModel.showUpdateBanner(appVersion, appBuild);
|
|
134
|
-
} else if (mode === 'forceReload') {
|
|
135
|
-
XH.suspendApp({
|
|
136
|
-
reason: 'APP_UPDATE',
|
|
137
|
-
message: `A new version of ${XH.clientAppName} is now available (${appVersion}) and requires an immediate update.`
|
|
138
|
-
});
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
// Note that the case of version mismatches across the client and server we do *not* show
|
|
143
|
-
// the update bar to the user - that would indicate a deployment issue that a client reload
|
|
144
|
-
// is unlikely to resolve, leaving the user in a frustrating state where they are endlessly
|
|
145
|
-
// prompted to refresh.
|
|
146
|
-
const clientVersion = this.get('clientVersion');
|
|
147
|
-
if (appVersion !== clientVersion) {
|
|
148
|
-
this.logWarn(
|
|
149
|
-
`Version mismatch detected between client and server - ${clientVersion} vs ${appVersion}`
|
|
150
|
-
);
|
|
128
|
+
private async pollServerAsync() {
|
|
129
|
+
let data;
|
|
130
|
+
try {
|
|
131
|
+
data = await XH.fetchJson({url: 'xh/environmentPoll'});
|
|
132
|
+
} catch (e) {
|
|
133
|
+
this.logError('Error polling server environment', e);
|
|
134
|
+
return;
|
|
151
135
|
}
|
|
152
136
|
|
|
137
|
+
// Update config/interval, and server info
|
|
138
|
+
const {pollConfig, instanceName, appVersion, appBuild} = data;
|
|
139
|
+
this.pollConfig = pollConfig;
|
|
140
|
+
this.pollTimer.setInterval(this.pollIntervalMs);
|
|
153
141
|
this.setServerInfo(instanceName, appVersion, appBuild);
|
|
154
|
-
|
|
142
|
+
|
|
143
|
+
// Handle version change
|
|
144
|
+
if (appVersion != XH.getEnv('appVersion') || appBuild != XH.getEnv('appBuild')) {
|
|
145
|
+
// force the user to refresh or prompt the user to refresh via the banner according to config
|
|
146
|
+
// build checked to trigger refresh across SNAPSHOT updates in lower environments
|
|
147
|
+
const {onVersionChange} = pollConfig;
|
|
148
|
+
switch (onVersionChange) {
|
|
149
|
+
case 'promptReload':
|
|
150
|
+
XH.appContainerModel.showUpdateBanner(appVersion, appBuild);
|
|
151
|
+
return;
|
|
152
|
+
case 'forceReload':
|
|
153
|
+
XH.suspendApp({
|
|
154
|
+
reason: 'APP_UPDATE',
|
|
155
|
+
message: `A new version of ${XH.clientAppName} is now available (${appVersion}) and requires an immediate update.`
|
|
156
|
+
});
|
|
157
|
+
return;
|
|
158
|
+
default:
|
|
159
|
+
this.logWarn(
|
|
160
|
+
`New version ${appVersion} reported by server, onVersionChange is ${onVersionChange} - ignoring.`
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
155
165
|
|
|
156
166
|
@action
|
|
157
|
-
private setServerInfo(serverInstance, serverVersion, serverBuild) {
|
|
167
|
+
private setServerInfo(serverInstance: string, serverVersion: string, serverBuild: string) {
|
|
158
168
|
this.serverInstance = serverInstance;
|
|
159
169
|
this.serverVersion = serverVersion;
|
|
160
170
|
this.serverBuild = serverBuild;
|
|
161
171
|
}
|
|
172
|
+
|
|
173
|
+
private get pollIntervalMs(): number {
|
|
174
|
+
// Throttle to 5secs, disable if set to 0 or less.
|
|
175
|
+
const {interval} = this.pollConfig;
|
|
176
|
+
return isFinite(interval) && interval > 0 ? Math.max(interval, 5) * SECONDS : -1;
|
|
177
|
+
}
|
|
162
178
|
}
|
|
163
179
|
|
|
164
180
|
/**
|
|
@@ -175,3 +191,8 @@ export type AppEnvironment =
|
|
|
175
191
|
| 'Test'
|
|
176
192
|
| 'UAT'
|
|
177
193
|
| 'BCP';
|
|
194
|
+
|
|
195
|
+
interface PollConfig {
|
|
196
|
+
interval: number;
|
|
197
|
+
onVersionChange: 'forceReload' | 'promptReload' | 'silent';
|
|
198
|
+
}
|
package/svc/WebSocketService.ts
CHANGED
|
@@ -71,7 +71,8 @@ export class WebSocketService extends HoistService {
|
|
|
71
71
|
|
|
72
72
|
override async initAsync() {
|
|
73
73
|
if (!this.enabled) return;
|
|
74
|
-
|
|
74
|
+
const {environmentService} = XH;
|
|
75
|
+
if (environmentService.get('webSocketsEnabled') === false) {
|
|
75
76
|
this.logError(
|
|
76
77
|
`WebSockets enabled on this client app but disabled on server. Adjust your server-side config.`
|
|
77
78
|
);
|
|
@@ -81,6 +82,11 @@ export class WebSocketService extends HoistService {
|
|
|
81
82
|
|
|
82
83
|
this.connect();
|
|
83
84
|
|
|
85
|
+
this.addReaction({
|
|
86
|
+
track: () => environmentService.serverInstance,
|
|
87
|
+
run: () => this.onServerInstanceChange()
|
|
88
|
+
});
|
|
89
|
+
|
|
84
90
|
this._timer = Timer.create({
|
|
85
91
|
runFn: () => this.heartbeatOrReconnect(),
|
|
86
92
|
interval: 10 * SECONDS,
|
|
@@ -175,6 +181,12 @@ export class WebSocketService extends HoistService {
|
|
|
175
181
|
}
|
|
176
182
|
}
|
|
177
183
|
|
|
184
|
+
private onServerInstanceChange() {
|
|
185
|
+
this.logWarn('Server instance changed - attempting to connect to new instance.');
|
|
186
|
+
this.disconnect();
|
|
187
|
+
this.connect();
|
|
188
|
+
}
|
|
189
|
+
|
|
178
190
|
shutdown() {
|
|
179
191
|
if (this._timer) this._timer.cancel();
|
|
180
192
|
this.disconnect();
|