Skip to content

Commit

Permalink
Preliminary frontend implementation
Browse files Browse the repository at this point in the history
  • Loading branch information
jue-henry committed Oct 21, 2024
1 parent cc7895a commit 206e9a3
Show file tree
Hide file tree
Showing 4 changed files with 201 additions and 5 deletions.
7 changes: 7 additions & 0 deletions src/components/HydratedModal.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -65,6 +66,12 @@ const HydratedModal = () => {
content: <ExportModal />,
callBackOnClose: () => dispatch(clearExport()),
},
'delete-images': {
title: 'Delete Selected Images',
size: 'md',
content: <DeleteImagesModal />,
callBackOnClose: () => dispatch(clearExport()),
},
'camera-admin-modal': {
title: 'Manage Cameras and Deployments',
size: 'md',
Expand Down
50 changes: 45 additions & 5 deletions src/features/filters/FiltersPanelFooter.jsx
Original file line number Diff line number Diff line change
@@ -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 {
Expand All @@ -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%',
Expand All @@ -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',
Expand Down Expand Up @@ -115,7 +128,11 @@ const FiltersPanelFooter = () => {
<Tooltip>
<TooltipTrigger asChild>
<InfoButton>
<IconButton variant="ghost" size="large" onClick={() => handleModalToggle('stats-modal')}>
<IconButton
variant="ghost"
size="large"
onClick={() => handleModalToggle('stats-modal')}
>
<InfoCircledIcon />
</IconButton>
</InfoButton>
Expand All @@ -130,7 +147,11 @@ const FiltersPanelFooter = () => {
<Tooltip>
<TooltipTrigger asChild>
<ExportCSVButton>
<IconButton variant="ghost" size="large" onClick={() => handleModalToggle('export-modal')}>
<IconButton
variant="ghost"
size="large"
onClick={() => handleModalToggle('export-modal')}
>
<DownloadIcon />
</IconButton>
</ExportCSVButton>
Expand All @@ -141,6 +162,25 @@ const FiltersPanelFooter = () => {
</TooltipContent>
</Tooltip>
)}
{hasRole(userRoles, WRITE_IMAGES_ROLES) && (
<Tooltip>
<TooltipTrigger asChild>
<DeleteImagesButton>
<IconButton
variant="ghost"
size="large"
onClick={() => handleModalToggle('delete-images')}
>
<TrashIcon />
</IconButton>
</DeleteImagesButton>
</TooltipTrigger>
<TooltipContent side="top" sideOffset={5}>
Delete all images shown
<TooltipArrow />
</TooltipContent>
</Tooltip>
)}
<Tooltip>
<TooltipTrigger asChild>
<RefreshButton>
Expand Down
73 changes: 73 additions & 0 deletions src/features/images/DeleteImagesModal.jsx
Original file line number Diff line number Diff line change
@@ -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 (
<div>
{deleteImagesLoading.isLoading && (
<SpinnerOverlay>
<SimpleSpinner />
</SpinnerOverlay>
)}
{imageCount === 0 && (
<NoneFoundAlert>
We couldn&apos;t find any images that matched this set of filters.
</NoneFoundAlert>
)}
<HelperText>
<p>
Do you wish to delete all images that match the current filters? This action will delete
{imageCount} images and cannot be undone.
</p>
</HelperText>
<ButtonRow>
<Button
type="submit"
size="large"
disabled={deleteImagesLoading.isLoading}
data-format="coco"
onClick={handleDeleteImagesButtonClick}
>
Delete Images
</Button>
</ButtonRow>
</div>
);
};

export default DeleteImagesModal;
76 changes: 76 additions & 0 deletions src/features/tasks/tasksSlice.js
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,11 @@ const initialState = {
isLoading: false,
errors: null,
},
deleteImagesTask: {
taskId: null,
isLoading: false,
errors: null
}
},
imagesStats: null,
annotationsExport: null,
Expand Down Expand Up @@ -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);
},
},
});

Expand Down Expand Up @@ -294,6 +335,13 @@ export const {
updateCameraSerialNumberFailure,
clearCameraSerialNumberTask,
dismissCameraSerialNumberError,

deleteImagesTaskStart,
deleteImagesTaskUpdate,
deleteImagesTaskSuccess,
deleteImagesTaskFailure,
clearDeleteImagesTask,
dismissDeleteImagesTaskError,
} = tasksSlice.actions;

// fetchTask thunk
Expand Down Expand Up @@ -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;
Expand All @@ -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;

0 comments on commit 206e9a3

Please sign in to comment.