Skip to content

Conversation

@chris1984
Copy link
Member

@chris1984 chris1984 commented Nov 12, 2025

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:

  • List page with search, filter, pagination, and sorting
  • Create, copy, and delete host collection functionality
  • Details page with tabbed interface (Details and Hosts tabs)
  • Inline editing for name, description, and host limit
  • Host management (add/remove hosts from collections)
  • Bulk selection with SelectAllCheckbox integration
  • Permission-based action visibility
  • Toast notifications for success/error states

Considerations taken when implementing this change?

  • Used TableIndexPage from Foreman for consistency with other list/table pages
  • Implemented Redux infrastructure (actions, reducers, selectors) for state management
  • Fixed several API endpoint issues discovered during migration (copy endpoint parameters, hosts API path)
  • Added to /labs menu rather than replacing AngularJS version immediately to allow gradual rollout
  • Hash-based tab navigation enables direct linking to specific tabs
  • InlineEdit component fixes ensure fields populate with current values

What are the testing steps for this pull request?

  1. Navigate to /labs/host_collections to access the React version
  2. List page: Verify search, filter, pagination, and sorting work correctly
  3. Create: Click "Create Host Collection" and create a new collection
  4. Details page: Click on a collection name to view details
  5. Inline editing: Edit name, description, and host limit (test unlimited checkbox)
  6. Hosts tab: Add and remove hosts from the collection
  7. Bulk selection: Test select all/page/none functionality
  8. Copy: Use Actions → Copy and verify navigation to new collection
  9. Delete: Use Actions → Delete and verify return to list page

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:

  • Migrate Host Collections list and details pages from AngularJS to React using PatternFly 5 under /labs/host_collections
  • Implement list page with search, filter, pagination, sorting, create, copy, and delete actions with permission-based visibility
  • Add details page with tabbed interface (Details and Hosts) supporting inline editing of name, description, and host limit

Enhancements:

  • Introduce Redux infrastructure (actions, reducers, selectors) for host collections and fix API endpoint issues discovered during migration
  • Enable hash-based tab navigation and integrate Foreman’s TableIndexPage and SelectAllCheckbox for bulk selection workflows
  • Surface success and error states via toast notifications across create, update, copy, add/remove hosts, and delete operations

Tests:

  • Add unit tests for the React Host Collections list page to verify API calls and empty state rendering

chris1984 and others added 4 commits November 11, 2025 16:15
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]>
Copy link
Contributor

@sourcery-ai sourcery-ai bot left a 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>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment on lines +65 to +73
// 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;
};
Copy link
Contributor

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.

Suggested change
// 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;
};

Comment on lines +19 to +30
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}`;
};
Copy link
Contributor

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.

Suggested change
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}`);
};

Copy link
Member

@jeremylenz jeremylenz left a 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 = ({
Copy link
Member

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) => {
Copy link
Member

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 = () => {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

setDeleting(false);
dispatch(addToast({
type: 'success',
message: __('Host collection deleted successfully'),
Copy link
Member

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) => {
Copy link
Member

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 }) => (
Copy link
Member

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'),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
successToast: () => __('Host collection updated successfully'),
successToast: () => __('Host collection updated'),

isSorted: true,
},
total_hosts: {
title: __('Content Hosts'),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
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')}
Copy link
Member

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.

Suggested change
header={__('Host collections')}
header={__('Host Collections')}

Comment on lines +5 to +8
export default (state = initialState, action) => {
switch (action.type) {
default:
return state;
Copy link
Member

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.

@sjha4
Copy link
Member

sjha4 commented Nov 12, 2025

I have an agent for angular to react conversions I iterated upon a little using Sync plans UI..
https://gist.github.com/sjha4/66c62efc4df6b159198bb4ba17a8ad24

I am wondering if we can also try this agent for hostcollections and compare the results.

Copy link
Member

@sjha4 sjha4 left a 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

  1. Outstanding TableIndexPage Usage - Proper column configuration, kebab actions, and permission-based visibility
  2. Excellent Bulk Selection - Exemplary use of useBulkSelect and SelectAllCheckbox in both HostsTab and AddHostsModal
  3. Well-Designed InlineEdit Component - Reusable, accessible, with keyboard navigation
  4. Smart Hash-Based Tab Navigation - Enables deep linking to specific tabs
  5. Proper Permission Handling - Conditional action visibility based on user permissions
  6. Restricted Search Pattern - Smart filtering in HostsTab to scope results

❌ Critical Issues (Must Fix Before Merge)

  1. Custom Thunk Actions Instead of Foreman API Helpers (HostCollectionsActions.js)

    • Uses custom async thunks with manual try/catch blocks
    • Should use post(), del() from foremanReact/redux/API like HostCollectionDetailsActions.js does
    • Violates established Katello migration pattern
  2. 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
  3. Custom Selectors Accessing Wrong State Path (HostCollectionsSelectors.js)

    • Should use selectAPIResponse, selectAPIStatus, selectAPIError from Foreman
    • Current selectors access state[HOST_COLLECTIONS_KEY] which doesn't exist with API reducer
  4. Inconsistent Action Patterns

    • Details actions correctly use Foreman API helpers ✅
    • Main actions use custom thunks ❌
    • These should be consistent

⚠️ Minor Issues

  1. Full page reload after create instead of navigation to details page
  2. Consider adding organization_id to 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:

  1. Refactor HostCollectionsActions.js to use Foreman's post() and del() helpers
  2. Delete HostCollectionsReducer.js and remove from Redux registration
  3. Rewrite HostCollectionsSelectors.js to use Foreman's API selectors
  4. 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! 🚀

Copy link
Member

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.js already uses this pattern correctly - this file should match that approach

Copy link
Member

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:

  1. Delete this file
  2. Remove the import from webpack/redux/reducers/index.js
  3. Use API selectors with selectAPIResponse(state, key) instead

Copy link
Member

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 = (
Copy link
Member

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();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ MINOR: Full Page Reload Instead of Navigation

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.

@jeremylenz
Copy link
Member

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?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants