foreman_resource_quota 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/LICENSE +619 -0
- data/README.md +51 -0
- data/Rakefile +49 -0
- data/app/controllers/concerns/foreman/controller/parameters/resource_quota.rb +28 -0
- data/app/controllers/foreman_resource_quota/api/v2/resource_quotas_controller.rb +96 -0
- data/app/controllers/foreman_resource_quota/application_controller.rb +9 -0
- data/app/controllers/foreman_resource_quota/resource_quotas_controller.rb +50 -0
- data/app/helpers/foreman_resource_quota/hosts_helper.rb +18 -0
- data/app/helpers/foreman_resource_quota/resource_quota_helper.rb +107 -0
- data/app/models/concerns/foreman_resource_quota/host_managed_extensions.rb +115 -0
- data/app/models/concerns/foreman_resource_quota/user_extensions.rb +15 -0
- data/app/models/concerns/foreman_resource_quota/usergroup_extensions.rb +14 -0
- data/app/models/foreman_resource_quota/resource_quota.rb +83 -0
- data/app/models/foreman_resource_quota/resource_quota_user.rb +10 -0
- data/app/models/foreman_resource_quota/resource_quota_usergroup.rb +10 -0
- data/app/services/foreman_resource_quota/resource_origin.rb +97 -0
- data/app/services/foreman_resource_quota/resource_origins/compute_attributes_origin.rb +64 -0
- data/app/services/foreman_resource_quota/resource_origins/compute_resource_origin.rb +82 -0
- data/app/services/foreman_resource_quota/resource_origins/facts_origin.rb +68 -0
- data/app/services/foreman_resource_quota/resource_origins/vm_attributes_origin.rb +40 -0
- data/app/views/foreman_resource_quota/api/v2/hosts/resource_quota.json.rabl +3 -0
- data/app/views/foreman_resource_quota/api/v2/resource_quotas/base.json.rabl +6 -0
- data/app/views/foreman_resource_quota/api/v2/resource_quotas/create.json.rabl +5 -0
- data/app/views/foreman_resource_quota/api/v2/resource_quotas/hosts.json.rabl +7 -0
- data/app/views/foreman_resource_quota/api/v2/resource_quotas/index.json.rabl +5 -0
- data/app/views/foreman_resource_quota/api/v2/resource_quotas/main.json.rabl +7 -0
- data/app/views/foreman_resource_quota/api/v2/resource_quotas/show.json.rabl +5 -0
- data/app/views/foreman_resource_quota/api/v2/resource_quotas/update.json.rabl +5 -0
- data/app/views/foreman_resource_quota/api/v2/resource_quotas/usergroups.json.rabl +7 -0
- data/app/views/foreman_resource_quota/api/v2/resource_quotas/users.json.rabl +7 -0
- data/app/views/foreman_resource_quota/api/v2/resource_quotas/utilization.json.rabl +7 -0
- data/app/views/foreman_resource_quota/api/v2/usergroups/resource_quota.json.rabl +3 -0
- data/app/views/foreman_resource_quota/api/v2/users/resource_quota.json.rabl +3 -0
- data/app/views/foreman_resource_quota/resource_quotas/_form.html.erb +21 -0
- data/app/views/foreman_resource_quota/resource_quotas/edit.html.erb +12 -0
- data/app/views/foreman_resource_quota/resource_quotas/index.html.erb +55 -0
- data/app/views/foreman_resource_quota/resource_quotas/new.html.erb +10 -0
- data/app/views/foreman_resource_quota/resource_quotas/welcome.html.erb +10 -0
- data/app/views/hosts/_form_quota_fields.html.erb +4 -0
- data/app/views/users/_form_quota_tab.html.erb +45 -0
- data/config/initializers/inflections.rb +5 -0
- data/config/routes.rb +43 -0
- data/db/migrate/20230306120001_create_resource_quotas.rb +31 -0
- data/lib/foreman_resource_quota/engine.rb +56 -0
- data/lib/foreman_resource_quota/exceptions.rb +11 -0
- data/lib/foreman_resource_quota/register.rb +106 -0
- data/lib/foreman_resource_quota/version.rb +5 -0
- data/lib/foreman_resource_quota.rb +6 -0
- data/lib/tasks/foreman_resource_quota_tasks.rake +50 -0
- data/locale/Makefile +60 -0
- data/locale/en/foreman_resource_quota.po +18 -0
- data/locale/foreman_resource_quota.pot +19 -0
- data/locale/gemspec.rb +4 -0
- data/package.json +44 -0
- data/webpack/api_helper.js +113 -0
- data/webpack/api_helper.test.js +96 -0
- data/webpack/components/CreateResourceQuotaModal.js +46 -0
- data/webpack/components/ResourceQuotaEmptyState/index.js +58 -0
- data/webpack/components/ResourceQuotaForm/ResourceQuotaForm.scss +1 -0
- data/webpack/components/ResourceQuotaForm/ResourceQuotaFormConstants.js +71 -0
- data/webpack/components/ResourceQuotaForm/components/Properties/Properties.scss +9 -0
- data/webpack/components/ResourceQuotaForm/components/Properties/StaticDetail.js +72 -0
- data/webpack/components/ResourceQuotaForm/components/Properties/StatusPropertiesLabel.js +71 -0
- data/webpack/components/ResourceQuotaForm/components/Properties/StatusPropertiesLabel.test.js +50 -0
- data/webpack/components/ResourceQuotaForm/components/Properties/TextInputField.js +131 -0
- data/webpack/components/ResourceQuotaForm/components/Properties/index.js +190 -0
- data/webpack/components/ResourceQuotaForm/components/QuotaState.js +157 -0
- data/webpack/components/ResourceQuotaForm/components/Resource/Resource.scss +13 -0
- data/webpack/components/ResourceQuotaForm/components/Resource/UnitInputField.js +224 -0
- data/webpack/components/ResourceQuotaForm/components/Resource/UtilizationProgress.js +151 -0
- data/webpack/components/ResourceQuotaForm/components/Resource/UtilizationProgress.scss +10 -0
- data/webpack/components/ResourceQuotaForm/components/Resource/index.js +239 -0
- data/webpack/components/ResourceQuotaForm/components/Resources.js +105 -0
- data/webpack/components/ResourceQuotaForm/components/Submit.js +72 -0
- data/webpack/components/ResourceQuotaForm/index.js +185 -0
- data/webpack/components/UpdateResourceQuotaModal.js +143 -0
- data/webpack/global_index.js +15 -0
- data/webpack/global_test_setup.js +11 -0
- data/webpack/helper.js +86 -0
- data/webpack/index.js +23 -0
- data/webpack/lib/ActionableDetail.js +115 -0
- data/webpack/lib/ActionableDetail.scss +4 -0
- data/webpack/lib/EditableSwitch.js +47 -0
- data/webpack/lib/EditableTextInput/EditableTextInput.js +206 -0
- data/webpack/lib/EditableTextInput/PencilEditButton.js +27 -0
- data/webpack/lib/EditableTextInput/__tests__/editableTextInput.test.js +193 -0
- data/webpack/lib/EditableTextInput/editableTextInput.scss +38 -0
- data/webpack/lib/EditableTextInput/index.js +4 -0
- data/webpack/lib/react-testing-lib-wrapper.js +80 -0
- data/webpack/test_setup.js +17 -0
- 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,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;
|