+
+
+
+
+ {__('Host Collections')}
+
+
+ {hostCollection?.name || '...'}
+
+
+
+
+
+
+
+ {hostCollection?.name}
+
+
+
+
+
+
+
+
+
+ setIsActionsOpen(false)}
+ isOpen={isActionsOpen}
+ toggle={
+ setIsActionsOpen(isOpen)}
+ toggleIndicator={CaretDownIcon}
+ ouiaId="host-collection-actions-dropdown"
+ >
+ {__('Actions')}
+
+ }
+ dropdownItems={[
+ {
+ setIsCopyModalOpen(true);
+ setIsActionsOpen(false);
+ }}
+ ouiaId="copy-action"
+ >
+ {__('Copy')}
+ ,
+ ,
+ {
+ setIsDeleteModalOpen(true);
+ setIsActionsOpen(false);
+ }}
+ ouiaId="delete-action"
+ >
+ {__('Delete')}
+ ,
+ ]}
+ ouiaId="host-collection-actions"
+ />
+
+
+
+
+
+ {hostCollection?.description ?
+
+
+ {hostCollection.description}
+
+ :
+
+ }
+
+
+
+
+
+ {__('Details')}}
+ ouiaId="details-tab"
+ >
+
+
+ {__('Hosts')}}
+ ouiaId="hosts-tab"
+ >
+
+
+
+
+
+ {/* Modals */}
+ {hostCollection?.name && (
+ <>
+
setIsCopyModalOpen(false)}
+ hostCollection={{ id: parseInt(id, 10), name: hostCollection.name }}
+ />
+ setIsDeleteModalOpen(false)}
+ hostCollection={{ id: parseInt(id, 10), name: hostCollection.name }}
+ />
+ >
+ )}
+
+ );
+};
+
+export default HostCollectionDetails;
diff --git a/webpack/scenes/HostCollections/Details/HostCollectionDetails.scss b/webpack/scenes/HostCollections/Details/HostCollectionDetails.scss
new file mode 100644
index 00000000000..4ee72f4b33b
--- /dev/null
+++ b/webpack/scenes/HostCollections/Details/HostCollectionDetails.scss
@@ -0,0 +1,24 @@
+#host-collection-details {
+ .host-collection-details-header {
+ padding: 1.5rem;
+ margin-bottom: 1rem;
+
+ .breadcrumb-bar-pf4 {
+ margin-bottom: 1rem;
+ }
+
+ h2 {
+ font-size: 1.5rem;
+ font-weight: 600;
+ margin: 0;
+ }
+
+ .host-collection-description {
+ margin-top: 1rem;
+ }
+ }
+
+ .host-collection-tabs-section {
+ padding-top: 0;
+ }
+}
diff --git a/webpack/scenes/HostCollections/Details/HostCollectionDetailsActions.js b/webpack/scenes/HostCollections/Details/HostCollectionDetailsActions.js
new file mode 100644
index 00000000000..ec1e8b79311
--- /dev/null
+++ b/webpack/scenes/HostCollections/Details/HostCollectionDetailsActions.js
@@ -0,0 +1,40 @@
+import { translate as __ } from 'foremanReact/common/I18n';
+import { get, put, API_OPERATIONS } from 'foremanReact/redux/API';
+import katelloApi from '../../../services/api/index';
+
+export const getHostCollection = id => get({
+ type: API_OPERATIONS.GET,
+ key: `HOST_COLLECTION_DETAILS_${id}`,
+ url: katelloApi.getApiUrl(`/host_collections/${id}`),
+});
+
+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 || __('Failed to update host collection'),
+ handleSuccess,
+});
+
+export const addHostsToCollection = (id, hostIds, handleSuccess, handleError) => put({
+ type: API_OPERATIONS.PUT,
+ key: `ADD_HOSTS_TO_COLLECTION_${id}`,
+ url: katelloApi.getApiUrl(`/host_collections/${id}/add_hosts`),
+ params: { host_ids: hostIds },
+ successToast: () => __('Hosts added successfully'),
+ errorToast: error => error.response?.data?.error?.message || __('Failed to add hosts'),
+ handleSuccess,
+ handleError,
+});
+
+export const removeHostsFromCollection = (id, hostIds, handleSuccess) => put({
+ type: API_OPERATIONS.PUT,
+ key: `REMOVE_HOSTS_FROM_COLLECTION_${id}`,
+ url: katelloApi.getApiUrl(`/host_collections/${id}/remove_hosts`),
+ params: { host_ids: hostIds },
+ successToast: () => __('Hosts removed successfully'),
+ errorToast: error => error.response?.data?.error?.message || __('Failed to remove hosts'),
+ handleSuccess,
+});
diff --git a/webpack/scenes/HostCollections/Details/HostsTab/AddHostsModal.js b/webpack/scenes/HostCollections/Details/HostsTab/AddHostsModal.js
new file mode 100644
index 00000000000..b007e808a47
--- /dev/null
+++ b/webpack/scenes/HostCollections/Details/HostsTab/AddHostsModal.js
@@ -0,0 +1,173 @@
+import React, { useState } from 'react';
+import { useDispatch, useSelector } from 'react-redux';
+import PropTypes from 'prop-types';
+import { translate as __ } from 'foremanReact/common/I18n';
+import { urlBuilder } from 'foremanReact/common/urlHelpers';
+import { selectAPIResponse } from 'foremanReact/redux/API/APISelectors';
+import { useBulkSelect } from 'foremanReact/components/PF4/TableIndexPage/Table/TableHooks';
+import {
+ Modal,
+ ModalVariant,
+ Button,
+ ToolbarItem,
+} from '@patternfly/react-core';
+import TableIndexPage from 'foremanReact/components/PF4/TableIndexPage/TableIndexPage';
+import SelectAllCheckbox from 'foremanReact/components/PF4/TableIndexPage/Table/SelectAllCheckbox';
+import { addHostsToCollection } from '../HostCollectionDetailsActions';
+
+const AddHostsModal = ({
+ isOpen, onClose, hostCollectionId, onHostsAdded,
+}) => {
+ const dispatch = useDispatch();
+ const [isAdding, setIsAdding] = useState(false);
+
+ const response = useSelector(state =>
+ selectAPIResponse(state, `HOST_COLLECTION_${hostCollectionId}_AVAILABLE_HOSTS`));
+ const { results = [], total = 0, per_page: perPage = 20, ...metadata } = response;
+
+ const {
+ selectOne,
+ isSelected,
+ selectedResults,
+ selectNone,
+ selectAll,
+ selectPage,
+ selectedCount,
+ areAllRowsOnPageSelected,
+ areAllRowsSelected,
+ } = useBulkSelect({
+ results,
+ metadata: { ...metadata, total, page: perPage },
+ });
+
+ const handleAddHosts = () => {
+ if (selectedCount === 0) return;
+
+ setIsAdding(true);
+ // Get selected host IDs - works for both individual selections and select-all mode
+ const hostIds = selectedResults.length > 0
+ ? selectedResults.map(host => host.id)
+ : results.filter(host => isSelected(host.id)).map(host => host.id);
+
+ dispatch(addHostsToCollection(
+ hostCollectionId,
+ hostIds,
+ () => {
+ setIsAdding(false);
+ selectNone();
+ if (onHostsAdded) onHostsAdded();
+ },
+ () => {
+ setIsAdding(false);
+ },
+ ));
+ };
+
+ const columns = {
+ name: {
+ title: __('Name'),
+ wrapper: ({ id, name }) => (
+