@xh/hoist 86.1.0 → 86.3.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 (104) hide show
  1. package/.gitattributes +4 -0
  2. package/CHANGELOG.md +104 -0
  3. package/admin/AdminJsonDisplay.ts +31 -0
  4. package/admin/App.scss +3 -11
  5. package/admin/AppModel.ts +3 -4
  6. package/admin/jsonsearch/impl/JsonSearchImplModel.ts +4 -4
  7. package/admin/tabs/activity/tracking/ActivityTrackingModel.ts +56 -6
  8. package/admin/tabs/activity/tracking/ActivityTrackingPanel.ts +9 -26
  9. package/admin/tabs/clients/ClientsModel.ts +4 -1
  10. package/admin/tabs/clients/activity/ClientDetailModel.ts +5 -1
  11. package/admin/tabs/cluster/instances/connpool/ConnPoolMonitorPanel.ts +3 -11
  12. package/admin/tabs/cluster/instances/services/DetailsPanel.ts +2 -13
  13. package/admin/tabs/cluster/objects/DetailPanel.ts +3 -12
  14. package/admin/tabs/userData/roles/RolePanel.ts +2 -2
  15. package/appcontainer/RouterModel.ts +6 -6
  16. package/build/types/admin/AdminJsonDisplay.d.ts +7 -0
  17. package/build/types/admin/AppModel.d.ts +3 -4
  18. package/build/types/admin/jsonsearch/impl/JsonSearchImplModel.d.ts +2 -2
  19. package/build/types/admin/tabs/activity/tracking/ActivityTrackingModel.d.ts +14 -3
  20. package/build/types/admin/tabs/clients/ClientsModel.d.ts +3 -0
  21. package/build/types/appcontainer/RouterModel.d.ts +5 -5
  22. package/build/types/cmp/grid/Grid.d.ts +8 -4
  23. package/build/types/cmp/grid/GridModel.d.ts +13 -2
  24. package/build/types/cmp/grid/Types.d.ts +8 -1
  25. package/build/types/cmp/grid/filter/GridFilterFieldSpec.d.ts +24 -6
  26. package/build/types/cmp/input/SegmentedControlOption.d.ts +12 -0
  27. package/build/types/core/HoistAppModel.d.ts +2 -3
  28. package/build/types/core/types/Types.d.ts +12 -0
  29. package/build/types/desktop/cmp/button/grid/ExpandToLevelButton.d.ts +2 -1
  30. package/build/types/desktop/cmp/filter/FilterChooser.d.ts +5 -0
  31. package/build/types/desktop/cmp/filter/PopoverFilterChooser.d.ts +3 -1
  32. package/build/types/desktop/cmp/grid/impl/colchooser/ColChooserModel.d.ts +2 -2
  33. package/build/types/desktop/cmp/grid/impl/filter/headerfilter/custom/CustomRowModel.d.ts +9 -1
  34. package/build/types/desktop/cmp/grid/impl/filter/headerfilter/custom/CustomTabModel.d.ts +8 -2
  35. package/build/types/desktop/cmp/input/CodeInput.d.ts +7 -0
  36. package/build/types/desktop/cmp/input/NumberInput.d.ts +1 -1
  37. package/build/types/desktop/cmp/input/Select.d.ts +8 -0
  38. package/build/types/desktop/cmp/input/impl/CalcWindowedMenuWidth.d.ts +20 -0
  39. package/build/types/desktop/cmp/rest/impl/RestFormModel.d.ts +1 -1
  40. package/build/types/format/FormatNumber.d.ts +11 -3
  41. package/build/types/icon/Icon.d.ts +4 -3
  42. package/build/types/kit/react-dropzone/index.d.ts +1 -0
  43. package/build/types/kit/swiper/index.d.ts +4 -3
  44. package/build/types/mobile/cmp/button/grid/ExpandToLevelButton.d.ts +2 -1
  45. package/build/types/mobile/cmp/grid/impl/ColChooserModel.d.ts +2 -2
  46. package/build/types/mobile/cmp/input/NumberInput.d.ts +1 -1
  47. package/build/types/mobile/cmp/input/Select.d.ts +8 -0
  48. package/cmp/grid/Grid.ts +27 -2
  49. package/cmp/grid/GridModel.ts +21 -8
  50. package/cmp/grid/Types.ts +9 -1
  51. package/cmp/grid/filter/GridFilterFieldSpec.ts +28 -5
  52. package/cmp/grid/impl/MenuSupport.ts +3 -5
  53. package/cmp/input/SegmentedControlOption.ts +14 -0
  54. package/cmp/store/impl/StoreFilterFieldImplModel.ts +5 -6
  55. package/core/HoistAppModel.ts +2 -3
  56. package/core/types/Types.ts +14 -5
  57. package/desktop/cmp/button/grid/ExpandToLevelButton.ts +4 -5
  58. package/desktop/cmp/filechooser/FileChooser.ts +6 -3
  59. package/desktop/cmp/filechooser/FileChooserModel.ts +12 -5
  60. package/desktop/cmp/filter/FilterChooser.scss +41 -0
  61. package/desktop/cmp/filter/FilterChooser.ts +147 -7
  62. package/desktop/cmp/filter/PopoverFilterChooser.ts +7 -84
  63. package/desktop/cmp/grid/editors/NumberEditor.ts +16 -17
  64. package/desktop/cmp/grid/editors/SelectEditor.ts +15 -10
  65. package/desktop/cmp/grid/editors/impl/InlineEditorModel.ts +6 -1
  66. package/desktop/cmp/grid/find/impl/GridFindFieldImplModel.ts +3 -5
  67. package/desktop/cmp/grid/impl/colchooser/ColChooserModel.ts +2 -2
  68. package/desktop/cmp/grid/impl/filter/GridFilterDialog.ts +2 -2
  69. package/desktop/cmp/grid/impl/filter/headerfilter/custom/CustomRow.ts +12 -12
  70. package/desktop/cmp/grid/impl/filter/headerfilter/custom/CustomRowModel.ts +18 -0
  71. package/desktop/cmp/grid/impl/filter/headerfilter/custom/CustomTab.scss +5 -0
  72. package/desktop/cmp/grid/impl/filter/headerfilter/custom/CustomTab.ts +13 -2
  73. package/desktop/cmp/grid/impl/filter/headerfilter/custom/CustomTabModel.ts +97 -14
  74. package/desktop/cmp/grid/impl/filter/headerfilter/values/ValuesTabModel.ts +46 -7
  75. package/desktop/cmp/input/CheckboxButton.ts +21 -1
  76. package/desktop/cmp/input/CodeInput.ts +20 -3
  77. package/desktop/cmp/input/NumberInput.ts +1 -1
  78. package/desktop/cmp/input/SegmentedControl.ts +7 -3
  79. package/desktop/cmp/input/Select.ts +37 -18
  80. package/desktop/cmp/input/impl/CalcWindowedMenuWidth.ts +106 -0
  81. package/desktop/cmp/rest/impl/RestFormField.ts +1 -1
  82. package/desktop/cmp/tab/Tabs.scss +72 -25
  83. package/docs/routing.md +4 -2
  84. package/docs/upgrade-notes/v86-upgrade-notes.md +73 -8
  85. package/format/FormatNumber.ts +69 -32
  86. package/icon/Icon.scss +13 -0
  87. package/icon/Icon.ts +4 -3
  88. package/icon/impl/IconHtml.ts +1 -1
  89. package/kit/react-dropzone/index.ts +1 -0
  90. package/mcp/README.md +12 -0
  91. package/mcp/data/ts-registry.spec.ts +156 -0
  92. package/mcp/data/ts-registry.ts +14 -7
  93. package/mcp/formatters/typescript.ts +7 -3
  94. package/mcp/util/paths.ts +21 -0
  95. package/mobile/cmp/button/grid/ExpandToLevelButton.ts +4 -5
  96. package/mobile/cmp/grid/impl/ColChooserModel.ts +2 -2
  97. package/mobile/cmp/input/CheckboxButton.ts +18 -1
  98. package/mobile/cmp/input/NumberInput.ts +1 -1
  99. package/mobile/cmp/input/SegmentedControl.ts +7 -3
  100. package/mobile/cmp/input/Select.ts +21 -15
  101. package/mobx/README.md +2 -1
  102. package/package.json +10 -10
  103. package/styles/vars.scss +15 -0
  104. package/desktop/cmp/filter/PopoverFilterChooser.scss +0 -42
