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,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
+ }