@xh/hoist 72.2.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 (59) hide show
  1. package/CHANGELOG.md +40 -2
  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/InstancesTabModel.ts +4 -3
  5. package/admin/tabs/cluster/instances/logs/LogDisplay.ts +12 -14
  6. package/admin/tabs/cluster/instances/logs/LogDisplayModel.ts +0 -2
  7. package/admin/tabs/cluster/instances/logs/LogViewer.ts +6 -5
  8. package/admin/tabs/cluster/instances/logs/LogViewerModel.ts +8 -1
  9. package/admin/tabs/cluster/instances/memory/MemoryMonitorModel.ts +1 -0
  10. package/admin/tabs/cluster/instances/services/DetailsModel.ts +1 -2
  11. package/admin/tabs/cluster/instances/services/DetailsPanel.ts +20 -14
  12. package/admin/tabs/cluster/instances/services/ServiceModel.ts +14 -6
  13. package/admin/tabs/cluster/instances/services/ServicePanel.ts +9 -10
  14. package/admin/tabs/cluster/instances/websocket/WebSocketColumns.ts +9 -0
  15. package/admin/tabs/cluster/instances/websocket/WebSocketModel.ts +2 -1
  16. package/admin/tabs/cluster/objects/DetailModel.ts +4 -40
  17. package/admin/tabs/cluster/objects/DetailPanel.ts +2 -1
  18. package/admin/tabs/userData/roles/RoleModel.ts +1 -1
  19. package/appcontainer/AppContainerModel.ts +2 -0
  20. package/appcontainer/AppStateModel.ts +46 -9
  21. package/build/types/admin/tabs/activity/tracking/ActivityTrackingModel.d.ts +4 -1
  22. package/build/types/admin/tabs/cluster/instances/BaseInstanceModel.d.ts +1 -3
  23. package/build/types/admin/tabs/cluster/instances/services/DetailsModel.d.ts +2 -3
  24. package/build/types/admin/tabs/cluster/instances/websocket/WebSocketColumns.d.ts +1 -0
  25. package/build/types/admin/tabs/cluster/objects/DetailModel.d.ts +1 -3
  26. package/build/types/appcontainer/AppStateModel.d.ts +7 -1
  27. package/build/types/cmp/viewmanager/ViewManagerModel.d.ts +7 -0
  28. package/build/types/core/XH.d.ts +11 -1
  29. package/build/types/desktop/cmp/tab/TabSwitcher.d.ts +1 -1
  30. package/build/types/format/FormatDate.d.ts +22 -1
  31. package/build/types/format/FormatMisc.d.ts +3 -2
  32. package/build/types/security/BaseOAuthClient.d.ts +6 -7
  33. package/build/types/security/Types.d.ts +32 -5
  34. package/build/types/security/msal/MsalClient.d.ts +14 -1
  35. package/build/types/svc/ClientHealthService.d.ts +58 -0
  36. package/build/types/svc/TrackService.d.ts +19 -1
  37. package/build/types/svc/index.d.ts +1 -0
  38. package/build/types/utils/js/index.d.ts +0 -1
  39. package/cmp/viewmanager/ViewManagerModel.ts +10 -1
  40. package/core/XH.ts +26 -1
  41. package/data/Store.ts +3 -0
  42. package/desktop/cmp/grid/editors/BooleanEditor.ts +15 -3
  43. package/desktop/cmp/tab/TabSwitcher.ts +1 -1
  44. package/desktop/cmp/viewmanager/ViewMenu.ts +11 -9
  45. package/format/FormatDate.ts +45 -3
  46. package/format/FormatMisc.ts +6 -4
  47. package/package.json +2 -2
  48. package/security/BaseOAuthClient.ts +12 -10
  49. package/security/Types.ts +35 -6
  50. package/security/msal/MsalClient.ts +126 -21
  51. package/svc/ClientHealthService.ts +165 -0
  52. package/svc/FetchService.ts +3 -2
  53. package/svc/TrackService.ts +27 -5
  54. package/svc/WebSocketService.ts +1 -2
  55. package/svc/index.ts +1 -0
  56. package/tsconfig.tsbuildinfo +1 -1
  57. package/utils/js/index.ts +0 -1
  58. package/build/types/utils/js/BrowserUtils.d.ts +0 -4
  59. package/utils/js/BrowserUtils.ts +0 -52
