@xh/hoist 72.3.0 → 72.4.0

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 (36) hide show
  1. package/CHANGELOG.md +13 -1
  2. package/admin/tabs/activity/tracking/detail/ActivityDetailModel.ts +2 -2
  3. package/admin/tabs/cluster/instances/BaseInstanceModel.ts +1 -29
  4. package/admin/tabs/cluster/instances/services/DetailsPanel.ts +2 -1
  5. package/admin/tabs/cluster/objects/DetailModel.ts +4 -40
  6. package/admin/tabs/cluster/objects/DetailPanel.ts +2 -1
  7. package/appcontainer/AppContainerModel.ts +2 -0
  8. package/appcontainer/AppStateModel.ts +40 -8
  9. package/build/types/admin/tabs/cluster/instances/BaseInstanceModel.d.ts +1 -3
  10. package/build/types/admin/tabs/cluster/objects/DetailModel.d.ts +1 -3
  11. package/build/types/appcontainer/AppStateModel.d.ts +2 -0
  12. package/build/types/cmp/viewmanager/ViewManagerModel.d.ts +7 -0
  13. package/build/types/core/XH.d.ts +11 -1
  14. package/build/types/format/FormatDate.d.ts +22 -1
  15. package/build/types/format/FormatMisc.d.ts +3 -2
  16. package/build/types/security/Types.d.ts +3 -3
  17. package/build/types/security/msal/MsalClient.d.ts +2 -0
  18. package/build/types/svc/ClientHealthService.d.ts +58 -0
  19. package/build/types/svc/TrackService.d.ts +0 -12
  20. package/build/types/svc/index.d.ts +1 -0
  21. package/build/types/utils/js/index.d.ts +0 -1
  22. package/cmp/viewmanager/ViewManagerModel.ts +10 -1
  23. package/core/XH.ts +26 -1
  24. package/desktop/cmp/viewmanager/ViewMenu.ts +11 -9
  25. package/format/FormatDate.ts +45 -3
  26. package/format/FormatMisc.ts +6 -4
  27. package/package.json +1 -1
  28. package/security/Types.ts +3 -3
  29. package/security/msal/MsalClient.ts +13 -13
  30. package/svc/ClientHealthService.ts +165 -0
  31. package/svc/TrackService.ts +3 -67
  32. package/svc/index.ts +1 -0
  33. package/tsconfig.tsbuildinfo +1 -1
  34. package/utils/js/index.ts +0 -1
  35. package/build/types/utils/js/BrowserUtils.d.ts +0 -41
  36. package/utils/js/BrowserUtils.ts +0 -103
package/CHANGELOG.md CHANGED
@@ -1,12 +1,24 @@
1
1
  # Changelog
2
2
 
3
+ ## v72.4.0 - 2025-04-09
4
+
5
+ ### 🎁 New Features
6
+ * New methods for formatting timestamps within nested JSON objects. See `withFormattedTimestamps`
7
+ and `timestampReplacer` in the `@xh/hoist/format` package.
8
+ * `ViewManagerConfig` takes new optional key `viewMenuItemFn` to allow ViewManager implementations
9
+ to customize the menu items for views in the view manager menu.
10
+
11
+ ### ⚙️ Technical
12
+ * Added dedicated `ClientHealthService` for managing client health report. Additional enhancements
13
+ to health report to include information about web sockets, idle time, and page state.
14
+
3
15
  ## v72.3.0 - 2025-04-08
4
16
 
5
17
  ### 🎁 New Features
6
18
 
7
19
  * Added support for posting a "Client Health Report" track message on a configurable interval. This
8
20
  message will include basic client information, and can be extended to include any other desired
9
- data via `XH.trackService.addClientHealthReportSource()`. Enable by updating your app's
21
+ data via `XH.clientHealthService.addSource()`. Enable by updating your app's
10
22
  `xhActivityTrackingConfig` to include `clientHealthReport: {intervalMins: XXXX}`.
