@xh/hoist 71.0.0-SNAPSHOT.1735844948971 → 71.0.0-SNAPSHOT.1736119965537

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 (67) hide show
  1. package/CHANGELOG.md +1 -1
  2. package/admin/AppModel.ts +15 -8
  3. package/admin/tabs/cluster/ClusterTab.ts +14 -63
  4. package/admin/tabs/cluster/{BaseInstanceModel.ts → instances/BaseInstanceModel.ts} +2 -2
  5. package/admin/tabs/cluster/instances/InstancesTab.ts +73 -0
  6. package/admin/tabs/cluster/{ClusterTabModel.ts → instances/InstancesTabModel.ts} +16 -16
  7. package/admin/tabs/cluster/{connpool → instances/connpool}/ConnPoolMonitorModel.ts +1 -1
  8. package/admin/tabs/cluster/{connpool → instances/connpool}/ConnPoolMonitorPanel.ts +1 -1
  9. package/admin/tabs/cluster/{environment → instances/environment}/ServerEnvModel.ts +1 -1
  10. package/admin/tabs/cluster/{environment → instances/environment}/ServerEnvPanel.ts +1 -1
  11. package/admin/tabs/cluster/{logs → instances/logs}/LogViewer.ts +1 -1
  12. package/admin/tabs/cluster/{logs → instances/logs}/LogViewerModel.ts +1 -1
  13. package/admin/tabs/cluster/{logs → instances/logs}/levels/LogLevelDialog.ts +1 -1
  14. package/admin/tabs/cluster/{logs → instances/logs}/levels/LogLevelDialogModel.ts +1 -1
  15. package/admin/tabs/cluster/{memory → instances/memory}/MemoryMonitorModel.ts +1 -1
  16. package/admin/tabs/cluster/{memory → instances/memory}/MemoryMonitorPanel.ts +1 -1
  17. package/admin/tabs/cluster/{services → instances/services}/DetailsPanel.ts +1 -1
  18. package/admin/tabs/cluster/{services → instances/services}/ServiceModel.ts +3 -3
  19. package/admin/tabs/cluster/{services → instances/services}/ServicePanel.ts +1 -1
  20. package/admin/tabs/cluster/{websocket → instances/websocket}/WebSocketModel.ts +1 -1
  21. package/admin/tabs/cluster/{websocket → instances/websocket}/WebSocketPanel.ts +1 -1
  22. package/admin/tabs/cluster/objects/ClusterObjects.scss +25 -0
  23. package/admin/tabs/cluster/objects/ClusterObjectsModel.ts +427 -0
  24. package/admin/tabs/cluster/objects/ClusterObjectsPanel.ts +114 -0
  25. package/admin/tabs/cluster/objects/DetailModel.ts +158 -0
  26. package/admin/tabs/cluster/objects/DetailPanel.ts +51 -0
  27. package/build/types/admin/tabs/cluster/ClusterTab.d.ts +1 -4
  28. package/build/types/admin/tabs/cluster/{BaseInstanceModel.d.ts → instances/BaseInstanceModel.d.ts} +2 -2
  29. package/build/types/admin/tabs/cluster/instances/InstancesTab.d.ts +4 -0
  30. package/build/types/admin/tabs/cluster/{ClusterTabModel.d.ts → instances/InstancesTabModel.d.ts} +2 -1
  31. package/build/types/admin/tabs/cluster/{connpool → instances/connpool}/ConnPoolMonitorModel.d.ts +1 -1
  32. package/build/types/admin/tabs/cluster/{connpool → instances/connpool}/ConnPoolMonitorPanel.d.ts +1 -1
  33. package/build/types/admin/tabs/cluster/{environment → instances/environment}/ServerEnvModel.d.ts +1 -1
  34. package/build/types/admin/tabs/cluster/{environment → instances/environment}/ServerEnvPanel.d.ts +1 -1
  35. package/build/types/admin/tabs/cluster/{logs → instances/logs}/LogViewerModel.d.ts +1 -1
  36. package/build/types/admin/tabs/cluster/{logs → instances/logs}/levels/LogLevelDialog.d.ts +1 -1
  37. package/build/types/admin/tabs/cluster/{logs → instances/logs}/levels/LogLevelDialogModel.d.ts +1 -1
  38. package/build/types/admin/tabs/cluster/{memory → instances/memory}/MemoryMonitorModel.d.ts +1 -1
  39. package/build/types/admin/tabs/cluster/{memory → instances/memory}/MemoryMonitorPanel.d.ts +1 -1
  40. package/build/types/admin/tabs/cluster/{services → instances/services}/DetailsPanel.d.ts +1 -1
  41. package/build/types/admin/tabs/cluster/{services → instances/services}/ServiceModel.d.ts +1 -1
  42. package/build/types/admin/tabs/cluster/{websocket → instances/websocket}/WebSocketModel.d.ts +1 -1
  43. package/build/types/admin/tabs/cluster/{websocket → instances/websocket}/WebSocketPanel.d.ts +1 -1
  44. package/build/types/admin/tabs/cluster/objects/ClusterObjectsModel.d.ts +30 -0
  45. package/build/types/admin/tabs/cluster/objects/ClusterObjectsPanel.d.ts +3 -0
  46. package/build/types/admin/tabs/cluster/objects/DetailModel.d.ts +19 -0
  47. package/build/types/admin/tabs/cluster/objects/DetailPanel.d.ts +3 -0
  48. package/build/types/cmp/viewmanager/ViewManagerModel.d.ts +3 -3
  49. package/cmp/viewmanager/ViewManagerModel.ts +3 -3
  50. package/desktop/cmp/viewmanager/dialog/ViewPanelModel.ts +5 -11
  51. package/package.json +1 -1
  52. package/tsconfig.tsbuildinfo +1 -1
  53. package/admin/tabs/cluster/distobjects/DistributedObjectsModel.ts +0 -199
  54. package/admin/tabs/cluster/distobjects/DistributedObjectsPanel.ts +0 -99
  55. package/build/types/admin/tabs/cluster/distobjects/DistributedObjectsModel.d.ts +0 -16
  56. package/build/types/admin/tabs/cluster/distobjects/DistributedObjectsPanel.d.ts +0 -2
  57. /package/admin/tabs/cluster/{logs → instances/logs}/LogDisplay.ts +0 -0
  58. /package/admin/tabs/cluster/{logs → instances/logs}/LogDisplayModel.ts +0 -0
  59. /package/admin/tabs/cluster/{logs → instances/logs}/LogViewer.scss +0 -0
  60. /package/admin/tabs/cluster/{services → instances/services}/DetailsModel.ts +0 -0
  61. /package/admin/tabs/cluster/{websocket → instances/websocket}/WebSocketColumns.ts +0 -0
  62. /package/build/types/admin/tabs/cluster/{logs → instances/logs}/LogDisplay.d.ts +0 -0
  63. /package/build/types/admin/tabs/cluster/{logs → instances/logs}/LogDisplayModel.d.ts +0 -0
  64. /package/build/types/admin/tabs/cluster/{logs → instances/logs}/LogViewer.d.ts +0 -0
  65. /package/build/types/admin/tabs/cluster/{services → instances/services}/DetailsModel.d.ts +0 -0
  66. /package/build/types/admin/tabs/cluster/{services → instances/services}/ServicePanel.d.ts +0 -0
  67. /package/build/types/admin/tabs/cluster/{websocket → instances/websocket}/WebSocketColumns.d.ts +0 -0
