@xh/hoist 73.0.0-SNAPSHOT.1744145524716 → 73.0.0-SNAPSHOT.1744147015222

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 (32) hide show
  1. package/CHANGELOG.md +3 -5
  2. package/admin/AdminUtils.ts +18 -1
  3. package/admin/tabs/activity/tracking/detail/ActivityDetailModel.ts +3 -3
  4. package/admin/tabs/cluster/instances/BaseInstanceModel.ts +1 -29
  5. package/admin/tabs/cluster/instances/services/DetailsPanel.ts +3 -1
  6. package/admin/tabs/cluster/objects/DetailModel.ts +5 -40
  7. package/admin/tabs/cluster/objects/DetailPanel.ts +3 -1
  8. package/appcontainer/AppContainerModel.ts +2 -0
  9. package/appcontainer/AppStateModel.ts +1 -2
  10. package/build/types/admin/AdminUtils.d.ts +4 -0
  11. package/build/types/admin/tabs/cluster/instances/BaseInstanceModel.d.ts +1 -3
  12. package/build/types/admin/tabs/cluster/objects/DetailModel.d.ts +1 -3
  13. package/build/types/cmp/viewmanager/ViewManagerModel.d.ts +0 -7
  14. package/build/types/core/XH.d.ts +2 -1
  15. package/build/types/format/FormatMisc.d.ts +3 -2
  16. package/build/types/svc/ClientHealthService.d.ts +80 -0
  17. package/build/types/svc/TrackService.d.ts +0 -12
  18. package/build/types/svc/index.d.ts +1 -0
  19. package/build/types/utils/js/index.d.ts +0 -1
  20. package/cmp/viewmanager/ViewManagerModel.ts +1 -10
  21. package/core/XH.ts +3 -1
  22. package/desktop/cmp/viewmanager/ViewMenu.ts +8 -10
  23. package/format/FormatMisc.ts +6 -4
  24. package/package.json +1 -1
  25. package/security/msal/MsalClient.ts +2 -6
  26. package/svc/ClientHealthService.ts +219 -0
  27. package/svc/TrackService.ts +3 -67
  28. package/svc/index.ts +1 -0
  29. package/tsconfig.tsbuildinfo +1 -1
  30. package/utils/js/index.ts +0 -1
  31. package/build/types/utils/js/BrowserUtils.d.ts +0 -41
  32. package/utils/js/BrowserUtils.ts +0 -103
package/CHANGELOG.md CHANGED
@@ -2,10 +2,8 @@
2
2
 
3
3
  ## v73.0.0-SNAPSHOT - unreleased
4
4
 
5
- ### 🎁 New Features
6
-
7
- `ViewManagerConfig` takes new optional key `customViewMenuItem` to allow ViewManagers implementations to customize
8
- the menu items for views in the view manager menu.
5
+ ### ⚙️ Technical
6
+ * Added enhanced `ClientHealthService` for managing client health report.
9
7
 
10
8
  ## v72.3.0 - 2025-04-08
11
9
 
@@ -13,7 +11,7 @@
13
11
 
14
12
  * Added support for posting a "Client Health Report" track message on a configurable interval. This
15
13
  message will include basic client information, and can be extended to include any other desired
16
- data via `XH.trackService.addClientHealthReportSource()`. Enable by updating your app's
14
+ data via `XH.clientHealthService.addSource()`. Enable by updating your app's
17
15
  `xhActivityTrackingConfig` to include `clientHealthReport: {intervalMins: XXXX}`.
18
16
  * Enabled opt-in support for telemetry in `MsalClient`, leveraging hooks built-in to MSAL to collect
19
17
  timing and success/failure count for all events emitted by the library.
@@ -5,7 +5,9 @@
5
5
  * Copyright © 2025 Extremely Heavy Industries Inc.
6
6
  */
7
7
  import {XH} from '@xh/hoist/core';
8
- import {LocalDate} from '@xh/hoist/utils/datetime';
8
+ import {DAYS, LocalDate} from '@xh/hoist/utils/datetime';
9
+ import {isNumber} from 'lodash';
10
+ import {fmtDateTimeSec} from '@xh/hoist/format';
9
11
 
