import React, { useState, useCallback } from 'react'; import useDeepCompareEffect from 'use-deep-compare-effect'; import { useDispatch, useSelector } from 'react-redux'; import { Bullseye, Split, SplitItem, Button, ActionList, ActionListItem, Dropdown, DropdownItem, KebabToggle, } from '@patternfly/react-core'; import { TableVariant, fitContent, TableText } from '@patternfly/react-table'; import { PencilAltIcon } from '@patternfly/react-icons'; import { STATUS } from 'foremanReact/constants'; import { translate as __ } from 'foremanReact/common/I18n'; import { urlBuilder } from 'foremanReact/common/urlHelpers'; import PropTypes from 'prop-types'; import TableWrapper from '../../../../components/Table/TableWrapper'; import onSelect from '../../../../components/Table/helpers'; import { selectCVComponents, selectCVComponentsStatus, selectCVComponentsError, selectCVComponentAddStatus, selectCVComponentRemoveStatus, } from '../ContentViewDetailSelectors'; import getContentViewDetails, { addComponent, getContentViewComponents, removeComponent, } from '../ContentViewDetailActions'; import AddedStatusLabel from '../../../../components/AddedStatusLabel'; import ComponentVersion from './ComponentVersion'; import ComponentEnvironments from './ComponentEnvironments'; import ContentViewIcon from '../../components/ContentViewIcon'; import { ADDED, ALL_STATUSES, NOT_ADDED } from '../../ContentViewsConstants'; import SelectableDropdown from '../../../../components/SelectableDropdown/SelectableDropdown'; import '../../../../components/EditableTextInput/editableTextInput.scss'; import ComponentContentViewAddModal from './ComponentContentViewAddModal'; import ComponentContentViewBulkAddModal from './ComponentContentViewBulkAddModal'; import { hasPermission } from '../../helpers'; import InactiveText from '../../components/InactiveText'; import { truncate } from '../../../../utils/helpers'; const ContentViewComponents = ({ cvId, details }) => { const response = useSelector(state => selectCVComponents(state, cvId)); const status = useSelector(state => selectCVComponentsStatus(state, cvId)); const error = useSelector(state => selectCVComponentsError(state, cvId)); const { results, ...metadata } = response; const componentAddedStatus = useSelector(state => selectCVComponentAddStatus(state, cvId)); const componentRemovedStatus = useSelector(state => selectCVComponentRemoveStatus(state, cvId)); const [rows, setRows] = useState([]); const [searchQuery, updateSearchQuery] = useState(''); const [statusSelected, setStatusSelected] = useState(ALL_STATUSES); const [versionEditing, setVersionEditing] = useState(false); const [compositeCvEditing, setCompositeCvEditing] = useState(null); const [componentCvEditing, setComponentCvEditing] = useState(null); const [componentLatest, setComponentLatest] = useState(false); const [componentId, setComponentId] = useState(null); const [componentVersionId, setComponentVersionId] = useState(null); const [selectedComponentsToAdd, setSelectedComponentsToAdd] = useState(null); const [bulkAdding, setBulkAdding] = useState(false); const [bulkActionOpen, setBulkActionOpen] = useState(false); const dispatch = useDispatch(); const resetFilters = () => setStatusSelected(ALL_STATUSES); const columnHeaders = [ { title: __('Type'), transforms: [fitContent] }, { title: __('Name') }, { title: __('Version') }, { title: __('Environments') }, { title: __('Repositories') }, { title: __('Status') }, { title: __('Description') }, ]; const loading = status === STATUS.PENDING; const addComponentsResolved = componentAddedStatus === STATUS.RESOLVED; const removeComponentsResolved = componentRemovedStatus === STATUS.RESOLVED; const { permissions } = details || {}; const bulkRemoveEnabled = () => rows.some(row => row.selected && row.added); const bulkAddEnabled = () => rows.some(row => row.selected && !row.added); const onAdd = useCallback(({ componentCvId, published, added, latest, }) => { if (published) { // If 1 or more versions present, open a modal to let user select version dispatch(getContentViewDetails(componentCvId, 'bulk_add')); setVersionEditing(true); setCompositeCvEditing(cvId); setComponentCvEditing(componentCvId); if (added) { setComponentVersionId(published?.id); } else { setComponentVersionId(null); } setComponentLatest(latest); setComponentId(added); } else { // if no versions are present, default to always latest and add cv without modal dispatch(addComponent({ compositeContentViewId: cvId, components: [{ latest: true, content_view_id: componentCvId }], }, () => dispatch(getContentViewDetails(cvId)))); } }, [cvId, dispatch]); const removeBulk = () => { const componentIds = []; rows.forEach(row => row.selected && componentIds.push(row.added)); dispatch(removeComponent({ compositeContentViewId: cvId, component_ids: componentIds, }, () => dispatch(getContentViewDetails(cvId)))); }; const addBulk = () => { const rowsToAdd = rows.filter(row => row.selected && !row.added); setSelectedComponentsToAdd(rowsToAdd); setCompositeCvEditing(cvId); setBulkAdding(true); }; const onRemove = (componentIdToRemove) => { dispatch(removeComponent({ compositeContentViewId: cvId, component_ids: [componentIdToRemove], }, () => dispatch(getContentViewDetails(cvId)))); }; const toggleBulkAction = () => { setBulkActionOpen(!bulkActionOpen); }; const buildRows = useCallback(() => { const newRows = []; results.forEach((componentCV) => { const { id: componentCvId, content_view: cv, content_view_version: cvVersion, latest, component_content_view_versions: componentCvVersions, } = componentCV; const { environments, repositories, id: cvVersionId } = cvVersion || {}; const { id, name, description, } = cv; const cells = [ { title: }, { title: {truncate(name)} }, { title: ( {hasPermission(permissions, 'edit_content_views') && componentCvId && cvVersion && } ), }, { title: environments ? : }, { title: cvVersionId ? {repositories ? repositories.length : 0} : 0, }, { title: , }, { title: description ? {description} : , }, ]; newRows.push({ componentCvId: id, componentCvName: name, added: componentCvId, componentCvVersions, published: cvVersion, latest, cells, }); }); return newRows; }, [onAdd, results, permissions]); const actionResolver = (rowData, { _rowIndex }) => [ { title: __('Add'), isDisabled: rowData.added, onClick: (_event, rowId, rowInfo) => { onAdd({ componentCvId: rowInfo.componentCvId, published: rowInfo.published, added: rowInfo.added, latest: rowInfo.latest, }); }, }, { title: __('Remove'), isDisabled: !rowData.added, onClick: (_event, rowId, rowInfo) => { onRemove(rowInfo.added); }, }, ]; const dropdownItems = [ {__('Remove')} , ]; const emptyContentTitle = __('No content views to add yet'); const emptyContentBody = __('Please create some content views.'); const emptySearchTitle = __('No matching content views found'); const emptySearchBody = __('Try changing your search settings.'); const activeFilters = [statusSelected]; const defaultFilters = [ALL_STATUSES]; useDeepCompareEffect(() => { if (!loading && results) { const newRows = buildRows(results); setRows(newRows); } }, [results, response, buildRows, loading]); return ( getContentViewComponents(cvId, params, statusSelected), [cvId, statusSelected])} additionalListeners={[statusSelected, addComponentsResolved, removeComponentsResolved]} actionButtons={hasPermission(permissions, 'edit_content_views') && status === STATUS.RESOLVED && rows.length !== 0 && <> {hasPermission(permissions, 'edit_content_views') && } isOpen={bulkActionOpen} isPlain ouiaId="cv-components-bulk-actions" dropdownItems={dropdownItems} /> } {versionEditing && } {bulkAdding && setBulkAdding(false)} aria-label="bulk_add_components_modal" />} } /> ); }; ContentViewComponents.propTypes = { cvId: PropTypes.number.isRequired, details: PropTypes.shape({ label: PropTypes.string, permissions: PropTypes.shape({}), }), }; ContentViewComponents.defaultProps = { details: { label: '', permissions: {}, }, }; export default ContentViewComponents;