mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-08-02 09:02:15 +03:00
refactor sections and overrides
This commit is contained in:
@@ -1,12 +1,9 @@
|
||||
// Global Configuration View
|
||||
// Main view for configuring global Frigate settings
|
||||
|
||||
import { useMemo, useCallback, useState, useEffect, useRef } from "react";
|
||||
import { useMemo, useCallback, useState } from "react";
|
||||
import useSWR from "swr";
|
||||
import axios from "axios";
|
||||
import { toast } from "sonner";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { ConfigForm } from "@/components/config-form/ConfigForm";
|
||||
import { DetectSection } from "@/components/config-form/sections/DetectSection";
|
||||
import { RecordSection } from "@/components/config-form/sections/RecordSection";
|
||||
import { SnapshotsSection } from "@/components/config-form/sections/SnapshotsSection";
|
||||
@@ -14,545 +11,90 @@ import { MotionSection } from "@/components/config-form/sections/MotionSection";
|
||||
import { ObjectsSection } from "@/components/config-form/sections/ObjectsSection";
|
||||
import { ReviewSection } from "@/components/config-form/sections/ReviewSection";
|
||||
import { AudioSection } from "@/components/config-form/sections/AudioSection";
|
||||
import { AudioTranscriptionSection } from "@/components/config-form/sections/AudioTranscriptionSection";
|
||||
import { AuthSection } from "@/components/config-form/sections/AuthSection";
|
||||
import { BirdseyeSection } from "@/components/config-form/sections/BirdseyeSection";
|
||||
import { ClassificationSection } from "@/components/config-form/sections/ClassificationSection";
|
||||
import { DatabaseSection } from "@/components/config-form/sections/DatabaseSection";
|
||||
import { DetectorsSection } from "@/components/config-form/sections/DetectorsSection";
|
||||
import { EnvironmentVarsSection } from "@/components/config-form/sections/EnvironmentVarsSection";
|
||||
import { FaceRecognitionSection } from "@/components/config-form/sections/FaceRecognitionSection";
|
||||
import { FfmpegSection } from "@/components/config-form/sections/FfmpegSection";
|
||||
import { GenaiSection } from "@/components/config-form/sections/GenaiSection";
|
||||
import { LiveSection } from "@/components/config-form/sections/LiveSection";
|
||||
import { LoggerSection } from "@/components/config-form/sections/LoggerSection";
|
||||
import { LprSection } from "@/components/config-form/sections/LprSection";
|
||||
import { ModelSection } from "@/components/config-form/sections/ModelSection";
|
||||
import { MqttSection } from "@/components/config-form/sections/MqttSection";
|
||||
import { NetworkingSection } from "@/components/config-form/sections/NetworkingSection";
|
||||
import { ProxySection } from "@/components/config-form/sections/ProxySection";
|
||||
import { SemanticSearchSection } from "@/components/config-form/sections/SemanticSearchSection";
|
||||
import { TimestampSection } from "@/components/config-form/sections/TimestampSection";
|
||||
import type { RJSFSchema } from "@rjsf/utils";
|
||||
import { TelemetrySection } from "@/components/config-form/sections/TelemetrySection";
|
||||
import { TlsSection } from "@/components/config-form/sections/TlsSection";
|
||||
import { UiSection } from "@/components/config-form/sections/UiSection";
|
||||
import type { FrigateConfig } from "@/types/frigateConfig";
|
||||
import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { extractSchemaSection } from "@/lib/config-schema";
|
||||
import ActivityIndicator from "@/components/indicators/activity-indicator";
|
||||
import Heading from "@/components/ui/heading";
|
||||
import { LuSave } from "react-icons/lu";
|
||||
import isEqual from "lodash/isEqual";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { getSectionConfig } from "@/components/config-form/sectionConfigs";
|
||||
|
||||
// Shared sections that can be overridden at camera level
|
||||
const sharedSections = [
|
||||
{ key: "detect", component: DetectSection },
|
||||
{ key: "record", component: RecordSection },
|
||||
{
|
||||
key: "snapshots",
|
||||
component: SnapshotsSection,
|
||||
},
|
||||
{ key: "snapshots", component: SnapshotsSection },
|
||||
{ key: "motion", component: MotionSection },
|
||||
{
|
||||
key: "objects",
|
||||
component: ObjectsSection,
|
||||
},
|
||||
{ key: "objects", component: ObjectsSection },
|
||||
{ key: "review", component: ReviewSection },
|
||||
{ key: "audio", component: AudioSection },
|
||||
{ key: "live", component: LiveSection },
|
||||
{
|
||||
key: "timestamp_style",
|
||||
component: TimestampSection,
|
||||
},
|
||||
{ key: "timestamp_style", component: TimestampSection },
|
||||
];
|
||||
|
||||
// Section configurations for global-only settings (system and integrations)
|
||||
const globalSectionConfigs: Record<
|
||||
string,
|
||||
{
|
||||
fieldOrder?: string[];
|
||||
hiddenFields?: string[];
|
||||
advancedFields?: string[];
|
||||
liveValidate?: boolean;
|
||||
uiSchema?: Record<string, unknown>;
|
||||
}
|
||||
> = {
|
||||
mqtt: {
|
||||
fieldOrder: [
|
||||
"enabled",
|
||||
"host",
|
||||
"port",
|
||||
"user",
|
||||
"password",
|
||||
"topic_prefix",
|
||||
"client_id",
|
||||
"stats_interval",
|
||||
"qos",
|
||||
"tls_ca_certs",
|
||||
"tls_client_cert",
|
||||
"tls_client_key",
|
||||
"tls_insecure",
|
||||
],
|
||||
advancedFields: [
|
||||
"stats_interval",
|
||||
"qos",
|
||||
"tls_ca_certs",
|
||||
"tls_client_cert",
|
||||
"tls_client_key",
|
||||
"tls_insecure",
|
||||
],
|
||||
liveValidate: true,
|
||||
},
|
||||
database: {
|
||||
fieldOrder: ["path"],
|
||||
advancedFields: [],
|
||||
},
|
||||
auth: {
|
||||
fieldOrder: [
|
||||
"enabled",
|
||||
"reset_admin_password",
|
||||
"cookie_name",
|
||||
"cookie_secure",
|
||||
"session_length",
|
||||
"refresh_time",
|
||||
"native_oauth_url",
|
||||
"failed_login_rate_limit",
|
||||
"trusted_proxies",
|
||||
"hash_iterations",
|
||||
"roles",
|
||||
],
|
||||
hiddenFields: ["admin_first_time_login"],
|
||||
advancedFields: [
|
||||
"cookie_name",
|
||||
"cookie_secure",
|
||||
"session_length",
|
||||
"refresh_time",
|
||||
"failed_login_rate_limit",
|
||||
"trusted_proxies",
|
||||
"hash_iterations",
|
||||
"roles",
|
||||
],
|
||||
uiSchema: {
|
||||
reset_admin_password: {
|
||||
"ui:widget": "switch",
|
||||
},
|
||||
},
|
||||
},
|
||||
tls: {
|
||||
fieldOrder: ["enabled", "cert", "key"],
|
||||
advancedFields: [],
|
||||
},
|
||||
networking: {
|
||||
fieldOrder: ["ipv6"],
|
||||
advancedFields: [],
|
||||
},
|
||||
proxy: {
|
||||
fieldOrder: [
|
||||
"header_map",
|
||||
"logout_url",
|
||||
"auth_secret",
|
||||
"default_role",
|
||||
"separator",
|
||||
],
|
||||
advancedFields: ["header_map", "auth_secret", "separator"],
|
||||
liveValidate: true,
|
||||
},
|
||||
ui: {
|
||||
fieldOrder: [
|
||||
"timezone",
|
||||
"time_format",
|
||||
"date_style",
|
||||
"time_style",
|
||||
"unit_system",
|
||||
],
|
||||
advancedFields: [],
|
||||
},
|
||||
logger: {
|
||||
fieldOrder: ["default", "logs"],
|
||||
advancedFields: ["logs"],
|
||||
},
|
||||
environment_vars: {
|
||||
fieldOrder: [],
|
||||
advancedFields: [],
|
||||
},
|
||||
telemetry: {
|
||||
fieldOrder: ["network_interfaces", "stats", "version_check"],
|
||||
advancedFields: [],
|
||||
},
|
||||
birdseye: {
|
||||
fieldOrder: [
|
||||
"enabled",
|
||||
"restream",
|
||||
"width",
|
||||
"height",
|
||||
"quality",
|
||||
"mode",
|
||||
"layout",
|
||||
"inactivity_threshold",
|
||||
"idle_heartbeat_fps",
|
||||
],
|
||||
advancedFields: ["width", "height", "quality", "inactivity_threshold"],
|
||||
},
|
||||
ffmpeg: {
|
||||
fieldOrder: [
|
||||
"path",
|
||||
"global_args",
|
||||
"hwaccel_args",
|
||||
"input_args",
|
||||
"output_args",
|
||||
"retry_interval",
|
||||
"apple_compatibility",
|
||||
"gpu",
|
||||
],
|
||||
advancedFields: [
|
||||
"global_args",
|
||||
"hwaccel_args",
|
||||
"input_args",
|
||||
"output_args",
|
||||
"retry_interval",
|
||||
"apple_compatibility",
|
||||
"gpu",
|
||||
],
|
||||
uiSchema: {
|
||||
global_args: {
|
||||
"ui:widget": "ArrayAsTextWidget",
|
||||
"ui:options": {
|
||||
suppressMultiSchema: true,
|
||||
},
|
||||
},
|
||||
hwaccel_args: {
|
||||
"ui:widget": "ArrayAsTextWidget",
|
||||
"ui:options": {
|
||||
suppressMultiSchema: true,
|
||||
},
|
||||
},
|
||||
input_args: {
|
||||
"ui:widget": "ArrayAsTextWidget",
|
||||
"ui:options": {
|
||||
suppressMultiSchema: true,
|
||||
},
|
||||
},
|
||||
output_args: {
|
||||
"ui:widget": "ArrayAsTextWidget",
|
||||
"ui:options": {
|
||||
suppressMultiSchema: true,
|
||||
},
|
||||
detect: {
|
||||
"ui:widget": "ArrayAsTextWidget",
|
||||
"ui:options": {
|
||||
suppressMultiSchema: true,
|
||||
},
|
||||
},
|
||||
record: {
|
||||
"ui:widget": "ArrayAsTextWidget",
|
||||
"ui:options": {
|
||||
suppressMultiSchema: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
detectors: {
|
||||
fieldOrder: [],
|
||||
advancedFields: [],
|
||||
},
|
||||
model: {
|
||||
fieldOrder: [
|
||||
"path",
|
||||
"labelmap_path",
|
||||
"width",
|
||||
"height",
|
||||
"input_pixel_format",
|
||||
"input_tensor",
|
||||
"input_dtype",
|
||||
"model_type",
|
||||
],
|
||||
advancedFields: [
|
||||
"input_pixel_format",
|
||||
"input_tensor",
|
||||
"input_dtype",
|
||||
"model_type",
|
||||
],
|
||||
hiddenFields: ["labelmap", "attributes_map"],
|
||||
},
|
||||
genai: {
|
||||
fieldOrder: [
|
||||
"provider",
|
||||
"api_key",
|
||||
"base_url",
|
||||
"model",
|
||||
"provider_options",
|
||||
"runtime_options",
|
||||
],
|
||||
advancedFields: ["base_url", "provider_options", "runtime_options"],
|
||||
hiddenFields: ["genai.enabled_in_config"],
|
||||
},
|
||||
classification: {
|
||||
hiddenFields: ["custom"],
|
||||
advancedFields: [],
|
||||
},
|
||||
semantic_search: {
|
||||
fieldOrder: ["enabled", "reindex", "model", "model_size", "device"],
|
||||
advancedFields: ["reindex", "device"],
|
||||
},
|
||||
audio_transcription: {
|
||||
fieldOrder: ["enabled", "language", "device", "model_size", "live_enabled"],
|
||||
advancedFields: ["language", "device", "model_size"],
|
||||
},
|
||||
face_recognition: {
|
||||
fieldOrder: [
|
||||
"enabled",
|
||||
"model_size",
|
||||
"unknown_score",
|
||||
"detection_threshold",
|
||||
"recognition_threshold",
|
||||
"min_area",
|
||||
"min_faces",
|
||||
"save_attempts",
|
||||
"blur_confidence_filter",
|
||||
"device",
|
||||
],
|
||||
advancedFields: [
|
||||
"unknown_score",
|
||||
"detection_threshold",
|
||||
"recognition_threshold",
|
||||
"min_area",
|
||||
"min_faces",
|
||||
"save_attempts",
|
||||
"blur_confidence_filter",
|
||||
"device",
|
||||
],
|
||||
},
|
||||
lpr: {
|
||||
fieldOrder: [
|
||||
"enabled",
|
||||
"model_size",
|
||||
"detection_threshold",
|
||||
"min_area",
|
||||
"recognition_threshold",
|
||||
"min_plate_length",
|
||||
"format",
|
||||
"match_distance",
|
||||
"known_plates",
|
||||
"enhancement",
|
||||
"debug_save_plates",
|
||||
"device",
|
||||
"replace_rules",
|
||||
],
|
||||
advancedFields: [
|
||||
"detection_threshold",
|
||||
"recognition_threshold",
|
||||
"min_plate_length",
|
||||
"format",
|
||||
"match_distance",
|
||||
"known_plates",
|
||||
"enhancement",
|
||||
"debug_save_plates",
|
||||
"device",
|
||||
"replace_rules",
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
// System sections (global only)
|
||||
const systemSections = [
|
||||
"database",
|
||||
"tls",
|
||||
"auth",
|
||||
"networking",
|
||||
"proxy",
|
||||
"ui",
|
||||
"logger",
|
||||
"environment_vars",
|
||||
"telemetry",
|
||||
"birdseye",
|
||||
"ffmpeg",
|
||||
"detectors",
|
||||
"model",
|
||||
{ key: "database", component: DatabaseSection },
|
||||
{ key: "tls", component: TlsSection },
|
||||
{ key: "auth", component: AuthSection },
|
||||
{ key: "networking", component: NetworkingSection },
|
||||
{ key: "proxy", component: ProxySection },
|
||||
{ key: "ui", component: UiSection },
|
||||
{ key: "logger", component: LoggerSection },
|
||||
{ key: "environment_vars", component: EnvironmentVarsSection },
|
||||
{ key: "telemetry", component: TelemetrySection },
|
||||
{ key: "birdseye", component: BirdseyeSection },
|
||||
{ key: "ffmpeg", component: FfmpegSection },
|
||||
{ key: "detectors", component: DetectorsSection },
|
||||
{ key: "model", component: ModelSection },
|
||||
];
|
||||
|
||||
// Integration sections (global only)
|
||||
const integrationSections = [
|
||||
"mqtt",
|
||||
"semantic_search",
|
||||
"genai",
|
||||
"face_recognition",
|
||||
"lpr",
|
||||
"classification",
|
||||
"audio_transcription",
|
||||
{ key: "mqtt", component: MqttSection },
|
||||
{ key: "semantic_search", component: SemanticSearchSection },
|
||||
{ key: "genai", component: GenaiSection },
|
||||
{ key: "face_recognition", component: FaceRecognitionSection },
|
||||
{ key: "lpr", component: LprSection },
|
||||
{ key: "classification", component: ClassificationSection },
|
||||
{ key: "audio_transcription", component: AudioTranscriptionSection },
|
||||
];
|
||||
|
||||
interface GlobalConfigSectionProps {
|
||||
sectionKey: string;
|
||||
schema: RJSFSchema | null;
|
||||
config: FrigateConfig | undefined;
|
||||
onSave: () => void;
|
||||
title: string;
|
||||
}
|
||||
|
||||
function GlobalConfigSection({
|
||||
sectionKey,
|
||||
schema,
|
||||
config,
|
||||
onSave,
|
||||
title,
|
||||
}: GlobalConfigSectionProps) {
|
||||
const sectionConfig = globalSectionConfigs[sectionKey];
|
||||
const { t, i18n } = useTranslation([
|
||||
"config/global",
|
||||
"views/settings",
|
||||
"common",
|
||||
]);
|
||||
const [pendingData, setPendingData] = useState<unknown | null>(null);
|
||||
const [isSaving, setIsSaving] = useState(false);
|
||||
const [formKey, setFormKey] = useState(0);
|
||||
const isResettingRef = useRef(false);
|
||||
|
||||
const formData = useMemo((): unknown => {
|
||||
if (!config) return {};
|
||||
return (config as unknown as Record<string, unknown>)[sectionKey];
|
||||
}, [config, sectionKey]);
|
||||
|
||||
useEffect(() => {
|
||||
setPendingData(null);
|
||||
}, [formData]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isResettingRef.current) {
|
||||
isResettingRef.current = false;
|
||||
}
|
||||
}, [formKey]);
|
||||
|
||||
const hasChanges = useMemo(() => {
|
||||
if (!pendingData) return false;
|
||||
return !isEqual(formData, pendingData);
|
||||
}, [formData, pendingData]);
|
||||
|
||||
const handleChange = useCallback(
|
||||
(data: unknown) => {
|
||||
if (isResettingRef.current) {
|
||||
setPendingData(null);
|
||||
return;
|
||||
}
|
||||
if (!data || typeof data !== "object") {
|
||||
setPendingData(null);
|
||||
return;
|
||||
}
|
||||
if (isEqual(formData, data)) {
|
||||
setPendingData(null);
|
||||
return;
|
||||
}
|
||||
setPendingData(data);
|
||||
},
|
||||
[formData],
|
||||
);
|
||||
|
||||
const handleReset = useCallback(() => {
|
||||
isResettingRef.current = true;
|
||||
setPendingData(null);
|
||||
setFormKey((prev) => prev + 1);
|
||||
}, []);
|
||||
|
||||
const handleSave = useCallback(async () => {
|
||||
if (!pendingData) return;
|
||||
|
||||
setIsSaving(true);
|
||||
try {
|
||||
// await axios.put("config/set", {
|
||||
// update_topic: `config/${sectionKey}`,
|
||||
// config_data: {
|
||||
// [sectionKey]: pendingData,
|
||||
// },
|
||||
// });
|
||||
|
||||
// log axios for debugging
|
||||
console.log("Saved config section", sectionKey, pendingData);
|
||||
|
||||
toast.success(
|
||||
t("toast.success", {
|
||||
ns: "views/settings",
|
||||
defaultValue: "Settings saved successfully",
|
||||
}),
|
||||
);
|
||||
|
||||
setPendingData(null);
|
||||
onSave();
|
||||
} catch {
|
||||
toast.error(
|
||||
t("toast.error", {
|
||||
ns: "views/settings",
|
||||
defaultValue: "Failed to save settings",
|
||||
}),
|
||||
);
|
||||
} finally {
|
||||
setIsSaving(false);
|
||||
}
|
||||
}, [sectionKey, pendingData, t, onSave]);
|
||||
|
||||
if (!schema || !sectionConfig) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center gap-3">
|
||||
<Heading as="h4">{title}</Heading>
|
||||
{hasChanges && (
|
||||
<Badge variant="outline" className="text-xs">
|
||||
{t("modified", { ns: "common", defaultValue: "Modified" })}
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
<ConfigForm
|
||||
key={formKey}
|
||||
schema={schema}
|
||||
formData={pendingData || formData}
|
||||
onChange={handleChange}
|
||||
fieldOrder={sectionConfig.fieldOrder}
|
||||
hiddenFields={sectionConfig.hiddenFields}
|
||||
advancedFields={sectionConfig.advancedFields}
|
||||
liveValidate={sectionConfig.liveValidate}
|
||||
uiSchema={sectionConfig.uiSchema}
|
||||
showSubmit={false}
|
||||
i18nNamespace="config/global"
|
||||
formContext={{ level: "global", sectionI18nPrefix: sectionKey }}
|
||||
disabled={isSaving}
|
||||
/>
|
||||
|
||||
<div className="flex items-center justify-between pt-2">
|
||||
<div className="flex items-center gap-2">
|
||||
{hasChanges && (
|
||||
<span className="text-sm text-muted-foreground">
|
||||
{t("unsavedChanges", {
|
||||
ns: "views/settings",
|
||||
defaultValue: "You have unsaved changes",
|
||||
})}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
{hasChanges && (
|
||||
<Button
|
||||
onClick={handleReset}
|
||||
variant="outline"
|
||||
disabled={isSaving}
|
||||
className="gap-2"
|
||||
>
|
||||
{t("reset", { ns: "common", defaultValue: "Reset" })}
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
onClick={handleSave}
|
||||
disabled={!hasChanges || isSaving}
|
||||
className="gap-2"
|
||||
>
|
||||
<LuSave className="h-4 w-4" />
|
||||
{isSaving
|
||||
? t("saving", { ns: "common", defaultValue: "Saving..." })
|
||||
: t("save", { ns: "common", defaultValue: "Save" })}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function GlobalConfigView() {
|
||||
const { t, i18n } = useTranslation([
|
||||
"views/settings",
|
||||
"config/global",
|
||||
"common",
|
||||
]);
|
||||
const defaultSharedSection = sharedSections[0]?.key ?? "";
|
||||
const defaultSystemSection = systemSections[0]?.key ?? "";
|
||||
const defaultIntegrationSection = integrationSections[0]?.key ?? "";
|
||||
const [activeTab, setActiveTab] = useState("shared");
|
||||
const [activeSection, setActiveSection] = useState("detect");
|
||||
const [activeSection, setActiveSection] = useState(defaultSharedSection);
|
||||
|
||||
const { data: config, mutate: refreshConfig } =
|
||||
useSWR<FrigateConfig>("config");
|
||||
const { data: schema } = useSWR<RJSFSchema>("config/schema.json");
|
||||
|
||||
const handleSave = useCallback(() => {
|
||||
refreshConfig();
|
||||
@@ -562,32 +104,29 @@ export default function GlobalConfigView() {
|
||||
const currentSections = useMemo(() => {
|
||||
if (activeTab === "shared") {
|
||||
return sharedSections;
|
||||
} else if (activeTab === "system") {
|
||||
return systemSections.map((key) => ({
|
||||
key,
|
||||
component: null, // Uses GlobalConfigSection instead
|
||||
}));
|
||||
} else {
|
||||
return integrationSections.map((key) => ({
|
||||
key,
|
||||
component: null,
|
||||
}));
|
||||
}
|
||||
if (activeTab === "system") {
|
||||
return systemSections;
|
||||
}
|
||||
return integrationSections;
|
||||
}, [activeTab]);
|
||||
|
||||
// Reset active section when tab changes
|
||||
const handleTabChange = useCallback((tab: string) => {
|
||||
setActiveTab(tab);
|
||||
if (tab === "shared") {
|
||||
setActiveSection("detect");
|
||||
} else if (tab === "system") {
|
||||
setActiveSection("database");
|
||||
} else {
|
||||
setActiveSection("mqtt");
|
||||
}
|
||||
}, []);
|
||||
const handleTabChange = useCallback(
|
||||
(tab: string) => {
|
||||
setActiveTab(tab);
|
||||
if (tab === "shared") {
|
||||
setActiveSection(defaultSharedSection);
|
||||
} else if (tab === "system") {
|
||||
setActiveSection(defaultSystemSection);
|
||||
} else {
|
||||
setActiveSection(defaultIntegrationSection);
|
||||
}
|
||||
},
|
||||
[defaultSharedSection, defaultSystemSection, defaultIntegrationSection],
|
||||
);
|
||||
|
||||
if (!config || !schema) {
|
||||
if (!config) {
|
||||
return (
|
||||
<div className="flex h-full items-center justify-center">
|
||||
<ActivityIndicator />
|
||||
@@ -666,105 +205,42 @@ export default function GlobalConfigView() {
|
||||
|
||||
{/* Section Content */}
|
||||
<div className="scrollbar-container flex-1 overflow-y-auto pr-4">
|
||||
{activeTab === "shared" && (
|
||||
<>
|
||||
{sharedSections.map((section) => {
|
||||
const SectionComponent = section.component;
|
||||
return (
|
||||
<div
|
||||
key={section.key}
|
||||
className={cn(
|
||||
activeSection === section.key ? "block" : "hidden",
|
||||
)}
|
||||
>
|
||||
<Heading as="h4" className="mb-1">
|
||||
{t(`${section.key}.label`, {
|
||||
ns: "config/global",
|
||||
defaultValue:
|
||||
section.key.charAt(0).toUpperCase() +
|
||||
section.key.slice(1).replace(/_/g, " "),
|
||||
})}
|
||||
</Heading>
|
||||
{i18n.exists(`${section.key}.description`, {
|
||||
{currentSections.map((section) => {
|
||||
const SectionComponent = section.component;
|
||||
return (
|
||||
<div
|
||||
key={section.key}
|
||||
className={cn(
|
||||
activeSection === section.key ? "block" : "hidden",
|
||||
)}
|
||||
>
|
||||
<Heading as="h4" className="mb-1">
|
||||
{t(`${section.key}.label`, {
|
||||
ns: "config/global",
|
||||
defaultValue:
|
||||
section.key.charAt(0).toUpperCase() +
|
||||
section.key.slice(1).replace(/_/g, " "),
|
||||
})}
|
||||
</Heading>
|
||||
{i18n.exists(`${section.key}.description`, {
|
||||
ns: "config/global",
|
||||
}) && (
|
||||
<p className="mb-4 text-sm text-muted-foreground">
|
||||
{t(`${section.key}.description`, {
|
||||
ns: "config/global",
|
||||
}) && (
|
||||
<p className="mb-4 text-sm text-muted-foreground">
|
||||
{t(`${section.key}.description`, {
|
||||
ns: "config/global",
|
||||
})}
|
||||
</p>
|
||||
)}
|
||||
})}
|
||||
</p>
|
||||
)}
|
||||
|
||||
<SectionComponent
|
||||
level="global"
|
||||
onSave={handleSave}
|
||||
showTitle={false}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
)}
|
||||
|
||||
{activeTab === "system" && (
|
||||
<>
|
||||
{systemSections.map((sectionKey) => {
|
||||
const sectionTitle = t(`${sectionKey}.label`, {
|
||||
ns: "config/global",
|
||||
defaultValue:
|
||||
sectionKey.charAt(0).toUpperCase() +
|
||||
sectionKey.slice(1).replace(/_/g, " "),
|
||||
});
|
||||
|
||||
return (
|
||||
<div
|
||||
key={sectionKey}
|
||||
className={cn(
|
||||
activeSection === sectionKey ? "block" : "hidden",
|
||||
)}
|
||||
>
|
||||
<GlobalConfigSection
|
||||
sectionKey={sectionKey}
|
||||
schema={extractSchemaSection(schema, sectionKey)}
|
||||
config={config}
|
||||
onSave={handleSave}
|
||||
title={sectionTitle}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
)}
|
||||
|
||||
{activeTab === "integrations" && (
|
||||
<>
|
||||
{integrationSections.map((sectionKey) => {
|
||||
const sectionTitle = t(`${sectionKey}.label`, {
|
||||
ns: "config/global",
|
||||
defaultValue:
|
||||
sectionKey.charAt(0).toUpperCase() +
|
||||
sectionKey.slice(1).replace(/_/g, " "),
|
||||
});
|
||||
|
||||
return (
|
||||
<div
|
||||
key={sectionKey}
|
||||
className={cn(
|
||||
activeSection === sectionKey ? "block" : "hidden",
|
||||
)}
|
||||
>
|
||||
<GlobalConfigSection
|
||||
sectionKey={sectionKey}
|
||||
schema={extractSchemaSection(schema, sectionKey)}
|
||||
config={config}
|
||||
onSave={handleSave}
|
||||
title={sectionTitle}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
)}
|
||||
<SectionComponent
|
||||
level="global"
|
||||
onSave={handleSave}
|
||||
showTitle={false}
|
||||
sectionConfig={getSectionConfig(section.key, "global")}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</Tabs>
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
import Heading from "@/components/ui/heading";
|
||||
import type {
|
||||
BaseSectionProps,
|
||||
SectionConfig,
|
||||
} from "@/components/config-form/sections";
|
||||
import type { PolygonType } from "@/types/canvas";
|
||||
|
||||
export type SettingsPageProps = {
|
||||
selectedCamera?: string;
|
||||
setUnsavedChanges?: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
selectedZoneMask?: PolygonType[];
|
||||
};
|
||||
|
||||
export type SingleSectionPageOptions = {
|
||||
sectionKey: string;
|
||||
level: "global" | "camera";
|
||||
SectionComponent: React.ComponentType<BaseSectionProps>;
|
||||
sectionConfig?: SectionConfig;
|
||||
requiresRestart?: boolean;
|
||||
showOverrideIndicator?: boolean;
|
||||
};
|
||||
|
||||
export function createSingleSectionPage({
|
||||
sectionKey,
|
||||
level,
|
||||
SectionComponent,
|
||||
sectionConfig,
|
||||
requiresRestart,
|
||||
showOverrideIndicator = true,
|
||||
}: SingleSectionPageOptions) {
|
||||
return function SingleSectionPage({
|
||||
selectedCamera,
|
||||
setUnsavedChanges,
|
||||
}: SettingsPageProps) {
|
||||
const sectionNamespace =
|
||||
level === "camera" ? "config/cameras" : "config/global";
|
||||
const { t, i18n } = useTranslation([
|
||||
sectionNamespace,
|
||||
"views/settings",
|
||||
"common",
|
||||
]);
|
||||
|
||||
if (level === "camera" && !selectedCamera) {
|
||||
return (
|
||||
<div className="flex h-full items-center justify-center text-muted-foreground">
|
||||
{t("configForm.camera.noCameras", { ns: "views/settings" })}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex size-full flex-col pr-2">
|
||||
<div className="mb-4">
|
||||
<Heading as="h2">
|
||||
{t(`${sectionKey}.label`, { ns: sectionNamespace })}
|
||||
</Heading>
|
||||
{i18n.exists(`${sectionKey}.description`, {
|
||||
ns: sectionNamespace,
|
||||
}) && (
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{t(`${sectionKey}.description`, { ns: sectionNamespace })}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
<SectionComponent
|
||||
level={level}
|
||||
cameraName={level === "camera" ? selectedCamera : undefined}
|
||||
showOverrideIndicator={showOverrideIndicator}
|
||||
onSave={() => setUnsavedChanges?.(false)}
|
||||
showTitle={false}
|
||||
sectionConfig={sectionConfig}
|
||||
requiresRestart={requiresRestart}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user