10
12
  /**
11
13
  * Generate a standardized filename for an Admin module grid export, without datestamp.
@@ -21,3 +23,18 @@ export function exportFilename(moduleName: string): string {
21
23
  export function exportFilenameWithDate(moduleName: string): () => string {
22
24
  return () => `${XH.appCode}-${moduleName}-${LocalDate.today()}`;
23
25
  }
26
+
27
+ /**
28
+ * Replacer for JSON.stringify to replace timestamp properties with formatted strings.
29
+ */
30
+ export function timestampReplacer(k: string, v: any) {
31
+ if (
32
+ (k.endsWith('Time') || k.endsWith('Date') || k.endsWith('Timestamp') || k == 'timestamp') &&
33
+ isNumber(v) &&
34
+ v > Date.now() - 25 * 365 * DAYS // heuristic to avoid catching smaller ms ranges
35
+ ) {
36
+ return fmtDateTimeSec(v, {fmt: 'MMM DD HH:mm:ss.SSS'});
37
+ }
38
+
39
+ return v;
40
+ }
@@ -4,14 +4,14 @@
4
4
  *
5
5
  * Copyright © 2025 Extremely Heavy Industries Inc.
6
6
  */
7
- import {exportFilename} from '@xh/hoist/admin/AdminUtils';
7
+ import {exportFilename, timestampReplacer} from '@xh/hoist/admin/AdminUtils';
8
8
  import * as Col from '@xh/hoist/admin/columns';
9
9
  import {FormModel} from '@xh/hoist/cmp/form';
10
10
  import {GridModel} from '@xh/hoist/cmp/grid';
11
11
  import {HoistModel, lookup, managed} from '@xh/hoist/core';
12
- import {fmtJson} from '@xh/hoist/format';
13
12
  import {action, computed, makeObservable, observable} from '@xh/hoist/mobx';
14
13
  import {ActivityTrackingModel} from '../ActivityTrackingModel';
14
+ import {fmtJson} from '@xh/hoist/format';
15
15
 
