add docs links, readonly keys, and restart required per field

This commit is contained in:
Josh Hawkins 2026-02-02 09:20:49 -06:00
parent edc980ab8b
commit 59b0f3a680
37 changed files with 236 additions and 35 deletions

View File

@ -1286,6 +1286,7 @@
}, },
"toast": { "toast": {
"success": "Settings saved successfully", "success": "Settings saved successfully",
"successRestartRequired": "Settings saved successfully. Restart Frigate to apply your changes.",
"error": "Failed to save settings", "error": "Failed to save settings",
"validationError": "Validation failed: {{message}}", "validationError": "Validation failed: {{message}}",
"resetSuccess": "Reset to global defaults", "resetSuccess": "Reset to global defaults",

View File

@ -2,6 +2,8 @@ import type { SectionConfigOverrides } from "./types";
const audio: SectionConfigOverrides = { const audio: SectionConfigOverrides = {
base: { base: {
sectionDocs: "/configuration/audio_detectors",
restartRequired: [],
fieldOrder: [ fieldOrder: [
"enabled", "enabled",
"listen", "listen",

View File

@ -2,6 +2,8 @@ import type { SectionConfigOverrides } from "./types";
const audioTranscription: SectionConfigOverrides = { const audioTranscription: SectionConfigOverrides = {
base: { base: {
sectionDocs: "/configuration/audio_detectors#audio-transcription",
restartRequired: [],
fieldOrder: ["enabled", "language", "device", "model_size", "live_enabled"], fieldOrder: ["enabled", "language", "device", "model_size", "live_enabled"],
hiddenFields: ["enabled_in_config"], hiddenFields: ["enabled_in_config"],
advancedFields: ["language", "device", "model_size"], advancedFields: ["language", "device", "model_size"],

View File

@ -2,6 +2,8 @@ import type { SectionConfigOverrides } from "./types";
const auth: SectionConfigOverrides = { const auth: SectionConfigOverrides = {
base: { base: {
sectionDocs: "/configuration/authentication",
restartRequired: [],
fieldOrder: [ fieldOrder: [
"enabled", "enabled",
"reset_admin_password", "reset_admin_password",

View File

@ -2,6 +2,8 @@ import type { SectionConfigOverrides } from "./types";
const birdseye: SectionConfigOverrides = { const birdseye: SectionConfigOverrides = {
base: { base: {
sectionDocs: "/configuration/birdseye",
restartRequired: [],
fieldOrder: ["enabled", "mode", "order"], fieldOrder: ["enabled", "mode", "order"],
hiddenFields: [], hiddenFields: [],
advancedFields: [], advancedFields: [],

View File

@ -2,6 +2,8 @@ import type { SectionConfigOverrides } from "./types";
const classification: SectionConfigOverrides = { const classification: SectionConfigOverrides = {
base: { base: {
sectionDocs: "/configuration/custom_classification/object_classification",
restartRequired: [],
hiddenFields: ["custom"], hiddenFields: ["custom"],
advancedFields: [], advancedFields: [],
}, },

View File

@ -2,6 +2,8 @@ import type { SectionConfigOverrides } from "./types";
const database: SectionConfigOverrides = { const database: SectionConfigOverrides = {
base: { base: {
sectionDocs: "/configuration/advanced#database",
restartRequired: [],
fieldOrder: ["path"], fieldOrder: ["path"],
advancedFields: [], advancedFields: [],
}, },

View File

@ -2,6 +2,8 @@ import type { SectionConfigOverrides } from "./types";
const detect: SectionConfigOverrides = { const detect: SectionConfigOverrides = {
base: { base: {
sectionDocs: "/configuration/camera_specific",
restartRequired: [],
fieldOrder: [ fieldOrder: [
"enabled", "enabled",
"fps", "fps",

View File

@ -2,6 +2,8 @@ import type { SectionConfigOverrides } from "./types";
const detectors: SectionConfigOverrides = { const detectors: SectionConfigOverrides = {
base: { base: {
sectionDocs: "/configuration/object_detectors",
restartRequired: [],
fieldOrder: [], fieldOrder: [],
advancedFields: [], advancedFields: [],
hiddenFields: [ hiddenFields: [

View File

@ -2,6 +2,8 @@ import type { SectionConfigOverrides } from "./types";
const environmentVars: SectionConfigOverrides = { const environmentVars: SectionConfigOverrides = {
base: { base: {
sectionDocs: "/configuration/advanced#environment_vars",
restartRequired: [],
fieldOrder: [], fieldOrder: [],
advancedFields: [], advancedFields: [],
}, },

View File

@ -2,6 +2,8 @@ import type { SectionConfigOverrides } from "./types";
const faceRecognition: SectionConfigOverrides = { const faceRecognition: SectionConfigOverrides = {
base: { base: {
sectionDocs: "/configuration/face_recognition",
restartRequired: [],
fieldOrder: ["enabled", "min_area"], fieldOrder: ["enabled", "min_area"],
hiddenFields: [], hiddenFields: [],
advancedFields: ["min_area"], advancedFields: ["min_area"],

View File

@ -2,6 +2,8 @@ import type { SectionConfigOverrides } from "./types";
const ffmpeg: SectionConfigOverrides = { const ffmpeg: SectionConfigOverrides = {
base: { base: {
sectionDocs: "/configuration/ffmpeg_presets",
restartRequired: [],
fieldOrder: [ fieldOrder: [
"inputs", "inputs",
"path", "path",

View File

@ -2,6 +2,8 @@ import type { SectionConfigOverrides } from "./types";
const genai: SectionConfigOverrides = { const genai: SectionConfigOverrides = {
base: { base: {
sectionDocs: "/configuration/genai/config",
restartRequired: [],
fieldOrder: [ fieldOrder: [
"provider", "provider",
"api_key", "api_key",

View File

@ -2,6 +2,8 @@ import type { SectionConfigOverrides } from "./types";
const live: SectionConfigOverrides = { const live: SectionConfigOverrides = {
base: { base: {
sectionDocs: "/configuration/live",
restartRequired: [],
fieldOrder: ["stream_name", "height", "quality"], fieldOrder: ["stream_name", "height", "quality"],
fieldGroups: {}, fieldGroups: {},
hiddenFields: ["enabled_in_config"], hiddenFields: ["enabled_in_config"],

View File

@ -2,6 +2,8 @@ import type { SectionConfigOverrides } from "./types";
const logger: SectionConfigOverrides = { const logger: SectionConfigOverrides = {
base: { base: {
sectionDocs: "/configuration/advanced#logger",
restartRequired: [],
fieldOrder: ["default", "logs"], fieldOrder: ["default", "logs"],
advancedFields: ["logs"], advancedFields: ["logs"],
}, },

View File

@ -2,6 +2,8 @@ import type { SectionConfigOverrides } from "./types";
const lpr: SectionConfigOverrides = { const lpr: SectionConfigOverrides = {
base: { base: {
sectionDocs: "/configuration/license_plate_recognition",
restartRequired: [],
fieldOrder: ["enabled", "expire_time", "min_area", "enhancement"], fieldOrder: ["enabled", "expire_time", "min_area", "enhancement"],
hiddenFields: [], hiddenFields: [],
advancedFields: ["expire_time", "min_area", "enhancement"], advancedFields: ["expire_time", "min_area", "enhancement"],

View File

@ -2,6 +2,8 @@ import type { SectionConfigOverrides } from "./types";
const model: SectionConfigOverrides = { const model: SectionConfigOverrides = {
base: { base: {
sectionDocs: "/configuration/object_detectors#model",
restartRequired: [],
fieldOrder: [ fieldOrder: [
"path", "path",
"labelmap_path", "labelmap_path",

View File

@ -2,6 +2,8 @@ import type { SectionConfigOverrides } from "./types";
const motion: SectionConfigOverrides = { const motion: SectionConfigOverrides = {
base: { base: {
sectionDocs: "/configuration/motion_detection",
restartRequired: [],
fieldOrder: [ fieldOrder: [
"enabled", "enabled",
"threshold", "threshold",

View File

@ -2,6 +2,8 @@ import type { SectionConfigOverrides } from "./types";
const mqtt: SectionConfigOverrides = { const mqtt: SectionConfigOverrides = {
base: { base: {
sectionDocs: "/integrations/mqtt",
restartRequired: [],
fieldOrder: [ fieldOrder: [
"enabled", "enabled",
"timestamp", "timestamp",

View File

@ -2,6 +2,8 @@ import type { SectionConfigOverrides } from "./types";
const networking: SectionConfigOverrides = { const networking: SectionConfigOverrides = {
base: { base: {
sectionDocs: "/configuration/reference",
restartRequired: [],
fieldOrder: [], fieldOrder: [],
advancedFields: [], advancedFields: [],
}, },

View File

@ -2,6 +2,8 @@ import type { SectionConfigOverrides } from "./types";
const notifications: SectionConfigOverrides = { const notifications: SectionConfigOverrides = {
base: { base: {
sectionDocs: "/configuration/notifications",
restartRequired: [],
fieldOrder: ["enabled", "email"], fieldOrder: ["enabled", "email"],
fieldGroups: {}, fieldGroups: {},
hiddenFields: ["enabled_in_config"], hiddenFields: ["enabled_in_config"],

View File

@ -2,6 +2,8 @@ import type { SectionConfigOverrides } from "./types";
const objects: SectionConfigOverrides = { const objects: SectionConfigOverrides = {
base: { base: {
sectionDocs: "/configuration/object_filters",
restartRequired: [],
fieldOrder: ["track", "alert", "detect", "filters"], fieldOrder: ["track", "alert", "detect", "filters"],
fieldGroups: { fieldGroups: {
tracking: ["track", "alert", "detect"], tracking: ["track", "alert", "detect"],
@ -14,6 +16,8 @@ const objects: SectionConfigOverrides = {
"genai.enabled_in_config", "genai.enabled_in_config",
"filters.*.mask", "filters.*.mask",
"filters.*.raw_mask", "filters.*.raw_mask",
"filters.mask",
"filters.raw_mask",
], ],
advancedFields: ["filters"], advancedFields: ["filters"],
uiSchema: { uiSchema: {
@ -22,6 +26,11 @@ const objects: SectionConfigOverrides = {
suppressMultiSchema: true, suppressMultiSchema: true,
}, },
}, },
"filters.*": {
"ui:options": {
additionalPropertyKeyReadonly: true,
},
},
"filters.*.max_area": { "filters.*.max_area": {
"ui:options": { "ui:options": {
suppressMultiSchema: true, suppressMultiSchema: true,
@ -56,7 +65,17 @@ const objects: SectionConfigOverrides = {
}, },
}, },
global: { global: {
hiddenFields: ["genai.required_zones"], hiddenFields: [
"enabled_in_config",
"mask",
"raw_mask",
"genai.enabled_in_config",
"filters.*.mask",
"filters.*.raw_mask",
"filters.mask",
"filters.raw_mask",
"genai.required_zones",
],
}, },
}; };

View File

@ -2,6 +2,8 @@ import type { SectionConfigOverrides } from "./types";
const onvif: SectionConfigOverrides = { const onvif: SectionConfigOverrides = {
base: { base: {
sectionDocs: "/configuration/cameras#setting-up-camera-ptz-controls",
restartRequired: [],
fieldOrder: [ fieldOrder: [
"host", "host",
"port", "port",

View File

@ -2,6 +2,8 @@ import type { SectionConfigOverrides } from "./types";
const proxy: SectionConfigOverrides = { const proxy: SectionConfigOverrides = {
base: { base: {
sectionDocs: "/configuration/authentication#proxy",
restartRequired: [],
fieldOrder: [ fieldOrder: [
"header_map", "header_map",
"logout_url", "logout_url",

View File

@ -2,6 +2,8 @@ import type { SectionConfigOverrides } from "./types";
const record: SectionConfigOverrides = { const record: SectionConfigOverrides = {
base: { base: {
sectionDocs: "/configuration/record",
restartRequired: [],
fieldOrder: [ fieldOrder: [
"enabled", "enabled",
"expire_interval", "expire_interval",

View File

@ -2,6 +2,8 @@ import type { SectionConfigOverrides } from "./types";
const review: SectionConfigOverrides = { const review: SectionConfigOverrides = {
base: { base: {
sectionDocs: "/configuration/review",
restartRequired: [],
fieldOrder: ["alerts", "detections", "genai"], fieldOrder: ["alerts", "detections", "genai"],
fieldGroups: {}, fieldGroups: {},
hiddenFields: [ hiddenFields: [

View File

@ -2,6 +2,8 @@ import type { SectionConfigOverrides } from "./types";
const semanticSearch: SectionConfigOverrides = { const semanticSearch: SectionConfigOverrides = {
base: { base: {
sectionDocs: "/configuration/semantic_search",
restartRequired: [],
fieldOrder: ["triggers"], fieldOrder: ["triggers"],
hiddenFields: [], hiddenFields: [],
advancedFields: [], advancedFields: [],

View File

@ -2,6 +2,8 @@ import type { SectionConfigOverrides } from "./types";
const snapshots: SectionConfigOverrides = { const snapshots: SectionConfigOverrides = {
base: { base: {
sectionDocs: "/configuration/snapshots",
restartRequired: [],
fieldOrder: [ fieldOrder: [
"enabled", "enabled",
"bounding_box", "bounding_box",

View File

@ -2,6 +2,8 @@ import type { SectionConfigOverrides } from "./types";
const telemetry: SectionConfigOverrides = { const telemetry: SectionConfigOverrides = {
base: { base: {
sectionDocs: "/configuration/reference",
restartRequired: [],
fieldOrder: ["network_interfaces", "stats", "version_check"], fieldOrder: ["network_interfaces", "stats", "version_check"],
advancedFields: [], advancedFields: [],
}, },

View File

@ -2,6 +2,8 @@ import type { SectionConfigOverrides } from "./types";
const timestampStyle: SectionConfigOverrides = { const timestampStyle: SectionConfigOverrides = {
base: { base: {
sectionDocs: "/configuration/reference",
restartRequired: [],
fieldOrder: ["position", "format", "color", "thickness"], fieldOrder: ["position", "format", "color", "thickness"],
hiddenFields: ["effect", "enabled_in_config"], hiddenFields: ["effect", "enabled_in_config"],
advancedFields: [], advancedFields: [],

View File

@ -2,6 +2,8 @@ import type { SectionConfigOverrides } from "./types";
const tls: SectionConfigOverrides = { const tls: SectionConfigOverrides = {
base: { base: {
sectionDocs: "/configuration/tls",
restartRequired: [],
fieldOrder: ["enabled", "cert", "key"], fieldOrder: ["enabled", "cert", "key"],
advancedFields: [], advancedFields: [],
}, },

View File

@ -2,6 +2,8 @@ import type { SectionConfigOverrides } from "./types";
const ui: SectionConfigOverrides = { const ui: SectionConfigOverrides = {
base: { base: {
sectionDocs: "/configuration/reference",
restartRequired: [],
fieldOrder: ["dashboard", "order"], fieldOrder: ["dashboard", "order"],
hiddenFields: [], hiddenFields: [],
advancedFields: [], advancedFields: [],

View File

@ -61,6 +61,12 @@ export interface SectionConfig {
advancedFields?: string[]; advancedFields?: string[];
/** Fields to compare for override detection */ /** Fields to compare for override detection */
overrideFields?: string[]; overrideFields?: string[];
/** Documentation link for the section */
sectionDocs?: string;
/** Per-field documentation links */
fieldDocs?: Record<string, string>;
/** Fields that require restart when modified (empty means all fields) */
restartRequired?: string[];
/** Whether to enable live validation */ /** Whether to enable live validation */
liveValidate?: boolean; liveValidate?: boolean;
/** Additional uiSchema overrides */ /** Additional uiSchema overrides */
@ -402,6 +408,27 @@ export function ConfigSection({
], ],
); );
const requiresRestartForOverrides = useCallback(
(overrides: unknown) => {
if (sectionConfig.restartRequired === undefined) {
return requiresRestart;
}
if (sectionConfig.restartRequired.length === 0) {
return true;
}
if (!overrides || typeof overrides !== "object") {
return false;
}
return sectionConfig.restartRequired.some(
(path) => get(overrides as JsonObject, path) !== undefined,
);
},
[requiresRestart, sectionConfig.restartRequired],
);
const handleReset = useCallback(() => { const handleReset = useCallback(() => {
isResettingRef.current = true; isResettingRef.current = true;
setPendingData(null); setPendingData(null);
@ -426,8 +453,10 @@ export function ConfigSection({
return; return;
} }
const needsRestart = requiresRestartForOverrides(overrides);
await axios.put("config/sett", { await axios.put("config/sett", {
requires_restart: requiresRestart ? 0 : 1, requires_restart: needsRestart ? 1 : 0,
update_topic: updateTopic, update_topic: updateTopic,
config_data: { config_data: {
[basePath]: overrides, [basePath]: overrides,
@ -438,13 +467,15 @@ export function ConfigSection({
console.log("Saved config data:", { console.log("Saved config data:", {
[basePath]: overrides, [basePath]: overrides,
update_topic: updateTopic, update_topic: updateTopic,
requires_restart: requiresRestart ? 0 : 1, requires_restart: needsRestart ? 1 : 0,
}); });
toast.success( toast.success(
t("toast.success", { t(needsRestart ? "toast.successRestartRequired" : "toast.success", {
ns: "views/settings", ns: "views/settings",
defaultValue: "Settings saved successfully", defaultValue: needsRestart
? "Settings saved successfully. Restart Frigate to apply your changes."
: "Settings saved successfully",
}), }),
); );
@ -494,7 +525,6 @@ export function ConfigSection({
pendingData, pendingData,
level, level,
cameraName, cameraName,
requiresRestart,
t, t,
refreshConfig, refreshConfig,
onSave, onSave,
@ -504,6 +534,7 @@ export function ConfigSection({
schemaDefaults, schemaDefaults,
updateTopic, updateTopic,
setPendingData, setPendingData,
requiresRestartForOverrides,
]); ]);
// Handle reset to global/defaults - removes camera-level override or resets global to defaults // Handle reset to global/defaults - removes camera-level override or resets global to defaults
@ -662,6 +693,8 @@ export function ConfigSection({
t, t,
renderers: renderers:
sectionConfig?.renderers ?? sectionRenderers?.[sectionPath], sectionConfig?.renderers ?? sectionRenderers?.[sectionPath],
sectionDocs: sectionConfig.sectionDocs,
fieldDocs: sectionConfig.fieldDocs,
}} }}
/> />

View File

@ -13,6 +13,9 @@ import { useTranslation } from "react-i18next";
import { isNullableUnionSchema } from "../fields/nullableUtils"; import { isNullableUnionSchema } from "../fields/nullableUtils";
import { getTranslatedLabel } from "@/utils/i18n"; import { getTranslatedLabel } from "@/utils/i18n";
import { ConfigFormContext } from "@/types/configForm"; import { ConfigFormContext } from "@/types/configForm";
import { Link } from "react-router-dom";
import { LuExternalLink } from "react-icons/lu";
import { useDocDomain } from "@/hooks/use-doc-domain";
/** /**
* Build the i18n translation key path for nested fields using the field path * Build the i18n translation key path for nested fields using the field path
@ -114,6 +117,7 @@ export function FieldTemplate(props: FieldTemplateProps) {
i18nNamespace || "common", i18nNamespace || "common",
"views/settings", "views/settings",
]); ]);
const { getLocaleDocUrl } = useDocDomain();
if (hidden) { if (hidden) {
return <div className="hidden">{children}</div>; return <div className="hidden">{children}</div>;
@ -148,6 +152,13 @@ export function FieldTemplate(props: FieldTemplateProps) {
const translatedFilterObjectLabel = filterObjectLabel const translatedFilterObjectLabel = filterObjectLabel
? getTranslatedLabel(filterObjectLabel, "object") ? getTranslatedLabel(filterObjectLabel, "object")
: undefined; : undefined;
const fieldDocsKey = translationPath || pathSegments.join(".");
const fieldDocsPath = fieldDocsKey
? formContext?.fieldDocs?.[fieldDocsKey]
: undefined;
const fieldDocsUrl = fieldDocsPath
? getLocaleDocUrl(fieldDocsPath)
: undefined;
// Use schema title/description as primary source (from JSON Schema) // Use schema title/description as primary source (from JSON Schema)
const schemaTitle = schema.title; const schemaTitle = schema.title;
@ -394,6 +405,19 @@ export function FieldTemplate(props: FieldTemplateProps) {
{finalDescription} {finalDescription}
</p> </p>
)} )}
{fieldDocsUrl && !isMultiSchemaWrapper && !isObjectField && (
<div className="flex items-center text-xs text-primary">
<Link
to={fieldDocsUrl}
target="_blank"
rel="noopener noreferrer"
className="inline"
>
{t("readTheDocumentation", { ns: "common" })}
<LuExternalLink className="ml-2 inline-flex size-3" />
</Link>
</div>
)}
</div> </div>
<div className="flex items-center gap-2">{children}</div> <div className="flex items-center gap-2">{children}</div>
</div> </div>
@ -406,6 +430,19 @@ export function FieldTemplate(props: FieldTemplateProps) {
{finalDescription} {finalDescription}
</p> </p>
)} )}
{fieldDocsUrl && !isMultiSchemaWrapper && !isObjectField && (
<div className="flex items-center text-xs text-primary">
<Link
to={fieldDocsUrl}
target="_blank"
rel="noopener noreferrer"
className="inline"
>
{t("readTheDocumentation", { ns: "common" })}
<LuExternalLink className="ml-2 inline-flex size-3" />
</Link>
</div>
)}
</> </>
)} )}

View File

@ -1,6 +1,7 @@
import { import {
ADDITIONAL_PROPERTY_FLAG, ADDITIONAL_PROPERTY_FLAG,
FormContextType, FormContextType,
getUiOptions,
RJSFSchema, RJSFSchema,
StrictRJSFSchema, StrictRJSFSchema,
WrapIfAdditionalTemplateProps, WrapIfAdditionalTemplateProps,
@ -30,6 +31,7 @@ export function WrapIfAdditionalTemplate<
readonly, readonly,
required, required,
schema, schema,
uiSchema,
} = props; } = props;
const { t } = useTranslation(["views/settings"]); const { t } = useTranslation(["views/settings"]);
@ -57,41 +59,63 @@ export function WrapIfAdditionalTemplate<
const removeLabel = t("configForm.additionalProperties.remove", { const removeLabel = t("configForm.additionalProperties.remove", {
ns: "views/settings", ns: "views/settings",
}); });
const uiOptions = getUiOptions(uiSchema);
const keyIsReadonly = uiOptions.additionalPropertyKeyReadonly === true;
return ( return (
<div <div
className={cn("grid grid-cols-12 items-start gap-2", classNames)} className={cn("grid grid-cols-12 items-start gap-2", classNames)}
style={style} style={style}
> >
<div className="col-span-12 space-y-2 md:col-span-1"> {!keyIsReadonly && (
{displayLabel && <Label htmlFor={keyId}>{keyLabel}</Label>} <div className="col-span-12 space-y-2 md:col-span-1">
<Input {displayLabel && <Label htmlFor={keyId}>{keyLabel}</Label>}
id={keyId} {keyIsReadonly ? (
name={keyId} <div
required={required} id={keyId}
defaultValue={label} className="flex items-center text-sm text-muted-foreground"
placeholder={keyPlaceholder} >
disabled={disabled || readonly} {label}
onBlur={!readonly ? onKeyRenameBlur : undefined} </div>
/> ) : (
</div> <Input
<div className="col-span-12 space-y-2 md:col-span-10"> id={keyId}
{displayLabel && <Label htmlFor={id}>{valueLabel}</Label>} name={keyId}
required={required}
defaultValue={label}
placeholder={keyPlaceholder}
disabled={disabled || readonly}
onBlur={!readonly ? onKeyRenameBlur : undefined}
/>
)}
</div>
)}
<div
className={cn(
"col-span-12 space-y-2",
!keyIsReadonly && "md:col-span-10",
)}
>
{!keyIsReadonly && displayLabel && (
<Label htmlFor={id}>{valueLabel}</Label>
)}
<div className="min-w-0">{children}</div> <div className="min-w-0">{children}</div>
</div> </div>
<div className="col-span-12 flex items-center md:col-span-1 md:justify-center"> {!keyIsReadonly && (
<Button <div className="col-span-12 flex items-center md:col-span-1 md:justify-center">
type="button" <Button
variant="ghost" type="button"
size="icon" variant="ghost"
onClick={onRemoveProperty} size="icon"
disabled={disabled || readonly} onClick={onRemoveProperty}
aria-label={removeLabel} disabled={disabled || readonly}
title={removeLabel} aria-label={removeLabel}
> title={removeLabel}
<LuTrash2 className="h-4 w-4" /> >
</Button> <LuTrash2 className="h-4 w-4" />
</div> </Button>
</div>
)}
</div> </div>
); );
} }

View File

@ -22,6 +22,8 @@ export type ConfigFormContext = {
fullConfig?: FrigateConfig; fullConfig?: FrigateConfig;
i18nNamespace?: string; i18nNamespace?: string;
sectionI18nPrefix?: string; sectionI18nPrefix?: string;
sectionDocs?: string;
fieldDocs?: Record<string, string>;
t?: (key: string, options?: Record<string, unknown>) => string; t?: (key: string, options?: Record<string, unknown>) => string;
renderers?: Record<string, RendererComponent>; renderers?: Record<string, RendererComponent>;
}; };

View File

@ -1,10 +1,14 @@
import { useCallback, useState } from "react"; import { useCallback, useMemo, useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
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"; import { Badge } from "@/components/ui/badge";
import type { ConfigSectionData } from "@/types/configForm"; import type { ConfigSectionData } from "@/types/configForm";
import { getSectionConfig } from "@/utils/sectionConfigsUtils";
import { useDocDomain } from "@/hooks/use-doc-domain";
import { Link } from "react-router-dom";
import { LuExternalLink } from "react-icons/lu";
export type SettingsPageProps = { export type SettingsPageProps = {
selectedCamera?: string; selectedCamera?: string;
@ -58,10 +62,18 @@ export function SingleSectionPage({
"views/settings", "views/settings",
"common", "common",
]); ]);
const { getLocaleDocUrl } = useDocDomain();
const [sectionStatus, setSectionStatus] = useState<SectionStatus>({ const [sectionStatus, setSectionStatus] = useState<SectionStatus>({
hasChanges: false, hasChanges: false,
isOverridden: false, isOverridden: false,
}); });
const resolvedSectionConfig = useMemo(
() => sectionConfig ?? getSectionConfig(sectionKey, level),
[level, sectionConfig, sectionKey],
);
const sectionDocsUrl = resolvedSectionConfig.sectionDocs
? getLocaleDocUrl(resolvedSectionConfig.sectionDocs)
: undefined;
const handleSectionStatusChange = useCallback( const handleSectionStatusChange = useCallback(
(status: SectionStatus) => { (status: SectionStatus) => {
@ -93,6 +105,19 @@ export function SingleSectionPage({
{t(`${sectionKey}.description`, { ns: sectionNamespace })} {t(`${sectionKey}.description`, { ns: sectionNamespace })}
</div> </div>
)} )}
{sectionDocsUrl && (
<div className="flex items-center text-sm text-primary">
<Link
to={sectionDocsUrl}
target="_blank"
rel="noopener noreferrer"
className="inline"
>
{t("readTheDocumentation", { ns: "common" })}
<LuExternalLink className="ml-2 inline-flex size-3" />
</Link>
</div>
)}
</div> </div>
<div className="flex flex-col items-end gap-2 md:flex-row md:items-center"> <div className="flex flex-col items-end gap-2 md:flex-row md:items-center">
<div className="flex flex-wrap items-center justify-end gap-2"> <div className="flex flex-wrap items-center justify-end gap-2">
@ -127,7 +152,7 @@ export function SingleSectionPage({
showOverrideIndicator={showOverrideIndicator} showOverrideIndicator={showOverrideIndicator}
onSave={() => setUnsavedChanges?.(false)} onSave={() => setUnsavedChanges?.(false)}
showTitle={false} showTitle={false}
sectionConfig={sectionConfig} sectionConfig={resolvedSectionConfig}
pendingDataBySection={pendingDataBySection} pendingDataBySection={pendingDataBySection}
onPendingDataChange={onPendingDataChange} onPendingDataChange={onPendingDataChange}
requiresRestart={requiresRestart} requiresRestart={requiresRestart}