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,157 @@
|
|
1
|
+
import { useState, useEffect } from 'react';
|
2
|
+
import { useDispatch } from 'react-redux';
|
3
|
+
|
4
|
+
import { deepEqual, deepCopy } from '../../../helper';
|
5
|
+
import {
|
6
|
+
apiCreateResourceQuota,
|
7
|
+
apiUpdateResourceQuota,
|
8
|
+
apiGetResourceQuota,
|
9
|
+
apiGetResourceQuotaUtilization,
|
10
|
+
} from '../../../api_helper';
|
11
|
+
|
12
|
+
import {
|
13
|
+
RESOURCE_IDENTIFIER_ID,
|
14
|
+
RESOURCE_IDENTIFIER_NAME,
|
15
|
+
RESOURCE_IDENTIFIER_DESCRIPTION,
|
16
|
+
RESOURCE_IDENTIFIER_CPU,
|
17
|
+
RESOURCE_IDENTIFIER_MEMORY,
|
18
|
+
RESOURCE_IDENTIFIER_DISK,
|
19
|
+
RESOURCE_IDENTIFIER_STATUS_NUM_HOSTS,
|
20
|
+
RESOURCE_IDENTIFIER_STATUS_NUM_USERS,
|
21
|
+
RESOURCE_IDENTIFIER_STATUS_NUM_USERGROUPS,
|
22
|
+
RESOURCE_IDENTIFIER_STATUS_UTILIZATION,
|
23
|
+
RESOURCE_IDENTIFIER_STATUS_MISSING_HOSTS,
|
24
|
+
} from '../ResourceQuotaFormConstants';
|
25
|
+
|
26
|
+
const QuotaState = ({ initialProperties, initialStatus, changesCallback }) => {
|
27
|
+
const defaultProperties = {
|
28
|
+
[RESOURCE_IDENTIFIER_ID]: null,
|
29
|
+
[RESOURCE_IDENTIFIER_NAME]: '',
|
30
|
+
[RESOURCE_IDENTIFIER_DESCRIPTION]: '',
|
31
|
+
[RESOURCE_IDENTIFIER_CPU]: null,
|
32
|
+
[RESOURCE_IDENTIFIER_MEMORY]: null,
|
33
|
+
[RESOURCE_IDENTIFIER_DISK]: null,
|
34
|
+
};
|
35
|
+
const defaultStatus = {
|
36
|
+
[RESOURCE_IDENTIFIER_STATUS_NUM_HOSTS]: null,
|
37
|
+
[RESOURCE_IDENTIFIER_STATUS_NUM_USERS]: null,
|
38
|
+
[RESOURCE_IDENTIFIER_STATUS_NUM_USERGROUPS]: null,
|
39
|
+
[RESOURCE_IDENTIFIER_STATUS_MISSING_HOSTS]: null,
|
40
|
+
[RESOURCE_IDENTIFIER_STATUS_UTILIZATION]: {
|
41
|
+
[RESOURCE_IDENTIFIER_CPU]: null,
|
42
|
+
[RESOURCE_IDENTIFIER_MEMORY]: null,
|
43
|
+
[RESOURCE_IDENTIFIER_DISK]: null,
|
44
|
+
},
|
45
|
+
};
|
46
|
+
const dispatch = useDispatch();
|
47
|
+
const [quotaProperties, setQuotaProperties] = useState({
|
48
|
+
...defaultProperties,
|
49
|
+
...initialProperties,
|
50
|
+
});
|
51
|
+
const [quotaStatus, setQuotaStatus] = useState({
|
52
|
+
...defaultStatus,
|
53
|
+
...initialStatus,
|
54
|
+
});
|
55
|
+
const [isValid, setIsValid] = useState({
|
56
|
+
[RESOURCE_IDENTIFIER_NAME]: true,
|
57
|
+
[RESOURCE_IDENTIFIER_DESCRIPTION]: true,
|
58
|
+
[RESOURCE_IDENTIFIER_CPU]: true,
|
59
|
+
[RESOURCE_IDENTIFIER_MEMORY]: true,
|
60
|
+
[RESOURCE_IDENTIFIER_DISK]: true,
|
61
|
+
});
|
62
|
+
|
63
|
+
/* callback to notify outside listeners that the quota changed */
|
64
|
+
useEffect(() => {
|
65
|
+
// changesCallback is optional + check if name is defined (otherwise, it's most likely an empty re-render call)
|
66
|
+
if (changesCallback && quotaProperties[RESOURCE_IDENTIFIER_NAME])
|
67
|
+
changesCallback(quotaProperties, quotaStatus);
|
68
|
+
}, [quotaProperties, quotaStatus, changesCallback]);
|
69
|
+
|
70
|
+
/* callback to handle api response and update quota state */
|
71
|
+
const apiCallback = (success, response, callback) => {
|
72
|
+
if (response.hasOwnProperty('data')) {
|
73
|
+
const responseData = response.data;
|
74
|
+
const apiProperties = JSON.parse(JSON.stringify(quotaProperties));
|
75
|
+
const apiStatus = JSON.parse(JSON.stringify(quotaStatus));
|
76
|
+
|
77
|
+
deepCopy(apiProperties, responseData);
|
78
|
+
deepCopy(apiStatus, responseData);
|
79
|
+
|
80
|
+
if (!deepEqual(quotaProperties, apiProperties))
|
81
|
+
setQuotaProperties(apiProperties);
|
82
|
+
if (!deepEqual(quotaStatus, apiStatus)) setQuotaStatus(apiStatus);
|
83
|
+
}
|
84
|
+
callback(success, response);
|
85
|
+
};
|
86
|
+
|
87
|
+
/* update quotaProperties */
|
88
|
+
const onChange = changes => {
|
89
|
+
const updatedQuota = { ...quotaProperties, ...changes };
|
90
|
+
if (!deepEqual(quotaProperties, updatedQuota)) {
|
91
|
+
setQuotaProperties(updatedQuota);
|
92
|
+
}
|
93
|
+
};
|
94
|
+
|
95
|
+
/* perform API call: apply indidividual changes to existing quota */
|
96
|
+
const onApply = (callback, payload) => {
|
97
|
+
const quotaId = quotaProperties[RESOURCE_IDENTIFIER_ID];
|
98
|
+
dispatch(apiUpdateResourceQuota(quotaId, payload, apiCallback, callback));
|
99
|
+
};
|
100
|
+
|
101
|
+
/* perform API call: create a new Resource Quota */
|
102
|
+
const onCreate = callback => {
|
103
|
+
const payload = (({ [RESOURCE_IDENTIFIER_ID]: _, ...obj }) => obj)(
|
104
|
+
quotaProperties
|
105
|
+
);
|
106
|
+
dispatch(apiCreateResourceQuota(payload, apiCallback, callback));
|
107
|
+
};
|
108
|
+
|
109
|
+
/* perform API call: fetch basic information of a Resource Quota */
|
110
|
+
const onFetch = callback => {
|
111
|
+
const quotaId = quotaProperties[RESOURCE_IDENTIFIER_ID];
|
112
|
+
dispatch(apiGetResourceQuota(quotaId, apiCallback, callback));
|
113
|
+
};
|
114
|
+
|
115
|
+
/* perform API call: fetch basic information of a Resource Quota */
|
116
|
+
const onFetchUtilization = callback => {
|
117
|
+
const quotaId = quotaProperties[RESOURCE_IDENTIFIER_ID];
|
118
|
+
dispatch(apiGetResourceQuotaUtilization(quotaId, apiCallback, callback));
|
119
|
+
};
|
120
|
+
|
121
|
+
/* handle data validity response */
|
122
|
+
const handleInputValidation = changes => {
|
123
|
+
const isValidChanges = { ...isValid, ...changes };
|
124
|
+
if (!deepEqual(isValid, isValidChanges)) {
|
125
|
+
setIsValid(isValidChanges);
|
126
|
+
}
|
127
|
+
};
|
128
|
+
|
129
|
+
/* get the current quota properties */
|
130
|
+
const getQuotaProperties = (sub = null) => {
|
131
|
+
if (sub) return quotaProperties[sub];
|
132
|
+
return quotaProperties;
|
133
|
+
};
|
134
|
+
|
135
|
+
/* get the current quota status */
|
136
|
+
const getQuotaStatus = (sub = null) => {
|
137
|
+
if (sub) return quotaStatus[sub];
|
138
|
+
return quotaStatus;
|
139
|
+
};
|
140
|
+
|
141
|
+
/* get validity status of current data */
|
142
|
+
const getIsValid = () => isValid;
|
143
|
+
|
144
|
+
return {
|
145
|
+
getQuotaProperties,
|
146
|
+
getQuotaStatus,
|
147
|
+
getIsValid,
|
148
|
+
handleInputValidation,
|
149
|
+
onChange,
|
150
|
+
onApply,
|
151
|
+
onCreate,
|
152
|
+
onFetch,
|
153
|
+
onFetchUtilization,
|
154
|
+
};
|
155
|
+
};
|
156
|
+
|
157
|
+
export default QuotaState;
|
@@ -0,0 +1,13 @@
|
|
1
|
+
//@import '~@theforeman/vendor/scss/variables';
|
2
|
+
|
3
|
+
.pf-c-card__body:first-child {
|
4
|
+
padding-top: 1rem;
|
5
|
+
}
|
6
|
+
|
7
|
+
.pf-c-form__label {
|
8
|
+
font-size: var(--pf-global--FontSize--sm);
|
9
|
+
}
|
10
|
+
|
11
|
+
.pf-c-form__group-label-help {
|
12
|
+
font-size: var(--pf-global--FontSize--sm);
|
13
|
+
}
|
@@ -0,0 +1,224 @@
|
|
1
|
+
import React, { useState, useEffect, useCallback } from 'react';
|
2
|
+
import PropTypes from 'prop-types';
|
3
|
+
import {
|
4
|
+
FormGroup,
|
5
|
+
TextInput,
|
6
|
+
InputGroup,
|
7
|
+
InputGroupText,
|
8
|
+
Dropdown,
|
9
|
+
DropdownItem,
|
10
|
+
DropdownToggle,
|
11
|
+
} from '@patternfly/react-core';
|
12
|
+
|
13
|
+
import { sprintf, translate as __ } from 'foremanReact/common/I18n';
|
14
|
+
|
15
|
+
import { findLargestFittingUnit } from '../../../../helper';
|
16
|
+
|
17
|
+
const UnitInputField = ({
|
18
|
+
initialValue,
|
19
|
+
onChange,
|
20
|
+
isDisabled,
|
21
|
+
handleInputValidation,
|
22
|
+
minValue,
|
23
|
+
maxValue,
|
24
|
+
units,
|
25
|
+
labelIcon,
|
26
|
+
}) => {
|
27
|
+
/* flexible unit adaption variables */
|
28
|
+
const [errorText, setErrorText] = useState('');
|
29
|
+
const [validated, setValidated] = useState('default');
|
30
|
+
const [isUnitOpen, setIsUnitOpen] = useState(false);
|
31
|
+
const bestFitUnit = findLargestFittingUnit(initialValue, units);
|
32
|
+
const [selectedUnit, setSelectedUnit] = useState(bestFitUnit);
|
33
|
+
const [inputValue, setInputValue] = useState(
|
34
|
+
initialValue / bestFitUnit.factor
|
35
|
+
);
|
36
|
+
let unitDropdownItems = [];
|
37
|
+
|
38
|
+
/* generate unitDropdownItems depending on unit */
|
39
|
+
if (units.length > 1) {
|
40
|
+
unitDropdownItems = units.map(unit => (
|
41
|
+
<DropdownItem
|
42
|
+
id={`unit-dropdownitem-${unit.symbol.toLowerCase()}`}
|
43
|
+
key={unit.symbol.toLowerCase()}
|
44
|
+
>
|
45
|
+
{unit.symbol}
|
46
|
+
</DropdownItem>
|
47
|
+
));
|
48
|
+
}
|
49
|
+
|
50
|
+
/* text for bounds errors */
|
51
|
+
const errorTextBounds = useCallback(() => {
|
52
|
+
const boundsText = 'Value must be between %d and %d.';
|
53
|
+
let errorMin = minValue;
|
54
|
+
let errorMax = maxValue;
|
55
|
+
if (selectedUnit) {
|
56
|
+
errorMin = minValue / selectedUnit.factor;
|
57
|
+
errorMax = maxValue / selectedUnit.factor;
|
58
|
+
}
|
59
|
+
return __(sprintf(boundsText, errorMin, errorMax));
|
60
|
+
}, [minValue, maxValue, selectedUnit]);
|
61
|
+
|
62
|
+
/* text for float errors */
|
63
|
+
const errorTextNatural = useCallback(
|
64
|
+
() => __('Value must be a natural number.'),
|
65
|
+
[]
|
66
|
+
);
|
67
|
+
|
68
|
+
/* text for float errors */
|
69
|
+
const errorTextFloating = useCallback(
|
70
|
+
() => __(`No floating point for smallest unit (${units[0].symbol}).`),
|
71
|
+
[units]
|
72
|
+
);
|
73
|
+
|
74
|
+
/* applies the selected unit and checks the bounds */
|
75
|
+
const isValid = useCallback(
|
76
|
+
val => {
|
77
|
+
if (Number.isNaN(Number(val))) {
|
78
|
+
setErrorText(errorTextNatural());
|
79
|
+
return false;
|
80
|
+
}
|
81
|
+
const baseValue = valueToBaseUnit(val);
|
82
|
+
if (baseValue < minValue || baseValue > maxValue) {
|
83
|
+
setErrorText(errorTextBounds());
|
84
|
+
return false;
|
85
|
+
}
|
86
|
+
if (baseValue !== Math.floor(baseValue)) {
|
87
|
+
setErrorText(errorTextFloating());
|
88
|
+
return false;
|
89
|
+
}
|
90
|
+
return true;
|
91
|
+
},
|
92
|
+
[
|
93
|
+
minValue,
|
94
|
+
maxValue,
|
95
|
+
valueToBaseUnit,
|
96
|
+
errorTextNatural,
|
97
|
+
errorTextBounds,
|
98
|
+
errorTextFloating,
|
99
|
+
]
|
100
|
+
);
|
101
|
+
|
102
|
+
/* applies the selected unit and returns the base-unit value */
|
103
|
+
const valueToBaseUnit = useCallback(
|
104
|
+
val => {
|
105
|
+
if (units.length > 1) {
|
106
|
+
return selectedUnit.factor * val;
|
107
|
+
}
|
108
|
+
return Number(val);
|
109
|
+
},
|
110
|
+
[units, selectedUnit]
|
111
|
+
);
|
112
|
+
|
113
|
+
useEffect(() => {
|
114
|
+
if (isDisabled) {
|
115
|
+
handleInputValidation(true);
|
116
|
+
setValidated('default');
|
117
|
+
} else if (isValid(inputValue)) {
|
118
|
+
const baseValue = valueToBaseUnit(inputValue);
|
119
|
+
onChange(baseValue);
|
120
|
+
handleInputValidation(true);
|
121
|
+
setValidated('default');
|
122
|
+
} else {
|
123
|
+
handleInputValidation(false);
|
124
|
+
setValidated('error');
|
125
|
+
}
|
126
|
+
}, [
|
127
|
+
isDisabled,
|
128
|
+
inputValue,
|
129
|
+
selectedUnit,
|
130
|
+
handleInputValidation,
|
131
|
+
onChange,
|
132
|
+
isValid,
|
133
|
+
valueToBaseUnit,
|
134
|
+
]);
|
135
|
+
|
136
|
+
/* set the selected unit */
|
137
|
+
const onUnitSelect = event => {
|
138
|
+
const { id } = event.currentTarget;
|
139
|
+
const selectedListItem = unitDropdownItems.find(
|
140
|
+
item => item.props.id === id
|
141
|
+
);
|
142
|
+
const unitItem = units.find(
|
143
|
+
item => item.symbol === selectedListItem.props.children
|
144
|
+
);
|
145
|
+
setSelectedUnit(unitItem);
|
146
|
+
setIsUnitOpen(false);
|
147
|
+
// FIXME: Fix input validation on unit selection
|
148
|
+
};
|
149
|
+
|
150
|
+
const onUnitToggle = () => {
|
151
|
+
setIsUnitOpen(!isUnitOpen);
|
152
|
+
};
|
153
|
+
|
154
|
+
/* return the unit view depending on the available units
|
155
|
+
*
|
156
|
+
* unit = null : don't print any unit
|
157
|
+
* unit = [.] : print a single Textfield
|
158
|
+
* unit = [..] : print an editable Dropdown menu
|
159
|
+
* */
|
160
|
+
const unitView = () => {
|
161
|
+
if (units.length === 1) {
|
162
|
+
return <InputGroupText>{__(units[0].symbol)}</InputGroupText>;
|
163
|
+
}
|
164
|
+
|
165
|
+
return (
|
166
|
+
<Dropdown
|
167
|
+
onSelect={onUnitSelect}
|
168
|
+
toggle={
|
169
|
+
<DropdownToggle isDisabled={isDisabled} onToggle={onUnitToggle}>
|
170
|
+
{__(`${selectedUnit.symbol}`)}
|
171
|
+
</DropdownToggle>
|
172
|
+
}
|
173
|
+
isOpen={isUnitOpen}
|
174
|
+
dropdownItems={unitDropdownItems}
|
175
|
+
/>
|
176
|
+
);
|
177
|
+
};
|
178
|
+
|
179
|
+
return (
|
180
|
+
<FormGroup
|
181
|
+
label={__('Quota Limit')}
|
182
|
+
validated={validated}
|
183
|
+
helperTextInvalid={errorText}
|
184
|
+
fieldId="quota-limit-resource-quota-form-group"
|
185
|
+
labelIcon={labelIcon || {}}
|
186
|
+
>
|
187
|
+
<InputGroup>
|
188
|
+
<TextInput
|
189
|
+
isDisabled={isDisabled}
|
190
|
+
value={inputValue}
|
191
|
+
min={minValue}
|
192
|
+
max={maxValue}
|
193
|
+
validated={validated}
|
194
|
+
id="reg_token_life_time_input"
|
195
|
+
onChange={setInputValue}
|
196
|
+
/>
|
197
|
+
{unitView()}
|
198
|
+
</InputGroup>
|
199
|
+
</FormGroup>
|
200
|
+
);
|
201
|
+
};
|
202
|
+
|
203
|
+
UnitInputField.propTypes = {
|
204
|
+
initialValue: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
|
205
|
+
onChange: PropTypes.func.isRequired,
|
206
|
+
isDisabled: PropTypes.bool.isRequired,
|
207
|
+
handleInputValidation: PropTypes.func.isRequired,
|
208
|
+
units: PropTypes.arrayOf(
|
209
|
+
PropTypes.shape({
|
210
|
+
symbol: PropTypes.string,
|
211
|
+
factor: PropTypes.number,
|
212
|
+
})
|
213
|
+
).isRequired,
|
214
|
+
labelIcon: PropTypes.node,
|
215
|
+
minValue: PropTypes.number.isRequired,
|
216
|
+
maxValue: PropTypes.number.isRequired,
|
217
|
+
};
|
218
|
+
|
219
|
+
UnitInputField.defaultProps = {
|
220
|
+
initialValue: 0,
|
221
|
+
labelIcon: null,
|
222
|
+
};
|
223
|
+
|
224
|
+
export default UnitInputField;
|
@@ -0,0 +1,151 @@
|
|
1
|
+
import React, { useState } from 'react';
|
2
|
+
import PropTypes from 'prop-types';
|
3
|
+
import {
|
4
|
+
Progress,
|
5
|
+
ProgressSize,
|
6
|
+
ProgressVariant,
|
7
|
+
ProgressMeasureLocation,
|
8
|
+
Tooltip,
|
9
|
+
} from '@patternfly/react-core';
|
10
|
+
import SyncAltIcon from '@patternfly/react-icons/dist/esm/icons/sync-alt-icon';
|
11
|
+
|
12
|
+
import { translate as __ } from 'foremanReact/common/I18n';
|
13
|
+
|
14
|
+
import './UtilizationProgress.scss';
|
15
|
+
import {
|
16
|
+
findLargestFittingUnit,
|
17
|
+
areReactElementsEqual,
|
18
|
+
} from '../../../../helper';
|
19
|
+
|
20
|
+
const UtilizationProgress = ({
|
21
|
+
cardId,
|
22
|
+
resourceValue,
|
23
|
+
resourceUnits,
|
24
|
+
resourceUtilization,
|
25
|
+
isEnabled,
|
26
|
+
isNewQuota,
|
27
|
+
}) => {
|
28
|
+
const [resourceUtilizationPercent, setResourceUtilizationPercent] = useState(
|
29
|
+
null
|
30
|
+
);
|
31
|
+
const [
|
32
|
+
resourceUtilizationTooltipText,
|
33
|
+
setResourceUtilizationTooltipText,
|
34
|
+
] = useState(<div />);
|
35
|
+
const tooltipRefUtilization = React.useRef();
|
36
|
+
|
37
|
+
/* resource utilization bar */
|
38
|
+
const resourceProgressVariant = () => {
|
39
|
+
if (resourceUtilizationPercent < 80) return null;
|
40
|
+
else if (resourceUtilizationPercent < 100) return ProgressVariant.warning;
|
41
|
+
return ProgressVariant.danger;
|
42
|
+
};
|
43
|
+
const updateResourceUtilizationView = () => {
|
44
|
+
let newPercent;
|
45
|
+
let newTooltipText;
|
46
|
+
|
47
|
+
if (isNewQuota) {
|
48
|
+
newPercent = 0;
|
49
|
+
newTooltipText = (
|
50
|
+
<div>
|
51
|
+
<div>{__('New quota - no consumption to display.')}</div>
|
52
|
+
</div>
|
53
|
+
);
|
54
|
+
} else if (resourceUtilization == null) {
|
55
|
+
newPercent = 0;
|
56
|
+
newTooltipText = (
|
57
|
+
<div>
|
58
|
+
<div>{__('No consumption to display.')}</div>
|
59
|
+
<div>
|
60
|
+
{__('Click on "Fetch quota utilization": ')}
|
61
|
+
<SyncAltIcon />
|
62
|
+
</div>
|
63
|
+
</div>
|
64
|
+
);
|
65
|
+
} else if (resourceUtilization > resourceValue) {
|
66
|
+
const valueUnit = findLargestFittingUnit(resourceValue, resourceUnits);
|
67
|
+
const utilizationUnit = findLargestFittingUnit(
|
68
|
+
resourceUtilization,
|
69
|
+
resourceUnits
|
70
|
+
);
|
71
|
+
const utilizationInUnit = resourceUtilization / utilizationUnit.factor;
|
72
|
+
const valueInUnit = resourceValue / valueUnit.factor;
|
73
|
+
newPercent = 100;
|
74
|
+
newTooltipText = (
|
75
|
+
<div>
|
76
|
+
{`${utilizationInUnit} ${utilizationUnit.symbol} / ${valueInUnit} ${valueUnit.symbol}`}
|
77
|
+
</div>
|
78
|
+
);
|
79
|
+
} else {
|
80
|
+
const valueUnit = findLargestFittingUnit(resourceValue, resourceUnits);
|
81
|
+
const utilizationUnit = findLargestFittingUnit(
|
82
|
+
resourceUtilization,
|
83
|
+
resourceUnits
|
84
|
+
);
|
85
|
+
const utilizationInUnit = resourceUtilization / utilizationUnit.factor;
|
86
|
+
const valueInUnit = resourceValue / valueUnit.factor;
|
87
|
+
const percent = (100 * resourceUtilization) / resourceValue;
|
88
|
+
if (Number.isFinite(percent)) newPercent = percent;
|
89
|
+
else newPercent = 0;
|
90
|
+
newTooltipText = (
|
91
|
+
<div>
|
92
|
+
{`${utilizationInUnit} ${utilizationUnit.symbol} / ${valueInUnit} ${valueUnit.symbol}`}
|
93
|
+
</div>
|
94
|
+
);
|
95
|
+
}
|
96
|
+
if (resourceUtilizationPercent !== newPercent)
|
97
|
+
setResourceUtilizationPercent(newPercent);
|
98
|
+
if (!areReactElementsEqual(resourceUtilizationTooltipText, newTooltipText))
|
99
|
+
setResourceUtilizationTooltipText(newTooltipText);
|
100
|
+
};
|
101
|
+
// call it once
|
102
|
+
updateResourceUtilizationView();
|
103
|
+
|
104
|
+
return (
|
105
|
+
<div>
|
106
|
+
<Tooltip
|
107
|
+
content={resourceUtilizationTooltipText}
|
108
|
+
reference={tooltipRefUtilization}
|
109
|
+
/>
|
110
|
+
<div
|
111
|
+
className={isEnabled ? '' : 'progress-disabled'}
|
112
|
+
ref={tooltipRefUtilization}
|
113
|
+
>
|
114
|
+
<Progress
|
115
|
+
aria-label={`resource-card-${cardId}-progress`}
|
116
|
+
value={resourceUtilizationPercent}
|
117
|
+
measureLocation={ProgressMeasureLocation.inside}
|
118
|
+
size={ProgressSize.lg}
|
119
|
+
variant={resourceProgressVariant()}
|
120
|
+
/>
|
121
|
+
</div>
|
122
|
+
</div>
|
123
|
+
);
|
124
|
+
};
|
125
|
+
|
126
|
+
UtilizationProgress.propTypes = {
|
127
|
+
cardId: PropTypes.string.isRequired,
|
128
|
+
resourceUnits: PropTypes.arrayOf(
|
129
|
+
PropTypes.shape({
|
130
|
+
symbol: PropTypes.string,
|
131
|
+
factor: PropTypes.number,
|
132
|
+
})
|
133
|
+
).isRequired,
|
134
|
+
resourceValue: PropTypes.oneOfType([
|
135
|
+
PropTypes.number,
|
136
|
+
PropTypes.oneOf([null]),
|
137
|
+
]),
|
138
|
+
resourceUtilization: PropTypes.oneOfType([
|
139
|
+
PropTypes.number,
|
140
|
+
PropTypes.oneOf([null]),
|
141
|
+
]),
|
142
|
+
isNewQuota: PropTypes.bool.isRequired,
|
143
|
+
isEnabled: PropTypes.bool.isRequired,
|
144
|
+
};
|
145
|
+
|
146
|
+
UtilizationProgress.defaultProps = {
|
147
|
+
resourceValue: 0,
|
148
|
+
resourceUtilization: null,
|
149
|
+
};
|
150
|
+
|
151
|
+
export default UtilizationProgress;
|
@@ -0,0 +1,10 @@
|
|
1
|
+
.progress-disabled {
|
2
|
+
background-color: transparent;
|
3
|
+
.pf-c-progress .pf-c-progress__bar::before {
|
4
|
+
background-color: var(--pf-global--disabled-color--100);
|
5
|
+
}
|
6
|
+
}
|
7
|
+
|
8
|
+
.progress-disabled .pf-c-progress.pf-m-inside .pf-c-progress__indicator {
|
9
|
+
background-color: var(--pf-global--palette--black-500);
|
10
|
+
}
|