diff --git a/web/public/locales/en/views/settings.json b/web/public/locales/en/views/settings.json
index edfbb19360..05e272f76f 100644
--- a/web/public/locales/en/views/settings.json
+++ b/web/public/locales/en/views/settings.json
@@ -482,6 +482,8 @@
"disableDesc": "Enable a camera that is currently not visible in the UI and disabled in the configuration. A restart of Frigate is required after enabling.",
"enableSuccess": "Enabled {{cameraName}} in configuration. Restart Frigate to apply the changes.",
"reorderHandle": "Drag to reorder",
+ "saving": "Saving…",
+ "saved": "Saved",
"friendlyName": {
"edit": "Edit camera display name",
"title": "Edit Display Name",
diff --git a/web/src/views/settings/CameraManagementView.tsx b/web/src/views/settings/CameraManagementView.tsx
index 08f6e9d8e4..83f2111c6c 100644
--- a/web/src/views/settings/CameraManagementView.tsx
+++ b/web/src/views/settings/CameraManagementView.tsx
@@ -15,6 +15,7 @@ import CameraEditForm from "@/components/settings/CameraEditForm";
import CameraWizardDialog from "@/components/settings/CameraWizardDialog";
import DeleteCameraDialog from "@/components/overlay/dialog/DeleteCameraDialog";
import {
+ LuCheck,
LuExternalLink,
LuGripVertical,
LuPencil,
@@ -52,6 +53,10 @@ import {
SelectValue,
} from "@/components/ui/select";
+const REORDER_SAVED_INDICATOR_MS = 1500;
+
+type ReorderSaveStatus = "idle" | "saving" | "saved";
+
type CameraManagementViewProps = {
setUnsavedChanges: React.Dispatch>;
profileState?: ProfileState;
@@ -113,6 +118,19 @@ export default function CameraManagementView({
});
}, [enabledCameras]);
+ const [reorderSaveStatus, setReorderSaveStatus] =
+ useState("idle");
+ const reorderSavedTimerRef = useRef | null>(
+ null,
+ );
+ useEffect(() => {
+ return () => {
+ if (reorderSavedTimerRef.current) {
+ clearTimeout(reorderSavedTimerRef.current);
+ }
+ };
+ }, []);
+
const handleReorderDragEnd = useCallback(async () => {
const current = orderedCamerasRef.current;
if (
@@ -127,14 +145,26 @@ export default function CameraManagementView({
cameraUpdates[cam] = { ui: { order: i * 10 } };
});
+ if (reorderSavedTimerRef.current) {
+ clearTimeout(reorderSavedTimerRef.current);
+ reorderSavedTimerRef.current = null;
+ }
+ setReorderSaveStatus("saving");
+
try {
await axios.put("config/set", {
requires_restart: 0,
config_data: { cameras: cameraUpdates },
});
await updateConfig();
+ setReorderSaveStatus("saved");
+ reorderSavedTimerRef.current = setTimeout(() => {
+ setReorderSaveStatus("idle");
+ reorderSavedTimerRef.current = null;
+ }, REORDER_SAVED_INDICATOR_MS);
} catch (error) {
setOrderedCameras(enabledCameras);
+ setReorderSaveStatus("idle");
const errorMessage =
axios.isAxiosError(error) &&
(error.response?.data?.message || error.response?.data?.detail)
@@ -238,22 +268,27 @@ export default function CameraManagementView({
-
- {orderedCameras.map((camera) => (
-
- ))}
-
+
+
+ {orderedCameras.map((camera) => (
+
+ ))}
+
+
+
cameraManagement.streams.enableDesc
@@ -373,6 +408,37 @@ export default function CameraManagementView({
);
}
+type ReorderSaveStatusIndicatorProps = {
+ status: ReorderSaveStatus;
+};
+
+function ReorderSaveStatusIndicator({
+ status,
+}: ReorderSaveStatusIndicatorProps) {
+ const { t } = useTranslation(["views/settings"]);
+ return (
+
+ {status === "saving" && (
+
+ {t("cameraManagement.streams.saving")}
+
+ )}
+ {status === "saved" && (
+
+
+ {t("cameraManagement.streams.saved")}
+
+ )}
+
+ );
+}
+
type EnabledCameraRowProps = {
camera: string;
onConfigChanged: () => Promise;