From 5d09434bfc56bc97aa03f22a2c585361ca99c858 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Ricks?= Date: Tue, 18 Jun 2024 11:00:38 +0200 Subject: [PATCH] Change: Refactor PortLists page to a HOC less entities page Use the PortLists page as an example on howto refactor an entities page to use the new hooks instead of the withEntitiesContainer HOC. Despite having some more lines of code it should be easier to understand then before. --- src/web/pages/portlists/listpage.jsx | 387 +++++++++++++++++++++------ 1 file changed, 304 insertions(+), 83 deletions(-) diff --git a/src/web/pages/portlists/listpage.jsx b/src/web/pages/portlists/listpage.jsx index c55a96641c..e8d0479d5e 100644 --- a/src/web/pages/portlists/listpage.jsx +++ b/src/web/pages/portlists/listpage.jsx @@ -3,37 +3,59 @@ * SPDX-License-Identifier: AGPL-3.0-or-later */ -import React from 'react'; +import React, {useCallback, useEffect, useState} from 'react'; -import _ from 'gmp/locale'; +import {useDispatch} from 'react-redux'; import {PORTLISTS_FILTER_FILTER} from 'gmp/models/filter'; -import IconDivider from 'web/components/layout/icondivider'; -import PageTitle from 'web/components/layout/pagetitle'; +import {isDefined, hasValue} from 'gmp/utils/identity'; + +import useCapabilities from 'web/utils/useCapabilities'; +import useUserSessionTimeout from 'web/utils/useUserSessionTimeout'; + +import useGmp from 'web/utils/useGmp'; +import usePageFilter from 'web/hooks/usePageFilter'; +import useShallowEqualSelector from 'web/hooks/useShallowEqualSelector'; +import useReload from 'web/hooks/useReload'; +import useSelection from 'web/hooks/useSelection'; +import useFilterSortBy from 'web/hooks/useFilterSortBy'; +import usePreviousValue from 'web/hooks/usePreviousValue'; +import usePagination from 'web/hooks/usePagination'; +import useTranslation from 'web/hooks/useTranslation'; import PropTypes from 'web/utils/proptypes'; -import withCapabilities from 'web/utils/withCapabilities'; +import SelectionType from 'web/utils/selectiontype'; +import {generateFilename} from 'web/utils/render'; + +import {loadEntities, selector} from 'web/store/entities/portlists'; +import {getUserSettingsDefaults} from 'web/store/usersettings/defaults/selectors'; +import useEntitiesReloadInterval from 'web/entities/useEntitiesReloadInterval'; +import BulkTags from 'web/entities/BulkTags'; import EntitiesPage from 'web/entities/page'; -import withEntitiesContainer from 'web/entities/withEntitiesContainer'; +import DialogNotification from 'web/components/notification/dialognotification'; +import useDialogNotification from 'web/components/notification/useDialogNotification'; + +import PageTitle from 'web/components/layout/pagetitle'; +import Download from 'web/components/form/download'; +import PortListIcon from 'web/components/icon/portlisticon'; +import IconDivider from 'web/components/layout/icondivider'; import ManualIcon from 'web/components/icon/manualicon'; -import UploadIcon from 'web/components/icon/uploadicon'; import NewIcon from 'web/components/icon/newicon'; -import PortListIcon from 'web/components/icon/portlisticon'; +import UploadIcon from 'web/components/icon/uploadicon'; -import { - loadEntities, - selector as entitiesSelector, -} from 'web/store/entities/portlists'; +import useDownload from 'web/components/form/useDownload'; import PortListComponent from './component'; -import PortListsFilterDialog from './filterdialog'; import PortListsTable from './table'; +import PortListsFilterDialog from './filterdialog'; -const ToolBarIcons = withCapabilities( - ({capabilities, onPortListCreateClick, onPortListImportClick}) => ( +const ToolBarIcons = ({onPortListCreateClick, onPortListImportClick}) => { + const capabilities = useCapabilities(); + const [_] = useTranslation(); + return ( - ), -); + ); +}; ToolBarIcons.propTypes = { onPortListCreateClick: PropTypes.func.isRequired, onPortListImportClick: PropTypes.func.isRequired, }; -const PortListsPage = ({ - onChanged, - onDownloaded, - onError, - onInteraction, - ...props -}) => ( - - {({ - clone, - create, - delete: delete_func, - download, - edit, - save, - import: import_func, - }) => ( - - - } - table={PortListsTable} - title={_('Portlists')} - toolBarIcons={ToolBarIcons} - onChanged={onChanged} - onDownloaded={onDownloaded} - onError={onError} - onInteraction={onInteraction} - onPortListCloneClick={clone} - onPortListCreateClick={create} - onPortListDeleteClick={delete_func} - onPortListDownloadClick={download} - onPortListEditClick={edit} - onPortListSaveClick={save} - onPortListImportClick={import_func} - /> - - )} - -); - -PortListsPage.propTypes = { - onChanged: PropTypes.func.isRequired, - onDownloaded: PropTypes.func.isRequired, - onError: PropTypes.func.isRequired, - onInteraction: PropTypes.func.isRequired, +const getData = (filter, eSelector) => { + const entities = eSelector.getEntities(filter); + return { + entities, + entitiesCounts: eSelector.getEntitiesCounts(filter), + entitiesError: eSelector.getEntitiesError(filter), + filter, + isLoading: eSelector.isLoadingEntities(filter), + loadedFilter: eSelector.getLoadedFilter(filter), + }; }; -export default withEntitiesContainer('portlist', { - entitiesSelector, - loadEntities, -})(PortListsPage); +const PortListsPage = () => { + const [_] = useTranslation(); + const gmp = useGmp(); + const dispatch = useDispatch(); + const [isTagsDialogVisible, setIsTagsDialogVisible] = useState(false); + const [downloadRef, handleDownload] = useDownload(); + const [, renewSession] = useUserSessionTimeout(); + const [filter, isLoadingFilter, changeFilter, resetFilter, removeFilter] = + usePageFilter('portlist'); + const previousFilter = usePreviousValue(filter); + const portListsSelector = useShallowEqualSelector(selector); + const listExportFileName = useShallowEqualSelector(state => + getUserSettingsDefaults(state).getValueByName('listexportfilename'), + ); + const { + selectionType, + selected: selectedEntities = [], + changeSelectionType, + select, + deselect, + } = useSelection(); + const [sortBy, sortDir, handleSortChange] = useFilterSortBy( + filter, + changeFilter, + ); + const { + dialogState: notificationDialogState, + closeDialog: closeNotificationDialog, + showError, + } = useDialogNotification(); + + // fetch port lists + const fetch = useCallback( + withFilter => { + dispatch(loadEntities(gmp)(withFilter)); + }, + [dispatch, gmp], + ); + + // refetch port lists with the current filter + const refetch = useCallback(() => { + fetch(filter); + }, [filter, fetch]); + + const {entities, entitiesCounts, isLoading} = getData( + filter, + portListsSelector, + ); + + const paginationChanged = useCallback( + newFilter => { + fetch(newFilter); + changeFilter(newFilter); + }, + [changeFilter, fetch], + ); + + const [getFirst, getLast, getNext, getPrevious] = usePagination( + filter, + entitiesCounts, + paginationChanged, + ); + const timeoutFunc = useEntitiesReloadInterval(entities); + const [startReload, stopReload, hasRunningTimer] = useReload( + refetch, + timeoutFunc, + ); + + useEffect(() => { + // load initial data + if (isDefined(filter) && !isLoadingFilter) { + fetch(filter); + } + }, [filter, isLoadingFilter, fetch]); + + useEffect(() => { + // reload if filter has changed + if (!filter.equals(previousFilter)) { + fetch(filter); + } + }, [filter, previousFilter, fetch]); + + useEffect(() => { + // start reloading if tasks are available and no timer is running yet + if (hasValue(entities) && !hasRunningTimer) { + startReload(); + } + }, [entities, startReload]); // eslint-disable-line react-hooks/exhaustive-deps + + // stop reload on unmount + useEffect(() => stopReload, [stopReload]); + + const closeTagsDialog = useCallback(() => { + renewSession(); + setIsTagsDialogVisible(false); + }, [renewSession, setIsTagsDialogVisible]); + + const openTagsDialog = useCallback(() => { + renewSession(); + setIsTagsDialogVisible(true); + }, [renewSession, setIsTagsDialogVisible]); + + const handleBulkDelete = useCallback(async () => { + const entitiesCommand = gmp.portlists; + let promise; + if (selectionType === SelectionType.SELECTION_USER) { + promise = entitiesCommand.delete(selectedEntities); + } else if (selectionType === SelectionType.SELECTION_PAGE_CONTENTS) { + promise = entitiesCommand.deleteByFilter(filter); + } else { + promise = entitiesCommand.deleteByFilter(filter.all()); + } + + renewSession(); + + try { + await promise; + refetch(); + } catch (error) { + showError(error); + } + }, [ + selectionType, + filter, + selectedEntities, + showError, + gmp.portlists, + refetch, + renewSession, + ]); + + const handleBulkDownload = useCallback(async () => { + const entitiesCommand = gmp.portlists; + let promise; + if (selectionType === SelectionType.SELECTION_USER) { + promise = entitiesCommand.export(selectedEntities); + } else if (selectionType === SelectionType.SELECTION_PAGE_CONTENTS) { + promise = entitiesCommand.exportByFilter(filter); + } else { + promise = entitiesCommand.exportByFilter(filter.all()); + } + + renewSession(); + + try { + const response = await promise; + const filename = generateFilename({ + fileNameFormat: listExportFileName, + resourceType: 'portlists', + }); + const {data: downloadData} = response; + handleDownload({filename, data: downloadData}); + } catch (error) { + showError(error); + } + }, [ + renewSession, + handleDownload, + showError, + gmp.portlists, + filter, + selectedEntities, + selectionType, + listExportFileName, + ]); + + return ( + + {({ + clone, + create, + delete: delete_func, + download, + edit, + save, + import: import_func, + }) => ( + <> + + } + selectionType={selectionType} + table={PortListsTable} + title={_('Portlists')} + toolBarIcons={ToolBarIcons} + onDeleteError={showError} + onError={showError} + onFirstClick={getFirst} + onLastClick={getLast} + onNextClick={getNext} + onPreviousClick={getPrevious} + onEntitySelected={select} + onEntityDeselected={deselect} + onFilterChanged={changeFilter} + onFilterCreated={changeFilter} + onFilterReset={resetFilter} + onFilterRemoved={removeFilter} + onInteraction={renewSession} + onPortListCloneClick={clone} + onPortListCreateClick={create} + onPortListDeleteClick={delete_func} + onPortListDownloadClick={download} + onPortListEditClick={edit} + onPortListSaveClick={save} + onPortListImportClick={import_func} + onSelectionTypeChange={changeSelectionType} + onSortChange={handleSortChange} + onDeleteBulk={handleBulkDelete} + onDownloadBulk={handleBulkDownload} + onTagsBulk={openTagsDialog} + /> + + + {isTagsDialogVisible && ( + + )} + + )} + + ); +}; -// vim: set ts=2 sw=2 tw=80: +export default PortListsPage;