package/CHANGELOG.md CHANGED
@@ -1,8 +1,46 @@
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
+
15
+ ## v72.3.0 - 2025-04-08
16
+
17
+ ### 🎁 New Features
18
+
19
+ * Added support for posting a "Client Health Report" track message on a configurable interval. This
20
+ message will include basic client information, and can be extended to include any other desired
21
+ data via `XH.clientHealthService.addSource()`. Enable by updating your app's
22
+ `xhActivityTrackingConfig` to include `clientHealthReport: {intervalMins: XXXX}`.
23
+ * Enabled opt-in support for telemetry in `MsalClient`, leveraging hooks built-in to MSAL to collect
24
+ timing and success/failure count for all events emitted by the library.
25
+ * Added the reported client app version as a column in the Admin Console WebSockets tab.
26
+
27
+ ### 🐞 Bug Fixes
28
+
29
+ * Improved fetch request tracking to include time spent loading headers as specified by application.
30
+
31
+ ### ⚙️ Technical
32
+
33
+ * Update shape of returned `BrowserUtils.getClientDeviceInfo()` to nest several properties under new
34
+ top-level `window` key and report JS heap size / usage values under the `memory` block in MB.
35
+
36
+ ### 📚 Libraries
37
+
38
+ * @azure/msal-browser `3.28 → 4.8.0`
39
+
3
40
  ## v72.2.0 - 2025-03-13
4
41
 
5
42
  ### 🎁 New Features
43
+
6
44
  * Modified `TabContainerModel` to make more methods `protected`, improving extensibility for
7
45
  advanced use-cases.
8
46
  * Enhanced `XH.reloadApp` with new argument to clear query parameters before loading.
@@ -15,8 +53,8 @@
15
53
 
16
54
  ### 🐞 Bug Fixes
17
55
 
18
- * Prevent native browser context menu on Dash Canvas surfaces. It can hide the Dash Canvas custom
19
- context menu when an app's `showBrowserContextMenu` flag is `true`.
56
+ * Prevented native browser context menu from showing on `DashCanvas` surfaces and obscuring the
57
+ `DashCanvas` custom context menu.
20
58
 
21
59
  ## v72.1.0 - 2025-02-13
22
60
 
@@ -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
  }
@@ -149,7 +149,7 @@ export class InstancesTabModel extends HoistModel {
149
149
  },
150
150
  {
151
151
  ...usedHeapMb,
152
- headerName: 'Heap (MB)'
152
+ headerName: 'Heap (mb)'
153
153
  },
