foreman_resource_quota 0.3.0 → 0.4.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.
Files changed (174) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +13 -6
  3. data/app/assets/javascripts/foreman_resource_quota/locale/bn/foreman_resource_quota.js +213 -0
  4. data/app/assets/javascripts/foreman_resource_quota/locale/bn_IN/foreman_resource_quota.js +214 -0
  5. data/app/assets/javascripts/foreman_resource_quota/locale/bqi/foreman_resource_quota.js +213 -0
  6. data/app/assets/javascripts/foreman_resource_quota/locale/ca/foreman_resource_quota.js +214 -0
  7. data/app/assets/javascripts/foreman_resource_quota/locale/cs_CZ/foreman_resource_quota.js +214 -0
  8. data/app/assets/javascripts/foreman_resource_quota/locale/de/foreman_resource_quota.js +214 -0
  9. data/app/assets/javascripts/foreman_resource_quota/locale/de_AT/foreman_resource_quota.js +213 -0
  10. data/app/assets/javascripts/foreman_resource_quota/locale/de_DE/foreman_resource_quota.js +214 -0
  11. data/app/assets/javascripts/foreman_resource_quota/locale/el/foreman_resource_quota.js +214 -0
  12. data/app/assets/javascripts/foreman_resource_quota/locale/en/foreman_resource_quota.js +214 -0
  13. data/app/assets/javascripts/foreman_resource_quota/locale/en_GB/foreman_resource_quota.js +214 -0
  14. data/app/assets/javascripts/foreman_resource_quota/locale/en_US/foreman_resource_quota.js +213 -0
  15. data/app/assets/javascripts/foreman_resource_quota/locale/es/foreman_resource_quota.js +214 -0
  16. data/app/assets/javascripts/foreman_resource_quota/locale/et_EE/foreman_resource_quota.js +213 -0
  17. data/app/assets/javascripts/foreman_resource_quota/locale/fr/foreman_resource_quota.js +214 -0
  18. data/app/assets/javascripts/foreman_resource_quota/locale/gl/foreman_resource_quota.js +214 -0
  19. data/app/assets/javascripts/foreman_resource_quota/locale/gu/foreman_resource_quota.js +214 -0
  20. data/app/assets/javascripts/foreman_resource_quota/locale/he_IL/foreman_resource_quota.js +213 -0
  21. data/app/assets/javascripts/foreman_resource_quota/locale/hi/foreman_resource_quota.js +214 -0
  22. data/app/assets/javascripts/foreman_resource_quota/locale/id/foreman_resource_quota.js +214 -0
  23. data/app/assets/javascripts/foreman_resource_quota/locale/it/foreman_resource_quota.js +214 -0
  24. data/app/assets/javascripts/foreman_resource_quota/locale/ja/foreman_resource_quota.js +214 -0
  25. data/app/assets/javascripts/foreman_resource_quota/locale/ka/foreman_resource_quota.js +214 -0
  26. data/app/assets/javascripts/foreman_resource_quota/locale/kn/foreman_resource_quota.js +214 -0
  27. data/app/assets/javascripts/foreman_resource_quota/locale/ko/foreman_resource_quota.js +214 -0
  28. data/app/assets/javascripts/foreman_resource_quota/locale/ml_IN/foreman_resource_quota.js +213 -0
  29. data/app/assets/javascripts/foreman_resource_quota/locale/mr/foreman_resource_quota.js +214 -0
  30. data/app/assets/javascripts/foreman_resource_quota/locale/nl_NL/foreman_resource_quota.js +214 -0
  31. data/app/assets/javascripts/foreman_resource_quota/locale/or/foreman_resource_quota.js +214 -0
  32. data/app/assets/javascripts/foreman_resource_quota/locale/pa/foreman_resource_quota.js +214 -0
  33. data/app/assets/javascripts/foreman_resource_quota/locale/pl/foreman_resource_quota.js +214 -0
  34. data/app/assets/javascripts/foreman_resource_quota/locale/pl_PL/foreman_resource_quota.js +213 -0
  35. data/app/assets/javascripts/foreman_resource_quota/locale/pt/foreman_resource_quota.js +213 -0
  36. data/app/assets/javascripts/foreman_resource_quota/locale/pt_BR/foreman_resource_quota.js +214 -0
  37. data/app/assets/javascripts/foreman_resource_quota/locale/ro/foreman_resource_quota.js +213 -0
  38. data/app/assets/javascripts/foreman_resource_quota/locale/ro_RO/foreman_resource_quota.js +213 -0
  39. data/app/assets/javascripts/foreman_resource_quota/locale/ru/foreman_resource_quota.js +214 -0
  40. data/app/assets/javascripts/foreman_resource_quota/locale/sl/foreman_resource_quota.js +214 -0
  41. data/app/assets/javascripts/foreman_resource_quota/locale/sv_SE/foreman_resource_quota.js +214 -0
  42. data/app/assets/javascripts/foreman_resource_quota/locale/ta/foreman_resource_quota.js +213 -0
  43. data/app/assets/javascripts/foreman_resource_quota/locale/ta_IN/foreman_resource_quota.js +214 -0
  44. data/app/assets/javascripts/foreman_resource_quota/locale/te/foreman_resource_quota.js +214 -0
  45. data/app/assets/javascripts/foreman_resource_quota/locale/tr/foreman_resource_quota.js +213 -0
  46. data/app/assets/javascripts/foreman_resource_quota/locale/vi/foreman_resource_quota.js +213 -0
  47. data/app/assets/javascripts/foreman_resource_quota/locale/vi_VN/foreman_resource_quota.js +213 -0
  48. data/app/assets/javascripts/foreman_resource_quota/locale/zh/foreman_resource_quota.js +213 -0
  49. data/app/assets/javascripts/foreman_resource_quota/locale/zh_CN/foreman_resource_quota.js +214 -0
  50. data/app/assets/javascripts/foreman_resource_quota/locale/zh_TW/foreman_resource_quota.js +214 -0
  51. data/app/controllers/foreman_resource_quota/api/v2/resource_quotas_controller.rb +17 -10
  52. data/app/controllers/foreman_resource_quota/concerns/api/v2/hosts_controller_extensions.rb +20 -0
  53. data/app/controllers/foreman_resource_quota/concerns/api/v2/usergroups_controller_extensions.rb +19 -0
  54. data/app/controllers/foreman_resource_quota/concerns/api/v2/users_controller_extensions.rb +22 -0
  55. data/app/models/concerns/foreman_resource_quota/host_managed_extensions.rb +9 -7
  56. data/app/views/foreman_resource_quota/api/v2/users/resource_quota.json.rabl +1 -1
  57. data/app/views/foreman_resource_quota/resource_quotas/index.html.erb +1 -1
  58. data/config/initializers/inflections.rb +1 -0
  59. data/lib/foreman_resource_quota/engine.rb +8 -1
  60. data/lib/foreman_resource_quota/register.rb +4 -2
  61. data/lib/foreman_resource_quota/version.rb +1 -1
  62. data/lib/tasks/foreman_resource_quota_tasks.rake +3 -3
  63. data/locale/Makefile +19 -6
  64. data/locale/bn/LC_MESSAGES/foreman_resource_quota.mo +0 -0
  65. data/locale/bn/foreman_resource_quota.po +214 -0
  66. data/locale/bn_IN/LC_MESSAGES/foreman_resource_quota.mo +0 -0
  67. data/locale/bn_IN/foreman_resource_quota.po +219 -0
  68. data/locale/bqi/LC_MESSAGES/foreman_resource_quota.mo +0 -0
  69. data/locale/bqi/foreman_resource_quota.po +215 -0
  70. data/locale/ca/LC_MESSAGES/foreman_resource_quota.mo +0 -0
  71. data/locale/ca/foreman_resource_quota.po +218 -0
  72. data/locale/cs_CZ/LC_MESSAGES/foreman_resource_quota.mo +0 -0
  73. data/locale/cs_CZ/foreman_resource_quota.po +221 -0
  74. data/locale/de/LC_MESSAGES/foreman_resource_quota.mo +0 -0
  75. data/locale/de/foreman_resource_quota.po +223 -0
  76. data/locale/de_AT/LC_MESSAGES/foreman_resource_quota.mo +0 -0
  77. data/locale/de_AT/foreman_resource_quota.po +215 -0
  78. data/locale/de_DE/LC_MESSAGES/foreman_resource_quota.mo +0 -0
  79. data/locale/de_DE/foreman_resource_quota.po +219 -0
  80. data/locale/el/LC_MESSAGES/foreman_resource_quota.mo +0 -0
  81. data/locale/el/foreman_resource_quota.po +218 -0
  82. data/locale/en/LC_MESSAGES/foreman_resource_quota.mo +0 -0
  83. data/locale/en/foreman_resource_quota.po +207 -9
  84. data/locale/en_GB/LC_MESSAGES/foreman_resource_quota.mo +0 -0
  85. data/locale/en_GB/foreman_resource_quota.po +220 -0
  86. data/locale/en_US/LC_MESSAGES/foreman_resource_quota.mo +0 -0
  87. data/locale/en_US/foreman_resource_quota.po +215 -0
  88. data/locale/es/LC_MESSAGES/foreman_resource_quota.mo +0 -0
  89. data/locale/es/foreman_resource_quota.po +223 -0
  90. data/locale/et_EE/LC_MESSAGES/foreman_resource_quota.mo +0 -0
  91. data/locale/et_EE/foreman_resource_quota.po +215 -0
  92. data/locale/foreman_resource_quota.pot +322 -8
  93. data/locale/fr/LC_MESSAGES/foreman_resource_quota.mo +0 -0
  94. data/locale/fr/foreman_resource_quota.po +222 -0
  95. data/locale/gl/LC_MESSAGES/foreman_resource_quota.mo +0 -0
  96. data/locale/gl/foreman_resource_quota.po +218 -0
  97. data/locale/gu/LC_MESSAGES/foreman_resource_quota.mo +0 -0
  98. data/locale/gu/foreman_resource_quota.po +218 -0
  99. data/locale/he_IL/LC_MESSAGES/foreman_resource_quota.mo +0 -0
  100. data/locale/he_IL/foreman_resource_quota.po +216 -0
  101. data/locale/hi/LC_MESSAGES/foreman_resource_quota.mo +0 -0
  102. data/locale/hi/foreman_resource_quota.po +218 -0
  103. data/locale/id/LC_MESSAGES/foreman_resource_quota.mo +0 -0
  104. data/locale/id/foreman_resource_quota.po +218 -0
  105. data/locale/it/LC_MESSAGES/foreman_resource_quota.mo +0 -0
  106. data/locale/it/foreman_resource_quota.po +221 -0
  107. data/locale/ja/LC_MESSAGES/foreman_resource_quota.mo +0 -0
  108. data/locale/ja/foreman_resource_quota.po +219 -0
  109. data/locale/ka/LC_MESSAGES/foreman_resource_quota.mo +0 -0
  110. data/locale/ka/foreman_resource_quota.po +218 -0
  111. data/locale/kn/LC_MESSAGES/foreman_resource_quota.mo +0 -0
  112. data/locale/kn/foreman_resource_quota.po +218 -0
  113. data/locale/ko/LC_MESSAGES/foreman_resource_quota.mo +0 -0
  114. data/locale/ko/foreman_resource_quota.po +220 -0
  115. data/locale/ml_IN/LC_MESSAGES/foreman_resource_quota.mo +0 -0
  116. data/locale/ml_IN/foreman_resource_quota.po +215 -0
  117. data/locale/mr/LC_MESSAGES/foreman_resource_quota.mo +0 -0
  118. data/locale/mr/foreman_resource_quota.po +218 -0
  119. data/locale/nl_NL/LC_MESSAGES/foreman_resource_quota.mo +0 -0
  120. data/locale/nl_NL/foreman_resource_quota.po +223 -0
  121. data/locale/or/LC_MESSAGES/foreman_resource_quota.mo +0 -0
  122. data/locale/or/foreman_resource_quota.po +218 -0
  123. data/locale/pa/LC_MESSAGES/foreman_resource_quota.mo +0 -0
  124. data/locale/pa/foreman_resource_quota.po +219 -0
  125. data/locale/pl/LC_MESSAGES/foreman_resource_quota.mo +0 -0
  126. data/locale/pl/foreman_resource_quota.po +221 -0
  127. data/locale/pl_PL/LC_MESSAGES/foreman_resource_quota.mo +0 -0
  128. data/locale/pl_PL/foreman_resource_quota.po +217 -0
  129. data/locale/pt/LC_MESSAGES/foreman_resource_quota.mo +0 -0
  130. data/locale/pt/foreman_resource_quota.po +215 -0
  131. data/locale/pt_BR/LC_MESSAGES/foreman_resource_quota.mo +0 -0
  132. data/locale/pt_BR/foreman_resource_quota.po +222 -0
  133. data/locale/ro/LC_MESSAGES/foreman_resource_quota.mo +0 -0
  134. data/locale/ro/foreman_resource_quota.po +215 -0
  135. data/locale/ro_RO/LC_MESSAGES/foreman_resource_quota.mo +0 -0
  136. data/locale/ro_RO/foreman_resource_quota.po +216 -0
  137. data/locale/ru/LC_MESSAGES/foreman_resource_quota.mo +0 -0
  138. data/locale/ru/foreman_resource_quota.po +222 -0
  139. data/locale/sl/LC_MESSAGES/foreman_resource_quota.mo +0 -0
  140. data/locale/sl/foreman_resource_quota.po +219 -0
  141. data/locale/sv_SE/LC_MESSAGES/foreman_resource_quota.mo +0 -0
  142. data/locale/sv_SE/foreman_resource_quota.po +221 -0
  143. data/locale/ta/LC_MESSAGES/foreman_resource_quota.mo +0 -0
  144. data/locale/ta/foreman_resource_quota.po +214 -0
  145. data/locale/ta_IN/LC_MESSAGES/foreman_resource_quota.mo +0 -0
  146. data/locale/ta_IN/foreman_resource_quota.po +219 -0
  147. data/locale/te/LC_MESSAGES/foreman_resource_quota.mo +0 -0
  148. data/locale/te/foreman_resource_quota.po +218 -0
  149. data/locale/tr/LC_MESSAGES/foreman_resource_quota.mo +0 -0
  150. data/locale/tr/foreman_resource_quota.po +214 -0
  151. data/locale/vi/LC_MESSAGES/foreman_resource_quota.mo +0 -0
  152. data/locale/vi/foreman_resource_quota.po +214 -0
  153. data/locale/vi_VN/LC_MESSAGES/foreman_resource_quota.mo +0 -0
  154. data/locale/vi_VN/foreman_resource_quota.po +215 -0
  155. data/locale/zh/LC_MESSAGES/foreman_resource_quota.mo +0 -0
  156. data/locale/zh/foreman_resource_quota.po +214 -0
  157. data/locale/zh_CN/LC_MESSAGES/foreman_resource_quota.mo +0 -0
  158. data/locale/zh_CN/foreman_resource_quota.po +221 -0
  159. data/locale/zh_TW/LC_MESSAGES/foreman_resource_quota.mo +0 -0
  160. data/locale/zh_TW/foreman_resource_quota.po +220 -0
  161. data/package.json +10 -10
  162. data/webpack/components/ResourceQuotaEmptyState/__test__/ResourceQuotaEmptyState.test.js +35 -0
  163. data/webpack/components/ResourceQuotaEmptyState/__test__/__snapshots__/ResourceQuotaEmptyState.test.js.snap +80 -0
  164. data/webpack/components/ResourceQuotaEmptyState/index.js +20 -3
  165. data/webpack/components/ResourceQuotaForm/ResourceQuotaFormConstants.js +6 -6
  166. data/webpack/components/ResourceQuotaForm/components/Properties/TextInputField.js +1 -1
  167. data/webpack/components/ResourceQuotaForm/components/Properties/index.js +1 -1
  168. data/webpack/components/ResourceQuotaForm/components/Resource/UnitInputField.js +23 -21
  169. data/webpack/components/ResourceQuotaForm/components/Resource/__test__/UnitInputField.test.js +108 -0
  170. data/webpack/components/ResourceQuotaForm/components/Resource/__test__/__snapshots__/UnitInputField.test.js.snap +155 -0
  171. data/webpack/components/ResourceQuotaForm/components/Resource/index.js +1 -1
  172. data/webpack/components/ResourceQuotaForm/components/Submit.js +1 -1
  173. data/webpack/test_helper.js +49 -0
  174. metadata +158 -7
