mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-03-12 03:17:36 +03:00
re-add override and modified indicators
This commit is contained in:
parent
f8eee8ed0b
commit
95a0530ce6
@ -87,6 +87,11 @@ export interface BaseSectionProps {
|
|||||||
defaultCollapsed?: boolean;
|
defaultCollapsed?: boolean;
|
||||||
/** Whether to show the section title (default: false for global, true for camera) */
|
/** Whether to show the section title (default: false for global, true for camera) */
|
||||||
showTitle?: boolean;
|
showTitle?: boolean;
|
||||||
|
/** Callback when section status changes */
|
||||||
|
onStatusChange?: (status: {
|
||||||
|
hasChanges: boolean;
|
||||||
|
isOverridden: boolean;
|
||||||
|
}) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CreateSectionOptions {
|
export interface CreateSectionOptions {
|
||||||
@ -134,6 +139,7 @@ export function ConfigSection({
|
|||||||
collapsible = false,
|
collapsible = false,
|
||||||
defaultCollapsed = false,
|
defaultCollapsed = false,
|
||||||
showTitle,
|
showTitle,
|
||||||
|
onStatusChange,
|
||||||
}: ConfigSectionProps) {
|
}: ConfigSectionProps) {
|
||||||
const { t, i18n } = useTranslation([
|
const { t, i18n } = useTranslation([
|
||||||
level === "camera" ? "config/cameras" : "config/global",
|
level === "camera" ? "config/cameras" : "config/global",
|
||||||
@ -301,6 +307,10 @@ export function ConfigSection({
|
|||||||
return !isEqual(formData, pendingData);
|
return !isEqual(formData, pendingData);
|
||||||
}, [formData, pendingData]);
|
}, [formData, pendingData]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
onStatusChange?.({ hasChanges, isOverridden });
|
||||||
|
}, [hasChanges, isOverridden, onStatusChange]);
|
||||||
|
|
||||||
// Handle form data change
|
// Handle form data change
|
||||||
const handleChange = useCallback(
|
const handleChange = useCallback(
|
||||||
(data: unknown) => {
|
(data: unknown) => {
|
||||||
@ -346,14 +356,15 @@ export function ConfigSection({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// await axios.put("config/set", {
|
await axios.put("config/sett", {
|
||||||
// requires_restart: requiresRestart ? 0 : 1,
|
requires_restart: requiresRestart ? 0 : 1,
|
||||||
// update_topic: updateTopic,
|
update_topic: updateTopic,
|
||||||
// config_data: {
|
config_data: {
|
||||||
// [basePath]: overrides,
|
[basePath]: overrides,
|
||||||
// },
|
},
|
||||||
// });
|
});
|
||||||
// log save to console for debugging
|
// log save to console for debugging
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
console.log("Saved config data:", {
|
console.log("Saved config data:", {
|
||||||
[basePath]: overrides,
|
[basePath]: overrides,
|
||||||
update_topic: updateTopic,
|
update_topic: updateTopic,
|
||||||
@ -432,17 +443,20 @@ export function ConfigSection({
|
|||||||
const basePath =
|
const basePath =
|
||||||
level === "camera" && cameraName
|
level === "camera" && cameraName
|
||||||
? `cameras.${cameraName}.${sectionPath}`
|
? `cameras.${cameraName}.${sectionPath}`
|
||||||
: sectionPath; // const configData = level === "global" ? schemaDefaults : "";
|
: sectionPath;
|
||||||
|
|
||||||
// await axios.put("config/set", {
|
const configData = level === "global" ? schemaDefaults : "";
|
||||||
// requires_restart: requiresRestart ? 0 : 1,
|
|
||||||
// update_topic: updateTopic,
|
await axios.put("config/sett", {
|
||||||
// config_data: {
|
requires_restart: requiresRestart ? 0 : 1,
|
||||||
// [basePath]: configData,
|
update_topic: updateTopic,
|
||||||
// },
|
config_data: {
|
||||||
// });
|
[basePath]: configData,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
// log reset to console for debugging
|
// log reset to console for debugging
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
console.log(
|
console.log(
|
||||||
level === "global"
|
level === "global"
|
||||||
? "Reset to defaults for path:"
|
? "Reset to defaults for path:"
|
||||||
@ -474,6 +488,7 @@ export function ConfigSection({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}, [
|
}, [
|
||||||
|
schemaDefaults,
|
||||||
sectionPath,
|
sectionPath,
|
||||||
level,
|
level,
|
||||||
cameraName,
|
cameraName,
|
||||||
|
|||||||
@ -17,7 +17,13 @@ import {
|
|||||||
} from "@/components/ui/alert-dialog";
|
} from "@/components/ui/alert-dialog";
|
||||||
import { Drawer, DrawerContent, DrawerTrigger } from "@/components/ui/drawer";
|
import { Drawer, DrawerContent, DrawerTrigger } from "@/components/ui/drawer";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
import {
|
||||||
|
useCallback,
|
||||||
|
useEffect,
|
||||||
|
useMemo,
|
||||||
|
useState,
|
||||||
|
type ReactNode,
|
||||||
|
} from "react";
|
||||||
import useOptimisticState from "@/hooks/use-optimistic-state";
|
import useOptimisticState from "@/hooks/use-optimistic-state";
|
||||||
import { isMobile } from "react-device-detect";
|
import { isMobile } from "react-device-detect";
|
||||||
import { FaVideo } from "react-icons/fa";
|
import { FaVideo } from "react-icons/fa";
|
||||||
@ -39,12 +45,14 @@ import MaintenanceSettingsView from "@/views/settings/MaintenanceSettingsView";
|
|||||||
import {
|
import {
|
||||||
SingleSectionPage,
|
SingleSectionPage,
|
||||||
type SettingsPageProps,
|
type SettingsPageProps,
|
||||||
|
type SectionStatus,
|
||||||
} from "@/views/settings/SingleSectionPage";
|
} from "@/views/settings/SingleSectionPage";
|
||||||
import { useSearchEffect } from "@/hooks/use-overlay-state";
|
import { useSearchEffect } from "@/hooks/use-overlay-state";
|
||||||
import { useNavigate, useSearchParams } from "react-router-dom";
|
import { useNavigate, useSearchParams } from "react-router-dom";
|
||||||
import { useInitialCameraState } from "@/api/ws";
|
import { useInitialCameraState } from "@/api/ws";
|
||||||
import { useIsAdmin } from "@/hooks/use-is-admin";
|
import { useIsAdmin } from "@/hooks/use-is-admin";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { useAllCameraOverrides } from "@/hooks/use-config-override";
|
||||||
import TriggerView from "@/views/settings/TriggerView";
|
import TriggerView from "@/views/settings/TriggerView";
|
||||||
import { CameraNameLabel } from "@/components/camera/FriendlyNameLabel";
|
import { CameraNameLabel } from "@/components/camera/FriendlyNameLabel";
|
||||||
import {
|
import {
|
||||||
@ -420,6 +428,45 @@ const CAMERA_SELECT_BUTTON_PAGES = [
|
|||||||
|
|
||||||
const ALLOWED_VIEWS_FOR_VIEWER = ["ui", "debug", "notifications"];
|
const ALLOWED_VIEWS_FOR_VIEWER = ["ui", "debug", "notifications"];
|
||||||
|
|
||||||
|
// keys for camera sections
|
||||||
|
const CAMERA_SECTION_MAPPING: Record<string, SettingsType> = {
|
||||||
|
detect: "cameraDetect",
|
||||||
|
ffmpeg: "cameraFfmpeg",
|
||||||
|
record: "cameraRecording",
|
||||||
|
snapshots: "cameraSnapshots",
|
||||||
|
motion: "cameraMotion",
|
||||||
|
objects: "cameraObjects",
|
||||||
|
review: "cameraConfigReview",
|
||||||
|
audio: "cameraAudioEvents",
|
||||||
|
audio_transcription: "cameraAudioTranscription",
|
||||||
|
notifications: "cameraNotifications",
|
||||||
|
live: "cameraLivePlayback",
|
||||||
|
birdseye: "cameraBirdseye",
|
||||||
|
face_recognition: "cameraFaceRecognition",
|
||||||
|
lpr: "cameraLpr",
|
||||||
|
mqtt: "cameraMqttConfig",
|
||||||
|
onvif: "cameraOnvif",
|
||||||
|
ui: "cameraUi",
|
||||||
|
timestamp_style: "cameraTimestampStyle",
|
||||||
|
};
|
||||||
|
|
||||||
|
// keys for global sections
|
||||||
|
const GLOBAL_SECTION_MAPPING: Record<string, SettingsType> = {
|
||||||
|
detect: "globalDetect",
|
||||||
|
record: "globalRecording",
|
||||||
|
snapshots: "globalSnapshots",
|
||||||
|
motion: "globalMotion",
|
||||||
|
objects: "globalObjects",
|
||||||
|
review: "globalReview",
|
||||||
|
audio: "globalAudioEvents",
|
||||||
|
live: "globalLivePlayback",
|
||||||
|
timestamp_style: "globalTimestampStyle",
|
||||||
|
};
|
||||||
|
|
||||||
|
const CAMERA_SECTION_KEYS = new Set<SettingsType>(
|
||||||
|
Object.values(CAMERA_SECTION_MAPPING),
|
||||||
|
);
|
||||||
|
|
||||||
const getCurrentComponent = (page: SettingsType) => {
|
const getCurrentComponent = (page: SettingsType) => {
|
||||||
for (const group of settingsGroups) {
|
for (const group of settingsGroups) {
|
||||||
for (const item of group.items) {
|
for (const item of group.items) {
|
||||||
@ -436,11 +483,13 @@ function MobileMenuItem({
|
|||||||
onSelect,
|
onSelect,
|
||||||
onClose,
|
onClose,
|
||||||
className,
|
className,
|
||||||
|
label,
|
||||||
}: {
|
}: {
|
||||||
item: { key: string };
|
item: { key: string };
|
||||||
onSelect: (key: string) => void;
|
onSelect: (key: string) => void;
|
||||||
onClose?: () => void;
|
onClose?: () => void;
|
||||||
className?: string;
|
className?: string;
|
||||||
|
label?: ReactNode;
|
||||||
}) {
|
}) {
|
||||||
const { t } = useTranslation(["views/settings"]);
|
const { t } = useTranslation(["views/settings"]);
|
||||||
|
|
||||||
@ -455,7 +504,11 @@ function MobileMenuItem({
|
|||||||
onClose?.();
|
onClose?.();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div className="smart-capitalize">{t("menu." + item.key)}</div>
|
<div className="w-full">
|
||||||
|
{label ?? (
|
||||||
|
<div className="smart-capitalize">{t("menu." + item.key)}</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
<LuChevronRight className="size-4" />
|
<LuChevronRight className="size-4" />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@ -466,6 +519,9 @@ export default function Settings() {
|
|||||||
const [page, setPage] = useState<SettingsType>("profileSettings");
|
const [page, setPage] = useState<SettingsType>("profileSettings");
|
||||||
const [pageToggle, setPageToggle] = useOptimisticState(page, setPage, 100);
|
const [pageToggle, setPageToggle] = useOptimisticState(page, setPage, 100);
|
||||||
const [contentMobileOpen, setContentMobileOpen] = useState(false);
|
const [contentMobileOpen, setContentMobileOpen] = useState(false);
|
||||||
|
const [sectionStatusByKey, setSectionStatusByKey] = useState<
|
||||||
|
Partial<Record<SettingsType, SectionStatus>>
|
||||||
|
>({});
|
||||||
|
|
||||||
const { data: config } = useSWR<FrigateConfig>("config");
|
const { data: config } = useSWR<FrigateConfig>("config");
|
||||||
|
|
||||||
@ -497,6 +553,9 @@ export default function Settings() {
|
|||||||
|
|
||||||
const [selectedCamera, setSelectedCamera] = useState<string>("");
|
const [selectedCamera, setSelectedCamera] = useState<string>("");
|
||||||
|
|
||||||
|
// Get all camera overrides for the selected camera
|
||||||
|
const cameraOverrides = useAllCameraOverrides(config, selectedCamera);
|
||||||
|
|
||||||
const { payload: allCameraStates } = useInitialCameraState(
|
const { payload: allCameraStates } = useInitialCameraState(
|
||||||
cameras.length > 0 ? cameras[0].name : "",
|
cameras.length > 0 ? cameras[0].name : "",
|
||||||
true,
|
true,
|
||||||
@ -589,6 +648,81 @@ export default function Settings() {
|
|||||||
}
|
}
|
||||||
}, [t, contentMobileOpen]);
|
}, [t, contentMobileOpen]);
|
||||||
|
|
||||||
|
const handleSectionStatusChange = useCallback(
|
||||||
|
(sectionKey: string, level: "global" | "camera", status: SectionStatus) => {
|
||||||
|
// Map section keys to menu keys based on level
|
||||||
|
let menuKey: string;
|
||||||
|
if (level === "camera") {
|
||||||
|
menuKey = CAMERA_SECTION_MAPPING[sectionKey] || sectionKey;
|
||||||
|
} else {
|
||||||
|
menuKey = GLOBAL_SECTION_MAPPING[sectionKey] || sectionKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
setSectionStatusByKey((prev) => ({
|
||||||
|
...prev,
|
||||||
|
[menuKey]: status,
|
||||||
|
}));
|
||||||
|
},
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
|
// Initialize override status for all camera sections
|
||||||
|
useEffect(() => {
|
||||||
|
if (!selectedCamera || !cameraOverrides) return;
|
||||||
|
|
||||||
|
const overrideMap: Partial<Record<SettingsType, SectionStatus>> = {};
|
||||||
|
|
||||||
|
// Set override status for all camera sections using the shared mapping
|
||||||
|
Object.entries(CAMERA_SECTION_MAPPING).forEach(
|
||||||
|
([sectionKey, settingsKey]) => {
|
||||||
|
const isOverridden = cameraOverrides.includes(sectionKey);
|
||||||
|
overrideMap[settingsKey] = {
|
||||||
|
hasChanges: false,
|
||||||
|
isOverridden,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
setSectionStatusByKey((prev) => {
|
||||||
|
// Merge but preserve hasChanges from previous state
|
||||||
|
const merged = { ...prev };
|
||||||
|
Object.entries(overrideMap).forEach(([key, status]) => {
|
||||||
|
merged[key as SettingsType] = {
|
||||||
|
hasChanges: prev[key as SettingsType]?.hasChanges || false,
|
||||||
|
isOverridden: status.isOverridden,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
return merged;
|
||||||
|
});
|
||||||
|
}, [selectedCamera, cameraOverrides]);
|
||||||
|
|
||||||
|
const renderMenuItemLabel = useCallback(
|
||||||
|
(key: SettingsType) => {
|
||||||
|
const status = sectionStatusByKey[key];
|
||||||
|
const showOverrideDot =
|
||||||
|
CAMERA_SECTION_KEYS.has(key) && status?.isOverridden;
|
||||||
|
// const showUnsavedDot = status?.hasChanges;
|
||||||
|
const showUnsavedDot = false; // Disable unsaved changes indicator for now
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex w-full items-center justify-between pr-4 md:pr-0">
|
||||||
|
<div className="smart-capitalize">{t("menu." + key)}</div>
|
||||||
|
{(showOverrideDot || showUnsavedDot) && (
|
||||||
|
<div className="ml-2 flex items-center gap-2">
|
||||||
|
{showOverrideDot && (
|
||||||
|
<span className="inline-block size-2 rounded-full bg-selected" />
|
||||||
|
)}
|
||||||
|
{showUnsavedDot && (
|
||||||
|
<span className="inline-block size-2 rounded-full bg-danger" />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
[sectionStatusByKey, t],
|
||||||
|
);
|
||||||
|
|
||||||
if (isMobile) {
|
if (isMobile) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@ -625,6 +759,7 @@ export default function Settings() {
|
|||||||
key={item.key}
|
key={item.key}
|
||||||
item={item}
|
item={item}
|
||||||
className={cn(filteredItems.length == 1 && "pl-2")}
|
className={cn(filteredItems.length == 1 && "pl-2")}
|
||||||
|
label={renderMenuItemLabel(item.key as SettingsType)}
|
||||||
onSelect={(key) => {
|
onSelect={(key) => {
|
||||||
if (
|
if (
|
||||||
!isAdmin &&
|
!isAdmin &&
|
||||||
@ -688,6 +823,7 @@ export default function Settings() {
|
|||||||
selectedCamera={selectedCamera}
|
selectedCamera={selectedCamera}
|
||||||
setUnsavedChanges={setUnsavedChanges}
|
setUnsavedChanges={setUnsavedChanges}
|
||||||
selectedZoneMask={filterZoneMask}
|
selectedZoneMask={filterZoneMask}
|
||||||
|
onSectionStatusChange={handleSectionStatusChange}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
})()}
|
})()}
|
||||||
@ -779,9 +915,9 @@ export default function Settings() {
|
|||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div className="smart-capitalize">
|
{renderMenuItemLabel(
|
||||||
{t("menu." + filteredItems[0].key)}
|
filteredItems[0].key as SettingsType,
|
||||||
</div>
|
)}
|
||||||
</SidebarMenuButton>
|
</SidebarMenuButton>
|
||||||
</SidebarMenuItem>
|
</SidebarMenuItem>
|
||||||
</SidebarMenu>
|
</SidebarMenu>
|
||||||
@ -819,8 +955,10 @@ export default function Settings() {
|
|||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div className="w-full cursor-pointer smart-capitalize">
|
<div className="w-full cursor-pointer">
|
||||||
{t("menu." + item.key)}
|
{renderMenuItemLabel(
|
||||||
|
item.key as SettingsType,
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</SidebarMenuSubButton>
|
</SidebarMenuSubButton>
|
||||||
</SidebarMenuSubItem>
|
</SidebarMenuSubItem>
|
||||||
@ -844,6 +982,7 @@ export default function Settings() {
|
|||||||
selectedCamera={selectedCamera}
|
selectedCamera={selectedCamera}
|
||||||
setUnsavedChanges={setUnsavedChanges}
|
setUnsavedChanges={setUnsavedChanges}
|
||||||
selectedZoneMask={filterZoneMask}
|
selectedZoneMask={filterZoneMask}
|
||||||
|
onSectionStatusChange={handleSectionStatusChange}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
})()}
|
})()}
|
||||||
|
|||||||
@ -1,13 +1,25 @@
|
|||||||
|
import { useEffect, useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import Heading from "@/components/ui/heading";
|
import Heading from "@/components/ui/heading";
|
||||||
import type { SectionConfig } from "@/components/config-form/sections";
|
import type { SectionConfig } from "@/components/config-form/sections";
|
||||||
import { ConfigSectionTemplate } from "@/components/config-form/sections";
|
import { ConfigSectionTemplate } from "@/components/config-form/sections";
|
||||||
import type { PolygonType } from "@/types/canvas";
|
import type { PolygonType } from "@/types/canvas";
|
||||||
|
import { Badge } from "@/components/ui/badge";
|
||||||
|
|
||||||
export type SettingsPageProps = {
|
export type SettingsPageProps = {
|
||||||
selectedCamera?: string;
|
selectedCamera?: string;
|
||||||
setUnsavedChanges?: React.Dispatch<React.SetStateAction<boolean>>;
|
setUnsavedChanges?: React.Dispatch<React.SetStateAction<boolean>>;
|
||||||
selectedZoneMask?: PolygonType[];
|
selectedZoneMask?: PolygonType[];
|
||||||
|
onSectionStatusChange?: (
|
||||||
|
sectionKey: string,
|
||||||
|
level: "global" | "camera",
|
||||||
|
status: SectionStatus,
|
||||||
|
) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type SectionStatus = {
|
||||||
|
hasChanges: boolean;
|
||||||
|
isOverridden: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type SingleSectionPageOptions = {
|
export type SingleSectionPageOptions = {
|
||||||
@ -29,6 +41,7 @@ export function SingleSectionPage({
|
|||||||
showOverrideIndicator = true,
|
showOverrideIndicator = true,
|
||||||
selectedCamera,
|
selectedCamera,
|
||||||
setUnsavedChanges,
|
setUnsavedChanges,
|
||||||
|
onSectionStatusChange,
|
||||||
}: SingleSectionPageProps) {
|
}: SingleSectionPageProps) {
|
||||||
const sectionNamespace =
|
const sectionNamespace =
|
||||||
level === "camera" ? "config/cameras" : "config/global";
|
level === "camera" ? "config/cameras" : "config/global";
|
||||||
@ -37,6 +50,14 @@ export function SingleSectionPage({
|
|||||||
"views/settings",
|
"views/settings",
|
||||||
"common",
|
"common",
|
||||||
]);
|
]);
|
||||||
|
const [sectionStatus, setSectionStatus] = useState<SectionStatus>({
|
||||||
|
hasChanges: false,
|
||||||
|
isOverridden: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
onSectionStatusChange?.(sectionKey, level, sectionStatus);
|
||||||
|
}, [onSectionStatusChange, sectionKey, level, sectionStatus]);
|
||||||
|
|
||||||
if (level === "camera" && !selectedCamera) {
|
if (level === "camera" && !selectedCamera) {
|
||||||
return (
|
return (
|
||||||
@ -48,10 +69,11 @@ export function SingleSectionPage({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex size-full flex-col pr-2">
|
<div className="flex size-full flex-col pr-2">
|
||||||
<div className="mb-4">
|
<div className="space-y-4">
|
||||||
<Heading as="h3">
|
<Heading as="h3">
|
||||||
{t(`${sectionKey}.label`, { ns: sectionNamespace })}
|
{t(`${sectionKey}.label`, { ns: sectionNamespace })}
|
||||||
</Heading>
|
</Heading>
|
||||||
|
|
||||||
{i18n.exists(`${sectionKey}.description`, {
|
{i18n.exists(`${sectionKey}.description`, {
|
||||||
ns: sectionNamespace,
|
ns: sectionNamespace,
|
||||||
}) && (
|
}) && (
|
||||||
@ -59,6 +81,20 @@ export function SingleSectionPage({
|
|||||||
{t(`${sectionKey}.description`, { ns: sectionNamespace })}
|
{t(`${sectionKey}.description`, { ns: sectionNamespace })}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
|
<div className="flex flex-wrap items-center gap-2">
|
||||||
|
{level === "camera" &&
|
||||||
|
showOverrideIndicator &&
|
||||||
|
sectionStatus.isOverridden && (
|
||||||
|
<Badge variant="secondary" className="text-xs">
|
||||||
|
{t("overridden", { ns: "common", defaultValue: "Overridden" })}
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
{sectionStatus.hasChanges && (
|
||||||
|
<Badge variant="outline" className="text-xs">
|
||||||
|
{t("modified", { ns: "common", defaultValue: "Modified" })}
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<ConfigSectionTemplate
|
<ConfigSectionTemplate
|
||||||
sectionKey={sectionKey}
|
sectionKey={sectionKey}
|
||||||
@ -69,6 +105,7 @@ export function SingleSectionPage({
|
|||||||
showTitle={false}
|
showTitle={false}
|
||||||
sectionConfig={sectionConfig}
|
sectionConfig={sectionConfig}
|
||||||
requiresRestart={requiresRestart}
|
requiresRestart={requiresRestart}
|
||||||
|
onStatusChange={setSectionStatus}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user