@@ -0,0 +1,427 @@
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 © 2025 Extremely Heavy Industries Inc.
6
+ */
7
+ import {exportFilenameWithDate} from '@xh/hoist/admin/AdminUtils';
8
+ import {AppModel} from '@xh/hoist/admin/AppModel';
9
+ import {GridModel, tagsRenderer} from '@xh/hoist/cmp/grid';
10
+ import {br, fragment} from '@xh/hoist/cmp/layout';
11
+ import {HoistModel, LoadSpec, managed, PlainObject, XH} from '@xh/hoist/core';
12
+ import {FilterLike, FilterTestFn, RecordActionSpec, StoreRecord} from '@xh/hoist/data';
13
+ import {Icon} from '@xh/hoist/icon';
14
+ import {bindable, makeObservable, computed, observable, runInAction} from '@xh/hoist/mobx';
15
+ import {isDisplayed, pluralize} from '@xh/hoist/utils/js';
16
+ import {groupBy, isEmpty, mapValues, size} from 'lodash';
17
+ import {createRef} from 'react';
18
+
19
+ export class ClusterObjectsModel extends HoistModel {
20
+ viewRef = createRef<HTMLElement>();
21
+
22
+ @observable.ref startTimestamp: Date = null;
23
+ @observable runDurationMs: number = 0;
24
+
25
+ @bindable hideUnchecked: boolean = false;
26
+ @bindable.ref textFilter: FilterTestFn = null;
27
+
28
+ clearHibernateCachesAction: RecordActionSpec = {
29
+ text: 'Clear Selected Hibernate Caches',
30
+ icon: Icon.reset(),
31
+ intent: 'warning',
32
+ actionFn: () => this.clearHibernateCachesAsync(),
33
+ displayFn: ({selectedRecords}) => {
34
+ const caches = selectedRecords.filter(it => it.data.type === 'Hibernate Cache');
35
+ return {
36
+ hidden: AppModel.readonly || isEmpty(caches),
37
+ text: `Clear Hibernate Cache`
38
+ };
39
+ },
40
+ recordsRequired: true
41
+ };
42
+
43
+ @managed gridModel = new GridModel({
44
+ selModel: 'multiple',
45
+ treeMode: true,
46
+ autosizeOptions: {mode: 'managed', includeCollapsedChildren: true},
47
+ enableExport: true,
48
+ exportOptions: {filename: exportFilenameWithDate('cluster-objects'), columns: 'ALL'},
49
+ sortBy: ['displayName'],
50
+ store: {
51
+ fields: [
52
+ {name: 'name', type: 'string'},
53
+ {name: 'type', type: 'string'},
54
+ {name: 'parentName', type: 'string'},
55
+ {name: 'provider', type: 'string'},
56
+ {name: 'compareState', type: 'string'},
57
+ {name: 'comparableAdminStats', type: 'auto'},
58
+ {name: 'adminStatsByInstance', type: 'auto'}
59
+ ],
60
+ idSpec: 'name'
61
+ },
62
+ rowClassRules: {
63
+ 'xh-cluster-objects-row-has-break': ({data: record}) =>
64
+ record?.data.compareState === 'failed'
65
+ },
66
+ columns: [
67
+ {
68
+ field: 'compareState',
69
+ width: 30,
70
+ align: 'center',
71
+ resizable: false,
72
+ headerName: '',
73
+ headerTooltip: 'Compare State',
74
+ renderer: v =>
75
+ v === 'failed'
76
+ ? Icon.diff({prefix: 'fas', intent: 'danger'})
77
+ : v === 'passed'
78
+ ? Icon.check({prefix: 'fas', intent: 'success'})
79
+ : null
80
+ },
81
+ {field: 'displayName', isTreeColumn: true},
82
+ {field: 'type'},
83
+ {
84
+ field: 'comparableAdminStats',
85
+ renderer: v => (!isEmpty(v) ? tagsRenderer(v) : null),
86
+ hidden: true
87
+ },
88
+ {field: 'name', headerName: 'Full Name', hidden: true},
89
+ {field: 'parentName', hidden: true}
90
+ ],
91
+ contextMenu: [this.clearHibernateCachesAction, '-', ...GridModel.defaultContextMenu]
92
+ });
93
+
94
+ get selectedRecord(): StoreRecord {
95
+ return this.gridModel.selectedRecord;
96
+ }
97
+
98
+ get isSingleInstance() {
99
+ return this.gridModel.store.allRecords.every(
100
+ rec => size(rec.data?.adminStatsByInstance) <= 1
101
+ );
102
+ }
103
+
104
+ @computed
105
+ get counts() {
106
+ const ret = {passed: 0, failed: 0, unchecked: 0};
107
+ this.gridModel.store.allRecords.forEach(record => {
108
+ ret[record.data.compareState]++;
109
+ });
110
+ return ret;
111
+ }
112
+
113
+ constructor() {
114
+ super();
115
+ makeObservable(this);
116
+ this.addReaction({
117
+ track: () => [this.textFilter, this.hideUnchecked],
118
+ run: this.applyFilters,
119
+ fireImmediately: true
120
+ });
121
+ }
122
+
123
+ async clearHibernateCachesAsync() {
124
+ const {selectedRecords} = this.gridModel,
125
+ cacheRecords = selectedRecords.filter(it => it.data.type === 'Hibernate Cache'),
126
+ count = cacheRecords.length,
127
+ confirmed = await XH.confirm({
128
+ message: fragment(
129
+ `This will clear ${pluralize('Hibernate Cache', count, true)}.`,
130
+ br(),
131
+ br(),
132
+ `This can resolve issues with data modifications made directly to the database not appearing in a running application, but should be used with care as it can have a temporary performance impact.`
133
+ ),
134
+ confirmProps: {
135
+ text: `Clear ${pluralize('Hibernate Cache', count, true)}`,
136
+ icon: Icon.reset(),
137
+ intent: 'warning',
138
+ outlined: true,
139
+ autoFocus: false
140
+ }
141
+ });
142
+
143
+ if (!confirmed) return;
144
+
145
+ try {
146
+ await XH.postJson({
147
+ url: 'clusterObjectsAdmin/clearHibernateCaches',
148
+ body: {
149
+ names: cacheRecords.map(it => it.id)
150
+ }
151
+ }).linkTo(this.loadModel);
152
+
153
+ await this.refreshAsync();
154
+ XH.successToast(`${pluralize('Hibernate Cache', count, true)} cleared.`);
155
+ } catch (e) {
156
+ XH.handleException(e);
157
+ }
158
+ }
159
+
160
+ async clearAllHibernateCachesAsync() {
161
+ const confirmed = await XH.confirm({
162
+ message: fragment(
163
+ 'This will clear the second-level Hibernate caches for all domain objects, requiring the server to re-query the database for their latest state.',
164
+ br(),
165
+ br(),
166
+ `This can resolve issues with data modifications made directly to the database not appearing in a running application, but should be used with care as it can have a temporary performance impact.`
167
+ ),
168
+ confirmProps: {
169
+ text: 'Clear All Hibernate Caches',
170
+ icon: Icon.reset(),
171
+ intent: 'warning',
172
+ outlined: true,
173
+ autoFocus: false
174
+ }
175
+ });
176
+ if (!confirmed) return;
177
+
178
+ try {
179
+ await XH.fetchJson({
180
+ url: 'clusterObjectsAdmin/clearAllHibernateCaches'
181
+ }).linkTo(this.loadModel);
182
+
183
+ await this.refreshAsync();
184
+ XH.successToast('All Hibernate Caches cleared.');
185
+ } catch (e) {
186
+ XH.handleException(e);
187
+ }
188
+ }
189
+
190
+ override async doLoadAsync(loadSpec: LoadSpec) {
191
+ try {
192
+ const report = await XH.fetchJson({
193
+ url: 'clusterObjectsAdmin/getClusterObjectsReport'
194
+ });
195
+
196
+ this.gridModel.loadData(this.processReport(report));
197
+ runInAction(() => {
198
+ this.startTimestamp = report.startTimestamp
199
+ ? new Date(report.startTimestamp)
200
+ : null;
201
+ this.runDurationMs =
202
+ report.endTimestamp && report.startTimestamp
203
+ ? report.endTimestamp - report.startTimestamp
204
+ : null;
205
+ });
206
+ } catch (e) {
207
+ XH.handleException(e, {
208
+ alertType: 'toast',
209
+ showAlert: this.isVisible && !loadSpec.isAutoRefresh,
210
+ logOnServer: this.isVisible && !loadSpec.isAutoRefresh
211
+ });
212
+ }
213
+ }
214
+
215
+ get isVisible() {
216
+ return isDisplayed(this.viewRef.current);
217
+ }
218
+
219
+ //----------------------
220
+ // Implementation
221
+ //----------------------
222
+ private applyFilters() {
223
+ const {hideUnchecked, textFilter, isSingleInstance} = this,
224
+ filters: FilterLike[] = [textFilter];
225
+
226
+ if (hideUnchecked && !isSingleInstance) {
227
+ filters.push({
228
+ field: 'compareState',
229
+ op: '!=',
230
+ value: 'unchecked'
231
+ });
232
+ }
233
+
234
+ this.gridModel.store.setFilter(filters);
235
+ }
236
+
237
+ private processReport({
238
+ info,
239
+ breaks
240
+ }: {
241
+ info: PlainObject[];
242
+ breaks: Record<string, [string, string]>;
243
+ }): ClusterObjectRecord[] {
244
+ const byName = groupBy(info, 'name'),
245
+ recordsByName: Record<string, ClusterObjectRecord> = mapValues(byName, objs => {
246
+ const {name, type, comparableAdminStats} = objs[0],
247
+ adminStatsByInstance: PlainObject = Object.fromEntries(
248
+ objs.map(obj => [obj.instanceName, obj.adminStats])
249
+ );
250
+ return {
251
+ name,
252
+ displayName: this.deriveDisplayName(name, type),
253
+ type,
254
+ parentName: this.deriveParent(name, type),
255
+ compareState: (isEmpty(comparableAdminStats) || objs.length < 2
256
+ ? 'unchecked'
257
+ : !isEmpty(breaks[name])
258
+ ? 'failed'
259
+ : 'passed') as CompareState,
260
+ comparableAdminStats: comparableAdminStats ?? [],
261
+ adminStatsByInstance,
262
+ children: []
263
+ };
264
+ });
265
+
266
+ // Create known parent/grouping records.
267
+ // We leave children empty for now, as we'll populate them all in the next step.
268
+ recordsByName['App'] = this.createParentRecord({
269
+ name: 'App',
270
+ displayName: 'App',
271
+ type: 'Provider',
272
+ parentName: null
273
+ });
274
+ recordsByName['Hoist'] = this.createParentRecord({
275
+ name: 'Hoist',
276
+ displayName: 'Hoist',
277
+ type: 'Provider',
278
+ parentName: null
279
+ });
280
+ recordsByName['Hibernate (Hoist)'] = this.createParentRecord({
281
+ name: 'Hibernate (Hoist)',
282
+ displayName: 'Hibernate',
283
+ type: 'Hibernate',
284
+ parentName: 'Hoist'
285
+ });
286
+ recordsByName['Hibernate (App)'] = this.createParentRecord({
287
+ name: 'Hibernate (App)',
288
+ displayName: 'Hibernate',
289
+ type: 'Hibernate',
290
+ parentName: 'App'
291
+ });
292
+
293
+ // Place child records into the children of their parent record. Note that this may create
294
+ // any missing parents as needed - they will be appended to the end of the list.
295
+ const recordNames = Object.keys(recordsByName);
296
+ for (let idx = 0; idx < recordNames.length; idx++) {
297
+ const name = recordNames[idx],
298
+ record = recordsByName[name],
299
+ parentName = record.parentName;
300
+ if (parentName) {
301
+ // Create any unknown/missing parent records.
302
+ if (!recordsByName[parentName]) {
303
+ recordsByName[parentName] = this.createParentRecord({
304
+ name: parentName,
305
+ displayName: this.deriveDisplayName(parentName, null),
306
+ type: null,
307
+ parentName: this.deriveParent(parentName, null)
308
+ });
309
+ // Also append to end of list, to ensure we eventually also process this parent.
310
+ recordNames.push(parentName);
311
+ }
312
+
313
+ // Place self under parent.
314
+ recordsByName[parentName].children.push(record);
315
+
316
+ // Aggregate parent compareState.
317
+ const state = record.compareState,
318
+ parentState = recordsByName[parentName].compareState;
319
+ recordsByName[parentName].compareState =
320
+ state === 'failed' || parentState === 'failed'
321
+ ? 'failed'
322
+ : state === 'passed' || parentState === 'passed'
323
+ ? 'passed'
324
+ : 'unchecked';
325
+ }
326
+ }
327
+
328
+ return Object.values(recordsByName).filter(record => !record.parentName);
329
+ }
330
+
331
+ private createParentRecord(args: {
332
+ name: string;
333
+ type: string;
334
+ parentName: string;
335
+ displayName: string;
336
+ }): ClusterObjectRecord {
337
+ return {
338
+ ...args,
339
+ compareState: 'unchecked',
340
+ comparableAdminStats: [],
341
+ adminStatsByInstance: {},
342
+ children: []
343
+ };
344
+ }
345
+
346
+ private deriveParent(name: string, type: string): string {
347
+ // Group collection caches under their parent object.
348
+ if (type === 'Hibernate Cache') {
349
+ const lastDotIdx = name.lastIndexOf('.');
350
+ if (lastDotIdx != -1) {
351
+ const last = name.substring(lastDotIdx + 1),
352
+ rest = name.substring(0, lastDotIdx);
353
+ // Identify collection caches by lowercase name after last dot.
354
+ if (!isEmpty(last) && last[0] !== last[0].toUpperCase()) return rest;
355
+ }
356
+ // Otherwise, group under the correct hibernate group record.
357
+ return name.startsWith('io.xh.hoist') ||
358
+ name === 'default-query-results-region' ||
359
+ name == 'default-update-timestamps-region'
360
+ ? 'Hibernate (Hoist)'
361
+ : 'Hibernate (App)';
362
+ }
363
+ // Hz Ringbuffer that implements a CachedValue.
364
+ if (name.startsWith('_hz_rb_xhcachedvalue.')) {
365
+ return name.substring(21);
366
+ }
367
+ // Hz ITopic that implements a CachedValue.
368
+ if (name.startsWith('xhcachedvalue.')) {
369
+ return name.substring(14);
370
+ }
371
+ // Hz ReplicatedMap that implements a Cache.
372
+ if (name.startsWith('xhcache.')) {
373
+ return name.substring(8);
374
+ }
375
+ // Any object that utilizes `svc.hzName()`.
376
+ if (name.lastIndexOf('[') !== -1) {
377
+ return name.substring(0, name.lastIndexOf('['));
378
+ }
379
+ // XH Services and impl objects.
380
+ if (name.startsWith('xh') || name.startsWith('io.xh.hoist')) {
381
+ return 'Hoist';
382
+ }
383
+ // Everything else belongs in the 'App' group.
384
+ if (name !== 'App' && name !== 'Hoist') {
385
+ return 'App';
386
+ }
387
+ return null;
388
+ }
389
+
390
+ private deriveDisplayName(name: string, type: string): string {
391
+ // Hz Ringbuffer that implements a CachedValue.
392
+ if (name.startsWith('_hz_rb_xhcachedvalue.')) {
393
+ return type;
394
+ }
395
+ // Hz ITopic that implements a CachedValue.
396
+ if (name.startsWith('xhcachedvalue.')) {
397
+ return type;
398
+ }
399
+ // Hz ReplicatedMap that implements a Cache.
400
+ if (name.startsWith('xhcache.')) {
401
+ return type;
402
+ }
403
+ // Any object that utilizes `svc.hzName()`.
404
+ if (name.lastIndexOf('[') !== -1) {
405
+ return name.substring(name.lastIndexOf('[') + 1, name.lastIndexOf(']'));
406
+ }
407
+ // Any object that utilizes `class.getName()`.
408
+ if (name.lastIndexOf('.') !== -1) {
409
+ return name.substring(name.lastIndexOf('.') + 1);
410
+ }
411
+ // Other groupings, Services, impl objects, etc.
412
+ return name;
413
+ }
414
+ }
415
+
416
+ type CompareState = 'failed' | 'passed' | 'unchecked';
417
+
418
+ interface ClusterObjectRecord {
419
+ name: string;
420
+ displayName: string;
421
+ type: string;
422
+ parentName?: string;
423
+ compareState: CompareState;
424
+ comparableAdminStats: string[];
425
+ adminStatsByInstance: Record<string, PlainObject>;
426
+ children: ClusterObjectRecord[];
427
+ }
@@ -0,0 +1,114 @@
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 © 2025 Extremely Heavy Industries Inc.
6
+ */
7
+ import {grid} from '@xh/hoist/cmp/grid';
8
+ import {div, filler, hframe, label} from '@xh/hoist/cmp/layout';
9
+ import {relativeTimestamp} from '@xh/hoist/cmp/relativetimestamp';
10
+ import {storeFilterField} from '@xh/hoist/cmp/store';
11
+ import {creates, hoistCmp} from '@xh/hoist/core';
12
+ import {button, exportButton} from '@xh/hoist/desktop/cmp/button';
13
+ import {switchInput} from '@xh/hoist/desktop/cmp/input';
14
+ import {panel} from '@xh/hoist/desktop/cmp/panel';
15
+ import {toolbar, toolbarSep} from '@xh/hoist/desktop/cmp/toolbar';
16
+ import {fmtNumber} from '@xh/hoist/format';
17
+ import {Icon} from '@xh/hoist/icon';
18
+ import {ClusterObjectsModel} from './ClusterObjectsModel';
19
+ import {detailPanel} from './DetailPanel';
20
+ import './ClusterObjects.scss';
21
+
22
+ export const clusterObjectsPanel = hoistCmp.factory({
23
+ displayName: 'ClusterObjectsPanel',
24
+ model: creates(ClusterObjectsModel),
25
+
26
+ render({model}) {
27
+ return panel({
28
+ tbar: tbar(),
29
+ item: hframe(
30
+ panel({
31
+ modelConfig: {
32
+ side: 'left',
33
+ defaultSize: 475,
34
+ collapsible: false
35
+ },
36
+ item: grid({agOptions: {groupDefaultExpanded: 2}})
37
+ }),
38
+ detailPanel()
39
+ ),
40
+ bbar: bbar(),
41
+ mask: 'onLoad',
42
+ ref: model.viewRef
43
+ });
44
+ }
45
+ });
46
+
47
+ const tbar = hoistCmp.factory<ClusterObjectsModel>(({model}) => {
48
+ const {isSingleInstance} = model;
49
+ return toolbar(
50
+ diffBar({omit: isSingleInstance}),
51
+ filler(),
52
+ 'As of',
53
+ relativeTimestamp({bind: 'startTimestamp'}),
54
+ toolbarSep(),
55
+ 'Took ',
56
+ fmtNumber(model.runDurationMs, {label: 'ms'})
57
+ );
58
+ });
59
+
60
+ const diffBar = hoistCmp.factory<ClusterObjectsModel>(({model}) => {
61
+ const {counts} = model;
62
+ return [
63
+ switchInput({
64
+ label: 'Hide unchecked',
65
+ bind: 'hideUnchecked'
66
+ }),
67
+ div({
68
+ className: 'xh-cluster-objects-result-count',
69
+ omit: !counts.failed,
70
+ items: [
71
+ toolbarSep(),
72
+ Icon.error({prefix: 'fas', className: 'xh-red'}),
73
+ label(`${counts.failed} Failed`)
74
+ ]
75
+ }),
76
+ div({
77
+ className: 'xh-cluster-objects-result-count',
78
+ omit: !counts.passed,
79
+ items: [
80
+ toolbarSep(),
81
+ Icon.checkCircle({prefix: 'fas', className: 'xh-green'}),
82
+ label(`${counts.passed} OK`)
83
+ ]
84
+ }),
85
+ div({
86
+ className: 'xh-cluster-objects-result-count',
87
+ omit: !counts.unchecked || model.hideUnchecked,
88
+ items: [
89
+ toolbarSep(),
90
+ Icon.disabled({prefix: 'fas', className: 'xh-gray'}),
91
+ label(`${counts.unchecked} Unchecked`)
92
+ ]
93
+ })
94
+ ];
95
+ });
96
+
97
+ const bbar = hoistCmp.factory<ClusterObjectsModel>(({model}) => {
98
+ return toolbar(
99
+ button({
100
+ text: 'Clear All Hibernate Caches',
101
+ icon: Icon.reset(),
102
+ intent: 'warning',
103
+ tooltip: 'Clear the Hibernate caches using the native Hibernate API',
104
+ onClick: () => model.clearAllHibernateCachesAsync()
105
+ }),
106
+ filler(),
107
+ storeFilterField({
108
+ matchMode: 'any',
109
+ autoApply: false,
110
+ onFilterChange: f => (model.textFilter = f)
111
+ }),
112
+ exportButton()
113
+ );
114
+ });
@@ -0,0 +1,158 @@
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 © 2025 Extremely Heavy Industries Inc.
6
+ */
7
+ import {ClusterObjectsModel} from '@xh/hoist/admin/tabs/cluster/objects/ClusterObjectsModel';
8
+ import {ColumnSpec, GridModel} from '@xh/hoist/cmp/grid';
9
+ import {HoistModel, lookup, managed, PlainObject, XH} from '@xh/hoist/core';
10
+ import {StoreRecord} from '@xh/hoist/data';
11
+ import {fmtDateTimeSec, fmtJson} from '@xh/hoist/format';
12
+ import {makeObservable, action, 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';
24
+
25
+ export class DetailModel extends HoistModel {
26
+ @lookup(ClusterObjectsModel)
27
+ parent: ClusterObjectsModel;
28
+
29
+ @managed
30
+ @observable.ref
31
+ gridModel: GridModel = null;
32
+
33
+ //---------------------------------------------
34
+ // Current cluster object and related.
35
+ //--------------------------------------------
36
+ get selectedObject(): StoreRecord {
37
+ const selRecord = this.parent.selectedRecord;
38
+ return !isEmpty(selRecord?.data.adminStatsByInstance) ? selRecord : null;
39
+ }
40
+
41
+ get objectName(): string {
42
+ return this.selectedObject?.data.name ?? null;
43
+ }
44
+
45
+ get objectType(): string {
46
+ return this.selectedObject?.data.type ?? null;
47
+ }
48
+
49
+ //--------------------------------
50
+ // Selected instance and related.
51
+ //--------------------------------
52
+ get instanceName(): string {
53
+ return this.gridModel?.selectedRecord?.id as string;
54
+ }
55
+
56
+ get selectedAdminStats() {
57
+ return this.selectedObject?.data.adminStatsByInstance[this.instanceName];
58
+ }
59
+
60
+ constructor() {
61
+ super();
62
+ makeObservable(this);
63
+ this.addReaction({
64
+ track: () => this.selectedObject,
65
+ run: record => this.updateGridModel(record)
66
+ });
67
+ }
68
+
69
+ fmtStats(stats: PlainObject): string {
70
+ stats = cloneDeep(stats);
71
+ this.processTimestamps(stats);
72
+ return fmtJson(JSON.stringify(stats));
73
+ }
74
+
75
+ //----------------------
76
+ // Implementation
77
+ //----------------------
78
+ @action
79
+ private updateGridModel(record: StoreRecord) {
80
+ if (isEmpty(record)) return;
81
+
82
+ const {adminStatsByInstance, comparableAdminStats} = record.data,
83
+ instanceNames = Object.keys(adminStatsByInstance),
84
+ diffFields = comparableAdminStats ?? [],
85
+ otherFields = without(
86
+ Object.keys(adminStatsByInstance[instanceNames[0]] ?? {}),
87
+ ...diffFields,
88
+ 'name',
89
+ 'type',
90
+ `config`,
91
+ 'replicate'
92
+ ),
93
+ selectedId = this.gridModel?.selectedId;
94
+
95
+ const gridModel = this.createGridModel(diffFields, otherFields);
96
+ gridModel.loadData(
97
+ instanceNames.map(instanceName => {
98
+ const data = cloneDeep(adminStatsByInstance[instanceName] ?? {});
99
+ this.processTimestamps(data);
100
+ return {instanceName, ...data};
101
+ })
102
+ );
103
+
104
+ XH.safeDestroy(this.gridModel);
105
+ this.gridModel = gridModel;
106
+ selectedId ? gridModel.selectAsync(selectedId) : gridModel.selectFirstAsync();
107
+ }
108
+
109
+ private createGridModel(diffFields: string[], otherFields: string[]) {
110
+ return new GridModel({
111
+ autosizeOptions: {mode: 'managed', includeCollapsedChildren: true},
112
+ store: {idSpec: 'instanceName'},
113
+ columns: [
114
+ {field: {name: 'instanceName', type: 'string', displayName: 'Instance'}},
115
+ ...diffFields.map(f => this.createColSpec(f, true)),
116
+ ...otherFields.map(f => this.createColSpec(f, false))
117
+ ]
118
+ });
119
+ }
120
+
121
+ private createColSpec(fieldName: string, isDiff: boolean) {
122
+ const ret: ColumnSpec = {
123
+ field: {name: fieldName, displayName: fieldName},
124
+ renderer: v => (typeof v === 'object' ? JSON.stringify(v) : v),
125
+ autosizeMaxWidth: 200
126
+ };
127
+ if (isDiff) {
128
+ ret.cellClassRules = {
129
+ 'xh-cluster-objects-cell-danger': ({value, colDef}) =>
130
+ !colDef ||
131
+ this.gridModel.store.records.some(r => !isEqual(r.data[colDef.colId], value)),
132
+ 'xh-cluster-objects-cell-success': ({value, colDef}) =>
133
+ colDef &&
134
+ this.gridModel.store.records.every(r => isEqual(r.data[colDef.colId], value))
135
+ };
136
+ }
137
+ return ret;
138
+ }
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
+ }