mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-02-08 20:25:26 +03:00
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
This commit is contained in:
parent
7b11ff1af6
commit
5e8f482bb7
26
web-old/src/icons/Cleanup.jsx
Normal file
26
web-old/src/icons/Cleanup.jsx
Normal file
@ -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 (
|
||||||
|
<svg
|
||||||
|
width="800px"
|
||||||
|
height="800px"
|
||||||
|
viewBox="0 0 1024 1024"
|
||||||
|
className={className}
|
||||||
|
version="1.1"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
fill={fill}
|
||||||
|
stroke={stroke}
|
||||||
|
onClick={onClick}
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M864.453 386.372H604.968V135.834c0-39.533-32.049-71.582-71.582-71.582h-35.791c-39.533 0-71.582 32.049-71.582 71.582v250.538H166.527c-34.592 0-62.634 28.042-62.634 62.634 0 30.327 21.556 55.617 50.181 61.392L85.997 833.761c0 49.417 35.791 90.596 89.478 89.478 53.687-1.118 85.893-53.687 156.91-53.687 172.801 0 397.852 53.687 397.852 53.687 49.417 0 89.478-40.061 89.478-89.478l68.827-326.927c22.634-9.439 38.547-31.772 38.547-57.828-0.001-34.591-28.043-62.634-62.636-62.634zM461.803 153.73c0-29.651 24.036-53.687 53.687-53.687 29.651 0 53.687 24.036 53.687 53.687v232.642H461.803V153.73z m319.456 662.753c-11.092 41.902-31.537 70.965-70.44 70.965 0 0-197.096-49.497-355.544-53.438l41.811-142.707c2.779-9.485-2.658-19.427-12.142-22.207-9.485-2.777-19.426 2.658-22.205 12.142l-45.103 153.939c-55.562 8.478-102.763 52.27-142.161 52.27-43.62 0-67.243-33.993-53.687-70.965 13.556-36.974 74.247-305.459 74.247-305.459l641.576 0.617c-0.001 0.001-45.261 262.941-56.352 304.843z m83.194-340.633H166.527c-14.825 0-26.843-12.019-26.843-26.843 0-14.825 12.019-26.843 26.843-26.843h697.927c14.825 0 26.843 12.019 26.843 26.843s-12.019 26.843-26.844 26.843z"
|
||||||
|
fill={fill}>
|
||||||
|
<title>{title}</title>
|
||||||
|
</path>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default memo(Cleanup);
|
||||||
@ -53,3 +53,33 @@
|
|||||||
display: none;
|
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;
|
||||||
|
}
|
||||||
|
|||||||
@ -17,6 +17,7 @@ import { UploadPlus } from '../icons/UploadPlus';
|
|||||||
import { Clip } from '../icons/Clip';
|
import { Clip } from '../icons/Clip';
|
||||||
import { Zone } from '../icons/Zone';
|
import { Zone } from '../icons/Zone';
|
||||||
import { Camera } from '../icons/Camera';
|
import { Camera } from '../icons/Camera';
|
||||||
|
import { Cleanup } from '../icons/Cleanup';
|
||||||
import { Clock } from '../icons/Clock';
|
import { Clock } from '../icons/Clock';
|
||||||
import { Delete } from '../icons/Delete';
|
import { Delete } from '../icons/Delete';
|
||||||
import { Download } from '../icons/Download';
|
import { Download } from '../icons/Download';
|
||||||
@ -97,6 +98,14 @@ export default function Events({ path, ...props }) {
|
|||||||
showDeleteFavorite: false,
|
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 [showInProgress, setShowInProgress] = useState((props.event || props.cameras || props.labels) == null);
|
||||||
|
|
||||||
const eventsFetcher = useCallback(
|
const eventsFetcher = useCallback(
|
||||||
@ -136,6 +145,7 @@ export default function Events({ path, ...props }) {
|
|||||||
isValidating,
|
isValidating,
|
||||||
} = useSWRInfinite(getKey, eventsFetcher);
|
} = useSWRInfinite(getKey, eventsFetcher);
|
||||||
const mutate = () => {
|
const mutate = () => {
|
||||||
|
console.log("mutating refresh events");
|
||||||
refreshEvents();
|
refreshEvents();
|
||||||
refreshOngoingEvents();
|
refreshOngoingEvents();
|
||||||
};
|
};
|
||||||
@ -293,6 +303,58 @@ export default function Events({ path, ...props }) {
|
|||||||
[path, searchParams, setSearchParams]
|
[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(() => {
|
const onClickFilterSubmitted = useCallback(() => {
|
||||||
if (++searchParams.is_submitted > 1) {
|
if (++searchParams.is_submitted > 1) {
|
||||||
searchParams.is_submitted = -1;
|
searchParams.is_submitted = -1;
|
||||||
@ -449,6 +511,18 @@ export default function Events({ path, ...props }) {
|
|||||||
onClick={() => setState({ ...state, showDatePicker: true })}
|
onClick={() => setState({ ...state, showDatePicker: true })}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className="ml-right batch-actions">
|
||||||
|
<div className="ml-auto flex">
|
||||||
|
<Cleanup
|
||||||
|
className="h-8 w-8 text-red-500 cursor-pointer ml-auto"
|
||||||
|
onClick={(e) => onClearUnretained(e)}
|
||||||
|
fill="#f87171"
|
||||||
|
title="Cleanup Unsaved Events"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
{state.showDownloadMenu && (
|
{state.showDownloadMenu && (
|
||||||
<Menu onDismiss={() => setState({ ...state, showDownloadMenu: false })} relativeTo={downloadButton}>
|
<Menu onDismiss={() => setState({ ...state, showDownloadMenu: false })} relativeTo={downloadButton}>
|
||||||
@ -658,6 +732,76 @@ export default function Events({ path, ...props }) {
|
|||||||
</div>
|
</div>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
)}
|
)}
|
||||||
|
{clearUnretainedState.showConfirmation && (
|
||||||
|
<Dialog>
|
||||||
|
<div className="p-4">
|
||||||
|
<Heading size="lg">Delete {clearUnretainedState.deletableEventList.length} unsaved Events?</Heading>
|
||||||
|
<p className="mb-2">Confirm deletion of all unsaved events currently on display?</p>
|
||||||
|
{
|
||||||
|
clearUnretainedState.favoriteCount > 0 ? (
|
||||||
|
<p className="mb-2">{clearUnretainedState.favoriteCount} saved events will not be deleted.</p>
|
||||||
|
) : (
|
||||||
|
<p className="mb-2" style="color: red;">This selection has no saved events!!</p>
|
||||||
|
)}
|
||||||
|
<p className="mb-2">Events not loaded to the web UI and any ongoing events are also not deleted.</p>
|
||||||
|
</div>
|
||||||
|
<div className="p-2 flex justify-start flex-row-reverse space-x-2">
|
||||||
|
<Button
|
||||||
|
className="ml-2"
|
||||||
|
onClick={() => setClearUnretainedState({ ...state, deletableEventList: clearUnretainedState.deletableEventList, favoriteCount: clearUnretainedState.favoriteCount, showConfirmation: false, showFeedback: false, showProgress: false })}
|
||||||
|
type="text"
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
className="ml-2"
|
||||||
|
color="red"
|
||||||
|
onClick={async (e) => {
|
||||||
|
// const delay = ms => new Promise(res => setTimeout(res, ms));
|
||||||
|
setClearUnretainedState({ ...state, deletableEventList: clearUnretainedState.deletableEventList, favoriteCount: clearUnretainedState.favoriteCount, showConfirmation: false, showFeedback: false, showProgress: true });
|
||||||
|
for (let id of clearUnretainedState.deletableEventList) {
|
||||||
|
console.log("mock deleting event: ", id);
|
||||||
|
// await delay(20000);
|
||||||
|
await onDelete(e, id, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
setClearUnretainedState({ ...state, deletableEventList: clearUnretainedState.deletableEventList, favoriteCount: clearUnretainedState.favoriteCount, showConfirmation: false, showFeedback: true, showProgress: false });
|
||||||
|
}}
|
||||||
|
type="text"
|
||||||
|
>
|
||||||
|
Delete
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</Dialog>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{clearUnretainedState.showProgress && (
|
||||||
|
<Dialog>
|
||||||
|
<div className="p-4">
|
||||||
|
<Heading size="lg">Deleting {clearUnretainedState.deletableEventList.length} unsaved events</Heading>
|
||||||
|
<div class="loader"></div>
|
||||||
|
</div>
|
||||||
|
</Dialog>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{clearUnretainedState.showFeedback && (
|
||||||
|
<Dialog>
|
||||||
|
<div className="p-4">
|
||||||
|
<Heading size="lg">{clearUnretainedState.deletableEventList.length} unsaved events were deleted.</Heading>
|
||||||
|
<p className="mb-2">{clearUnretainedState.favoriteCount} saved events were kept.</p>
|
||||||
|
</div>
|
||||||
|
<div className="p-2 flex justify-start flex-row-reverse space-x-2">
|
||||||
|
<Button
|
||||||
|
className="ml-2"
|
||||||
|
onClick={() => setClearUnretainedState({ ...state, showFeedback: false })}
|
||||||
|
type="text"
|
||||||
|
>
|
||||||
|
Ok
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</Dialog>
|
||||||
|
)}
|
||||||
|
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
{ongoingEvents ? (
|
{ongoingEvents ? (
|
||||||
<div>
|
<div>
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user