foreman_resource_quota 0.4.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +5 -5
  3. data/app/controllers/foreman_resource_quota/api/v2/resource_quotas_controller.rb +1 -1
  4. data/app/controllers/foreman_resource_quota/concerns/api/v2/hosts_controller_extensions.rb +21 -0
  5. data/app/helpers/foreman_resource_quota/hosts_helper.rb +18 -7
  6. data/app/lib/foreman_resource_quota/exceptions.rb +1 -0
  7. data/app/models/concerns/foreman_resource_quota/host_managed_extensions.rb +9 -10
  8. data/app/models/concerns/foreman_resource_quota/user_extensions.rb +27 -0
  9. data/app/models/concerns/foreman_resource_quota/usergroup_extensions.rb +18 -0
  10. data/app/models/foreman_resource_quota/resource_quota.rb +23 -0
  11. data/app/views/foreman_resource_quota/resource_quotas/index.html.erb +9 -1
  12. data/app/views/hosts/_form_quota_fields.html.erb +14 -2
  13. data/app/views/users/_form_quota_tab.html.erb +2 -2
  14. data/db/migrate/20240611141939_drop_missing_hosts.rb +9 -2
  15. data/db/migrate/20250410082728_add_unassigned_flag_to_resource_quota.rb +7 -0
  16. data/db/seeds.d/030-unassigned_quota.rb +36 -0
  17. data/lib/foreman_resource_quota/register.rb +8 -0
  18. data/lib/foreman_resource_quota/version.rb +1 -1
  19. data/lib/tasks/foreman_resource_quota_tasks.rake +4 -5
  20. data/package.json +9 -10
  21. data/webpack/components/CreateResourceQuotaModal.js +1 -0
  22. data/webpack/components/ResourceQuotaEmptyState/__test__/__snapshots__/ResourceQuotaEmptyState.test.js.snap +4 -0
  23. data/webpack/components/ResourceQuotaEmptyState/index.js +1 -0
  24. data/webpack/components/ResourceQuotaForm/ResourceQuotaForm.scss +1 -1
  25. data/webpack/components/ResourceQuotaForm/ResourceQuotaFormConstants.js +1 -0
  26. data/webpack/components/ResourceQuotaForm/components/Properties/Properties.scss +4 -3
  27. data/webpack/components/ResourceQuotaForm/components/Properties/StaticDetail.js +3 -2
  28. data/webpack/components/ResourceQuotaForm/components/Properties/TextInputField.js +4 -1
  29. data/webpack/components/ResourceQuotaForm/components/Properties/index.js +86 -45
  30. data/webpack/components/ResourceQuotaForm/components/Resource/Resource.scss +5 -5
  31. data/webpack/components/ResourceQuotaForm/components/Resource/UnitInputField.js +84 -37
  32. data/webpack/components/ResourceQuotaForm/components/Resource/UnitInputField.scss +7 -0
  33. data/webpack/components/ResourceQuotaForm/components/Resource/UtilizationProgress.js +14 -13
  34. data/webpack/components/ResourceQuotaForm/components/Resource/UtilizationProgress.scss +4 -4
  35. data/webpack/components/ResourceQuotaForm/components/Resource/__test__/__snapshots__/UnitInputField.test.js.snap +149 -140
  36. data/webpack/components/ResourceQuotaForm/components/Resource/index.js +28 -17
  37. data/webpack/components/ResourceQuotaForm/components/Submit.js +2 -1
  38. data/webpack/components/ResourceQuotaForm/index.js +33 -19
  39. data/webpack/components/UpdateResourceQuotaModal.js +7 -1
  40. data/webpack/lib/ActionableDetail.scss +1 -1
  41. data/webpack/lib/EditableSwitch.js +1 -1
  42. data/webpack/lib/EditableTextInput/EditableTextInput.js +81 -77
  43. data/webpack/lib/EditableTextInput/editableTextInput.scss +30 -28
  44. metadata +8 -8
@@ -8,15 +8,16 @@ import {
8
8
  Card,
9
9
  CardBody,
10
10
  CardHeader,
11
- CardActions,
12
11
  CardTitle,
13
- Level,
12
+ Flex,
13
+ FlexItem,
14
14
  LabelGroup,
15
15
  Button,
16
16
  Tooltip,
17
17
  } from '@patternfly/react-core';
