diff --git a/src/components/HydratedModal.jsx b/src/components/HydratedModal.jsx index b2389b09..3486124c 100644 --- a/src/components/HydratedModal.jsx +++ b/src/components/HydratedModal.jsx @@ -4,6 +4,7 @@ import moment from 'moment-timezone'; import { Modal } from './Modal.jsx'; import ImagesStatsModal from '../features/images/ImagesStatsModal.jsx'; import ExportModal from '../features/images/ExportModal.jsx'; +import DeleteImagesModal from '../features/images/DeleteImagesModal.jsx'; import CameraAdminModal from '../features/cameras/CameraAdminModal.jsx'; import AutomationRulesForm from '../features/projects/AutomationRulesForm.jsx'; import SaveViewForm from '../features/projects/SaveViewForm.jsx'; @@ -65,6 +66,12 @@ const HydratedModal = () => { content: , callBackOnClose: () => dispatch(clearExport()), }, + 'delete-images': { + title: 'Delete Selected Images', + size: 'md', + content: , + callBackOnClose: () => dispatch(clearExport()), + }, 'camera-admin-modal': { title: 'Manage Cameras and Deployments', size: 'md', diff --git a/src/features/filters/FiltersPanelFooter.jsx b/src/features/filters/FiltersPanelFooter.jsx index d9acdff9..9f877edd 100644 --- a/src/features/filters/FiltersPanelFooter.jsx +++ b/src/features/filters/FiltersPanelFooter.jsx @@ -1,7 +1,7 @@ import React from 'react'; import { styled, keyframes } from '../../theme/stitches.config.js'; import { selectUserCurrentRoles } from '../auth/authSlice.js'; -import { hasRole, READ_STATS_ROLES, EXPORT_DATA_ROLES } from '../auth/roles.js'; +import { hasRole, READ_STATS_ROLES, EXPORT_DATA_ROLES, WRITE_IMAGES_ROLES } from '../auth/roles.js'; import { useDispatch, useSelector } from 'react-redux'; import { selectImagesCount, selectImagesCountLoading } from '../images/imagesSlice.js'; import { @@ -12,9 +12,14 @@ import { fetchProjects, } from '../projects/projectsSlice.js'; import { toggleOpenLoupe } from '../loupe/loupeSlice.js'; -import { InfoCircledIcon, DownloadIcon, SymbolIcon } from '@radix-ui/react-icons'; +import { InfoCircledIcon, DownloadIcon, SymbolIcon, TrashIcon } from '@radix-ui/react-icons'; import IconButton from '../../components/IconButton.jsx'; -import { Tooltip, TooltipContent, TooltipArrow, TooltipTrigger } from '../../components/Tooltip.jsx'; +import { + Tooltip, + TooltipContent, + TooltipArrow, + TooltipTrigger, +} from '../../components/Tooltip.jsx'; const RefreshButton = styled('div', { height: '100%', @@ -40,6 +45,14 @@ const ExportCSVButton = styled('div', { padding: '0 $1', }); +const DeleteImagesButton = styled('div', { + height: '100%', + borderLeft: '1px solid $border', + display: 'flex', + alignItems: 'center', + padding: '0 $1', +}); + const ImagesCount = styled('div', { flexGrow: 1, display: 'flex', @@ -115,7 +128,11 @@ const FiltersPanelFooter = () => { - handleModalToggle('stats-modal')}> + handleModalToggle('stats-modal')} + > @@ -130,7 +147,11 @@ const FiltersPanelFooter = () => { - handleModalToggle('export-modal')}> + handleModalToggle('export-modal')} + > @@ -141,6 +162,25 @@ const FiltersPanelFooter = () => { )} + {hasRole(userRoles, WRITE_IMAGES_ROLES) && ( + + + + handleModalToggle('delete-images')} + > + + + + + + Delete all images shown + + + + )} diff --git a/src/features/images/DeleteImagesModal.jsx b/src/features/images/DeleteImagesModal.jsx new file mode 100644 index 00000000..1bdecb84 --- /dev/null +++ b/src/features/images/DeleteImagesModal.jsx @@ -0,0 +1,73 @@ +import React, { useEffect } from 'react'; +import { useDispatch, useSelector } from 'react-redux'; +import { deleteImagesTask, fetchTask, selectDeleteImagesLoading } from '../tasks/tasksSlice.js'; +import { selectActiveFilters } from '../filters/filtersSlice.js'; +import { SimpleSpinner, SpinnerOverlay } from '../../components/Spinner'; +import { ButtonRow, HelperText } from '../../components/Form'; +import Button from '../../components/Button'; +import NoneFoundAlert from '../../components/NoneFoundAlert'; +import { fetchImagesCount, fetchImages, deleteImages } from './imagesSlice.js'; + +const DeleteImagesModal = async () => { + const filters = useSelector(selectActiveFilters); + const deleteImagesLoading = useSelector(selectDeleteImagesLoading); + const dispatch = useDispatch(); + + const imageCount = await dispatch(fetchImagesCount(filters)); + + const deleteImagesPending = deleteImagesLoading.isLoading && deleteImagesLoading.taskId; + useEffect(() => { + if (deleteImagesPending) { + dispatch(fetchTask(deleteImagesLoading.taskId)); + } + }, [deleteImagesPending, deleteImagesLoading, dispatch]); + + const handleDeleteImagesButtonClick = () => { + const { isLoading, errors, noneFound } = deleteImagesLoading; + const noErrors = !errors || errors.length === 0; + if (!noneFound && !isLoading && noErrors) { + // const images = dispatch(fetchImages(filters)); + if (imageCount > 50) { + dispatch(deleteImagesTask({ filters })); + } else { + dispatch(fetchImages(filters)).then((images) => { + dispatch(deleteImages({ images })); + }); + } + } + }; + + return ( +
+ {deleteImagesLoading.isLoading && ( + + + + )} + {imageCount === 0 && ( + + We couldn't find any images that matched this set of filters. + + )} + +

+ Do you wish to delete all images that match the current filters? This action will delete + {imageCount} images and cannot be undone. +

+
+ + + +
+ ); +}; + +export default DeleteImagesModal; diff --git a/src/features/tasks/tasksSlice.js b/src/features/tasks/tasksSlice.js index 211cca19..19857143 100644 --- a/src/features/tasks/tasksSlice.js +++ b/src/features/tasks/tasksSlice.js @@ -42,6 +42,11 @@ const initialState = { isLoading: false, errors: null, }, + deleteImagesTask: { + taskId: null, + isLoading: false, + errors: null + } }, imagesStats: null, annotationsExport: null, @@ -254,6 +259,42 @@ export const tasksSlice = createSlice({ state.loadingStates.cameraSerialNumber.taskId = null; state.loadingStates.cameraSerialNumber.errors.splice(index, 1); }, + + // delete images + + deleteImagesTaskStart: (state) => { + let ls = state.loadingStates.deleteImages; + ls.taskId = null; + ls.isLoading = true; + ls.errors = null; + }, + + deleteImagesTaskUpdate: (state, { payload }) => { + state.loadingStates.deleteImages.taskId = payload.taskId; + }, + + deleteImagesTaskSuccess: (state) => { + let ls = state.loadingStates.deleteImages; + ls.taskId = null; + ls.isLoading = false; + ls.errors = null; + }, + + deleteImagesTaskFailure: (state, { payload }) => { + let ls = state.loadingStates.deleteImages; + ls.isLoading = false; + ls.errors = [payload.task.output.error]; + }, + + clearDeleteImagesTask: (state) => { + state.loadingStates.deleteImages = initialState.loadingStates.deleteImages; + }, + + dismissDeleteImagesTaskError: (state, { payload }) => { + const index = payload; + state.loadingStates.deleteImages.taskId = null; + state.loadingStates.deleteImages.errors.splice(index, 1); + }, }, }); @@ -294,6 +335,13 @@ export const { updateCameraSerialNumberFailure, clearCameraSerialNumberTask, dismissCameraSerialNumberError, + + deleteImagesTaskStart, + deleteImagesTaskUpdate, + deleteImagesTaskSuccess, + deleteImagesTaskFailure, + clearDeleteImagesTask, + dismissDeleteImagesTaskError, } = tasksSlice.actions; // fetchTask thunk @@ -505,6 +553,33 @@ export const updateCameraSerialNumber = (payload) => { }; }; +// delete images thunk +export const deleteImagesTask = ({ filters }) => { + return async (dispatch, getState) => { + try { + dispatch(updateCameraSerialNumberStart()); + const currentUser = await Auth.currentAuthenticatedUser(); + const token = currentUser.getSignInUserSession().getIdToken().getJwtToken(); + const projects = getState().projects.projects; + const selectedProj = projects.find((proj) => proj.selected); + console.log(filters); + + if (token && selectedProj) { + const res = await call({ + projId: selectedProj._id, + request: 'deleteImagesTask', + input: filters, + }); + console.log('deleteImages - res: ', res); + dispatch(deleteImagesTaskUpdate({ taskId: res.deleteImagesTask._id })); + } + } + catch (err) { + dispatch(updateCameraSerialNumberFailure(err)); + } + }; +}; + export const selectImagesStats = (state) => state.tasks.imagesStats; export const selectStatsLoading = (state) => state.tasks.loadingStates.stats; export const selectStatsErrors = (state) => state.tasks.loadingStates.stats.errors; @@ -522,5 +597,6 @@ export const selectCameraSerialNumberLoading = (state) => state.tasks.loadingStates.cameraSerialNumber; export const selectCameraSerialNumberErrors = (state) => state.tasks.loadingStates.cameraSerialNumber.errors; +export const selectDeleteImagesLoading = (state) => state.tasks.loadingStates.deleteImagesTask; export default tasksSlice.reducer;