diff --git a/web/src/components/classification/wizard/Step2StateArea.tsx b/web/src/components/classification/wizard/Step2StateArea.tsx index e768b1d97..6ca265399 100644 --- a/web/src/components/classification/wizard/Step2StateArea.tsx +++ b/web/src/components/classification/wizard/Step2StateArea.tsx @@ -64,7 +64,6 @@ export default function Step2StateArea({ const selectedCameraNames = cameraAreas.map((ca) => ca.camera); return Object.entries(config.cameras) - .sort() .filter( ([name, cam]) => cam.enabled && @@ -72,6 +71,7 @@ export default function Step2StateArea({ !isReplayCamera(name) && !selectedCameraNames.includes(name), ) + .sort(([, a], [, b]) => a.ui.order - b.ui.order) .map(([name]) => ({ name, displayName: resolveCameraName(config, name), diff --git a/web/src/components/filter/ExportFilterGroup.tsx b/web/src/components/filter/ExportFilterGroup.tsx index c5fe4f33c..9b1cfd7a7 100644 --- a/web/src/components/filter/ExportFilterGroup.tsx +++ b/web/src/components/filter/ExportFilterGroup.tsx @@ -29,9 +29,13 @@ export default function ExportFilterGroup({ const filterValues = useMemo( () => ({ - cameras: allowedCameras, + cameras: [...allowedCameras].sort( + (a, b) => + (config?.cameras[a]?.ui?.order ?? 0) - + (config?.cameras[b]?.ui?.order ?? 0), + ), }), - [allowedCameras], + [config, allowedCameras], ); const groups = useMemo(() => { diff --git a/web/src/components/filter/SearchFilterGroup.tsx b/web/src/components/filter/SearchFilterGroup.tsx index fe9a70e18..a9fd0276a 100644 --- a/web/src/components/filter/SearchFilterGroup.tsx +++ b/web/src/components/filter/SearchFilterGroup.tsx @@ -127,12 +127,16 @@ export default function SearchFilterGroup({ const filterValues = useMemo( () => ({ - cameras: allowedCameras, + cameras: [...allowedCameras].sort( + (a, b) => + (config?.cameras[a]?.ui?.order ?? 0) - + (config?.cameras[b]?.ui?.order ?? 0), + ), labels: Object.values(allLabels || {}), zones: Object.values(allZones || {}), search_type: ["thumbnail", "description"] as SearchSource[], }), - [allLabels, allZones, allowedCameras], + [config, allLabels, allZones, allowedCameras], ); const availableSortTypes = useMemo(() => { diff --git a/web/src/components/overlay/CreateRoleDialog.tsx b/web/src/components/overlay/CreateRoleDialog.tsx index fef205ea4..317f635e3 100644 --- a/web/src/components/overlay/CreateRoleDialog.tsx +++ b/web/src/components/overlay/CreateRoleDialog.tsx @@ -53,9 +53,13 @@ export default function CreateRoleDialog({ const { t } = useTranslation(["views/settings"]); const [isLoading, setIsLoading] = useState(false); - const cameras = Object.keys(config.cameras || {}).filter( - (name) => !isReplayCamera(name), - ); + const cameras = Object.keys(config.cameras || {}) + .filter((name) => !isReplayCamera(name)) + .sort( + (a, b) => + (config.cameras[a]?.ui?.order ?? 0) - + (config.cameras[b]?.ui?.order ?? 0), + ); const existingRoles = Object.keys(config.auth?.roles || {}); diff --git a/web/src/components/overlay/EditRoleCamerasDialog.tsx b/web/src/components/overlay/EditRoleCamerasDialog.tsx index cfa84db81..1c70b6d10 100644 --- a/web/src/components/overlay/EditRoleCamerasDialog.tsx +++ b/web/src/components/overlay/EditRoleCamerasDialog.tsx @@ -47,9 +47,13 @@ export default function EditRoleCamerasDialog({ const { t } = useTranslation(["views/settings"]); const [isLoading, setIsLoading] = useState(false); - const cameras = Object.keys(config.cameras || {}).filter( - (name) => !isReplayCamera(name), - ); + const cameras = Object.keys(config.cameras || {}) + .filter((name) => !isReplayCamera(name)) + .sort( + (a, b) => + (config.cameras[a]?.ui?.order ?? 0) - + (config.cameras[b]?.ui?.order ?? 0), + ); const formSchema = z.object({ cameras: z diff --git a/web/src/components/overlay/detail/ObjectPathPlotter.tsx b/web/src/components/overlay/detail/ObjectPathPlotter.tsx index aa65ae409..b926f28ed 100644 --- a/web/src/components/overlay/detail/ObjectPathPlotter.tsx +++ b/web/src/components/overlay/detail/ObjectPathPlotter.tsx @@ -39,7 +39,9 @@ export default function ObjectPathPlotter() { const cameraNames = useMemo(() => { if (!config) return []; - return Object.keys(config.cameras).filter((name) => !isReplayCamera(name)); + return Object.keys(config.cameras) + .filter((name) => !isReplayCamera(name)) + .sort((a, b) => config.cameras[a].ui.order - config.cameras[b].ui.order); }, [config]); useEffect(() => { diff --git a/web/src/components/settings/CloneCameraDialog.tsx b/web/src/components/settings/CloneCameraDialog.tsx index 9b85a3fbd..d475339cc 100644 --- a/web/src/components/settings/CloneCameraDialog.tsx +++ b/web/src/components/settings/CloneCameraDialog.tsx @@ -96,7 +96,7 @@ export default function CloneCameraDialog({ if (!config) return []; return Object.keys(config.cameras) .filter((c) => !isReplayCamera(c)) - .sort(); + .sort((a, b) => config.cameras[a].ui.order - config.cameras[b].ui.order); }, [config]); const formSchema = useMemo(() => { @@ -176,7 +176,7 @@ export default function CloneCameraDialog({ if (!config) return []; return Object.keys(config.cameras) .filter((c) => c !== sourceCamera && !isReplayCamera(c)) - .sort(); + .sort((a, b) => config.cameras[a].ui.order - config.cameras[b].ui.order); }, [config, sourceCamera]); const srcCfg = config?.cameras?.[sourceCamera]; diff --git a/web/src/components/ws/WsMessageFeed.tsx b/web/src/components/ws/WsMessageFeed.tsx index 47a3b41ae..45e015833 100644 --- a/web/src/components/ws/WsMessageFeed.tsx +++ b/web/src/components/ws/WsMessageFeed.tsx @@ -169,7 +169,7 @@ export default function WsMessageFeed({ const cam = config.cameras[name]; return !isReplayCamera(name) && cam.enabled_in_config; }) - .sort(); + .sort((a, b) => config.cameras[a].ui.order - config.cameras[b].ui.order); }, [config]); const filteredMessages = useMemo(() => { diff --git a/web/src/pages/MotionSearch.tsx b/web/src/pages/MotionSearch.tsx index c1651b72e..d8143e6ee 100644 --- a/web/src/pages/MotionSearch.tsx +++ b/web/src/pages/MotionSearch.tsx @@ -30,9 +30,9 @@ export default function MotionSearch() { const cameras = useMemo(() => { if (!config?.cameras) return []; - return Object.keys(config.cameras).filter((cam) => - allowedCameras.includes(cam), - ); + return Object.keys(config.cameras) + .filter((cam) => allowedCameras.includes(cam)) + .sort((a, b) => config.cameras[a].ui.order - config.cameras[b].ui.order); }, [config?.cameras, allowedCameras]); // Selected camera state diff --git a/web/src/views/settings/CameraManagementView.tsx b/web/src/views/settings/CameraManagementView.tsx index a51f3374a..b9c7fd262 100644 --- a/web/src/views/settings/CameraManagementView.tsx +++ b/web/src/views/settings/CameraManagementView.tsx @@ -208,7 +208,12 @@ export default function CameraManagementView({ !config.cameras[camera].enabled_in_config && !isReplayCamera(camera), ) - .sort(); + .sort((a, b) => { + const orderA = config.cameras[a].ui?.order ?? 0; + const orderB = config.cameras[b].ui?.order ?? 0; + if (orderA !== orderB) return orderA - orderB; + return a.localeCompare(b); + }); } return []; }, [config]); @@ -217,7 +222,12 @@ export default function CameraManagementView({ if (config) { return Object.keys(config.cameras) .filter((camera) => !isReplayCamera(camera)) - .sort(); + .sort((a, b) => { + const orderA = config.cameras[a].ui?.order ?? 0; + const orderB = config.cameras[b].ui?.order ?? 0; + if (orderA !== orderB) return orderA - orderB; + return a.localeCompare(b); + }); } return []; }, [config]); diff --git a/web/src/views/settings/FrigatePlusSettingsView.tsx b/web/src/views/settings/FrigatePlusSettingsView.tsx index 7bc60fa1f..c6164b35b 100644 --- a/web/src/views/settings/FrigatePlusSettingsView.tsx +++ b/web/src/views/settings/FrigatePlusSettingsView.tsx @@ -140,6 +140,7 @@ export default function FrigatePlusSettingsView(_props: SettingsPageProps) { {Object.entries(config.cameras) .filter(([name]) => !isReplayCamera(name)) + .sort(([, a], [, b]) => a.ui.order - b.ui.order) .map(([name, camera]) => ( > = {}; const cameras = Object.keys(config.cameras) .filter((name) => !isReplayCamera(name)) - .sort(); + .sort((a, b) => config.cameras[a].ui.order - config.cameras[b].ui.order); for (const profile of allProfileNames) { data[profile] = {}; @@ -474,7 +474,11 @@ export default function ProfilesView({ const color = getProfileColor(profile, allProfileNames); const isActive = activeProfile === profile; const cameraData = profileOverviewData[profile] ?? {}; - const cameras = Object.keys(cameraData).sort(); + const cameras = Object.keys(cameraData).sort( + (a, b) => + (config?.cameras[a]?.ui?.order ?? 0) - + (config?.cameras[b]?.ui?.order ?? 0), + ); const isExpanded = expandedProfiles.has(profile); return ( diff --git a/web/src/views/system/CameraMetrics.tsx b/web/src/views/system/CameraMetrics.tsx index 340953b41..1f40fded0 100644 --- a/web/src/views/system/CameraMetrics.tsx +++ b/web/src/views/system/CameraMetrics.tsx @@ -316,114 +316,114 @@ export default function CameraMetrics({
{config && - Object.values(config.cameras).map((camera) => { - if (camera.enabled && !isReplayCamera(camera.name)) { - return ( - - {probeCameraName == camera.name && ( - - )} -
-
-
-
- -
- {statsHistory.length > 0 && - statsHistory[statsHistory.length - 1]?.cameras[ - camera.name - ] && ( - - )} -
- - - { - setShowCameraInfoDialog(true); - setProbeCameraName(camera.name); - }} - /> - - - {t("cameras.info.tips.title")} - - -
-
- {Object.keys(cameraCpuSeries).includes(camera.name) ? ( -
-
CPU
- -
- ) : ( - - )} - {Object.keys(cameraFpsSeries).includes(camera.name) ? ( -
-
- {t("cameras.framesAndDetections")} + Object.values(config.cameras) + .sort((a, b) => a.ui.order - b.ui.order) + .map((camera) => { + if (camera.enabled && !isReplayCamera(camera.name)) { + return ( + + {probeCameraName == camera.name && ( + + )} +
+
+
+
+
- 0 && + statsHistory[statsHistory.length - 1]?.cameras[ + camera.name + ] && ( + )} - isActive={isActive} - />
- ) : ( - - )} + + + { + setShowCameraInfoDialog(true); + setProbeCameraName(camera.name); + }} + /> + + + {t("cameras.info.tips.title")} + + +
+
+ {Object.keys(cameraCpuSeries).includes(camera.name) ? ( +
+
CPU
+ +
+ ) : ( + + )} + {Object.keys(cameraFpsSeries).includes(camera.name) ? ( +
+
+ {t("cameras.framesAndDetections")} +
+ +
+ ) : ( + + )} +
-
- - ); - } + + ); + } - return null; - })} + return null; + })}
);