webpack/scenes/ContentViews/Details/Filters/CVModuleStreamFilterContent.js
import React, { useState, useEffect, useCallback } from 'react';
import useDeepCompareEffect from 'use-deep-compare-effect';
import PropTypes from 'prop-types';
import { shallowEqual, useSelector, useDispatch } from 'react-redux';
import { omit } from 'lodash';
import { TableVariant } from '@patternfly/react-table';
import {
Tabs, Tab, TabTitleText, Split, SplitItem, Button, Dropdown, DropdownItem,
KebabToggle, Select, SelectOption, SelectVariant,
} from '@patternfly/react-core';
import { STATUS } from 'foremanReact/constants';
import { translate as __ } from 'foremanReact/common/I18n';
import onSelect from '../../../../components/Table/helpers';
import TableWrapper from '../../../../components/Table/TableWrapper';
import {
selectCVFilterModuleStream,
selectCVFilterModuleStreamStatus,
selectCVFilterModuleStreamError,
selectCVFilters, selectCVFilterDetails, selectCVFiltersStatus,
} from '../ContentViewDetailSelectors';
import getContentViewDetails, {
addCVFilterRule, removeCVFilterRule, getCVFilterModuleStreams,
deleteContentViewFilterRules, addContentViewFilterRules,
} from '../ContentViewDetailActions';
import AddedStatusLabel from '../../../../components/AddedStatusLabel';
import AffectedRepositoryTable from './AffectedRepositories/AffectedRepositoryTable';
import { ADDED, ALL_STATUSES, NOT_ADDED } from '../../ContentViewsConstants';
import { hasPermission } from '../../helpers';
const CVModuleStreamFilterContent = ({
cvId, filterId, showAffectedRepos, setShowAffectedRepos, details,
}) => {
const dispatch = useDispatch();
const { results: filterResults } =
useSelector(state => selectCVFilters(state, cvId), shallowEqual);
const response = useSelector(state =>
selectCVFilterModuleStream(state, cvId, filterId), shallowEqual);
const status = useSelector(state =>
selectCVFilterModuleStreamStatus(state, cvId, filterId), shallowEqual);
const filterLoad = useSelector(state =>
selectCVFiltersStatus(state, cvId), shallowEqual);
const error = useSelector(state =>
selectCVFilterModuleStreamError(state, cvId, filterId), shallowEqual);
const filterDetails = useSelector(state =>
selectCVFilterDetails(state, cvId, filterId), shallowEqual);
const { repositories = [] } = filterDetails;
const [rows, setRows] = useState([]);
const [searchQuery, updateSearchQuery] = useState('');
const [activeTabKey, setActiveTabKey] = useState(0);
const filterLoaded = filterLoad === 'RESOLVED';
const loading = status === STATUS.PENDING;
const [bulkActionOpen, setBulkActionOpen] = useState(false);
const [selectOpen, setSelectOpen] = useState(false);
const [selectedIndex, setSelectedIndex] = useState(0);
const deselectAll = () => setRows(rows.map(row => ({ ...row, selected: false })));
const toggleBulkAction = () => setBulkActionOpen(prevState => !prevState);
const hasAddedSelected = rows.some(({ selected, added }) => selected && added);
const hasNotAddedSelected = rows.some(({ selected, added }) => selected && !added);
const metadata = omit(response, ['results']);
const { permissions } = details;
const columnHeaders = [
__('Name'),
__('Stream'),
__('Version'),
__('Context'),
__('Status'),
];
const allAddedNotAdded = [
ALL_STATUSES,
ADDED,
NOT_ADDED,
];
const selectedAdded = allAddedNotAdded[selectedIndex];
const resetFilters = () => setSelectedIndex(0);
const fetchItems = useCallback((params) => {
const adjustedParams = { ...params };
switch (selectedIndex) {
case 0:
adjustedParams.show_all_for = 'content_view_filter';
adjustedParams.available_for = undefined;
break;
case 1:
adjustedParams.show_all_for = undefined;
adjustedParams.available_for = undefined;
break;
case 2:
adjustedParams.show_all_for = undefined;
adjustedParams.available_for = 'content_view_filter';
break;
default:
}
return getCVFilterModuleStreams(cvId, filterId, adjustedParams);
}, [cvId, filterId, selectedIndex]);
const buildRows = useCallback((results) => {
const newRows = [];
const filterRules = filterResults.find(({ id }) => id === Number(filterId))?.rules || [];
results.forEach((moduleStreams) => {
const {
id,
name,
stream,
version,
context,
filter_ids: filterIds,
...rest
} = moduleStreams;
const added = filterIds.includes(parseInt(filterId, 10));
const cells = [
{ title: name },
{ title: stream },
{ title: version },
{ title: context },
{ title: <AddedStatusLabel added={added} /> },
];
/* eslint-disable camelcase */
newRows.push({
cells,
module_stream_id: id,
moduleStreamRuleId: filterRules?.find(({ module_stream_id }) => module_stream_id === id)?.id, // eslint-disable-line max-len, no-shadow, no-self-compare
added,
...rest,
name,
});
});
return newRows.sort(({ added: addedA }, { added: addedB }) => {
if (addedA === addedB) return 0;
return addedA ? -1 : 1;
});
}, [filterResults, filterId]);
const bulkAdd = () => {
setBulkActionOpen(false);
const addData = rows.filter(({ selected, added }) =>
selected && !added).map(({ module_stream_id }) => ({ module_stream_ids: [module_stream_id] })); // eslint-disable-line max-len
dispatch(addContentViewFilterRules(filterId, addData, () =>
dispatch(getContentViewDetails(cvId))));
deselectAll();
};
const bulkRemove = () => {
setBulkActionOpen(false);
const moduleStreamRuleIds =
rows.filter(({ selected, added }) =>
selected && added).map(({ moduleStreamRuleId }) => moduleStreamRuleId);
dispatch(deleteContentViewFilterRules(filterId, moduleStreamRuleIds, () =>
dispatch(getContentViewDetails(cvId))));
deselectAll();
};
useEffect(() => {
if (!repositories.length && showAffectedRepos) {
setActiveTabKey(1);
} else {
setActiveTabKey(0);
}
}, [showAffectedRepos, repositories.length]);
useDeepCompareEffect(() => {
const { results } = response;
if (!loading && results && filterLoaded) {
const newRows = buildRows(results);
setRows(newRows);
}
}, [response, loading, filterLoaded, buildRows]);
const actionResolver = ({ added }) => [
{
title: __('Add'),
isDisabled: added,
onClick: (_event, _rowId, { module_stream_id }) => {
dispatch(addCVFilterRule(filterId, { module_stream_ids: [module_stream_id] }, () =>
dispatch(getContentViewDetails(cvId))));
},
},
{
title: __('Remove'),
isDisabled: !added,
onClick: (_event, _rowId, { moduleStreamRuleId }) => {
dispatch(removeCVFilterRule(filterId, moduleStreamRuleId, () =>
dispatch(getContentViewDetails(cvId))));
},
},
];
const emptyContentTitle = __('No module stream filter rules yet');
const emptyContentBody = __('No module streams to add yet.');
const emptySearchTitle = __('No matching filter rules found.');
const emptySearchBody = __('Try changing your search settings.');
return (
<Tabs
className="margin-0-24"
ouiaId="module-stream-filter-content-tabs"
activeKey={activeTabKey}
onSelect={(_event, eventKey) => setActiveTabKey(eventKey)}
>
<Tab
ouiaId="module-stream-filter-content-table-tab"
eventKey={0}
title={<TabTitleText>{__('Module Streams')}</TabTitleText>}
>
<div className="margin-24-0">
<TableWrapper
{...{
rows,
metadata,
emptyContentTitle,
emptyContentBody,
emptySearchTitle,
emptySearchBody,
searchQuery,
updateSearchQuery,
error,
status,
resetFilters,
}}
ouiaId="content-view-module-stream-filter-table"
additionalListeners={[selectedIndex]}
activeFilters={[selectedAdded]}
defaultFilters={[allAddedNotAdded[0]]}
cells={columnHeaders}
variant={TableVariant.compact}
autocompleteEndpoint="/katello/api/v2/module_streams"
autocompleteQueryParams={{ filterid: filterId }}
bookmarkController="katello_content_view_module_stream_filter_rules"
fetchItems={fetchItems}
actionResolver={hasPermission(permissions, 'edit_content_views') ? actionResolver : null}
onSelect={hasPermission(permissions, 'edit_content_views') ? onSelect(rows, setRows) : null}
actionButtons={hasPermission(permissions, 'edit_content_views') &&
status === STATUS.RESOLVED && rows.length !== 0 &&
<Split hasGutter>
<SplitItem data-testid="allAddedNotAdded">
<Select
variant={SelectVariant.single}
onToggle={setSelectOpen}
ouiaId="added-notAdded-selector"
onSelect={(_event, selection) => {
setSelectedIndex(allAddedNotAdded.indexOf(selection));
setSelectOpen(false);
}}
selections={selectedAdded}
isOpen={selectOpen}
isCheckboxSelectionBadgeHidden
>
{allAddedNotAdded.map(item =>
<SelectOption aria-label={item} key={item} value={item} />)}
</Select>
</SplitItem>
<SplitItem>
<Button
ouiaId="add-module-stream-rule-button"
isDisabled={!hasNotAddedSelected}
onClick={bulkAdd}
variant="primary"
aria-label="add_filter_rule"
>
{__('Add filter rule')}
</Button>
</SplitItem>
<SplitItem>
<Dropdown
toggle={<KebabToggle aria-label="bulk_actions" onToggle={toggleBulkAction} />}
isOpen={bulkActionOpen}
ouiaId="bulk-actions-dropdown"
isPlain
dropdownItems={[
<DropdownItem ouiaId="bulk-remove" aria-label="bulk_remove" key="bulk_remove" isDisabled={!hasAddedSelected} component="button" onClick={bulkRemove}>
{__('Remove')}
</DropdownItem>]
}
/>
</SplitItem>
</Split>
}
/>
</div>
</Tab>
{(repositories.length || showAffectedRepos) &&
<Tab
ouiaId="affected-repos-tab"
eventKey={1}
title={<TabTitleText>{__('Affected repositories')}</TabTitleText>}
>
<div className="margin-24-0">
<AffectedRepositoryTable cvId={cvId} filterId={filterId} repoType="yum" setShowAffectedRepos={setShowAffectedRepos} details={details} />
</div>
</Tab>
}
</Tabs >
);
};
CVModuleStreamFilterContent.propTypes = {
cvId: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
filterId: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
showAffectedRepos: PropTypes.bool.isRequired,
setShowAffectedRepos: PropTypes.func.isRequired,
details: PropTypes.shape({
permissions: PropTypes.shape({}),
}).isRequired,
};
export default CVModuleStreamFilterContent;