From c80573f7b2936d6ddabd3a10be69b344d7bbb79d Mon Sep 17 00:00:00 2001 From: jue-henry Date: Tue, 8 Oct 2024 16:39:06 -0700 Subject: [PATCH 01/15] preliminary implementation --- src/components/HydratedModal.jsx | 29 +++++-- src/features/cameras/CameraList.jsx | 17 ++++ src/features/cameras/DeleteCameraForm.jsx | 82 +++++++++++++++++++ ...merasSlice.jsx => wirelessCamerasSlice.js} | 0 4 files changed, 122 insertions(+), 6 deletions(-) create mode 100644 src/features/cameras/DeleteCameraForm.jsx rename src/features/cameras/{wirelessCamerasSlice.jsx => wirelessCamerasSlice.js} (100%) diff --git a/src/components/HydratedModal.jsx b/src/components/HydratedModal.jsx index 900a9c8c..d8d8ac34 100644 --- a/src/components/HydratedModal.jsx +++ b/src/components/HydratedModal.jsx @@ -11,6 +11,7 @@ import DeleteViewForm from '../features/projects/DeleteViewForm.jsx'; import ManageUsersModal from '../features/projects/ManageUsersModal.jsx'; import BulkUploadForm from '../features/upload/BulkUploadForm.jsx'; import UpdateCameraSerialNumberForm from '../features/cameras/UpdateCameraSerialNumberForm.jsx'; +import DeleteCameraForm from '../features/cameras/DeleteCameraForm.jsx'; import { clearStats, clearExport, @@ -32,7 +33,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 = () => { @@ -55,7 +59,7 @@ const HydratedModal = () => { cameraSerialNumberLoading.isLoading || deleteImagesLoading.isLoading; - const [manageTagsAndLabelsTab, setManageTagsAndLabelsTab] = useState("labels"); + const [manageTagsAndLabelsTab, setManageTagsAndLabelsTab] = useState('labels'); const modalContentMap = { 'stats-modal': { @@ -120,15 +124,28 @@ const HydratedModal = () => { dispatch(clearCameraSerialNumberTask()); }, }, + 'delete-camera-form': { + title: 'Delete Camera', + size: 'md', + content: , + callBackOnClose: () => { + dispatch(setSelectedCamera(null)); + }, + }, 'manage-tags-and-labels-form': { - title: , + title: ( + + ), size: 'md', - content: , + content: , callBackOnClose: () => { - setManageTagsAndLabelsTab("labels"); + setManageTagsAndLabelsTab('labels'); return true; }, - } + }, }; const handleModalToggle = (content) => { diff --git a/src/features/cameras/CameraList.jsx b/src/features/cameras/CameraList.jsx index 0b2687c9..3fd3fe92 100644 --- a/src/features/cameras/CameraList.jsx +++ b/src/features/cameras/CameraList.jsx @@ -176,6 +176,10 @@ const CameraList = ({ cameras, handleSaveDepClick, handleDeleteDepClick }) => { dispatch(setSelectedCamera(cameraId)); }; + const handleDeleteCameraClick = () => { + dispatch(setModalContent('delete-camera-form')); + }; + const [tooltipOpen, setTooltipOpen] = useState(false); const [dropdownOpen, setDropdownOpen] = useState(null); @@ -271,6 +275,19 @@ const CameraList = ({ cameras, handleSaveDepClick, handleDeleteDepClick }) => { Re-register camera )} + {hasRole(userRoles, WRITE_CAMERA_REGISTRATION_ROLES) && ( + { + e.stopPropagation; + handleDeleteCameraClick({ + cameraId: cam._id, + }); + }} + > + Delete camera + + )} diff --git a/src/features/cameras/DeleteCameraForm.jsx b/src/features/cameras/DeleteCameraForm.jsx new file mode 100644 index 00000000..ea6300ae --- /dev/null +++ b/src/features/cameras/DeleteCameraForm.jsx @@ -0,0 +1,82 @@ +import React, { useState, useEffect } from 'react'; +// import { useSelector, useDispatch } from 'react-redux'; +import { styled } from '../../theme/stitches.config.js'; +import { Formik, Form, Field } from 'formik'; +import * as Yup from 'yup'; +// import { editDeployments } from '../tasks/tasksSlice.js'; +import Button from '../../components/Button.jsx'; +import { FormWrapper, ButtonRow, HelperText } from '../../components/Form.jsx'; +// import { SimpleSpinner, SpinnerOverlay } from '../../components/Spinner.jsx'; + +const CameraId = styled('span', { + fontWeight: '$5', +}); + +const deleteCameraSchema = Yup.object().shape({ + cameraId: Yup.string().required('A camera ID is required'), +}); + +const DeleteCameraForm = ({ cameraId, handleClose }) => { + const [queuedForClose, setQueuedForClose] = useState(false); + // const dispatch = useDispatch(); + + // TODO: extract into hook? + useEffect(() => { + if (queuedForClose) handleClose(); + }, [queuedForClose, handleClose]); + + const handleDeleteCameraSubmit = () => { + // dispatch(editDeployments('deleteCamera', formVals)); + + setQueuedForClose(true); + }; + + // // handle polling for task completion + // useEffect(() => { + // const getDepsPending = depsLoading.isLoading && depsLoading.taskId; + // if (getDepsPending) { + // dispatch(fetchTask(depsLoading.taskId)); + // } + // }, [depsLoading, dispatch]); + + return ( +
+ {/* {depsLoading.isLoading && ( + + + + )} */} + + { + handleDeleteCameraSubmit(values); + }} + > + {() => ( +
+ + Are you sure you'd like to delete Camera {cameraId}? This + will remove the camera from the project, remove all deployments associated with it, + and delete all images. This action cannot be undone. + + + + + + + + + )} +
+
+
+ ); +}; + +export default DeleteCameraForm; diff --git a/src/features/cameras/wirelessCamerasSlice.jsx b/src/features/cameras/wirelessCamerasSlice.js similarity index 100% rename from src/features/cameras/wirelessCamerasSlice.jsx rename to src/features/cameras/wirelessCamerasSlice.js From 35be09a8bc7e95b0db4b777c27fab9851f56cb1c Mon Sep 17 00:00:00 2001 From: jue-henry Date: Wed, 9 Oct 2024 17:03:10 -0700 Subject: [PATCH 02/15] creating deleteCamera task --- src/features/cameras/DeleteCameraForm.jsx | 32 ++++---- src/features/tasks/tasksSlice.js | 91 ++++++++++++++++++++--- 2 files changed, 98 insertions(+), 25 deletions(-) diff --git a/src/features/cameras/DeleteCameraForm.jsx b/src/features/cameras/DeleteCameraForm.jsx index ea6300ae..d2f7873a 100644 --- a/src/features/cameras/DeleteCameraForm.jsx +++ b/src/features/cameras/DeleteCameraForm.jsx @@ -1,12 +1,12 @@ import React, { useState, useEffect } from 'react'; -// import { useSelector, useDispatch } from 'react-redux'; +import { useSelector, useDispatch } from 'react-redux'; import { styled } from '../../theme/stitches.config.js'; import { Formik, Form, Field } from 'formik'; import * as Yup from 'yup'; -// import { editDeployments } from '../tasks/tasksSlice.js'; import Button from '../../components/Button.jsx'; import { FormWrapper, ButtonRow, HelperText } from '../../components/Form.jsx'; -// import { SimpleSpinner, SpinnerOverlay } from '../../components/Spinner.jsx'; +import { deleteCamera, fetchTask, selectDeleteCameraLoading } from '../tasks/tasksSlice.js'; +import { SimpleSpinner, SpinnerOverlay } from '../../components/Spinner.jsx'; const CameraId = styled('span', { fontWeight: '$5', @@ -18,34 +18,34 @@ const deleteCameraSchema = Yup.object().shape({ const DeleteCameraForm = ({ cameraId, handleClose }) => { const [queuedForClose, setQueuedForClose] = useState(false); - // const dispatch = useDispatch(); + const deleteCameraLoading = useSelector(selectDeleteCameraLoading); + const dispatch = useDispatch(); // TODO: extract into hook? useEffect(() => { if (queuedForClose) handleClose(); }, [queuedForClose, handleClose]); - const handleDeleteCameraSubmit = () => { - // dispatch(editDeployments('deleteCamera', formVals)); - + const handleDeleteCameraSubmit = (formVals) => { + dispatch(deleteCamera(formVals)); setQueuedForClose(true); }; - // // handle polling for task completion - // useEffect(() => { - // const getDepsPending = depsLoading.isLoading && depsLoading.taskId; - // if (getDepsPending) { - // dispatch(fetchTask(depsLoading.taskId)); - // } - // }, [depsLoading, dispatch]); + // handle polling for task completion + useEffect(() => { + const getDepsPending = deleteCameraLoading.isLoading && deleteCameraLoading.taskId; + if (getDepsPending) { + dispatch(fetchTask(deleteCameraLoading.taskId)); + } + }, [deleteCameraLoading, dispatch]); return (
- {/* {depsLoading.isLoading && ( + {deleteCameraLoading.isLoading && ( - )} */} + )} { + let ls = state.loadingStates.deleteCamera; + ls.taskId = null; + ls.isLoading = true; + ls.errors = null; + }, + + updateDeleteCameraUpdate: (state, { payload }) => { + state.loadingStates.deleteCamera.taskId = payload.taskId; + }, + + updateDeleteCameraSuccess: (state) => { + let ls = state.loadingStates.deleteCamera; + ls.taskId = null; + ls.isLoading = false; + ls.errors = null; + }, + + updateDeleteCameraFailure: (state, { payload }) => { + let ls = state.loadingStates.deleteCamera; + ls.isLoading = false; + ls.errors = [payload.task.output.error]; + }, + + clearDeleteCameraTask: (state) => { + state.loadingStates.cameraSerialNumber = initialState.loadingStates.cameraSerialNumber; + }, + + dismissDeleteCameraError: (state, { payload }) => { + const index = payload; + state.loadingStates.deleteCamera.taskId = null; + state.loadingStates.deleteCamera.errors.splice(index, 1); + }, + // delete images deleteImagesStart: (state) => { @@ -297,7 +342,6 @@ export const tasksSlice = createSlice({ state.loadingStates.deleteImages.taskId = null; state.loadingStates.deleteImages.errors.splice(index, 1); }, - }, }); @@ -339,6 +383,13 @@ export const { clearCameraSerialNumberTask, dismissCameraSerialNumberError, + updateDeleteCameraStart, + updateDeleteCameraUpdate, + updateDeleteCameraSuccess, + updateDeleteCameraFailure, + clearDeleteCameraTask, + dismissDeleteCameraError, + deleteImagesStart, deleteImagesUpdate, deleteImagesSuccess, @@ -425,7 +476,7 @@ export const fetchTask = (taskId) => { dispatch(fetchImages(filters)); dispatch(fetchImagesCount(filters)); }, - FAIL: (res) => dispatch(deleteImagesFailure(res)) + FAIL: (res) => dispatch(deleteImagesFailure(res)), }, DeleteImagesByFilter: { COMPLETE: (res) => { @@ -442,8 +493,8 @@ export const fetchTask = (taskId) => { dispatch(fetchImagesCount(res.task.output.filters)); }, - FAIL: (res) => dispatch(deleteImagesFailure(res)) - } + FAIL: (res) => dispatch(deleteImagesFailure(res)), + }, }; if (res.task.type.includes('Deployment')) { @@ -590,6 +641,29 @@ export const updateCameraSerialNumber = (payload) => { }; }; +export const deleteCamera = (payload) => { + return async (dispatch, getState) => { + try { + dispatch(updateDeleteCameraStart()); + const currentUser = await Auth.currentAuthenticatedUser(); + const token = currentUser.getSignInUserSession().getIdToken().getJwtToken(); + const projects = getState().projects.projects; + const selectedProj = projects.find((proj) => proj.selected); + if (token && selectedProj) { + const res = await call({ + projId: selectedProj._id, + request: 'deleteCamera', + input: payload, + }); + console.log('deleteCamera - res: ', res); + dispatch(updateDeleteCameraUpdate({ taskId: res.deleteCamera._id })); + } + } catch (err) { + console.log('error attempting to delete camera: ', err); + } + }; +}; + // delete images thunk export const deleteImagesTask = ({ imageIds = [], filters = null }) => { /** @@ -621,14 +695,12 @@ export const deleteImagesTask = ({ imageIds = [], filters = null }) => { dispatch(deleteImagesUpdate({ taskId: res.deleteImagesTask._id })); } } - } - catch (err) { + } catch (err) { dispatch(deleteImagesFailure(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; @@ -646,6 +718,7 @@ export const selectCameraSerialNumberLoading = (state) => state.tasks.loadingStates.cameraSerialNumber; export const selectCameraSerialNumberErrors = (state) => state.tasks.loadingStates.cameraSerialNumber.errors; +export const selectDeleteCameraLoading = (state) => state.tasks.loadingStates.deleteCamera; export const selectDeleteImagesLoading = (state) => state.tasks.loadingStates.deleteImages; export const selectDeleteImagesErrors = (state) => state.tasks.loadingStates.deleteImages.errors; From dbcc7df5236db34e04537d88a8baaf3041462f9a Mon Sep 17 00:00:00 2001 From: jue-henry Date: Thu, 10 Oct 2024 17:23:05 -0700 Subject: [PATCH 03/15] creating deleteCamera graphql query --- src/api/buildQuery.js | 11 +++++++++++ src/features/cameras/CameraList.jsx | 3 ++- src/features/cameras/DeleteCameraForm.jsx | 12 +++++++----- 3 files changed, 20 insertions(+), 6 deletions(-) diff --git a/src/api/buildQuery.js b/src/api/buildQuery.js index 809afa9f..edc5d564 100644 --- a/src/api/buildQuery.js +++ b/src/api/buildQuery.js @@ -767,6 +767,17 @@ const queries = { variables: { input: input }, }), + deleteCamera: (input) => ({ + template: ` + mutation deleteCamera($input: DeleteCameraInput!) { + deleteCamera(input: $input) { + _id + } + } + `, + variables: { input: input }, + }), + createUpload: (input) => ({ template: ` mutation CreateUpload($input: CreateUploadInput!) { diff --git a/src/features/cameras/CameraList.jsx b/src/features/cameras/CameraList.jsx index 3fd3fe92..6dc34b0f 100644 --- a/src/features/cameras/CameraList.jsx +++ b/src/features/cameras/CameraList.jsx @@ -176,8 +176,9 @@ const CameraList = ({ cameras, handleSaveDepClick, handleDeleteDepClick }) => { dispatch(setSelectedCamera(cameraId)); }; - const handleDeleteCameraClick = () => { + const handleDeleteCameraClick = ({ cameraId }) => { dispatch(setModalContent('delete-camera-form')); + dispatch(setSelectedCamera(cameraId)); }; const [tooltipOpen, setTooltipOpen] = useState(false); diff --git a/src/features/cameras/DeleteCameraForm.jsx b/src/features/cameras/DeleteCameraForm.jsx index d2f7873a..7cc3e3ba 100644 --- a/src/features/cameras/DeleteCameraForm.jsx +++ b/src/features/cameras/DeleteCameraForm.jsx @@ -7,6 +7,7 @@ import Button from '../../components/Button.jsx'; import { FormWrapper, ButtonRow, HelperText } from '../../components/Form.jsx'; import { deleteCamera, fetchTask, selectDeleteCameraLoading } from '../tasks/tasksSlice.js'; import { SimpleSpinner, SpinnerOverlay } from '../../components/Spinner.jsx'; +import { selectSelectedCamera } from '../projects/projectsSlice.js'; const CameraId = styled('span', { fontWeight: '$5', @@ -16,9 +17,10 @@ const deleteCameraSchema = Yup.object().shape({ cameraId: Yup.string().required('A camera ID is required'), }); -const DeleteCameraForm = ({ cameraId, handleClose }) => { +const DeleteCameraForm = ({ handleClose }) => { const [queuedForClose, setQueuedForClose] = useState(false); const deleteCameraLoading = useSelector(selectDeleteCameraLoading); + const selectedCamera = useSelector(selectSelectedCamera); const dispatch = useDispatch(); // TODO: extract into hook? @@ -48,7 +50,7 @@ const DeleteCameraForm = ({ cameraId, handleClose }) => { )} { handleDeleteCameraSubmit(values); @@ -57,9 +59,9 @@ const DeleteCameraForm = ({ cameraId, handleClose }) => { {() => (
- Are you sure you'd like to delete Camera {cameraId}? This - will remove the camera from the project, remove all deployments associated with it, - and delete all images. This action cannot be undone. + Are you sure you'd like to delete Camera {selectedCamera}? + This will remove the camera from the project, remove all deployments associated with + it, and delete all images. This action cannot be undone. From 132ba93fc725c771e43b5e238f34442c8f346fb1 Mon Sep 17 00:00:00 2001 From: jue-henry Date: Thu, 5 Dec 2024 15:53:28 -0800 Subject: [PATCH 04/15] renaming api --- src/api/buildQuery.js | 6 +++--- src/features/tasks/tasksSlice.js | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/api/buildQuery.js b/src/api/buildQuery.js index edc5d564..8c7f48f4 100644 --- a/src/api/buildQuery.js +++ b/src/api/buildQuery.js @@ -767,10 +767,10 @@ const queries = { variables: { input: input }, }), - deleteCamera: (input) => ({ + deleteCameraConfig: (input) => ({ template: ` - mutation deleteCamera($input: DeleteCameraInput!) { - deleteCamera(input: $input) { + mutation deleteCameraConfig($input: DeleteCameraInput!) { + deleteCameraConfig(input: $input) { _id } } diff --git a/src/features/tasks/tasksSlice.js b/src/features/tasks/tasksSlice.js index 865c9349..72ff0df6 100644 --- a/src/features/tasks/tasksSlice.js +++ b/src/features/tasks/tasksSlice.js @@ -652,7 +652,7 @@ export const deleteCamera = (payload) => { if (token && selectedProj) { const res = await call({ projId: selectedProj._id, - request: 'deleteCamera', + request: 'deleteCameraConfig', input: payload, }); console.log('deleteCamera - res: ', res); From 811b97df65095c80090719938b44efc13e539c88 Mon Sep 17 00:00:00 2001 From: jue-henry Date: Wed, 11 Dec 2024 14:32:53 -0800 Subject: [PATCH 05/15] adding new camera image count thunk --- src/features/cameras/DeleteCameraForm.jsx | 29 +++-- src/features/cameras/wirelessCamerasSlice.js | 108 +++++++++++++------ src/features/images/DeleteImagesAlert.jsx | 10 +- src/features/tasks/tasksSlice.js | 2 +- 4 files changed, 109 insertions(+), 40 deletions(-) diff --git a/src/features/cameras/DeleteCameraForm.jsx b/src/features/cameras/DeleteCameraForm.jsx index 7cc3e3ba..a980092d 100644 --- a/src/features/cameras/DeleteCameraForm.jsx +++ b/src/features/cameras/DeleteCameraForm.jsx @@ -8,8 +8,13 @@ import { FormWrapper, ButtonRow, HelperText } from '../../components/Form.jsx'; import { deleteCamera, fetchTask, selectDeleteCameraLoading } from '../tasks/tasksSlice.js'; import { SimpleSpinner, SpinnerOverlay } from '../../components/Spinner.jsx'; import { selectSelectedCamera } from '../projects/projectsSlice.js'; +import { + fetchCameraImageCount, + selectCameraImageCount, + selectCameraImageCountLoading, +} from '../cameras/wirelessCamerasSlice.js'; -const CameraId = styled('span', { +const BoldText = styled('span', { fontWeight: '$5', }); @@ -21,12 +26,19 @@ const DeleteCameraForm = ({ handleClose }) => { const [queuedForClose, setQueuedForClose] = useState(false); const deleteCameraLoading = useSelector(selectDeleteCameraLoading); const selectedCamera = useSelector(selectSelectedCamera); + const imageCount = useSelector(selectCameraImageCount); + const imageCountLoading = useSelector(selectCameraImageCountLoading); const dispatch = useDispatch(); - // TODO: extract into hook? useEffect(() => { - if (queuedForClose) handleClose(); - }, [queuedForClose, handleClose]); + if (imageCount === null && selectedCamera !== null && !imageCountLoading) { + dispatch(fetchCameraImageCount({ cameraId: selectedCamera })); + } + }, [imageCount, selectedCamera, dispatch]); + + useEffect(() => { + if (queuedForClose && !deleteCameraLoading.isLoading) handleClose(); + }, [deleteCameraLoading, queuedForClose, handleClose]); const handleDeleteCameraSubmit = (formVals) => { dispatch(deleteCamera(formVals)); @@ -41,9 +53,11 @@ const DeleteCameraForm = ({ handleClose }) => { } }, [deleteCameraLoading, dispatch]); + const imagesText = `${imageCount} ${imageCount === 1 ? ' image' : ' images'}`; + return (
- {deleteCameraLoading.isLoading && ( + {(deleteCameraLoading.isLoading || imageCountLoading) && ( @@ -59,9 +73,10 @@ const DeleteCameraForm = ({ handleClose }) => { {() => ( - Are you sure you'd like to delete Camera {selectedCamera}? + Are you sure you'd like to delete Camera {selectedCamera}? This will remove the camera from the project, remove all deployments associated with - it, and delete all images. This action cannot be undone. + it, and {imagesText} will be deleted.{' '} + This action cannot be undone. diff --git a/src/features/cameras/wirelessCamerasSlice.js b/src/features/cameras/wirelessCamerasSlice.js index 32c6a80a..36b87962 100644 --- a/src/features/cameras/wirelessCamerasSlice.js +++ b/src/features/cameras/wirelessCamerasSlice.js @@ -5,9 +5,14 @@ import { setSelectedProjAndView } from '../projects/projectsSlice'; const initialState = { wirelessCameras: [], + cameraImageCount: { + isLoading: false, + currentCameraId: null, + count: null, + }, loadingState: { isLoading: false, - operation: null, /* 'fetching', 'updating', 'deleting' */ + operation: null /* 'fetching', 'updating', 'deleting' */, errors: null, noneFound: false, }, @@ -17,7 +22,6 @@ export const wirelessCamerasSlice = createSlice({ name: 'wirelessCameras', initialState, reducers: { - getWirelessCamerasStart: (state) => { state.loadingState.isLoading = true; state.loadingState.operation = 'fetching'; @@ -35,7 +39,7 @@ export const wirelessCamerasSlice = createSlice({ isLoading: false, operation: null, errors: null, - noneFound: (payload.length === 0), + noneFound: payload.length === 0, }; }, @@ -58,13 +62,13 @@ export const wirelessCamerasSlice = createSlice({ isLoading: false, operation: null, errors: null, - noneFound: (payload.wirelessCameras.length === 0), + noneFound: payload.wirelessCameras.length === 0, }; // TODO: make the cameras update update more surgical? - // i.e. ONLY return the new/updated Camera source record and merge it - // into existing cameras (like we do with Views), and only return the - // new cameraConfig & merge that with Project.cameras array. - // Advantages: don't have to do getCameras() on backend before returning, + // i.e. ONLY return the new/updated Camera source record and merge it + // into existing cameras (like we do with Views), and only return the + // new cameraConfig & merge that with Project.cameras array. + // Advantages: don't have to do getCameras() on backend before returning, // less data in payload }, @@ -87,7 +91,7 @@ export const wirelessCamerasSlice = createSlice({ isLoading: false, operation: null, errors: null, - noneFound: (payload.wirelessCameras.length === 0), + noneFound: payload.wirelessCameras.length === 0, }; }, @@ -95,28 +99,45 @@ export const wirelessCamerasSlice = createSlice({ const index = payload; state.loadingState.errors.splice(index, 1); }, - + + /* Fetch image count for specific camera, so as to not clash with imageCount in imagesSlice */ + + cameraImageCountStart: (state, { payload }) => { + state.cameraImageCount.isLoading = true; + state.cameraImageCount.currentCameraId = payload.cameraId; + state.cameraImageCount.count = null; + }, + + cameraImageCountSuccess(state, { payload }) { + state.cameraImageCount.isLoading = false; + state.cameraImageCount.currentCameraId = null; + state.cameraImageCount.count = payload.imagesCount.count; + }, + + cameraImageCountError(state) { + state.cameraImageCount.isLoading = false; + state.cameraImageCount.currentCameraId = null; + state.cameraImageCount.count = null; + }, }, extraReducers: (builder) => { - builder - .addCase(setSelectedProjAndView, (state, { payload }) => { - if (payload.newProjSelected) { - state.wirelessCameras = []; - state.loadingState = { - isLoading: false, - operation: null, - errors: null, - noneFound: null, - }; - } - }) + builder.addCase(setSelectedProjAndView, (state, { payload }) => { + if (payload.newProjSelected) { + state.wirelessCameras = []; + state.loadingState = { + isLoading: false, + operation: null, + errors: null, + noneFound: null, + }; + } + }); }, }); // export actions from slice export const { - getWirelessCamerasStart, getWirelessCamerasFailure, getWirelessCamerasSuccess, @@ -131,6 +152,9 @@ export const { dismissWirelessCamerasError, + cameraImageCountStart, + cameraImageCountSuccess, + cameraImageCountError, } = wirelessCamerasSlice.actions; // fetchWirelessCameras thunk @@ -169,9 +193,8 @@ export const registerCamera = (payload) => { request: 'registerCamera', input: payload, }); - dispatch(registerCameraSuccess(res.registerCamera)) + dispatch(registerCameraSuccess(res.registerCamera)); } - } catch (err) { console.log(`error(s) attempting to register camera: `, err); dispatch(registerCameraFailure(err)); @@ -179,7 +202,7 @@ export const registerCamera = (payload) => { }; }; -// unregisger camera thunk +// unregister camera thunk export const unregisterCamera = (payload) => async (dispatch, getState) => { try { dispatch(unregisterCameraStart()); @@ -192,7 +215,7 @@ export const unregisterCamera = (payload) => async (dispatch, getState) => { const res = await call({ projId: selectedProj._id, request: 'unregisterCamera', - input: payload + input: payload, }); dispatch(unregisterCameraSuccess(res.unregisterCamera)); } @@ -201,10 +224,35 @@ export const unregisterCamera = (payload) => async (dispatch, getState) => { } }; +// fetchCameraImageCount thunk +export const fetchCameraImageCount = (payload) => async (dispatch, getState) => { + try { + dispatch(cameraImageCountStart(payload)); + const currentUser = await Auth.currentAuthenticatedUser(); + const token = currentUser.getSignInUserSession().getIdToken().getJwtToken(); + const projects = getState().projects.projects; + const selectedProj = projects.find((proj) => proj.selected); + + if (token && selectedProj) { + const res = await call({ + projId: selectedProj._id, + request: 'getImagesCount', + input: { filters: { cameras: [payload.cameraId] } }, + }); + dispatch(cameraImageCountSuccess(res)); + } + } catch (err) { + console.log(`error(s) attempting to fetch image count for camera ${payload.cameraId}: `, err); + dispatch(cameraImageCountError(err)); + } +}; // Selectors -export const selectWirelessCameras = state => state.wirelessCameras.wirelessCameras; -export const selectWirelessCamerasLoading = state => state.wirelessCameras.loadingState; -export const selectWirelessCamerasErrors = state => state.wirelessCameras.loadingState.errors; +export const selectWirelessCameras = (state) => state.wirelessCameras.wirelessCameras; +export const selectWirelessCamerasLoading = (state) => state.wirelessCameras.loadingState; +export const selectWirelessCamerasErrors = (state) => state.wirelessCameras.loadingState.errors; +export const selectCameraImageCount = (state) => state.wirelessCameras.cameraImageCount.count; +export const selectCameraImageCountLoading = (state) => + state.wirelessCameras.cameraImageCount.isLoading; export default wirelessCamerasSlice.reducer; diff --git a/src/features/images/DeleteImagesAlert.jsx b/src/features/images/DeleteImagesAlert.jsx index 2a3d3b91..a523a7a0 100644 --- a/src/features/images/DeleteImagesAlert.jsx +++ b/src/features/images/DeleteImagesAlert.jsx @@ -130,8 +130,14 @@ const DeleteImagesAlert = () => { (alertState.deleteImagesAlertByFilter && imageCountIsLoading.isLoading) || imagesLoading.isLoading; - const filterTitle = `Are you sure you'd like to delete ${imageCount === 1 ? 'this image' : `these ${imageCount && imageCount.toLocaleString()} images`}?`; - const selectionTitle = `Are you sure you'd like to delete ${selectedImages.length === 1 ? 'this image' : `these ${selectedImages && selectedImages.length.toLocaleString()} images`}?`; + const filterTitle = `Are you sure you'd like to delete ${ + imageCount === 1 ? 'this image' : `these ${imageCount && imageCount.toLocaleString()} images` + }?`; + const selectionTitle = `Are you sure you'd like to delete ${ + selectedImages.length === 1 + ? 'this image' + : `these ${selectedImages && selectedImages.length.toLocaleString()} images` + }?`; const filterText = (

diff --git a/src/features/tasks/tasksSlice.js b/src/features/tasks/tasksSlice.js index 72ff0df6..355c5b24 100644 --- a/src/features/tasks/tasksSlice.js +++ b/src/features/tasks/tasksSlice.js @@ -656,7 +656,7 @@ export const deleteCamera = (payload) => { input: payload, }); console.log('deleteCamera - res: ', res); - dispatch(updateDeleteCameraUpdate({ taskId: res.deleteCamera._id })); + dispatch(updateDeleteCameraUpdate({ taskId: res.deleteCameraConfig._id })); } } catch (err) { console.log('error attempting to delete camera: ', err); From 839bdd8f097541a38ca6dad0445bbefee717ced9 Mon Sep 17 00:00:00 2001 From: jue-henry Date: Wed, 11 Dec 2024 15:13:26 -0800 Subject: [PATCH 06/15] adding error handling and 200k limit text --- src/components/ErrorToast.jsx | 7 ++- src/components/HydratedModal.jsx | 2 + src/config.js | 2 +- src/features/cameras/DeleteCameraForm.jsx | 59 +++++++++++++++-------- src/features/images/DeleteImagesAlert.jsx | 13 ++++- src/features/tasks/tasksSlice.js | 3 +- 6 files changed, 61 insertions(+), 25 deletions(-) diff --git a/src/components/ErrorToast.jsx b/src/components/ErrorToast.jsx index b450a27c..7a3448f0 100644 --- a/src/components/ErrorToast.jsx +++ b/src/components/ErrorToast.jsx @@ -23,7 +23,7 @@ import { selectManageLabelsErrors, dismissManageLabelsError, selectProjectTagErrors, - dismissProjectTagErrors + dismissProjectTagErrors, } from '../features/projects/projectsSlice'; import { selectWirelessCamerasErrors, @@ -46,6 +46,8 @@ import { dismissDeploymentsError, selectCameraSerialNumberErrors, dismissCameraSerialNumberError, + selectDeleteCameraErrors, + dismissDeleteCameraError, selectDeleteImagesErrors, dismissDeleteImagesError, } from '../features/tasks/tasksSlice'; @@ -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); @@ -109,6 +112,7 @@ const ErrorToast = () => { 'Error Updating Camera Serial Number', 'cameraSerialNumber', ), + enrichErrors(deleteCameraErrors, 'Error Deleting Camera', 'deleteCamera'), enrichErrors(deleteImagesErrors, 'Error Deleting Images', 'deleteImages'), ]; @@ -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), }; diff --git a/src/components/HydratedModal.jsx b/src/components/HydratedModal.jsx index d8d8ac34..40989a81 100644 --- a/src/components/HydratedModal.jsx +++ b/src/components/HydratedModal.jsx @@ -24,6 +24,7 @@ import { selectCameraSerialNumberLoading, clearCameraSerialNumberTask, selectDeleteImagesLoading, + clearDeleteCameraTask, } from '../features/tasks/tasksSlice.js'; import { selectModalOpen, @@ -130,6 +131,7 @@ const HydratedModal = () => { content: , callBackOnClose: () => { dispatch(setSelectedCamera(null)); + dispatch(clearDeleteCameraTask()); }, }, 'manage-tags-and-labels-form': { diff --git a/src/config.js b/src/config.js index bf680155..058c5929 100644 --- a/src/config.js +++ b/src/config.js @@ -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']; diff --git a/src/features/cameras/DeleteCameraForm.jsx b/src/features/cameras/DeleteCameraForm.jsx index a980092d..8c286562 100644 --- a/src/features/cameras/DeleteCameraForm.jsx +++ b/src/features/cameras/DeleteCameraForm.jsx @@ -13,6 +13,7 @@ import { selectCameraImageCount, selectCameraImageCountLoading, } from '../cameras/wirelessCamerasSlice.js'; +import { ASYNC_IMAGE_DELETE_BY_FILTER_LIMIT } from '../../config.js'; const BoldText = styled('span', { fontWeight: '$5', @@ -70,26 +71,44 @@ const DeleteCameraForm = ({ handleClose }) => { handleDeleteCameraSubmit(values); }} > - {() => ( - - - Are you sure you'd like to delete Camera {selectedCamera}? - This will remove the camera from the project, remove all deployments associated with - it, and {imagesText} will be deleted.{' '} - This action cannot be undone. - - - - - - - - - )} + {() => + imageCount > ASYNC_IMAGE_DELETE_BY_FILTER_LIMIT ? ( +

+ + Due to the large number of images associated with this camera, we are unable to + delete Camera {selectedCamera} 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. + + + + +
+ ) : ( +
+ + Are you sure you'd like to delete Camera{' '} + {selectedCamera}? This will remove the camera from the + project, remove all deployments associated with it, and{' '} + {imagesText} will be deleted.{' '} + This action cannot be undone. + + + + + + + + + ) + }
diff --git a/src/features/images/DeleteImagesAlert.jsx b/src/features/images/DeleteImagesAlert.jsx index a523a7a0..3fedd2a6 100644 --- a/src/features/images/DeleteImagesAlert.jsx +++ b/src/features/images/DeleteImagesAlert.jsx @@ -20,7 +20,12 @@ import { } from '../../components/AlertDialog.jsx'; import Button from '../../components/Button.jsx'; import { red, green } from '@radix-ui/colors'; -import { deleteImagesTask, fetchTask, selectDeleteImagesLoading } from '../tasks/tasksSlice.js'; +import { + clearDeleteImagesTask, + deleteImagesTask, + fetchTask, + selectDeleteImagesLoading, +} from '../tasks/tasksSlice.js'; import { SYNC_IMAGE_DELETE_LIMIT, ASYNC_IMAGE_DELETE_BY_ID_LIMIT, @@ -91,7 +96,10 @@ const DeleteImagesAlert = () => { dispatch(deleteImages(selectedImageIds)); } } - if (selectedImages.length > 3000 || imageCount > 3000) { + if ( + selectedImages.length > ASYNC_IMAGE_DELETE_BY_ID_LIMIT || + imageCount > ASYNC_IMAGE_DELETE_BY_ID_LIMIT + ) { // show progress bar if deleting more than 3000 images (approx wait time will be > 10 seconds) const count = !alertState.deleteImagesAlertByFilter ? selectedImages.length : imageCount; setEstimatedTotalTime(count * 0.0055); // estimated deletion time per image in seconds @@ -118,6 +126,7 @@ const DeleteImagesAlert = () => { const handleCancelDelete = () => { dispatch(setDeleteImagesAlertStatus({ openStatus: false })); + dispatch(clearDeleteImagesTask()); }; const deleteByIdLimitExceeded = diff --git a/src/features/tasks/tasksSlice.js b/src/features/tasks/tasksSlice.js index 355c5b24..edfdf1d1 100644 --- a/src/features/tasks/tasksSlice.js +++ b/src/features/tasks/tasksSlice.js @@ -659,7 +659,7 @@ export const deleteCamera = (payload) => { dispatch(updateDeleteCameraUpdate({ taskId: res.deleteCameraConfig._id })); } } catch (err) { - console.log('error attempting to delete camera: ', err); + dispatch(updateDeleteCameraFailure(err)); } }; }; @@ -719,6 +719,7 @@ export const selectCameraSerialNumberLoading = (state) => export const selectCameraSerialNumberErrors = (state) => state.tasks.loadingStates.cameraSerialNumber.errors; export const selectDeleteCameraLoading = (state) => state.tasks.loadingStates.deleteCamera; +export const selectDeleteCameraErrors = (state) => state.tasks.loadingStates.deleteCamera.errors; export const selectDeleteImagesLoading = (state) => state.tasks.loadingStates.deleteImages; export const selectDeleteImagesErrors = (state) => state.tasks.loadingStates.deleteImages.errors; From 074c41013ade1413fe3c5f3143887f50d650a2f2 Mon Sep 17 00:00:00 2001 From: jue-henry Date: Wed, 11 Dec 2024 15:20:25 -0800 Subject: [PATCH 07/15] adding error handling --- src/features/tasks/tasksSlice.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/features/tasks/tasksSlice.js b/src/features/tasks/tasksSlice.js index edfdf1d1..43be8c9f 100644 --- a/src/features/tasks/tasksSlice.js +++ b/src/features/tasks/tasksSlice.js @@ -461,6 +461,12 @@ export const fetchTask = (taskId) => { }, FAIL: (res) => dispatch(updateCameraSerialNumberFailure(res)), }, + DeleteCamera: { + COMPLETE: () => { + dispatch(updateDeleteCameraSuccess()); + }, + FAIL: (res) => dispatch(updateDeleteCameraFailure(res)), + }, DeleteImages: { COMPLETE: (res) => { const filters = getState().filters.activeFilters; From 2666205fa38cae45fa10f1d8fcbaca7d901e426e Mon Sep 17 00:00:00 2001 From: jue-henry Date: Mon, 16 Dec 2024 16:33:20 -0800 Subject: [PATCH 08/15] creating delete images progress bar, and converting delete camera modal to alert dialog --- src/components/DeleteImagesProgressBar.jsx | 74 ++++++++++++ src/components/HydratedModal.jsx | 11 -- src/features/cameras/CameraList.jsx | 10 +- src/features/cameras/DeleteCameraAlert.jsx | 112 ++++++++++++++++++ src/features/cameras/DeleteCameraForm.jsx | 118 ------------------- src/features/cameras/wirelessCamerasSlice.js | 9 ++ src/features/images/DeleteImagesAlert.jsx | 76 +----------- 7 files changed, 207 insertions(+), 203 deletions(-) create mode 100644 src/components/DeleteImagesProgressBar.jsx create mode 100644 src/features/cameras/DeleteCameraAlert.jsx delete mode 100644 src/features/cameras/DeleteCameraForm.jsx diff --git a/src/components/DeleteImagesProgressBar.jsx b/src/components/DeleteImagesProgressBar.jsx new file mode 100644 index 00000000..66ec3660 --- /dev/null +++ b/src/components/DeleteImagesProgressBar.jsx @@ -0,0 +1,74 @@ +import React, { useState, useEffect } from 'react'; +import * as Progress from '@radix-ui/react-progress'; +import { green } from '@radix-ui/colors'; +import { styled } from '../theme/stitches.config.js'; + +const ProgressBar = styled('div', { + display: 'flex', + alignItems: 'center', + justifyContent: 'space-between', + position: 'absolute', + bottom: 0, + width: '100%', +}); + +const ProgressRoot = styled(Progress.Root, { + overflow: 'hidden', + background: '$backgroundDark', + // borderRadius: '99999px', + width: '100%', + height: '8px', + + /* Fix overflow clipping in Safari */ + /* https://gist.github.com/domske/b66047671c780a238b51c51ffde8d3a0 */ + transform: 'translateZ(0)', +}); + +const ProgressIndicator = styled(Progress.Indicator, { + backgroundColor: green.green9, //sky.sky4, //'$blue600', + width: '100%', + height: '100%', + transition: 'transform 660ms cubic-bezier(0.65, 0, 0.35, 1)', +}); + +export const DeleteImagesProgressBar = ({ imageCount }) => { + const [estimatedTotalTime, setEstimatedTotalTime] = useState(null); // in seconds + const [elapsedTime, setElapsedTime] = useState(null); + + if (imageCount > 3000) { + // show progress bar if deleting more than 3000 images (approx wait time will be > 10 seconds) + setEstimatedTotalTime(imageCount * 0.0055); // estimated deletion time per image in seconds + setElapsedTime(0); + } + + useEffect(() => { + if (estimatedTotalTime) { + const interval = setInterval(() => { + setElapsedTime((prevElapsedTime) => { + if (prevElapsedTime >= estimatedTotalTime) { + clearInterval(interval); + setEstimatedTotalTime(null); + setElapsedTime(null); + return estimatedTotalTime; + } + return prevElapsedTime + 1; + }); + }, 1000); + return () => clearInterval(interval); + } + }, [estimatedTotalTime, elapsedTime]); + + return ( + + + + + + ); +}; + +// export default DeleteImagesProgressBar; diff --git a/src/components/HydratedModal.jsx b/src/components/HydratedModal.jsx index 40989a81..8d9d9c06 100644 --- a/src/components/HydratedModal.jsx +++ b/src/components/HydratedModal.jsx @@ -11,7 +11,6 @@ import DeleteViewForm from '../features/projects/DeleteViewForm.jsx'; import ManageUsersModal from '../features/projects/ManageUsersModal.jsx'; import BulkUploadForm from '../features/upload/BulkUploadForm.jsx'; import UpdateCameraSerialNumberForm from '../features/cameras/UpdateCameraSerialNumberForm.jsx'; -import DeleteCameraForm from '../features/cameras/DeleteCameraForm.jsx'; import { clearStats, clearExport, @@ -24,7 +23,6 @@ import { selectCameraSerialNumberLoading, clearCameraSerialNumberTask, selectDeleteImagesLoading, - clearDeleteCameraTask, } from '../features/tasks/tasksSlice.js'; import { selectModalOpen, @@ -125,15 +123,6 @@ const HydratedModal = () => { dispatch(clearCameraSerialNumberTask()); }, }, - 'delete-camera-form': { - title: 'Delete Camera', - size: 'md', - content: , - callBackOnClose: () => { - dispatch(setSelectedCamera(null)); - dispatch(clearDeleteCameraTask()); - }, - }, 'manage-tags-and-labels-form': { title: ( { }; const handleDeleteCameraClick = ({ cameraId }) => { - dispatch(setModalContent('delete-camera-form')); + dispatch(setDeleteCameraAlertStatus(true)); dispatch(setSelectedCamera(cameraId)); }; @@ -370,6 +375,7 @@ const CameraList = ({ cameras, handleSaveDepClick, handleDeleteDepClick }) => { Deployment + )} diff --git a/src/features/cameras/DeleteCameraAlert.jsx b/src/features/cameras/DeleteCameraAlert.jsx new file mode 100644 index 00000000..dd46c1a3 --- /dev/null +++ b/src/features/cameras/DeleteCameraAlert.jsx @@ -0,0 +1,112 @@ +import React, { useState, useEffect } from 'react'; +import { useSelector, useDispatch } from 'react-redux'; +import { styled } from '../../theme/stitches.config.js'; +import Button from '../../components/Button.jsx'; +import { ButtonRow } from '../../components/Form.jsx'; +import { deleteCamera, fetchTask, selectDeleteCameraLoading } from '../tasks/tasksSlice.js'; +import { SimpleSpinner, SpinnerOverlay } from '../../components/Spinner.jsx'; +import { selectSelectedCamera } from '../projects/projectsSlice.js'; +import { + fetchCameraImageCount, + selectCameraImageCount, + selectCameraImageCountLoading, + 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 '../../components/DeleteImagesProgressBar.jsx'; + +const BoldText = styled('span', { + fontWeight: '$5', +}); + +const DeleteCameraAlert = () => { + const [queuedForClose, setQueuedForClose] = useState(false); + const deleteCameraLoading = useSelector(selectDeleteCameraLoading); + const selectedCamera = useSelector(selectSelectedCamera); + const imageCount = useSelector(selectCameraImageCount); + const imageCountLoading = useSelector(selectCameraImageCountLoading); + const alertState = useSelector(selectDeleteCameraAlertStatus); + const dispatch = useDispatch(); + + useEffect(() => { + if (imageCount === null && selectedCamera !== null && !imageCountLoading) { + dispatch(fetchCameraImageCount({ cameraId: selectedCamera })); + } + }, [imageCount, selectedCamera, dispatch]); + + useEffect(() => { + if (queuedForClose && !deleteCameraLoading.isLoading) + dispatch(setDeleteCameraAlertStatus(false)); + }, [deleteCameraLoading, queuedForClose]); + + const handleDeleteCameraSubmit = () => { + dispatch(deleteCamera({ cameraId: selectedCamera })); + setQueuedForClose(true); + }; + + // handle polling for task completion + useEffect(() => { + const deleteCameraPending = deleteCameraLoading.isLoading && deleteCameraLoading.taskId; + if (deleteCameraPending) { + dispatch(fetchTask(deleteCameraLoading.taskId)); + } + }, [deleteCameraLoading, dispatch]); + + const imagesText = `${imageCount} ${imageCount === 1 ? ' image' : ' images'}`; + + return ( + + + + + {(deleteCameraLoading.isLoading || imageCountLoading) && ( + + + + + )} + Delete Camera + {imageCount > ASYNC_IMAGE_DELETE_BY_FILTER_LIMIT ? ( + <> + Due to the large number of images associated with this camera, we are unable to delete + Camera {selectedCamera} 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. + + + + + ) : ( + <> + Are you sure you'd like to delete Camera {selectedCamera}? + This will remove the camera from the project, remove all deployments associated with + it, and {imagesText} will be deleted.{' '} + This action cannot be undone. + + + + + + )} + + + + ); +}; + +export default DeleteCameraAlert; diff --git a/src/features/cameras/DeleteCameraForm.jsx b/src/features/cameras/DeleteCameraForm.jsx deleted file mode 100644 index 8c286562..00000000 --- a/src/features/cameras/DeleteCameraForm.jsx +++ /dev/null @@ -1,118 +0,0 @@ -import React, { useState, useEffect } from 'react'; -import { useSelector, useDispatch } from 'react-redux'; -import { styled } from '../../theme/stitches.config.js'; -import { Formik, Form, Field } from 'formik'; -import * as Yup from 'yup'; -import Button from '../../components/Button.jsx'; -import { FormWrapper, ButtonRow, HelperText } from '../../components/Form.jsx'; -import { deleteCamera, fetchTask, selectDeleteCameraLoading } from '../tasks/tasksSlice.js'; -import { SimpleSpinner, SpinnerOverlay } from '../../components/Spinner.jsx'; -import { selectSelectedCamera } from '../projects/projectsSlice.js'; -import { - fetchCameraImageCount, - selectCameraImageCount, - selectCameraImageCountLoading, -} from '../cameras/wirelessCamerasSlice.js'; -import { ASYNC_IMAGE_DELETE_BY_FILTER_LIMIT } from '../../config.js'; - -const BoldText = styled('span', { - fontWeight: '$5', -}); - -const deleteCameraSchema = Yup.object().shape({ - cameraId: Yup.string().required('A camera ID is required'), -}); - -const DeleteCameraForm = ({ handleClose }) => { - const [queuedForClose, setQueuedForClose] = useState(false); - const deleteCameraLoading = useSelector(selectDeleteCameraLoading); - const selectedCamera = useSelector(selectSelectedCamera); - const imageCount = useSelector(selectCameraImageCount); - const imageCountLoading = useSelector(selectCameraImageCountLoading); - const dispatch = useDispatch(); - - useEffect(() => { - if (imageCount === null && selectedCamera !== null && !imageCountLoading) { - dispatch(fetchCameraImageCount({ cameraId: selectedCamera })); - } - }, [imageCount, selectedCamera, dispatch]); - - useEffect(() => { - if (queuedForClose && !deleteCameraLoading.isLoading) handleClose(); - }, [deleteCameraLoading, queuedForClose, handleClose]); - - const handleDeleteCameraSubmit = (formVals) => { - dispatch(deleteCamera(formVals)); - setQueuedForClose(true); - }; - - // handle polling for task completion - useEffect(() => { - const getDepsPending = deleteCameraLoading.isLoading && deleteCameraLoading.taskId; - if (getDepsPending) { - dispatch(fetchTask(deleteCameraLoading.taskId)); - } - }, [deleteCameraLoading, dispatch]); - - const imagesText = `${imageCount} ${imageCount === 1 ? ' image' : ' images'}`; - - return ( -
- {(deleteCameraLoading.isLoading || imageCountLoading) && ( - - - - )} - - { - handleDeleteCameraSubmit(values); - }} - > - {() => - imageCount > ASYNC_IMAGE_DELETE_BY_FILTER_LIMIT ? ( -
- - Due to the large number of images associated with this camera, we are unable to - delete Camera {selectedCamera} 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. - - - - -
- ) : ( -
- - Are you sure you'd like to delete Camera{' '} - {selectedCamera}? This will remove the camera from the - project, remove all deployments associated with it, and{' '} - {imagesText} will be deleted.{' '} - This action cannot be undone. - - - - - - - - - ) - } -
-
-
- ); -}; - -export default DeleteCameraForm; diff --git a/src/features/cameras/wirelessCamerasSlice.js b/src/features/cameras/wirelessCamerasSlice.js index 36b87962..38f5933b 100644 --- a/src/features/cameras/wirelessCamerasSlice.js +++ b/src/features/cameras/wirelessCamerasSlice.js @@ -16,6 +16,7 @@ const initialState = { errors: null, noneFound: false, }, + isDeleteCameraAlertOpen: false, }; export const wirelessCamerasSlice = createSlice({ @@ -119,6 +120,10 @@ export const wirelessCamerasSlice = createSlice({ state.cameraImageCount.currentCameraId = null; state.cameraImageCount.count = null; }, + + setDeleteCameraAlertStatus: (state, { payload }) => { + state.isDeleteCameraAlertOpen = payload; + }, }, extraReducers: (builder) => { @@ -155,6 +160,8 @@ export const { cameraImageCountStart, cameraImageCountSuccess, cameraImageCountError, + + setDeleteCameraAlertStatus, } = wirelessCamerasSlice.actions; // fetchWirelessCameras thunk @@ -254,5 +261,7 @@ export const selectWirelessCamerasErrors = (state) => state.wirelessCameras.load export const selectCameraImageCount = (state) => state.wirelessCameras.cameraImageCount.count; export const selectCameraImageCountLoading = (state) => state.wirelessCameras.cameraImageCount.isLoading; +export const selectDeleteCameraAlertStatus = (state) => + state.wirelessCameras.isDeleteCameraAlertOpen; export default wirelessCamerasSlice.reducer; diff --git a/src/features/images/DeleteImagesAlert.jsx b/src/features/images/DeleteImagesAlert.jsx index 3fedd2a6..3ddd659a 100644 --- a/src/features/images/DeleteImagesAlert.jsx +++ b/src/features/images/DeleteImagesAlert.jsx @@ -1,5 +1,4 @@ -import React, { useState, useEffect } from 'react'; -import { styled } from '../../theme/stitches.config'; +import React, { useEffect } from 'react'; import { useDispatch, useSelector } from 'react-redux'; import { deleteImages, @@ -19,7 +18,7 @@ import { AlertTitle, } from '../../components/AlertDialog.jsx'; import Button from '../../components/Button.jsx'; -import { red, green } from '@radix-ui/colors'; +import { red } from '@radix-ui/colors'; import { clearDeleteImagesTask, deleteImagesTask, @@ -32,35 +31,7 @@ import { ASYNC_IMAGE_DELETE_BY_FILTER_LIMIT, } from '../../config.js'; import { SimpleSpinner, SpinnerOverlay } from '../../components/Spinner.jsx'; -import * as Progress from '@radix-ui/react-progress'; - -const ProgressBar = styled('div', { - display: 'flex', - alignItems: 'center', - justifyContent: 'space-between', - position: 'absolute', - bottom: 0, - width: '100%', -}); - -const ProgressRoot = styled(Progress.Root, { - overflow: 'hidden', - background: '$backgroundDark', - // borderRadius: '99999px', - width: '100%', - height: '8px', - - /* Fix overflow clipping in Safari */ - /* https://gist.github.com/domske/b66047671c780a238b51c51ffde8d3a0 */ - transform: 'translateZ(0)', -}); - -const ProgressIndicator = styled(Progress.Indicator, { - backgroundColor: green.green9, //sky.sky4, //'$blue600', - width: '100%', - height: '100%', - transition: 'transform 660ms cubic-bezier(0.65, 0, 0.35, 1)', -}); +import { DeleteImagesProgressBar } from '../../components/DeleteImagesProgressBar.jsx'; const DeleteImagesAlert = () => { const dispatch = useDispatch(); @@ -81,9 +52,6 @@ const DeleteImagesAlert = () => { } }, [deleteImagesTaskLoading, dispatch]); - const [estimatedTotalTime, setEstimatedTotalTime] = useState(null); // in seconds - const [elapsedTime, setElapsedTime] = useState(null); - const handleConfirmDelete = () => { if (alertState.deleteImagesAlertByFilter) { // if deleting by filter, always delete using task handler @@ -96,34 +64,8 @@ const DeleteImagesAlert = () => { dispatch(deleteImages(selectedImageIds)); } } - if ( - selectedImages.length > ASYNC_IMAGE_DELETE_BY_ID_LIMIT || - imageCount > ASYNC_IMAGE_DELETE_BY_ID_LIMIT - ) { - // show progress bar if deleting more than 3000 images (approx wait time will be > 10 seconds) - const count = !alertState.deleteImagesAlertByFilter ? selectedImages.length : imageCount; - setEstimatedTotalTime(count * 0.0055); // estimated deletion time per image in seconds - setElapsedTime(0); - } }; - useEffect(() => { - if (estimatedTotalTime) { - const interval = setInterval(() => { - setElapsedTime((prevElapsedTime) => { - if (prevElapsedTime >= estimatedTotalTime) { - clearInterval(interval); - setEstimatedTotalTime(null); - setElapsedTime(null); - return estimatedTotalTime; - } - return prevElapsedTime + 1; - }); - }, 1000); - return () => clearInterval(interval); - } - }, [estimatedTotalTime, elapsedTime]); - const handleCancelDelete = () => { dispatch(setDeleteImagesAlertStatus({ openStatus: false })); dispatch(clearDeleteImagesTask()); @@ -210,17 +152,7 @@ const DeleteImagesAlert = () => { {isSpinnerActive && ( - - - - - + )} {title} From f7e38ee0316f6eceb5823b77a3a0b36f5afbbfc4 Mon Sep 17 00:00:00 2001 From: jue-henry Date: Mon, 16 Dec 2024 17:10:43 -0800 Subject: [PATCH 09/15] clean up --- src/features/cameras/DeleteCameraAlert.jsx | 15 +++++++++++++-- src/features/images/DeleteImagesAlert.jsx | 14 +++++++------- 2 files changed, 20 insertions(+), 9 deletions(-) diff --git a/src/features/cameras/DeleteCameraAlert.jsx b/src/features/cameras/DeleteCameraAlert.jsx index dd46c1a3..1fa227dd 100644 --- a/src/features/cameras/DeleteCameraAlert.jsx +++ b/src/features/cameras/DeleteCameraAlert.jsx @@ -1,5 +1,6 @@ import React, { useState, 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'; @@ -21,7 +22,7 @@ import { AlertContent, AlertTitle, } from '../../components/AlertDialog.jsx'; -import { DeleteImagesProgressBar } from '../../components/DeleteImagesProgressBar.jsx'; +import { DeleteImagesProgressBar } from '../images/DeleteImagesProgressBar.jsx'; const BoldText = styled('span', { fontWeight: '$5', @@ -97,7 +98,17 @@ const DeleteCameraAlert = () => { - diff --git a/src/features/images/DeleteImagesAlert.jsx b/src/features/images/DeleteImagesAlert.jsx index 3ddd659a..96bf09aa 100644 --- a/src/features/images/DeleteImagesAlert.jsx +++ b/src/features/images/DeleteImagesAlert.jsx @@ -1,5 +1,11 @@ import React, { useEffect } from 'react'; import { useDispatch, useSelector } from 'react-redux'; +import { red } from '@radix-ui/colors'; +import { + SYNC_IMAGE_DELETE_LIMIT, + ASYNC_IMAGE_DELETE_BY_ID_LIMIT, + ASYNC_IMAGE_DELETE_BY_FILTER_LIMIT, +} from '../../config.js'; import { deleteImages, selectImagesCountLoading, @@ -18,20 +24,14 @@ import { AlertTitle, } from '../../components/AlertDialog.jsx'; import Button from '../../components/Button.jsx'; -import { red } from '@radix-ui/colors'; import { clearDeleteImagesTask, deleteImagesTask, fetchTask, selectDeleteImagesLoading, } from '../tasks/tasksSlice.js'; -import { - SYNC_IMAGE_DELETE_LIMIT, - ASYNC_IMAGE_DELETE_BY_ID_LIMIT, - ASYNC_IMAGE_DELETE_BY_FILTER_LIMIT, -} from '../../config.js'; import { SimpleSpinner, SpinnerOverlay } from '../../components/Spinner.jsx'; -import { DeleteImagesProgressBar } from '../../components/DeleteImagesProgressBar.jsx'; +import { DeleteImagesProgressBar } from './DeleteImagesProgressBar.jsx'; const DeleteImagesAlert = () => { const dispatch = useDispatch(); From 8d6da73de643edc4cd114447a78cdae03ed906ba Mon Sep 17 00:00:00 2001 From: jue-henry Date: Mon, 16 Dec 2024 17:18:50 -0800 Subject: [PATCH 10/15] fixing proper image count --- src/features/images/DeleteImagesAlert.jsx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/features/images/DeleteImagesAlert.jsx b/src/features/images/DeleteImagesAlert.jsx index 96bf09aa..e69fda23 100644 --- a/src/features/images/DeleteImagesAlert.jsx +++ b/src/features/images/DeleteImagesAlert.jsx @@ -152,7 +152,11 @@ const DeleteImagesAlert = () => { {isSpinnerActive && ( - + )} {title} From 9fbd62f2a7ee3def1c4a9a854921f389c3332370 Mon Sep 17 00:00:00 2001 From: jue-henry Date: Wed, 18 Dec 2024 13:55:45 -0800 Subject: [PATCH 11/15] fixing rendering errors --- src/features/cameras/DeleteCameraAlert.jsx | 25 ++++++++++++------- .../images}/DeleteImagesProgressBar.jsx | 23 +++++++++-------- src/features/tasks/tasksSlice.js | 2 ++ 3 files changed, 30 insertions(+), 20 deletions(-) rename src/{components => features/images}/DeleteImagesProgressBar.jsx (76%) diff --git a/src/features/cameras/DeleteCameraAlert.jsx b/src/features/cameras/DeleteCameraAlert.jsx index 1fa227dd..b708c802 100644 --- a/src/features/cameras/DeleteCameraAlert.jsx +++ b/src/features/cameras/DeleteCameraAlert.jsx @@ -1,10 +1,15 @@ -import React, { useState, useEffect } from 'react'; +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 { deleteCamera, fetchTask, selectDeleteCameraLoading } from '../tasks/tasksSlice.js'; +import { + clearDeleteCameraTask, + deleteCamera, + fetchTask, + selectDeleteCameraLoading, +} from '../tasks/tasksSlice.js'; import { SimpleSpinner, SpinnerOverlay } from '../../components/Spinner.jsx'; import { selectSelectedCamera } from '../projects/projectsSlice.js'; import { @@ -29,7 +34,6 @@ const BoldText = styled('span', { }); const DeleteCameraAlert = () => { - const [queuedForClose, setQueuedForClose] = useState(false); const deleteCameraLoading = useSelector(selectDeleteCameraLoading); const selectedCamera = useSelector(selectSelectedCamera); const imageCount = useSelector(selectCameraImageCount); @@ -44,13 +48,16 @@ const DeleteCameraAlert = () => { }, [imageCount, selectedCamera, dispatch]); useEffect(() => { - if (queuedForClose && !deleteCameraLoading.isLoading) - dispatch(setDeleteCameraAlertStatus(false)); - }, [deleteCameraLoading, queuedForClose]); + if (!deleteCameraLoading.isLoading) dispatch(setDeleteCameraAlertStatus(false)); + }, [deleteCameraLoading]); const handleDeleteCameraSubmit = () => { dispatch(deleteCamera({ cameraId: selectedCamera })); - setQueuedForClose(true); + }; + + const handleCancelDelete = () => { + dispatch(setDeleteCameraAlertStatus(false)); + dispatch(clearDeleteCameraTask()); }; // handle polling for task completion @@ -83,7 +90,7 @@ const DeleteCameraAlert = () => { {ASYNC_IMAGE_DELETE_BY_FILTER_LIMIT} before trying again. We apologize for the inconvenience. - @@ -95,7 +102,7 @@ const DeleteCameraAlert = () => { it, and {imagesText} will be deleted.{' '} This action cannot be undone. - - ) : ( <> - Are you sure you'd like to delete Camera {selectedCamera}? - This will remove the camera from the project, remove all deployments associated with - it, and {imagesText} will be deleted.{' '} - This action cannot be undone. +

+ Are you sure you'd like to delete Camera {selectedCamera}?{' '} + {imageCount === 0 && 'This will remove the Camera and the Deployments '} + {imageCount > 0 && ( + <> + This will remove the Camera, its Deployments, and{' '} + {imageCount > 1 ? 'all' : 'the'}{' '} + + {imageCount} image{imageCount > 1 && 's'} + {' '} + + )} + associated with it from the Project. +

+

+ This action cannot be undone. +

-