@xh/hoist 72.3.0 → 72.5.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 (55) hide show
  1. package/CHANGELOG.md +25 -6
  2. package/admin/tabs/activity/tracking/ActivityTrackingModel.ts +22 -14
  3. package/admin/tabs/activity/tracking/detail/ActivityDetailModel.ts +2 -2
  4. package/admin/tabs/cluster/instances/BaseInstanceModel.ts +1 -29
  5. package/admin/tabs/cluster/instances/services/DetailsPanel.ts +2 -1
  6. package/admin/tabs/cluster/instances/websocket/WebSocketColumns.ts +24 -2
  7. package/admin/tabs/cluster/instances/websocket/WebSocketModel.ts +76 -25
  8. package/admin/tabs/cluster/instances/websocket/WebSocketPanel.ts +2 -2
  9. package/admin/tabs/cluster/objects/DetailModel.ts +4 -40
  10. package/admin/tabs/cluster/objects/DetailPanel.ts +2 -1
  11. package/appcontainer/AppContainerModel.ts +2 -0
  12. package/appcontainer/AppStateModel.ts +40 -8
  13. package/build/types/admin/tabs/activity/tracking/ActivityTrackingModel.d.ts +1 -1
  14. package/build/types/admin/tabs/cluster/instances/BaseInstanceModel.d.ts +1 -3
  15. package/build/types/admin/tabs/cluster/instances/websocket/WebSocketColumns.d.ts +4 -1
  16. package/build/types/admin/tabs/cluster/instances/websocket/WebSocketModel.d.ts +5 -2
  17. package/build/types/admin/tabs/cluster/objects/DetailModel.d.ts +1 -3
  18. package/build/types/appcontainer/AppStateModel.d.ts +2 -0
  19. package/build/types/cmp/viewmanager/ViewManagerModel.d.ts +7 -0
  20. package/build/types/core/XH.d.ts +11 -1
  21. package/build/types/core/types/Interfaces.d.ts +2 -2
  22. package/build/types/format/FormatDate.d.ts +22 -1
  23. package/build/types/format/FormatMisc.d.ts +3 -2
  24. package/build/types/security/Types.d.ts +0 -25
  25. package/build/types/security/msal/MsalClient.d.ts +42 -4
  26. package/build/types/svc/ClientHealthService.d.ts +64 -0
  27. package/build/types/svc/TrackService.d.ts +3 -11
  28. package/build/types/svc/WebSocketService.d.ts +38 -15
  29. package/build/types/svc/index.d.ts +1 -0
  30. package/build/types/utils/js/index.d.ts +0 -1
  31. package/cmp/viewmanager/ViewManagerModel.ts +10 -1
  32. package/core/XH.ts +26 -1
  33. package/core/types/Interfaces.ts +2 -2
  34. package/data/filter/BaseFilterFieldSpec.ts +6 -2
  35. package/desktop/appcontainer/AboutDialog.ts +14 -0
  36. package/desktop/cmp/button/AppMenuButton.ts +1 -1
  37. package/desktop/cmp/contextmenu/ContextMenu.ts +1 -1
  38. package/desktop/cmp/viewmanager/ViewMenu.ts +11 -9
  39. package/format/FormatDate.ts +45 -3
  40. package/format/FormatMisc.ts +6 -4
  41. package/kit/onsen/theme.scss +5 -0
  42. package/mobile/appcontainer/AboutDialog.scss +1 -1
  43. package/mobile/appcontainer/AboutDialog.ts +24 -1
  44. package/mobile/cmp/menu/impl/Menu.ts +2 -2
  45. package/package.json +1 -1
  46. package/security/Types.ts +0 -27
  47. package/security/msal/MsalClient.ts +77 -25
  48. package/svc/ClientHealthService.ts +179 -0
  49. package/svc/TrackService.ts +9 -69
  50. package/svc/WebSocketService.ts +74 -33
  51. package/svc/index.ts +1 -0
  52. package/tsconfig.tsbuildinfo +1 -1
  53. package/utils/js/index.ts +0 -1
  54. package/build/types/utils/js/BrowserUtils.d.ts +0 -41
  55. package/utils/js/BrowserUtils.ts +0 -103
