enforce atomic config update in the frontend

This commit is contained in:
Josh Hawkins 2026-01-16 08:28:13 -06:00
parent 1e061538a1
commit 7df7330eae
2 changed files with 75 additions and 15 deletions

View File

@ -33,6 +33,7 @@ import IconWrapper from "../ui/icon-wrapper";
import { buttonVariants } from "../ui/button"; import { buttonVariants } from "../ui/button";
import { Trans, useTranslation } from "react-i18next"; import { Trans, useTranslation } from "react-i18next";
import ActivityIndicator from "../indicators/activity-indicator"; import ActivityIndicator from "../indicators/activity-indicator";
import { cn } from "@/lib/utils";
type PolygonItemProps = { type PolygonItemProps = {
polygon: Polygon; polygon: Polygon;
@ -42,6 +43,10 @@ type PolygonItemProps = {
setActivePolygonIndex: (index: number | undefined) => void; setActivePolygonIndex: (index: number | undefined) => void;
setEditPane: (type: PolygonType) => void; setEditPane: (type: PolygonType) => void;
handleCopyCoordinates: (index: number) => void; handleCopyCoordinates: (index: number) => void;
isLoading: boolean;
setIsLoading: (loading: boolean) => void;
loadingPolygonIndex: number | undefined;
setLoadingPolygonIndex: (index: number | undefined) => void;
}; };
export default function PolygonItem({ export default function PolygonItem({
@ -52,12 +57,15 @@ export default function PolygonItem({
setActivePolygonIndex, setActivePolygonIndex,
setEditPane, setEditPane,
handleCopyCoordinates, handleCopyCoordinates,
isLoading,
setIsLoading,
loadingPolygonIndex,
setLoadingPolygonIndex,
}: PolygonItemProps) { }: PolygonItemProps) {
const { t } = useTranslation("views/settings"); const { t } = useTranslation("views/settings");
const { data: config, mutate: updateConfig } = const { data: config, mutate: updateConfig } =
useSWR<FrigateConfig>("config"); useSWR<FrigateConfig>("config");
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false); const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
const [isLoading, setIsLoading] = useState(false);
const cameraConfig = useMemo(() => { const cameraConfig = useMemo(() => {
if (polygon?.camera && config) { if (polygon?.camera && config) {
@ -89,6 +97,7 @@ export default function PolygonItem({
: polygon.type; : polygon.type;
setIsLoading(true); setIsLoading(true);
setLoadingPolygonIndex(index);
if (polygon.type === "zone") { if (polygon.type === "zone") {
// Zones use query string format // Zones use query string format
@ -233,9 +242,17 @@ export default function PolygonItem({
}) })
.finally(() => { .finally(() => {
setIsLoading(false); setIsLoading(false);
setLoadingPolygonIndex(undefined);
}); });
}, },
[updateConfig, cameraConfig, t], [
updateConfig,
cameraConfig,
t,
setIsLoading,
index,
setLoadingPolygonIndex,
],
); );
const handleDelete = () => { const handleDelete = () => {
@ -261,6 +278,7 @@ export default function PolygonItem({
: polygon.type; : polygon.type;
setIsLoading(true); setIsLoading(true);
setLoadingPolygonIndex(index);
if (polygon.type === "zone") { if (polygon.type === "zone") {
// Zones use query string format // Zones use query string format
@ -390,9 +408,18 @@ export default function PolygonItem({
}) })
.finally(() => { .finally(() => {
setIsLoading(false); setIsLoading(false);
setLoadingPolygonIndex(undefined);
}); });
}, },
[updateConfig, cameraConfig, t, polygon], [
updateConfig,
cameraConfig,
t,
polygon,
setIsLoading,
index,
setLoadingPolygonIndex,
],
); );
return ( return (
@ -420,7 +447,7 @@ export default function PolygonItem({
}`} }`}
> >
{PolygonItemIcon && {PolygonItemIcon &&
(isLoading ? ( (isLoading && loadingPolygonIndex === index ? (
<div className="mr-2"> <div className="mr-2">
<ActivityIndicator className="size-5" /> <ActivityIndicator className="size-5" />
</div> </div>
@ -431,7 +458,7 @@ export default function PolygonItem({
type="button" type="button"
onClick={handleToggleEnabled} onClick={handleToggleEnabled}
disabled={isLoading} disabled={isLoading}
className="mr-2 cursor-pointer border-none bg-transparent p-0 transition-opacity hover:opacity-70" className="mr-2 cursor-pointer border-none bg-transparent p-0 transition-opacity hover:opacity-70 disabled:cursor-not-allowed disabled:opacity-50"
> >
<PolygonItemIcon <PolygonItemIcon
className="size-5" className="size-5"
@ -509,6 +536,7 @@ export default function PolygonItem({
<DropdownMenuContent> <DropdownMenuContent>
<DropdownMenuItem <DropdownMenuItem
aria-label={t("button.edit", { ns: "common" })} aria-label={t("button.edit", { ns: "common" })}
disabled={isLoading}
onClick={() => { onClick={() => {
setActivePolygonIndex(index); setActivePolygonIndex(index);
setEditPane(polygon.type); setEditPane(polygon.type);
@ -518,6 +546,7 @@ export default function PolygonItem({
</DropdownMenuItem> </DropdownMenuItem>
<DropdownMenuItem <DropdownMenuItem
aria-label={t("button.copy", { ns: "common" })} aria-label={t("button.copy", { ns: "common" })}
disabled={isLoading}
onClick={() => handleCopyCoordinates(index)} onClick={() => handleCopyCoordinates(index)}
> >
{t("button.copy", { ns: "common" })} {t("button.copy", { ns: "common" })}
@ -539,10 +568,17 @@ export default function PolygonItem({
<TooltipTrigger asChild> <TooltipTrigger asChild>
<IconWrapper <IconWrapper
icon={LuPencil} icon={LuPencil}
className={`size-[15px] cursor-pointer ${hoveredPolygonIndex === index && "text-primary-variant"}`} disabled={isLoading}
className={cn(
"size-[15px] cursor-pointer",
hoveredPolygonIndex === index && "text-primary-variant",
isLoading && "cursor-not-allowed opacity-50",
)}
onClick={() => { onClick={() => {
setActivePolygonIndex(index); if (!isLoading) {
setEditPane(polygon.type); setActivePolygonIndex(index);
setEditPane(polygon.type);
}
}} }}
/> />
</TooltipTrigger> </TooltipTrigger>
@ -555,10 +591,16 @@ export default function PolygonItem({
<TooltipTrigger asChild> <TooltipTrigger asChild>
<IconWrapper <IconWrapper
icon={LuCopy} icon={LuCopy}
className={`size-[15px] cursor-pointer ${ className={cn(
hoveredPolygonIndex === index && "text-primary-variant" "size-[15px] cursor-pointer",
}`} hoveredPolygonIndex === index && "text-primary-variant",
onClick={() => handleCopyCoordinates(index)} isLoading && "cursor-not-allowed opacity-50",
)}
onClick={() => {
if (!isLoading) {
handleCopyCoordinates(index);
}
}}
/> />
</TooltipTrigger> </TooltipTrigger>
<TooltipContent> <TooltipContent>
@ -570,10 +612,13 @@ export default function PolygonItem({
<TooltipTrigger asChild> <TooltipTrigger asChild>
<IconWrapper <IconWrapper
icon={HiTrash} icon={HiTrash}
className={`size-[15px] cursor-pointer ${ disabled={isLoading}
className={cn(
"size-[15px] cursor-pointer",
hoveredPolygonIndex === index && hoveredPolygonIndex === index &&
"fill-primary-variant text-primary-variant" "fill-primary-variant text-primary-variant",
}`} isLoading && "cursor-not-allowed opacity-50",
)}
onClick={() => !isLoading && setDeleteDialogOpen(true)} onClick={() => !isLoading && setDeleteDialogOpen(true)}
/> />
</TooltipTrigger> </TooltipTrigger>

View File

@ -53,6 +53,9 @@ export default function MasksAndZonesView({
const [allPolygons, setAllPolygons] = useState<Polygon[]>([]); const [allPolygons, setAllPolygons] = useState<Polygon[]>([]);
const [editingPolygons, setEditingPolygons] = useState<Polygon[]>([]); const [editingPolygons, setEditingPolygons] = useState<Polygon[]>([]);
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
const [loadingPolygonIndex, setLoadingPolygonIndex] = useState<
number | undefined
>(undefined);
const [activePolygonIndex, setActivePolygonIndex] = useState< const [activePolygonIndex, setActivePolygonIndex] = useState<
number | undefined number | undefined
>(undefined); >(undefined);
@ -538,6 +541,10 @@ export default function MasksAndZonesView({
setActivePolygonIndex={setActivePolygonIndex} setActivePolygonIndex={setActivePolygonIndex}
setEditPane={setEditPane} setEditPane={setEditPane}
handleCopyCoordinates={handleCopyCoordinates} handleCopyCoordinates={handleCopyCoordinates}
isLoading={isLoading}
setIsLoading={setIsLoading}
loadingPolygonIndex={loadingPolygonIndex}
setLoadingPolygonIndex={setLoadingPolygonIndex}
/> />
))} ))}
</div> </div>
@ -608,6 +615,10 @@ export default function MasksAndZonesView({
setActivePolygonIndex={setActivePolygonIndex} setActivePolygonIndex={setActivePolygonIndex}
setEditPane={setEditPane} setEditPane={setEditPane}
handleCopyCoordinates={handleCopyCoordinates} handleCopyCoordinates={handleCopyCoordinates}
isLoading={isLoading}
setIsLoading={setIsLoading}
loadingPolygonIndex={loadingPolygonIndex}
setLoadingPolygonIndex={setLoadingPolygonIndex}
/> />
))} ))}
</div> </div>
@ -678,6 +689,10 @@ export default function MasksAndZonesView({
setActivePolygonIndex={setActivePolygonIndex} setActivePolygonIndex={setActivePolygonIndex}
setEditPane={setEditPane} setEditPane={setEditPane}
handleCopyCoordinates={handleCopyCoordinates} handleCopyCoordinates={handleCopyCoordinates}
isLoading={isLoading}
setIsLoading={setIsLoading}
loadingPolygonIndex={loadingPolygonIndex}
setLoadingPolygonIndex={setLoadingPolygonIndex}
/> />
))} ))}
</div> </div>