@xh/hoist 71.0.0-SNAPSHOT.1735861709598 → 71.0.0-SNAPSHOT.1736120910674
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.
- package/CHANGELOG.md +5 -2
- package/admin/AppModel.ts +15 -8
- package/admin/tabs/cluster/ClusterTab.ts +14 -63
- package/admin/tabs/cluster/{BaseInstanceModel.ts → instances/BaseInstanceModel.ts} +2 -2
- package/admin/tabs/cluster/instances/InstancesTab.ts +73 -0
- package/admin/tabs/cluster/{ClusterTabModel.ts → instances/InstancesTabModel.ts} +16 -16
- package/admin/tabs/cluster/{connpool → instances/connpool}/ConnPoolMonitorModel.ts +1 -1
- package/admin/tabs/cluster/{connpool → instances/connpool}/ConnPoolMonitorPanel.ts +1 -1
- package/admin/tabs/cluster/{environment → instances/environment}/ServerEnvModel.ts +1 -1
- package/admin/tabs/cluster/{environment → instances/environment}/ServerEnvPanel.ts +1 -1
- package/admin/tabs/cluster/{logs → instances/logs}/LogViewer.ts +1 -1
- package/admin/tabs/cluster/{logs → instances/logs}/LogViewerModel.ts +1 -1
- package/admin/tabs/cluster/{logs → instances/logs}/levels/LogLevelDialog.ts +1 -1
- package/admin/tabs/cluster/{logs → instances/logs}/levels/LogLevelDialogModel.ts +1 -1
- package/admin/tabs/cluster/{memory → instances/memory}/MemoryMonitorModel.ts +1 -1
- package/admin/tabs/cluster/{memory → instances/memory}/MemoryMonitorPanel.ts +1 -1
- package/admin/tabs/cluster/{services → instances/services}/DetailsPanel.ts +1 -1
- package/admin/tabs/cluster/{services → instances/services}/ServiceModel.ts +3 -3
- package/admin/tabs/cluster/{services → instances/services}/ServicePanel.ts +1 -1
- package/admin/tabs/cluster/{websocket → instances/websocket}/WebSocketModel.ts +1 -1
- package/admin/tabs/cluster/{websocket → instances/websocket}/WebSocketPanel.ts +1 -1
- package/admin/tabs/cluster/objects/ClusterObjects.scss +25 -0
- package/admin/tabs/cluster/objects/ClusterObjectsModel.ts +427 -0
- package/admin/tabs/cluster/objects/ClusterObjectsPanel.ts +114 -0
- package/admin/tabs/cluster/objects/DetailModel.ts +158 -0
- package/admin/tabs/cluster/objects/DetailPanel.ts +51 -0
- package/build/types/admin/tabs/cluster/ClusterTab.d.ts +1 -4
- package/build/types/admin/tabs/cluster/{BaseInstanceModel.d.ts → instances/BaseInstanceModel.d.ts} +2 -2
- package/build/types/admin/tabs/cluster/instances/InstancesTab.d.ts +4 -0
- package/build/types/admin/tabs/cluster/{ClusterTabModel.d.ts → instances/InstancesTabModel.d.ts} +2 -1
- package/build/types/admin/tabs/cluster/{connpool → instances/connpool}/ConnPoolMonitorModel.d.ts +1 -1
- package/build/types/admin/tabs/cluster/{connpool → instances/connpool}/ConnPoolMonitorPanel.d.ts +1 -1
- package/build/types/admin/tabs/cluster/{environment → instances/environment}/ServerEnvModel.d.ts +1 -1
- package/build/types/admin/tabs/cluster/{environment → instances/environment}/ServerEnvPanel.d.ts +1 -1
- package/build/types/admin/tabs/cluster/{logs → instances/logs}/LogViewerModel.d.ts +1 -1
- package/build/types/admin/tabs/cluster/{logs → instances/logs}/levels/LogLevelDialog.d.ts +1 -1
- package/build/types/admin/tabs/cluster/{logs → instances/logs}/levels/LogLevelDialogModel.d.ts +1 -1
- package/build/types/admin/tabs/cluster/{memory → instances/memory}/MemoryMonitorModel.d.ts +1 -1
- package/build/types/admin/tabs/cluster/{memory → instances/memory}/MemoryMonitorPanel.d.ts +1 -1
- package/build/types/admin/tabs/cluster/{services → instances/services}/DetailsPanel.d.ts +1 -1
- package/build/types/admin/tabs/cluster/{services → instances/services}/ServiceModel.d.ts +1 -1
- package/build/types/admin/tabs/cluster/{websocket → instances/websocket}/WebSocketModel.d.ts +1 -1
- package/build/types/admin/tabs/cluster/{websocket → instances/websocket}/WebSocketPanel.d.ts +1 -1
- package/build/types/admin/tabs/cluster/objects/ClusterObjectsModel.d.ts +30 -0
- package/build/types/admin/tabs/cluster/objects/ClusterObjectsPanel.d.ts +3 -0
- package/build/types/admin/tabs/cluster/objects/DetailModel.d.ts +19 -0
- package/build/types/admin/tabs/cluster/objects/DetailPanel.d.ts +3 -0
- package/package.json +1 -1
- package/tsconfig.tsbuildinfo +1 -1
- package/admin/tabs/cluster/distobjects/DistributedObjectsModel.ts +0 -199
- package/admin/tabs/cluster/distobjects/DistributedObjectsPanel.ts +0 -99
- package/build/types/admin/tabs/cluster/distobjects/DistributedObjectsModel.d.ts +0 -16
- package/build/types/admin/tabs/cluster/distobjects/DistributedObjectsPanel.d.ts +0 -2
- /package/admin/tabs/cluster/{logs → instances/logs}/LogDisplay.ts +0 -0
- /package/admin/tabs/cluster/{logs → instances/logs}/LogDisplayModel.ts +0 -0
- /package/admin/tabs/cluster/{logs → instances/logs}/LogViewer.scss +0 -0
- /package/admin/tabs/cluster/{services → instances/services}/DetailsModel.ts +0 -0
- /package/admin/tabs/cluster/{websocket → instances/websocket}/WebSocketColumns.ts +0 -0
- /package/build/types/admin/tabs/cluster/{logs → instances/logs}/LogDisplay.d.ts +0 -0
- /package/build/types/admin/tabs/cluster/{logs → instances/logs}/LogDisplayModel.d.ts +0 -0
- /package/build/types/admin/tabs/cluster/{logs → instances/logs}/LogViewer.d.ts +0 -0
- /package/build/types/admin/tabs/cluster/{services → instances/services}/DetailsModel.d.ts +0 -0
- /package/build/types/admin/tabs/cluster/{services → instances/services}/ServicePanel.d.ts +0 -0
- /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
|
+
}
|