@@ -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
+ loadId: 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
+ tabId: 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 genLoadId;
416
+ private genTabId;
407
417
  }
408
418
  /** The app-wide singleton instance. */
409
419
  export declare const XH: XHApi;
@@ -1,5 +1,5 @@
1
1
  import { RuleLike } from '@xh/hoist/data';
2
- import { ReactElement, ReactNode } from 'react';
2
+ import { MouseEvent, ReactElement, ReactNode } from 'react';
3
3
  import { LoadSpec } from '../load';
4
4
  import { Intent, PlainObject, Thunkable } from './Types';
5
5
  /**
@@ -226,7 +226,7 @@ export interface MenuItem {
226
226
  /** Css class name to be added when rendering the menu item. */
227
227
  className?: string;
228
228
  /** Executed when the user clicks the menu item. */
229
- actionFn?: () => void;
229
+ actionFn?: (e: MouseEvent | PointerEvent) => void;
230
230
  /** Executed before the item is shown. Use to adjust properties dynamically. */
231
231
  prepareFn?: (me: MenuItem) => void;
232
232
  /** Child menu items. */
@@ -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.
@@ -12,28 +12,3 @@ export interface AccessTokenSpec {
12
12
  scopes: string[];
13
13
  }
14
14
  export type TokenMap = Record<string, Token>;
15
- /** Aggregated telemetry results, produced by {@link MsalClient} when enabled via config. */
16
- export interface TelemetryResults {
17
- /** Stats by event type - */
18
- events: Record<string, TelemetryEventResults>;
19
- }
20
- /** Aggregated telemetry results for a single type of event. */
21
- export interface TelemetryEventResults {
22
- firstTime: Date;
23
- lastTime: Date;
24
- successCount: number;
25
- failureCount: number;
26
- /** Timing info (in ms) for event instances reported with duration. */
27
- duration: {
28
- count: number;
29
- total: number;
30
- average: number;
31
- worst: number;
32
- };
33
- lastFailure?: {
34
- time: Date;
35
- duration: number;
36
- code: string;
37
- name: string;
38
- };
39
- }
@@ -1,8 +1,9 @@
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
- import { AccessTokenSpec, TelemetryResults, TokenMap } from '../Types';
6
+ import { AccessTokenSpec, TokenMap } from '../Types';
6
7
  export interface MsalClientConfig extends BaseOAuthClientConfig<MsalTokenSpec> {
7
8
  /**
8
9
  * Authority for your organization's tenant: `https://login.microsoftonline.com/[tenantId]`.
@@ -18,8 +19,7 @@ export interface MsalClientConfig extends BaseOAuthClientConfig<MsalTokenSpec> {
18
19
  domainHint?: string;
19
20
  /**
20
21
  * True to enable support for built-in telemetry provided by this class's internal MSAL client.
21
- * Captured performance events will be summarized via {@link telemetryResults}.
22
- * See https://github.com/AzureAD/microsoft-authentication-library-for-js/blob/dev/lib/msal-browser/docs/performance.md
22
+ * Captured performance events will be summarized as {@link MsalClientTelemetry}.
23
23
  */
24
24
  enableTelemetry?: boolean;
25
25
  /**
@@ -87,7 +87,7 @@ export declare class MsalClient extends BaseOAuthClient<MsalClientConfig, MsalTo
87
87
  private account;
88
88
  private initialTokenLoad;
89
89
  /** Enable telemetry via `enableTelemetry` ctor config, or via {@link enableTelemetry}. */
90
- telemetryResults: TelemetryResults;
90
+ telemetry: MsalClientTelemetry;
91
91
  private _telemetryCbHandle;
92
92
  constructor(config: MsalClientConfig);
93
93
  protected doInitAsync(): Promise<TokenMap>;
@@ -96,6 +96,7 @@ export declare class MsalClient extends BaseOAuthClient<MsalClientConfig, MsalTo
96
96
  protected fetchIdTokenAsync(useCache?: boolean): Promise<Token>;
97
97
  protected fetchAccessTokenAsync(spec: MsalTokenSpec, useCache?: boolean): Promise<Token>;
98
98
  protected doLogoutAsync(): Promise<void>;
99
+ getFormattedTelemetry(): PlainObject;
99
100
  enableTelemetry(): void;
100
101
  disableTelemetry(): void;
101
102
  private loginSsoAsync;
@@ -106,3 +107,40 @@ export declare class MsalClient extends BaseOAuthClient<MsalClientConfig, MsalTo
106
107
  private get refreshOffsetArgs();
107
108
  private noteUserAuthenticated;
108
109
  }