@@ -0,0 +1,35 @@
1
+ import React from 'react';
2
+ import '@testing-library/jest-dom';
3
+
4
+ import { mount, testComponentSnapshotsWithFixtures } from '@theforeman/test';
5
+ // Notice: (not) importing Modal affects the snapshot test since it fills
6
+ // the components data dynamically in snapshots as soon as it can find the component.
7
+ import { Modal } from '@patternfly/react-core';
8
+
9
+ import { withMockedProvider, withRedux } from '../../../test_helper';
10
+ import ResourceQuotaForm from '../../ResourceQuotaForm';
11
+ import ResourceQuotaEmptyState from '../index';
12
+
13
+ const TestComponent = withRedux(withMockedProvider(ResourceQuotaEmptyState));
14
+
15
+ describe('ResourceQuotaEmptyState', () => {
16
+ testComponentSnapshotsWithFixtures(ResourceQuotaEmptyState, {
17
+ 'should render': {}, // component has no props
18
+ });
19
+
20
+ test('opens the modal on clicking "Create resource quota" button', () => {
21
+ const wrapper = mount(<TestComponent />);
22
+
23
+ expect(wrapper.find(Modal).prop('isOpen')).toBe(false); // check we provide the correct input to Modal
24
+ expect(wrapper.find(ResourceQuotaForm).exists()).toBe(false);
25
+
26
+ wrapper
27
+ .find('button')
28
+ .filterWhere(button => button.text() === 'Create resource quota')
29
+ .simulate('click');
30
+ wrapper.update();
31
+
32
+ expect(wrapper.find(Modal).prop('isOpen')).toBe(true);
33
+ expect(wrapper.find(ResourceQuotaForm).exists()).toBe(true);
34
+ });
35
+ });
@@ -0,0 +1,80 @@
1
+ // Jest Snapshot v1, https://goo.gl/fbAQLP
2
+
3
+ exports[`ResourceQuotaEmptyState should render 1`] = `
4
+ <div>
5
+ <EmptyStatePattern
6
+ action={
7
+ <Button
8
+ id="foreman-resource-quota-welcome-create-modal-button"
9
+ onClick={[Function]}
10
+ variant="primary"
11
+ >
12
+ Create resource quota
13
+ </Button>
14
+ }
15
+ description={
16
+ <span>
17
+ Resource Quotas help admins to manage resources including CPUs, memory, and disk space among users or user groups.
18
+ <br />
19
+ Define a Resource Quota here and apply it to users to guarantee a fair share of your resources.
20
+ <br />
21
+ </span>
22
+ }
23
+ documentation={
24
+ Object {
25
+ "url": "/links/docs/Administering_Project?chapter=limiting-host-resources",
26
+ }
27
+ }
28
+ header="Resource Quotas"
29
+ icon="pficon pficon-cluster"
30
+ iconType="pf"
31
+ secondaryActions={Array []}
32
+ />
33
+ <Modal
34
+ actions={Array []}
35
+ appendTo={<body />}
36
+ aria-describedby=""
37
+ aria-label=""
38
+ aria-labelledby=""
39
+ className=""
40
+ hasNoBodyWrapper={false}
41
+ isOpen={false}
42
+ onClose={[Function]}
43
+ ouiaId="foreman-resource-quota-create-modal"
44
+ ouiaSafe={true}
45
+ showClose={true}
46
+ title="Create resource quota"
47
+ titleIconVariant={null}
48
+ titleLabel=""
49
+ variant="small"
50
+ >
51
+ <ResourceQuotaForm
52
+ initialProperties={
53
+ Object {
54
+ "cpu_cores": null,
55
+ "description": "",
56
+ "disk_gb": null,
57
+ "memory_mb": null,
58
+ "name": "",
59
+ }
60
+ }
61
+ initialStatus={
62
+ Object {
63
+ "missing_hosts": null,
64
+ "number_of_hosts": null,
65
+ "number_of_usergroups": null,
66
+ "number_of_users": null,
67
+ "utilization": Object {
68
+ "cpu_cores": null,
69
+ "disk_gb": null,
70
+ "memory_mb": null,
71
+ },
72
+ }
73
+ }
74
+ isNewQuota={true}
75
+ onSubmit={[Function]}
76
+ quotaChangesCallback={null}
77
+ />
78
+ </Modal>
79
+ </div>
80
+ `;
@@ -2,6 +2,7 @@ import React, { useState } from 'react';
2
2
  import { Button, Modal, ModalVariant } from '@patternfly/react-core';
