@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 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
- ### 🎁 New Features
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
- ### ⚙️ Technical
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
- * Added a workaround for a bug where Panel drag resizing was broken in Safari.
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)
@@ -85,6 +85,7 @@ export class AppStateModel extends HoistModel {
85
85
  XH.track({
86
86
  category: 'App',
87
87
  message: `Loaded ${XH.clientAppCode}`,
88
+ timestamp: loadStarted,
88
89
  elapsed: Date.now() - loadStarted - (timings.LOGIN_REQUIRED ?? 0),
89
90
  data: {
90
91
  appVersion: XH.appVersion,
@@ -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 _oncePerSessionSent;
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 doTrackAsync;
16
+ private pushPendingAsync;
17
+ private pushPendingBuffered;
18
+ private toServerJson;
19
+ private logMessage;
15
20
  }
@@ -234,6 +234,9 @@ export interface TrackOptions {
234
234
  /** Optional LoadSpec associated with this track.*/
235
235
  loadSpec?: LoadSpec;
236
236
 
237
+ /** Timestamp for action. */
238
+ timestamp?: number;
239
+
237
240
  /** Elapsed time (ms) for action. */
238
241
  elapsed?: number;
239
242
 
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.1728678076307",
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",
@@ -198,8 +198,11 @@ const enhancePromise = promisePrototype => {
198
198
 
199
199
  const startTime = Date.now();
200
200
  return this.finally(() => {
201
- options.elapsed = Date.now() - startTime;
202
- XH.track(options);
201
+ XH.track({
202
+ timestamp: startTime,
203
+ elapsed: Date.now() - startTime,
204
+ ...options
205
+ });
203
206
  });
204
207
  },
205
208
 
@@ -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, debounce, forEach, isEmpty, isEqual, size} from 'lodash';
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
- if (!isEmpty(updates)) {
146
- await XH.fetchJson({
147
- url: 'xh/setPrefs',
148
- params: {
149
- updates: JSON.stringify(updates),
150
- clientUsername: XH.getUsername()
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',
@@ -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 _oncePerSessionSent = new Map();
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.message = withDefault(options.message, (options as any).msg);
44
- options.severity = withDefault(options.severity, 'INFO');
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._oncePerSessionSent.set(key, true);
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 doTrackAsync(options: TrackOptions) {
80
- try {
81
- const query: any = {
82
- msg: stripTags(options.message),
83
- clientUsername: XH.getUsername(),
84
- appVersion: XH.getEnv('clientVersion'),
85
- url: window.location.href
86
- };
87
-
88
- if (options.category) query.category = options.category;
89
- if (options.correlationId) query.correlationId = options.correlationId;
90
- if (options.data) query.data = options.data;
91
- if (options.severity) query.severity = options.severity;
92
- if (options.logData !== undefined) query.logData = options.logData;
93
- if (options.elapsed !== undefined) query.elapsed = options.elapsed;
94
-
95
- const {maxDataLength} = this.conf;
96
- if (query.data?.length > maxDataLength) {
97
- this.logWarn(
98
- `Track log includes ${query.data.length} chars of JSON data`,
99
- `exceeds limit of ${maxDataLength}`,
100
- 'data will not be persisted',
101
- options.data
102
- );
103
- query.data = null;
104
- }
105
-
106
- const elapsedStr = query.elapsed != null ? `${query.elapsed}ms` : null,
107
- consoleMsgs = [query.category, query.msg, query.correlationId, elapsedStr].filter(
108
- it => !isNil(it)
109
- );
110
-
111
- this.logInfo(...consoleMsgs);
112
-
113
- await XH.fetchService.postJson({
114
- url: 'xh/track',
115
- body: query,
116
- // Post clientUsername as a parameter to ensure client username matches session.
117
- params: {clientUsername: query.clientUsername}
118
- });
119
- } catch (e) {
120
- this.logError('Failed to persist track log', options, e);
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
  }