154
154
  {
155
155
  ...usedPctMax,
@@ -214,11 +214,12 @@ export class InstancesTabModel extends HoistModel {
214
214
  private async shutdownInstanceAsync(instance: PlainObject) {
215
215
  if (
216
216
  !(await XH.confirm({
217
- message: `Are you SURE you want to shutdown instance ${instance.name}?`,
217
+ message: `Are you sure you wish to immediately terminate instance ${instance.name}?`,
218
218
  confirmProps: {
219
219
  icon: Icon.skull(),
220
- text: 'Shutdown Now',
220
+ text: 'Yes, kill the instance',
221
221
  intent: 'danger',
222
+ outlined: true,
222
223
  autoFocus: false
223
224
  }
224
225
  }))
@@ -6,12 +6,12 @@
6
6
  */
7
7
  import {clock} from '@xh/hoist/cmp/clock';
8
8
  import {grid} from '@xh/hoist/cmp/grid';
9
- import {code, filler, fragment, hspacer, label, placeholder} from '@xh/hoist/cmp/layout';
9
+ import {filler, fragment, hspacer, label, placeholder} from '@xh/hoist/cmp/layout';
10
+ import {loadingIndicator} from '@xh/hoist/cmp/loadingindicator';
10
11
  import {hoistCmp, uses, XH} from '@xh/hoist/core';
11
12
  import {button, modalToggleButton} from '@xh/hoist/desktop/cmp/button';
12
13
  import {gridFindField} from '@xh/hoist/desktop/cmp/grid';
13
14
  import {numberInput, switchInput, textInput} from '@xh/hoist/desktop/cmp/input';
14
- import {loadingIndicator} from '@xh/hoist/cmp/loadingindicator';
15
15
  import {panel} from '@xh/hoist/desktop/cmp/panel';
16
16
  import {toolbar, toolbarSep} from '@xh/hoist/desktop/cmp/toolbar';
17
17
  import {Icon} from '@xh/hoist/icon';
@@ -48,7 +48,7 @@ const tbar = hoistCmp.factory<LogDisplayModel>(({model}) => {
48
48
  numberInput({
49
49
  bind: 'startLine',
50
50
  min: 1,
51
- width: 80,
51
+ width: 70,
52
52
  disabled: model.tail,
53
53
  displayWithCommas: true
54
54
  }),
@@ -57,13 +57,13 @@ const tbar = hoistCmp.factory<LogDisplayModel>(({model}) => {
57
57
  numberInput({
58
58
  bind: 'maxLines',
59
59
  min: 1,
60
- width: 80,
60
+ width: 70,
61
61
  displayWithCommas: true
62
62
  }),
63
63
  '-',
64
64
  textInput({
65
65
  bind: 'pattern',
66
- placeholder: 'Filter',
66
+ placeholder: 'Filter lines...',
67
67
  leftIcon: Icon.filter(),
68
68
  flex: 1,
69
69
  rightElement: fragment(
@@ -85,7 +85,7 @@ const tbar = hoistCmp.factory<LogDisplayModel>(({model}) => {
85
85
  })
86
86
  )
87
87
  }),
88
- gridFindField({flex: 1}),
88
+ gridFindField({flex: 1, placeholder: 'Find lines...'}),
89
89
  '-',
90
90
  switchInput({
91
91
  bind: 'tail',
@@ -123,16 +123,14 @@ const bbar = hoistCmp.factory<LogDisplayModel>({
123
123
  }),
124
124
  filler(),
125
125
  Icon.clock(),
126
- code(
127
- clock({
128
- timezone: zone,
129
- format: 'HH:mm',
130
- suffix: fmtTimeZone(zone, offset)
131
- })
132
- ),
126
+ clock({
127
+ timezone: zone,
128
+ format: 'HH:mm',
129
+ suffix: fmtTimeZone(zone, offset)
130
+ }),
133
131
  fragment({
134
132
  omit: !logRootPath,
135
- items: [toolbarSep(), Icon.folder(), code(logRootPath)]
133
+ items: [toolbarSep(), Icon.folder({className: 'xh-margin-right'}), logRootPath]
136
134
  })
137
135
  );
138
136
  }
@@ -117,8 +117,6 @@ export class LogDisplayModel extends HoistModel {
117
117
  },
118
118
  loadSpec
119
119
  });
120
- // Backward compatibility for Hoist Core < v22, which returned exception in-band
121
- if (!response.success) throw XH.exception(response.exception);
122
120
  this.updateGridData(response.content);
123
121
  }
124
122
 
@@ -5,8 +5,8 @@
5
5
  * Copyright © 2025 Extremely Heavy Industries Inc.
6
6
  */
7
7
  import {logLevelDialog} from '@xh/hoist/admin/tabs/cluster/instances/logs/levels/LogLevelDialog';
8
- import {grid} from '@xh/hoist/cmp/grid';
9
- import {hframe} from '@xh/hoist/cmp/layout';
8
+ import {grid, gridCountLabel} from '@xh/hoist/cmp/grid';
9
+ import {filler, hframe} from '@xh/hoist/cmp/layout';
10
10
  import {storeFilterField} from '@xh/hoist/cmp/store';
11
11
  import {creates, hoistCmp} from '@xh/hoist/core';
12
12
  import {errorMessage} from '@xh/hoist/cmp/error';
@@ -37,9 +37,8 @@ export const logViewer = hoistCmp.factory({
37
37
  side: 'left',
38
38
  defaultSize: 380
39
39
  },
40
- item: grid(),
41
- bbar: [
42
- storeFilterField({flex: 1}),
40
+ tbar: [
41
+ storeFilterField({flex: 1, placeholder: 'Filter files...'}),
43
42
  select({
44
43
  leftIcon: Icon.server(),
45
44
  bind: 'instanceOnly',
@@ -53,6 +52,8 @@ export const logViewer = hoistCmp.factory({
53
52
  ]
54
53
  })
55
54
  ],
55
+ item: grid(),
56
+ bbar: [filler(), gridCountLabel({unit: 'log file'})],
56
57
  mask: 'onLoad'
57
58
  }),
58
59
  logDisplay(),
@@ -13,6 +13,7 @@ import {RecordActionSpec} from '@xh/hoist/data';
13
13
  import {compactDateRenderer, fmtNumber} from '@xh/hoist/format';
14
14
  import {Icon} from '@xh/hoist/icon';
15
15
  import {bindable, makeObservable, observable} from '@xh/hoist/mobx';
16
+ import {pluralize} from '@xh/hoist/utils/js';
16
17
  import download from 'downloadjs';
17
18
  import {LogDisplayModel} from './LogDisplayModel';
18
19
 
@@ -119,7 +120,13 @@ export class LogViewerModel extends BaseInstanceModel {
119
120
  if (!count) return;
120
121
 
121
122
  const confirmed = await XH.confirm({
122
- message: `Delete ${count} log files on the server? This cannot be undone.`
123
+ message: `Are you sure you want to delete ${pluralize('log file', count, true)}? This cannot be undone.`,
124
+ confirmProps: {
125
+ text: `Yes, delete the ${pluralize('file', count)}`,
126
+ intent: 'danger',
127
+ outlined: true,
128
+ autoFocus: false
129
+ }
123
130
  });
124
131
  if (!confirmed) return;
125
132
 
@@ -131,6 +131,7 @@ export class MemoryMonitorModel extends BaseInstanceModel {
131
131
  },
132
132
  {
133
133
  groupId: 'GC',
134
+ headerName: 'Garbage Collection',
134
135
  headerAlign: 'center',
135
136
  children: [collectionCount, avgCollectionTime, pctCollectionTime]
136
137
  }
@@ -5,7 +5,6 @@
5
5
  * Copyright © 2025 Extremely Heavy Industries Inc.
6
6
  */
7
7
  import {HoistModel, LoadSpec, lookup, PlainObject, XH} from '@xh/hoist/core';
8
- import {StoreRecord} from '@xh/hoist/data';
9
8
  import {bindable} from '@xh/hoist/mobx';
10
9
  import {ServiceModel} from './ServiceModel';
11
10
 
@@ -14,7 +13,7 @@ export class DetailsModel extends HoistModel {
14
13
  parent: ServiceModel;
15
14
 
16
15
  @bindable.ref
17
- svcName: StoreRecord;
16
+ svcName: String;
18
17
 
19
18
  @bindable.ref
20
19
  stats: PlainObject;
@@ -11,6 +11,8 @@ import {errorMessage} from '@xh/hoist/cmp/error';
11
11
  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
+ import {isEmpty} from 'lodash';
15
+ import {fmtJson, timestampReplacer} from '@xh/hoist/format';
14
16
 
15
17
  export const detailsPanel = hoistCmp.factory({
16
18
  model: creates(DetailsModel),
@@ -18,7 +20,7 @@ export const detailsPanel = hoistCmp.factory({
18
20
  render({model}) {
19
21
  const {svcName} = model;
20
22
  return panel({
21
- title: svcName ? `Stats: ${svcName}` : 'Stats',
23
+ title: svcName ?? 'Stats',
22
24
  mask: 'onLoad',
23
25
  icon: Icon.info(),
24
26
  compactHeader: true,
@@ -42,18 +44,22 @@ const stats = hoistCmp.factory<DetailsModel>({
42
44
  });
43
45
  }
44
46
 
45
- if (stats == null) return null;
46
-
47
- return panel(
48
- jsonInput({
49
- readonly: true,
50
- width: '100%',
51
- height: '100%',
52
- enableSearch: true,
53
- showFullscreenButton: false,
54
- editorProps: {lineNumbers: false},
55
- value: model.parent.fmtStats(stats)
56
- })
57
- );
47
+ return isEmpty(stats)
48
+ ? placeholder(
49
+ ...(loadModel.isPending
50
+ ? []
51
+ : [Icon.questionCircle(), 'This service does not report any admin stats.'])
52
+ )
53
+ : panel(
54
+ jsonInput({
55
+ readonly: true,
56
+ width: '100%',
57
+ height: '100%',
58
+ enableSearch: true,
59
+ showFullscreenButton: false,
60
+ editorProps: {lineNumbers: false},
61
+ value: fmtJson(stats, {replacer: timestampReplacer()})
62
+ })
63
+ );
58
64
  }
59
65
  });
@@ -10,7 +10,7 @@ import {timestampNoYear} from '@xh/hoist/admin/columns';
10
10
  import {BaseInstanceModel} from '@xh/hoist/admin/tabs/cluster/instances/BaseInstanceModel';
11
11
  import {GridModel} from '@xh/hoist/cmp/grid';
12
12
  import {br, fragment} from '@xh/hoist/cmp/layout';
13
- import {LoadSpec, managed, XH} from '@xh/hoist/core';
13
+ import {LoadSpec, managed, PlainObject, XH} from '@xh/hoist/core';
14
14
  import {FilterLike, FilterTestFn, RecordActionSpec} from '@xh/hoist/data';
15
15
  import {Icon} from '@xh/hoist/icon';
16
16
  import {bindable, makeObservable} from '@xh/hoist/mobx';
@@ -52,15 +52,16 @@ export class ServiceModel extends BaseInstanceModel {
52
52
  selModel: 'multiple',
53
53
  enableExport: true,
54
54
  exportOptions: {filename: exportFilenameWithDate('services')},
55
+ groupBy: 'provider',
55
56
  store: {
56
57
  idSpec: 'name',
57
58
  processRawData: this.processRawData,
58
59
  fields: [
59
60
  {name: 'provider', type: 'string'},
60
61
  {name: 'name', type: 'string'},
61
- {name: 'displayName', type: 'string'},
62
+ {name: 'displayName', type: 'string', displayName: 'Service'},
62
63
  {name: 'initializedDate', type: 'date', displayName: 'Initialized'},
63
- {name: 'lastCachesCleared', type: 'date', displayName: 'Last Cleared'}
64
+ {name: 'lastCachesCleared', type: 'date', displayName: 'Caches Last Cleared'}
64
65
  ]
65
66
  },
66
67
  sortBy: ['provider', 'displayName'],
@@ -81,6 +82,7 @@ export class ServiceModel extends BaseInstanceModel {
81
82
  constructor() {
82
83
  super();
83
84
  makeObservable(this);
85
+
84
86
  this.addReaction({
85
87
  track: () => [this.textFilter, this.typeFilter],
86
88
  run: this.applyFilters,
@@ -91,6 +93,7 @@ export class ServiceModel extends BaseInstanceModel {
91
93
  async clearCachesAsync(entireCluster: boolean) {
92
94
  const {gridModel, instanceName, loadModel} = this,
93
95
  {selectedRecords} = gridModel;
96
+
94
97
  if (isEmpty(selectedRecords)) return;
95
98
 
96
99
  const cacheStr =
@@ -131,19 +134,24 @@ export class ServiceModel extends BaseInstanceModel {
131
134
  }
132
135
 
133
136
  override async doLoadAsync(loadSpec: LoadSpec) {
137
+ const {gridModel, instanceName: instance} = this;
134
138
  try {
135
139
  const data = await XH.fetchJson({
136
140
  url: 'serviceManagerAdmin/listServices',
137
- params: {instance: this.instanceName},
141
+ params: {instance},
138
142
  loadSpec
139
143
  });
140
- return this.gridModel.loadData(data);
144
+
145
+ if (!loadSpec.isStale) {
146
+ gridModel.loadData(data);
147
+ gridModel.preSelectFirstAsync();
148
+ }
141
149
  } catch (e) {
142
150
  this.handleLoadException(e, loadSpec);
143
151
  }
144
152
  }
145
153
 
146
- private processRawData(r) {
154
+ private processRawData(r: PlainObject) {
147
155
  const provider = r.name && r.name.startsWith('hoistCore') ? 'Hoist' : 'App';
148
156
  const displayName = lowerFirst(r.name.replace('hoistCore', ''));
149
157
  return {provider, displayName, ...r};
@@ -9,11 +9,11 @@ import {grid, gridCountLabel} from '@xh/hoist/cmp/grid';
9
9
  import {filler, hframe} from '@xh/hoist/cmp/layout';
10
10
  import {storeFilterField} from '@xh/hoist/cmp/store';
11
11
  import {creates, hoistCmp, uses} from '@xh/hoist/core';
12
- import {exportButton} from '@xh/hoist/desktop/cmp/button';
12
+ import {button, exportButton} from '@xh/hoist/desktop/cmp/button';
13
+ import {buttonGroupInput} from '@xh/hoist/desktop/cmp/input';
13
14
  import {panel} from '@xh/hoist/desktop/cmp/panel';
14
15
  import {recordActionBar} from '@xh/hoist/desktop/cmp/record';
15
16
  import {toolbar} from '@xh/hoist/desktop/cmp/toolbar';
16
- import {select} from '@xh/hoist/desktop/cmp/input';
17
17
  import {ServiceModel} from './ServiceModel';
18
18
 
19
19
  export const servicePanel = hoistCmp.factory({
@@ -52,15 +52,14 @@ const bbar = hoistCmp.factory({
52
52
  filler(),
53
53
  gridCountLabel({unit: 'service'}),
54
54
  '-',
55
- select({
56
- options: [
57
- {value: 'all', label: 'All'},
58
- {value: 'app', label: 'App Only'},
59
- {value: 'hoist', label: 'Hoist Only'}
60
- ],
61
- width: 125,
55
+ buttonGroupInput({
62
56
  bind: 'typeFilter',
63
- hideDropdownIndicator: true
57
+ outlined: true,
58
+ items: [
59
+ button({value: 'all', text: 'All'}),
60
+ button({value: 'app', text: 'App'}),
61
+ button({value: 'hoist', text: 'Hoist'})
62
+ ]
64
63
  }),
65
64
  storeFilterField({
66
65
  matchMode: 'any',
@@ -72,3 +72,12 @@ export const lastReceivedTime: ColumnSpec = {
72
72
  ...Col.compactDate,
73
73
  width: 140
74
74
  };
75
+
76
+ export const clientAppVersion: ColumnSpec = {
77
+ field: {
78
+ name: 'clientAppVersion',
79
+ type: 'string',
80
+ displayName: 'Client Version'
81
+ },
82
+ width: 120
83
+ };
@@ -77,7 +77,8 @@ export class WebSocketModel extends BaseInstanceModel {
77
77
  WSCol.sentMessageCount,
78
78
  WSCol.lastSentTime,
79
79
  WSCol.receivedMessageCount,
80
- WSCol.lastReceivedTime
80
+ WSCol.lastReceivedTime,
81
+ WSCol.clientAppVersion
81
82
  ]
82
83
  });
83
84
 
@@ -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
  ]
@@ -275,7 +275,7 @@ export class RoleModel extends HoistModel {
275
275
  return new GridModel({
276
276
  treeMode: true,
277
277
  treeStyle: TreeStyle.HIGHLIGHTS_AND_BORDERS,
278
- autosizeOptions: {mode: 'managed'},
278
+ autosizeOptions: {mode: 'managed', includeCollapsedChildren: true},
279
279
  selModel: 'multiple',
280
280
  emptyText: 'No roles found.',
281
281
  colChooserModel: true,
@@ -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,