package/.gitattributes ADDED
@@ -0,0 +1,4 @@
1
+ # Launcher scripts run under node via a `#!/usr/bin/env node` shebang; a CRLF
2
+ # shebang line breaks execution on Linux/macOS. Force LF so Windows checkouts
3
+ # don't rewrite them to CRLF (which otherwise shows them as perpetually modified).
4
+ bin/*.mjs text eol=lf
package/CHANGELOG.md CHANGED
@@ -1,5 +1,109 @@
1
1
  # Changelog
2
2
 
3
+ ## 86.3.0 - 2026-07-10
4
+
5
+ ### 🎁 New Features
6
+
7
+ * `Select` now accepts a `generateOptionFn` prop to resolve an option for a selected value that is
8
+ not present in the current options list (e.g. with `queryFn`-based selects or readonly forms),
9
+ ensuring such values render with their proper label rather than falling back to the raw value.
10
+ * `SegmentedControl` options (desktop and mobile) now accept a `testId`, emitted on the option's
11
+ rendered button as `data-testid` for E2E targeting. If an option omits its own `testId` but the
12
+ control has one, an id is auto-derived as `${controlTestId}-${value}` - restoring parity with
13
+ the legacy `ButtonGroupInput` test-hook pattern for apps migrating between the two.
14
+
15
+ ### 🐞 Bug Fixes
16
+
17
+ * Fixed grid columns configured as `hidden` becoming visible after being grouped and then
18
+ ungrouped. `GridModel` now re-asserts each column's configured visibility whenever `groupBy`
19
+ changes, keeping AG Grid's column state in sync with `columnState`.
20
+ * Fixed `StoreFilterField` and grid Find so an active quick-filter or find query no longer returns
21
+ different results when the grid's `groupBy` changes.
22
+ * Fixed inline grid cell editors to reliably commit their value when editing ends, including popup
23
+ editors (e.g. `textAreaEditor`) within a dialog, which previously dropped edits on Enter or
24
+ click-away.
25
+ * Fixed `Select` to correctly handle non-primitive (object) values: selected-option matching and
26
+ async query de-duplication now use deep equality, so object values no longer render as
27
+ `[object Object]` or collide with one another.
28
+ * Hardened the grid column filter's Custom tab against filters it previously mishandled -
29
+ multi-value clauses are now expanded into editable rows and recombined on commit, and filters it
30
+ cannot represent are left untouched rather than corrupted.
31
+ * Fixed `FilterChooser` popover mode (formerly `PopoverFilterChooser`) so its collapsed control no
32
+ longer disappears when opened - it now always occupies its place in the layout, so surrounding
33
+ elements no longer shift. Its clear and favorites controls also respond to a single click rather
34
+ than requiring the popover to be opened first. This mode is now enabled more naturally via
35
+ a new option `filterChooser({popover: true})`, deprecating `PopoverFilterChooser`, which remains
36
+ as a thin alias.
37
+ * Fixed "not a valid MIME type" console warnings from `FileChooser`. Accepted extensions are now
38
+ passed under a dummy MIME type key, silencing the warnings while continuing to filter selected
39
+ files by extension.
40
+
41
+ ### ⚙️ Typescript API Adjustments
42
+
43
+ * Retyped `GridModel.colChooserModel` as the new cross-platform `IColChooserModel` interface,
44
+ replacing the bare `HoistModel` type and exposing `isOpen`, `open()`, and `close()` directly.
45
+ * Added the exported `HoistRoute` type - Router5's `Route` extended with Hoist's `omit` key - and
46
+ retyped `HoistAppModel.getRoutes()` to return it, so declarative route exclusion (e.g.
47
+ `omit: !XH.getUser().isHoistAdmin`) now type-checks without a cast.
48
+
49
+ ### 🤖 AI Docs + Tooling
50
+
51
+ * Fixed the MCP server and `hoist-ts` CLI TypeScript symbol tools (`search`, `symbol`, `members`)
52
+ returning no results on Windows, where a path-separator mismatch left the symbol index empty.
53
+ Path handling is now normalized so the developer tools work on Windows as well as macOS/Linux.
54
+
55
+ ### 📚 Libraries
56
+
57
+ * @auth0/auth0-spa-js `2.21 → 2.23`
58
+ * @azure/msal-browser `5.14 → 5.16`
59
+ * swiper `12.1 -> 14.0`
60
+
61
+ ## 86.2.0 - 2026-06-25
62
+
63
+ ### 🎁 New Features
64
+
65
+ * `CodeInput` / `JsonInput` now auto-format content for display via their configured `formatter`,
66
+ controlled by a new `autoFormat` prop. On editable inputs, content is tidied automatically on blur
67
+ (never mid-edit) - users get clean, consistently-formatted JSON without reaching for the format
68
+ button. For `readonly` inputs it defaults to true, so applications can bind directly to raw source
69
+ values and drop their pre-formatting logic, simplifying call sites substantially.
70
+ * Grid column filter specs now support a `sortValue` config, letting the Values tab of the filter
71
+ dialog sort its entries the same way the underlying grid column sorts them. When not provided,
72
+ the column's own `sortValue` is used.
73
+ * `GridModel.levelLabels` now accepts a partial array covering only the top levels of a tree or
74
+ grouped grid. The "Expand to..." menu and `ExpandToLevelButton` offer one entry per labelled
75
+ level, so deeper, unlabelled levels (e.g. system-managed) are no longer required and are omitted
76
+ as expand-to targets - previously a too-short array disabled the feature entirely.
77
+ * Added a `lossless` option to `fmtQuantity` to compact values to millions / billions units only
78
+ when doing so loses no precision, rendering the full value otherwise (e.g. `7,100,100` stays
79
+ `7,100,100` rather than collapsing to `7.10m`).
80
+
81
+ ### 🐞 Bug Fixes
82
+
83
+ * Fixed grid `NumberEditor` to allow starting an edit by typing `-`, `+`, or `.` (e.g. to enter a
84
+ negative or decimal value), while reliably rejecting other non-numeric keypresses.
85
+ * `fmtNumber` now treats a `precision` of `null` as full, unrestricted precision rather than
86
+ `'auto'`, aligning a `NumberInput` with `precision: null` so its blurred display matches its
87
+ focused and committed (full-precision) value.
88
+ * Fixed inline grid editing not ending when clicking empty grid space to the right of the last
89
+ column or below the last row - such clicks now commit the active edit.
90
+ * Fixed `Icon.placeholder()` to render with the correct width.
91
+ * `CheckboxButton` no longer leaks `HoistInputProps` into underlying HTML `<button>` element.
92
+ * Fixed `Select` (and `SelectEditor`) dropdown menu sizing: windowed menus now auto-size to their
93
+ option labels instead of the control/cell width, and an explicit `menuWidth` is respected rather
94
+ than overridden by content auto-sizing.
95
+
96
+ ### ⚙️ Typescript API Adjustments
97
+
98
+ * `GridFilterFieldSpec.renderer` is now typed as a pure value transform (`GridFilterRenderer`),
99
+ rather than a `ColumnRenderer`. This more accurately represents the existing run-time limitation
100
+ that a complex column renderer would throw.
101
+
102
+ ### ✨ Styles
103
+
104
+ * Vertical (left/right) `TabContainer` switchers now render a modern rounded-pill treatment by
105
+ default, customizable via new `--xh-tab-switcher-vertical-*` CSS variables.
106
+
3
107
  ## 86.1.0 - 2026-06-22
4
108
 
5
109
  ### 🎁 New Features
@@ -0,0 +1,31 @@
1
+ /*
2
+ * This file belongs to Hoist, an application development toolkit
3
+ * developed by Extremely Heavy Industries (www.xh.io | info@xh.io)
4
+ *
5
+ * Copyright © 2026 Extremely Heavy Industries Inc.
6
+ */
7
+ import {hoistCmp} from '@xh/hoist/core';
8
+ import {jsonInput, JsonInputProps} from '@xh/hoist/desktop/cmp/input';
9
+ import {fmtJson, timestampReplacer} from '@xh/hoist/format';
10
+
11
+ /**
12
+ * Readonly `jsonInput` preconfigured for Admin Console stat / config displays - sized to fill its
13
+ * container, without line numbers or a fullscreen button, and rendering timestamp-suffixed fields
14
+ * as readable dates. Pass the data via `value` (any object) or `bind`, plus any additional
15
+ * `jsonInput` props (e.g. `enableSearch`) to extend or override the defaults.
16
+ */
17
+ export const adminJsonDisplay = hoistCmp.factory<JsonInputProps>({
18
+ displayName: 'AdminJsonDisplay',
19
+ render(props) {
20
+ return jsonInput({
21
+ readonly: true,
22
+ flex: 1,
23
+ width: '100%',
24
+ height: '100%',
25
+ showFullscreenButton: false,
26
+ lineNumbers: false,
27
+ formatter: v => fmtJson(v, {replacer: timestampReplacer()}),
28
+ ...props
29
+ });
30
+ }
31
+ });
package/admin/App.scss CHANGED
@@ -6,19 +6,11 @@
6
6
  */
