Skip to content

Commit

Permalink
Merge pull request #248 from tnc-ca-geo/feature/229-delete_camera_config
Browse files Browse the repository at this point in the history
Creating DeleteCamera Option
  • Loading branch information
nathanielrindlaub authored Dec 24, 2024
2 parents 7220aa6 + 1295c85 commit 5387387
Show file tree
Hide file tree
Showing 11 changed files with 500 additions and 127 deletions.
11 changes: 11 additions & 0 deletions src/api/buildQuery.js
Original file line number Diff line number Diff line change
Expand Up @@ -767,6 +767,17 @@ const queries = {
variables: { input: input },
}),

deleteCameraConfig: (input) => ({
template: `
mutation deleteCameraConfig($input: DeleteCameraInput!) {
deleteCameraConfig(input: $input) {
_id
}
}
`,
variables: { input: input },
}),

createUpload: (input) => ({
template: `
mutation CreateUpload($input: CreateUploadInput!) {
Expand Down
7 changes: 6 additions & 1 deletion src/components/ErrorToast.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ import {
selectManageLabelsErrors,
dismissManageLabelsError,
selectProjectTagErrors,
dismissProjectTagErrors
dismissProjectTagErrors,
} from '../features/projects/projectsSlice';
import {
selectWirelessCamerasErrors,
Expand All @@ -46,6 +46,8 @@ import {
dismissDeploymentsError,
selectCameraSerialNumberErrors,
dismissCameraSerialNumberError,
selectDeleteCameraErrors,
dismissDeleteCameraError,
selectDeleteImagesErrors,
dismissDeleteImagesError,
} from '../features/tasks/tasksSlice';
Expand Down Expand Up @@ -81,6 +83,7 @@ const ErrorToast = () => {
const manageLabelsErrors = useSelector(selectManageLabelsErrors);
const uploadErrors = useSelector(selectUploadErrors);
const cameraSerialNumberErrors = useSelector(selectCameraSerialNumberErrors);
const deleteCameraErrors = useSelector(selectDeleteCameraErrors);
const projectTagErrors = useSelector(selectProjectTagErrors);
const deleteImagesErrors = useSelector(selectDeleteImagesErrors);

Expand Down Expand Up @@ -109,6 +112,7 @@ const ErrorToast = () => {
'Error Updating Camera Serial Number',
'cameraSerialNumber',
),
enrichErrors(deleteCameraErrors, 'Error Deleting Camera', 'deleteCamera'),
enrichErrors(deleteImagesErrors, 'Error Deleting Images', 'deleteImages'),
];

Expand Down Expand Up @@ -175,6 +179,7 @@ const dismissErrorActions = {
manageLabels: (i) => dismissManageLabelsError(i),
upload: (i) => dismissUploadError(i),
cameraSerialNumber: (i) => dismissCameraSerialNumberError(i),
deleteCamera: (i) => dismissDeleteCameraError(i),
deleteImagesError: (i) => dismissDeleteImagesError(i),
};

Expand Down
20 changes: 14 additions & 6 deletions src/components/HydratedModal.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,10 @@ import {
setSelectedCamera,
} from '../features/projects/projectsSlice';
import { clearUsers } from '../features/projects/usersSlice.js';
import { ManageLabelsAndTagsModal, ManageLabelsAndTagsModalTitle } from '../features/projects/ManageTagsAndLabelsModal.jsx';
import {
ManageLabelsAndTagsModal,
ManageLabelsAndTagsModalTitle,
} from '../features/projects/ManageTagsAndLabelsModal.jsx';

// Modal populated with content
const HydratedModal = () => {
Expand All @@ -55,7 +58,7 @@ const HydratedModal = () => {
cameraSerialNumberLoading.isLoading ||
deleteImagesLoading.isLoading;

const [manageTagsAndLabelsTab, setManageTagsAndLabelsTab] = useState("labels");
const [manageTagsAndLabelsTab, setManageTagsAndLabelsTab] = useState('labels');

const modalContentMap = {
'stats-modal': {
Expand Down Expand Up @@ -121,14 +124,19 @@ const HydratedModal = () => {
},
},
'manage-tags-and-labels-form': {
title: <ManageLabelsAndTagsModalTitle tab={manageTagsAndLabelsTab} setTab={setManageTagsAndLabelsTab} />,
title: (
<ManageLabelsAndTagsModalTitle
tab={manageTagsAndLabelsTab}
setTab={setManageTagsAndLabelsTab}
/>
),
size: 'md',
content: <ManageLabelsAndTagsModal tab={manageTagsAndLabelsTab}/>,
content: <ManageLabelsAndTagsModal tab={manageTagsAndLabelsTab} />,
callBackOnClose: () => {
setManageTagsAndLabelsTab("labels");
setManageTagsAndLabelsTab('labels');
return true;
},
}
},
};

const handleModalToggle = (content) => {
Expand Down
2 changes: 1 addition & 1 deletion src/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ export const API_URL = API_URLS[stage];
export const IMAGES_URL = IMAGES_URLS[stage];
export const IMAGE_QUERY_LIMITS = [10, 50, 100];
export const SYNC_IMAGE_DELETE_LIMIT = 300; // when deleting w/o using task handler
export const ASYNC_IMAGE_DELETE_BY_ID_LIMIT = 4000; // when deleting using task handler (by _id). Constrained by POST request size limits
export const ASYNC_IMAGE_DELETE_BY_ID_LIMIT = 3000; // when deleting using task handler (by _id). Constrained by POST request size limits
export const ASYNC_IMAGE_DELETE_BY_FILTER_LIMIT = 200000; // when deleting using task handler (by filter). Constrained by task Lambda timeout

export const SUPPORTED_WIRELESS_CAMS = ['BuckEyeCam', 'RidgeTec', 'CUDDEBACK', 'RECONYX'];
Expand Down
26 changes: 25 additions & 1 deletion src/features/cameras/CameraList.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,11 @@ import {
TooltipTrigger,
} from '../../components/Tooltip.jsx';
import { selectUserCurrentRoles } from '../auth/authSlice';
import { unregisterCamera, registerCamera } from './wirelessCamerasSlice';
import {
unregisterCamera,
registerCamera,
setDeleteCameraAlertStatus,
} from './wirelessCamerasSlice';
import { setModalContent, setSelectedCamera } from '../projects/projectsSlice.js';
import IconButton from '../../components/IconButton';
import {
Expand All @@ -36,6 +40,7 @@ import {
WRITE_CAMERA_SERIAL_NUMBER_ROLES,
WRITE_DEPLOYMENTS_ROLES,
} from '../auth/roles';
import DeleteCameraAlert from './DeleteCameraAlert.jsx';

const StyledCameraList = styled('div', {
border: '1px solid $border',
Expand Down Expand Up @@ -176,6 +181,11 @@ const CameraList = ({ cameras, handleSaveDepClick, handleDeleteDepClick }) => {
dispatch(setSelectedCamera(cameraId));
};

const handleDeleteCameraClick = ({ cameraId }) => {
dispatch(setDeleteCameraAlertStatus({ isOpen: true }));
dispatch(setSelectedCamera(cameraId));
};

const [tooltipOpen, setTooltipOpen] = useState(false);
const [dropdownOpen, setDropdownOpen] = useState(null);

Expand Down Expand Up @@ -271,6 +281,19 @@ const CameraList = ({ cameras, handleSaveDepClick, handleDeleteDepClick }) => {
Re-register camera
</DropdownMenuItem>
)}
{hasRole(userRoles, WRITE_CAMERA_REGISTRATION_ROLES) && (
<DropdownMenuItem
className=""
onClick={(e) => {
e.stopPropagation;
handleDeleteCameraClick({
cameraId: cam._id,
});
}}
>
Delete camera
</DropdownMenuItem>
)}
<DropdownMenuArrow offset={12} />
</StyledDropdownMenuContent>
</DropdownMenu>
Expand Down Expand Up @@ -352,6 +375,7 @@ const CameraList = ({ cameras, handleSaveDepClick, handleDeleteDepClick }) => {
Deployment
</a>
</IconKey>
<DeleteCameraAlert />
</>
)}
</>
Expand Down
146 changes: 146 additions & 0 deletions src/features/cameras/DeleteCameraAlert.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
import React, { useEffect } from 'react';
import { useSelector, useDispatch } from 'react-redux';
import { red } from '@radix-ui/colors';
import { styled } from '../../theme/stitches.config.js';
import Button from '../../components/Button.jsx';
import { ButtonRow } from '../../components/Form.jsx';
import {
clearDeleteCameraTask,
deleteCamera,
fetchTask,
selectDeleteCameraLoading,
} from '../tasks/tasksSlice.js';
import { SimpleSpinner, SpinnerOverlay } from '../../components/Spinner.jsx';
import { selectSelectedCamera, setSelectedCamera } from '../projects/projectsSlice.js';
import {
fetchCameraImageCount,
selectCameraImageCount,
selectCameraImageCountLoading,
clearCameraImageCount,
selectDeleteCameraAlertStatus,
setDeleteCameraAlertStatus,
} from './wirelessCamerasSlice.js';
import { ASYNC_IMAGE_DELETE_BY_FILTER_LIMIT } from '../../config.js';
import {
Alert,
AlertPortal,
AlertOverlay,
AlertContent,
AlertTitle,
} from '../../components/AlertDialog.jsx';
import { DeleteImagesProgressBar } from '../images/DeleteImagesProgressBar.jsx';

const BoldText = styled('span', {
fontWeight: '$5',
});

const DeleteCameraAlert = () => {
const deleteCameraLoading = useSelector(selectDeleteCameraLoading);
const selectedCamera = useSelector(selectSelectedCamera);
const imageCount = useSelector(selectCameraImageCount);
const imageCountLoading = useSelector(selectCameraImageCountLoading);
const isAlertOpen = useSelector(selectDeleteCameraAlertStatus);
const dispatch = useDispatch();

useEffect(() => {
if (imageCount === null && selectedCamera !== null && !imageCountLoading) {
dispatch(fetchCameraImageCount({ cameraId: selectedCamera }));
}
}, [imageCount, selectedCamera, dispatch]);

const handleDeleteCameraSubmit = () => {
dispatch(deleteCamera({ cameraId: selectedCamera }));
};

const handleCancelDelete = () => {
dispatch(setDeleteCameraAlertStatus({ isOpen: false }));
dispatch(setSelectedCamera(null));
dispatch(clearDeleteCameraTask());
dispatch(clearCameraImageCount());
};

// handle polling for task completion
useEffect(() => {
const deleteCameraPending = deleteCameraLoading.isLoading && deleteCameraLoading.taskId;
if (deleteCameraPending) {
dispatch(fetchTask(deleteCameraLoading.taskId));
}
}, [deleteCameraLoading, dispatch]);

return (
<Alert open={isAlertOpen}>
<AlertPortal>
<AlertOverlay />
<AlertContent>
{(deleteCameraLoading.isLoading || imageCountLoading) && (
<SpinnerOverlay>
<SimpleSpinner />
<DeleteImagesProgressBar imageCount={imageCount} />
</SpinnerOverlay>
)}
<AlertTitle>Delete Camera</AlertTitle>
{/*TODO: Add a link to the documentation for more information on how to delete images.*/}
{imageCount > ASYNC_IMAGE_DELETE_BY_FILTER_LIMIT ? (
<>
Due to the large number of images associated with this camera, we are unable to delete
Camera <BoldText>{selectedCamera}</BoldText> at this time. Please ensure that the
number of images associated with this camera do not exceed{' '}
{ASYNC_IMAGE_DELETE_BY_FILTER_LIMIT} before trying again. We apologize for the
inconvenience.
</>
) : (
<>
<p>
Are you sure you&apos;d like to delete Camera <BoldText>{selectedCamera}</BoldText>?{' '}
{imageCount === 0 && 'This will remove the Camera and the Deployments '}
{imageCount > 0 && (
<>
This will remove the Camera, its Deployments, and{' '}
{imageCount > 1 ? 'all' : 'the'}{' '}
<BoldText>
{imageCount} image{imageCount > 1 && 's'}
</BoldText>{' '}
</>
)}
associated with it from the Project.
</p>
<p>
<BoldText>This action cannot be undone.</BoldText>
</p>
<ButtonRow>
<Button
type="button"
size="large"
disabled={deleteCameraLoading.isLoading}
onClick={handleCancelDelete}
>
Cancel
</Button>
<Button
type="submit"
size="large"
disabled={
deleteCameraLoading.isLoading ||
imageCount > ASYNC_IMAGE_DELETE_BY_FILTER_LIMIT ||
imageCountLoading
}
onClick={handleDeleteCameraSubmit}
css={{
backgroundColor: red.red4,
color: red.red11,
border: 'none',
'&:hover': { color: red.red11, backgroundColor: red.red5 },
}}
>
Delete Camera
</Button>
</ButtonRow>
</>
)}
</AlertContent>
</AlertPortal>
</Alert>
);
};

export default DeleteCameraAlert;
Loading

0 comments on commit 5387387

Please sign in to comment.