Skip to content

Commit ef193a4

Browse files
MariaAgaLukshio
authored andcommitted
Fixes #39056 - Update CounterInput component to PF5
1 parent fdfcf25 commit ef193a4

9 files changed

Lines changed: 449 additions & 123 deletions

File tree

test/integration/compute_profile_js_test.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ class ComputeProfileJSTest < IntegrationTestWithJavascript
5959
assert click_button("Submit")
6060
visit compute_profile_path(selected_profile)
6161
assert click_link(compute_resources(:mycompute).to_s)
62-
assert_equal "2048 MB", find_field('compute_attribute_vm_attrs_memory').value
62+
assert_equal "2048", find_field('compute_attribute_vm_attrs_memory').value
6363
assert_equal "1", find_field('compute_attribute_vm_attrs_cpus').value
6464
end
6565

webpack/assets/javascripts/react_app/components/MemoryAllocationInput/MemoryAllocationInput.js

Lines changed: 22 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,7 @@
1-
import RCInputNumber from 'rc-input-number';
2-
import React, { useEffect, useState } from 'react';
1+
import React, { useState } from 'react';
32
import PropTypes from 'prop-types';
4-
import { sprintf, translate as __ } from '../../common/I18n';
53
import { DEFAULT_MEMORY_MB, MB_FORMAT, BYTES_PER_MB } from './constants';
6-
import '../common/forms/NumericInput.scss';
4+
import CounterInput from '../common/forms/CounterInput/CounterInput';
75
import { noop } from '../../common/helpers';
86