11
23
  * Enabled opt-in support for telemetry in `MsalClient`, leveraging hooks built-in to MSAL to collect
12
24
  timing and success/failure count for all events emitted by the library.
@@ -9,9 +9,9 @@ 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, timestampReplacer} 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,7 @@ 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, timestampReplacer} from '@xh/hoist/format';
15
16
 
16
17
  export const detailsPanel = hoistCmp.factory({
17
18
  model: creates(DetailsModel),
@@ -57,7 +58,7 @@ const stats = hoistCmp.factory<DetailsModel>({
57
58
  enableSearch: true,
58
59
  showFullscreenButton: false,
59
60
  editorProps: {lineNumbers: false},
60
- value: model.parent.fmtStats(stats)
61
+ value: fmtJson(stats, {replacer: timestampReplacer()})
61
62
  })
62
63
  );
63
64
  }
@@ -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 {withFormattedTimestamps} from '@xh/hoist/format';
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,7 @@ 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
+ const data = withFormattedTimestamps(adminStatsByInstance[instanceName] ?? {});
100
83
  return {instanceName, ...data};
101
84
  })
102
85
  );
@@ -136,23 +119,4 @@ export class DetailModel extends HoistModel {
136
119
  }
137
120
  return ret;
138
121
  }
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
122
  }
@@ -12,6 +12,7 @@ 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 {fmtJson, timestampReplacer} from '@xh/hoist/format';
15
16
 
16
17
  export const detailPanel = hoistCmp.factory({
17
18
  model: creates(DetailModel),
@@ -42,7 +43,7 @@ export const detailPanel = hoistCmp.factory({
42
43
  height: '100%',
43
44
  showFullscreenButton: false,
44
45
  editorProps: {lineNumbers: false},
45
- value: model.fmtStats(selectedAdminStats)
46
+ value: fmtJson(selectedAdminStats, {replacer: timestampReplacer()})
46
47
  })
47
48
  })
48
49
  ]
@@ -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,
@@ -4,11 +4,10 @@
4
4
  *
5
5
  * Copyright © 2025 Extremely Heavy Industries Inc.
6
6
  */
7
- import {AppState, AppSuspendData, HoistModel, XH} from '@xh/hoist/core';
7
+ import {AppState, AppSuspendData, HoistModel, PlainObject, 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
- import {camelCase, isBoolean, isString, mapKeys} from 'lodash';
10
+ import {camelCase, isBoolean, isString, mapKeys, pick} from 'lodash';
12
11
 
13
12
  /**
14
13
  * Support for Core Hoist Application state and loading.
@@ -93,13 +92,14 @@ export class AppStateModel extends HoistModel {
93
92
  timestamp: loadStarted,
94
93
  elapsed: Date.now() - loadStarted - (timings.LOGIN_REQUIRED ?? 0),
95
94
  data: {
96
- appVersion: XH.appVersion,
97
- appBuild: XH.appBuild,
98
- locationHref: window.location.href,
95
+ clientId: XH.clientId,
96
+ sessionId: XH.sessionId,
99
97
  timings: mapKeys(timings, (v, k) => camelCase(k)),
100
- ...getClientDeviceInfo()
98
+ clientHealth: XH.clientHealthService.getReport(),
99
+ window: this.getWindowData(),
100
+ screen: this.getScreenData()
101
101
  },
102
- logData: ['appVersion', 'appBuild'],
102
+ logData: ['clientId', 'sessionId'],
103
103
  omit: !XH.appSpec.trackAppLoad
104
104
  })
105
105
  });
@@ -115,4 +115,36 @@ export class AppStateModel extends HoistModel {
115
115
  });
116
116
  });
117
117
  }
118
+
119
+ private getScreenData(): PlainObject {
120
+ const screen = window.screen as any;
121
+ if (!screen) return null;
122
+
123
+ const ret: PlainObject = pick(screen, [
124
+ 'availWidth',
125
+ 'availHeight',
126
+ 'width',
127
+ 'height',
128
+ 'colorDepth',
129
+ 'pixelDepth',
130
+ 'availLeft',
131
+ 'availTop'
132
+ ]);
133
+ if (screen.orientation) {
134
+ ret.orientation = pick(screen.orientation, ['angle', 'type']);
135
+ }
136
+ return ret;
137
+ }
138
+
139
+ private getWindowData(): PlainObject {
140
+ return pick(window, [
141
+ 'devicePixelRatio',
142
+ 'screenX',
143
+ 'screenY',
144
+ 'innerWidth',
145
+ 'innerHeight',
146
+ 'outerWidth',
147
+ 'outerHeight'
148
+ ]);
149
+ }
118
150
  }
@@ -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
  }
@@ -23,4 +23,6 @@ export declare class AppStateModel extends HoistModel {
23
23
  checkAccess(): boolean;
24
24
  private trackLoad;
25
25
  private createActivityListeners;
26
+ private getScreenData;
27
+ private getWindowData;
26
28
  }
@@ -1,5 +1,6 @@
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';
3
4
  import { ViewInfo } from './ViewInfo';
4
5
  import { View } from './View';
5
6
  export interface ViewCreateSpec {
@@ -85,6 +86,11 @@ export interface ViewManagerConfig {
85
86
  * Optional user-facing display name for describing global views. Defaults to 'global'
86
87
  */
87
88
  globalDisplayName?: string;
89
+ /**
90
+ * Optional key to pass a method that returns a customized BlueprintJS `menuItem` for listing
91
+ * views in the ViewManager menu.
92
+ */
93
+ viewMenuItemFn?: (view: ViewInfo, model: ViewManagerModel) => ReactNode;
88
94
  }