110
+ /**
111
+ * Telemetry produced by this client (if enabled) + included in {@link ClientHealthService}
112
+ * reporting. Leverages MSAL's opt-in support for emitting performance events.
113
+ * See https://github.com/AzureAD/microsoft-authentication-library-for-js/blob/dev/lib/msal-browser/docs/performance.md
114
+ */
115
+ interface MsalClientTelemetry {
116
+ /** Stats across all events */
117
+ summary: {
118
+ successCount: number;
119
+ failureCount: number;
120
+ maxDuration: number;
121
+ lastFailureTime: number;
122
+ };
123
+ /** Stats by event type */
124
+ events: Record<string, MsalEventTelemetry>;
125
+ }
126
+ /** Aggregated telemetry results for a single type of event. */
127
+ interface MsalEventTelemetry {
128
+ firstTime: number;
129
+ lastTime: number;
130
+ successCount: number;
131
+ failureCount: number;
132
+ /** Timing info (in ms) for event instances reported with duration. */
133
+ duration?: {
134
+ count: number;
135
+ total: number;
136
+ average: number;
137
+ max: number;
138
+ };
139
+ lastFailure?: {
140
+ time: number;
141
+ duration: number;
142
+ code: string;
143
+ name: string;
144
+ };
145
+ }
146
+ export {};
@@ -0,0 +1,64 @@
1
+ import { HoistService, PageState, PlainObject } from '@xh/hoist/core';
2
+ import { WebSocketTelemetry } from '@xh/hoist/svc/WebSocketService';
3
+ /**
4
+ * Service for gathering data about the current state and health of the client app, for submission
5
+ * to the server or review on the console during interactive troubleshooting.
6
+ *
7
+ * Hoist sends this data once on application load and can be configured to send at regular intervals
8
+ * throughout a user's session via the `xhActivityTracking.clientHealthReport` app config. Reports
9
+ * are submitted via activity tracking for review within the Admin Console.
10
+ */
11
+ export declare class ClientHealthService extends HoistService {
12
+ static instance: ClientHealthService;
13
+ private sources;
14
+ initAsync(): Promise<void>;
15
+ get enabled(): boolean;
16
+ /** @returns a customizable report with metrics capturing client app/session state. */
17
+ getReport(): ClientHealthReport;
18
+ /** @returns a report, formatted for easier viewing in console. **/
19
+ getFormattedReport(): PlainObject;
20
+ /**
21
+ * Register a new source for app-specific data to be sent with each report.
22
+ * @param key - key under which to report the data - can be used to remove this source later.
23
+ * @param callback - function returning serializable to include with each report.
24
+ */
25
+ addSource(key: string, callback: () => any): void;
26
+ /** Unregister a previously-enabled source for client health report data. */
27
+ removeSource(key: string): void;
28
+ /**
29
+ * Generate and submit a report to the server, via TrackService.
30
+ *
31
+ * For ad-hoc troubleshooting. Apps may also configure this service to
32
+ * submit on regular intervals.
33
+ */
34
+ sendReportAsync(): Promise<void>;
35
+ getGeneral(): GeneralData;
36
+ getConnection(): ConnectionData;
37
+ getMemory(): MemoryData;
38
+ getCustom(): PlainObject;
39
+ private sendReportInternal;
40
+ }
41
+ export interface GeneralData {
42
+ startTime: number;
43
+ durationMins: number;
44
+ idleMins: number;
45
+ pageState: PageState;
46
+ }
47
+ export interface ConnectionData {
48
+ downlink: number;
49
+ effectiveType: string;
50
+ rtt: number;
51
+ }
52
+ export interface MemoryData {
53
+ modelCount: number;
54
+ usedPctLimit?: number;
55
+ jsHeapSizeLimit?: number;
56
+ totalJSHeapSize?: number;
57
+ usedJSHeapSize?: number;
58
+ }
59
+ export interface ClientHealthReport {
60
+ general: GeneralData;
61
+ connection: ConnectionData;
62
+ memory: MemoryData;
63
+ webSockets: WebSocketTelemetry;
64
+ }
@@ -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>;
@@ -15,20 +14,13 @@ export declare class TrackService extends HoistService {
15
14
  /** Track User Activity. */
16
15
  track(options: TrackOptions | string): void;
17
16
  /**
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.
17
+ * Flush the queue of pending activity tracking messages to the server.
18
+ * @internal - apps should generally allow this service to manage w/its internal debounce.
23
19
  */
24
- addClientHealthReportSource(key: string, callback: () => any): void;
25
- /** Unregister a previously-enabled source for client health report data. */
26
- removeClientHealthReportSource(key: string): void;
27
- private pushPendingAsync;
20
+ pushPendingAsync(): Promise<void>;
28
21
  private pushPendingBuffered;
29
22
  private toServerJson;
30
23
  private logMessage;
31
- private sendClientHealthReport;
32
24
  }
