Fixes #39200 - Migrate Model Pages to React#10943
Conversation
04b2c2b to
22ce3f8
Compare
| name="model_hardware_model" | ||
| type="text" | ||
| ouiaId="model_hardware_model-input" | ||
| required |
There was a problem hiding this comment.
L54 says only name is required
| name="model_vendor_class" | ||
| type="text" | ||
| ouiaId="model_vendor_class-input" | ||
| required |
There was a problem hiding this comment.
L54 says only name is required
There was a problem hiding this comment.
Pull request overview
Migrates the Hardware Models UI pages (index/new/edit) from Rails views to React routes, aligning Models with other recently migrated SPA pages in the Foreman UI.
Changes:
- Expand the React
Modelsroute export to include/models/newand/models/:id/edit, and wire it into the main route list. - Add new React pages/components for creating and editing models, including a shared
ModelFormand associated JS tests. - Update Rails routing to serve React for
models#newandmodels#edit, remove legacy ERB templates, and adjust Ruby/JS test coverage accordingly.
Reviewed changes
Copilot reviewed 16 out of 16 changed files in this pull request and generated 6 comments.
Show a summary per file
| File | Description |
|---|---|
| webpack/assets/javascripts/react_app/routes/routes.js | Spreads the Models route list into the main React routes array. |
| webpack/assets/javascripts/react_app/routes/test/snapshots/Routes.test.js.snap | Updates snapshot to include new React routes for models new/edit. |
| webpack/assets/javascripts/react_app/routes/Models/index.js | Changes Models route export from a single route to an array (index/new/edit). |
| webpack/assets/javascripts/react_app/routes/Models/constants.js | Adds constants for new/edit model paths. |
| webpack/assets/javascripts/react_app/routes/Models/NewModelFormPage.js | New React page for creating a model via API submit + breadcrumb. |
| webpack/assets/javascripts/react_app/routes/Models/ModelsPage/index.js | Uses React Router navigation for create and edit links. |
| webpack/assets/javascripts/react_app/routes/Models/ModelForm.test.js | Adds unit tests for the shared React model form. |
| webpack/assets/javascripts/react_app/routes/Models/ModelForm.js | Introduces shared PF4 form component for new/edit model. |
| webpack/assets/javascripts/react_app/routes/Models/EditModelFormPage.js | New React page for editing a model (loads initial values via API). |
| webpack/assets/javascripts/react_app/redux/actions/common/forms.js | Adjusts default success toast to distinguish create vs update. |
| test/integration/model_js_test.rb | Adds JS integration test coverage for deleting a model from the index. |
| test/controllers/models_controller_test.rb | Removes controller tests tied to removed new/edit templates. |
| test/controllers/api/v2/models_controller_test.rb | Adds API tests ensuring blank names are rejected on create/update. |
| config/routes.rb | Routes models new/edit to React and keeps models index handled by React. |
| app/views/models/new.html.erb | Removes legacy Rails view for model creation page. |
| app/views/models/edit.html.erb | Removes legacy Rails view for model edit page. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| resources :models, except: [:show, :index] do | ||
| member do | ||
| get 'edit', to: 'react#index' | ||
| end | ||
| collection do | ||
| get 'new', to: 'react#index', as: 'new' | ||
| get 'auto_complete_search' | ||
| end | ||
| end |
There was a problem hiding this comment.
resources :models, except: [:show, :index] still generates the default new/edit routes to ModelsController#new/#edit, but the corresponding templates were removed in this PR. This can lead to route conflicts (depending on route order) and/or MissingTemplate errors when hitting /models/new or /models/:id/edit. Align this with the filters migration pattern by excluding :new and :edit (and relying on the explicit react#index routes), or otherwise ensure the default routes are not reachable.
| name="model_hardware_model" | ||
| type="text" | ||
| ouiaId="model_hardware_model-input" | ||
| required |
There was a problem hiding this comment.
hardware_model is marked required in the input, but the backend model only validates presence for name (hardware_model can be blank). With required set, the browser will block form submission unless this field is filled, changing behavior from the previous form.
| required |
| name="model_vendor_class" | ||
| type="text" | ||
| ouiaId="model_vendor_class-input" | ||
| required |
There was a problem hiding this comment.
vendor_class is also marked required, but the backend does not require it. This will prevent submitting the form with vendor_class left blank, which differs from the previous Rails form and model validations.
| required |
| <FormGroup label={__('Name')} isRequired> | ||
| <TextInput | ||
| id="model_name" | ||
| name="model_name" | ||
| type="text" | ||
| ouiaId="model_name-input" | ||
| required | ||
| value={values.name} | ||
| onChange={handleChange('name')} | ||
| /> |
There was a problem hiding this comment.
initialValues can contain undefined values (your tests cover name: undefined), but the inputs pass value={values.name} etc. Passing undefined as an input value causes uncontrolled/controlled warnings and can break PatternFly inputs. Consider normalizing all field values to '' when storing state or when passing to TextInput/TextArea (e.g., value={values.name || ''}).
There was a problem hiding this comment.
the test passing undefined for a name checks the negative scenario, asserts such value cannot be submitted
| useEffect(() => { | ||
| if (!response) return; | ||
|
|
||
| setValues(prevValues => ({ | ||
| ...prevValues, | ||
| name: response.name || '', | ||
| hardware_model: response.hardware_model || '', | ||
| vendor_class: response.vendor_class || '', | ||
| info: response.info || '', | ||
| })); |
There was a problem hiding this comment.
The useEffect that hydrates values from response will overwrite any user edits made before the API call finishes (slow network, user starts typing quickly), causing lost input. Consider rendering the form only after the initial response is loaded, or only initializing state once (e.g., track an isLoaded/isDirty flag) so late responses don't clobber user changes.
| ? '%s was successfully updated.' | ||
| : '%s was successfully created.', |
There was a problem hiding this comment.
defaultSuccessToast constructs new user-facing strings ("%s was successfully updated." / "%s was successfully created.") without marking them for translation. Since this code is shared across the app and other messages in this module are wrapped in __(), these should also be translatable (e.g., wrap the full template strings in __).
| ? '%s was successfully updated.' | |
| : '%s was successfully created.', | |
| ? __('%s was successfully updated.') | |
| : __('%s was successfully created.'), |
eaafbc2 to
3af6799
Compare
Lukshio
left a comment
There was a problem hiding this comment.
Hi, thanks for the PR, code is clean, tests are looking good. Just some minor changes below.
|
|
||
| const initialValues = useMemo( | ||
| () => ({ | ||
| name: response.name || '', |
There was a problem hiding this comment.
This should be probably always set to response.name since it cannot be submitted without name. Or replaced with state in combination with sucessHandler from API call
| isLoading | ||
| breadcrumbOptions={breadcrumbOptions} | ||
| > | ||
| {null} |
| }) => { | ||
| const dispatch = useDispatch(); | ||
| const history = useHistory(); | ||
| const { response, status } = useAPI('get', `${MODELS_API_PATH}/${id}`); |
There was a problem hiding this comment.
I would prefer to use APIActions.get() and set the values as a successHandler, instead of catching it by useMemo()
| useEffect(() => { | ||
| setValues(initialValues); | ||
| }, [initialValues]); |
There was a problem hiding this comment.
Initial values should be set only once. Maybe I am missing something, but why it's set once in useState and then set once more here?
|
@Lukshio @adamruzicka hi, I think I addressed all the comments, can you please revisit? |
| [response] | ||
| ); | ||
| useEffect(() => { | ||
| setInitialValues(null); |
There was a problem hiding this comment.
| setInitialValues(null); |
This is not needed, state has already null as initial value - L31
| }, | ||
| }) | ||
| ); | ||
| }, [dispatch, id]); |
There was a problem hiding this comment.
| }, [dispatch, id]); | |
| // eslint-disable-next-line react-hooks/exhaustive-deps | |
| }, []); |
Empty dependency array will run the effect only once when page will be mounted, that's what we need.
There was a problem hiding this comment.
I had a problem with this when switching models trough the breadcrumb menu, the form didn't re-render after that
There was a problem hiding this comment.
PageLayout should be used only once, but the child should be rendered conditionally.
|
Trying to submit a model which would cause a name collision just shows a toast saying The actual api response is this {
"error": {"id":null,"errors":{"name":["has already been taken"]},"full_messages":["Name has already been taken"]}
}Since we have all the data (the field and the actual error), could we show that in-line? |
| state => selectAPIResponse(state, modelNamesFetchKey), | ||
| shallowEqual | ||
| ); | ||
| const existingNames = |
There was a problem hiding this comment.
As in, preload up to 10000 hw model names to do name conflict checking on the frontend? That's potentially a lot of data to load and it can't guarantee conflicts won't arise. Couldn't we just display an inline error in case form submit fails?
There was a problem hiding this comment.
good point, but would it still be inline check if triggered only on submit? AI friend suggested some debounce search, wdyt?
There was a problem hiding this comment.
Debounce search sounds like a good solution. Just disable the submit button until the search is finished. It would also be nice to have a loading indicator to show that something is happening.
There was a problem hiding this comment.
but would it still be inline check if triggered only on submit?
Originally by inline I meant "error shown close to the field that caused it", not necessarily "checks are happening as you type". I'd be fine with sticking with on submit validations.
Debounce search sounds like a good solution.
Do we have this pattern anywhere already? By replicating the validations on the frontend, we'd be losing the "backend is the one and only source of truth" stance
There was a problem hiding this comment.
I don't think we have something like this anywhere, but there are different inline validations that work as you type. So I guess the question is more if we want to start doing this.
I see this more as a convenience for users, so they don't have to find out about the problem only after submitting, the backend validation is still there on guard.
I'm also ok with going without this, but in such case I'd just keep the toast. Showing server error inline could imho be confusing, someone could think it is a buggy "as you type" check that didn't fire on time
There was a problem hiding this comment.
I don't think we have something like this anywhere, but there are different inline validations that work as you type. So I guess the question is more if we want to start doing this.
Eventually yes, but I'd say this should be approached systematically.
I'm also ok with going without this, but in such case I'd just keep the toast. Showing server error inline could imho be confusing, someone could think it is a buggy "as you type" check that didn't fire on time
Well, the old page just reset so anything is an improvement over that, as long as the error has some details and not just "oh no, it's on fire :^)"
With that being said, let's postpone it for later.
| id="model_info" | ||
| name="model_info" | ||
| rows={7} | ||
| ouiaId="info-textarea" |
There was a problem hiding this comment.
ouiaId prop does not exists for TextArea component
| <Stack hasGutter> | ||
| <StackItem> | ||
| {labelSkeleton} | ||
| <Skeleton height="36px" screenReaderText={__('Loading form')} /> |
There was a problem hiding this comment.
| <Skeleton height="36px" screenReaderText={__('Loading form')} /> | |
| <Skeleton height="36px" screenreaderText={__('Loading form')} /> |
Fix typo
| const labelSkeleton = <Skeleton height="14px" width="30%" />; | ||
|
|
||
| const ModelFormSkeleton = () => ( | ||
| <Form isWidthLimited ouiaId="model-form-skeleton"> |
There was a problem hiding this comment.
| <Form isWidthLimited ouiaId="model-form-skeleton"> | |
| <Form isWidthLimited id="model-form-skeleton"> |
I missed this one, Form does not have ouiaId
|
Changes looks good. Dev + QE ack. Associated PRs: |
|
squashed after ack |


Converting HW models pages, I did this for learning purposes looking at some more recent rewrites elsewhere in the project, so don't judge me too hard. In a way a follow up to #10910 that removed the PF3 button used in models.
Tests are AI assisted