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;