-
Notifications
You must be signed in to change notification settings - Fork 303
Fixes #38900 - Convert Host Collections list/details page to React #11563
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Conversation
This commit converts the Host Collections list page from AngularJS to React with PatternFly 5 components, accessible at /labs/host_collections. Features implemented: - List page using TableIndexPage from Foreman - Columns: Name (linked), Content Hosts count, Limit - Search, filter, pagination, and sorting capabilities - Create host collection modal with form validation - Copy host collection functionality - Delete host collection with confirmation modal - Row kebab menus with Copy/Delete actions - Success/error toast notifications - Permission-based action visibility Redux infrastructure: - Actions for create, copy, delete operations - Reducer and selectors - API integration with /katello/api/v2/host_collections Components: - HostCollectionsPage - Main list page - CreateHostCollectionModal/Form - Create functionality - CopyHostCollectionModal - Copy functionality - DeleteHostCollectionModal - Delete with confirmation - Jest tests and fixtures The details page, host management, and bulk actions will be implemented in a follow-up phase. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]>
This commit adds the Host Collections details page and several
improvements to the Host Collections React implementation:
Details Page:
- Created details page with tabbed interface (Details and Hosts tabs)
- Hash-based tab navigation allowing direct links to specific tabs
- Details tab with inline editing for name, description, and host limit
- Hosts tab displaying hosts in the collection with add/remove functionality
- Host count label positioned next to collection name in header
- Copy and Delete actions available from Actions dropdown
Navigation:
- List page links to details page for collection names and host counts
- Content Hosts count links directly to Hosts tab via hash (#hosts)
- Total Hosts count in Details tab links to Hosts tab
- Copy action navigates to newly created collection's details page
- Delete action returns to list page after successful deletion
Bug Fixes:
- Fixed copy host collection API call to send correct parameter structure
(changed from {new_name} to {host_collection: {name}})
- Fixed InlineEdit component to populate text fields with current values
when entering edit mode instead of starting with empty fields
- Fixed PropType warnings by ensuring data is loaded before rendering modals
Components Added:
- HostCollectionDetails - Main details page with header and tabs
- DetailsTab - Basic information display with inline editing
- HostsTab - Host management with TableIndexPage
- AddHostsModal - Modal for adding hosts to collection
- HostLimitEdit - Custom inline editor for host limit with unlimited option
- InlineEdit - Reusable inline editing component
- HostCollectionDetailsActions - Redux actions for details page operations
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <[email protected]>
This commit enhances the Host Collections details page with several
improvements to host management and inline editing:
SelectAllCheckbox Integration:
- Added SelectAllCheckbox component to Hosts tab for bulk selection
- Added SelectAllCheckbox component to Add Hosts modal
- Implemented select all/page/none functionality
- Fixed selection handling to work with both individual selections
and select-all mode using exclusion sets
- Updated button disabled states to use selectedCount instead of
selectedResults.length
API and UI Fixes:
- Fixed Add Hosts modal API endpoint from /katello/api/hosts to
/api/hosts (hosts are managed by Foreman core, not Katello)
- Removed "Create New" button from Add Hosts modal by setting
creatable={false} (hosts cannot be created from host collections)
Host Limit Edit Improvements:
- Fixed unlimited checkbox onChange handler to properly extract
boolean value from PatternFly Checkbox component
- Added useEffect hook to sync local state with host collection data
- Fixed NumberInput to display actual limit value instead of always
showing 1
TableIndexPage Migration:
- Replaced deprecated selectionEnabled/onSelect props with
showCheckboxes/selectOne/isSelected pattern
- Replaced actionButtons prop with customToolbarItems
- Added selectionToolbar for SelectAllCheckbox component
- Integrated useBulkSelect hook for advanced selection features
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <[email protected]>
This commit adds the Host Collections React page to the labs menu
and updates host links to use the new React host details page.
Changes:
- Added Host Collections menu entry to labs menu in plugin.rb
- Updated host links in HostsTab to navigate to /new/hosts/{fqdn}#/Overview
instead of the old /hosts/{id}/ format
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <[email protected]>
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hey there - I've reviewed your changes and they look great!
Prompt for AI Agents
Please address the comments from this code review:
## Individual Comments
### Comment 1
<location> `webpack/scenes/HostCollections/Details/HostsTab/HostsTab.js:65-73` </location>
<code_context>
+ };
+
+ // Restrict search to only hosts in this collection
+ const restrictedSearchQuery = (userSearch) => {
+ const filter = `host_collection_id=${hostCollectionId}`;
+ const trimmedSearch = userSearch?.trim() ?? '';
+ if (!!trimmedSearch && !trimmedSearch.includes(filter)) {
+ return `${filter} and ${trimmedSearch}`;
+ }
+ return filter;
+ };
+
</code_context>
<issue_to_address>
**suggestion:** The restrictedSearchQuery function may not handle complex user search queries robustly.
Prepending the filter may cause issues with queries containing logical operators or parentheses. Consider wrapping the user input in parentheses or using a more reliable method to combine filters.
```suggestion
// Restrict search to only hosts in this collection
const restrictedSearchQuery = (userSearch) => {
const filter = `host_collection_id=${hostCollectionId}`;
const trimmedSearch = userSearch?.trim() ?? '';
if (!!trimmedSearch && !trimmedSearch.includes(filter)) {
// Wrap user search in parentheses to preserve logical grouping
return `${filter} and (${trimmedSearch})`;
}
return filter;
};
```
</issue_to_address>
### Comment 2
<location> `webpack/scenes/HostCollections/Create/CreateHostCollectionForm.js:49-50` </location>
<code_context>
+ type: 'success',
+ message: __('Host collection copied successfully'),
+ }));
+ onClose();
+ window.location.href = `/labs/host_collections/${data.id}`;
+ };
</code_context>
<issue_to_address>
**suggestion:** Using window.location.reload after creation may be disruptive to user experience.
Instead of reloading, update the local state or redirect to the new host collection page to maintain application state and improve user experience.
Suggested implementation:
```javascript
// Use React Router's navigation to redirect without reloading the page
navigate(`/labs/host_collections/${data.id}`);
```
1. Make sure to import `useNavigate` from `react-router-dom` at the top of the file:
`import { useNavigate } from 'react-router-dom';`
2. Initialize the `navigate` function in your component:
`const navigate = useNavigate();`
3. If you are not using React Router v6+, you may need to use `useHistory` and `history.push` instead.
4. Remove any other instances of `window.location.reload` or `window.location.href` in the file to ensure navigation is handled via React Router.
</issue_to_address>
### Comment 3
<location> `webpack/scenes/HostCollections/Copy/CopyHostCollectionModal.js:19-30` </location>
<code_context>
+ message: __('Host collection copied successfully'),
+ }));
+ onClose();
+ window.location.href = `/labs/host_collections/${data.id}`;
+ };
+
</code_context>
<issue_to_address>
**suggestion:** Directly setting window.location.href for navigation may bypass SPA routing.
Use the router's navigation method (such as history.push) to maintain SPA behavior and prevent unnecessary page reloads.
```suggestion
import { useHistory } from 'react-router-dom';
const [newName, setNewName] = useState(`Copy of ${hostCollection?.name || ''}`);
const [copying, setCopying] = useState(false);
const history = useHistory();
const handleSuccess = (data) => {
setCopying(false);
dispatch(addToast({
type: 'success',
message: __('Host collection copied successfully'),
}));
onClose();
history.push(`/labs/host_collections/${data.id}`);
};
```
</issue_to_address>
### Comment 4
<location> `webpack/scenes/HostCollections/Details/DetailsTab/HostLimitEdit.js:64-66` </location>
<code_context>
+ setMaxHosts((maxHosts || 1) + 1);
+ };
+
+ const onChange = (event) => {
+ const value = Number(event.target.value);
+ setMaxHosts(value < 1 ? 1 : value);
</code_context>
<issue_to_address>
**suggestion:** NumberInput onChange handler may not handle empty input gracefully.
Clearing the input results in NaN, which prevents value updates. Please handle empty input explicitly or restrict clearing the field.
</issue_to_address>
### Comment 5
<location> `webpack/scenes/HostCollections/__tests__/HostCollectionsPage.test.js:22-31` </location>
<code_context>
+ [firstCollection] = results;
+});
+
+test('Can call API for Host Collections and show on screen on page load', async () => {
+ const autocompleteScope = nockInstance
+ .get(autocompleteUrl)
+ .query(true)
+ .reply(200, []);
+
+ const scope = nockInstance
+ .get(hostCollectionsPath)
+ .query(true)
+ .reply(200, hostCollectionsData);
+
+ const { queryByText } = renderWithRedux(
+ <HostCollectionsPage />,
+ renderOptions,
+ );
+
+ expect(queryByText(firstCollection.name)).toBeNull();
+
+ await patientlyWaitFor(() => {
+ expect(queryByText(firstCollection.name)).toBeInTheDocument();
+ });
+
+ assertNockRequest(autocompleteScope);
+ assertNockRequest(scope);
+});
</code_context>
<issue_to_address>
**suggestion (testing):** Missing tests for error states and API failures.
Add tests for API error responses (e.g., 500, 404) to confirm error handling and toast notifications work as expected.
Suggested implementation:
```javascript
test('Can call API for Host Collections and show on screen on page load', async () => {
const autocompleteScope = nockInstance
.get(autocompleteUrl)
.query(true)
.reply(200, []);
const scope = nockInstance
.get(hostCollectionsPath)
.query(true)
.reply(200, hostCollectionsData);
const { queryByText } = renderWithRedux(
<HostCollectionsPage />,
renderOptions,
);
expect(queryByText(firstCollection.name)).toBeNull();
await patientlyWaitFor(() => {
expect(queryByText(firstCollection.name)).toBeInTheDocument();
});
assertNockRequest(autocompleteScope);
assertNockRequest(scope);
});
test('Shows error message when Host Collections API returns 500', async () => {
const autocompleteScope = nockInstance
.get(autocompleteUrl)
.query(true)
.reply(200, []);
const scope = nockInstance
.get(hostCollectionsPath)
.query(true)
.reply(500, { message: 'Internal Server Error' });
const { getByText } = renderWithRedux(
<HostCollectionsPage />,
renderOptions,
);
await patientlyWaitFor(() => {
expect(getByText(/error/i)).toBeInTheDocument();
});
assertNockRequest(autocompleteScope);
assertNockRequest(scope);
});
test('Shows error message when Host Collections API returns 404', async () => {
const autocompleteScope = nockInstance
.get(autocompleteUrl)
.query(true)
.reply(200, []);
const scope = nockInstance
.get(hostCollectionsPath)
.query(true)
.reply(404, { message: 'Not Found' });
const { getByText } = renderWithRedux(
<HostCollectionsPage />,
renderOptions,
);
await patientlyWaitFor(() => {
expect(getByText(/error/i)).toBeInTheDocument();
});
assertNockRequest(autocompleteScope);
assertNockRequest(scope);
});
test('Shows error message when autocomplete API returns 500', async () => {
const autocompleteScope = nockInstance
.get(autocompleteUrl)
.query(true)
.reply(500, { message: 'Internal Server Error' });
const scope = nockInstance
.get(hostCollectionsPath)
.query(true)
.reply(200, hostCollectionsData);
const { getByText } = renderWithRedux(
<HostCollectionsPage />,
renderOptions,
);
await patientlyWaitFor(() => {
expect(getByText(/error/i)).toBeInTheDocument();
});
assertNockRequest(autocompleteScope);
assertNockRequest(scope);
});
```
- If your error UI uses a specific text (e.g., "An error occurred" or a toast notification), replace `/error/i` in `getByText` with the actual error message or selector.
- If you use a custom toast notification system, you may need to use `queryByRole('alert')` or similar queries to check for the toast.
- Ensure that your error handling in `HostCollectionsPage` actually renders an error message when the API fails.
</issue_to_address>Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.
| // Restrict search to only hosts in this collection | ||
| const restrictedSearchQuery = (userSearch) => { | ||
| const filter = `host_collection_id=${hostCollectionId}`; | ||
| const trimmedSearch = userSearch?.trim() ?? ''; | ||
| if (!!trimmedSearch && !trimmedSearch.includes(filter)) { | ||
| return `${filter} and ${trimmedSearch}`; | ||
| } | ||
| return filter; | ||
| }; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
suggestion: The restrictedSearchQuery function may not handle complex user search queries robustly.
Prepending the filter may cause issues with queries containing logical operators or parentheses. Consider wrapping the user input in parentheses or using a more reliable method to combine filters.
| // Restrict search to only hosts in this collection | |
| const restrictedSearchQuery = (userSearch) => { | |
| const filter = `host_collection_id=${hostCollectionId}`; | |
| const trimmedSearch = userSearch?.trim() ?? ''; | |
| if (!!trimmedSearch && !trimmedSearch.includes(filter)) { | |
| return `${filter} and ${trimmedSearch}`; | |
| } | |
| return filter; | |
| }; | |
| // Restrict search to only hosts in this collection | |
| const restrictedSearchQuery = (userSearch) => { | |
| const filter = `host_collection_id=${hostCollectionId}`; | |
| const trimmedSearch = userSearch?.trim() ?? ''; | |
| if (!!trimmedSearch && !trimmedSearch.includes(filter)) { | |
| // Wrap user search in parentheses to preserve logical grouping | |
| return `${filter} and (${trimmedSearch})`; | |
| } | |
| return filter; | |
| }; |
| const [newName, setNewName] = useState(`Copy of ${hostCollection?.name || ''}`); | ||
| const [copying, setCopying] = useState(false); | ||
|
|
||
| const handleSuccess = (data) => { | ||
| setCopying(false); | ||
| dispatch(addToast({ | ||
| type: 'success', | ||
| message: __('Host collection copied successfully'), | ||
| })); | ||
| onClose(); | ||
| window.location.href = `/labs/host_collections/${data.id}`; | ||
| }; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
suggestion: Directly setting window.location.href for navigation may bypass SPA routing.
Use the router's navigation method (such as history.push) to maintain SPA behavior and prevent unnecessary page reloads.
| const [newName, setNewName] = useState(`Copy of ${hostCollection?.name || ''}`); | |
| const [copying, setCopying] = useState(false); | |
| const handleSuccess = (data) => { | |
| setCopying(false); | |
| dispatch(addToast({ | |
| type: 'success', | |
| message: __('Host collection copied successfully'), | |
| })); | |
| onClose(); | |
| window.location.href = `/labs/host_collections/${data.id}`; | |
| }; | |
| import { useHistory } from 'react-router-dom'; | |
| const [newName, setNewName] = useState(`Copy of ${hostCollection?.name || ''}`); | |
| const [copying, setCopying] = useState(false); | |
| const history = useHistory(); | |
| const handleSuccess = (data) => { | |
| setCopying(false); | |
| dispatch(addToast({ | |
| type: 'success', | |
| message: __('Host collection copied successfully'), | |
| })); | |
| onClose(); | |
| history.push(`/labs/host_collections/${data.id}`); | |
| }; |
jeremylenz
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Haven't spun it up yet, but here are some initial comments to pass on to Claude :)
| import { CheckIcon, TimesIcon, PencilAltIcon } from '@patternfly/react-icons'; | ||
| import './InlineEdit.scss'; | ||
|
|
||
| const InlineEdit = ({ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Katello already has an ActionableDetail component (used in ContentViewInfo) that does this same thing. Make sure to tell your AI to inspect the Katello code base and follow existing patterns - it likes to default to reinventing the wheel.
| window.location.href = `/labs/host_collections/${data.id}`; | ||
| }; | ||
|
|
||
| const handleError = (error) => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This seems like wheel-reinvention too. Tell it to look in webpack/scenes/Tasks/helpers.js and reuse stuff from there.
| const [maxHosts, setMaxHosts] = useState(1); | ||
| const [saving, setSaving] = useState(false); | ||
|
|
||
| const onMinus = () => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I feel like Patternfly's NumberInput does a lot of this logic for you -- https://github.com/katello/katello/blob/0832f953654ae28b723f6108f3f09d7f47373148/webpack/scenes/ActivationKeys/Details/components/EditModal.js#L142-L143
| setDeleting(false); | ||
| dispatch(addToast({ | ||
| type: 'success', | ||
| message: __('Host collection deleted successfully'), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
DON'T SAY SUCCESSFULLY 😠
| window.location.href = '/labs/host_collections'; | ||
| }; | ||
|
|
||
| const handleError = (error) => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same comment as above - reuse the toast helpers already in Katello.
| }, | ||
| lifecycle_environment: { | ||
| title: __('Lifecycle Environment'), | ||
| wrapper: ({ content_facet_attributes }) => ( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Tell your AI about propsToCamelCase :)
| key: `UPDATE_HOST_COLLECTION_${id}`, | ||
| url: katelloApi.getApiUrl(`/host_collections/${id}`), | ||
| params, | ||
| successToast: () => __('Host collection updated successfully'), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| successToast: () => __('Host collection updated successfully'), | |
| successToast: () => __('Host collection updated'), |
| isSorted: true, | ||
| }, | ||
| total_hosts: { | ||
| title: __('Content Hosts'), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| title: __('Content Hosts'), | |
| title: __('Hosts'), |
We don't use the term "Content hosts" anymore.
| <TableIndexPage | ||
| apiUrl="/katello/api/host_collections" | ||
| apiOptions={{ key: HOST_COLLECTIONS_KEY }} | ||
| header={__('Host collections')} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This may be the one place where title case was correct.
| header={__('Host collections')} | |
| header={__('Host Collections')} |
| export default (state = initialState, action) => { | ||
| switch (action.type) { | ||
| default: | ||
| return state; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why have a reducer if it's not going to alter the state?
Actually, tell your AI to look at useAPI and see if we can use that instead. I haven't needed to add a reducer to Katello in ages.
|
I have an agent for angular to react conversions I iterated upon a little using Sync plans UI.. I am wondering if we can also try this agent for hostcollections and compare the results. |
sjha4
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Review by angular-to-react-migrator Agent
I've completed a comprehensive review of this AngularJS to React migration. The PR demonstrates excellent UI implementation and use of modern React patterns, but there are critical architectural issues that need to be addressed to align with established Katello/Foreman migration patterns.
🎯 Key Findings Summary
✅ Excellent Implementation Areas
- Outstanding TableIndexPage Usage - Proper column configuration, kebab actions, and permission-based visibility
- Excellent Bulk Selection - Exemplary use of
useBulkSelectandSelectAllCheckboxin both HostsTab and AddHostsModal - Well-Designed InlineEdit Component - Reusable, accessible, with keyboard navigation
- Smart Hash-Based Tab Navigation - Enables deep linking to specific tabs
- Proper Permission Handling - Conditional action visibility based on user permissions
- Restricted Search Pattern - Smart filtering in HostsTab to scope results
❌ Critical Issues (Must Fix Before Merge)
-
Custom Thunk Actions Instead of Foreman API Helpers (
HostCollectionsActions.js)- Uses custom async thunks with manual try/catch blocks
- Should use
post(),del()fromforemanReact/redux/APIlikeHostCollectionDetailsActions.jsdoes - Violates established Katello migration pattern
-
Unnecessary Custom Reducer (
HostCollectionsReducer.js)- Empty reducer that does nothing
- Should be deleted - migration pattern uses generic API Redux system
- Remove from
webpack/redux/reducers/index.js
-
Custom Selectors Accessing Wrong State Path (
HostCollectionsSelectors.js)- Should use
selectAPIResponse,selectAPIStatus,selectAPIErrorfrom Foreman - Current selectors access
state[HOST_COLLECTIONS_KEY]which doesn't exist with API reducer
- Should use
-
Inconsistent Action Patterns
- Details actions correctly use Foreman API helpers ✅
- Main actions use custom thunks ❌
- These should be consistent
⚠️ Minor Issues
- Full page reload after create instead of navigation to details page
- Consider adding
organization_idto API calls (verify if backend requires it)
📊 Architecture Comparison
| Pattern | Reference (FlatpakRemotes) | This PR | Status |
|---|---|---|---|
| TableIndexPage | ✅ | ✅ | Good |
| Foreman API helpers | ✅ | ❌ | Needs Fix |
| API selectors | ✅ | ❌ | Needs Fix |
| No custom reducer | ✅ | ❌ | Needs Fix |
| Bulk selection | N/A | ✅ | Excellent |
| Permission handling | ✅ | ✅ | Good |
🔧 Required Changes
See inline comments for specific code examples. The main architectural changes needed:
- Refactor
HostCollectionsActions.jsto use Foreman'spost()anddel()helpers - Delete
HostCollectionsReducer.jsand remove from Redux registration - Rewrite
HostCollectionsSelectors.jsto use Foreman's API selectors - Update constants to include separate keys for create/copy/delete operations
📚 Reference Implementation
Your HostCollectionDetailsActions.js file shows the correct pattern:
export const updateHostCollection = (id, params, handleSuccess) => put({
type: API_OPERATIONS.PUT,
key: `UPDATE_HOST_COLLECTION_${id}`,
url: katelloApi.getApiUrl(`/host_collections/${id}`),
params,
successToast: () => __('Host collection updated successfully'),
errorToast: error => error.response?.data?.error?.message,
handleSuccess,
});Apply this same pattern to the create/copy/delete actions in HostCollectionsActions.js.
🎓 Final Verdict
Overall Quality: Good implementation with critical architecture issues
Recommendation: Request changes to align with Katello migration patterns
The functionality is solid and the UI implementation is excellent, but the architecture needs to match established conventions (like FlatpakRemotes, ContentViews, etc.) for maintainability and consistency across the codebase.
Once the architectural issues are resolved, this will be a great addition to the React migration effort! 🚀
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
❌ CRITICAL: Custom Thunk Actions Instead of Foreman API Helpers
This file uses custom async thunk actions with manual try/catch blocks instead of Foreman's standardized API helpers. This violates the established Katello migration pattern.
Should use this pattern instead (like FlatpakRemotes and the Details actions):
import { API_OPERATIONS, post, del } from 'foremanReact/redux/API';
import api, { orgId } from '../../services/api';
export const createHostCollection = params => post({
type: API_OPERATIONS.POST,
key: CREATE_HOST_COLLECTION_KEY,
url: api.getApiUrl('/host_collections'),
params: { organization_id: orgId(), ...params },
successToast: () => __('Host collection created successfully'),
errorToast: error => error.response?.data?.error?.message || __('Failed to create host collection'),
});Why this matters:
- Foreman's API helpers provide consistent error handling, toast notifications, and Redux state management
- The generic API Redux system is used across all Foreman/Katello features
- Your
HostCollectionDetailsActions.jsalready uses this pattern correctly - this file should match that approach
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
❌ CRITICAL: Unnecessary Custom Reducer
This empty reducer should be deleted. The migration pattern explicitly states: "NO custom reducers - Use generic API Redux system instead"
- This reducer does nothing (just returns state on all actions)
- All API state is managed by Foreman's generic API reducer using selectors
- Other migrated features (FlatpakRemotes, ContentViews) don't have custom reducers
Fix:
- Delete this file
- Remove the import from
webpack/redux/reducers/index.js - Use API selectors with
selectAPIResponse(state, key)instead
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
❌ CRITICAL: Custom Selectors Accessing Wrong State Path
This selector tries to access state[HOST_COLLECTIONS_KEY] directly, which doesn't exist when using Foreman's API reducer.
Should use Foreman's API selectors instead:
import {
selectAPIStatus,
selectAPIError,
selectAPIResponse,
} from 'foremanReact/redux/API/APISelectors';
import { HOST_COLLECTIONS_KEY } from './HostCollectionsConstants';
export const selectHostCollections = (state, index = '') =>
selectAPIResponse(state, HOST_COLLECTIONS_KEY + index) || {};
export const selectHostCollectionsStatus = (state, index = '') =>
selectAPIStatus(state, HOST_COLLECTIONS_KEY + index);
export const selectHostCollectionsError = (state, index = '') =>
selectAPIError(state, HOST_COLLECTIONS_KEY + index);Reference: See how FlatpakRemotes does this correctly.
| })); | ||
| }; | ||
|
|
||
| const selectionToolbar = ( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
✅ EXCELLENT: Bulk Selection Implementation
This is an outstanding example of using useBulkSelect and SelectAllCheckbox! The implementation correctly:
- Handles both individual selections and select-all mode
- Provides proper toolbar integration
- Works with paginated data
- Extracts selected IDs correctly for both modes
This pattern should be used as a reference for other bulk selection implementations.
| type: 'success', | ||
| message: __('Host collection created successfully'), | ||
| })); | ||
| onClose(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Using window.location.reload() forces a full page reload. Better to navigate to the new host collection's details page:
const handleSuccess = (data) => {
setSaving(false);
dispatch(addToast({
type: 'success',
message: __('Host collection created successfully'),
}));
onClose();
// Navigate to details page instead of reloading
window.location.href = `/labs/host_collections/${data.id}`;
};This matches the pattern used in CopyHostCollectionModal.js.
|
I really like Sourcery's "Prompt for AI Agents" that I can paste into Claude. @sjha4 Can you have your agent output its comments in a similar way so it's more easily transferable? |
What are the changes introduced in this pull request?
This PR migrates the Host Collections feature from AngularJS to React with PatternFly 5, accessible at /labs/host_collections.
Features implemented:
Considerations taken when implementing this change?
What are the testing steps for this pull request?
Summary by Sourcery
Convert the Host Collections feature to a React/PatternFly 5 implementation under the labs menu, providing a unified list and details experience with CRUD operations, inline edits, host membership management, and permission-aware actions backed by Redux state management and enhanced API integration.
New Features:
Enhancements:
Tests: