From 2df6427454dcef98839502465568c8d6e6374777 Mon Sep 17 00:00:00 2001 From: Nathaniel Rindlaub Date: Thu, 5 Dec 2024 13:39:46 -0800 Subject: [PATCH 1/6] feat(227): enforce deletion limits --- src/config.js | 5 +- src/features/loupe/DeleteImagesAlert.jsx | 75 +++++++++++++++++++++--- 2 files changed, 69 insertions(+), 11 deletions(-) diff --git a/src/config.js b/src/config.js index 700db5a8..bf680155 100644 --- a/src/config.js +++ b/src/config.js @@ -16,8 +16,9 @@ const stage = import.meta.env.VITE_STAGE || process.env.NODE_ENV; export const API_URL = API_URLS[stage]; export const IMAGES_URL = IMAGES_URLS[stage]; export const IMAGE_QUERY_LIMITS = [10, 50, 100]; -export const IMAGE_DELETE_LIMIT = 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_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/loupe/DeleteImagesAlert.jsx b/src/features/loupe/DeleteImagesAlert.jsx index 1edbf998..4604b9bf 100644 --- a/src/features/loupe/DeleteImagesAlert.jsx +++ b/src/features/loupe/DeleteImagesAlert.jsx @@ -2,11 +2,11 @@ import React, { useEffect } from 'react'; import { useDispatch, useSelector } from 'react-redux'; import { deleteImages, - selectImagesCount, selectImagesCountLoading, selectDeleteImagesAlertState, setDeleteImagesAlertStatus, selectImagesLoading, + selectImagesCount, } from '../images/imagesSlice.js'; import { selectActiveFilters } from '../filters/filtersSlice.js'; import { selectSelectedImages } from '../review/reviewSlice.js'; @@ -20,7 +20,11 @@ import { import Button from '../../components/Button.jsx'; import { red } from '@radix-ui/colors'; import { deleteImagesTask, fetchTask, selectDeleteImagesLoading } from '../tasks/tasksSlice.js'; -import { IMAGE_DELETE_LIMIT } from '../../config.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'; const DeleteImagesAlert = () => { @@ -44,9 +48,11 @@ const DeleteImagesAlert = () => { const handleConfirmDelete = () => { if (alertState.deleteImagesAlertByFilter) { + // if deleting by filter, always delete using task handler dispatch(deleteImagesTask({ imageIds: [], filters: filters })); } else { - if (selectedImages.length > IMAGE_DELETE_LIMIT) { + // if deleting by selection of IDs, use task handler if over limit + if (selectedImages.length > SYNC_IMAGE_DELETE_LIMIT) { dispatch(deleteImagesTask({ imageIds: selectedImageIds, filters: null })); } else { dispatch(deleteImages(selectedImageIds)); @@ -58,13 +64,65 @@ const DeleteImagesAlert = () => { dispatch(setDeleteImagesAlertStatus({ openStatus: false })); }; + const deleteByIdLimitExceeded = + !alertState.deleteImagesAlertByFilter && selectedImages.length > ASYNC_IMAGE_DELETE_BY_ID_LIMIT; + const byFilterLimitExceeded = + alertState.deleteImagesAlertByFilter && imageCount > ASYNC_IMAGE_DELETE_BY_FILTER_LIMIT; + const isSpinnerActive = (deleteImagesTaskLoading.isLoading && deleteImagesTaskLoading.taskId) || (alertState.deleteImagesAlertByFilter && imageCountIsLoading.isLoading) || imagesLoading.isLoading; - const filterText = `Are you sure you'd like to delete ${imageCount === 1 ? 'this image' : `these ${imageCount && imageCount.toLocaleString()} images`}?`; - const selectionText = `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 defaultText = ( +
+

This action can not be undone.

+
+ ); + + const deleteByIdLimitAlertTitle = 'Delete Limit Exceeded'; + const deleteByIdLimitAlertText = ( +
+

+ You have selected {selectedImages.length.toLocaleString()} images, which is more than the{' '} + {ASYNC_IMAGE_DELETE_BY_ID_LIMIT.toLocaleString()} image limit Animl supports when deleting + individually-selected images. +

+ {/*TODO: Add a link to the documentation for more information on how to delete images.*/} +

+ Please select fewer images, or use the delete-by-filter option, which can accommodate + deleting up to {ASYNC_IMAGE_DELETE_BY_FILTER_LIMIT.toLocaleString()} images at a time. +

+
+ ); + + const deleteByFilterLimitAlertTitle = 'Delete Limit Exceeded'; + const deleteByFilterLimitAlertText = ( +
+

+ There are {imageCount.toLocaleString()} images that match the currently selected filters, + which is more than the {ASYNC_IMAGE_DELETE_BY_FILTER_LIMIT.toLocaleString()} image limit + Animl supports when deleting images by filter. +

+

+ To delete all of the currently matching images, you may need to apply additional filters to + stay within the limit and perform multiple separate deletion requests. +

+
+ ); + + let title = alertState.deleteImagesAlertByFilter ? filterTitle : selectionTitle; + let text = defaultText; + if (deleteByIdLimitExceeded) { + title = deleteByIdLimitAlertTitle; + text = deleteByIdLimitAlertText; + } else if (byFilterLimitExceeded) { + title = deleteByFilterLimitAlertTitle; + text = deleteByFilterLimitAlertText; + } + return ( @@ -75,16 +133,15 @@ const DeleteImagesAlert = () => { )} - - {alertState.deleteImagesAlertByFilter ? filterText : selectionText} - -

This action can not be undone.

+ {title} + {text}