16
16
  export class ActivityDetailModel extends HoistModel {
17
17
  @lookup(ActivityTrackingModel) activityTrackingModel: ActivityTrackingModel;
@@ -118,7 +118,7 @@ export class ActivityDetailModel extends HoistModel {
118
118
  let formattedTrackData = trackData;
119
119
  if (formattedTrackData) {
120
120
  try {
121
- formattedTrackData = fmtJson(trackData);
121
+ formattedTrackData = fmtJson(trackData, {replacer: timestampReplacer});
122
122
  } catch (ignored) {}
123
123
  }
124
124
 
@@ -5,10 +5,7 @@
5
5
  * Copyright © 2025 Extremely Heavy Industries Inc.
6
6
  */
7
7
  import {InstancesTabModel} from '@xh/hoist/admin/tabs/cluster/instances/InstancesTabModel';
8
- import {HoistModel, LoadSpec, lookup, PlainObject, XH} from '@xh/hoist/core';
9
- import {fmtDateTimeSec, fmtJson} from '@xh/hoist/format';
10
- import {DAYS} from '@xh/hoist/utils/datetime';
11
- import {cloneDeep, forOwn, isArray, isNumber, isPlainObject} from 'lodash';
8
+ import {HoistModel, LoadSpec, lookup, XH} from '@xh/hoist/core';
12
9
  import {createRef} from 'react';
13
10
  import {isDisplayed} from '@xh/hoist/utils/js';
14
11
 
@@ -21,12 +18,6 @@ export class BaseInstanceModel extends HoistModel {
21
18
  return this.parent.instanceName;
22
19
  }
23
20
 
24
- fmtStats(stats: PlainObject): string {
25
- stats = cloneDeep(stats);
26
- this.processTimestamps(stats);
27
- return fmtJson(JSON.stringify(stats));
28
- }
29
-
30
21
  handleLoadException(e: unknown, loadSpec: LoadSpec) {
31
22
  const instanceNotFound = this.isInstanceNotFound(e),
32
23
  connDown = this.parent.lastLoadException,
@@ -49,23 +40,4 @@ export class BaseInstanceModel extends HoistModel {
49
40
  private isInstanceNotFound(e: unknown): boolean {
50
41
  return e['name'] == 'InstanceNotFoundException';
51
42
  }
52
-
53
- private processTimestamps(stats: PlainObject) {
54
- forOwn(stats, (v, k) => {
55
- // Convert numbers that look like recent timestamps to date values.
56
- if (
57
- (k.endsWith('Time') ||
58
- k.endsWith('Date') ||
59
- k.endsWith('Timestamp') ||
60
- k == 'timestamp') &&
61
- isNumber(v) &&
62
- v > Date.now() - 365 * DAYS
63
- ) {
64
- stats[k] = v ? fmtDateTimeSec(v, {fmt: 'MMM DD HH:mm:ss.SSS'}) : null;
65
- }
66
- if (isPlainObject(v) || isArray(v)) {
67
- this.processTimestamps(v);
68
- }
69
- });
70
- }
71
43
  }
@@ -12,6 +12,8 @@ import {panel} from '@xh/hoist/desktop/cmp/panel';
12
12
  import {jsonInput} from '@xh/hoist/desktop/cmp/input';
13
13
  import {Icon} from '@xh/hoist/icon';
14
14
  import {isEmpty} from 'lodash';
15
+ import {fmtJson} from '@xh/hoist/format';
16
+ import {timestampReplacer} from '@xh/hoist/admin/AdminUtils';
15
17
 
16
18
  export const detailsPanel = hoistCmp.factory({
17
19
  model: creates(DetailsModel),
@@ -57,7 +59,7 @@ const stats = hoistCmp.factory<DetailsModel>({
57
59
  enableSearch: true,
58
60
  showFullscreenButton: false,
59
61
  editorProps: {lineNumbers: false},
60
- value: model.parent.fmtStats(stats)
62
+ value: fmtJson(stats, {replacer: timestampReplacer})
61
63
  })
62
64
  );
63
65
  }
@@ -6,21 +6,11 @@
6
6
  */
7
7
  import {ClusterObjectsModel} from '@xh/hoist/admin/tabs/cluster/objects/ClusterObjectsModel';
8
8
  import {ColumnSpec, GridModel} from '@xh/hoist/cmp/grid';
9
- import {HoistModel, lookup, managed, PlainObject, XH} from '@xh/hoist/core';
9
+ import {HoistModel, lookup, managed, XH} from '@xh/hoist/core';
10
10
  import {StoreRecord} from '@xh/hoist/data';
11
- import {fmtDateTimeSec, fmtJson} from '@xh/hoist/format';
12
11
  import {action, makeObservable, observable} from '@xh/hoist/mobx';
13
- import {DAYS} from '@xh/hoist/utils/datetime';
14
- import {
15
- cloneDeep,
16
- forOwn,
17
- isArray,
18
- isEmpty,
19
- isEqual,
20
- isNumber,
21
- isPlainObject,
22
- without
23
- } from 'lodash';
12
+ import {isEmpty, isEqual, without} from 'lodash';
13
+ import {timestampReplacer} from '@xh/hoist/admin/AdminUtils';
24
14
 
25
15
  export class DetailModel extends HoistModel {
26
16
  @lookup(ClusterObjectsModel)
@@ -66,12 +56,6 @@ export class DetailModel extends HoistModel {
66
56
  });
67
57
  }
68
58
 
69
- fmtStats(stats: PlainObject): string {
70
- stats = cloneDeep(stats);
71
- this.processTimestamps(stats);
72
- return fmtJson(JSON.stringify(stats));
73
- }
74
-
75
59
  //----------------------
76
60
  // Implementation
77
61
  //----------------------
@@ -95,8 +79,8 @@ export class DetailModel extends HoistModel {
95
79
  const gridModel = this.createGridModel(diffFields, otherFields);
96
80
  gridModel.loadData(
97
81
  instanceNames.map(instanceName => {
98
- const data = cloneDeep(adminStatsByInstance[instanceName] ?? {});
99
- this.processTimestamps(data);
82
+ let data = adminStatsByInstance[instanceName] ?? {};
83
+ data = JSON.parse(JSON.stringify(data, timestampReplacer));
100
84
  return {instanceName, ...data};
101
85
  })
102
86
  );
@@ -136,23 +120,4 @@ export class DetailModel extends HoistModel {
136
120
  }
137
121
  return ret;
138
122
  }
139
-
140
- private processTimestamps(stats: PlainObject) {
141
- forOwn(stats, (v, k) => {
142
- // Convert numbers that look like recent timestamps to date values.
143
- if (
144
- (k.endsWith('Time') ||
145
- k.endsWith('Date') ||
146
- k.endsWith('Timestamp') ||
147
- k == 'timestamp') &&
148
- isNumber(v) &&
149
- v > Date.now() - 365 * DAYS
150
- ) {
151
- stats[k] = v ? fmtDateTimeSec(v, {fmt: 'MMM DD HH:mm:ss.SSS'}) : null;
152
- }
153
- if (isPlainObject(v) || isArray(v)) {
154
- this.processTimestamps(v);
155
- }
156
- });
157
- }
158
123
  }
@@ -12,6 +12,8 @@ import {panel} from '@xh/hoist/desktop/cmp/panel';
12
12
  import {Icon} from '@xh/hoist/icon';
13
13
  import {DetailModel} from './DetailModel';
14
14
  import './ClusterObjects.scss';
15
+ import {timestampReplacer} from '@xh/hoist/admin/AdminUtils';
16
+ import {fmtJson} from '@xh/hoist/format';
15
17
 
16
18
  export const detailPanel = hoistCmp.factory({
17
19
  model: creates(DetailModel),
@@ -42,7 +44,7 @@ export const detailPanel = hoistCmp.factory({
42
44
  height: '100%',
43
45
  showFullscreenButton: false,
44
46
  editorProps: {lineNumbers: false},
45
- value: model.fmtStats(selectedAdminStats)
47
+ value: fmtJson(selectedAdminStats, {replacer: timestampReplacer})
46
48
  })
47
49
  })
48
50
  ]
