@xh/hoist 44.0.0 → 44.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,30 @@
1
1
  # Changelog
2
2
 
3
+ ## v44.1.0 - 2021-11-08
4
+
5
+ ### 🎁 New Features
6
+
7
+ * Changes to App Options are now tracked in the admin activity tab.
8
+ * New Server > Environment tab added to Admin Console to display UI server environment variables and
9
+ JVM system properties. (Requires `hoist-core >= 10.1` to enable this optional feature.)
10
+ * Provided observable getters `XH.viewportSize`, `XH.isPortrait` and `XH.isLandscape` to allow apps
11
+ to react to changes in viewport size and orientation.
12
+
13
+ ### 🐞 Bug Fixes
14
+
15
+ * Desktop inline grid editor `DateInput` now reliably shows its date picker pop-up aligned with the
16
+ grid cell under edit.
17
+ * Desktop `Select.hideDropdownIndicator` now defaults to `true` on tablet devices due to UX bugs
18
+ with the select library component and touch devices.
19
+ * Ensure `Column.autosizeBufferPx` is respected if provided.
20
+
21
+ ### ✨ Style
22
+
23
+ * New `--xh-menu-item` CSS vars added, with tweaks to default desktop menu styling.
24
+ * Highlight background color added to mobile menu items while pressed.
25
+
26
+ [Commit Log](https://github.com/xh/hoist-react/compare/v44.0.0...v44.1.0)
27
+
3
28
  ## v44.0.0 - 2021-10-26
4
29
 
5
30
  ⚠ NOTE - apps must update to `hoist-core >= 10.0.0` when taking this hoist-react update.
package/SECURITY.md ADDED
@@ -0,0 +1,23 @@
1
+ # Security Policy
2
+
3
+ Extremely Heavy takes the security of its code and clients very seriously, and we welcome any and
4
+ all reports of possible vulnerabilities or security-related issues with both Hoist code and its
5
+ declared dependencies.
6
+
7
+ ## Reporting a Vulnerability
8
+
9
+ To report any issues, or if you have any questions, please contact us immediately at support@xh.io.
10
+ We will respond to all genuine, security-related reports or questions within one US business day.
11
+
12
+ ## Secure Usage
13
+
14
+ Hoist is a toolkit designed to allow professional developers to build advanced enterprise web
15
+ applications with the support of XH in the form of direct development, co-development, and/or
16
+ consulting services. As such, many decisions critical to the security of Hoist-powered applications
17
+ are highly specific to implementation choices made during the design and development process,
18
+ including but not limited to the choice of other project dependencies, creation and interaction
19
+ with any Hoist or third-party server APIs, input sanitization, authentication protocols, and more.
20
+
21
+ Hoist is not and does not claim to be fully secure "out of the box" - it is dependent upon
22
+ application developers to make and implement security decisions appropriate to their particular
23
+ application and its deployment.
package/admin/AppModel.js CHANGED
@@ -5,7 +5,7 @@
5
5
  * Copyright © 2021 Extremely Heavy Industries Inc.
6
6
  */
7
7
  import {TabContainerModel} from '@xh/hoist/cmp/tab';
8
- import {HoistAppModel, managed} from '@xh/hoist/core';
8
+ import {HoistAppModel, managed, XH} from '@xh/hoist/core';
9
9
  import {Icon} from '@xh/hoist/icon';
10
10
  import {activityTab} from './tabs/activity/ActivityTab';
11
11
  import {generalTab} from './tabs/general/GeneralTab';
@@ -69,8 +69,10 @@ export class AppModel extends HoistAppModel {
69
69
  children: [
70
70
  {name: 'logViewer', path: '/logViewer'},
71
71
  {name: 'logLevels', path: '/logLevels'},
72
+ {name: 'environment', path: '/environment'},
72
73
  {name: 'services', path: '/services'},
73
74
  {name: 'ehCache', path: '/ehCache'},
75
+ {name: 'memory', path: '/memory'},
74
76
  {name: 'webSockets', path: '/webSockets'}
75
77
  ]
76
78
  },
@@ -79,8 +81,7 @@ export class AppModel extends HoistAppModel {
79
81
  path: '/monitor',
80
82
  children: [
81
83
  {name: 'status', path: '/status'},
82
- {name: 'config', path: '/config'},
83
- {name: 'memory', path: '/memory'}
84
+ {name: 'config', path: '/config'}
84
85
  ]
85
86
  },
86
87
  {
@@ -97,11 +98,32 @@ export class AppModel extends HoistAppModel {
97
98
 
98
99
  createTabs() {
99
100
  return [
100
- {id: 'general', icon: Icon.info(), content: generalTab},
101
- {id: 'activity', icon: Icon.analytics(), content: activityTab},
102
- {id: 'server', icon: Icon.server(), content: serverTab},
103
- {id: 'monitor', icon: Icon.shieldCheck(), content: monitorTab},
104
- {id: 'userData', icon: Icon.users(), content: userDataTab}
101
+ {
102
+ id: 'general',
103
+ icon: Icon.info(),
104
+ content: generalTab
105
+ },
106
+ {
107
+ id: 'activity',
108
+ icon: Icon.analytics(),
109
+ content: activityTab
110
+ },
111
+ {
112
+ id: 'server',
113
+ icon: Icon.server(),
114
+ content: serverTab
115
+ },
116
+ {
117
+ id: 'monitor',
118
+ icon: Icon.shieldCheck(),
119
+ content: monitorTab,
120
+ omit: !XH.getConf('xhEnableMonitoring', true)
121
+ },
122
+ {
123
+ id: 'userData',
124
+ icon: Icon.users(),
125
+ content: userDataTab
126
+ }
105
127
  ];
106
128
  }
107
129
  }
@@ -4,27 +4,21 @@
4
4
  *
5
5
  * Copyright © 2021 Extremely Heavy Industries Inc.
6
6
  */
7
- import {XH} from '@xh/hoist/core';
8
- import {memoryMonitorPanel} from '@xh/hoist/admin/tabs/monitor/MemoryMonitorPanel';
9
7
  import {tabContainer} from '@xh/hoist/cmp/tab';
10
8
  import {hoistCmp} from '@xh/hoist/core';
11
9
  import {Icon} from '@xh/hoist/icon';
12
10
  import {monitorEditorPanel} from './MonitorEditorPanel';
13
11
  import {monitorResultsPanel} from './MonitorResultsPanel';
14
12
 
15
- export const monitorTab = hoistCmp.factory(
16
- () => {
17
- const omitStatusMonitoring = !XH.getConf('xhEnableMonitoring', true);
18
- return tabContainer({
19
- model: {
20
- route: 'default.monitor',
21
- switcher: {orientation: 'left'},
22
- tabs: [
23
- {id: 'status', icon: Icon.shieldCheck(), content: monitorResultsPanel, omit: omitStatusMonitoring},
24
- {id: 'config', icon: Icon.settings(), content: monitorEditorPanel, omit: omitStatusMonitoring},
25
- {id: 'memory', icon: Icon.server(), content: memoryMonitorPanel}
26
- ]
27
- }
28
- });
29
- }
30
- );
13
+ export const monitorTab = hoistCmp.factory(() => {
14
+ return tabContainer({
15
+ model: {
16
+ route: 'default.monitor',
17
+ switcher: {orientation: 'left'},
18
+ tabs: [
19
+ {id: 'status', icon: Icon.shieldCheck(), content: monitorResultsPanel},
20
+ {id: 'config', icon: Icon.settings(), content: monitorEditorPanel}
21
+ ]
22
+ }
23
+ });
24
+ });
@@ -7,15 +7,18 @@
7
7
 
8
8
  .xh-status-tile {
9
9
  color: white;
10
- border: 1px solid white;
11
- border-radius: var(--xh-border-radius);
10
+ border: var(--xh-border-solid);
12
11
  overflow: hidden;
13
12
 
14
- &-ok { background-color: var(--xh-green); }
13
+ .xh-dark & {
14
+ border-color: white;
15
+ }
16
+
17
+ &-ok { background-color: var(--xh-intent-success-darker); }
15
18
 
16
- &-warn { background-color: var(--xh-orange); }
19
+ &-warn { background-color: var(--xh-intent-warning-darker); }
17
20
 
18
- &-fail { background-color: var(--xh-red); }
21
+ &-fail { background-color: var(--xh-intent-danger-darker); }
19
22
 
20
23
  &-inactive { background-color: var(--xh-gray-dark); }
21
24
 
@@ -4,12 +4,14 @@
4
4
  *
5
5
  * Copyright © 2021 Extremely Heavy Industries Inc.
6
6
  */
7
+ import {serverEnvPanel} from '@xh/hoist/admin/tabs/server/environment/ServerEnvPanel';
7
8
  import {tabContainer} from '@xh/hoist/cmp/tab';
8
9
  import {hoistCmp, XH} from '@xh/hoist/core';
9
10
  import {Icon} from '@xh/hoist/icon';
10
11
  import {ehCachePanel} from './ehcache/EhCachePanel';
11
12
  import {logLevelPanel} from './logLevel/LogLevelPanel';
12
13
  import {logViewer} from './logViewer/LogViewer';
14
+ import {memoryMonitorPanel} from './memory/MemoryMonitorPanel';
13
15
  import {servicePanel} from './services/ServicePanel';
14
16
  import {webSocketPanel} from './websocket/WebSocketPanel';
15
17
 
@@ -21,6 +23,8 @@ export const serverTab = hoistCmp.factory(
21
23
  tabs: [
22
24
  {id: 'logViewer', icon: Icon.fileText(), content: logViewer, omit: !XH.getConf('xhEnableLogViewer', true)},
23
25
  {id: 'logLevels', icon: Icon.settings(), content: logLevelPanel},
26
+ {id: 'memory', title: 'Memory Monitor', icon: Icon.server(), content: memoryMonitorPanel},
27
+ {id: 'environment', icon: Icon.globe(), content: serverEnvPanel},
24
28
  {id: 'services', icon: Icon.gears(), content: servicePanel},
25
29
  {id: 'ehCache', icon: Icon.database(), title: 'Caches', content: ehCachePanel},
26
30
  {id: 'webSockets', title: 'WebSockets', icon: Icon.bolt(), content: webSocketPanel}
@@ -5,7 +5,7 @@
5
5
  * Copyright © 2021 Extremely Heavy Industries Inc.
6
6
  */
7
7
  import {grid, gridCountLabel} from '@xh/hoist/cmp/grid';
8
- import {filler} from '@xh/hoist/cmp/layout';
8
+ import {filler, span} from '@xh/hoist/cmp/layout';
9
9
  import {storeFilterField} from '@xh/hoist/cmp/store';
10
10
  import {creates, hoistCmp} from '@xh/hoist/core';
11
11
  import {button, exportButton} from '@xh/hoist/desktop/cmp/button';
@@ -20,15 +20,22 @@ export const ehCachePanel = hoistCmp.factory({
20
20
  return panel({
21
21
  mask: 'onLoad',
22
22
  tbar: [
23
+ Icon.info(),
24
+ span({
25
+ item: 'Hibernate (Ehcache) caches for server-side domain objects',
26
+ className: 'xh-bold'
27
+ }),
28
+ filler(),
23
29
  button({
24
30
  icon: Icon.reset(),
25
31
  text: 'Clear All',
26
32
  intent: 'danger',
27
33
  onClick: () => model.clearAllAsync()
28
34
  }),
29
- filler(),
35
+ '-',
30
36
  gridCountLabel({unit: 'cache'}),
31
- storeFilterField(),
37
+ '-',
38
+ storeFilterField({matchMode: 'any'}),
32
39
  exportButton()
33
40
  ],
34
41
  item: grid()
@@ -0,0 +1,68 @@
1
+ import {GridModel} from '@xh/hoist/cmp/grid';
2
+ import {HoistModel, managed, XH} from '@xh/hoist/core';
3
+ import {FieldType} from '@xh/hoist/data';
4
+ import {checkMinVersion} from '@xh/hoist/utils/js';
5
+ import {forOwn} from 'lodash';
6
+
7
+ const {STRING} = FieldType;
8
+
9
+ /**
10
+ * Model/tab to list server-side environment variables and JVM system properties, as loaded from
11
+ * a dedicated admin-only endpoint.
12
+ */
13
+ export class ServerEnvModel extends HoistModel {
14
+
15
+ /** @member {GridModel} */
16
+ @managed gridModel;
17
+
18
+ get minVersionWarning() {
19
+ const minVersion = '10.1.0',
20
+ currVersion = XH.environmentService.get('hoistCoreVersion');
21
+ return checkMinVersion(currVersion, minVersion) ? null : `This feature requires Hoist Core v${minVersion} or greater.`;
22
+ }
23
+
24
+ constructor() {
25
+ super();
26
+
27
+ this.gridModel = new GridModel({
28
+ groupBy: 'type',
29
+ sortBy: 'name',
30
+ enableExport: true,
31
+ store: {idSpec: XH.genId},
32
+ columns: [
33
+ {
34
+ field: {name: 'type', type: STRING},
35
+ hidden: true
36
+ },
37
+ {
38
+ field: {name: 'name', type: STRING},
39
+ width: 300,
40
+ cellClass: 'xh-font-family-mono'
41
+ },
42
+ {
43
+ field: {name: 'value', type: STRING},
44
+ flex: 1,
45
+ cellClass: 'xh-font-family-mono',
46
+ autoHeight: true
47
+ }
48
+ ]
49
+ });
50
+ }
51
+
52
+ async doLoadAsync(loadSpec) {
53
+ if (this.minVersionWarning) return;
54
+
55
+ const resp = await XH.fetchJson({url: 'envAdmin'}),
56
+ data = [];
57
+
58
+ forOwn(resp.environment, (value, name) => {
59
+ data.push({type: 'Environment Variables', value, name});
60
+ });
61
+
62
+ forOwn(resp.properties, (value, name) => {
63
+ data.push({type: 'System Properties', value, name});
64
+ });
65
+
66
+ this.gridModel.loadData(data);
67
+ }
68
+ }
@@ -0,0 +1,36 @@
1
+ import {ServerEnvModel} from '@xh/hoist/admin/tabs/server/environment/ServerEnvModel';
2
+ import {grid, gridCountLabel} from '@xh/hoist/cmp/grid';
3
+ import {filler, placeholder, span} from '@xh/hoist/cmp/layout';
4
+ import {storeFilterField} from '@xh/hoist/cmp/store';
5
+ import {creates, hoistCmp} from '@xh/hoist/core';
6
+ import {exportButton} from '@xh/hoist/desktop/cmp/button';
7
+ import {errorMessage} from '@xh/hoist/desktop/cmp/error';
8
+ import {panel} from '@xh/hoist/desktop/cmp/panel';
9
+ import {Icon} from '@xh/hoist/icon';
10
+
11
+ export const serverEnvPanel = hoistCmp.factory({
12
+ model: creates(ServerEnvModel),
13
+
14
+ /** @param {ServerEnvModel} model */
15
+ render({model}) {
16
+ const {lastLoadException, minVersionWarning} = model;
17
+ if (minVersionWarning) return placeholder(minVersionWarning);
18
+
19
+ return panel({
20
+ tbar: [
21
+ Icon.info(),
22
+ span({
23
+ item: 'Server-side environment variables and JVM system properties',
24
+ className: 'xh-bold'
25
+ }),
26
+ filler(),
27
+ gridCountLabel({unit: 'entries'}),
28
+ '-',
29
+ storeFilterField({matchMode: 'any'}),
30
+ exportButton()
31
+ ],
32
+ item: lastLoadException ? errorMessage({error: lastLoadException}) : grid(),
33
+ mask: 'onLoad'
34
+ });
35
+ }
36
+ });
@@ -6,11 +6,11 @@
6
6
  */
7
7
  import {ChartModel} from '@xh/hoist/cmp/chart';
8
8
  import {GridModel} from '@xh/hoist/cmp/grid';
9
- import {HoistModel, XH, managed} from '@xh/hoist/core';
9
+ import {HoistModel, managed, XH} from '@xh/hoist/core';
10
10
  import {fmtTime} from '@xh/hoist/format';
11
11
  import {checkMinVersion} from '@xh/hoist/utils/js';
12
12
  import {forOwn, sortBy} from 'lodash';
13
- import * as MCol from './MonitorColumns';
13
+ import * as MCol from '../../monitor/MonitorColumns';
14
14
 
15
15
  export class MemoryMonitorModel extends HoistModel {
16
16
 
@@ -1,4 +1,4 @@
1
- import {MemoryMonitorModel} from '@xh/hoist/admin/tabs/monitor/MemoryMonitorModel';
1
+ import {MemoryMonitorModel} from '@xh/hoist/admin/tabs/server/memory/MemoryMonitorModel';
2
2
  import {chart} from '@xh/hoist/cmp/chart';
3
3
  import {grid, gridCountLabel} from '@xh/hoist/cmp/grid';
4
4
  import {filler, placeholder} from '@xh/hoist/cmp/layout';
@@ -30,6 +30,7 @@ export const memoryMonitorPanel = hoistCmp.factory({
30
30
  }),
31
31
  filler(),
32
32
  gridCountLabel({unit: 'snapshot'}),
33
+ '-',
33
34
  exportButton()
34
35
  ],
35
36
  items: [
@@ -5,7 +5,7 @@
5
5
  * Copyright © 2021 Extremely Heavy Industries Inc.
6
6
  */
7
7
  import {grid, gridCountLabel} from '@xh/hoist/cmp/grid';
8
- import {filler} from '@xh/hoist/cmp/layout';
8
+ import {filler, span} from '@xh/hoist/cmp/layout';
9
9
  import {storeFilterField} from '@xh/hoist/cmp/store';
10
10
  import {creates, hoistCmp} from '@xh/hoist/core';
11
11
  import {button, exportButton} from '@xh/hoist/desktop/cmp/button';
@@ -20,6 +20,12 @@ export const servicePanel = hoistCmp.factory({
20
20
  return panel({
21
21
  mask: 'onLoad',
22
22
  tbar: [
23
+ Icon.info(),
24
+ span({
25
+ item: 'Service classes for server-side Hoist and application-level business logic',
26
+ className: 'xh-bold'
27
+ }),
28
+ filler(),
23
29
  button({
24
30
  icon: Icon.reset(),
25
31
  text: 'Clear Selected',
@@ -27,9 +33,10 @@ export const servicePanel = hoistCmp.factory({
27
33
  onClick: () => model.clearCachesAsync(),
28
34
  disabled: model.gridModel.selModel.isEmpty
29
35
  }),
30
- filler(),
36
+ '-',
31
37
  gridCountLabel({unit: 'service'}),
32
- storeFilterField(),
38
+ '-',
39
+ storeFilterField({matchMode: 'any'}),
33
40
  exportButton()
34
41
  ],
35
42
  item: grid({
@@ -15,6 +15,7 @@ import {ImpersonationBarModel} from './ImpersonationBarModel';
15
15
  import {MessageSourceModel} from './MessageSourceModel';
16
16
  import {OptionsDialogModel} from './OptionsDialogModel';
17
17
  import {SizingModeModel} from './SizingModeModel';
18
+ import {ViewportSizeModel} from './ViewportSizeModel';
18
19
  import {ThemeModel} from './ThemeModel';
19
20
  import {ToastSourceModel} from './ToastSourceModel';
20
21
 
@@ -42,6 +43,7 @@ export class AppContainerModel extends HoistModel {
42
43
 
43
44
  @managed refreshContextModel = new RootRefreshContextModel();
44
45
  @managed sizingModeModel = new SizingModeModel();
46
+ @managed viewportSizeModel = new ViewportSizeModel();
45
47
  @managed themeModel = new ThemeModel();
46
48
 
47
49
  init() {
@@ -58,6 +60,7 @@ export class AppContainerModel extends HoistModel {
58
60
  this.toastSourceModel,
59
61
  this.refreshContextModel,
60
62
  this.sizingModeModel,
63
+ this.viewportSizeModel,
61
64
  this.themeModel
62
65
  ];
63
66
  models.forEach(it => {
@@ -7,7 +7,7 @@
7
7
  import {FormModel} from '@xh/hoist/cmp/form';
8
8
  import {HoistModel, managed, TaskObserver, XH} from '@xh/hoist/core';
9
9
  import {action, computed, makeObservable, observable} from '@xh/hoist/mobx';
10
- import {assign} from 'lodash';
10
+ import {assign, mapValues, pickBy} from 'lodash';
11
11
  import {AppOption} from './AppOption';
12
12
 
13
13
  /**
@@ -113,11 +113,18 @@ export class OptionsDialogModel extends HoistModel {
113
113
  }
114
114
 
115
115
  async doSaveAsync() {
116
- const {formModel} = this;
117
- const promises = this.options.map(option => {
118
- return option.setValueAsync(option.name, formModel.values[option.name]);
119
- });
116
+ const {formModel} = this,
117
+ dirtyFields = pickBy(formModel.fields, {isDirty: true}),
118
+ promises = this.options
119
+ .filter(o => dirtyFields[o.name])
120
+ .map(o => o.setValueAsync(o.name, dirtyFields[o.name].value));
120
121
  await Promise.allSettled(promises);
121
- return XH.prefService.pushPendingAsync();
122
+ await XH.prefService.pushPendingAsync();
123
+
124
+ XH.track({
125
+ message: 'Changed options',
126
+ category: 'App',
127
+ data: mapValues(dirtyFields, f => ({value: f.value, oldValue: f.initialValue}))
128
+ });
122
129
  }
123
130
  }
@@ -0,0 +1,49 @@
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 © 2021 Extremely Heavy Industries Inc.
6
+ */
7
+ import {HoistModel} from '@xh/hoist/core';
8
+ import {action, computed, observable, makeObservable} from '@xh/hoist/mobx';
9
+
10
+ /**
11
+ * Track observable width / height of the browser viewport, and provide observable
12
+ * access to device orientation
13
+ *
14
+ * @private
15
+ */
16
+ export class ViewportSizeModel extends HoistModel {
17
+
18
+ /** @member {Object} - contains `width` and `height` in pixels */
19
+ @observable.ref size;
20
+
21
+ /** @returns {boolean} */
22
+ @computed
23
+ get isPortrait() {
24
+ return this.size.width < this.size.height;
25
+ }
26
+
27
+ /** @returns {boolean} */
28
+ get isLandscape() {
29
+ return !this.isPortrait;
30
+ }
31
+
32
+ constructor() {
33
+ super();
34
+ makeObservable(this);
35
+ window.addEventListener('resize', () => this.setViewportSize());
36
+ this.setViewportSize();
37
+ }
38
+
39
+ //---------------------
40
+ // Implementation
41
+ //---------------------
42
+ @action
43
+ setViewportSize() {
44
+ this.size = {
45
+ width: window.innerWidth,
46
+ height: window.innerHeight
47
+ };
48
+ }
49
+ }
@@ -391,21 +391,23 @@
391
391
  }
392
392
 
393
393
  //------------------------
394
- // Context Menu (matching ag-Grid provided menus to BP menus)
394
+ // Context Menu
395
395
  //------------------------
396
396
  .ag-theme-balham .ag-menu,
397
397
  .ag-theme-balham-dark .ag-menu {
398
398
  font-family: var(--xh-font-family);
399
+ font-size: var(--xh-menu-item-font-size-px);
400
+ color: var(--xh-menu-item-text-color);
399
401
 
400
402
  // Minimal/high-contrast bg
401
- background-color: var(--xh-bg);
402
- border: var(--xh-border-solid);
403
+ background-color: var(--xh-menu-bg);
404
+ border: var(--xh-menu-border);
403
405
 
404
406
  // Matching box-shadow of Blueprint context menu popover
405
407
  box-shadow: 0 0 0 1px rgba(16, 22, 26, 0.2), 0 2px 4px rgba(16, 22, 26, 0.4), 0 8px 24px rgba(16, 22, 26, 0.4);
406
408
 
407
409
  .ag-menu-option.ag-menu-option-active {
408
- background-color: var(--xh-bg-highlight);
410
+ background-color: var(--xh-menu-item-highlight-bg);
409
411
  }
410
412
 
411
413
  .ag-menu-option-icon {
@@ -421,6 +423,6 @@
421
423
 
422
424
  // Keep the submenu caret in the standard text color
423
425
  .ag-menu-option-popup-pointer {
424
- color: var(--xh-text-color);
426
+ color: var(--xh-menu-item-text-color);
425
427
  }
426
428
  }