foreman_resource_quota 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (92) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +619 -0
  3. data/README.md +51 -0
  4. data/Rakefile +49 -0
  5. data/app/controllers/concerns/foreman/controller/parameters/resource_quota.rb +28 -0
  6. data/app/controllers/foreman_resource_quota/api/v2/resource_quotas_controller.rb +96 -0
  7. data/app/controllers/foreman_resource_quota/application_controller.rb +9 -0
  8. data/app/controllers/foreman_resource_quota/resource_quotas_controller.rb +50 -0
  9. data/app/helpers/foreman_resource_quota/hosts_helper.rb +18 -0
  10. data/app/helpers/foreman_resource_quota/resource_quota_helper.rb +107 -0
  11. data/app/models/concerns/foreman_resource_quota/host_managed_extensions.rb +115 -0
  12. data/app/models/concerns/foreman_resource_quota/user_extensions.rb +15 -0
  13. data/app/models/concerns/foreman_resource_quota/usergroup_extensions.rb +14 -0
  14. data/app/models/foreman_resource_quota/resource_quota.rb +83 -0
  15. data/app/models/foreman_resource_quota/resource_quota_user.rb +10 -0
  16. data/app/models/foreman_resource_quota/resource_quota_usergroup.rb +10 -0
  17. data/app/services/foreman_resource_quota/resource_origin.rb +97 -0
  18. data/app/services/foreman_resource_quota/resource_origins/compute_attributes_origin.rb +64 -0
  19. data/app/services/foreman_resource_quota/resource_origins/compute_resource_origin.rb +82 -0
  20. data/app/services/foreman_resource_quota/resource_origins/facts_origin.rb +68 -0
  21. data/app/services/foreman_resource_quota/resource_origins/vm_attributes_origin.rb +40 -0
  22. data/app/views/foreman_resource_quota/api/v2/hosts/resource_quota.json.rabl +3 -0
  23. data/app/views/foreman_resource_quota/api/v2/resource_quotas/base.json.rabl +6 -0
  24. data/app/views/foreman_resource_quota/api/v2/resource_quotas/create.json.rabl +5 -0
  25. data/app/views/foreman_resource_quota/api/v2/resource_quotas/hosts.json.rabl +7 -0
  26. data/app/views/foreman_resource_quota/api/v2/resource_quotas/index.json.rabl +5 -0
  27. data/app/views/foreman_resource_quota/api/v2/resource_quotas/main.json.rabl +7 -0
  28. data/app/views/foreman_resource_quota/api/v2/resource_quotas/show.json.rabl +5 -0
  29. data/app/views/foreman_resource_quota/api/v2/resource_quotas/update.json.rabl +5 -0
  30. data/app/views/foreman_resource_quota/api/v2/resource_quotas/usergroups.json.rabl +7 -0
  31. data/app/views/foreman_resource_quota/api/v2/resource_quotas/users.json.rabl +7 -0
  32. data/app/views/foreman_resource_quota/api/v2/resource_quotas/utilization.json.rabl +7 -0
  33. data/app/views/foreman_resource_quota/api/v2/usergroups/resource_quota.json.rabl +3 -0
  34. data/app/views/foreman_resource_quota/api/v2/users/resource_quota.json.rabl +3 -0
  35. data/app/views/foreman_resource_quota/resource_quotas/_form.html.erb +21 -0
  36. data/app/views/foreman_resource_quota/resource_quotas/edit.html.erb +12 -0
  37. data/app/views/foreman_resource_quota/resource_quotas/index.html.erb +55 -0
  38. data/app/views/foreman_resource_quota/resource_quotas/new.html.erb +10 -0
  39. data/app/views/foreman_resource_quota/resource_quotas/welcome.html.erb +10 -0
  40. data/app/views/hosts/_form_quota_fields.html.erb +4 -0
  41. data/app/views/users/_form_quota_tab.html.erb +45 -0
  42. data/config/initializers/inflections.rb +5 -0
  43. data/config/routes.rb +43 -0
  44. data/db/migrate/20230306120001_create_resource_quotas.rb +31 -0
  45. data/lib/foreman_resource_quota/engine.rb +56 -0
  46. data/lib/foreman_resource_quota/exceptions.rb +11 -0
  47. data/lib/foreman_resource_quota/register.rb +106 -0
  48. data/lib/foreman_resource_quota/version.rb +5 -0
  49. data/lib/foreman_resource_quota.rb +6 -0
  50. data/lib/tasks/foreman_resource_quota_tasks.rake +50 -0
  51. data/locale/Makefile +60 -0
  52. data/locale/en/foreman_resource_quota.po +18 -0
  53. data/locale/foreman_resource_quota.pot +19 -0
  54. data/locale/gemspec.rb +4 -0
  55. data/package.json +44 -0
  56. data/webpack/api_helper.js +113 -0
  57. data/webpack/api_helper.test.js +96 -0
  58. data/webpack/components/CreateResourceQuotaModal.js +46 -0
  59. data/webpack/components/ResourceQuotaEmptyState/index.js +58 -0
  60. data/webpack/components/ResourceQuotaForm/ResourceQuotaForm.scss +1 -0
  61. data/webpack/components/ResourceQuotaForm/ResourceQuotaFormConstants.js +71 -0
  62. data/webpack/components/ResourceQuotaForm/components/Properties/Properties.scss +9 -0
  63. data/webpack/components/ResourceQuotaForm/components/Properties/StaticDetail.js +72 -0
  64. data/webpack/components/ResourceQuotaForm/components/Properties/StatusPropertiesLabel.js +71 -0
  65. data/webpack/components/ResourceQuotaForm/components/Properties/StatusPropertiesLabel.test.js +50 -0
  66. data/webpack/components/ResourceQuotaForm/components/Properties/TextInputField.js +131 -0
  67. data/webpack/components/ResourceQuotaForm/components/Properties/index.js +190 -0
  68. data/webpack/components/ResourceQuotaForm/components/QuotaState.js +157 -0
  69. data/webpack/components/ResourceQuotaForm/components/Resource/Resource.scss +13 -0
  70. data/webpack/components/ResourceQuotaForm/components/Resource/UnitInputField.js +224 -0
  71. data/webpack/components/ResourceQuotaForm/components/Resource/UtilizationProgress.js +151 -0
  72. data/webpack/components/ResourceQuotaForm/components/Resource/UtilizationProgress.scss +10 -0
  73. data/webpack/components/ResourceQuotaForm/components/Resource/index.js +239 -0
  74. data/webpack/components/ResourceQuotaForm/components/Resources.js +105 -0
  75. data/webpack/components/ResourceQuotaForm/components/Submit.js +72 -0
  76. data/webpack/components/ResourceQuotaForm/index.js +185 -0
  77. data/webpack/components/UpdateResourceQuotaModal.js +143 -0
  78. data/webpack/global_index.js +15 -0
  79. data/webpack/global_test_setup.js +11 -0
  80. data/webpack/helper.js +86 -0
  81. data/webpack/index.js +23 -0
  82. data/webpack/lib/ActionableDetail.js +115 -0
  83. data/webpack/lib/ActionableDetail.scss +4 -0
  84. data/webpack/lib/EditableSwitch.js +47 -0
  85. data/webpack/lib/EditableTextInput/EditableTextInput.js +206 -0
  86. data/webpack/lib/EditableTextInput/PencilEditButton.js +27 -0
  87. data/webpack/lib/EditableTextInput/__tests__/editableTextInput.test.js +193 -0
  88. data/webpack/lib/EditableTextInput/editableTextInput.scss +38 -0
  89. data/webpack/lib/EditableTextInput/index.js +4 -0
  90. data/webpack/lib/react-testing-lib-wrapper.js +80 -0
  91. data/webpack/test_setup.js +17 -0
  92. metadata +134 -0
