diff --git a/frontend/src/api.ts b/frontend/src/api.ts index b984f5cb3..6c04a532c 100644 --- a/frontend/src/api.ts +++ b/frontend/src/api.ts @@ -74,8 +74,11 @@ export const API = { POOLS: (projectName: IProject['project_name']) => `${API.BASE()}/project/${projectName}/pool/list`, POOL_DETAILS: (projectName: IProject['project_name']) => `${API.BASE()}/project/${projectName}/pool/show`, - // Pools + // Fleets FLEETS: (projectName: IProject['project_name']) => `${API.BASE()}/project/${projectName}/fleets/list`, + + // Fleets + VOLUMES_DELETE: (projectName: IProject['project_name']) => `${API.BASE()}/project/${projectName}/volumes/delete`, }, BACKENDS: { diff --git a/frontend/src/locale/en.json b/frontend/src/locale/en.json index 399a95192..9b16aafb2 100644 --- a/frontend/src/locale/en.json +++ b/frontend/src/locale/en.json @@ -461,6 +461,8 @@ "empty_message_text": "No volumes to display.", "nomatch_message_title": "No matches", "nomatch_message_text": "We can't find a match.", + "delete_volumes_confirm_title": "Delete volumes", + "delete_volumes_confirm_message": "Are you sure you want to delete these volumes?", "active_only": "Active volumes", "name": "Name", diff --git a/frontend/src/pages/Volumes/List/hooks.tsx b/frontend/src/pages/Volumes/List/hooks.tsx index 3f0cb0b05..31bf3e4f4 100644 --- a/frontend/src/pages/Volumes/List/hooks.tsx +++ b/frontend/src/pages/Volumes/List/hooks.tsx @@ -1,4 +1,4 @@ -import React, { useCallback, useEffect, useMemo, useState } from 'react'; +import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { format } from 'date-fns'; @@ -9,7 +9,9 @@ import { DATE_TIME_FORMAT, DEFAULT_TABLE_PAGE_SIZE } from 'consts'; import { useLocalStorageState } from 'hooks/useLocalStorageState'; import { getStatusIconType } from 'libs/volumes'; import { useGetProjectsQuery } from 'services/project'; -import { useLazyGetAllVolumesQuery } from 'services/volume'; +import { useDeleteVolumesMutation, useLazyGetAllVolumesQuery } from 'services/volume'; + +import { useNotifications } from '../../../hooks'; export const useVolumesTableEmptyMessages = ({ clearFilters, @@ -100,10 +102,13 @@ export const useVolumesData = ({ project_name, only_active }: TVolumesListReques const [data, setData] = useState([]); const [pagesCount, setPagesCount] = useState(1); const [disabledNext, setDisabledNext] = useState(false); + const lastRequestParams = useRef(undefined); const [getVolumes, { isLoading, isFetching }] = useLazyGetAllVolumesQuery(); const getVolumesRequest = (params?: TVolumesListRequestParams) => { + lastRequestParams.current = params; + return getVolumes({ project_name, only_active, @@ -116,6 +121,13 @@ export const useVolumesData = ({ project_name, only_active }: TVolumesListReques }); }; + const refreshList = () => { + getVolumesRequest(lastRequestParams.current).then((result) => { + setDisabledNext(false); + setData(result); + }); + }; + useEffect(() => { getVolumesRequest().then((result) => { setPagesCount(1); @@ -171,7 +183,7 @@ export const useVolumesData = ({ project_name, only_active }: TVolumesListReques } }; - return { data, pagesCount, disabledNext, isLoading: isLoading || isFetching, nextPage, prevPage }; + return { data, pagesCount, disabledNext, isLoading: isLoading || isFetching, nextPage, prevPage, refreshList }; }; export const useFilters = (storagePrefix?: string) => { @@ -211,3 +223,48 @@ export const useFilters = (storagePrefix?: string) => { isDisabledClearFilter, } as const; }; + +export const useVolumesDelete = () => { + const { t } = useTranslation(); + const [deleteVolumesRequest] = useDeleteVolumesMutation(); + const [pushNotification] = useNotifications(); + const [isDeleting, setIsDeleting] = useState(() => false); + + const namesOfVolumesGroupByProjectName = (volumes: IVolume[]) => { + return volumes.reduce>((acc, volume) => { + if (acc[volume.project_name]) { + acc[volume.project_name].push(volume.name); + } else { + acc[volume.project_name] = [volume.name]; + } + + return acc; + }, {}); + }; + + const deleteVolumes = (volumes: IVolume[]) => { + if (!volumes.length) return Promise.reject('No volumes'); + + setIsDeleting(true); + + const groupedVolumes = namesOfVolumesGroupByProjectName(volumes); + + const requests = Object.keys(groupedVolumes).map((projectName) => { + return deleteVolumesRequest({ + project_name: projectName, + names: groupedVolumes[projectName], + }).unwrap(); + }); + + return Promise.all(requests) + .finally(() => setIsDeleting(false)) + .catch((error) => { + pushNotification({ + type: 'error', + content: t('common.server_error', { error: error?.error }), + }); + }); + }; + + return { isDeleting, deleteVolumes }; +}; diff --git a/frontend/src/pages/Volumes/List/index.tsx b/frontend/src/pages/Volumes/List/index.tsx index 158ece02a..6c354540f 100644 --- a/frontend/src/pages/Volumes/List/index.tsx +++ b/frontend/src/pages/Volumes/List/index.tsx @@ -1,12 +1,12 @@ import React from 'react'; import { useTranslation } from 'react-i18next'; -import { Button, FormField, Header, Pagination, SelectCSD, Table, Toggle } from 'components'; +import { Button, ButtonWithConfirmation, FormField, Header, Pagination, SelectCSD, Table, Toggle } from 'components'; import { useBreadcrumbs, useCollection } from 'hooks'; import { ROUTES } from 'routes'; -import { useColumnsDefinitions, useFilters, useVolumesData, useVolumesTableEmptyMessages } from './hooks'; +import { useColumnsDefinitions, useFilters, useVolumesData, useVolumesDelete, useVolumesTableEmptyMessages } from './hooks'; import styles from './styles.module.scss'; @@ -22,12 +22,14 @@ export const VolumeList: React.FC = () => { setSelectedProject, } = useFilters(); + const { isDeleting, deleteVolumes } = useVolumesDelete(); + const { renderEmptyMessage, renderNoMatchMessage } = useVolumesTableEmptyMessages({ clearFilters, isDisabledClearFilter, }); - const { data, isLoading, pagesCount, disabledNext, prevPage, nextPage } = useVolumesData({ + const { data, isLoading, pagesCount, disabledNext, prevPage, nextPage, refreshList } = useVolumesData({ project_name: selectedProject?.value ?? undefined, only_active: onlyActive, }); @@ -41,7 +43,7 @@ export const VolumeList: React.FC = () => { }, ]); - const { items, collectionProps } = useCollection(data, { + const { items, collectionProps, actions } = useCollection(data, { filtering: { empty: renderEmptyMessage(), noMatch: renderNoMatchMessage(), @@ -50,6 +52,21 @@ export const VolumeList: React.FC = () => { selection: {}, }); + const { selectedItems } = collectionProps; + + const deleteSelectedVolumes = () => { + if (!selectedItems?.length) return; + + deleteVolumes([...selectedItems]) + .finally(() => { + refreshList(); + actions.setSelectedItems([]); + }) + .catch(console.log); + }; + + const isDisabledDeleteSelected = !selectedItems?.length || isDeleting; + return ( { loading={isLoading} loadingText={t('common.loading')} stickyHeader={true} - header={
{t('volume.volumes')}
} + header={ +
+ {t('common.delete')} + + } + > + {t('volume.volumes')} +
+ } + selectionType="multi" filter={
diff --git a/frontend/src/services/volume.ts b/frontend/src/services/volume.ts index 46790c192..514b51b83 100644 --- a/frontend/src/services/volume.ts +++ b/frontend/src/services/volume.ts @@ -25,7 +25,19 @@ export const volumeApi = createApi({ providesTags: (result) => result ? [...result.map(({ id }) => ({ type: 'Volumes' as const, id: id })), 'Volumes'] : ['Volumes'], }), + + deleteVolumes: builder.mutation({ + query: ({ project_name, names }) => ({ + url: API.PROJECTS.VOLUMES_DELETE(project_name), + method: 'POST', + body: { + names, + }, + }), + + invalidatesTags: () => ['Volumes'], + }), }), }); -export const { useLazyGetAllVolumesQuery } = volumeApi; +export const { useLazyGetAllVolumesQuery, useDeleteVolumesMutation } = volumeApi;