webpack/scenes/ContentViews/Details/Repositories/ContentViewRepositories.js in katello-4.4.2.2 vs webpack/scenes/ContentViews/Details/Repositories/ContentViewRepositories.js in katello-4.5.0.rc1
- old
+ new
@@ -1,43 +1,71 @@
-import React, { useState, useEffect, useCallback } from 'react';
+import React, {
+ useCallback,
+ useEffect,
+ useState,
+} from 'react';
+
+import { translate as __ } from 'foremanReact/common/I18n';
+import { urlBuilder } from 'foremanReact/common/urlHelpers';
+import { STATUS } from 'foremanReact/constants';
+import {
+ lowerCase,
+ upperFirst,
+} from 'lodash';
+import PropTypes from 'prop-types';
+import {
+ shallowEqual,
+ useDispatch,
+ useSelector,
+} from 'react-redux';
import useDeepCompareEffect from 'use-deep-compare-effect';
-import { lowerCase, upperFirst } from 'lodash';
-import { useSelector, shallowEqual, useDispatch } from 'react-redux';
+
import {
- Bullseye,
- Split,
- SplitItem,
- Button,
ActionList,
ActionListItem,
+ Bullseye,
+ Button,
Dropdown,
DropdownItem,
KebabToggle,
+ Split,
+ SplitItem,
+ Checkbox,
} from '@patternfly/react-core';
-import { TableVariant, fitContent } from '@patternfly/react-table';
-import { STATUS } from 'foremanReact/constants';
-import { translate as __ } from 'foremanReact/common/I18n';
-import { urlBuilder } from 'foremanReact/common/urlHelpers';
-import PropTypes from 'prop-types';
-
+import {
+ TableVariant,
+ Thead,
+ Tbody,
+ Tr,
+ Th,
+ Td,
+} from '@patternfly/react-table';
+import AddedStatusLabel from '../../../../components/AddedStatusLabel';
+import SelectableDropdown from '../../../../components/SelectableDropdown';
import TableWrapper from '../../../../components/Table/TableWrapper';
-import onSelect from '../../../../components/Table/helpers';
-import { getContentViewRepositories, getRepositoryTypes, updateContentView } from '../ContentViewDetailActions';
import {
+ ADDED,
+ ALL_STATUSES,
+ NOT_ADDED,
+} from '../../ContentViewsConstants';
+import { hasPermission } from '../../helpers';
+import {
+ getContentViewRepositories,
+ getRepositoryTypes,
+ updateContentView,
+} from '../ContentViewDetailActions';
+import {
selectCVRepos,
- selectCVReposStatus,
selectCVReposError,
+ selectCVReposStatus,
selectRepoTypes,
selectRepoTypesStatus,
} from '../ContentViewDetailSelectors';
-import { ADDED, NOT_ADDED, ALL_STATUSES } from '../../ContentViewsConstants';
import ContentCounts from './ContentCounts';
import LastSync from './LastSync';
import RepoIcon from './RepoIcon';
-import AddedStatusLabel from '../../../../components/AddedStatusLabel';
-import SelectableDropdown from '../../../../components/SelectableDropdown';
-import { hasPermission } from '../../helpers';
+import { useSelectionSet } from '../../../../components/Table/TableHooks';
const allRepositories = 'All repositories';
// Add any exceptions to the display names here
// [API_value]: displayed_value
@@ -52,78 +80,50 @@
const { results, ...metadata } = response;
const status = useSelector(state => selectCVReposStatus(state, cvId), shallowEqual);
const error = useSelector(state => selectCVReposError(state, cvId), shallowEqual);
const repoTypesResponse = useSelector(state => selectRepoTypes(state), shallowEqual);
const repoTypesStatus = useSelector(state => selectRepoTypesStatus(state), shallowEqual);
- const { permissions } = details;
-
- const [rows, setRows] = useState([]);
- const deselectAll = () => setRows(rows.map(row => ({ ...row, selected: false })));
+ const { permissions, generated_for: generatedFor, import_only: importOnly } = details;
+ const generatedContentView = generatedFor !== 'none';
const [searchQuery, updateSearchQuery] = useState('');
const [typeSelected, setTypeSelected] = useState(allRepositories);
const [statusSelected, setStatusSelected] = useState(ALL_STATUSES);
// repoTypes object format: [displayed_value]: API_value
const [repoTypes, setRepoTypes] = useState({});
const [bulkActionOpen, setBulkActionOpen] = useState(false);
- const hasAddedSelected = rows.some(({ selected, added }) => selected && added);
- const hasNotAddedSelected = rows.some(({ selected, added }) => selected && !added);
+ const { repository_ids: repositoryIds = [] } = details;
+ const {
+ isSelected,
+ selectOne,
+ selectNone,
+ selectedCount,
+ selectedResults,
+ selectionSet,
+ isSelectable,
+ ...selectAll
+ } = useSelectionSet({
+ results,
+ metadata,
+ });
+
+ const hasAddedSelected = selectedResults.some(({ id }) => repositoryIds.includes(id));
+ const hasNotAddedSelected = selectedResults.some(({ id }) => !repositoryIds.includes(id));
+
const columnHeaders = [
- { title: __('Type'), transforms: [fitContent] },
+ __('Type'),
__('Name'),
__('Product'),
__('Sync state'),
__('Content'),
- { title: __('Status') },
+ __('Status'),
];
- const loading = status === STATUS.PENDING;
- const buildRows = useCallback(() => {
- const newRows = [];
- results.forEach((repo) => {
- const {
- id,
- content_type: contentType,
- name,
- added_to_content_view: addedToCV,
- product: { id: productId, name: productName },
- content_counts: counts,
- last_sync_words: lastSyncWords,
- last_sync: lastSync,
- } = repo;
-
- const cells = [
- { title: <Bullseye><RepoIcon type={contentType} /></Bullseye> },
- { title: <a href={urlBuilder(`products/${productId}/repositories`, '', id)}>{name}</a> },
- productName,
- { title: <LastSync {...{ startedAt: lastSync?.started_at, lastSyncWords, lastSync }} /> },
- { title: <ContentCounts {...{ counts, productId }} repoId={id} /> },
- {
- title: <AddedStatusLabel added={addedToCV || statusSelected === ADDED} />,
- },
- ];
- newRows.push({
- repoId: id,
- cells,
- added: addedToCV || statusSelected === ADDED,
- });
- });
- return newRows;
- }, [statusSelected, results]);
-
- useDeepCompareEffect(() => {
- if (!loading && results) {
- const newRows = buildRows(results);
- setRows(newRows);
- }
- }, [response, loading, buildRows, results]);
-
useEffect(() => {
dispatch(getRepositoryTypes());
}, []); // eslint-disable-line react-hooks/exhaustive-deps
-
// Get repo type filter selections dynamically from the API
useDeepCompareEffect(() => {
if (repoTypesStatus === STATUS.RESOLVED && repoTypesResponse) {
const allRepoTypes = {};
allRepoTypes[allRepositories] = 'all';
@@ -135,16 +135,16 @@
});
setRepoTypes(allRepoTypes);
}
}, [repoTypesResponse, repoTypesStatus]);
+
const toggleBulkAction = () => {
setBulkActionOpen(!bulkActionOpen);
};
const onAdd = (repos) => {
- const { repository_ids: repositoryIds = [] } = details;
dispatch(updateContentView(
cvId,
{ repository_ids: repositoryIds.concat(repos) },
() =>
dispatch(getContentViewRepositories(
@@ -157,11 +157,10 @@
));
};
const onRemove = (repos) => {
const reposToDelete = [].concat(repos);
- const { repository_ids: repositoryIds = [] } = details;
const deletedRepos = repositoryIds.filter(x => !reposToDelete.includes(x));
dispatch(updateContentView(
cvId, { repository_ids: deletedRepos },
() =>
dispatch(getContentViewRepositories(
@@ -174,53 +173,46 @@
));
};
const addBulk = () => {
setBulkActionOpen(false);
- const reposToAdd = rows.filter(({ selected, added }) =>
- selected && !added).map(({ repoId }) => repoId);
- deselectAll();
+ const reposToAdd = selectedResults.filter(selectedRepo =>
+ !repositoryIds.includes(selectedRepo.id)).map(({ id }) => id);
+ selectNone();
onAdd(reposToAdd);
};
const removeBulk = () => {
setBulkActionOpen(false);
- const reposToDelete = rows.filter(({ selected, added }) =>
- selected && added).map(({ repoId }) => repoId);
- deselectAll();
+ const reposToDelete = selectedResults.filter(selectedRepo =>
+ repositoryIds.includes(selectedRepo.id)).map(({ id }) => id);
+ selectNone();
onRemove(reposToDelete);
};
- const actionResolver = ({
- parent,
- compoundParent,
- noactions,
- added,
- }) => {
- if (parent || compoundParent || noactions) return null;
- return [
- {
- title: 'Add',
- isDisabled: added,
- onClick: (_event, _rowId, rowInfo) => {
- onAdd(rowInfo.repoId);
- },
+ const rowDropdownItems = ({ id }) => [
+ {
+ title: 'Add',
+ ouiaId: `add-repository-${id}`,
+ isDisabled: importOnly || generatedContentView || repositoryIds.includes(id),
+ onClick: () => {
+ onAdd(id);
},
- {
- title: 'Remove',
- isDisabled: !added,
- onClick: (_event, _rowId, rowInfo) => {
- onRemove(rowInfo.repoId);
- },
+ },
+ {
+ title: 'Remove',
+ ouiaId: `remove-repository-${id}`,
+ isDisabled: importOnly || generatedContentView || !repositoryIds.includes(id),
+ onClick: () => {
+ onRemove(id);
},
- ];
- };
+ },
+ ];
const getCVReposWithOptions = useCallback((params = {}) => {
const allParams = { ...params };
if (typeSelected !== 'All repositories') allParams.content_type = repoTypes[typeSelected];
-
return getContentViewRepositories(cvId, allParams, statusSelected);
}, [cvId, repoTypes, statusSelected, typeSelected]);
const emptyContentTitle = __("You currently don't have any repositories to add to this content view.");
const emptyContentBody = __('Please add some repositories.'); // needs link
@@ -228,19 +220,18 @@
const emptySearchBody = __('Try changing your search settings.');
const activeFilters = [typeSelected, statusSelected];
const defaultFilters = [allRepositories, ALL_STATUSES];
const dropdownItems = [
- <DropdownItem aria-label="bulk_remove" key="bulk_remove" isDisabled={!hasAddedSelected} component="button" onClick={removeBulk}>
+ <DropdownItem ouiaId="bulk-remove-repositories" aria-label="bulk_remove" key="bulk_remove" isDisabled={!hasAddedSelected} component="button" onClick={removeBulk}>
{__('Remove')}
</DropdownItem>,
];
return (
<TableWrapper
{...{
- rows,
metadata,
emptyContentTitle,
emptyContentBody,
emptySearchTitle,
emptySearchBody,
@@ -248,18 +239,20 @@
updateSearchQuery,
error,
status,
activeFilters,
defaultFilters,
+ selectedCount,
+ selectNone,
}}
- actionResolver={hasPermission(permissions, 'edit_content_views') ? actionResolver : null}
- onSelect={hasPermission(permissions, 'edit_content_views') ? onSelect(rows, setRows) : null}
- cells={columnHeaders}
+ ouiaId="content-view-repositories-table"
+ {...selectAll}
variant={TableVariant.compact}
autocompleteEndpoint="/repositories/auto_complete_search"
fetchItems={useCallback(params => getCVReposWithOptions(params), [getCVReposWithOptions])}
additionalListeners={[typeSelected, statusSelected]}
+ displaySelectAllCheckbox={hasPermission(permissions, 'edit_content_views')}
actionButtons={
<Split hasGutter>
<SplitItem>
<SelectableDropdown
items={Object.keys(repoTypes)}
@@ -282,16 +275,17 @@
</SplitItem>
{hasPermission(permissions, 'edit_content_views') &&
<SplitItem>
<ActionList>
<ActionListItem>
- <Button onClick={addBulk} isDisabled={!hasNotAddedSelected} variant="primary" aria-label="add_repositories">
+ <Button ouiaId="add-repositories" onClick={addBulk} isDisabled={!hasNotAddedSelected || importOnly || generatedContentView} variant="primary" aria-label="add_repositories">
{__('Add repositories')}
</Button>
</ActionListItem>
<ActionListItem>
<Dropdown
+ ouiaId="repositoies-bulk-actions"
toggle={<KebabToggle aria-label="bulk_actions" onToggle={toggleBulkAction} />}
isOpen={bulkActionOpen}
isPlain
dropdownItems={dropdownItems}
/>
@@ -299,18 +293,77 @@
</ActionList>
</SplitItem>
}
</Split>
}
- />
+ >
+ <Thead>
+ <Tr key="version-header">
+ {hasPermission(permissions, 'edit_content_views') && <Th key="select-all" />}
+ {columnHeaders.map((title, index) => {
+ if (index === 0) {
+ return <Th modifier="fitContent" key={`col-header-${title}`}>{title}</Th>;
+ }
+ return <Th key={`col-header-${title}`}>{title}</Th>;
+ })}
+ </Tr>
+ </Thead>
+ <Tbody>
+ {results?.map((repo) => {
+ const {
+ id,
+ content_type: contentType,
+ name,
+ added_to_content_view: addedToCV,
+ product: { id: productId, name: productName },
+ content_counts: counts,
+ last_sync_words: lastSyncWords,
+ last_sync: lastSync,
+ } = repo;
+ return (
+ <Tr key={id} ouiaId={`repositories-table-row-${productName}-${name}`}>
+ {hasPermission(permissions, 'edit_content_views') &&
+ <Td>
+ <Checkbox
+ id={id}
+ isChecked={isSelected(id)}
+ onChange={selected =>
+ selectOne(selected, id, repo)
+ }
+ />
+ </Td>
+ }
+ <Td><Bullseye><RepoIcon type={contentType} /></Bullseye></Td>
+ <Td>
+ <a href={urlBuilder(`products/${productId}/repositories`, '', id)}>{name}</a>
+ </Td>
+ <Td>{productName}</Td>
+ <Td>
+ <LastSync {...{ startedAt: lastSync?.started_at, lastSyncWords, lastSync }} />
+ </Td>
+ <Td><ContentCounts {...{ counts, productId }} repoId={id} /></Td>
+ <Td><AddedStatusLabel added={addedToCV || statusSelected === ADDED} /></Td>
+ {hasPermission(permissions, 'edit_content_views') &&
+ <Td
+ actions={{
+ items: rowDropdownItems(repo),
+ }}
+ />}
+ </Tr>
+ );
+ })}
+ </Tbody>
+ </TableWrapper>
);
};
ContentViewRepositories.propTypes = {
cvId: PropTypes.number.isRequired,
details: PropTypes.shape({
repository_ids: PropTypes.arrayOf(PropTypes.number),
permissions: PropTypes.shape({}),
+ import_only: PropTypes.bool,
+ generated_for: PropTypes.string,
}).isRequired,
};
export default ContentViewRepositories;