From 5e8f482bb7318c83c50ecb32f6531f0c766e4b15 Mon Sep 17 00:00:00 2001 From: chamila Date: Mon, 26 Feb 2024 23:28:17 +1300 Subject: [PATCH] Add PoC implementation of batch delete The batch delete goes through all the already displayed Events pages and deletes any Event that is not marked as favorite/saved. It doesn't query the database to get the full list of Events, as the user should be able to clearly understand which Events will be cleaned up. After the list of Events to be deleted is identified, it will prompt the user to confirm the deletion of the number of Events. It will then show a spinner until the Events are deleted and then show a feedback dialog mentioning the number of Events deleted. The button for the Cleanup operation is put on the same row as the filters as adding a new row for a single button seemed ugly. It's marked to be different with a border, since the Cleanup operation is not a filtering option. The SVG was downloaded from [1] and it's licensed as Public Domain [2]. 1 - https://www.svgrepo.com/svg/494112/clean-up 2 - https://www.svgrepo.com/page/licensing/#CC0 --- web-old/src/icons/Cleanup.jsx | 26 ++++++ web-old/src/index.css | 30 +++++++ web-old/src/routes/Events.jsx | 144 ++++++++++++++++++++++++++++++++++ 3 files changed, 200 insertions(+) create mode 100644 web-old/src/icons/Cleanup.jsx diff --git a/web-old/src/icons/Cleanup.jsx b/web-old/src/icons/Cleanup.jsx new file mode 100644 index 000000000..b8e7a454f --- /dev/null +++ b/web-old/src/icons/Cleanup.jsx @@ -0,0 +1,26 @@ +import { h } from 'preact'; +import { memo } from 'preact/compat'; + +export function Cleanup({ className = 'h-6 w-6', stroke = 'currentColor', fill = 'none', title = "", onClick = () => {} }) { + return ( + + + {title} + + + ); +} + +export default memo(Cleanup); diff --git a/web-old/src/index.css b/web-old/src/index.css index f45694699..3999783d9 100644 --- a/web-old/src/index.css +++ b/web-old/src/index.css @@ -53,3 +53,33 @@ display: none; } } + +/* + loader used for cleanup operation progression +*/ +.loader { + width: 50px; + padding: 8px; + aspect-ratio: 1; + border-radius: 50%; + background: #fff; + --_m: + conic-gradient(#0000 10%,#000), + linear-gradient(#000 0 0) content-box; + -webkit-mask: var(--_m); + mask: var(--_m); + -webkit-mask-composite: source-out; + mask-composite: subtract; + animation: l3 1s infinite linear; + margin: auto; +} +@keyframes l3 {to{transform: rotate(1turn)}} + +.batch-actions { + border: 1px solid #fff; + border-radius: 6px; +} + +.batch-actions div { + padding: 3px; +} diff --git a/web-old/src/routes/Events.jsx b/web-old/src/routes/Events.jsx index 0777829a8..f91d90b71 100644 --- a/web-old/src/routes/Events.jsx +++ b/web-old/src/routes/Events.jsx @@ -17,6 +17,7 @@ import { UploadPlus } from '../icons/UploadPlus'; import { Clip } from '../icons/Clip'; import { Zone } from '../icons/Zone'; import { Camera } from '../icons/Camera'; +import { Cleanup } from '../icons/Cleanup'; import { Clock } from '../icons/Clock'; import { Delete } from '../icons/Delete'; import { Download } from '../icons/Download'; @@ -97,6 +98,14 @@ export default function Events({ path, ...props }) { showDeleteFavorite: false, }); + const [clearUnretainedState, setClearUnretainedState] = useState({ + deletableEventList: [], + favoriteCount: 0, + showConfirmation: false, + showProgress: false, + showFeedback: false, + }); + const [showInProgress, setShowInProgress] = useState((props.event || props.cameras || props.labels) == null); const eventsFetcher = useCallback( @@ -136,6 +145,7 @@ export default function Events({ path, ...props }) { isValidating, } = useSWRInfinite(getKey, eventsFetcher); const mutate = () => { + console.log("mutating refresh events"); refreshEvents(); refreshOngoingEvents(); }; @@ -293,6 +303,58 @@ export default function Events({ path, ...props }) { [path, searchParams, setSearchParams] ); + /** + * Invoked when the Cleanup button is clicked to batch delete all unsaved + * Events. + * + * Only iterates through the currently loaded Events in the eventPages array. + * This is to avoid user confusion that could result in deleting Events that + * are not displayed already. + * + * Blocks on loading the newest page if eventPages isn't loaded already. + * + */ + const onClearUnretained = useCallback( + async (e) => { + e.stopPropagation(); + console.log("clear unretained button clicked"); + if (!eventPages) { + // unless output like this, eventPages is undefined. Not sure, I don't know web + // console.log(eventPages); + // eventPages?.map((page, i) => { + // console.log("found ", page.length, " events in page ", i); + // }); + // } else { + console.log("refreshing events"); + // console.debug(eventPages); + // const refreshedEvents = eventsFetcher('events'. searchParams).then((res) => res.data); + // console.log("refreshed: ", refreshedEvents); + await mutate(['events', searchParams]); + } + + let favorites = []; + let deletables = []; + eventPages?.map((page, i) => { + for (let ev of page) { + if (ev.retain_indefinitely) { + favorites.push(ev.id); + } else { + console.log("adding deletable event id: ", ev.id); + deletables.push(ev.id); + } + } + // console.log("found ", page.length, " events in page ", i); + }); + + console.log("found ", favorites.length, " favorites"); + console.log("found ", deletables.length, " events to clear"); + // console.log("eventPages is undefined"); + + setClearUnretainedState({...state, deletableEventList: deletables, favoriteCount: favorites.length, showConfirmation: true, showFeedback: false, showProgress: false}); + }, + [eventPages, mutate, searchParams, setClearUnretainedState] + ); + const onClickFilterSubmitted = useCallback(() => { if (++searchParams.is_submitted > 1) { searchParams.is_submitted = -1; @@ -449,6 +511,18 @@ export default function Events({ path, ...props }) { onClick={() => setState({ ...state, showDatePicker: true })} /> + +
+
+ onClearUnretained(e)} + fill="#f87171" + title="Cleanup Unsaved Events" + /> +
+
+ {state.showDownloadMenu && ( setState({ ...state, showDownloadMenu: false })} relativeTo={downloadButton}> @@ -658,6 +732,76 @@ export default function Events({ path, ...props }) { )} + {clearUnretainedState.showConfirmation && ( + +
+ Delete {clearUnretainedState.deletableEventList.length} unsaved Events? +

Confirm deletion of all unsaved events currently on display?

+ { + clearUnretainedState.favoriteCount > 0 ? ( +

{clearUnretainedState.favoriteCount} saved events will not be deleted.

+ ) : ( +

This selection has no saved events!!

+ )} +

Events not loaded to the web UI and any ongoing events are also not deleted.

+
+
+ + +
+
+ )} + + {clearUnretainedState.showProgress && ( + +
+ Deleting {clearUnretainedState.deletableEventList.length} unsaved events +
+
+
+ )} + + {clearUnretainedState.showFeedback && ( + +
+ {clearUnretainedState.deletableEventList.length} unsaved events were deleted. +

{clearUnretainedState.favoriteCount} saved events were kept.

+
+
+ +
+
+ )} +
{ongoingEvents ? (