3
3
 
4
4
  import { translate as __ } from 'foremanReact/common/I18n';
5
+ import { getDocsURL } from 'foremanReact/common/helpers';
5
6
  import EmptyStatePattern from 'foremanReact/components/common/EmptyState/EmptyStatePattern';
6
7
 
7
8
  import ResourceQuotaForm from '../ResourceQuotaForm';
@@ -28,16 +29,32 @@ const ResourceQuotaEmptyState = () => {
28
29
  {__('Create resource quota')}
29
30
  </Button>
30
31
  );
32
+
33
+ const description = (
34
+ <span>
35
+ {__(
36
+ 'Resource Quotas help admins to manage resources including CPUs, memory, and disk space among users or user groups.'
37
+ )}
38
+ <br />
39
+ {__(
40
+ 'Define a Resource Quota here and apply it to users to guarantee a fair share of your resources.'
41
+ )}
42
+ <br />
43
+ </span>
44
+ );
45
+ const documentation = {
46
+ url: getDocsURL('Administering_Project', 'limiting-host-resources'),
47
+ };
48
+
31
49
  return (
32
50
  <div>
33
51
  <EmptyStatePattern
34
52
  icon="pficon pficon-cluster"
35
53
  iconType="pf"
36
54
  header={__('Resource Quotas')}
37
- description={__(
38
- 'Resource Quotas help admins to manage hardware resources (like CPUs, RAM, and disk space) among users or usergroups. \n\rDefine a Resource Quota here and apply it to users in order to guarantee a free share of your resources.'
39
- )}
55
+ description={description}
40
56
  action={ActionButton}
57
+ documentation={documentation}
41
58
  />
42
59
  <Modal
43
60
  ouiaId={MODAL_ID_CREATE_RESOURCE_QUOTA}
@@ -19,14 +19,14 @@ export const RESOURCE_NAME_DISK = 'Disk space';
19
19
  /* Resource units (order the units with increasing factor!) */
20
20
  export const RESOURCE_UNIT_CPU = [{ symbol: 'cores', factor: 1 }];
21
21
  export const RESOURCE_UNIT_MEMORY = [
22
- { symbol: 'MB', factor: 1 },
23
- { symbol: 'GB', factor: 1024 },
24
- { symbol: 'TB', factor: 1024 * 1024 },
22
+ { symbol: 'MiB', factor: 1 },
23
+ { symbol: 'GiB', factor: 1024 },
24
+ { symbol: 'TiB', factor: 1024 * 1024 },
25
25
  ];
26
26
  export const RESOURCE_UNIT_DISK = [
27
- { symbol: 'GB', factor: 1 },
28
- { symbol: 'TB', factor: 1024 },
29
- { symbol: 'PB', factor: 1024 * 1024 },
27
+ { symbol: 'GiB', factor: 1 },
28
+ { symbol: 'TiB', factor: 1024 },
29
+ { symbol: 'PiB', factor: 1024 * 1024 },
30
30
  ];
31
31
 
32
32
  /* Resource value bounds */
@@ -40,7 +40,7 @@ const TextInputField = ({
40
40
  dispatchAPICallbackToast(
41
41
  success,
42
42
  response,
43
- `Sucessfully applied ${label}.`,
43
+ `Successfully applied ${label}.`,
44
44
  `An error occurred appyling ${label}.`
45
45
  )
46
46
  );
@@ -64,7 +64,7 @@ const Properties = ({
64
64
  dispatchAPICallbackToast(
65
65
  success,
66
66
  response,
67
- `Sucessfully fetched latest data.`,
67
+ `Successfully fetched latest data.`,
68
68
  `An error occurred fetching quota information.`
69
69
  )
70
70
  );
@@ -2,6 +2,7 @@ import React, { useState, useEffect, useCallback } from 'react';
2
2
  import PropTypes from 'prop-types';
3
3
  import {
4
4
  FormGroup,
5
+ FormHelperText,
5
6
  TextInput,
6
7
  InputGroup,
7
8
  InputGroupText,
@@ -60,17 +61,19 @@ const UnitInputField = ({
60
61
  }, [minValue, maxValue, selectedUnit]);
61
62
 
62
63
  /* text for float errors */
63
- const errorTextNatural = useCallback(
64
- () => __('Value must be a natural number.'),
65
- []
66
- );
64
+ const errorTextNatural = useCallback(() => __('Value must be a number.'), []);
67
65
 
68
- /* text for float errors */
69
- const errorTextFloating = useCallback(
70
- () => __(`No floating point for smallest unit (${units[0].symbol}).`),
66
+ /* text for float inputs (rounding) */
67
+ const warningTextRounded = useCallback(
68
+ roundedValue => __(`Rounding to: ${roundedValue} (${units[0].symbol}).`),
71
69
  [units]
72
70
  );
73
71
 
72
+ /* warning text displayed beneath value input field (built-in is used for errors) */
73
+ const helperTextWarning = (text, isHidden) => (
74
+ <FormHelperText isHidden={isHidden}>{text}</FormHelperText>
75
+ );
76
+
74
77
  /* applies the selected unit and checks the bounds */
75
78
  const isValid = useCallback(
76
79
  val => {
@@ -83,20 +86,9 @@ const UnitInputField = ({
83
86
  setErrorText(errorTextBounds());
84
87
  return false;
85
88
  }
86
- if (baseValue !== Math.floor(baseValue)) {
87
- setErrorText(errorTextFloating());
88
- return false;
89
- }
90
89
  return true;
91
90
  },
92
- [
93
- minValue,
94
- maxValue,
95
- valueToBaseUnit,
96
- errorTextNatural,
97
- errorTextBounds,
98
- errorTextFloating,
99
- ]
91
+ [minValue, maxValue, valueToBaseUnit, errorTextNatural, errorTextBounds]
100
92
  );
101
93
 
102
94
  /* applies the selected unit and returns the base-unit value */
@@ -116,9 +108,17 @@ const UnitInputField = ({
116
108
  setValidated('default');
117
109
  } else if (isValid(inputValue)) {
118
110
  const baseValue = valueToBaseUnit(inputValue);
119
- onChange(baseValue);
111
+ let validatedValue = baseValue;
112
+ if (baseValue !== Math.floor(baseValue)) {
113
+ validatedValue = Math.floor(baseValue);
114
+ setErrorText(warningTextRounded(validatedValue));
115
+ setValidated('warning');
116
+ } else {
117
+ // Keep baseValue as validatedValue
118
+ setValidated('default');
119
+ }
120
+ onChange(validatedValue);
120
121
  handleInputValidation(true);
121
- setValidated('default');
122
122
  } else {
123
123
  handleInputValidation(false);
124
124
  setValidated('error');
@@ -131,6 +131,7 @@ const UnitInputField = ({
131
131
  onChange,
132
132
  isValid,
133
133
  valueToBaseUnit,
134
+ warningTextRounded,
134
135
  ]);
135
136
 
136
137
  /* set the selected unit */
@@ -181,6 +182,7 @@ const UnitInputField = ({
181
182
  label={__('Quota Limit')}
182
183
  validated={validated}
183
184
  helperTextInvalid={errorText}
185
+ helperText={helperTextWarning(errorText, validated !== 'warning')}
184
186
  fieldId="quota-limit-resource-quota-form-group"
185
187
  labelIcon={labelIcon || {}}
186
188
  >
@@ -0,0 +1,108 @@
1
+ import React from 'react';
2
+ import '@testing-library/jest-dom';
3
+ import { render, screen, fireEvent } from '@testing-library/react';
4
+
5
+ import { testComponentSnapshotsWithFixtures } from '@theforeman/test';
6
+ import LabelIcon from 'foremanReact/components/common/LabelIcon';
7
+
8
+ import UnitInputField from '../UnitInputField';
9
+
10
+ const getDefaultProps = () => ({
11
+ initialValue: 0,
12
+ onChange: jest.fn(),
13
+ isDisabled: false,
14
+ handleInputValidation: jest.fn(),
15
+ units: [
16
+ { symbol: 'MiB', factor: 1 },
17
+ { symbol: 'GiB', factor: 1024 },
18
+ ],
19
+ labelIcon: <LabelIcon text="Descriptive title." />,
20
+ minValue: 0,
21
+ maxValue: 5,
22
+ });
23
+
24
+ const fixtureDefault = {
25
+ 'should render default': {
26
+ ...getDefaultProps(),
27
+ },
28
+ };
29
+
30
+ const fixtureSingleUnit = {
31
+ 'should render without dropdown (single unit)': {
32
+ ...getDefaultProps(),
33
+ units: [{ symbol: 'cores', factor: 1 }],
34
+ },
35
+ };
36
+
37
+ const fixtureDisabled = {
38
+ 'should render as disabled field': {
39
+ ...getDefaultProps(),
40
+ isDisabled: true,
41
+ },
42
+ };
43
+
44
+ describe('UnitInputField', () => {
45
+ testComponentSnapshotsWithFixtures(UnitInputField, fixtureDefault);
46
+ testComponentSnapshotsWithFixtures(UnitInputField, fixtureSingleUnit);
47
+ testComponentSnapshotsWithFixtures(UnitInputField, fixtureDisabled);
48
+
49
+ it('triggers handleInputValidation on unit change', async () => {
50
+ const props = getDefaultProps();
51
+
52
+ render(<UnitInputField {...props} />);
53
+ const input = screen.getByRole('textbox');
54
+ fireEvent.change(input, { target: { value: 3 } });
55
+
56
+ // gets called (1.) with initialValue and (2.) the simulated change
57
+ expect(props.onChange).toHaveBeenCalledTimes(2);
58
+ expect(props.onChange).toHaveBeenCalledWith(props.initialValue);
59
+ expect(props.onChange).toHaveBeenLastCalledWith(3);
60
+ expect(props.handleInputValidation).toHaveBeenCalledTimes(2);
61
+ expect(props.handleInputValidation).toHaveBeenCalledWith(true);
62
+ });
63
+
64
+ test('triggers onChange with rounded value', () => {
65
+ const props = getDefaultProps();
66
+
67
+ render(<UnitInputField {...props} />);
68
+ const input = screen.getByRole('textbox');
69
+ fireEvent.change(input, { target: { value: 3.5 } });
70
+
71
+ // gets called (1.) with initialValue and (2.) the simulated change
72
+ expect(props.onChange).toHaveBeenCalledTimes(2);
73
+ expect(props.onChange).toHaveBeenCalledWith(props.initialValue);
74
+ expect(props.onChange).toHaveBeenLastCalledWith(3);
75
+ expect(props.handleInputValidation).toHaveBeenCalledTimes(2);
76
+ expect(props.handleInputValidation).toHaveBeenCalledWith(true);
77
+ });
78
+
79
+ test('does not trigger onChange when value out of bounds', () => {
80
+ const props = getDefaultProps();
81
+
82
+ render(<UnitInputField {...props} />);
83
+ const input = screen.getByRole('textbox');
84
+ fireEvent.change(input, { target: { value: props.maxValue + 1 } });
85
+
86
+ // onChange only called for initialValue
87
+ expect(props.onChange).toHaveBeenCalledTimes(1);
88
+ expect(props.onChange).toHaveBeenCalledWith(props.initialValue);
89
+ // handleInputValidation called with false => invalid
90
+ expect(props.handleInputValidation).toHaveBeenCalledTimes(2);
91
+ expect(props.handleInputValidation).toHaveBeenLastCalledWith(false);
92
+ });
93
+
94
+ test('does not trigger onChange when value is not a number', () => {
95
+ const props = getDefaultProps();
96
+
97
+ render(<UnitInputField {...props} />);
98
+ const input = screen.getByRole('textbox');
99
+ fireEvent.change(input, { target: { value: 'no number' } });
100
+
101
+ // onChange only called for initialValue
102
+ expect(props.onChange).toHaveBeenCalledTimes(1);
103
+ expect(props.onChange).toHaveBeenCalledWith(props.initialValue);
104
+ // handleInputValidation called with false => invalid
105
+ expect(props.handleInputValidation).toHaveBeenCalledTimes(2);
106
+ expect(props.handleInputValidation).toHaveBeenLastCalledWith(false);
107
+ });
108
+ });
@@ -0,0 +1,155 @@
1
+ // Jest Snapshot v1, https://goo.gl/fbAQLP
2
+
3
+ exports[`UnitInputField should render as disabled field 1`] = `
4
+ <FormGroup
5
+ fieldId="quota-limit-resource-quota-form-group"
6
+ helperText={
7
+ <FormHelperText
8
+ isHidden={true}
9
+ >
10
+
11
+ </FormHelperText>
12
+ }
13
+ helperTextInvalid=""
14
+ label="Quota Limit"
15
+ labelIcon={
16
+ <LabelIcon
17
+ text="Descriptive title."
18
+ />
19
+ }
20
+ validated="default"
21
+ >
22
+ <InputGroup>
23
+ <TextInput
24
+ id="reg_token_life_time_input"
25
+ isDisabled={true}
26
+ max={5}
27
+ min={0}
28
+ onChange={[Function]}
29
+ validated="default"
30
+ value={0}
31
+ />
32
+ <Dropdown
33
+ dropdownItems={
34
+ Array [
35
+ <DropdownItem
36
+ id="unit-dropdownitem-mib"
37
+ >
38
+ MiB
39
+ </DropdownItem>,
40
+ <DropdownItem
41
+ id="unit-dropdownitem-gib"
42
+ >
43
+ GiB
44
+ </DropdownItem>,
45
+ ]
46
+ }
47
+ isOpen={false}
48
+ onSelect={[Function]}
49
+ toggle={
50
+ <DropdownToggle
51
+ isDisabled={true}
52
+ onToggle={[Function]}
53
+ >
54
+ MiB
55
+ </DropdownToggle>
56
+ }
57
+ />
58
+ </InputGroup>
59
+ </FormGroup>
60
+ `;
61
+
62
+ exports[`UnitInputField should render default 1`] = `
63
+ <FormGroup
64
+ fieldId="quota-limit-resource-quota-form-group"
65
+ helperText={
66
+ <FormHelperText
67
+ isHidden={true}
68
+ >
69
+
70
+ </FormHelperText>
71
+ }
72
+ helperTextInvalid=""
73
+ label="Quota Limit"
74
+ labelIcon={
75
+ <LabelIcon
76
+ text="Descriptive title."
77
+ />
78
+ }
79
+ validated="default"
80
+ >
81
+ <InputGroup>
82
+ <TextInput
83
+ id="reg_token_life_time_input"
84
+ isDisabled={false}
85
+ max={5}
86
+ min={0}
87
+ onChange={[Function]}
88
+ validated="default"
89
+ value={0}
90
+ />
91
+ <Dropdown
92
+ dropdownItems={
93
+ Array [
94
+ <DropdownItem
95
+ id="unit-dropdownitem-mib"
96
+ >
97
+ MiB
98
+ </DropdownItem>,
99
+ <DropdownItem
100
+ id="unit-dropdownitem-gib"
101
+ >
102
+ GiB
103
+ </DropdownItem>,
104
+ ]
105
+ }
106
+ isOpen={false}
107
+ onSelect={[Function]}
108
+ toggle={
109
+ <DropdownToggle
110
+ isDisabled={false}
111
+ onToggle={[Function]}
112
+ >
113
+ MiB
114
+ </DropdownToggle>
115
+ }
116
+ />
117
+ </InputGroup>
118
+ </FormGroup>
119
+ `;
120
+
121
+ exports[`UnitInputField should render without dropdown (single unit) 1`] = `
122
+ <FormGroup
123
+ fieldId="quota-limit-resource-quota-form-group"
124
+ helperText={
125
+ <FormHelperText
126
+ isHidden={true}
127
+ >
128
+
129
+ </FormHelperText>
130
+ }
131
+ helperTextInvalid=""
132
+ label="Quota Limit"
133
+ labelIcon={
134
+ <LabelIcon
135
+ text="Descriptive title."
136
+ />
137
+ }
138
+ validated="default"
139
+ >
140
+ <InputGroup>
141
+ <TextInput
142
+ id="reg_token_life_time_input"
143
+ isDisabled={false}
144
+ max={5}
145
+ min={0}
146
+ onChange={[Function]}
147
+ validated="default"
148
+ value={0}
149
+ />
150
+ <InputGroupText>
151
+ cores
152
+ </InputGroupText>
153
+ </InputGroup>
154
+ </FormGroup>
155
+ `;
@@ -74,7 +74,7 @@ const Resource = ({
74
74
  dispatchAPICallbackToast(
75
75
  success,
76
76
  response,
77
- `Sucessfully applied ${resourceTitle}.`,
77
+ `Successfully applied ${resourceTitle}.`,
78
78
  `An error occurred appyling ${resourceTitle}.`
79
79
  )
80
80
  );
@@ -31,7 +31,7 @@ const Submit = ({ isValid, onCreate, onSubmit }) => {
31
31
  dispatchAPICallbackToast(
32
32
  success,
33
33
  response,
34
- `Sucessfully created new Resource Quota`,
34
+ `Successfully created new Resource Quota`,
35
35
  `An error occurred while creating new Resource Quota.`
36
36
  )
37
37
  );
@@ -0,0 +1,49 @@
1
+ /* Credits: https://github.com/theforeman/foreman_ansible/blob/master/webpack/testHelper.js */
2
+ import React, { useState } from 'react';
3
+ import { applyMiddleware, createStore, compose, combineReducers } from 'redux';
4
+ import { reducers as apiReducer, APIMiddleware } from 'foremanReact/redux/API';
5
+ import { Provider } from 'react-redux';
6
+ import { MockedProvider } from '@apollo/react-testing';
7
+ import thunk from 'redux-thunk';
8
+
9
+ import ConfirmModal, {
10
+ reducers as confirmModalReducers,
11
+ } from 'foremanReact/components/ConfirmModal';
12
+ import { getForemanContext } from 'foremanReact/Root/Context/ForemanContext';
13
+
14
+ const reducers = combineReducers({ ...apiReducer, ...confirmModalReducers });
15
+ export const generateStore = () =>
16
+ createStore(reducers, compose(applyMiddleware(thunk, APIMiddleware)));
17
+
18
+ // use to resolve async mock requests for apollo MockedProvider
19
+ export const tick = () => new Promise(resolve => setTimeout(resolve, 0));
20
+
21
+ export const withRedux = Component => props => (
22
+ <Provider store={generateStore()}>
23
+ <Component {...props} />
24
+ <ConfirmModal />
25
+ </Provider>
26
+ );
27
+
28
+ export const withMockedProvider = Component => props => {
29
+ const [context, setContext] = useState({
30
+ metadata: {
31
+ UISettings: {
32
+ perPage: 20,
33
+ },
34
+ },
35
+ });
36
+ const contextData = { context, setContext };
37
+ const ForemanContext = getForemanContext(contextData);
38
+
39
+ // eslint-disable-next-line react/prop-types
40
+ const { mocks, ...rest } = props;
41
+
42
+ return (
43
+ <ForemanContext.Provider value={contextData}>
44
+ <MockedProvider mocks={mocks}>
45
+ <Component {...rest} />
46
+ </MockedProvider>
47
+ </ForemanContext.Provider>
48
+ );
49
+ };