@xh/hoist 56.0.0 → 56.1.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.
- package/CHANGELOG.md +11 -4
- package/admin/tabs/general/about/AboutPanel.ts +4 -3
- package/admin/tabs/monitor/MonitorColumns.ts +32 -4
- package/admin/tabs/server/memory/MemoryMonitorModel.ts +80 -16
- package/admin/tabs/server/memory/MemoryMonitorPanel.ts +22 -1
- package/cmp/form/Form.ts +2 -9
- package/core/impl/InstallServices.ts +1 -1
- package/package.json +1 -1
- package/utils/js/LangUtils.ts +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 56.1.0 - 2023-04-14
|
|
4
|
+
* Add support for new memory management diagnostics provided by hoist-core
|
|
5
|
+
(requires hoist-core 16.1.0 for full operation).
|
|
6
|
+
|
|
7
|
+
### 🐞 Bug Fixes
|
|
8
|
+
* Fixes bug with display/reporting of exceptions during app initialization sequence.
|
|
9
|
+
|
|
10
|
+
|
|
3
11
|
## v56.0.0 - 2023-03-29
|
|
4
12
|
|
|
5
13
|
### 🎁 New Features
|
|
@@ -10,8 +18,6 @@
|
|
|
10
18
|
* New `FetchService.abort()` API allows manually aborting a pending fetch request.
|
|
11
19
|
* Hoist exceptions have been enhanced and standardized, including new TypeScript types. The
|
|
12
20
|
`Error.cause` property is now populated for wrapping exceptions.
|
|
13
|
-
* `PanelModel` now supports a `defaultSize` property specified in percentage as well as pixels
|
|
14
|
-
(e.g. `defaultSize: '20%'` as well as `defaultSize: 200`).
|
|
15
21
|
* New `GridModel.headerMenuDisplay` config for limiting column header menu visibility to on hover.
|
|
16
22
|
|
|
17
23
|
### 💥 Breaking Changes
|
|
@@ -20,10 +26,11 @@
|
|
|
20
26
|
* Requires AG Grid v29.0.0 or higher - update your AG Grid dependency in your app's `package.json`
|
|
21
27
|
file. See the [AG Grid Changelog](https://www.ag-grid.com/changelog) for details.
|
|
22
28
|
* Add a dependency on `@ag-grid-community/styles` to import new dedicated styles package.
|
|
23
|
-
* Imports of AG Grid CSS files within your app's `Bootstrap.ts` file will also need to be
|
|
29
|
+
* Imports of AG Grid CSS files within your app's `Bootstrap.ts` file will also need to be
|
|
30
|
+
updated to import styles from their new location. The recommended imports are now:
|
|
24
31
|
```typescript
|
|
25
32
|
import '@ag-grid-community/styles/ag-grid.css';
|
|
26
|
-
import '@ag-grid-community/styles/ag-theme-balham
|
|
33
|
+
import '@ag-grid-community/styles/ag-theme-balham.css';
|
|
27
34
|
```
|
|
28
35
|
* New `xhActivityTrackingConfig` soft-configuration entry places new limits on the size of
|
|
29
36
|
any `data` objects passed to `XH.track()` calls.
|
|
@@ -91,13 +91,14 @@ function renderBlurb() {
|
|
|
91
91
|
xhLogo(),
|
|
92
92
|
div(
|
|
93
93
|
p(
|
|
94
|
-
'Built with Hoist
|
|
94
|
+
'Built with Hoist, a toolkit for rapid application development from ',
|
|
95
95
|
a({
|
|
96
|
-
href: '
|
|
96
|
+
href: 'https://xh.io',
|
|
97
97
|
target: '_blank',
|
|
98
98
|
rel: 'noopener noreferrer',
|
|
99
99
|
item: 'Extremely Heavy'
|
|
100
|
-
})
|
|
100
|
+
}),
|
|
101
|
+
'.'
|
|
101
102
|
),
|
|
102
103
|
p(
|
|
103
104
|
'Please contact ',
|
|
@@ -9,7 +9,8 @@ import * as Col from '@xh/hoist/cmp/grid/columns';
|
|
|
9
9
|
import {ColumnSpec} from '@xh/hoist/cmp/grid/columns';
|
|
10
10
|
|
|
11
11
|
const mbCol = {width: 150, renderer: numberRenderer({precision: 2, withCommas: true})},
|
|
12
|
-
pctCol = {width: 150, renderer: numberRenderer({precision: 2, withCommas: true, label: '%'})}
|
|
12
|
+
pctCol = {width: 150, renderer: numberRenderer({precision: 2, withCommas: true, label: '%'})},
|
|
13
|
+
msCol = {width: 150, renderer: numberRenderer({precision: 0, withCommas: false})};
|
|
13
14
|
|
|
14
15
|
export const metricUnit: ColumnSpec = {
|
|
15
16
|
field: {name: 'metricUnit', type: 'string'},
|
|
@@ -84,11 +85,38 @@ export const usedHeapMb: ColumnSpec = {
|
|
|
84
85
|
...mbCol
|
|
85
86
|
};
|
|
86
87
|
|
|
87
|
-
export const
|
|
88
|
+
export const usedPctMax: ColumnSpec = {
|
|
88
89
|
field: {
|
|
89
|
-
name: '
|
|
90
|
+
name: 'usedPctMax',
|
|
90
91
|
type: 'number',
|
|
91
|
-
displayName: 'Used (pct
|
|
92
|
+
displayName: 'Used (pct Max)'
|
|
93
|
+
},
|
|
94
|
+
...pctCol
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
export const avgCollectionTime: ColumnSpec = {
|
|
98
|
+
field: {
|
|
99
|
+
name: 'avgCollectionTime',
|
|
100
|
+
type: 'number',
|
|
101
|
+
displayName: 'Avg (ms)'
|
|
102
|
+
},
|
|
103
|
+
...msCol
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
export const collectionCount: ColumnSpec = {
|
|
107
|
+
field: {
|
|
108
|
+
name: 'collectionCount',
|
|
109
|
+
type: 'number',
|
|
110
|
+
displayName: '# GCs'
|
|
111
|
+
},
|
|
112
|
+
...msCol
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
export const pctCollectionTime: ColumnSpec = {
|
|
116
|
+
field: {
|
|
117
|
+
name: 'pctCollectionTime',
|
|
118
|
+
type: 'number',
|
|
119
|
+
displayName: '% Time in GC'
|
|
92
120
|
},
|
|
93
121
|
...pctCol
|
|
94
122
|
};
|
|
@@ -7,7 +7,9 @@
|
|
|
7
7
|
import {ChartModel} from '@xh/hoist/cmp/chart';
|
|
8
8
|
import {GridModel} from '@xh/hoist/cmp/grid';
|
|
9
9
|
import {HoistModel, LoadSpec, managed, XH} from '@xh/hoist/core';
|
|
10
|
+
import {lengthIs, required} from '@xh/hoist/data';
|
|
10
11
|
import {fmtTime} from '@xh/hoist/format';
|
|
12
|
+
import {Icon} from '@xh/hoist/icon';
|
|
11
13
|
import {forOwn, sortBy} from 'lodash';
|
|
12
14
|
import * as MCol from '../../monitor/MonitorColumns';
|
|
13
15
|
|
|
@@ -15,6 +17,14 @@ export class MemoryMonitorModel extends HoistModel {
|
|
|
15
17
|
@managed gridModel: GridModel;
|
|
16
18
|
@managed chartModel: ChartModel;
|
|
17
19
|
|
|
20
|
+
get enabled(): boolean {
|
|
21
|
+
return this.conf.enabled;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
get heapDumpDir(): string {
|
|
25
|
+
return this.conf.heapDumpDir;
|
|
26
|
+
}
|
|
27
|
+
|
|
18
28
|
constructor() {
|
|
19
29
|
super();
|
|
20
30
|
|
|
@@ -24,13 +34,25 @@ export class MemoryMonitorModel extends HoistModel {
|
|
|
24
34
|
filterModel: true,
|
|
25
35
|
store: {idSpec: 'timestamp'},
|
|
26
36
|
colDefaults: {filterable: true},
|
|
37
|
+
headerMenuDisplay: 'hover',
|
|
27
38
|
columns: [
|
|
28
39
|
MCol.timestamp,
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
40
|
+
{
|
|
41
|
+
groupId: 'heap',
|
|
42
|
+
headerAlign: 'center',
|
|
43
|
+
children: [
|
|
44
|
+
MCol.totalHeapMb,
|
|
45
|
+
MCol.maxHeapMb,
|
|
46
|
+
MCol.freeHeapMb,
|
|
47
|
+
MCol.usedHeapMb,
|
|
48
|
+
MCol.usedPctMax
|
|
49
|
+
]
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
groupId: 'GC',
|
|
53
|
+
headerAlign: 'center',
|
|
54
|
+
children: [MCol.collectionCount, MCol.avgCollectionTime, MCol.pctCollectionTime]
|
|
55
|
+
}
|
|
34
56
|
]
|
|
35
57
|
});
|
|
36
58
|
|
|
@@ -58,12 +80,14 @@ export class MemoryMonitorModel extends HoistModel {
|
|
|
58
80
|
yAxis: [
|
|
59
81
|
{
|
|
60
82
|
floor: 0,
|
|
61
|
-
|
|
83
|
+
height: '20%',
|
|
84
|
+
title: {text: 'GC Avg (ms)'}
|
|
62
85
|
},
|
|
63
86
|
{
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
87
|
+
floor: 0,
|
|
88
|
+
top: '30%',
|
|
89
|
+
height: '70%',
|
|
90
|
+
title: {text: 'Heap (mb)'}
|
|
67
91
|
}
|
|
68
92
|
],
|
|
69
93
|
tooltip: {outside: true, shared: true}
|
|
@@ -91,34 +115,46 @@ export class MemoryMonitorModel extends HoistModel {
|
|
|
91
115
|
// Process further for chart series.
|
|
92
116
|
const maxSeries = [],
|
|
93
117
|
totalSeries = [],
|
|
94
|
-
usedSeries = []
|
|
118
|
+
usedSeries = [],
|
|
119
|
+
avgGCSeries = [];
|
|
95
120
|
|
|
96
121
|
snaps.forEach(snap => {
|
|
97
122
|
maxSeries.push([snap.timestamp, snap.maxHeapMb]);
|
|
98
123
|
totalSeries.push([snap.timestamp, snap.totalHeapMb]);
|
|
99
124
|
usedSeries.push([snap.timestamp, snap.usedHeapMb]);
|
|
125
|
+
|
|
126
|
+
avgGCSeries.push([snap.timestamp, snap.avgCollectionTime]);
|
|
100
127
|
});
|
|
101
128
|
|
|
102
129
|
chartModel.setSeries([
|
|
103
130
|
{
|
|
104
|
-
name: '
|
|
131
|
+
name: 'GC Avg',
|
|
132
|
+
data: avgGCSeries,
|
|
133
|
+
step: true,
|
|
134
|
+
yAxis: 0
|
|
135
|
+
},
|
|
136
|
+
{
|
|
137
|
+
name: 'Heap Max',
|
|
105
138
|
data: maxSeries,
|
|
106
139
|
color: '#ef6c00',
|
|
107
|
-
step: true
|
|
140
|
+
step: true,
|
|
141
|
+
yAxis: 1
|
|
108
142
|
},
|
|
109
143
|
{
|
|
110
|
-
name: 'Total',
|
|
144
|
+
name: 'Heap Total',
|
|
111
145
|
data: totalSeries,
|
|
112
146
|
color: '#1976d2',
|
|
113
|
-
step: true
|
|
147
|
+
step: true,
|
|
148
|
+
yAxis: 1
|
|
114
149
|
},
|
|
115
150
|
{
|
|
116
|
-
name: 'Used',
|
|
151
|
+
name: 'Heap Used',
|
|
117
152
|
type: 'area',
|
|
118
153
|
data: usedSeries,
|
|
119
154
|
color: '#bd7c7c',
|
|
120
155
|
fillOpacity: 0.3,
|
|
121
|
-
lineWidth: 1
|
|
156
|
+
lineWidth: 1,
|
|
157
|
+
yAxis: 1
|
|
122
158
|
}
|
|
123
159
|
]);
|
|
124
160
|
} catch (e) {
|
|
@@ -145,4 +181,32 @@ export class MemoryMonitorModel extends HoistModel {
|
|
|
145
181
|
XH.handleException(e);
|
|
146
182
|
}
|
|
147
183
|
}
|
|
184
|
+
|
|
185
|
+
async dumpHeapAsync() {
|
|
186
|
+
try {
|
|
187
|
+
const appEnv = XH.getEnv('appEnvironment').toLowerCase(),
|
|
188
|
+
filename = await XH.prompt({
|
|
189
|
+
title: 'Dump Heap',
|
|
190
|
+
icon: Icon.fileArchive(),
|
|
191
|
+
message: `Specify a filename for the heap dump (to be saved in ${this.heapDumpDir})`,
|
|
192
|
+
input: {
|
|
193
|
+
rules: [required, lengthIs({min: 3, max: 250})],
|
|
194
|
+
initialValue: `${XH.appCode}_${appEnv}_${Date.now()}.hprof`
|
|
195
|
+
}
|
|
196
|
+
});
|
|
197
|
+
if (!filename) return;
|
|
198
|
+
await XH.fetchJson({
|
|
199
|
+
url: 'memoryMonitorAdmin/dumpHeap',
|
|
200
|
+
params: {filename}
|
|
201
|
+
}).linkTo(this.loadModel);
|
|
202
|
+
await this.loadAsync();
|
|
203
|
+
XH.successToast('Heap dumped successfully to ' + filename);
|
|
204
|
+
} catch (e) {
|
|
205
|
+
XH.handleException(e);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
private get conf() {
|
|
210
|
+
return XH.getConf('xhMemoryMonitoringConfig', {heapDumpDir: null, enabled: true});
|
|
211
|
+
}
|
|
148
212
|
}
|
|
@@ -10,15 +10,24 @@ import {grid, gridCountLabel} from '@xh/hoist/cmp/grid';
|
|
|
10
10
|
import {filler} from '@xh/hoist/cmp/layout';
|
|
11
11
|
import {creates, hoistCmp} from '@xh/hoist/core';
|
|
12
12
|
import {button, exportButton} from '@xh/hoist/desktop/cmp/button';
|
|
13
|
+
import {errorMessage} from '@xh/hoist/desktop/cmp/error';
|
|
13
14
|
import {panel} from '@xh/hoist/desktop/cmp/panel';
|
|
14
15
|
import {Icon} from '@xh/hoist/icon';
|
|
15
16
|
import {AppModel} from '@xh/hoist/admin/AppModel';
|
|
17
|
+
import {isNil} from 'lodash';
|
|
16
18
|
|
|
17
19
|
export const memoryMonitorPanel = hoistCmp.factory({
|
|
18
20
|
model: creates(MemoryMonitorModel),
|
|
19
21
|
|
|
20
22
|
render({model}) {
|
|
21
|
-
|
|
23
|
+
if (!model.enabled) {
|
|
24
|
+
return errorMessage({
|
|
25
|
+
error: 'Memory Monitoring disabled via xhMemoryMonitoringConfig.'
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const {readonly} = AppModel,
|
|
30
|
+
dumpDisabled = isNil(model.heapDumpDir);
|
|
22
31
|
return panel({
|
|
23
32
|
tbar: [
|
|
24
33
|
button({
|
|
@@ -27,6 +36,7 @@ export const memoryMonitorPanel = hoistCmp.factory({
|
|
|
27
36
|
omit: readonly,
|
|
28
37
|
onClick: () => model.takeSnapshotAsync()
|
|
29
38
|
}),
|
|
39
|
+
'-',
|
|
30
40
|
button({
|
|
31
41
|
text: 'Request GC',
|
|
32
42
|
icon: Icon.trash(),
|
|
@@ -34,6 +44,17 @@ export const memoryMonitorPanel = hoistCmp.factory({
|
|
|
34
44
|
omit: readonly,
|
|
35
45
|
onClick: () => model.requestGcAsync()
|
|
36
46
|
}),
|
|
47
|
+
button({
|
|
48
|
+
text: 'Dump Heap',
|
|
49
|
+
icon: Icon.fileArchive(),
|
|
50
|
+
intent: 'danger',
|
|
51
|
+
omit: readonly,
|
|
52
|
+
disabled: dumpDisabled,
|
|
53
|
+
tooltip: dumpDisabled
|
|
54
|
+
? 'Missing required config xhMemoryMonitoringConfig.heapDumpDir'
|
|
55
|
+
: null,
|
|
56
|
+
onClick: () => model.dumpHeapAsync()
|
|
57
|
+
}),
|
|
37
58
|
filler(),
|
|
38
59
|
gridCountLabel({unit: 'snapshot'}),
|
|
39
60
|
'-',
|
package/cmp/form/Form.ts
CHANGED
|
@@ -4,14 +4,7 @@
|
|
|
4
4
|
*
|
|
5
5
|
* Copyright © 2022 Extremely Heavy Industries Inc.
|
|
6
6
|
*/
|
|
7
|
-
import {
|
|
8
|
-
BoxProps,
|
|
9
|
-
DefaultHoistProps,
|
|
10
|
-
elementFactory,
|
|
11
|
-
hoistCmp,
|
|
12
|
-
HoistProps,
|
|
13
|
-
uses
|
|
14
|
-
} from '@xh/hoist/core';
|
|
7
|
+
import {DefaultHoistProps, elementFactory, hoistCmp, HoistProps, uses} from '@xh/hoist/core';
|
|
15
8
|
import equal from 'fast-deep-equal';
|
|
16
9
|
import {createContext, useContext} from 'react';
|
|
17
10
|
import {useCached} from '@xh/hoist/utils/react';
|
|
@@ -31,7 +24,7 @@ export interface FormContextType {
|
|
|
31
24
|
export const FormContext = createContext<FormContextType>({});
|
|
32
25
|
const formContextProvider = elementFactory(FormContext.Provider);
|
|
33
26
|
|
|
34
|
-
export interface FormProps extends HoistProps<FormModel
|
|
27
|
+
export interface FormProps extends HoistProps<FormModel> {
|
|
35
28
|
/**
|
|
36
29
|
* Defaults for certain props on child/nested FormFields.
|
|
37
30
|
* @see FormField (note there are both desktop and mobile implementations).
|
|
@@ -69,7 +69,7 @@ async function initServicesInternalAsync(svcs: HoistService[]) {
|
|
|
69
69
|
it.name = svcs[idx].constructor.name;
|
|
70
70
|
});
|
|
71
71
|
|
|
72
|
-
throw
|
|
72
|
+
throw XH.exception({
|
|
73
73
|
message: [
|
|
74
74
|
'Failed to initialize services: ',
|
|
75
75
|
...errs.map(it => it.reason.message + ' (' + it.name + ')')
|
package/package.json
CHANGED
package/utils/js/LangUtils.ts
CHANGED
|
@@ -236,7 +236,7 @@ export function pluralize(s: string, count?: number, includeCount?: boolean): st
|
|
|
236
236
|
}
|
|
237
237
|
|
|
238
238
|
/**
|
|
239
|
-
* Returns the number with an ordinal suffix (
|
|
239
|
+
* Returns the number with an ordinal suffix (i.e. 1 becomes '1st', 11 becomes '11th').
|
|
240
240
|
*
|
|
241
241
|
* @param n - the number to ordinalize
|
|
242
242
|
*/
|