@xh/hoist 69.0.0-SNAPSHOT.1728678076307 → 69.0.0-SNAPSHOT.1728741604616
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 +16 -11
- package/appcontainer/AppStateModel.ts +1 -0
- package/build/types/core/types/Interfaces.d.ts +2 -0
- package/build/types/icon/Icon.d.ts +1 -0
- package/build/types/svc/PrefService.d.ts +1 -2
- package/build/types/svc/TrackService.d.ts +7 -2
- package/core/types/Interfaces.ts +3 -0
- package/icon/Icon.ts +5 -0
- package/icon/index.ts +8 -0
- package/package.json +1 -1
- package/promise/Promise.ts +5 -2
- package/svc/PrefService.ts +15 -20
- package/svc/TrackService.ts +83 -59
- package/tsconfig.tsbuildinfo +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -3,27 +3,32 @@
|
|
|
3
3
|
## 69.0.0-SNAPSHOT - unreleased
|
|
4
4
|
|
|
5
5
|
### 💥 Breaking Changes (upgrade difficulty: 🟢 LOW )
|
|
6
|
-
* The `INITIALIZING` AppState has been replaced with more fine-grained states (see below). This
|
|
7
|
-
is not expected to affect any applications.
|
|
8
6
|
|
|
9
|
-
|
|
7
|
+
* Requires `hoist-core >= 23.1` to support bulk upload of activity tracking logs to server.
|
|
8
|
+
* `AppState.INITIALIZING` replaced with finer-grained states (not expected to impact most apps).
|
|
10
9
|
|
|
10
|
+
### 🎁 New Features
|
|
11
11
|
* Added new AppStates `AUTHENTICATING`, `INITIALIZING_HOIST`, and `INITIALIZING_APP` to support
|
|
12
12
|
more granular tracking and timing of app startup lifecycle.
|
|
13
13
|
* Improved the default "Loaded App" activity tracking entry with more granular data on load timing.
|
|
14
14
|
* `RestGrid` now displays an optional refresh button in its toolbar.
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
* Improvements to the typing of `HoistBase.addReaction`. Note that applications may need to adjust
|
|
19
|
-
typescript slightly in calls to this method to conform to the tighter typing.
|
|
15
|
+
* Enhanced tracking data posted with the built-in "Loaded App" entry to include a new `timings`
|
|
16
|
+
block that breaks down the overall initial load time into more discrete phases. Supported by
|
|
17
|
+
new `AppState` enums `AUTHENTICATING`, `INITIALIZING_HOIST`, and `INITIALIZING_APP`.
|
|
20
18
|
* The filter field in the top toolbar of Grid's Column Filter Values tab now filters with `any`,
|
|
21
19
|
instead of `startsWith`.
|
|
22
20
|
|
|
23
|
-
### 🐞 Bug Fixes
|
|
24
21
|
|
|
25
|
-
|
|
22
|
+
### ⚙️ Typescript API Adjustments
|
|
23
|
+
* Improved typing of `HoistBase.addReaction` to flow types returned by the `track` closure through
|
|
24
|
+
to the `run` closure that receives them.
|
|
25
|
+
* Note that apps might need to adjust their reaction signatures slightly to accommodate the more
|
|
26
|
+
accurate typing, specifically if they are tracking an array of values, destructuring those
|
|
27
|
+
values in their `run` closure, and passing them on to typed APIs. Look out for `tsc` warnings.
|
|
28
|
+
|
|
29
|
+
### 🐞 Bug Fixes
|
|
26
30
|
|
|
31
|
+
* Fixed broken `Panel` resizing in Safari. (Other browsers were not affected.)
|
|
27
32
|
|
|
28
33
|
## 68.1.0 - 2024-09-27
|
|
29
34
|
|
|
@@ -33,6 +38,7 @@ typescript slightly in calls to this method to conform to the tighter typing.
|
|
|
33
38
|
props to the underlying `reactMarkdown` instance.
|
|
34
39
|
|
|
35
40
|
### ⚙️ Technical
|
|
41
|
+
|
|
36
42
|
* Misc. Improvements to Cluster Tab in Admin Panel.
|
|
37
43
|
|
|
38
44
|
## 68.0.0 - 2024-09-18
|
|
@@ -61,7 +67,6 @@ typescript slightly in calls to this method to conform to the tighter typing.
|
|
|
61
67
|
* mobx `6.9.1 -> 6.13.2`,
|
|
62
68
|
* mobx-react-lite `3.4.3 -> 4.0.7`,
|
|
63
69
|
|
|
64
|
-
|
|
65
70
|
## 67.0.0 - 2024-09-03
|
|
66
71
|
|
|
67
72
|
### 💥 Breaking Changes (upgrade difficulty: 🟢 LOW - Hoist Core update only)
|
|
@@ -186,6 +186,8 @@ export interface TrackOptions {
|
|
|
186
186
|
oncePerSession?: boolean;
|
|
187
187
|
/** Optional LoadSpec associated with this track.*/
|
|
188
188
|
loadSpec?: LoadSpec;
|
|
189
|
+
/** Timestamp for action. */
|
|
190
|
+
timestamp?: number;
|
|
189
191
|
/** Elapsed time (ms) for action. */
|
|
190
192
|
elapsed?: number;
|
|
191
193
|
/** Optional flag to omit sending message. */
|
|
@@ -135,6 +135,7 @@ export declare const Icon: {
|
|
|
135
135
|
filePowerpoint(p?: IconProps): any;
|
|
136
136
|
fileText(p?: IconProps): any;
|
|
137
137
|
fileWord(p?: IconProps): any;
|
|
138
|
+
fileXml(p?: IconProps): any;
|
|
138
139
|
flag(p?: IconProps): any;
|
|
139
140
|
floppyDisk(p?: IconProps): any;
|
|
140
141
|
folder(p?: IconProps): any;
|
|
@@ -19,8 +19,6 @@ export declare class PrefService extends HoistService {
|
|
|
19
19
|
static instance: PrefService;
|
|
20
20
|
private _data;
|
|
21
21
|
private _updates;
|
|
22
|
-
private readonly pushPendingBuffered;
|
|
23
|
-
constructor();
|
|
24
22
|
initAsync(): Promise<void>;
|
|
25
23
|
/**
|
|
26
24
|
* Check to see if a given preference has been *defined*.
|
|
@@ -69,6 +67,7 @@ export declare class PrefService extends HoistService {
|
|
|
69
67
|
* changes and option and then immediately hits a (browser) refresh.
|
|
70
68
|
*/
|
|
71
69
|
pushPendingAsync(): Promise<void>;
|
|
70
|
+
private pushPendingBuffered;
|
|
72
71
|
private loadPrefsAsync;
|
|
73
72
|
private migrateLocalPrefsAsync;
|
|
74
73
|
private validateBeforeSet;
|
|
@@ -6,10 +6,15 @@ import { HoistService, TrackOptions } from '@xh/hoist/core';
|
|
|
6
6
|
*/
|
|
7
7
|
export declare class TrackService extends HoistService {
|
|
8
8
|
static instance: TrackService;
|
|
9
|
-
private
|
|
9
|
+
private oncePerSessionSent;
|
|
10
|
+
private pending;
|
|
11
|
+
initAsync(): Promise<void>;
|
|
10
12
|
get conf(): any;
|
|
11
13
|
get enabled(): boolean;
|
|
12
14
|
/** Track User Activity. */
|
|
13
15
|
track(options: TrackOptions | string): void;
|
|
14
|
-
private
|
|
16
|
+
private pushPendingAsync;
|
|
17
|
+
private pushPendingBuffered;
|
|
18
|
+
private toServerJson;
|
|
19
|
+
private logMessage;
|
|
15
20
|
}
|
package/core/types/Interfaces.ts
CHANGED
package/icon/Icon.ts
CHANGED
|
@@ -375,6 +375,9 @@ export const Icon = {
|
|
|
375
375
|
fileWord(p?: IconProps) {
|
|
376
376
|
return Icon.icon({...p, iconName: 'file-word'});
|
|
377
377
|
},
|
|
378
|
+
fileXml(p?: IconProps) {
|
|
379
|
+
return Icon.icon({...p, iconName: 'file-xml'});
|
|
380
|
+
},
|
|
378
381
|
flag(p?: IconProps) {
|
|
379
382
|
return Icon.icon({...p, iconName: 'flag'});
|
|
380
383
|
},
|
|
@@ -981,6 +984,8 @@ function getFileIconConfig(filename: string) {
|
|
|
981
984
|
return {factory: Icon.filePdf, className: 'xh-file-icon-pdf'};
|
|
982
985
|
case 'txt':
|
|
983
986
|
return {factory: Icon.fileText};
|
|
987
|
+
case 'xml':
|
|
988
|
+
return {factory: Icon.fileXml};
|
|
984
989
|
case 'zip':
|
|
985
990
|
return {factory: Icon.fileArchive};
|
|
986
991
|
default:
|
package/icon/index.ts
CHANGED
|
@@ -110,6 +110,7 @@ import {
|
|
|
110
110
|
faFilePdf as faFilePdfLight,
|
|
111
111
|
faFilePowerpoint as faFilePowerpointLight,
|
|
112
112
|
faFileWord as faFileWordLight,
|
|
113
|
+
faFileXml as faFileXmlLight,
|
|
113
114
|
faFilter as faFilterLight,
|
|
114
115
|
faFilterSlash as faFilterSlashLight,
|
|
115
116
|
faFlag as faFlagLight,
|
|
@@ -321,6 +322,7 @@ import {
|
|
|
321
322
|
faFilePdf,
|
|
322
323
|
faFilePowerpoint,
|
|
323
324
|
faFileWord,
|
|
325
|
+
faFileXml,
|
|
324
326
|
faFilter,
|
|
325
327
|
faFilterSlash,
|
|
326
328
|
faFlag,
|
|
@@ -532,6 +534,7 @@ import {
|
|
|
532
534
|
faFilePdf as faFilePdfSolid,
|
|
533
535
|
faFilePowerpoint as faFilePowerpointSolid,
|
|
534
536
|
faFileWord as faFileWordSolid,
|
|
537
|
+
faFileXml as faFileXmlSolid,
|
|
535
538
|
faFilter as faFilterSolid,
|
|
536
539
|
faFilterSlash as faFilterSlashSolid,
|
|
537
540
|
faFlag as faFlagSolid,
|
|
@@ -744,6 +747,7 @@ import {
|
|
|
744
747
|
faFilePdf as faFilePdfThin,
|
|
745
748
|
faFilePowerpoint as faFilePowerpointThin,
|
|
746
749
|
faFileWord as faFileWordThin,
|
|
750
|
+
faFileXml as faFileXmlThin,
|
|
747
751
|
faFilter as faFilterThin,
|
|
748
752
|
faFilterSlash as faFilterSlashThin,
|
|
749
753
|
faFlag as faFlagThin,
|
|
@@ -1260,6 +1264,10 @@ library.add(
|
|
|
1260
1264
|
faFileWordLight,
|
|
1261
1265
|
faFileWordSolid,
|
|
1262
1266
|
faFileWordThin,
|
|
1267
|
+
faFileXml,
|
|
1268
|
+
faFileXmlLight,
|
|
1269
|
+
faFileXmlSolid,
|
|
1270
|
+
faFileXmlThin,
|
|
1263
1271
|
faFilter,
|
|
1264
1272
|
faFilterLight,
|
|
1265
1273
|
faFilterSolid,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xh/hoist",
|
|
3
|
-
"version": "69.0.0-SNAPSHOT.
|
|
3
|
+
"version": "69.0.0-SNAPSHOT.1728741604616",
|
|
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",
|
package/promise/Promise.ts
CHANGED
|
@@ -198,8 +198,11 @@ const enhancePromise = promisePrototype => {
|
|
|
198
198
|
|
|
199
199
|
const startTime = Date.now();
|
|
200
200
|
return this.finally(() => {
|
|
201
|
-
|
|
202
|
-
|
|
201
|
+
XH.track({
|
|
202
|
+
timestamp: startTime,
|
|
203
|
+
elapsed: Date.now() - startTime,
|
|
204
|
+
...options
|
|
205
|
+
});
|
|
203
206
|
});
|
|
204
207
|
},
|
|
205
208
|
|
package/svc/PrefService.ts
CHANGED
|
@@ -6,8 +6,8 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import {HoistService, XH} from '@xh/hoist/core';
|
|
8
8
|
import {SECONDS} from '@xh/hoist/utils/datetime';
|
|
9
|
-
import {deepFreeze, throwIf} from '@xh/hoist/utils/js';
|
|
10
|
-
import {cloneDeep,
|
|
9
|
+
import {debounced, deepFreeze, throwIf} from '@xh/hoist/utils/js';
|
|
10
|
+
import {cloneDeep, forEach, isEmpty, isEqual, size} from 'lodash';
|
|
11
11
|
|
|
12
12
|
/**
|
|
13
13
|
* Service to read and set user-specific preference values.
|
|
@@ -30,16 +30,9 @@ export class PrefService extends HoistService {
|
|
|
30
30
|
|
|
31
31
|
private _data = {};
|
|
32
32
|
private _updates = {};
|
|
33
|
-
private readonly pushPendingBuffered: any;
|
|
34
|
-
|
|
35
|
-
constructor() {
|
|
36
|
-
super();
|
|
37
|
-
const pushFn = () => this.pushPendingAsync();
|
|
38
|
-
window.addEventListener('beforeunload', pushFn);
|
|
39
|
-
this.pushPendingBuffered = debounce(pushFn, 5 * SECONDS);
|
|
40
|
-
}
|
|
41
33
|
|
|
42
34
|
override async initAsync() {
|
|
35
|
+
window.addEventListener('beforeunload', () => this.pushPendingAsync());
|
|
43
36
|
await this.migrateLocalPrefsAsync();
|
|
44
37
|
return this.loadPrefsAsync();
|
|
45
38
|
}
|
|
@@ -139,23 +132,25 @@ export class PrefService extends HoistService {
|
|
|
139
132
|
|
|
140
133
|
if (isEmpty(updates)) return;
|
|
141
134
|
|
|
142
|
-
// clear obj state immediately to allow picking up next batch during async operation
|
|
143
135
|
this._updates = {};
|
|
144
136
|
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
});
|
|
153
|
-
}
|
|
137
|
+
await XH.postJson({
|
|
138
|
+
url: 'xh/setPrefs',
|
|
139
|
+
body: updates,
|
|
140
|
+
params: {
|
|
141
|
+
clientUsername: XH.getUsername()
|
|
142
|
+
}
|
|
143
|
+
});
|
|
154
144
|
}
|
|
155
145
|
|
|
156
146
|
//-------------------
|
|
157
147
|
// Implementation
|
|
158
148
|
//-------------------
|
|
149
|
+
@debounced(5 * SECONDS)
|
|
150
|
+
private pushPendingBuffered() {
|
|
151
|
+
this.pushPendingAsync();
|
|
152
|
+
}
|
|
153
|
+
|
|
159
154
|
private async loadPrefsAsync() {
|
|
160
155
|
const data = await XH.fetchJson({
|
|
161
156
|
url: 'xh/getPrefs',
|
package/svc/TrackService.ts
CHANGED
|
@@ -4,10 +4,11 @@
|
|
|
4
4
|
*
|
|
5
5
|
* Copyright © 2024 Extremely Heavy Industries Inc.
|
|
6
6
|
*/
|
|
7
|
-
import {HoistService, TrackOptions, XH} from '@xh/hoist/core';
|
|
7
|
+
import {HoistService, PlainObject, TrackOptions, XH} from '@xh/hoist/core';
|
|
8
|
+
import {SECONDS} from '@xh/hoist/utils/datetime';
|
|
8
9
|
import {isOmitted} from '@xh/hoist/utils/impl';
|
|
9
|
-
import {stripTags, withDefault} from '@xh/hoist/utils/js';
|
|
10
|
-
import {isNil, isString} from 'lodash';
|
|
10
|
+
import {debounced, stripTags, withDefault} from '@xh/hoist/utils/js';
|
|
11
|
+
import {isEmpty, isNil, isString} from 'lodash';
|
|
11
12
|
|
|
12
13
|
/**
|
|
13
14
|
* Primary service for tracking any activity that an application's admins want to track.
|
|
@@ -17,7 +18,12 @@ import {isNil, isString} from 'lodash';
|
|
|
17
18
|
export class TrackService extends HoistService {
|
|
18
19
|
static instance: TrackService;
|
|
19
20
|
|
|
20
|
-
private
|
|
21
|
+
private oncePerSessionSent = new Map();
|
|
22
|
+
private pending: PlainObject[] = [];
|
|
23
|
+
|
|
24
|
+
override async initAsync() {
|
|
25
|
+
window.addEventListener('beforeunload', () => this.pushPendingAsync());
|
|
26
|
+
}
|
|
21
27
|
|
|
22
28
|
get conf() {
|
|
23
29
|
return XH.getConf('xhActivityTrackingConfig', {
|
|
@@ -40,8 +46,12 @@ export class TrackService extends HoistService {
|
|
|
40
46
|
// Normalize string form, msg -> message, default severity.
|
|
41
47
|
if (isString(options)) options = {message: options};
|
|
42
48
|
if (isOmitted(options)) return;
|
|
43
|
-
options
|
|
44
|
-
|
|
49
|
+
options = {
|
|
50
|
+
message: withDefault(options.message, (options as any).msg),
|
|
51
|
+
severity: withDefault(options.severity, 'INFO'),
|
|
52
|
+
timestamp: withDefault(options.timestamp, Date.now()),
|
|
53
|
+
...options
|
|
54
|
+
};
|
|
45
55
|
|
|
46
56
|
// Short-circuit if disabled...
|
|
47
57
|
if (!this.enabled) {
|
|
@@ -49,75 +59,89 @@ export class TrackService extends HoistService {
|
|
|
49
59
|
return;
|
|
50
60
|
}
|
|
51
61
|
|
|
52
|
-
// ...or invalid request (with warning for developer)
|
|
62
|
+
// ...or invalid request (with warning for developer)
|
|
53
63
|
if (!options.message) {
|
|
54
64
|
this.logWarn('Required message not provided - activity will not be tracked.', options);
|
|
55
65
|
return;
|
|
56
66
|
}
|
|
57
67
|
|
|
58
|
-
// ...or if auto-refresh
|
|
68
|
+
// ...or if auto-refresh
|
|
59
69
|
if (options.loadSpec?.isAutoRefresh) return;
|
|
60
70
|
|
|
61
|
-
// ...or if unauthenticated user
|
|
71
|
+
// ...or if unauthenticated user
|
|
62
72
|
if (!XH.getUsername()) return;
|
|
63
73
|
|
|
64
|
-
// ...or if already-sent once-per-session messages
|
|
65
|
-
const key = options.message + '_' + (options.category ?? '');
|
|
66
|
-
if (options.oncePerSession && this._oncePerSessionSent.has(key)) return;
|
|
67
|
-
|
|
68
|
-
// Otherwise - fire off (but do not await) request.
|
|
69
|
-
this.doTrackAsync(options);
|
|
70
|
-
|
|
74
|
+
// ...or if already-sent once-per-session messages
|
|
71
75
|
if (options.oncePerSession) {
|
|
72
|
-
this.
|
|
76
|
+
const sent = this.oncePerSessionSent,
|
|
77
|
+
key = options.message + '_' + (options.category ?? '');
|
|
78
|
+
if (sent.has(key)) return;
|
|
79
|
+
sent.set(key, true);
|
|
73
80
|
}
|
|
81
|
+
|
|
82
|
+
// Otherwise - log and for next batch,
|
|
83
|
+
this.logMessage(options);
|
|
84
|
+
|
|
85
|
+
this.pending.push(this.toServerJson(options));
|
|
86
|
+
this.pushPendingBuffered();
|
|
74
87
|
}
|
|
75
88
|
|
|
76
89
|
//------------------
|
|
77
90
|
// Implementation
|
|
78
91
|
//------------------
|
|
79
|
-
private async
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
92
|
+
private async pushPendingAsync() {
|
|
93
|
+
const {pending} = this;
|
|
94
|
+
if (isEmpty(pending)) return;
|
|
95
|
+
|
|
96
|
+
this.pending = [];
|
|
97
|
+
await XH.fetchService.postJson({
|
|
98
|
+
url: 'xh/track',
|
|
99
|
+
body: {entries: pending},
|
|
100
|
+
params: {clientUsername: XH.getUsername()}
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
@debounced(10 * SECONDS)
|
|
105
|
+
private pushPendingBuffered() {
|
|
106
|
+
this.pushPendingAsync();
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
private toServerJson(options: TrackOptions): PlainObject {
|
|
110
|
+
const ret: PlainObject = {
|
|
111
|
+
msg: stripTags(options.message),
|
|
112
|
+
clientUsername: XH.getUsername(),
|
|
113
|
+
appVersion: XH.getEnv('clientVersion'),
|
|
114
|
+
url: window.location.href,
|
|
115
|
+
timestamp: Date.now()
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
if (options.category) ret.category = options.category;
|
|
119
|
+
if (options.correlationId) ret.correlationId = options.correlationId;
|
|
120
|
+
if (options.data) ret.data = options.data;
|
|
121
|
+
if (options.severity) ret.severity = options.severity;
|
|
122
|
+
if (options.logData !== undefined) ret.logData = options.logData;
|
|
123
|
+
if (options.elapsed !== undefined) ret.elapsed = options.elapsed;
|
|
124
|
+
|
|
125
|
+
const {maxDataLength} = this.conf;
|
|
126
|
+
if (ret.data?.length > maxDataLength) {
|
|
127
|
+
this.logWarn(
|
|
128
|
+
`Track log includes ${ret.data.length} chars of JSON data`,
|
|
129
|
+
`exceeds limit of ${maxDataLength}`,
|
|
130
|
+
'data will not be persisted',
|
|
131
|
+
options.data
|
|
132
|
+
);
|
|
133
|
+
ret.data = null;
|
|
121
134
|
}
|
|
135
|
+
|
|
136
|
+
return ret;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
private logMessage(opts: TrackOptions) {
|
|
140
|
+
const elapsedStr = opts.elapsed != null ? `${opts.elapsed}ms` : null,
|
|
141
|
+
consoleMsgs = [opts.category, opts.message, opts.correlationId, elapsedStr].filter(
|
|
142
|
+
it => !isNil(it)
|
|
143
|
+
);
|
|
144
|
+
|
|
145
|
+
this.logInfo(...consoleMsgs);
|
|
122
146
|
}
|
|
123
147
|
}
|