@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 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 updated to import styles from their new location. The recommended imports are now:
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-no-font.css';
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: a plugin for rich web-application development provided by ',
94
+ 'Built with Hoist, a toolkit for rapid application development from ',
95
95
  a({
96
- href: 'http://xh.io',
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 usedPctTotal: ColumnSpec = {
88
+ export const usedPctMax: ColumnSpec = {
88
89
  field: {
89
- name: 'usedPctTotal',
90
+ name: 'usedPctMax',
90
91
  type: 'number',
91
- displayName: 'Used (pct Total)'
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
- MCol.totalHeapMb,
30
- MCol.maxHeapMb,
31
- MCol.freeHeapMb,
32
- MCol.usedHeapMb,
33
- MCol.usedPctTotal
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
- title: {text: 'JVM Heap (mb)'}
83
+ height: '20%',
84
+ title: {text: 'GC Avg (ms)'}
62
85
  },
63
86
  {
64
- opposite: true,
65
- linkedTo: 0,
66
- title: {text: undefined}
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: 'Max',
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
- const {readonly} = AppModel;
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>, BoxProps {
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 this.exception({
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xh/hoist",
3
- "version": "56.0.0",
3
+ "version": "56.1.0",
4
4
  "description": "Hoist add-on for building and deploying React Applications.",
5
5
  "repository": "github:xh/hoist-react",
6
6
  "homepage": "https://xh.io",
@@ -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 (ie. 1 => '1st', 11 => '11th').
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
  */