mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-07-07 20:41:13 +03:00
sort cameras by UI order in various components for consistent display
This commit is contained in:
parent
105d1e8885
commit
0cb3c720ca
@ -64,7 +64,6 @@ export default function Step2StateArea({
|
|||||||
|
|
||||||
const selectedCameraNames = cameraAreas.map((ca) => ca.camera);
|
const selectedCameraNames = cameraAreas.map((ca) => ca.camera);
|
||||||
return Object.entries(config.cameras)
|
return Object.entries(config.cameras)
|
||||||
.sort()
|
|
||||||
.filter(
|
.filter(
|
||||||
([name, cam]) =>
|
([name, cam]) =>
|
||||||
cam.enabled &&
|
cam.enabled &&
|
||||||
@ -72,6 +71,7 @@ export default function Step2StateArea({
|
|||||||
!isReplayCamera(name) &&
|
!isReplayCamera(name) &&
|
||||||
!selectedCameraNames.includes(name),
|
!selectedCameraNames.includes(name),
|
||||||
)
|
)
|
||||||
|
.sort(([, a], [, b]) => a.ui.order - b.ui.order)
|
||||||
.map(([name]) => ({
|
.map(([name]) => ({
|
||||||
name,
|
name,
|
||||||
displayName: resolveCameraName(config, name),
|
displayName: resolveCameraName(config, name),
|
||||||
|
|||||||
@ -29,9 +29,13 @@ export default function ExportFilterGroup({
|
|||||||
|
|
||||||
const filterValues = useMemo(
|
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(() => {
|
const groups = useMemo(() => {
|
||||||
|
|||||||
@ -127,12 +127,16 @@ export default function SearchFilterGroup({
|
|||||||
|
|
||||||
const filterValues = useMemo(
|
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 || {}),
|
labels: Object.values(allLabels || {}),
|
||||||
zones: Object.values(allZones || {}),
|
zones: Object.values(allZones || {}),
|
||||||
search_type: ["thumbnail", "description"] as SearchSource[],
|
search_type: ["thumbnail", "description"] as SearchSource[],
|
||||||
}),
|
}),
|
||||||
[allLabels, allZones, allowedCameras],
|
[config, allLabels, allZones, allowedCameras],
|
||||||
);
|
);
|
||||||
|
|
||||||
const availableSortTypes = useMemo(() => {
|
const availableSortTypes = useMemo(() => {
|
||||||
|
|||||||
@ -53,9 +53,13 @@ export default function CreateRoleDialog({
|
|||||||
const { t } = useTranslation(["views/settings"]);
|
const { t } = useTranslation(["views/settings"]);
|
||||||
const [isLoading, setIsLoading] = useState<boolean>(false);
|
const [isLoading, setIsLoading] = useState<boolean>(false);
|
||||||
|
|
||||||
const cameras = Object.keys(config.cameras || {}).filter(
|
const cameras = Object.keys(config.cameras || {})
|
||||||
(name) => !isReplayCamera(name),
|
.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 || {});
|
const existingRoles = Object.keys(config.auth?.roles || {});
|
||||||
|
|
||||||
|
|||||||
@ -47,9 +47,13 @@ export default function EditRoleCamerasDialog({
|
|||||||
const { t } = useTranslation(["views/settings"]);
|
const { t } = useTranslation(["views/settings"]);
|
||||||
const [isLoading, setIsLoading] = useState<boolean>(false);
|
const [isLoading, setIsLoading] = useState<boolean>(false);
|
||||||
|
|
||||||
const cameras = Object.keys(config.cameras || {}).filter(
|
const cameras = Object.keys(config.cameras || {})
|
||||||
(name) => !isReplayCamera(name),
|
.filter((name) => !isReplayCamera(name))
|
||||||
);
|
.sort(
|
||||||
|
(a, b) =>
|
||||||
|
(config.cameras[a]?.ui?.order ?? 0) -
|
||||||
|
(config.cameras[b]?.ui?.order ?? 0),
|
||||||
|
);
|
||||||
|
|
||||||
const formSchema = z.object({
|
const formSchema = z.object({
|
||||||
cameras: z
|
cameras: z
|
||||||
|
|||||||
@ -39,7 +39,9 @@ export default function ObjectPathPlotter() {
|
|||||||
|
|
||||||
const cameraNames = useMemo(() => {
|
const cameraNames = useMemo(() => {
|
||||||
if (!config) return [];
|
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]);
|
}, [config]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|||||||
@ -96,7 +96,7 @@ export default function CloneCameraDialog({
|
|||||||
if (!config) return [];
|
if (!config) return [];
|
||||||
return Object.keys(config.cameras)
|
return Object.keys(config.cameras)
|
||||||
.filter((c) => !isReplayCamera(c))
|
.filter((c) => !isReplayCamera(c))
|
||||||
.sort();
|
.sort((a, b) => config.cameras[a].ui.order - config.cameras[b].ui.order);
|
||||||
}, [config]);
|
}, [config]);
|
||||||
|
|
||||||
const formSchema = useMemo(() => {
|
const formSchema = useMemo(() => {
|
||||||
@ -176,7 +176,7 @@ export default function CloneCameraDialog({
|
|||||||
if (!config) return [];
|
if (!config) return [];
|
||||||
return Object.keys(config.cameras)
|
return Object.keys(config.cameras)
|
||||||
.filter((c) => c !== sourceCamera && !isReplayCamera(c))
|
.filter((c) => c !== sourceCamera && !isReplayCamera(c))
|
||||||
.sort();
|
.sort((a, b) => config.cameras[a].ui.order - config.cameras[b].ui.order);
|
||||||
}, [config, sourceCamera]);
|
}, [config, sourceCamera]);
|
||||||
|
|
||||||
const srcCfg = config?.cameras?.[sourceCamera];
|
const srcCfg = config?.cameras?.[sourceCamera];
|
||||||
|
|||||||
@ -169,7 +169,7 @@ export default function WsMessageFeed({
|
|||||||
const cam = config.cameras[name];
|
const cam = config.cameras[name];
|
||||||
return !isReplayCamera(name) && cam.enabled_in_config;
|
return !isReplayCamera(name) && cam.enabled_in_config;
|
||||||
})
|
})
|
||||||
.sort();
|
.sort((a, b) => config.cameras[a].ui.order - config.cameras[b].ui.order);
|
||||||
}, [config]);
|
}, [config]);
|
||||||
|
|
||||||
const filteredMessages = useMemo(() => {
|
const filteredMessages = useMemo(() => {
|
||||||
|
|||||||
@ -30,9 +30,9 @@ export default function MotionSearch() {
|
|||||||
|
|
||||||
const cameras = useMemo(() => {
|
const cameras = useMemo(() => {
|
||||||
if (!config?.cameras) return [];
|
if (!config?.cameras) return [];
|
||||||
return Object.keys(config.cameras).filter((cam) =>
|
return Object.keys(config.cameras)
|
||||||
allowedCameras.includes(cam),
|
.filter((cam) => allowedCameras.includes(cam))
|
||||||
);
|
.sort((a, b) => config.cameras[a].ui.order - config.cameras[b].ui.order);
|
||||||
}, [config?.cameras, allowedCameras]);
|
}, [config?.cameras, allowedCameras]);
|
||||||
|
|
||||||
// Selected camera state
|
// Selected camera state
|
||||||
|
|||||||
@ -208,7 +208,12 @@ export default function CameraManagementView({
|
|||||||
!config.cameras[camera].enabled_in_config &&
|
!config.cameras[camera].enabled_in_config &&
|
||||||
!isReplayCamera(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 [];
|
return [];
|
||||||
}, [config]);
|
}, [config]);
|
||||||
@ -217,7 +222,12 @@ export default function CameraManagementView({
|
|||||||
if (config) {
|
if (config) {
|
||||||
return Object.keys(config.cameras)
|
return Object.keys(config.cameras)
|
||||||
.filter((camera) => !isReplayCamera(camera))
|
.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 [];
|
return [];
|
||||||
}, [config]);
|
}, [config]);
|
||||||
|
|||||||
@ -140,6 +140,7 @@ export default function FrigatePlusSettingsView(_props: SettingsPageProps) {
|
|||||||
<tbody>
|
<tbody>
|
||||||
{Object.entries(config.cameras)
|
{Object.entries(config.cameras)
|
||||||
.filter(([name]) => !isReplayCamera(name))
|
.filter(([name]) => !isReplayCamera(name))
|
||||||
|
.sort(([, a], [, b]) => a.ui.order - b.ui.order)
|
||||||
.map(([name, camera]) => (
|
.map(([name, camera]) => (
|
||||||
<tr
|
<tr
|
||||||
key={name}
|
key={name}
|
||||||
|
|||||||
@ -148,7 +148,7 @@ export default function ProfilesView({
|
|||||||
const data: Record<string, Record<string, string[]>> = {};
|
const data: Record<string, Record<string, string[]>> = {};
|
||||||
const cameras = Object.keys(config.cameras)
|
const cameras = Object.keys(config.cameras)
|
||||||
.filter((name) => !isReplayCamera(name))
|
.filter((name) => !isReplayCamera(name))
|
||||||
.sort();
|
.sort((a, b) => config.cameras[a].ui.order - config.cameras[b].ui.order);
|
||||||
|
|
||||||
for (const profile of allProfileNames) {
|
for (const profile of allProfileNames) {
|
||||||
data[profile] = {};
|
data[profile] = {};
|
||||||
@ -474,7 +474,11 @@ export default function ProfilesView({
|
|||||||
const color = getProfileColor(profile, allProfileNames);
|
const color = getProfileColor(profile, allProfileNames);
|
||||||
const isActive = activeProfile === profile;
|
const isActive = activeProfile === profile;
|
||||||
const cameraData = profileOverviewData[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);
|
const isExpanded = expandedProfiles.has(profile);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@ -316,114 +316,114 @@ export default function CameraMetrics({
|
|||||||
</div>
|
</div>
|
||||||
<div className="grid grid-cols-1 gap-6 md:grid-cols-2">
|
<div className="grid grid-cols-1 gap-6 md:grid-cols-2">
|
||||||
{config &&
|
{config &&
|
||||||
Object.values(config.cameras).map((camera) => {
|
Object.values(config.cameras)
|
||||||
if (camera.enabled && !isReplayCamera(camera.name)) {
|
.sort((a, b) => a.ui.order - b.ui.order)
|
||||||
return (
|
.map((camera) => {
|
||||||
<Fragment key={camera.name}>
|
if (camera.enabled && !isReplayCamera(camera.name)) {
|
||||||
{probeCameraName == camera.name && (
|
return (
|
||||||
<CameraInfoDialog
|
<Fragment key={camera.name}>
|
||||||
camera={camera}
|
{probeCameraName == camera.name && (
|
||||||
showCameraInfoDialog={showCameraInfoDialog}
|
<CameraInfoDialog
|
||||||
setShowCameraInfoDialog={setShowCameraInfoDialog}
|
camera={camera}
|
||||||
/>
|
showCameraInfoDialog={showCameraInfoDialog}
|
||||||
)}
|
setShowCameraInfoDialog={setShowCameraInfoDialog}
|
||||||
<div className="flex w-full flex-col gap-3">
|
/>
|
||||||
<div className="flex flex-row items-center justify-between">
|
)}
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex w-full flex-col gap-3">
|
||||||
<div className="text-sm font-medium text-muted-foreground smart-capitalize">
|
<div className="flex flex-row items-center justify-between">
|
||||||
<CameraNameLabel camera={camera} />
|
<div className="flex items-center gap-2">
|
||||||
</div>
|
<div className="text-sm font-medium text-muted-foreground smart-capitalize">
|
||||||
{statsHistory.length > 0 &&
|
<CameraNameLabel camera={camera} />
|
||||||
statsHistory[statsHistory.length - 1]?.cameras[
|
|
||||||
camera.name
|
|
||||||
] && (
|
|
||||||
<ConnectionQualityIndicator
|
|
||||||
quality={
|
|
||||||
statsHistory[statsHistory.length - 1]?.cameras[
|
|
||||||
camera.name
|
|
||||||
]?.connection_quality
|
|
||||||
}
|
|
||||||
expectedFps={
|
|
||||||
statsHistory[statsHistory.length - 1]?.cameras[
|
|
||||||
camera.name
|
|
||||||
]?.expected_fps || 0
|
|
||||||
}
|
|
||||||
reconnects={
|
|
||||||
statsHistory[statsHistory.length - 1]?.cameras[
|
|
||||||
camera.name
|
|
||||||
]?.reconnects_last_hour || 0
|
|
||||||
}
|
|
||||||
stalls={
|
|
||||||
statsHistory[statsHistory.length - 1]?.cameras[
|
|
||||||
camera.name
|
|
||||||
]?.stalls_last_hour || 0
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<Tooltip>
|
|
||||||
<TooltipTrigger>
|
|
||||||
<MdInfo
|
|
||||||
className="size-5 cursor-pointer text-muted-foreground"
|
|
||||||
onClick={() => {
|
|
||||||
setShowCameraInfoDialog(true);
|
|
||||||
setProbeCameraName(camera.name);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</TooltipTrigger>
|
|
||||||
<TooltipContent>
|
|
||||||
{t("cameras.info.tips.title")}
|
|
||||||
</TooltipContent>
|
|
||||||
</Tooltip>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
key={camera.name}
|
|
||||||
className="grid gap-2 sm:grid-cols-2"
|
|
||||||
>
|
|
||||||
{Object.keys(cameraCpuSeries).includes(camera.name) ? (
|
|
||||||
<div className="rounded-lg bg-background_alt p-2.5 md:rounded-2xl">
|
|
||||||
<div className="mb-5">CPU</div>
|
|
||||||
<CameraLineGraph
|
|
||||||
graphId={`${camera.name}-cpu`}
|
|
||||||
unit="%"
|
|
||||||
dataLabels={["ffmpeg", "capture", "detect"]}
|
|
||||||
updateTimes={updateTimes}
|
|
||||||
data={Object.values(
|
|
||||||
cameraCpuSeries[camera.name] || {},
|
|
||||||
)}
|
|
||||||
isActive={isActive}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<Skeleton className="aspect-video size-full" />
|
|
||||||
)}
|
|
||||||
{Object.keys(cameraFpsSeries).includes(camera.name) ? (
|
|
||||||
<div className="rounded-lg bg-background_alt p-2.5 md:rounded-2xl">
|
|
||||||
<div className="mb-5">
|
|
||||||
{t("cameras.framesAndDetections")}
|
|
||||||
</div>
|
</div>
|
||||||
<CameraLineGraph
|
{statsHistory.length > 0 &&
|
||||||
graphId={`${camera.name}-dps`}
|
statsHistory[statsHistory.length - 1]?.cameras[
|
||||||
unit=""
|
camera.name
|
||||||
dataLabels={["camera", "detect", "skipped"]}
|
] && (
|
||||||
updateTimes={updateTimes}
|
<ConnectionQualityIndicator
|
||||||
data={Object.values(
|
quality={
|
||||||
cameraFpsSeries[camera.name] || {},
|
statsHistory[statsHistory.length - 1]
|
||||||
|
?.cameras[camera.name]?.connection_quality
|
||||||
|
}
|
||||||
|
expectedFps={
|
||||||
|
statsHistory[statsHistory.length - 1]
|
||||||
|
?.cameras[camera.name]?.expected_fps || 0
|
||||||
|
}
|
||||||
|
reconnects={
|
||||||
|
statsHistory[statsHistory.length - 1]
|
||||||
|
?.cameras[camera.name]
|
||||||
|
?.reconnects_last_hour || 0
|
||||||
|
}
|
||||||
|
stalls={
|
||||||
|
statsHistory[statsHistory.length - 1]
|
||||||
|
?.cameras[camera.name]?.stalls_last_hour ||
|
||||||
|
0
|
||||||
|
}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
isActive={isActive}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
) : (
|
<Tooltip>
|
||||||
<Skeleton className="aspect-video size-full" />
|
<TooltipTrigger>
|
||||||
)}
|
<MdInfo
|
||||||
|
className="size-5 cursor-pointer text-muted-foreground"
|
||||||
|
onClick={() => {
|
||||||
|
setShowCameraInfoDialog(true);
|
||||||
|
setProbeCameraName(camera.name);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>
|
||||||
|
{t("cameras.info.tips.title")}
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
key={camera.name}
|
||||||
|
className="grid gap-2 sm:grid-cols-2"
|
||||||
|
>
|
||||||
|
{Object.keys(cameraCpuSeries).includes(camera.name) ? (
|
||||||
|
<div className="rounded-lg bg-background_alt p-2.5 md:rounded-2xl">
|
||||||
|
<div className="mb-5">CPU</div>
|
||||||
|
<CameraLineGraph
|
||||||
|
graphId={`${camera.name}-cpu`}
|
||||||
|
unit="%"
|
||||||
|
dataLabels={["ffmpeg", "capture", "detect"]}
|
||||||
|
updateTimes={updateTimes}
|
||||||
|
data={Object.values(
|
||||||
|
cameraCpuSeries[camera.name] || {},
|
||||||
|
)}
|
||||||
|
isActive={isActive}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<Skeleton className="aspect-video size-full" />
|
||||||
|
)}
|
||||||
|
{Object.keys(cameraFpsSeries).includes(camera.name) ? (
|
||||||
|
<div className="rounded-lg bg-background_alt p-2.5 md:rounded-2xl">
|
||||||
|
<div className="mb-5">
|
||||||
|
{t("cameras.framesAndDetections")}
|
||||||
|
</div>
|
||||||
|
<CameraLineGraph
|
||||||
|
graphId={`${camera.name}-dps`}
|
||||||
|
unit=""
|
||||||
|
dataLabels={["camera", "detect", "skipped"]}
|
||||||
|
updateTimes={updateTimes}
|
||||||
|
data={Object.values(
|
||||||
|
cameraFpsSeries[camera.name] || {},
|
||||||
|
)}
|
||||||
|
isActive={isActive}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<Skeleton className="aspect-video size-full" />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</Fragment>
|
||||||
</Fragment>
|
);
|
||||||
);
|
}
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user