webpack/scenes/AlternateContentSources/MainTable/ACSTable.js in katello-4.6.2.1 vs webpack/scenes/AlternateContentSources/MainTable/ACSTable.js in katello-4.7.0.rc1

- old
+ new

@@ -1,146 +1,241 @@ import React, { useCallback, useEffect, useRef, useState } from 'react'; import { useHistory, useLocation } from 'react-router-dom'; import { useDispatch, useSelector } from 'react-redux'; +import { capitalize, upperCase, omit } from 'lodash'; import { translate as __ } from 'foremanReact/common/I18n'; import { STATUS } from 'foremanReact/constants'; import { Button, + Checkbox, Drawer, DrawerActions, DrawerCloseButton, DrawerContent, DrawerContentBody, DrawerHead, DrawerPanelContent, + DrawerPanelBody, + Dropdown, + DropdownItem, + KebabToggle, Text, TextContent, TextList, TextListItem, TextListItemVariants, TextListVariants, TextVariants, } from '@patternfly/react-core'; -import { TableVariant, Tbody, Td, Th, Thead, Tr } from '@patternfly/react-table'; +import { TableVariant, Tbody, Td, Th, Thead, Tr, ActionsColumn } from '@patternfly/react-table'; import TableWrapper from '../../../components/Table/TableWrapper'; import { selectAlternateContentSources, selectAlternateContentSourcesError, selectAlternateContentSourcesStatus, } from '../ACSSelectors'; -import { useTableSort } from '../../../components/Table/TableHooks'; -import getAlternateContentSources, { deleteACS, getACSDetails, refreshACS } from '../ACSActions'; +import { useSelectionSet, useTableSort } from '../../../components/Table/TableHooks'; +import getAlternateContentSources, { deleteACS, bulkDeleteACS, getACSDetails, refreshACS, bulkRefreshACS } from '../ACSActions'; import ACSCreateWizard from '../Create/ACSCreateWizard'; import LastSync from '../../ContentViews/Details/Repositories/LastSync'; import ACSExpandableDetails from '../Details/ACSExpandableDetails'; import './ACSTable.scss'; +import Loading from '../../../components/Loading'; +import EmptyStateMessage from '../../../components/Table/EmptyStateMessage'; +import { hasPermission } from '../../ContentViews/helpers'; const ACSTable = () => { const response = useSelector(selectAlternateContentSources); const status = useSelector(selectAlternateContentSourcesStatus); const error = useSelector(selectAlternateContentSourcesError); const resolved = status === STATUS.RESOLVED; const [searchQuery, updateSearchQuery] = useState(''); const [isCreateWizardOpen, setIsCreateWizardOpen] = useState(false); const dispatch = useDispatch(); - const { results, ...metadata } = response; + const metadata = omit(response, ['results']); + const { + can_create: canCreate = false, + can_edit: canEdit = false, + can_delete: canDelete = false, + can_view: canView = false, + results, + } = response; const { pathname } = useLocation(); const { push } = useHistory(); - const acsId = pathname.split('/')[3]; + const [acsId, setAcsId] = useState(pathname.split('/')[2]); const [expandedId, setExpandedId] = useState(acsId); const [isExpanded, setIsExpanded] = useState(false); const drawerRef = useRef(null); + const [kebabOpen, setKebabOpen] = useState(false); + const [detailsKebabOpen, setDetailsKebabOpen] = useState(false); + const [deleting, setDeleting] = useState(false); + const renderActionButtons = status === STATUS.RESOLVED && !!results?.length; + const { + selectOne, isSelected, isSelectable: _isSelectable, + selectedCount, selectionSet, ...selectionSetVars + } = useSelectionSet({ + results, + metadata, + }); useEffect(() => { if (acsId) { dispatch(getACSDetails(acsId)); + setExpandedId(acsId); setIsExpanded(true); } }, [dispatch, acsId]); - const onExpand = () => { - if (drawerRef.current) drawerRef.current.focus(); - }; + const onExpand = () => drawerRef.current && drawerRef.current.focus(); const onDelete = (id) => { - dispatch(deleteACS(id, () => - dispatch(getAlternateContentSources()))); + setDeleting(true); + dispatch(deleteACS(id, () => { + setDeleting(false); + if (id?.toString() === acsId?.toString()) { + push('/alternate_content_sources'); + } else { + dispatch(getAlternateContentSources()); + } + })); }; const onRefresh = (id) => { dispatch(refreshACS(id, () => dispatch(getAlternateContentSources()))); }; - const createButtonOnclick = () => { - setIsCreateWizardOpen(true); + const onBulkDelete = (ids) => { + setDeleting(true); + dispatch(bulkDeleteACS({ ids }, () => { + if (acsId && ids.has(Number(acsId))) { + push('/alternate_content_sources'); + } else { + dispatch(getAlternateContentSources()); + } + })); }; - const onClick = (id) => { - setExpandedId(id); + const onBulkRefresh = (ids) => { + dispatch(bulkRefreshACS({ ids }, () => + dispatch(getAlternateContentSources()))); }; + const createButtonOnclick = () => { + setIsCreateWizardOpen(true); + }; + const onCloseClick = () => { setExpandedId(null); - push('/labs/alternate_content_sources'); + setAcsId(null); + window.history.replaceState(null, '', '/alternate_content_sources'); setIsExpanded(false); }; + const onClick = (id) => { + if (Number(id) === Number(expandedId)) { + onCloseClick(); + } else { + setExpandedId(id); + setAcsId(id); + window.history.replaceState(null, '', `/alternate_content_sources/${id}/details`); + setIsExpanded(true); + } + }; + + const isSingleSelected = rowId => (Number(rowId) === Number(acsId) || + Number(rowId) === Number(expandedId)); + const customStyle = { + borderLeft: '5px solid var(--pf-global--primary-color--100)', + }; + const PanelContent = () => { if (!resolved) return <></>; const acs = results?.find(source => source?.id === Number(expandedId)); - const { last_refresh: lastTask } = acs ?? {}; + if (!acs && isExpanded) { + setExpandedId(null); + setIsExpanded(false); + } + const { last_refresh: lastTask, permissions } = acs ?? {}; const { last_refresh_words: lastRefreshWords, started_at: startedAt } = lastTask ?? {}; return ( - <DrawerPanelContent defaultSize="50%"> + <DrawerPanelContent defaultSize="35%"> <DrawerHead> {results && isExpanded && - <span ref={drawerRef}> - <TextContent> - <Text component={TextVariants.h1}> - {acs?.name} - </Text> - <TextList component={TextListVariants.dl}> - <TextListItem component={TextListItemVariants.dt}> - {__('Last refresh :')} - </TextListItem> - <TextListItem - aria-label="name_text_value" - component={TextListItemVariants.dd} - > - <LastSync - startedAt={startedAt} - lastSync={lastTask} - lastSyncWords={lastRefreshWords} - emptyMessage="N/A" - /> - </TextListItem> - </TextList> - </TextContent> - <ACSExpandableDetails /> - </span>} + <div ref={drawerRef}> + <Text component={TextVariants.h1} style={{ marginTop: '0px', fontWeight: 'bold' }}> + {acs?.name} + </Text> + <TextContent> + <TextList style={{ marginBottom: '0px' }} component={TextListVariants.dl}> + <TextListItem component={TextListItemVariants.dt} style={{ fontWeight: 'normal' }}> + {__('Last refresh :')} + </TextListItem> + <TextListItem + aria-label="name_text_value" + component={TextListItemVariants.dd} + > + <LastSync + startedAt={startedAt} + lastSync={lastTask} + lastSyncWords={lastRefreshWords} + emptyMessage="N/A" + /> + </TextListItem> + </TextList> + </TextContent> + </div> + } + {error && <EmptyStateMessage error={error} />} <DrawerActions> - <Button - ouiaId="refresh-acs" - onClick={() => onRefresh(acs?.id)} - variant="secondary" - isSmall - aria-label="refresh_acs" - > - {__('Refresh source')} - </Button> + {hasPermission(permissions, 'edit_alternate_content_sources') && + <> + <Button + ouiaId="refresh-acs" + onClick={() => onRefresh(acs?.id)} + variant="secondary" + isSmall + aria-label="refresh_acs" + > + {__('Refresh source')} + </Button> + <Dropdown + style={{ paddingRight: '0px' }} + toggle={<KebabToggle aria-label="details_actions" onToggle={setDetailsKebabOpen} style={{ paddingRight: '0px' }} />} + isOpen={detailsKebabOpen} + ouiaId="acs-details-actions" + isPlain + dropdownItems={[ + <DropdownItem + aria-label="details_delete" + ouiaId="details_delete" + key="details_delete" + component="button" + onClick={() => { + setDetailsKebabOpen(false); + onDelete(acs?.id); + }} + > + {__('Delete')} + </DropdownItem>]} + /> + </> + } <DrawerCloseButton onClick={onCloseClick} /> </DrawerActions> </DrawerHead> + <DrawerPanelBody> + <ACSExpandableDetails {...{ expandedId }} /> + </DrawerPanelBody> </DrawerPanelContent> ); }; const columnHeaders = [ __('Name'), __('Type'), - __('Last Refresh'), + __('Last refresh'), ]; const COLUMNS_TO_SORT_PARAMS = { [columnHeaders[0]]: 'name', [columnHeaders[1]]: 'alternate_content_source_type', @@ -162,127 +257,205 @@ ...params, }), [apiSortParams], ); - const rowDropdownItems = ({ id }) => [ - { + const actionsWithPermissions = (acs) => { + const { id, permissions } = acs; + const deleteAction = { title: __('Delete'), ouiaId: `remove-acs-${id}`, onClick: () => { onDelete(id); }, - }, - { + }; + const refreshAction = { title: __('Refresh'), - ouiaId: `remove-acs-${id}`, + ouiaId: `refresh-acs-${id}`, onClick: () => { onRefresh(id); }, - }, - ]; + }; + return [ + ...(hasPermission(permissions, 'destroy_alternate_content_sources') ? [deleteAction] : []), + ...(hasPermission(permissions, 'edit_alternate_content_sources') ? [refreshAction] : []), + ]; + }; + const primaryActionButton = ( + <Button + ouiaId="create-acs" + onClick={createButtonOnclick} + variant="primary" + aria-label="create_acs" + > + {__('Add source')} + </Button> + ); + const emptyContentTitle = __("You currently don't have any alternate content sources."); - const emptyContentBody = __('An alternate content source can be added by using the "Add source" button above.'); + const emptyContentBody = canCreate ? __('An alternate content source can be added by using the "Add source" button below.') : ''; const emptySearchTitle = __('No matching alternate content sources found'); const emptySearchBody = __('Try changing your search settings.'); + const showPrimaryAction = canCreate; /* eslint-disable react/no-array-index-key */ - + if (deleting) { + return <Loading loadingText={__('Please wait...')} />; + } return ( - <Drawer isExpanded={isExpanded} isInline onExpand={onExpand}> - <DrawerContent panelContent={<PanelContent />}> - <DrawerContentBody style={{ paddingBottom: '5%' }}> - <TableWrapper - {...{ - metadata, - emptyContentTitle, - emptyContentBody, - emptySearchTitle, - emptySearchBody, - searchQuery, - updateSearchQuery, - error, - status, - fetchItems, - }} - ouiaId="alternate-content-sources-table" - variant={TableVariant.compact} - additionalListeners={[activeSortColumn, activeSortDirection]} - autocompleteEndpoint="/alternate_content_sources/auto_complete_search" - actionButtons={ - <> - <Button - ouiaId="create-acs" - onClick={createButtonOnclick} - variant="primary" - aria-label="create_acs" - > - {__('Add source')} - </Button> - {isCreateWizardOpen && - <ACSCreateWizard - show={isCreateWizardOpen} - setIsOpen={setIsCreateWizardOpen} - /> - } - </> + <div className="primary-detail-border"> + <Drawer isExpanded={isExpanded} onExpand={onExpand} style={{ minHeight: '80vH' }}> + <DrawerContent panelContent={<PanelContent />} style={{ minHeight: '80vH' }}> + <DrawerContentBody> + <TableWrapper + {...{ + metadata, + emptyContentTitle, + emptyContentBody, + emptySearchTitle, + emptySearchBody, + searchQuery, + updateSearchQuery, + error, + status, + fetchItems, + showPrimaryAction, + primaryActionButton, + }} + ouiaId="alternate-content-sources-table" + variant={TableVariant.compact} + additionalListeners={[activeSortColumn, activeSortDirection]} + autocompleteEndpoint="/alternate_content_sources/auto_complete_search" + {...selectionSetVars} + actionButtons={ + <> + {renderActionButtons && canCreate && + <Button + ouiaId="create-acs" + onClick={createButtonOnclick} + variant="primary" + aria-label="create_acs" + > + {__('Add source')} + </Button>} + {renderActionButtons && (canEdit || canDelete) && + <Dropdown + toggle={<KebabToggle aria-label="bulk_actions" onToggle={setKebabOpen} />} + isOpen={kebabOpen} + ouiaId="acs-bulk-actions" + isPlain + dropdownItems={[ + <DropdownItem + aria-label="bulk_refresh" + ouiaId="bulk_refresh" + key="bulk_refresh" + isDisabled={selectedCount < 1 || !canEdit} + component="button" + onClick={() => { + setKebabOpen(false); + onBulkRefresh(selectionSet); + }} + > + {__('Refresh')} + </DropdownItem>, + <DropdownItem + aria-label="bulk_delete" + ouiaId="bulk_delete" + key="bulk_delete" + isDisabled={selectedCount < 1 || !canDelete} + component="button" + onClick={() => { + setKebabOpen(false); + onBulkDelete(selectionSet); + }} + > + {__('Delete')} + </DropdownItem>, + ]} + />} + {isCreateWizardOpen && + <ACSCreateWizard + show={isCreateWizardOpen} + setIsOpen={setIsCreateWizardOpen} + /> } - > - <Thead> - <Tr> - {columnHeaders.map(col => ( + </> + } + displaySelectAllCheckbox={renderActionButtons} + hideSearch={!canView} + > + <Thead> + <Tr> <Th - key={col} - sort={COLUMNS_TO_SORT_PARAMS[col] ? pfSortParams(col) : undefined} - > - {col} - </Th> - ))} - </Tr> - </Thead> - <Tbody> - {results?.map((acs, index) => { - const { - name, - id, - alternate_content_source_type: acsType, - last_refresh: lastTask, - } = acs; - const { - last_refresh_words: lastRefreshWords, - started_at: startedAt, - } = lastTask ?? {}; - return ( - <Tr key={index}> - <Td onClick={() => { - onClick(id); - push(`/labs/alternate_content_sources/${id}/details`); - }} + key="acs-checkbox" + style={{ width: 0 }} + /> + {columnHeaders.map(col => ( + <Th + key={col} + sort={COLUMNS_TO_SORT_PARAMS[col] ? pfSortParams(col) : undefined} > - <Text component="a">{name}</Text> - </Td> - <Td>{acsType}</Td> - <Td><LastSync - startedAt={startedAt} - lastSync={lastTask} - lastSyncWords={lastRefreshWords} - emptyMessage="N/A" - /> - </Td> - <Td - actions={{ - items: rowDropdownItems(acs), - }} - /> - </Tr> - ); - }) + {col} + </Th> + ))} + </Tr> + </Thead> + <Tbody> + {results?.map((acs, index) => { + const { + name, + id, + alternate_content_source_type: acsType, + last_refresh: lastTask, + permissions, + } = acs; + const { + last_refresh_words: lastRefreshWords, + started_at: startedAt, + } = lastTask ?? {}; + return ( + <Tr + key={index} + style={isSingleSelected(id) && isExpanded ? customStyle : {}} + isStriped={isSingleSelected(id) && isExpanded} + > + <Td> + <Checkbox + ouiaId={`select-acs-${id}`} + id={id} + aria-label={`Select ACS ${id}`} + isChecked={isSelected(id)} + onChange={selected => selectOne(selected, id)} + /> + </Td> + <Td> + <Text onClick={() => onClick(id)} component="a">{name}</Text> + </Td> + <Td>{acsType === 'rhui' ? upperCase(acsType) : capitalize(acsType)}</Td> + <Td><LastSync + startedAt={startedAt} + lastSync={lastTask} + lastSyncWords={lastRefreshWords} + emptyMessage="N/A" + /> + </Td> + {(hasPermission(permissions, 'destroy_alternate_content_sources') || + hasPermission(permissions, 'edit_alternate_content_sources')) ? + <Td isActionCell> + <ActionsColumn items={actionsWithPermissions(acs)} /> + </Td> : + <Td /> + } + </Tr> + ); + }) } - </Tbody> - </TableWrapper> - </DrawerContentBody> - </DrawerContent> - </Drawer> + </Tbody> + </TableWrapper> + </DrawerContentBody> + </DrawerContent> + </Drawer> + </div> ); /* eslint-enable react/no-array-index-key */ }; export default ACSTable;