33
25
  interface ActivityTrackingConfig {
34
26
  clientHealthReport?: Partial<TrackOptions> & {
@@ -1,4 +1,4 @@
1
- import { HoistService } from '@xh/hoist/core';
1
+ import { HoistService, PlainObject } from '@xh/hoist/core';
2
2
  /**
3
3
  * Establishes and maintains a websocket connection to the Hoist server, if enabled via `AppSpec`.
4
4
  *
@@ -27,6 +27,10 @@ export declare class WebSocketService extends HoistService {
27
27
  readonly HEARTBEAT_TOPIC = "xhHeartbeat";
28
28
  readonly REG_SUCCESS_TOPIC = "xhRegistrationSuccess";
29
29
  readonly FORCE_APP_SUSPEND_TOPIC = "xhForceAppSuspend";
30
+ readonly REQ_CLIENT_HEALTH_RPT_TOPIC = "xhRequestClientHealthReport";
31
+ readonly METADATA_FOR_HANDSHAKE: string[];
32
+ /** True if WebSockets generally enabled - set statically in code via {@link AppSpec}. */
33
+ enabled: boolean;
30
34
  /** Unique channel assigned by server upon successful connection. */
31
35
  channelKey: string;
32
36
  /** Last time a message was received, including heartbeat messages. */
@@ -35,10 +39,10 @@ export declare class WebSocketService extends HoistService {
35
39
  get connected(): boolean;
36
40
  /** Set to true to log all sent/received messages - very chatty. */
37
41
  logMessages: boolean;
42
+ telemetry: WebSocketTelemetry;
38
43
  private _timer;
39
44
  private _socket;
40
45
  private _subsByTopic;
41
- enabled: boolean;
42
46
  constructor();
43
47
  initAsync(): Promise<void>;
44
48
  /**
@@ -61,23 +65,24 @@ export declare class WebSocketService extends HoistService {
61
65
  * Send a message back to the server via the connected websocket.
62
66
  */
63
67
  sendMessage(message: WebSocketMessage): void;
64
- connect(): void;
65
- disconnect(): void;
66
- heartbeatOrReconnect(): void;
67
- private onServerInstanceChange;
68
68
  shutdown(): void;
69
+ getFormattedTelemetry(): PlainObject;
70
+ private connect;
71
+ private disconnect;
72
+ private heartbeatOrReconnect;
73
+ private onServerInstanceChange;
69
74
  onOpen(ev: any): void;
70
75
  onClose(ev: any): void;
71
76
  onError(ev: any): void;
72
- onMessage(rawMsg: any): void;
73
- notifySubscribers(message: any): void;
74
- getSubsForTopic(topic: any): WebSocketSubscription[];
75
- updateConnectedStatus(): void;
76
- installChannelKey(key: any): void;
77
- updateLastMessageTime(): void;
78
- buildWebSocketUrl(): string;
79
- showTestMessageAlert(message: any): void;
80
- maybeLogMessage(...args: any[]): void;
77
+ onMessage(rawMsg: MessageEvent): void;
78
+ private notifySubscribers;
79
+ private getSubsForTopic;
80
+ private updateConnectedStatus;
81
+ private installChannelKey;
82
+ private updateLastMessageTime;
83
+ private buildWebSocketUrl;
84
+ private maybeLogMessage;
85
+ private noteTelemetryEvent;
81
86
  }
82
87
  /**
83
88
  * Wrapper class to encapsulate and manage a subscription to messages for a given topic + handler.
@@ -93,3 +98,21 @@ export interface WebSocketMessage {
93
98
  topic: string;
94
99
  data?: any;
95
100
  }
101
+ /** Telemetry collected by this service + included in {@link ClientHealthService} reporting. */
102
+ export interface WebSocketTelemetry {
103
+ channelKey: string;
104
+ subscriptionCount: number;
105
+ events: {
106
+ connOpened?: WebSocketEventTelemetry;
107
+ connClosed?: WebSocketEventTelemetry;
108
+ connError?: WebSocketEventTelemetry;
109
+ msgReceived?: WebSocketEventTelemetry;
110
+ msgSent?: WebSocketEventTelemetry;
111
+ heartbeatReconnectAttempt?: WebSocketEventTelemetry;
112
+ instanceChangeReconnectAttempt?: WebSocketEventTelemetry;
113
+ };
114
+ }
115
+ export interface WebSocketEventTelemetry {
116
+ count: number;
117
+ lastTime: number;
118
+ }
@@ -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;
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';
@@ -64,6 +65,7 @@ import {
64
65
  import {installServicesAsync} from './impl/InstallServices';
65
66
  import {instanceManager} from './impl/InstanceManager';
66
67
  import {HoistModel, ModelSelector, RefreshContextModel} from './model';
68
+ import ShortUniqueId from 'short-unique-id';
67
69
 
68
70
  export const MIN_HOIST_CORE_VERSION = '28.0';
69
71
 
@@ -84,6 +86,15 @@ declare const xhIsDevelopmentMode: boolean;
84
86
  * Available via import as `XH` - also installed as `window.XH` for troubleshooting purposes.
85
87
  */
86
88
  export class XHApi {
89
+ /** Unique id for this loaded instance of the app. Unique for every refresh of document. */
90
+ loadId: string = this.genLoadId();
91
+
92
+ /**
93
+ * Unique id for this browser tab/window on this domain.
94
+ * Corresponds to the scope of the built-in sessionStorage object.
95
+ */
96
+ tabId: string = this.genTabId();
97
+
87
98
  //--------------------------
88
99
  // Implementation Delegates
89
100
  //--------------------------
@@ -131,6 +142,7 @@ export class XHApi {
131
142
  alertBannerService: AlertBannerService;
132
143
  autoRefreshService: AutoRefreshService;
133
144
  changelogService: ChangelogService;
145
+ clientHealthService: ClientHealthService;
134
146
  configService: ConfigService;
135
147
  environmentService: EnvironmentService;
136
148
  fetchService: FetchService;
@@ -794,6 +806,19 @@ export class XHApi {
794
806
  private get acm(): AppContainerModel {
795
807
  return this.appContainerModel;
796
808
  }
809
+
810
+ private genLoadId(): string {
811
+ return new ShortUniqueId({length: 8}).rnd();
812
+ }
813
+
814
+ private genTabId(): string {
815
+ let ret = window.sessionStorage?.getItem('xhTabId');
816
+ if (!ret) {
817
+ ret = new ShortUniqueId({length: 8}).rnd();
818
+ window.sessionStorage?.setItem('xhTabId', ret);
819
+ }
820
+ return ret;
821
+ }
797
822
  }
798
823
 
799
824
  /** The app-wide singleton instance. */
@@ -6,7 +6,7 @@
6
6
  */
7
7
 
8
8
  import {RuleLike} from '@xh/hoist/data';
9
- import {ReactElement, ReactNode} from 'react';
9
+ import {MouseEvent, ReactElement, ReactNode} from 'react';
10
10
  import {LoadSpec} from '../load';
11
11
  import {Intent, PlainObject, Thunkable} from './Types';
12
12
 
@@ -284,7 +284,7 @@ export interface MenuItem {
284
284
  className?: string;
285
285
 
286
286
  /** Executed when the user clicks the menu item. */
287
- actionFn?: () => void;
287
+ actionFn?: (e: MouseEvent | PointerEvent) => void;
288
288
 
289
289
  /** Executed before the item is shown. Use to adjust properties dynamically. */
290
290
  prepareFn?: (me: MenuItem) => void;
@@ -6,7 +6,7 @@
6
6
  */
7
7
  import {HoistBase} from '@xh/hoist/core';
8
8
  import {Field, Store, FieldFilter, FieldType, genDisplayName, View} from '@xh/hoist/data';
9
- import {isEmpty} from 'lodash';
9
+ import {compact, isArray, isEmpty} from 'lodash';
10
10
  import {FieldFilterOperator} from './Types';
11
11
 
12
12
  export interface BaseFilterFieldSpecConfig {
@@ -72,7 +72,11 @@ export abstract class BaseFilterFieldSpec extends HoistBase {
72
72
  this.displayName = displayName ?? sourceField?.displayName ?? genDisplayName(field);
73
73
  this.ops = this.parseOperators(ops);
74
74
  this.forceSelection = forceSelection ?? false;
75
- this.values = values ?? (this.isBoolFieldType ? [true, false] : null);
75
+ this.values = isArray(values)
76
+ ? compact(values)
77
+ : this.isBoolFieldType
78
+ ? [true, false]
79
+ : null;
76
80
  this.hasExplicitValues = !isEmpty(this.values);
77
81
  this.enableValues = this.hasExplicitValues || (enableValues ?? this.isEnumerableByDefault);
78
82
  }
@@ -42,10 +42,24 @@ export const aboutDialog = hoistCmp.factory({
42
42
  item: model.getTable()
43
43
  }),
44
44
  toolbar(
45
+ button({
46
+ text: 'Send Client Health Report',
47
+ icon: Icon.health(),
48
+ omit: !XH.clientHealthService.enabled,
49
+ onClick: async () => {
50
+ try {
51
+ await XH.clientHealthService.sendReportAsync();
52
+ XH.successToast('Client health report successfully submitted.');
53
+ } catch (e) {
54
+ XH.handleException('Error sending client health report', e);
55
+ }
56
+ }
57
+ }),
45
58
  filler(),
46
59
  button({
47
60
  text: 'Close',
48
61
  intent: 'primary',
62
+ outlined: true,
49
63
  onClick: onClose
50
64
  })
51
65
  )
@@ -202,7 +202,7 @@ function parseMenuItems(items: MenuItemLike[]): ReactNode[] {
202
202
  icon: item.icon,
203
203
  intent: item.intent,
204
204
  className: item.className,
205
- onClick: actionFn ? () => wait().then(actionFn) : null, // do async to allow menu to close
205
+ onClick: actionFn ? e => wait().then(() => actionFn(e)) : null, // do async to allow menu to close
206
206
  disabled: item.disabled
207
207
  };
208
208
 
@@ -70,7 +70,7 @@ function parseItems(items: MenuItemLike[]): ReactNode[] {
70
70
  icon: item.icon,
71
71
  intent: item.intent,
72
72
  className: item.className,
73
- onClick: item.actionFn ? () => wait().then(item.actionFn) : null, // do async to allow menu to close
73
+ onClick: item.actionFn ? e => wait().then(() => item.actionFn(e)) : null, // do async to allow menu to close
74
74
  popoverProps: {usePortal: true},
75
75
  disabled: item.disabled,
76
76
  items