7
7
 
8
8
  .xh-admin-app {
9
+ // The admin console uses left side-tab rails throughout - give them a consistent minimum width.
10
+ // All other vertical-switcher styling is inherited from the framework default.
9
11
  .xh-tab-switcher--left,
10
12
  .xh-tab-switcher--right {
11
- min-width: 160px;
12
- background-color: var(--xh-grid-bg-odd);
13
- border-right: var(--xh-border-solid);
14
-
15
- .bp6-tab {
16
- border-radius: 0 !important;
17
- }
18
-
19
- .xh-tab-switcher__tab {
20
- line-height: 40px;
21
- }
13
+ --xh-tab-switcher-vertical-min-width: 160px;
22
14
  }
23
15
 
24
16
  &__editor-dialog {
package/admin/AppModel.ts CHANGED
@@ -7,10 +7,9 @@
7
7
  import {GridModel} from '@xh/hoist/cmp/grid';
8
8
  import {TabConfig, TabContainerModel} from '@xh/hoist/cmp/tab';
9
9
  import {ViewManagerModel} from '@xh/hoist/cmp/viewmanager';
10
- import {HoistAppModel, InitContext, XH} from '@xh/hoist/core';
10
+ import {HoistAppModel, HoistRoute, InitContext, XH} from '@xh/hoist/core';
11
11
  import {Icon} from '@xh/hoist/icon';
12
12
  import {without} from 'lodash';
13
- import {Route} from 'router5';
14
13
  import {activityTrackingPanel} from './tabs/activity/tracking/ActivityTrackingPanel';
15
14
  import {clientsPanel} from './tabs/clients/ClientsPanel';
16
15
  import {monitorTab} from './tabs/monitor/MonitorTab';
@@ -49,7 +48,7 @@ export class AppModel extends HoistAppModel {
49
48
  await super.initAsync(ctx);
50
49
  }
51
50
 
52
- override getRoutes(): Route[] {
51
+ override getRoutes(): HoistRoute[] {
53
52
  return [
54
53
  {
55
54
  name: 'default',
@@ -66,7 +65,7 @@ export class AppModel extends HoistAppModel {
66
65
  return [];
67
66
  }
68
67
 
69
- getTabRoutes(): Route[] {
68
+ getTabRoutes(): HoistRoute[] {
70
69
  return [
71
70
  {
72
71
  name: 'general',
@@ -6,7 +6,7 @@
6
6
  */
7
7
 
8
8
  import {GridConfig, GridModel} from '@xh/hoist/cmp/grid';
9
- import {HoistModel, managed, TaskObserver, XH} from '@xh/hoist/core';
9
+ import {HoistModel, managed, PlainObject, TaskObserver, XH} from '@xh/hoist/core';
10
10
  import {action, bindable, makeObservable, observable} from '@xh/hoist/mobx';
11
11
  import {pluralize} from '@xh/hoist/utils/js';
12
12
  import {isEmpty, zipWith} from 'lodash';
@@ -32,7 +32,7 @@ export class JsonSearchImplModel extends HoistModel {
32
32
  @bindable path: string = '';
33
33
  @bindable readerContentType: 'document' | 'matches' = 'matches';
34
34
  @bindable pathFormat: 'XPath' | 'JSONPath' = 'XPath';
35
- @bindable readerContent: string = '';
35
+ @bindable readerContent: string | PlainObject[] = '';
36
36
  @bindable matchingNodeCount: number = 0;
37
37
 
38
38
  get subjectName(): string {
@@ -137,7 +137,7 @@ export class JsonSearchImplModel extends HoistModel {
137
137
  const {json} = this.selectedRecord.data;
138
138
 
139
139
  if (this.readerContentType === 'document') {
140
- this.readerContent = JSON.stringify(JSON.parse(json), null, 2);
140
+ this.readerContent = json;
141
141
  return;
142
142
  }
143
143
 
@@ -162,7 +162,7 @@ export class JsonSearchImplModel extends HoistModel {
162
162
  value
163
163
  };
164
164
  });
165
- this.readerContent = JSON.stringify(nodes, null, 2);
165
+ this.readerContent = nodes;
166
166
  });
167
167
  }
168
168
 
@@ -15,7 +15,17 @@ import {FilterChooserModel} from '@xh/hoist/cmp/filter';
15
15
  import {FormModel} from '@xh/hoist/cmp/form';
16
16
  import {ColumnRenderer, ColumnSpec, GridModel, TreeStyle} from '@xh/hoist/cmp/grid';
17
17
  import {GroupingChooserModel} from '@xh/hoist/cmp/grouping';
18
- import {HoistModel, LoadSpec, managed, PlainObject, XH} from '@xh/hoist/core';
18
+ import {
19
+ HoistModel,
20
+ LoadSpec,
21
+ LocalDateUnit,
22
+ managed,
23
+ PersistableState,
24
+ PersistenceProvider,
25
+ persistOptions,
26
+ PlainObject,
27
+ XH
28
+ } from '@xh/hoist/core';
19
29
  import {Cube, CubeFieldSpec, FieldSpec, ViewRowData} from '@xh/hoist/data';
20
30
  import {dateRenderer, dateTimeSecRenderer, numberRenderer} from '@xh/hoist/format';
21
31
  import {action, computed, makeObservable, observable} from '@xh/hoist/mobx';
@@ -24,6 +34,23 @@ import {compact, get, isEmpty, isEqual, round} from 'lodash';
24
34
  import moment from 'moment';
25
35
  import {ActivityDetailProvider} from './detail/ActivityDetailModel';
26
36
 
37
+ /**
38
+ * A relative time interval (e.g. 7 days) ending on the current app day. Drives the quick-select
39
+ * buttons and the persisted time period, which rolls forward to end on today on restore.
40
+ */
41
+ export interface ActivityInterval {
42
+ value: number;
43
+ unit: LocalDateUnit;
44
+ }
45
+
46
+ /** Quick-select intervals offered in the toolbar - labels are derived as `${value}${unit[0]}`. */
47
+ export const INTERVALS: ActivityInterval[] = [
48
+ {value: 6, unit: 'months'},
49
+ {value: 1, unit: 'months'},
50
+ {value: 7, unit: 'days'},
51
+ {value: 1, unit: 'days'}
52
+ ];
53
+
27
54
  export class ActivityTrackingModel extends HoistModel implements ActivityDetailProvider {
28
55
  override telemetryPrefix = 'xh.client.admin.tracking';
29
56
 
@@ -115,6 +142,7 @@ export class ActivityTrackingModel extends HoistModel implements ActivityDetailP
115
142
  this.markPersist('showFilterChooser');
116
143
 
117
144
  this.formModel = this.createQueryFormModel();
145
+ this.setupPeriodPersistence();
118
146
 
119
147
  this.dataFieldsEditorModel = new DataFieldsEditorModel(this);
120
148
  this.markPersist('dataFields');
@@ -203,17 +231,17 @@ export class ActivityTrackingModel extends HoistModel implements ActivityDetailP
203
231
  endDay.setValue(newEnd);
204
232
  }
205
233
 
206
- // Set the start date by taking the end date and pushing back [value] [units] - then pushing
234
+ // Set the start date by taking the end date and pushing back the interval - then pushing
207
235
  // forward one day as the day range query is inclusive.
208
- adjustStartDate(value, unit) {
236
+ adjustStartDate(interval: ActivityInterval) {
209
237
  this.formModel.setValues({
210
- startDay: this.endDay.subtract(value, unit).nextDay()
238
+ startDay: this.endDay.subtract(interval.value, interval.unit).nextDay()
211
239
  });
212
240
  }
213
241
 
214
- isInterval(value, unit) {
242
+ isInterval(interval: ActivityInterval) {
215
243
  const {startDay, endDay} = this.formModel.values;
216
- return startDay === endDay.subtract(value, unit).nextDay();
244
+ return startDay === endDay.subtract(interval.value, interval.unit).nextDay();
217
245
  }
218
246
 
219
247
  getDisplayName(fieldName: string) {
@@ -234,6 +262,28 @@ export class ActivityTrackingModel extends HoistModel implements ActivityDetailP
234
262
  });
235
263
  }
236
264
 
265
+ // Persist the selected time period as part of the active view, if endDay is currentDay
266
+ private setupPeriodPersistence() {
267
+ PersistenceProvider.create<ActivityInterval>({
268
+ persistOptions: persistOptions({path: 'queryPeriod'}, this.persistWith),
269
+ owner: this,
270
+ target: {
271
+ getPersistableState: () => {
272
+ const currDay = this.endDay === LocalDate.currentAppDay(),
273
+ interval = currDay ? INTERVALS.find(it => this.isInterval(it)) : null;
274
+ return new PersistableState(interval ?? null);
275
+ },
276
+ setPersistableState: ({value}) => {
277
+ if (value) {
278
+ const endDay = LocalDate.currentAppDay();
279
+ this.formModel.setValues({endDay});
280
+ this.adjustStartDate(value);
281
+ }
282
+ }
283
+ }
284
+ });
285
+ }
286
+
237
287
  private async loadGridAsync() {
238
288
  const {cube, gridModel, dimensions} = this,
239
289
  data = cube.executeQuery({
@@ -11,7 +11,7 @@ import {grid} from '@xh/hoist/cmp/grid';
11
11
  import {div, filler, hframe} from '@xh/hoist/cmp/layout';
12
12
  import {creates, hoistCmp} from '@xh/hoist/core';
13
13
  import {button, buttonGroup, colChooserButton, exportButton} from '@xh/hoist/desktop/cmp/button';
14
- import {popoverFilterChooser} from '@xh/hoist/desktop/cmp/filter';
14
+ import {filterChooser} from '@xh/hoist/desktop/cmp/filter';
15
15
  import {formField} from '@xh/hoist/desktop/cmp/form';
16
16
  import {groupingChooser} from '@xh/hoist/desktop/cmp/grouping';
17
17
  import {dateInput, DateInputProps, select} from '@xh/hoist/desktop/cmp/input';
@@ -20,7 +20,7 @@ import {toolbar, toolbarSep} from '@xh/hoist/desktop/cmp/toolbar';
20
20
  import {viewManager} from '@xh/hoist/desktop/cmp/viewmanager';
21
21
  import {Icon} from '@xh/hoist/icon';
22
22
  import {LocalDate} from '@xh/hoist/utils/datetime';
23
- import {ActivityTrackingModel} from './ActivityTrackingModel';
23
+ import {INTERVALS, ActivityTrackingModel} from './ActivityTrackingModel';
24
24
  import {aggChartPanel} from '@xh/hoist/admin/tabs/activity/tracking/chart/AggChartPanel';
25
25
  import {activityDetailView} from './detail/ActivityDetailView';
26
26
  import './ActivityTracking.scss';
@@ -75,32 +75,14 @@ const tbar = hoistCmp.factory<ActivityTrackingModel>(({model}) => {
75
75
  disabled: model.endDay >= LocalDate.currentAppDay()
76
76
  }),
77
77
  buttonGroup({
78
- items: [
78
+ items: INTERVALS.map(interval =>
79
79
  button({
80
- text: '6m',
81
- onClick: () => model.adjustStartDate(6, 'months'),
82
- active: model.isInterval(6, 'months'),
83
- ...dateBtn
84
- }),
85
- button({
86
- text: '1m',
87
- onClick: () => model.adjustStartDate(1, 'months'),
88
- active: model.isInterval(1, 'months'),
89
- ...dateBtn
90
- }),
91
- button({
92
- text: '7d',
93
- onClick: () => model.adjustStartDate(7, 'days'),
94
- active: model.isInterval(7, 'days'),
95
- ...dateBtn
96
- }),
97
- button({
98
- text: '1d',
99
- onClick: () => model.adjustStartDate(1, 'days'),
100
- active: model.isInterval(1, 'days'),
80
+ text: `${interval.value}${interval.unit[0]}`,
81
+ onClick: () => model.adjustStartDate(interval),
82
+ active: model.isInterval(interval),
101
83
  ...dateBtn
102
84
  })
103
- ]
85
+ )
104
86
  }),
105
87
  toolbarSep(),
106
88
  filterChooserToggleButton(),
@@ -137,7 +119,8 @@ const filterChooserToggleButton = hoistCmp.factory<ActivityTrackingModel>(({mode
137
119
  const filterBar = hoistCmp.factory<ActivityTrackingModel>(({model}) => {
138
120
  return model.showFilterChooser
139
121
  ? toolbar(
140
- popoverFilterChooser({
122
+ filterChooser({
123
+ popover: true,
141
124
  flex: 1,
142
125
  enableClear: true
143
126
  })
@@ -9,7 +9,7 @@ import {AppModel} from '@xh/hoist/admin/AppModel';
9
9
  import * as Col from '@xh/hoist/admin/columns';
10
10
  import {GridModel} from '@xh/hoist/cmp/grid';
11
11
  import {div, p} from '@xh/hoist/cmp/layout';
12
- import {LoadSpec, managed, XH} from '@xh/hoist/core';
12
+ import {LoadSpec, managed, persistOptions, XH} from '@xh/hoist/core';
13
13
  import {RecordActionSpec, StoreRecord} from '@xh/hoist/data';
14
14
  import {textInput} from '@xh/hoist/desktop/cmp/input';
15
15
  import {Icon} from '@xh/hoist/icon';
@@ -23,6 +23,8 @@ import {BaseAdminTabModel} from '@xh/hoist/admin/tabs/BaseAdminTabModel';
23
23
  export class ClientsModel extends BaseAdminTabModel {
24
24
  override telemetryPrefix = 'xh.client.admin.clients';
25
25
 
26
+ override persistWith = {localStorageKey: 'xhAdminClientsState'};
27
+
26
28
  @observable
27
29
  lastRefresh: number;
28
30
 
@@ -150,6 +152,7 @@ export class ClientsModel extends BaseAdminTabModel {
150
152
  return new GridModel({
151
153
  emptyText: 'No clients connected.',
152
154
  groupBy: this.groupBy,
155
+ persistWith: persistOptions(this.persistWith, {path: 'mainGrid'}),
153
156
  colChooserModel: true,
154
157
  enableExport: true,
155
158
  selModel: 'multiple',
@@ -6,7 +6,7 @@
6
6
  */
7
7
  import {ClientsModel} from '../ClientsModel';
8
8
  import {ColumnSpec} from '@xh/hoist/cmp/grid';
9
- import {HoistModel, LoadSpec, lookup, PlainObject, XH} from '@xh/hoist/core';
9
+ import {HoistModel, LoadSpec, lookup, persistOptions, PlainObject, XH} from '@xh/hoist/core';
10
10
  import {StoreRecord} from '@xh/hoist/data';
11
11
  import {bindable, computed, makeObservable} from '@xh/hoist/mobx';
12
12
  import {ReactNode} from 'react';
@@ -53,6 +53,10 @@ export class ClientDetailModel extends HoistModel implements ActivityDetailProvi
53
53
  override onLinked() {
54
54
  super.onLinked();
55
55
 
56
+ this.persistWith = persistOptions(this.clientsModel.persistWith, {
57
+ pathPrefix: 'activityGrid'
58
+ });
59
+
56
60
  this.addReaction(
57
61
  {
58
62
  track: () => this.clientsModel.gridModel.selectedRecord,
@@ -4,6 +4,7 @@
4
4
  *
5
5
  * Copyright © 2026 Extremely Heavy Industries Inc.
6
6
  */
7
+ import {adminJsonDisplay} from '@xh/hoist/admin/AdminJsonDisplay';
7
8
  import {AppModel} from '@xh/hoist/admin/AppModel';
8
9
  import {ConnPoolMonitorModel} from '@xh/hoist/admin/tabs/cluster/instances/connpool/ConnPoolMonitorModel';
9
10
  import {chart} from '@xh/hoist/cmp/chart';
@@ -12,7 +13,6 @@ import {filler, hframe, vframe} from '@xh/hoist/cmp/layout';
12
13
  import {creates, hoistCmp} from '@xh/hoist/core';
13
14
  import {button, exportButton} from '@xh/hoist/desktop/cmp/button';
14
15
  import {errorMessage} from '@xh/hoist/cmp/error';
15
- import {jsonInput} from '@xh/hoist/desktop/cmp/input';
16
16
  import {panel} from '@xh/hoist/desktop/cmp/panel';
17
17
  import {toolbar} from '@xh/hoist/desktop/cmp/toolbar';
18
18
  import {Icon} from '@xh/hoist/icon';
@@ -74,7 +74,7 @@ const bbar = hoistCmp.factory<ConnPoolMonitorModel>({
74
74
  });
75
75
 
76
76
  const poolConfigPanel = hoistCmp.factory<ConnPoolMonitorModel>({
77
- render({model}) {
77
+ render() {
78
78
  return panel({
79
79
  title: 'Pool Configuration',
80
80
  icon: Icon.info(),
@@ -84,15 +84,7 @@ const poolConfigPanel = hoistCmp.factory<ConnPoolMonitorModel>({
84
84
  defaultSize: 450,
85
85
  defaultCollapsed: true
86
86
  },
87
- item: jsonInput({
88
- readonly: true,
89
- height: '100%',
90
- width: '100%',
91
- enableSearch: true,
92
- showFullscreenButton: false,
93
- lineNumbers: false,
94
- value: JSON.stringify(model.poolConfiguration, null, 2)
95
- })
87
+ item: adminJsonDisplay({enableSearch: true, bind: 'poolConfiguration'})
96
88
  });
97
89
  }
98
90
  });
@@ -4,15 +4,14 @@
4
4
  *
5
5
  * Copyright © 2026 Extremely Heavy Industries Inc.
6
6
  */
7
+ import {adminJsonDisplay} from '@xh/hoist/admin/AdminJsonDisplay';
7
8
  import {DetailsModel} from '@xh/hoist/admin/tabs/cluster/instances/services/DetailsModel';
8
9
  import {placeholder} from '@xh/hoist/cmp/layout';
9
10
  import {creates, hoistCmp, XH} from '@xh/hoist/core';
10
11
  import {errorMessage} from '@xh/hoist/cmp/error';
11
12
  import {panel} from '@xh/hoist/desktop/cmp/panel';
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';
16
15
 
17
16
  export const detailsPanel = hoistCmp.factory({
18
17
  model: creates(DetailsModel),
@@ -50,16 +49,6 @@ const stats = hoistCmp.factory<DetailsModel>({
50
49
  ? []
51
50
  : [Icon.questionCircle(), 'This service does not report any admin stats.'])
52
51
  )
53
- : panel(
54
- jsonInput({
55
- readonly: true,
56
- width: '100%',
57
- height: '100%',
58
- enableSearch: true,
59
- showFullscreenButton: false,
60
- lineNumbers: false,
61
- value: fmtJson(stats, {replacer: timestampReplacer()})
62
- })
63
- );
52
+ : panel(adminJsonDisplay({enableSearch: true, bind: 'stats'}));
64
53
  }
65
54
  });
@@ -4,21 +4,20 @@
4
4
  *
5
5
  * Copyright © 2026 Extremely Heavy Industries Inc.
6
6
  */
7
+ import {adminJsonDisplay} from '@xh/hoist/admin/AdminJsonDisplay';
7
8
  import {grid} from '@xh/hoist/cmp/grid';
8
9
  import {placeholder} from '@xh/hoist/cmp/layout';
9
10
  import {creates, hoistCmp} from '@xh/hoist/core';
10
- import {jsonInput} from '@xh/hoist/desktop/cmp/input';
11
11
  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';
16
15
 
17
16
  export const detailPanel = hoistCmp.factory({
18
17
  model: creates(DetailModel),
19
18
 
20
19
  render({model}) {
21
- const {instanceName, selectedAdminStats, objectName, objectType} = model;
20
+ const {instanceName, objectName, objectType} = model;
22
21
  if (!objectName) return placeholder(Icon.grip(), 'Select an object to view details...');
23
22
 
24
23
  return panel({
@@ -36,15 +35,7 @@ export const detailPanel = hoistCmp.factory({
36
35
  defaultSize: '80%',
37
36
  collapsible: false
38
37
  },
39
- item: jsonInput({
40
- readonly: true,
41
- flex: 1,
42
- width: '100%',
43
- height: '100%',
44
- showFullscreenButton: false,
45
- lineNumbers: false,
46
- value: fmtJson(selectedAdminStats, {replacer: timestampReplacer()})
47
- })
38
+ item: adminJsonDisplay({bind: 'selectedAdminStats'})
48
39
  })
49
40
  ]
50
41
  });
@@ -10,7 +10,7 @@ import {fragment, hframe, vframe} from '@xh/hoist/cmp/layout';
10
10
  import {creates, hoistCmp} from '@xh/hoist/core';
11
11
  import {button, colChooserButton} from '@xh/hoist/desktop/cmp/button';
12
12
  import {errorMessage} from '@xh/hoist/cmp/error';
13
- import {popoverFilterChooser} from '@xh/hoist/desktop/cmp/filter';
13
+ import {filterChooser} from '@xh/hoist/desktop/cmp/filter';
14
14
  import {switchInput} from '@xh/hoist/desktop/cmp/input';
15
15
  import {panel} from '@xh/hoist/desktop/cmp/panel';
16
16
  import {recordActionBar} from '@xh/hoist/desktop/cmp/record';
@@ -44,7 +44,7 @@ export const rolePanel = hoistCmp.factory({
44
44
  selModel: gridModel.selModel
45
45
  }),
46
46
  '-',
47
- popoverFilterChooser({flex: 1}),
47
+ filterChooser({popover: true, flex: 1}),
48
48
  '-',
49
49
  switchInput({
50
50
  bind: 'showInGroups',
@@ -4,7 +4,7 @@
4
4
  *
5
5
  * Copyright © 2026 Extremely Heavy Industries Inc.
6
6
  */
7
- import {HoistModel} from '../core';
7
+ import {HoistModel, HoistRoute} from '../core';
8
8
  import {action, observable, makeObservable} from '@xh/hoist/mobx';
9
9
  import {mergeDeep} from '@xh/hoist/utils/js';
10
10
  import {isOmitted} from '@xh/hoist/utils/impl';
@@ -38,11 +38,11 @@ export class RouterModel extends HoistModel {
38
38
  /**
39
39
  * Add routes to the router.
40
40
  *
41
- * @param routes - collection of router5 route spec.
42
- * This method supports an additional keyword 'omit' on each spec, in order to allow declarative
43
- * exclusion. Otherwise these are Router5 configs to be passed directly to the Router5 API.
41
+ * @param routes - collection of {@link HoistRoute} specs. In addition to the standard Router5
42
+ * route config, each spec supports an `omit` keyword to allow declarative exclusion.
43
+ * Otherwise these are Router5 configs to be passed directly to the Router5 API.
44
44
  */
45
- addRoutes(routes: object[]) {
45
+ addRoutes(routes: HoistRoute[]) {
46
46
  this.router.add(this.preprocessRoutes(routes));
47
47
  }
48
48
 
@@ -100,7 +100,7 @@ export class RouterModel extends HoistModel {
100
100
  return ret;
101
101
  }
102
102
 
103
- private preprocessRoutes(routes) {
103
+ private preprocessRoutes(routes: HoistRoute[]): HoistRoute[] {
104
104
  const ret = routes.filter(r => !isOmitted(r));
105
105
  ret.forEach(r => {
106
106
  if (r.children) r.children = this.preprocessRoutes(r.children);
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Readonly `jsonInput` preconfigured for Admin Console stat / config displays - sized to fill its
3
+ * container, without line numbers or a fullscreen button, and rendering timestamp-suffixed fields
4
+ * as readable dates. Pass the data via `value` (any object) or `bind`, plus any additional
5
+ * `jsonInput` props (e.g. `enableSearch`) to extend or override the defaults.
6
+ */
7
+ export declare const adminJsonDisplay: import("@xh/hoist/core").ElementFactory<import("@xh/hoist/desktop/cmp/input").CodeInputProps>;
@@ -1,16 +1,15 @@
1
1
  import { TabConfig, TabContainerModel } from '@xh/hoist/cmp/tab';
2
2
  import { ViewManagerModel } from '@xh/hoist/cmp/viewmanager';
3
- import { HoistAppModel, InitContext } from '@xh/hoist/core';
4
- import { Route } from 'router5';
3
+ import { HoistAppModel, HoistRoute, InitContext } from '@xh/hoist/core';
5
4
  export declare class AppModel extends HoistAppModel {
6
5
  tabModel: TabContainerModel;
7
6
  viewManagerModels: Record<string, ViewManagerModel>;
8
7
  static get readonly(): boolean;
9
8
  constructor();
10
9
  initAsync(ctx: InitContext): Promise<void>;
11
- getRoutes(): Route[];
10
+ getRoutes(): HoistRoute[];
12
11
  getAppMenuButtonExtraItems(): any[];
13
- getTabRoutes(): Route[];
12
+ getTabRoutes(): HoistRoute[];
14
13
  createTabs(): TabConfig[];
15
14
  /** Open the primary business-facing application, typically 'app'. */
16
15
  openPrimaryApp(): void;