@@ -25,6 +25,7 @@ import {
25
25
  AlertBannerService,
26
26
  AutoRefreshService,
27
27
  ChangelogService,
28
+ ClientHealthService,
28
29
  ConfigService,
29
30
  EnvironmentService,
30
31
  FetchService,
@@ -237,6 +238,7 @@ export class AppContainerModel extends HoistModel {
237
238
  AlertBannerService,
238
239
  AutoRefreshService,
239
240
  ChangelogService,
241
+ ClientHealthService,
240
242
  IdleService,
241
243
  InspectorService,
242
244
  GridAutosizeService,
@@ -7,7 +7,6 @@
7
7
  import {AppState, AppSuspendData, HoistModel, XH} from '@xh/hoist/core';
8
8
  import {action, makeObservable, observable} from '@xh/hoist/mobx';
9
9
  import {Timer} from '@xh/hoist/utils/async';
10
- import {getClientDeviceInfo} from '@xh/hoist/utils/js';
11
10
  import {camelCase, isBoolean, isString, mapKeys} from 'lodash';
12
11
 
13
12
  /**
@@ -97,7 +96,7 @@ export class AppStateModel extends HoistModel {
97
96
  appBuild: XH.appBuild,
98
97
  locationHref: window.location.href,
99
98
  timings: mapKeys(timings, (v, k) => camelCase(k)),
100
- ...getClientDeviceInfo()
99
+ clientHealth: XH.clientHealthService.getReport()
101
100
  },
102
101
  logData: ['appVersion', 'appBuild'],
103
102
  omit: !XH.appSpec.trackAppLoad
@@ -7,3 +7,7 @@ export declare function exportFilename(moduleName: string): string;
7
7
  * Returned as a closure to ensure current date is evaluated at export time.
8
8
  */
9
9
  export declare function exportFilenameWithDate(moduleName: string): () => string;
10
+ /**
11
+ * Replacer for JSON.stringify to replace timestamp properties with formatted strings.
12
+ */
13
+ export declare function timestampReplacer(k: string, v: any): any;
@@ -1,13 +1,11 @@
1
1
  /// <reference types="react" />
2
2
  import { InstancesTabModel } from '@xh/hoist/admin/tabs/cluster/instances/InstancesTabModel';
3
- import { HoistModel, LoadSpec, PlainObject } from '@xh/hoist/core';
3
+ import { HoistModel, LoadSpec } from '@xh/hoist/core';
4
4
  export declare class BaseInstanceModel extends HoistModel {
5
5
  viewRef: import("react").RefObject<HTMLElement>;
6
6
  parent: InstancesTabModel;
7
7
  get instanceName(): string;
8
- fmtStats(stats: PlainObject): string;
9
8
  handleLoadException(e: unknown, loadSpec: LoadSpec): void;
10
9
  get isVisible(): boolean;
11
10
  private isInstanceNotFound;
12
- private processTimestamps;
13
11
  }
@@ -1,6 +1,6 @@
1
1
  import { ClusterObjectsModel } from '@xh/hoist/admin/tabs/cluster/objects/ClusterObjectsModel';
2
2
  import { GridModel } from '@xh/hoist/cmp/grid';
3
- import { HoistModel, PlainObject } from '@xh/hoist/core';
3
+ import { HoistModel } from '@xh/hoist/core';
4
4
  import { StoreRecord } from '@xh/hoist/data';
5
5
  export declare class DetailModel extends HoistModel {
6
6
  parent: ClusterObjectsModel;
@@ -11,9 +11,7 @@ export declare class DetailModel extends HoistModel {
11
11
  get instanceName(): string;
12
12
  get selectedAdminStats(): any;
13
13
  constructor();
14
- fmtStats(stats: PlainObject): string;
15
14
  private updateGridModel;
16
15
  private createGridModel;
17
16
  private createColSpec;
18
- private processTimestamps;
19
17
  }
@@ -1,6 +1,5 @@
1
1
  import { HoistModel, LoadSpec, PlainObject, TaskObserver, Thunkable } from '@xh/hoist/core';
2
2
  import type { ViewManagerProvider } from '@xh/hoist/core';
3
- import { ReactNode } from 'react';
4
3
  import { ViewInfo } from './ViewInfo';
5
4
  import { View } from './View';
6
5
  export interface ViewCreateSpec {
@@ -86,11 +85,6 @@ export interface ViewManagerConfig {
86
85
  * Optional user-facing display name for describing global views. Defaults to 'global'
87
86
  */
88
87
  globalDisplayName?: string;
89
- /**
90
- * Optional key to pass a method that returns customized BlueprintJS `menuItem` for listing
91
- * views in the ViewManager menu.
92
- */
93
- customViewMenuItem?: (view: ViewInfo, model: ViewManagerModel) => ReactNode;
94
88
  }
95
89
  /**
96
90
  * ViewManagerModel coordinates the loading, saving, and management of user-defined bundles of
@@ -127,7 +121,6 @@ export declare class ViewManagerModel<T = PlainObject> extends HoistModel {
127
121
  readonly instance: string;
128
122
  readonly typeDisplayName: string;
129
123
  readonly globalDisplayName: string;
130
- readonly customViewMenuItem: (view: ViewInfo, model: ViewManagerModel) => ReactNode;
131
124
  readonly enableAutoSave: boolean;
132
125
  readonly enableDefault: boolean;
133
126
  readonly enableGlobal: boolean;
@@ -1,7 +1,7 @@
1
1
  import { RouterModel } from '@xh/hoist/appcontainer/RouterModel';
2
2
  import { HoistAuthModel } from '@xh/hoist/core/HoistAuthModel';
3
3
  import { Store } from '@xh/hoist/data';
4
- import { AlertBannerService, AutoRefreshService, ChangelogService, ConfigService, EnvironmentService, FetchOptions, FetchService, GridAutosizeService, GridExportService, IdentityService, IdleService, InspectorService, JsonBlobService, LocalStorageService, PrefService, SessionStorageService, TrackService, WebSocketService } from '@xh/hoist/svc';
4
+ import { AlertBannerService, AutoRefreshService, ChangelogService, ConfigService, EnvironmentService, FetchOptions, FetchService, GridAutosizeService, GridExportService, IdentityService, IdleService, InspectorService, JsonBlobService, LocalStorageService, PrefService, SessionStorageService, TrackService, WebSocketService, ClientHealthService } from '@xh/hoist/svc';
5
5
  import { Router, State } from 'router5';
6
6
  import { CancelFn } from 'router5/types/types/base';
7
7
  import { SetOptional } from 'type-fest';
@@ -47,6 +47,7 @@ export declare class XHApi {
47
47
  alertBannerService: AlertBannerService;
48
48
  autoRefreshService: AutoRefreshService;
49
49
  changelogService: ChangelogService;
50
+ clientHealthService: ClientHealthService;
50
51
  configService: ConfigService;
51
52
  environmentService: EnvironmentService;
52
53
  fetchService: FetchService;
@@ -1,4 +1,5 @@
1
1
  import { CSSProperties, ReactNode } from 'react';
2
+ import { PlainObject } from '@xh/hoist/core';
2
3
  export interface FormatOptions<T = any> {
3
4
  /** Display value for null values. */
4
5
  nullDisplay?: ReactNode;
@@ -32,9 +33,9 @@ export interface JSONFormatOptions {
32
33
  space?: number | string;
33
34
  }
34
35
  /**
35
- * Pretty-print a JSON string, adding line breaks and indentation.
36
+ * Pretty-print a JSON string or (JSON Object), adding line breaks and indentation.
36
37
  */
37
- export declare function fmtJson(str: string, opts?: JSONFormatOptions): string;
38
+ export declare function fmtJson(v: string | PlainObject, opts?: JSONFormatOptions): string;
38
39
  /**
39
40
  * Basic util for splitting a string (via ' ') and capitalizing each word - e.g. for names.
40
41
  * Not intended to handle more advanced usages such as HTML or other word boundary characters.
@@ -0,0 +1,80 @@
1
+ import { HoistService, PlainObject } from '@xh/hoist/core';
2
+ /**
3
+ * Service for gathering data about client health.
4
+ *
5
+ * Hoist sends this data once on application load, and can be configured to send
6
+ * it at regularly scheduled intervals. Configure via soft-config property
7
+ * 'xhActivityTracking.clientHealthReport'.
8
+ */
9
+ export declare class ClientHealthService extends HoistService {
10
+ static instance: ClientHealthService;
11
+ private sources;
12
+ initAsync(): Promise<void>;
13
+ /**
14
+ * Main Entry report. Return a default report of client health.
15
+ */
16
+ getReport(): ClientHealthReport;
17
+ /**
18
+ * Register a new source for client health report data. No-op if background health report is
19
+ * not generally enabled via `xhActivityTrackingConfig.clientHealthReport.intervalMins`.
20
+ *
21
+ * @param key - key under which to report the data - can be used to remove this source later.
22
+ * @param callback - function returning serializable to include with each report.
23
+ */
24
+ addSource(key: string, callback: () => any): void;
25
+ /** Unregister a previously-enabled source for client health report data. */
26
+ removeSource(key: string): void;
27
+ getSession(): SessionData;
28
+ getConnection(): ConnectionData;
29
+ getMemory(): MemoryData;
30
+ getScreen(): ScreenData;
31
+ getWindow(): WindowData;
32
+ getCustom(): PlainObject;
33
+ private sendReport;
34
+ }
35
+ export interface SessionData {
36
+ startTime: number;
37
+ durationMins: number;
38
+ }
39
+ export interface ConnectionData {
40
+ downlink: number;
41
+ effectiveType: string;
42
+ rtt: number;
43
+ }
44
+ export interface MemoryData {
45
+ modelCount: number;
46
+ usedPctLimit?: number;
47
+ jsHeapSizeLimit?: number;
48
+ totalJSHeapSize?: number;
49
+ usedJSHeapSize?: number;
50
+ }
51
+ export interface WindowData {
52
+ devicePixelRatio: number;
53
+ screenX: number;
54
+ screenY: number;
55
+ innerWidth: number;
56
+ innerHeight: number;
57
+ outerWidth: number;
58
+ outerHeight: number;
59
+ }
60
+ export interface ScreenData {
61
+ availWidth: number;
62
+ availHeight: number;
63
+ width: number;
64
+ height: number;
65
+ colorDepth: number;
66
+ pixelDepth: number;
67
+ availLeft: number;
68
+ availTop: number;
69
+ orientation?: {
70
+ angle: number;
71
+ type: string;
72
+ };
73
+ }
74
+ export interface ClientHealthReport {
75
+ session: SessionData;
76
+ connection: ConnectionData;
77
+ memory: MemoryData;
78
+ window: WindowData;
79
+ screen: ScreenData;
80
+ }
@@ -6,7 +6,6 @@ import { HoistService, TrackOptions } from '@xh/hoist/core';
6
6
  */
7
7
  export declare class TrackService extends HoistService {
8
8
  static instance: TrackService;
9
- private clientHealthReportSources;
10
9
  private oncePerSessionSent;
11
10
  private pending;
12
11
  initAsync(): Promise<void>;
@@ -14,21 +13,10 @@ export declare class TrackService extends HoistService {
14
13
  get enabled(): boolean;
15
14
  /** Track User Activity. */
16
15
  track(options: TrackOptions | string): void;
17
- /**
18
- * Register a new source for client health report data. No-op if background health report is
19
- * not generally enabled via `xhActivityTrackingConfig.clientHealthReport.intervalMins`.
20
- *
21
- * @param key - key under which to report the data - can be used to remove this source later.
22
- * @param callback - function returning serializable to include with each report.
23
- */
24
- addClientHealthReportSource(key: string, callback: () => any): void;
25
- /** Unregister a previously-enabled source for client health report data. */
26
- removeClientHealthReportSource(key: string): void;
27
16
  private pushPendingAsync;
28
17
  private pushPendingBuffered;
29
18
  private toServerJson;
30
19
  private logMessage;
31
- private sendClientHealthReport;
32
20
  }
33
21
  interface ActivityTrackingConfig {
34
22
  clientHealthReport?: Partial<TrackOptions> & {
@@ -13,5 +13,6 @@ export * from './JsonBlobService';
13
13
  export * from './PrefService';
14
14
  export * from './TrackService';
15
15
  export * from './WebSocketService';
16
+ export * from './ClientHealthService';
16
17
  export * from './storage/LocalStorageService';
17
18
  export * from './storage/SessionStorageService';
@@ -3,6 +3,5 @@ export * from './LangUtils';
3
3
  export * from './Decorators';
4
4
  export * from './LogUtils';
5
5
  export * from './DomUtils';
6
- export * from './BrowserUtils';
7
6
  export * from './TestUtils';
8
7
  export * from './VersionUtils';
@@ -123,12 +123,6 @@ export interface ViewManagerConfig {
123
123
  * Optional user-facing display name for describing global views. Defaults to 'global'
124
124
  */
125
125
  globalDisplayName?: string;
126
-
127
- /**
128
- * Optional key to pass a method that returns customized BlueprintJS `menuItem` for listing
129
- * views in the ViewManager menu.
130
- */
131
- customViewMenuItem?: (view: ViewInfo, model: ViewManagerModel) => ReactNode;
132
126
  }
133
127
 
134
128
  /**
@@ -171,7 +165,6 @@ export class ViewManagerModel<T = PlainObject> extends HoistModel {
171
165
  readonly instance: string;
172
166
  readonly typeDisplayName: string;
173
167
  readonly globalDisplayName: string;
174
- readonly customViewMenuItem: (view: ViewInfo, model: ViewManagerModel) => ReactNode;
175
168
  readonly enableAutoSave: boolean;
176
169
  readonly enableDefault: boolean;
177
170
  readonly enableGlobal: boolean;
@@ -290,7 +283,6 @@ export class ViewManagerModel<T = PlainObject> extends HoistModel {
290
283
  instance = 'default',
291
284
  typeDisplayName,
292
285
  globalDisplayName = 'global',
293
- customViewMenuItem,
294
286
  manageGlobal = false,
295
287
  enableAutoSave = true,
296
288
  enableDefault = true,
@@ -304,14 +296,13 @@ export class ViewManagerModel<T = PlainObject> extends HoistModel {
304
296
 
305
297
  throwIf(
306
298
  !enableDefault && !initialViewSpec,
307
- "ViewManagerModel requires 'initialViewSpec' if 'enableDefault' is false."
299
+ "ViewManagerModel requires 'initialViewSpec' if `enableDefault` is false."
308
300
  );
309
301
 
310
302
  this.type = type;
311
303
  this.instance = instance;
312
304
  this.typeDisplayName = lowerCase(typeDisplayName ?? genDisplayName(type));
313
305
  this.globalDisplayName = globalDisplayName;
314
- this.customViewMenuItem = customViewMenuItem;
315
306
  this.manageGlobal = executeIfFunction(manageGlobal) ?? false;
316
307
  this.enableDefault = enableDefault;
317
308
  this.enableGlobal = enableGlobal;
package/core/XH.ts CHANGED
@@ -28,7 +28,8 @@ import {
28
28
  PrefService,
29
29
  SessionStorageService,
30
30
  TrackService,
31
- WebSocketService
31
+ WebSocketService,
32
+ ClientHealthService
32
33
  } from '@xh/hoist/svc';
33
34
  import {camelCase, flatten, isString, uniqueId} from 'lodash';
34
35
  import {Router, State} from 'router5';
@@ -131,6 +132,7 @@ export class XHApi {
131
132
  alertBannerService: AlertBannerService;
132
133
  autoRefreshService: AutoRefreshService;
133
134
  changelogService: ChangelogService;
135
+ clientHealthService: ClientHealthService;
134
136
  configService: ConfigService;
135
137
  environmentService: EnvironmentService;
136
138
  fetchService: FetchService;
@@ -162,14 +162,12 @@ function viewMenuItem(view: ViewInfo, model: ViewManagerModel): ReactNode {
162
162
  if (!view.isOwned && view.owner) title.push(view.owner);
163
163
  if (view.description) title.push(view.description);
164
164
 
165
- return model.customViewMenuItem
166
- ? model.customViewMenuItem(view, model)
167
- : menuItem({
168
- className: 'xh-view-manager__menu-item',
169
- key: view.token,
170
- text: view.name,
171
- title: title.join(' | '),
172
- icon,
173
- onClick: () => model.selectViewAsync(view).catchDefault()
174
- });
165
+ return menuItem({
166
+ className: 'xh-view-manager__menu-item',
167
+ key: view.token,
168
+ text: view.name,
169
+ title: title.join(' | '),
170
+ icon,
171
+ onClick: () => model.selectViewAsync(view).catchDefault()
172
+ });
175
173
  }
@@ -5,8 +5,9 @@
5
5
  * Copyright © 2025 Extremely Heavy Industries Inc.
6
6
  */
7
7
  import {span} from '@xh/hoist/cmp/layout';
8
- import {capitalize, isNil, kebabCase, map} from 'lodash';
8
+ import {capitalize, isNil, isString, kebabCase, map} from 'lodash';
9
9
  import {CSSProperties, ReactNode} from 'react';
10
+ import {PlainObject} from '@xh/hoist/core';
10
11
 
11
12
  export interface FormatOptions<T = any> {
12
13
  /** Display value for null values. */
@@ -63,11 +64,12 @@ export interface JSONFormatOptions {
63
64
  }
64
65
 
65
66
  /**
66
- * Pretty-print a JSON string, adding line breaks and indentation.
67
+ * Pretty-print a JSON string or (JSON Object), adding line breaks and indentation.
67
68
  */
68
- export function fmtJson(str: string, opts?: JSONFormatOptions): string {
69
+ export function fmtJson(v: string | PlainObject, opts?: JSONFormatOptions): string {
69
70
  const {replacer = undefined, space = 2} = opts ?? {};
70
- return isNil(str) ? '' : JSON.stringify(JSON.parse(str), replacer, space);
71
+ if (isString(v)) v = JSON.parse(v);
72
+ return isNil(v) ? '' : JSON.stringify(v, replacer, space);
71
73
  }
72
74
 
73
75
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xh/hoist",
3
- "version": "73.0.0-SNAPSHOT.1744145524716",
3
+ "version": "73.0.0-SNAPSHOT.1744147015222",
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",
@@ -320,11 +320,7 @@ export class MsalClient extends BaseOAuthClient<MsalClientConfig, MsalTokenSpec>
320
320
  // Handle TrackService not yet initialized (common, this client likely initialized before.)
321
321
  this.addReaction({
322
322
  when: () => XH.appIsRunning,
323
- run: () =>
324
- XH.trackService.addClientHealthReportSource(
325
- 'msalClient',
326
- () => this.telemetryResults
327
- )
323
+ run: () => XH.clientHealthService.addSource('msalClient', () => this.telemetryResults)
328
324
  });
329
325
 
330
326
  this.logInfo('Telemetry enabled');
@@ -339,7 +335,7 @@ export class MsalClient extends BaseOAuthClient<MsalClientConfig, MsalTokenSpec>
339
335
  this.client.removePerformanceCallback(this._telemetryCbHandle);
340
336
  this._telemetryCbHandle = null;
341
337
 
342
- XH.trackService.removeClientHealthReportSource('msalClient');
338
+ XH.clientHealthService.removeSource('msalClient');
343
339
  this.logInfo('Telemetry disabled', this.telemetryResults);
344
340
  }
345
341