89
95
  /**
90
96
  * ViewManagerModel coordinates the loading, saving, and management of user-defined bundles of
@@ -121,6 +127,7 @@ export declare class ViewManagerModel<T = PlainObject> extends HoistModel {
121
127
  readonly instance: string;
122
128
  readonly typeDisplayName: string;
123
129
  readonly globalDisplayName: string;
130
+ readonly viewMenuItemFn: (view: ViewInfo, model: ViewManagerModel) => ReactNode;
124
131
  readonly enableAutoSave: boolean;
125
132
  readonly enableDefault: boolean;
126
133
  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';
@@ -21,6 +21,13 @@ export declare const MIN_HOIST_CORE_VERSION = "28.0";
21
21
  * Available via import as `XH` - also installed as `window.XH` for troubleshooting purposes.
22
22
  */
23
23
  export declare class XHApi {
24
+ /** Unique id for this loaded instance of the app. Unique for every refresh of document. */
25
+ clientId: string;
26
+ /**
27
+ * Unique id for this browser tab/window on this domain.
28
+ * Corresponds to the scope of the built-in sessionStorage object.
29
+ */
30
+ sessionId: string;
24
31
  /** Core implementation model hosting all application state. */
25
32
  appContainerModel: AppContainerModel;
26
33
  /** Provider of centralized exception handling for the app. */
@@ -47,6 +54,7 @@ export declare class XHApi {
47
54
  alertBannerService: AlertBannerService;
48
55
  autoRefreshService: AutoRefreshService;
49
56
  changelogService: ChangelogService;
57
+ clientHealthService: ClientHealthService;
50
58
  configService: ConfigService;
51
59
  environmentService: EnvironmentService;
52
60
  fetchService: FetchService;
@@ -404,6 +412,8 @@ export declare class XHApi {
404
412
  */
405
413
  genId(): string;
406
414
  private get acm();
415
+ private genClientId;
416
+ private genSessionId;
407
417
  }
408
418
  /** The app-wide singleton instance. */
409
419
  export declare const XH: XHApi;
@@ -1,5 +1,5 @@
1
1
  import { ReactNode } from 'react';
2
- import { DateLike } from '../core/types/Types';
2
+ import { DateLike, PlainObject } from '../core/types/Types';
3
3
  import { FormatOptions } from './FormatMisc';
4
4
  export declare const DATE_FMT = "YYYY-MM-DD", DATETIME_FMT = "YYYY-MM-DD h:mma", DATETIMESEC_FMT = "YYYY-MM-DD h:mm:ssa", TIME_FMT = "h:mma", MONTH_DAY_FMT = "MMM D";
5
5
  /**
@@ -49,4 +49,25 @@ export interface CompactDateFormatOptions extends FormatOptions<DateLike> {
49
49
  * Render dates formatted based on distance in time from current day.
50
50
  */
51
51
  export declare function fmtCompactDate(v: DateLike, opts?: CompactDateFormatOptions): ReactNode;
52
+ export interface TimestampReplacerConfig {
53
+ /**
54
+ * Suffixes used to identify keys that may hold timestamps.
55
+ * Defaults to ['time', 'date', 'timestamp']
56
+ */
57
+ suffixes?: string[];
58
+ /**
59
+ * Format for replaced timestamp.
60
+ * Defaults to 'MMM DD HH:mm:ss.SSS'
61
+ */
62
+ format?: string;
63
+ }
64
+ /**
65
+ * Replace timestamps in an Object with formatted strings.
66
+ */
67
+ export declare function withFormattedTimestamps(obj: PlainObject, config?: TimestampReplacerConfig): PlainObject;
68
+ /**
69
+ * Create a replacer, suitable for JSON.stringify, that will replace timestamps with
70
+ * formatted strings.
71
+ */
72
+ export declare function timestampReplacer(config?: TimestampReplacerConfig): (k: string, v: any) => any;
52
73
  export declare const dateRenderer: (obj?: string | DateFormatOptions) => (v: DateLike) => ReactNode, dateTimeRenderer: (obj?: string | DateFormatOptions) => (v: any) => ReactNode, dateTimeSecRenderer: (obj?: string | DateFormatOptions) => (v: DateLike) => ReactNode, timeRenderer: (obj?: string | DateFormatOptions) => (v: DateLike) => ReactNode, compactDateRenderer: (obj?: CompactDateFormatOptions) => (v: DateLike) => ReactNode;
@@ -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.
@@ -19,8 +19,8 @@ export interface TelemetryResults {
19
19
  }
20
20
  /** Aggregated telemetry results for a single type of event. */
21
21
  export interface TelemetryEventResults {
22
- firstTime: Date;
23
- lastTime: Date;
22
+ firstTime: number;
23
+ lastTime: number;
24
24
  successCount: number;
25
25
  failureCount: number;
26
26
  /** Timing info (in ms) for event instances reported with duration. */
@@ -31,7 +31,7 @@ export interface TelemetryEventResults {
31
31
  worst: number;
32
32
  };
33
33
  lastFailure?: {
34
- time: Date;
34
+ time: number;
35
35
  duration: number;
36
36
  code: string;
37
37
  name: string;
@@ -1,5 +1,6 @@
1
1
  import * as msal from '@azure/msal-browser';
2
2
  import { LogLevel } from '@azure/msal-browser';
3
+ import { PlainObject } from '@xh/hoist/core';
3
4
  import { Token } from '@xh/hoist/security/Token';
4
5
  import { BaseOAuthClient, BaseOAuthClientConfig } from '../BaseOAuthClient';
5
6
  import { AccessTokenSpec, TelemetryResults, TokenMap } from '../Types';
@@ -96,6 +97,7 @@ export declare class MsalClient extends BaseOAuthClient<MsalClientConfig, MsalTo
96
97
  protected fetchIdTokenAsync(useCache?: boolean): Promise<Token>;
97
98
  protected fetchAccessTokenAsync(spec: MsalTokenSpec, useCache?: boolean): Promise<Token>;
98
99
  protected doLogoutAsync(): Promise<void>;
100
+ getFormattedTelemetry(): PlainObject;
99
101
  enableTelemetry(): void;
100
102
  disableTelemetry(): void;
101
103
  private loginSsoAsync;
@@ -0,0 +1,58 @@
1
+ import { HoistService, PageState, 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 point. Return a default report of client health.
15
+ */
16
+ getReport(): ClientHealthReport;
17
+ /** Get report, suitable for viewing in console. **/
18
+ getFormattedReport(): PlainObject;
19
+ /**
20
+ * Register a new source for client health report data. No-op if background health report is
21
+ * not generally enabled via `xhActivityTrackingConfig.clientHealthReport.intervalMins`.
22
+ *
23
+ * @param key - key under which to report the data - can be used to remove this source later.
24
+ * @param callback - function returning serializable to include with each report.
25
+ */
26
+ addSource(key: string, callback: () => any): void;
27
+ /** Unregister a previously-enabled source for client health report data. */
28
+ removeSource(key: string): void;
29
+ getGeneral(): GeneralData;
30
+ getConnection(): ConnectionData;
31
+ getMemory(): MemoryData;
32
+ getCustom(): PlainObject;
33
+ private sendReport;
34
+ }
35
+ export interface GeneralData {
36
+ startTime: number;
37
+ durationMins: number;
38
+ idleMins: number;
39
+ pageState: PageState;
40
+ webSocket: string;
41
+ }
42
+ export interface ConnectionData {
43
+ downlink: number;
44
+ effectiveType: string;
45
+ rtt: number;
46
+ }
47
+ export interface MemoryData {
48
+ modelCount: number;
49
+ usedPctLimit?: number;
50
+ jsHeapSizeLimit?: number;
51
+ totalJSHeapSize?: number;
52
+ usedJSHeapSize?: number;
53
+ }
54
+ export interface ClientHealthReport {
55
+ general: GeneralData;
56
+ connection: ConnectionData;
57
+ memory: MemoryData;
58
+ }
@@ -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,6 +123,12 @@ 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 a customized BlueprintJS `menuItem` for listing
129
+ * views in the ViewManager menu.
130
+ */
131
+ viewMenuItemFn?: (view: ViewInfo, model: ViewManagerModel) => ReactNode;
126
132
  }
127
133
 
128
134
  /**
@@ -165,6 +171,7 @@ export class ViewManagerModel<T = PlainObject> extends HoistModel {
165
171
  readonly instance: string;
166
172
  readonly typeDisplayName: string;
167
173
  readonly globalDisplayName: string;
174
+ readonly viewMenuItemFn: (view: ViewInfo, model: ViewManagerModel) => ReactNode;
168
175
  readonly enableAutoSave: boolean;
169
176
  readonly enableDefault: boolean;
170
177
  readonly enableGlobal: boolean;
@@ -283,6 +290,7 @@ export class ViewManagerModel<T = PlainObject> extends HoistModel {
283
290
  instance = 'default',
284
291
  typeDisplayName,
285
292
  globalDisplayName = 'global',
293
+ viewMenuItemFn,
286
294
  manageGlobal = false,
287
295
  enableAutoSave = true,
288
296
  enableDefault = true,
@@ -296,13 +304,14 @@ export class ViewManagerModel<T = PlainObject> extends HoistModel {
296
304
 
297
305
  throwIf(
298
306
  !enableDefault && !initialViewSpec,
299
- "ViewManagerModel requires 'initialViewSpec' if `enableDefault` is false."
307
+ "ViewManagerModel requires 'initialViewSpec' if 'enableDefault' is false."
300
308
  );
301
309
 
302
310
  this.type = type;
303
311
  this.instance = instance;
304
312
  this.typeDisplayName = lowerCase(typeDisplayName ?? genDisplayName(type));
305
313
  this.globalDisplayName = globalDisplayName;
314
+ this.viewMenuItemFn = viewMenuItemFn;
306
315
  this.manageGlobal = executeIfFunction(manageGlobal) ?? false;
307
316
  this.enableDefault = enableDefault;
308
317
  this.enableGlobal = enableGlobal;