18
18
 
19
19
  import {
20
+ ExclamationCircleIcon,
20
21
  UserIcon,
21
22
  UsersIcon,
22
23
  ClusterIcon,
@@ -42,6 +43,8 @@ const Properties = ({
42
43
  initialName,
43
44
  initialDescription,
44
45
  initialStatus,
46
+ unassigned,
47
+ showAssignmentWarning,
45
48
  handleInputValidation,
46
49
  onChange,
47
50
  onApply,
@@ -70,48 +73,70 @@ const Properties = ({
70
73
  );
71
74
  };
72
75
 
73
- return (
74
- <Card>
75
- <CardHeader>
76
- {!isNewQuota && (
77
- <CardActions>
78
- <Button
79
- isLoading={isFetchLoading}
80
- icon={<SyncAltIcon />}
81
- isSmall
82
- onClick={onClickFetch}
83
- ref={tooltipRefFetchButton}
84
- />
85
- <Tooltip
86
- content={
87
- <div>
88
- <b> {__('Fetch quota utilization')} </b>
89
- <div>
90
- {__(
91
- 'This can take some time since the resources of every host, assigned to this quota, must be requested.'
92
- )}
93
- </div>
94
- </div>
76
+ const renderSyncButton = () => {
77
+ if (isNewQuota) {
78
+ return <></>;
79
+ }
80
+ return (
81
+ <Tooltip
82
+ content={
83
+ <div>
84
+ <b> {__('Fetch quota utilization')} </b>
85
+ <div>
86
+ {__(
87
+ 'This can take some time since the resources of every host, assigned to this quota, must be requested.'
88
+ )}
89
+ </div>
90
+ </div>
91
+ }
92
+ reference={tooltipRefFetchButton}
93
+ >
94
+ <Button
95
+ id="resource-quota-index-button"
96
+ ouiaId="resource-quota-index-button"
97
+ isLoading={isFetchLoading}
98
+ icon={<SyncAltIcon />}
99
+ size="sm"
100
+ onClick={onClickFetch}
101
+ ref={tooltipRefFetchButton}
102
+ />
103
+ </Tooltip>
104
+ );
105
+ };
106
+
107
+ const renderHeaderTitle = () => {
108
+ if (isNewQuota) {
109
+ return <CardTitle>{__('Properties')}</CardTitle>;
110
+ }
111
+ return (
112
+ <Flex>
113
+ <FlexItem>
114
+ <CardTitle>{__('Properties')}</CardTitle>
115
+ </FlexItem>
116
+ <FlexItem>
117
+ <LabelGroup isCompact>
118
+ <StatusPropertiesLabel
119
+ color={showAssignmentWarning ? 'red' : 'blue'}
120
+ iconChild={
121
+ showAssignmentWarning ? (
122
+ <ExclamationCircleIcon />
123
+ ) : (
124
+ <ClusterIcon />
125
+ )
126
+ }
127
+ statusContent={
128
+ statusProperties[RESOURCE_IDENTIFIER_STATUS_NUM_HOSTS]
129
+ }
130
+ linkUrl={`/hosts?search=resource_quota="${initialName}"`}
131
+ tooltipText={
132
+ showAssignmentWarning
133
+ ? __(
134
+ "The setting 'resource_quota_optional_assignment' is set to 'No' but there are hosts with no quota assignment. Please check your hosts' quota assignments!"
135
+ )
136
+ : __('Number of assigned hosts')
95
137
  }
96
- reference={tooltipRefFetchButton}
97
138
  />
98
- </CardActions>
99
- )}
100
- {isNewQuota ? (
101
- <CardTitle>{__('Properties')}</CardTitle>
102
- ) : (
103
- <Level hasGutter>
104
- <CardTitle>{__('Properties')}</CardTitle>
105
- <LabelGroup isCompact>
106
- <StatusPropertiesLabel
107
- color="blue"
108
- iconChild={<ClusterIcon />}
109
- statusContent={
110
- statusProperties[RESOURCE_IDENTIFIER_STATUS_NUM_HOSTS]
111
- }
112
- linkUrl={`/hosts?search=resource_quota="${initialName}"`}
113
- tooltipText="Number of assigned hosts"
114
- />
139
+ {!unassigned && (
115
140
  <StatusPropertiesLabel
116
141
  color="blue"
117
142
  iconChild={<UserIcon />}
@@ -121,6 +146,8 @@ const Properties = ({
121
146
  linkUrl={`/users?search=resource_quota="${initialName}"`}
122
147
  tooltipText="Number of assigned users"
123
148
  />
149
+ )}
150
+ {!unassigned && (
124
151
  <StatusPropertiesLabel
125
152
  color="blue"
126
153
  iconChild={<UsersIcon />}
@@ -130,9 +157,17 @@ const Properties = ({
130
157
  linkUrl={`/usergroups?search=resource_quota="${initialName}"`}
131
158
  tooltipText="Number of assigned usergroups"
132
159
  />
133
- </LabelGroup>
134
- </Level>
135
- )}
160
+ )}
161
+ </LabelGroup>
162
+ </FlexItem>
163
+ </Flex>
164
+ );
165
+ };
166
+
167
+ return (
168
+ <Card id="resource-quota-index-card" ouiaId="resource-quota-index-card">
169
+ <CardHeader actions={{ actions: renderSyncButton() }}>
170
+ {renderHeaderTitle()}
136
171
  </CardHeader>
137
172
  <CardBody>
138
173
  <TextContent>
@@ -147,6 +182,7 @@ const Properties = ({
147
182
  onChange={onChange}
148
183
  isRestrictInputValidation
149
184
  isRequired
185
+ isDisabled={unassigned}
150
186
  />
151
187
  <TextInputField
152
188
  initialValue={initialDescription}
@@ -157,6 +193,7 @@ const Properties = ({
157
193
  onApply={onApply}
158
194
  onChange={onChange}
159
195
  isTextArea
196
+ isDisabled={unassigned}
160
197
  />
161
198
  </TextList>
162
199
  </TextContent>
@@ -173,6 +210,8 @@ Properties.defaultProps = {
173
210
  [RESOURCE_IDENTIFIER_STATUS_NUM_USERS]: null,
174
211
  [RESOURCE_IDENTIFIER_STATUS_NUM_USERGROUPS]: null,
175
212
  },
213
+ unassigned: false,
214
+ showAssignmentWarning: false,
176
215
  };
177
216
 
178
217
  Properties.propTypes = {
@@ -184,6 +223,8 @@ Properties.propTypes = {
184
223
  [RESOURCE_IDENTIFIER_STATUS_NUM_USERS]: PropTypes.number,
185
224
  [RESOURCE_IDENTIFIER_STATUS_NUM_USERGROUPS]: PropTypes.number,
186
225
  }),
226
+ unassigned: PropTypes.bool,
227
+ showAssignmentWarning: PropTypes.bool,
187
228
  handleInputValidation: PropTypes.func.isRequired,
188
229
  onChange: PropTypes.func.isRequired,
189
230
  onApply: PropTypes.func.isRequired,
@@ -1,13 +1,13 @@
1
1
  //@import '~@theforeman/vendor/scss/variables';
2
2
 
3
- .pf-c-card__body:first-child {
3
+ .pf-v5-c-card__body:first-child {
4
4
  padding-top: 1rem;
5
5
  }
6
6
 
7
- .pf-c-form__label {
8
- font-size: var(--pf-global--FontSize--sm);
7
+ .pf-v5-c-form__label {
8
+ font-size: var(--pf-v5-global--FontSize--sm);
9
9
  }
10
10
 
11
- .pf-c-form__group-label-help {
12
- font-size: var(--pf-global--FontSize--sm);
11
+ .pf-v5-c-form__group-label-help {
12
+ font-size: var(--pf-v5-global--FontSize--sm);
13
13
  }
@@ -3,16 +3,24 @@ import PropTypes from 'prop-types';
3
3
  import {
4
4
  FormGroup,
5
5
  FormHelperText,
6
+ HelperText,
7
+ HelperTextItem,
6
8
  TextInput,
7
9
  InputGroup,
10
+ InputGroupItem,
8
11
  InputGroupText,
12
+ } from '@patternfly/react-core';
13
+ import { ExclamationCircleIcon } from '@patternfly/react-icons';
14
+
15
+ import {
9
16
  Dropdown,
10
17
  DropdownItem,
11
18
  DropdownToggle,
12
- } from '@patternfly/react-core';
19
+ } from '@patternfly/react-core/deprecated';
13
20
 
14
21
  import { sprintf, translate as __ } from 'foremanReact/common/I18n';
15
22
 
23
+ import './UnitInputField.scss';
16
24
  import { findLargestFittingUnit } from '../../../../helper';
17
25
 
18
26
  const UnitInputField = ({
@@ -41,6 +49,7 @@ const UnitInputField = ({
41
49
  unitDropdownItems = units.map(unit => (
42
50
  <DropdownItem
43
51
  id={`unit-dropdownitem-${unit.symbol.toLowerCase()}`}
52
+ ouiaId={`unit-dropdownitem-${unit.symbol.toLowerCase()}`}
44
53
  key={unit.symbol.toLowerCase()}
45
54
  >
46
55
  {unit.symbol}
@@ -69,11 +78,6 @@ const UnitInputField = ({
69
78
  [units]
70
79
  );
71
80
 
72
- /* warning text displayed beneath value input field (built-in is used for errors) */
73
- const helperTextWarning = (text, isHidden) => (
74
- <FormHelperText isHidden={isHidden}>{text}</FormHelperText>
75
- );
76
-
77
81
  /* applies the selected unit and checks the bounds */
78
82
  const isValid = useCallback(
79
83
  val => {
@@ -164,41 +168,84 @@ const UnitInputField = ({
164
168
  }
165
169
 
166
170
  return (
167
- <Dropdown
168
- onSelect={onUnitSelect}
169
- toggle={
170
- <DropdownToggle isDisabled={isDisabled} onToggle={onUnitToggle}>
171
- {__(`${selectedUnit.symbol}`)}
172
- </DropdownToggle>
173
- }
174
- isOpen={isUnitOpen}
175
- dropdownItems={unitDropdownItems}
176
- />
171
+ <InputGroupItem>
172
+ <Dropdown
173
+ ouiaId="resource-quota-unit-input-field-input-group-item-dropdown"
174
+ onSelect={onUnitSelect}
175
+ toggle={
176
+ <DropdownToggle
177
+ isDisabled={isDisabled}
178
+ ouiaId="resource-quota-unit-input-field-input-group-item-dropdowni-toggle"
179
+ onToggle={(_event, _val) => onUnitToggle()}
180
+ >
181
+ {__(`${selectedUnit.symbol}`)}
182
+ </DropdownToggle>
183
+ }
184
+ isOpen={isUnitOpen}
185
+ dropdownItems={unitDropdownItems}
186
+ />
187
+ </InputGroupItem>
177
188
  );
178
189
  };
179
190
 
191
+ const renderFormHelperText = () => {
192
+ if (validated === 'error') {
193
+ return (
194
+ <FormHelperText>
195
+ <HelperText>
196
+ <HelperTextItem
197
+ icon={<ExclamationCircleIcon />}
198
+ variant={validated}
199
+ >
200
+ {errorText}
201
+ </HelperTextItem>
202
+ </HelperText>
203
+ </FormHelperText>
204
+ );
205
+ }
206
+ if (validated === 'warning') {
207
+ return (
208
+ <FormHelperText>
209
+ <HelperText>
210
+ <HelperTextItem
211
+ icon={<ExclamationCircleIcon />}
212
+ variant={validated}
213
+ >
214
+ {errorText}
215
+ </HelperTextItem>
216
+ </HelperText>
217
+ </FormHelperText>
218
+ );
219
+ }
220
+ return <></>;
221
+ };
222
+
180
223
  return (
181
- <FormGroup
182
- label={__('Quota Limit')}
183
- validated={validated}
184
- helperTextInvalid={errorText}
185
- helperText={helperTextWarning(errorText, validated !== 'warning')}
186
- fieldId="quota-limit-resource-quota-form-group"
187
- labelIcon={labelIcon || {}}
188
- >
189
- <InputGroup>
190
- <TextInput
191
- isDisabled={isDisabled}
192
- value={inputValue}
193
- min={minValue}
194
- max={maxValue}
195
- validated={validated}
196
- id="reg_token_life_time_input"
197
- onChange={setInputValue}
198
- />
199
- {unitView()}
200
- </InputGroup>
201
- </FormGroup>
224
+ <div className="container-unit-input-field">
225
+ <FormGroup
226
+ label={__('Quota Limit')}
227
+ validated={validated}
228
+ fieldId="quota-limit-resource-quota-form-group"
229
+ labelIcon={labelIcon || {}}
230
+ >
231
+ <InputGroup>
232
+ <InputGroupItem>
233
+ <TextInput
234
+ isDisabled={isDisabled}
235
+ value={inputValue}
236
+ min={minValue}
237
+ max={maxValue}
238
+ validated={validated}
239
+ id="resource-quota-reg-token-life-time-input"
240
+ ouiaId="resource-quota-reg-token-life-time-input"
241
+ onChange={(_event, val) => setInputValue(val)}
242
+ />
243
+ </InputGroupItem>
244
+ {unitView()}
245
+ </InputGroup>
246
+ {renderFormHelperText()}
247
+ </FormGroup>
248
+ </div>
202
249
  );
203
250
  };
204
251
 
@@ -0,0 +1,7 @@
1
+ @import 'foremanReact/common/variables';
2
+
3
+ .container-unit-input-field {
4
+ .pf-v5-c-input-group__text {
5
+ word-break: keep-all;
6
+ }
7
+ }
@@ -7,7 +7,7 @@ import {
7
7
  ProgressMeasureLocation,
8
8
  Tooltip,
9
9
  } from '@patternfly/react-core';
10
- import SyncAltIcon from '@patternfly/react-icons';
10
+ import { SyncAltIcon } from '@patternfly/react-icons';
11
11
 
12
12
  import { translate as __ } from 'foremanReact/common/I18n';
13
13
 
@@ -105,19 +105,20 @@ const UtilizationProgress = ({
105
105
  <Tooltip
106
106
  content={resourceUtilizationTooltipText}
107
107
  reference={tooltipRefUtilization}
108
- />
109
- <div
110
- className={isEnabled ? '' : 'progress-disabled'}
111
- ref={tooltipRefUtilization}
112
108
  >
113
- <Progress
114
- aria-label={`resource-card-${cardId}-progress`}
115
- value={resourceUtilizationPercent}
116
- measureLocation={ProgressMeasureLocation.inside}
117
- size={ProgressSize.lg}
118
- variant={resourceProgressVariant()}
119
- />
120
- </div>
109
+ <div
110
+ className={isEnabled ? '' : 'progress-disabled'}
111
+ ref={tooltipRefUtilization}
112
+ >
113
+ <Progress
114
+ aria-label={`resource-card-${cardId}-progress`}
115
+ value={resourceUtilizationPercent}
116
+ measureLocation={ProgressMeasureLocation.inside}
117
+ size={ProgressSize.lg}
118
+ variant={resourceProgressVariant()}
119
+ />
120
+ </div>
121
+ </Tooltip>
121
122
  </div>
122
123
  );
123
124
  };
@@ -1,10 +1,10 @@
1
1
  .progress-disabled {
2
2
  background-color: transparent;
3
- .pf-c-progress .pf-c-progress__bar::before {
4
- background-color: var(--pf-global--disabled-color--100);
3
+ .pf-v5-c-progress .pf-v5-c-progress__bar::before {
4
+ background-color: var(--pf-v5-global--disabled-color--100);
5
5
  }
6
6
  }
7
7
 
8
- .progress-disabled .pf-c-progress.pf-m-inside .pf-c-progress__indicator {
9
- background-color: var(--pf-global--palette--black-500);
8
+ .progress-disabled .pf-v5-c-progress.pf-v5-m-inside .pf-v5-c-progress__indicator {
9
+ background-color: var(--pf-v5-global--palette--black-500);
10
10
  }