@@ -0,0 +1,58 @@
1
+ import React, { useState } from 'react';
2
+ import { Button, Modal, ModalVariant } from '@patternfly/react-core';
3
+
4
+ import { translate as __ } from 'foremanReact/common/I18n';
5
+ import EmptyStatePattern from 'foremanReact/components/common/EmptyState/EmptyStatePattern';
6
+
7
+ import ResourceQuotaForm from '../ResourceQuotaForm';
8
+ import { MODAL_ID_CREATE_RESOURCE_QUOTA } from '../ResourceQuotaForm/ResourceQuotaFormConstants';
9
+
10
+ const ResourceQuotaEmptyState = () => {
11
+ const [isOpen, setIsOpen] = useState(false);
12
+
13
+ const onSubmitSuccessCallback = success => {
14
+ if (success) {
15
+ setIsOpen(false);
16
+ window.location.reload();
17
+ }
18
+ };
19
+
20
+ const ActionButton = (
21
+ <Button
22
+ id="foreman-resource-quota-welcome-create-modal-button"
23
+ variant="primary"
24
+ onClick={() => {
25
+ setIsOpen(true);
26
+ }}
27
+ >
28
+ {__('Create resource quota')}
29
+ </Button>
30
+ );
31
+ return (
32
+ <div>
33
+ <EmptyStatePattern
34
+ icon="pficon pficon-cluster"
35
+ iconType="pf"
36
+ 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
+ )}
40
+ action={ActionButton}
41
+ />
42
+ <Modal
43
+ ouiaId={MODAL_ID_CREATE_RESOURCE_QUOTA}
44
+ title={__('Create resource quota')}
45
+ variant={ModalVariant.small}
46
+ isOpen={isOpen}
47
+ onClose={() => {
48
+ setIsOpen(false);
49
+ }}
50
+ appendTo={document.body}
51
+ >
52
+ <ResourceQuotaForm isNewQuota onSubmit={onSubmitSuccessCallback} />
53
+ </Modal>
54
+ </div>
55
+ );
56
+ };
57
+
58
+ export default ResourceQuotaEmptyState;
@@ -0,0 +1 @@
1
+ @import '~@theforeman/vendor/scss/variables';
@@ -0,0 +1,71 @@
1
+ /* Resource identifier */
2
+ export const RESOURCE_IDENTIFIER_ID = 'id';
3
+ export const RESOURCE_IDENTIFIER_NAME = 'name';
4
+ export const RESOURCE_IDENTIFIER_DESCRIPTION = 'description';
5
+ export const RESOURCE_IDENTIFIER_CPU = 'cpu_cores';
6
+ export const RESOURCE_IDENTIFIER_MEMORY = 'memory_mb';
7
+ export const RESOURCE_IDENTIFIER_DISK = 'disk_gb';
8
+ export const RESOURCE_IDENTIFIER_STATUS_NUM_HOSTS = 'number_of_hosts';
9
+ export const RESOURCE_IDENTIFIER_STATUS_NUM_USERS = 'number_of_users';
10
+ export const RESOURCE_IDENTIFIER_STATUS_NUM_USERGROUPS = 'number_of_usergroups';
11
+ export const RESOURCE_IDENTIFIER_STATUS_UTILIZATION = 'utilization';
12
+ export const RESOURCE_IDENTIFIER_STATUS_MISSING_HOSTS = 'missing_hosts';
13
+
14
+ /* Resource names */
15
+ export const RESOURCE_NAME_CPU = 'CPU cores';
16
+ export const RESOURCE_NAME_MEMORY = 'Memory';
17
+ export const RESOURCE_NAME_DISK = 'Disk space';
18
+
19
+ /* Resource units (order the units with increasing factor!) */
20
+ export const RESOURCE_UNIT_CPU = [{ symbol: 'cores', factor: 1 }];
21
+ export const RESOURCE_UNIT_MEMORY = [
22
+ { symbol: 'MB', factor: 1 },
23
+ { symbol: 'GB', factor: 1024 },
24
+ { symbol: 'TB', factor: 1024 * 1024 },
25
+ ];
26
+ export const RESOURCE_UNIT_DISK = [
27
+ { symbol: 'GB', factor: 1 },
28
+ { symbol: 'TB', factor: 1024 },
29
+ { symbol: 'PB', factor: 1024 * 1024 },
30
+ ];
31
+
32
+ /* Resource value bounds */
33
+ export const RESOURCE_VALUE_MIN_CPU = 0;
34
+ export const RESOURCE_VALUE_MAX_CPU = 1999999999;
35
+ export const RESOURCE_VALUE_MIN_MEMORY = 0;
36
+ export const RESOURCE_VALUE_MAX_MEMORY = 1999999999;
37
+ export const RESOURCE_VALUE_MIN_DISK = 0;
38
+ export const RESOURCE_VALUE_MAX_DISK = 1999999999;
39
+
40
+ /* Map attributes to given resource identifier (name, unit, minValue, maxValue) */
41
+ export const resourceAttributesByIdentifier = identifier => {
42
+ switch (identifier) {
43
+ case RESOURCE_IDENTIFIER_CPU:
44
+ return {
45
+ name: RESOURCE_NAME_CPU,
46
+ unit: RESOURCE_UNIT_CPU,
47
+ minValue: RESOURCE_VALUE_MIN_CPU,
48
+ maxValue: RESOURCE_VALUE_MAX_CPU,
49
+ };
50
+ case RESOURCE_IDENTIFIER_MEMORY:
51
+ return {
52
+ name: RESOURCE_NAME_MEMORY,
53
+ unit: RESOURCE_UNIT_MEMORY,
54
+ minValue: RESOURCE_VALUE_MIN_MEMORY,
55
+ maxValue: RESOURCE_VALUE_MAX_MEMORY,
56
+ };
57
+ case RESOURCE_IDENTIFIER_DISK:
58
+ return {
59
+ name: RESOURCE_NAME_DISK,
60
+ unit: RESOURCE_UNIT_DISK,
61
+ minValue: RESOURCE_VALUE_MIN_DISK,
62
+ maxValue: RESOURCE_VALUE_MAX_DISK,
63
+ };
64
+ default:
65
+ return null;
66
+ }
67
+ };
68
+
69
+ /* HTML constants */
70
+ export const MODAL_ID_CREATE_RESOURCE_QUOTA = `foreman-resource-quota-create-modal`;
71
+ export const MODAL_ID_UPDATE_RESOURCE_QUOTA = `foreman-resource-quota-create-modal`;
@@ -0,0 +1,9 @@
1
+ @import '~@theforeman/vendor/scss/variables';
2
+
3
+ .pf-c-card__body {
4
+ padding-top: 1rem;
5
+ }
6
+
7
+ .pf-c-content dl {
8
+ margin-bottom: 0px;
9
+ }
@@ -0,0 +1,72 @@
1
+ import React from 'react';
2
+ import PropTypes from 'prop-types';
3
+ import {
4
+ TextListItem,
5
+ TextListItemVariants,
6
+ TextInput,
7
+ TextArea,
8
+ } from '@patternfly/react-core';
9
+
10
+ import { translate as __ } from 'foremanReact/common/I18n';
11
+
12
+ import '../../../../lib/EditableTextInput/editableTextInput.scss';
13
+
14
+ const StaticDetail = ({
15
+ value,
16
+ label,
17
+ id,
18
+ onChange,
19
+ isTextArea,
20
+ validated,
21
+ isRequired,
22
+ }) => {
23
+ const finalLabel = isRequired ? __(`${label} *`) : __(`${label}`);
24
+
25
+ return (
26
+ <React.Fragment key={label}>
27
+ <TextListItem component={TextListItemVariants.dt}>
28
+ {finalLabel}
29
+ </TextListItem>
30
+ <TextListItem
31
+ component={TextListItemVariants.dd}
32
+ className="foreman-spaced-list"
33
+ >
34
+ {isTextArea ? (
35
+ <TextArea
36
+ id={id}
37
+ onChange={onChange}
38
+ value={value}
39
+ validated={validated}
40
+ isRequired={isRequired}
41
+ />
42
+ ) : (
43
+ <TextInput
44
+ id={id}
45
+ onChange={onChange}
46
+ value={value}
47
+ validated={validated}
48
+ isRequired={isRequired}
49
+ />
50
+ )}
51
+ </TextListItem>
52
+ </React.Fragment>
53
+ );
54
+ };
55
+
56
+ StaticDetail.defaultProps = {
57
+ value: '',
58
+ isTextArea: false,
59
+ isRequired: false,
60
+ };
61
+
62
+ StaticDetail.propTypes = {
63
+ value: PropTypes.string,
64
+ id: PropTypes.string.isRequired,
65
+ label: PropTypes.string.isRequired,
66
+ onChange: PropTypes.func.isRequired,
67
+ validated: PropTypes.string.isRequired,
68
+ isTextArea: PropTypes.bool,
69
+ isRequired: PropTypes.bool,
70
+ };
71
+
72
+ export default StaticDetail;
@@ -0,0 +1,71 @@
1
+ import PropTypes from 'prop-types';
2
+ import React, { useState } from 'react';
3
+ import { Link } from 'react-router-dom';
4
+ import { Label, Tooltip } from '@patternfly/react-core';
5
+
6
+ import withReactRoutes from 'foremanReact/common/withReactRoutes';
7
+
8
+ const NULL_COLOR = 'gray';
9
+ const NULL_TEXT = 'none';
10
+
11
+ const StatusPropertiesLabel = ({
12
+ iconChild,
13
+ statusContent,
14
+ linkUrl,
15
+ color,
16
+ tooltipText,
17
+ }) => {
18
+ const [text, setText] = useState(
19
+ statusContent !== null ? statusContent : NULL_TEXT
20
+ );
21
+ const [iconColor, setIconColor] = useState(
22
+ statusContent !== null ? color : NULL_COLOR
23
+ );
24
+
25
+ if (statusContent !== null && text !== `${statusContent}`) {
26
+ setText(`${statusContent}`);
27
+ setIconColor(color);
28
+ }
29
+
30
+ return (
31
+ <Tooltip content={tooltipText}>
32
+ <Label
33
+ isCompact
34
+ icon={iconChild}
35
+ color={iconColor}
36
+ render={({ className, content, componentRef }) => (
37
+ <Link
38
+ to={`${linkUrl}`}
39
+ className={className}
40
+ innerRef={componentRef}
41
+ target="_blank"
42
+ rel="noopener noreferrer"
43
+ >
44
+ {content}
45
+ </Link>
46
+ )}
47
+ >
48
+ {text}
49
+ </Label>
50
+ </Tooltip>
51
+ );
52
+ };
53
+
54
+ StatusPropertiesLabel.defaultProps = {
55
+ color: 'blue',
56
+ statusContent: null,
57
+ };
58
+
59
+ StatusPropertiesLabel.propTypes = {
60
+ iconChild: PropTypes.element.isRequired,
61
+ statusContent: PropTypes.oneOfType([
62
+ PropTypes.string,
63
+ PropTypes.number,
64
+ PropTypes.oneOf([null]),
65
+ ]),
66
+ linkUrl: PropTypes.string.isRequired,
67
+ color: PropTypes.string,
68
+ tooltipText: PropTypes.string.isRequired,
69
+ };
70
+
71
+ export default withReactRoutes(StatusPropertiesLabel);
@@ -0,0 +1,50 @@
1
+ /* eslint-disable promise/prefer-await-to-then */
2
+ // Configure Enzyme
3
+ import { mount } from '@theforeman/test';
4
+ import React from 'react';
5
+ import { Provider } from 'react-redux';
6
+ import store from 'foremanReact/redux';
7
+ import LabelIcon from 'foremanReact/components/common/LabelIcon';
8
+ import StatusPropertiesLabel from './StatusPropertiesLabel';
9
+
10
+ const defaultProps = {
11
+ color: 'blue',
12
+ iconChild: <LabelIcon text="test" />,
13
+ statusContent: 'some content',
14
+ linkUrl: '/test/link',
15
+ tooltipText: 'Some nice tooltip',
16
+ };
17
+
18
+ describe('StatusPropertiesLabel', () => {
19
+ const wrapper = mount(
20
+ <Provider store={store}>
21
+ <StatusPropertiesLabel {...defaultProps} />
22
+ </Provider>
23
+ );
24
+
25
+ it('includes components', () => {
26
+ expect(wrapper.find('Tooltip').exists()).toBe(true);
27
+ expect(wrapper.find('Tooltip')).toHaveLength(1);
28
+ expect(wrapper.find('Label').exists()).toBe(true);
29
+ expect(wrapper.find('Label')).toHaveLength(1);
30
+ expect(wrapper.find('Link').exists()).toBe(true);
31
+ expect(wrapper.find('Link')).toHaveLength(1);
32
+ });
33
+
34
+ it('passes properties', () => {
35
+ // ToolTip
36
+ const tooltip = wrapper.find('Tooltip');
37
+ expect(tooltip.props()).toHaveProperty('content');
38
+ expect(tooltip.prop('content')).toContain(defaultProps.tooltipText);
39
+ // Label
40
+ const label = wrapper.find('Label');
41
+ expect(label.props()).toHaveProperty('icon');
42
+ expect(label.prop('icon')).toEqual(defaultProps.iconChild);
43
+ expect(label.props()).toHaveProperty('color');
44
+ expect(label.prop('color')).toEqual(defaultProps.color);
45
+ // Link
46
+ const link = wrapper.find('Link');
47
+ expect(link.props()).toHaveProperty('to');
48
+ expect(link.prop('to')).toEqual(defaultProps.linkUrl);
49
+ });
50
+ });
@@ -0,0 +1,131 @@
1
+ import React, { useState, useEffect, useCallback } from 'react';
2
+ import { useDispatch } from 'react-redux';
3
+ import PropTypes from 'prop-types';
4
+
5
+ import { translate as __ } from 'foremanReact/common/I18n';
6
+
7
+ import ActionableDetail from '../../../../lib/ActionableDetail';
8
+ import StaticDetail from './StaticDetail';
9
+ import dispatchAPICallbackToast from '../../../../api_helper';
10
+
11
+ const TextInputField = ({
12
+ initialValue,
13
+ label,
14
+ attribute,
15
+ onChange,
16
+ onApply,
17
+ handleInputValidation,
18
+ isRestrictInputValidation,
19
+ isRequired,
20
+ isTextArea,
21
+ isNewQuota,
22
+ }) => {
23
+ const dispatch = useDispatch();
24
+ const [currentAttribute, setCurrentAttribute] = useState();
25
+ const [isLoading, setIsLoading] = useState(false);
26
+ const [isInputValid, setIsInputValid] = useState(!isRequired);
27
+ const [validated, setValidated] = useState(isRequired ? 'error' : 'default');
28
+ const [value, setValue] = useState(initialValue || '');
29
+ const staticKey = `properties-resource-quota-${attribute}-static`;
30
+ const actionKey = `properties-resource-quota-${attribute}-actionable`;
31
+
32
+ const isValid = checkValue => {
33
+ if (isRequired && checkValue === '') return false;
34
+ return true;
35
+ };
36
+
37
+ const callback = (success, response) => {
38
+ setIsLoading(false);
39
+ dispatchAPICallbackToast(
40
+ dispatch,
41
+ success,
42
+ response,
43
+ `Sucessfully applied ${label}.`,
44
+ `An error occurred appyling ${label}.`
45
+ );
46
+ };
47
+
48
+ /* Guard setting of isInputValid to prevent re-render reace condition */
49
+ const localHandleInputValidation = useCallback(
50
+ (valid, firstCall = false) => {
51
+ if (firstCall) {
52
+ setIsInputValid(isInputValid);
53
+ handleInputValidation({ [attribute]: isInputValid });
54
+ } else if (valid !== isInputValid || firstCall) {
55
+ setIsInputValid(valid);
56
+ handleInputValidation({ [attribute]: valid });
57
+ }
58
+ },
59
+ [attribute, handleInputValidation, isInputValid]
60
+ );
61
+
62
+ useEffect(() => {
63
+ localHandleInputValidation(false, true); // this will be executed only on the first render (valid is ignored)
64
+ }, [localHandleInputValidation]);
65
+
66
+ const onEdit = val => {
67
+ if (val === value) return;
68
+ setValue(val);
69
+ if (isValid(val)) {
70
+ localHandleInputValidation(true);
71
+ setValidated('default');
72
+ if (isNewQuota) {
73
+ onChange({ [attribute]: val });
74
+ } else {
75
+ onApply(callback, { [attribute]: val });
76
+ setIsLoading(true);
77
+ }
78
+ } else {
79
+ localHandleInputValidation(false);
80
+ setValidated('error');
81
+ }
82
+ };
83
+
84
+ return isNewQuota ? (
85
+ <StaticDetail
86
+ id={staticKey}
87
+ value={value}
88
+ label={label}
89
+ onChange={onEdit}
90
+ isRequired={isRequired}
91
+ validated={validated}
92
+ isTextArea={isTextArea}
93
+ />
94
+ ) : (
95
+ <ActionableDetail
96
+ key={actionKey}
97
+ label={isRequired ? __(`${label} *`) : __(`${label}`)}
98
+ attribute={attribute}
99
+ loading={isLoading && currentAttribute === attribute}
100
+ onEdit={onEdit}
101
+ value={value}
102
+ disabled={false}
103
+ textArea={isTextArea}
104
+ validated={validated}
105
+ {...{ currentAttribute, setCurrentAttribute }}
106
+ />
107
+ );
108
+ };
109
+
110
+ TextInputField.defaultProps = {
111
+ initialValue: '',
112
+ isTextArea: false,
113
+ isRequired: false,
114
+ isRestrictInputValidation: false,
115
+ isNewQuota: false,
116
+ };
117
+
118
+ TextInputField.propTypes = {
119
+ initialValue: PropTypes.string,
120
+ label: PropTypes.string.isRequired,
121
+ attribute: PropTypes.string.isRequired,
122
+ onChange: PropTypes.func.isRequired,
123
+ onApply: PropTypes.func.isRequired,
124
+ handleInputValidation: PropTypes.func.isRequired,
125
+ isRestrictInputValidation: PropTypes.bool,
126
+ isTextArea: PropTypes.bool,
127
+ isRequired: PropTypes.bool,
128
+ isNewQuota: PropTypes.bool,
129
+ };
130
+
131
+ export default TextInputField;
@@ -0,0 +1,190 @@
1
+ import PropTypes from 'prop-types';
2
+ import React, { useState } from 'react';
3
+ import { useDispatch } from 'react-redux';
4
+ import {
5
+ TextList,
6
+ TextContent,
7
+ TextListVariants,
8
+ Card,
9
+ CardBody,
10
+ CardHeader,
11
+ CardActions,
12
+ CardTitle,
13
+ Level,
14
+ LabelGroup,
15
+ Button,
16
+ Tooltip,
17
+ } from '@patternfly/react-core';
18
+
19
+ import UserIcon from '@patternfly/react-icons/dist/esm/icons/user-icon';
20
+ import UsersIcon from '@patternfly/react-icons/dist/esm/icons/users-icon';
21
+ import ClusterIcon from '@patternfly/react-icons/dist/esm/icons/cluster-icon';
22
+ import SyncAltIcon from '@patternfly/react-icons/dist/esm/icons/sync-alt-icon';
23
+
24
+ import { translate as __ } from 'foremanReact/common/I18n';
25
+ import dispatchAPICallbackToast from '../../../../api_helper';
26
+
27
+ import './Properties.scss';
28
+ import StatusPropertiesLabel from './StatusPropertiesLabel';
29
+ import TextInputField from './TextInputField';
30
+ import {
31
+ RESOURCE_IDENTIFIER_NAME,
32
+ RESOURCE_IDENTIFIER_DESCRIPTION,
33
+ RESOURCE_IDENTIFIER_STATUS_NUM_HOSTS,
34
+ RESOURCE_IDENTIFIER_STATUS_NUM_USERS,
35
+ RESOURCE_IDENTIFIER_STATUS_NUM_USERGROUPS,
36
+ } from '../../ResourceQuotaFormConstants';
37
+
38
+ const Properties = ({
39
+ isNewQuota,
40
+ initialName,
41
+ initialDescription,
42
+ initialStatus,
43
+ handleInputValidation,
44
+ onChange,
45
+ onApply,
46
+ onFetch,
47
+ }) => {
48
+ const dispatch = useDispatch();
49
+ const tooltipRefFetchButton = React.useRef();
50
+
51
+ const [isFetchLoading, setIsFetchLoading] = useState(false);
52
+ const [statusProperties] = useState(initialStatus);
53
+
54
+ const onClickFetch = () => {
55
+ onFetch(callbackFetch);
56
+ setIsFetchLoading(true);
57
+ };
58
+
59
+ const callbackFetch = (success, response) => {
60
+ setIsFetchLoading(false);
61
+ dispatchAPICallbackToast(
62
+ dispatch,
63
+ success,
64
+ response,
65
+ `Sucessfully fetched latest data.`,
66
+ `An error occurred fetching quota information.`
67
+ );
68
+ };
69
+
70
+ return (
71
+ <Card>
72
+ <CardHeader>
73
+ {!isNewQuota && (
74
+ <CardActions>
75
+ <Button
76
+ isLoading={isFetchLoading}
77
+ icon={<SyncAltIcon />}
78
+ isSmall
79
+ onClick={onClickFetch}
80
+ ref={tooltipRefFetchButton}
81
+ />
82
+ <Tooltip
83
+ content={
84
+ <div>
85
+ <b> {__('Fetch quota utilization')} </b>
86
+ <div>
87
+ {__(
88
+ 'This can take some time since the resources of every host, assigned to this quota, must be requested.'
89
+ )}
90
+ </div>
91
+ </div>
92
+ }
93
+ reference={tooltipRefFetchButton}
94
+ />
95
+ </CardActions>
96
+ )}
97
+ {isNewQuota ? (
98
+ <CardTitle>{__('Properties')}</CardTitle>
99
+ ) : (
100
+ <Level hasGutter>
101
+ <CardTitle>{__('Properties')}</CardTitle>
102
+ <LabelGroup isCompact>
103
+ <StatusPropertiesLabel
104
+ color="blue"
105
+ iconChild={<ClusterIcon />}
106
+ statusContent={
107
+ statusProperties[RESOURCE_IDENTIFIER_STATUS_NUM_HOSTS]
108
+ }
109
+ linkUrl={`/hosts?search=resource_quota="${initialName}"`}
110
+ tooltipText="Number of assigned hosts"
111
+ />
112
+ <StatusPropertiesLabel
113
+ color="blue"
114
+ iconChild={<UserIcon />}
115
+ statusContent={
116
+ statusProperties[RESOURCE_IDENTIFIER_STATUS_NUM_USERS]
117
+ }
118
+ linkUrl={`/users?search=resource_quota="${initialName}"`}
119
+ tooltipText="Number of assigned users"
120
+ />
121
+ <StatusPropertiesLabel
122
+ color="blue"
123
+ iconChild={<UsersIcon />}
124
+ statusContent={
125
+ statusProperties[RESOURCE_IDENTIFIER_STATUS_NUM_USERGROUPS]
126
+ }
127
+ linkUrl={`/usergroups?search=resource_quota="${initialName}"`}
128
+ tooltipText="Number of assigned usergroups"
129
+ />
130
+ </LabelGroup>
131
+ </Level>
132
+ )}
133
+ </CardHeader>
134
+ <CardBody>
135
+ <TextContent>
136
+ <TextList component={TextListVariants.dl}>
137
+ <TextInputField
138
+ initialValue={initialName}
139
+ isNewQuota={isNewQuota}
140
+ label={__('Name')}
141
+ attribute={RESOURCE_IDENTIFIER_NAME}
142
+ handleInputValidation={handleInputValidation}
143
+ onApply={onApply}
144
+ onChange={onChange}
145
+ isRestrictInputValidation
146
+ isRequired
147
+ />
148
+ <TextInputField
149
+ initialValue={initialDescription}
150
+ isNewQuota={isNewQuota}
151
+ label={__('Description')}
152
+ attribute={RESOURCE_IDENTIFIER_DESCRIPTION}
153
+ handleInputValidation={handleInputValidation}
154
+ onApply={onApply}
155
+ onChange={onChange}
156
+ isTextArea
157
+ />
158
+ </TextList>
159
+ </TextContent>
160
+ </CardBody>
161
+ </Card>
162
+ );
163
+ };
164
+
165
+ Properties.defaultProps = {
166
+ initialName: '',
167
+ initialDescription: '',
168
+ initialStatus: {
169
+ [RESOURCE_IDENTIFIER_STATUS_NUM_HOSTS]: null,
170
+ [RESOURCE_IDENTIFIER_STATUS_NUM_USERS]: null,
171
+ [RESOURCE_IDENTIFIER_STATUS_NUM_USERGROUPS]: null,
172
+ },
173
+ };
174
+
175
+ Properties.propTypes = {
176
+ isNewQuota: PropTypes.bool.isRequired,
177
+ initialName: PropTypes.string,
178
+ initialDescription: PropTypes.string,
179
+ initialStatus: PropTypes.shape({
180
+ [RESOURCE_IDENTIFIER_STATUS_NUM_HOSTS]: PropTypes.number,
181
+ [RESOURCE_IDENTIFIER_STATUS_NUM_USERS]: PropTypes.number,
182
+ [RESOURCE_IDENTIFIER_STATUS_NUM_USERGROUPS]: PropTypes.number,
183
+ }),
184
+ handleInputValidation: PropTypes.func.isRequired,
185
+ onChange: PropTypes.func.isRequired,
186
+ onApply: PropTypes.func.isRequired,
187
+ onFetch: PropTypes.func.isRequired,
188
+ };
189
+
190
+ export default Properties;