97
const MemoryAllocationInput = ({
@@ -18,55 +16,38 @@ const MemoryAllocationInput = ({
1816
setError,
1917
setWarning,
2018
}) => {
21-
const [valueMB, setValueMB] = useState(value / BYTES_PER_MB);
22-
23-
useEffect(() => {
24-
const valueBytes = valueMB * BYTES_PER_MB;
25-
if (maxValue && valueBytes > maxValue) {
26-
setWarning(null);
27-
setError(
28-
sprintf(
29-
__('Specified value is higher than maximum value %s'),
30-
`${maxValue / BYTES_PER_MB} ${MB_FORMAT}`
31-
)
32-
);
33-
} else if (recommendedMaxValue && valueBytes > recommendedMaxValue) {
34-
setError(null);
35-
setWarning(
36-
sprintf(
37-
__('Specified value is higher than recommended maximum %s'),
38-
`${recommendedMaxValue / BYTES_PER_MB} ${MB_FORMAT}`
39-
)
40-
);
41-
} else {
42-
setWarning(null);
43-
}
44-
}, [valueMB, recommendedMaxValue, maxValue, setError, setWarning]);
19+
const [valueMB, setValueMB] = useState(Math.round(value / BYTES_PER_MB));
4520

4621
const handleChange = v => {
47-
if (v === valueMB + 1) {
48-
v = valueMB * 2;
49-
} else if (v === valueMB - 1) {
50-
v = Math.floor(valueMB / 2);
51-
}
5222
setValueMB(v);
5323
onChange(v * BYTES_PER_MB);
5424
};
55-
25+
const handlePlus = () => {
26+
handleChange(valueMB * 2);
27+
};
28+
const handleMinus = () => {
29+
handleChange(Math.floor(valueMB / 2));
30+
};
5631
return (
5732
<>
58-
<RCInputNumber
33+
<CounterInput
34+
unit={MB_FORMAT}
5935
value={valueMB}
6036
id={id}
61-
formatter={v => `${v} ${MB_FORMAT}`}
62-
parser={str => str.replace(/\D/g, '')}
6337
onChange={handleChange}
38+
handlePlus={handlePlus}
39+
handleMinus={handleMinus}
6440
disabled={disabled}
65-
min={minValue && minValue / BYTES_PER_MB}
66-
step={1}
67-
precision={0}
41+
min={minValue ? Math.round(minValue / BYTES_PER_MB) : undefined}
42+
max={maxValue ? Math.round(maxValue / BYTES_PER_MB) : undefined}
6843
name=""
69-
prefixCls="foreman-numeric-input"
44+
setError={setError}
45+
setWarning={setWarning}
46+
recommendedMaxValue={
47+
recommendedMaxValue
48+
? Math.round(recommendedMaxValue / BYTES_PER_MB)
49+
: undefined
50+
}
7051
/>
7152
<input type="hidden" name={name} value={valueMB * BYTES_PER_MB} />
7253
</>
Lines changed: 37 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,47 @@
11
import React from 'react';
2-
import { mount } from 'enzyme';
3-
import { Provider } from 'react-redux';
2+
import { render, screen, waitFor } from '@testing-library/react';
3+
import '@testing-library/jest-dom';
44
import { BYTES_PER_MB } from '../constants';
55
import MemoryAllocationInput from '../';
66

7-
87
describe('MemoryAllocationInput', () => {
8+
beforeEach(() => {
9+
jest.clearAllMocks();
10+
});
11+
12+
it('calls setWarning when value exceeds recommendedMaxValue', async () => {
13+
const setWarning = jest.fn();
14+
render(
15+
<MemoryAllocationInput
16+
value={11264 * BYTES_PER_MB}
17+
recommendedMaxValue={10240}
18+
setWarning={setWarning}
19+
/>
20+
);
921

10-
it('warning alert', async () => {
11-
const setWarning = jest.fn();
12-
const component = mount(
13-
<MemoryAllocationInput
14-
value={11264*BYTES_PER_MB}
15-
recommendedMaxValue={10240}
16-
setWarning={setWarning}
17-
/>
18-
);
19-
expect(component.find('.foreman-numeric-input-input').prop('value')).toEqual('11264 MB');
20-
expect(setWarning.mock.calls.length).toBe(1);
22+
const input = screen.getByRole('spinbutton');
23+
expect(input).toHaveValue(11264);
24+
25+
await waitFor(() => {
26+
expect(setWarning).toHaveBeenCalledTimes(1);
27+
});
2128
});
2229

23-
it('error alert', async () => {
24-
const setError = jest.fn();
25-
const component = mount(
26-
<MemoryAllocationInput value={21504*BYTES_PER_MB} maxValue={20480*BYTES_PER_MB} setError={setError} />
27-
);
28-
expect(component.find('.foreman-numeric-input-input').prop('value')).toEqual('21504 MB');
29-
expect(setError.mock.calls.length).toBe(1);
30+
it('calls setError when value exceeds maxValue', async () => {
31+
const setError = jest.fn();
32+
render(
33+
<MemoryAllocationInput
34+
value={21504 * BYTES_PER_MB}
35+
maxValue={20480 * BYTES_PER_MB}
36+
setError={setError}
37+
/>
38+
);
39+
40+
const input = screen.getByRole('spinbutton');
41+
expect(input).toHaveValue(21504);
42+
43+
await waitFor(() => {
44+
expect(setError).toHaveBeenCalledTimes(1);
45+
});
3046
});
3147
});
Lines changed: 101 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import React, { useEffect, useState } from 'react';
2-
import RCInputNumber from 'rc-input-number';
2+
import { NumberInput } from '@patternfly/react-core';
33
import PropTypes from 'prop-types';
4-
import { translate as __ } from '../../../../common/I18n';
4+
import { translate as __, sprintf } from '../../../../common/I18n';
55
import { noop } from '../../../../common/helpers';
66

77
const CounterInput = ({
@@ -16,37 +16,104 @@ const CounterInput = ({
1616
onChange,
1717
setError,
1818
setWarning,
19+
widthChars,
20+
unit,
21+
handlePlus,
22+
handleMinus,
1923
}) => {
20-
const [innerValue, setInnerValue] = useState(value);
24+
const parseValue = v => {
25+
if (v === '' || v == null) return v;
26+
const parsed = Number(v);
27+
return Number.isNaN(parsed) ? v : parsed;
28+
};
29+
30+
const [innerValue, setInnerValue] = useState(() => parseValue(value));
31+
2132
useEffect(() => {
22-
if (max && innerValue > max) {
33+
setInnerValue(parseValue(value));
34+
}, [value]);
35+
36+
const getValidated = () => {
37+
if (max && innerValue > max) return 'error';
38+
if (recommendedMaxValue && innerValue > recommendedMaxValue)
39+
return 'warning';
40+
return 'default';
41+
};
42+
const validated = getValidated();
43+
44+
useEffect(() => {
45+
if (validated === 'error') {
2346
setWarning(null);
24-
setError(__('Specified value is higher than maximum value'));
25-
} else if (recommendedMaxValue && innerValue > recommendedMaxValue) {
47+
setError(
48+
sprintf(
49+
__('Specified value is higher than maximum value %s%s'),
50+
max,
51+
unit ? ` ${unit}` : ''
52+
)
53+
);
54+
} else if (validated === 'warning') {
2655
setError(null);
27-
setWarning(__('Specified value is higher than recommended maximum'));
56+
setWarning(
57+
sprintf(
58+
__('Specified value is higher than recommended maximum %s%s'),
59+
recommendedMaxValue,
60+
unit ? ` ${unit}` : ''
61+
)
62+
);
2863
} else {
2964
setError(null);
3065
setWarning(null);
3166
}
32-
// eslint-disable-next-line react-hooks/exhaustive-deps
33-
}, [recommendedMaxValue, max, innerValue]);
67+
}, [validated, max, recommendedMaxValue, setError, setWarning, unit]);
68+
const setValue = newValue => {
69+
setInnerValue(newValue);
70+
onChange(newValue);
71+
};
72+
const handleChange = event => {
73+
const inputValue = event.target.value;
74+
if (inputValue === '') {
75+
setValue('');
76+
} else {
77+
const parsed = parseInt(inputValue, 10);
78+
const clamped = Number.isNaN(parsed) ? min : parsed;
79+
setValue(min != null ? Math.max(min, clamped) : clamped);
80+
}
81+
};
82+
83+
const defaultHandlePlus = () => {
84+
if (handlePlus) {
85+
handlePlus(innerValue);
86+
} else {
87+
const newValue = (innerValue || 0) + (step || 1);
88+
setValue(newValue);
89+
}
90+
};
3491

35-
const handleChange = v => {
36-
setInnerValue(v);
37-
onChange(v);
92+
const defaultHandleMinus = () => {
93+
if (handleMinus) {
94+
handleMinus(innerValue);
95+
} else {
96+
const current = innerValue || 0;
97+
if (min != null && current <= min) return;
98+
const newValue = current - (step || 1);
99+
setValue(min != null ? Math.max(min, newValue) : newValue);
100+
}
38101
};
39102

40103
return (
41-
<RCInputNumber
42-
value={innerValue}
43-
name={name}
44-
id={id}
104+
<NumberInput
105+
value={innerValue ?? 0}
106+
inputName={name}
107+
inputProps={{ id, name }}
45108
min={min}
46-
disabled={disabled}
109+
max={max}
110+
isDisabled={disabled}
47111
onChange={handleChange}
48-
step={step}
49-
prefixCls="foreman-numeric-input"
112+
onPlus={defaultHandlePlus}
113+
onMinus={defaultHandleMinus}
114+
validated={validated}
115+
widthChars={widthChars}
116+
unit={unit}
50117
/>
51118
);
52119
};
@@ -60,20 +127,28 @@ CounterInput.propTypes = {
60127
recommendedMaxValue: PropTypes.number,
61128
/** Set the max value of the numeric input */
62129
max: PropTypes.number,
63-
/** Set the min value of the numeric input */
130+
/** Set the min value of the numeric input, undefined will be defaulted to 0 */
64131
min: PropTypes.number,
65132
/** Set whether the numeric input will be disabled or not */
66133
disabled: PropTypes.bool,
67134
/** Set the onChange function of the numeric input */
68135
onChange: PropTypes.func,
69136
/** Set the default value of the numeric input */
70-
value: PropTypes.number,
137+
value: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
71138
/** Set the step, the counter will increase and decrease by */
72139
step: PropTypes.number,
73140
/** Component passes the validation error to this function */
74141
setError: PropTypes.func,
75142
/** Component passes the validation warning to this function */
76143
setWarning: PropTypes.func,
144+
/** Set the width of the numeric input in characters */
145+
widthChars: PropTypes.number,
146+
/** Set the unit of the numeric input */
147+
unit: PropTypes.string,
148+
/** Override the default handlePlus function */
149+
handlePlus: PropTypes.func,
150+
/** Override the default handleMinus function */
151+
handleMinus: PropTypes.func,
77152
};
78153

79154
CounterInput.defaultProps = {
@@ -83,11 +158,15 @@ CounterInput.defaultProps = {
83158
value: 1,
84159
step: 1,
85160
min: 1,
86-
max: null,
161+
max: undefined,
87162
recommendedMaxValue: null,
88163
onChange: noop,
89164
setError: noop,
90165
setWarning: noop,
166+
widthChars: 10,
167+
unit: '',
168+
handlePlus: null,
169+
handleMinus: null,
91170
};
92171

93172
export default CounterInput;

0 commit comments

Comments
 (0)