mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-03-10 10:33:11 +03:00
add docs links, readonly keys, and restart required per field
This commit is contained in:
parent
edc980ab8b
commit
59b0f3a680
@ -1286,6 +1286,7 @@
|
||||
},
|
||||
"toast": {
|
||||
"success": "Settings saved successfully",
|
||||
"successRestartRequired": "Settings saved successfully. Restart Frigate to apply your changes.",
|
||||
"error": "Failed to save settings",
|
||||
"validationError": "Validation failed: {{message}}",
|
||||
"resetSuccess": "Reset to global defaults",
|
||||
|
||||
@ -2,6 +2,8 @@ import type { SectionConfigOverrides } from "./types";
|
||||
|
||||
const audio: SectionConfigOverrides = {
|
||||
base: {
|
||||
sectionDocs: "/configuration/audio_detectors",
|
||||
restartRequired: [],
|
||||
fieldOrder: [
|
||||
"enabled",
|
||||
"listen",
|
||||
|
||||
@ -2,6 +2,8 @@ import type { SectionConfigOverrides } from "./types";
|
||||
|
||||
const audioTranscription: SectionConfigOverrides = {
|
||||
base: {
|
||||
sectionDocs: "/configuration/audio_detectors#audio-transcription",
|
||||
restartRequired: [],
|
||||
fieldOrder: ["enabled", "language", "device", "model_size", "live_enabled"],
|
||||
hiddenFields: ["enabled_in_config"],
|
||||
advancedFields: ["language", "device", "model_size"],
|
||||
|
||||
@ -2,6 +2,8 @@ import type { SectionConfigOverrides } from "./types";
|
||||
|
||||
const auth: SectionConfigOverrides = {
|
||||
base: {
|
||||
sectionDocs: "/configuration/authentication",
|
||||
restartRequired: [],
|
||||
fieldOrder: [
|
||||
"enabled",
|
||||
"reset_admin_password",
|
||||
|
||||
@ -2,6 +2,8 @@ import type { SectionConfigOverrides } from "./types";
|
||||
|
||||
const birdseye: SectionConfigOverrides = {
|
||||
base: {
|
||||
sectionDocs: "/configuration/birdseye",
|
||||
restartRequired: [],
|
||||
fieldOrder: ["enabled", "mode", "order"],
|
||||
hiddenFields: [],
|
||||
advancedFields: [],
|
||||
|
||||
@ -2,6 +2,8 @@ import type { SectionConfigOverrides } from "./types";
|
||||
|
||||
const classification: SectionConfigOverrides = {
|
||||
base: {
|
||||
sectionDocs: "/configuration/custom_classification/object_classification",
|
||||
restartRequired: [],
|
||||
hiddenFields: ["custom"],
|
||||
advancedFields: [],
|
||||
},
|
||||
|
||||
@ -2,6 +2,8 @@ import type { SectionConfigOverrides } from "./types";
|
||||
|
||||
const database: SectionConfigOverrides = {
|
||||
base: {
|
||||
sectionDocs: "/configuration/advanced#database",
|
||||
restartRequired: [],
|
||||
fieldOrder: ["path"],
|
||||
advancedFields: [],
|
||||
},
|
||||
|
||||
@ -2,6 +2,8 @@ import type { SectionConfigOverrides } from "./types";
|
||||
|
||||
const detect: SectionConfigOverrides = {
|
||||
base: {
|
||||
sectionDocs: "/configuration/camera_specific",
|
||||
restartRequired: [],
|
||||
fieldOrder: [
|
||||
"enabled",
|
||||
"fps",
|
||||
|
||||
@ -2,6 +2,8 @@ import type { SectionConfigOverrides } from "./types";
|
||||
|
||||
const detectors: SectionConfigOverrides = {
|
||||
base: {
|
||||
sectionDocs: "/configuration/object_detectors",
|
||||
restartRequired: [],
|
||||
fieldOrder: [],
|
||||
advancedFields: [],
|
||||
hiddenFields: [
|
||||
|
||||
@ -2,6 +2,8 @@ import type { SectionConfigOverrides } from "./types";
|
||||
|
||||
const environmentVars: SectionConfigOverrides = {
|
||||
base: {
|
||||
sectionDocs: "/configuration/advanced#environment_vars",
|
||||
restartRequired: [],
|
||||
fieldOrder: [],
|
||||
advancedFields: [],
|
||||
},
|
||||
|
||||
@ -2,6 +2,8 @@ import type { SectionConfigOverrides } from "./types";
|
||||
|
||||
const faceRecognition: SectionConfigOverrides = {
|
||||
base: {
|
||||
sectionDocs: "/configuration/face_recognition",
|
||||
restartRequired: [],
|
||||
fieldOrder: ["enabled", "min_area"],
|
||||
hiddenFields: [],
|
||||
advancedFields: ["min_area"],
|
||||
|
||||
@ -2,6 +2,8 @@ import type { SectionConfigOverrides } from "./types";
|
||||
|
||||
const ffmpeg: SectionConfigOverrides = {
|
||||
base: {
|
||||
sectionDocs: "/configuration/ffmpeg_presets",
|
||||
restartRequired: [],
|
||||
fieldOrder: [
|
||||
"inputs",
|
||||
"path",
|
||||
|
||||
@ -2,6 +2,8 @@ import type { SectionConfigOverrides } from "./types";
|
||||
|
||||
const genai: SectionConfigOverrides = {
|
||||
base: {
|
||||
sectionDocs: "/configuration/genai/config",
|
||||
restartRequired: [],
|
||||
fieldOrder: [
|
||||
"provider",
|
||||
"api_key",
|
||||
|
||||
@ -2,6 +2,8 @@ import type { SectionConfigOverrides } from "./types";
|
||||
|
||||
const live: SectionConfigOverrides = {
|
||||
base: {
|
||||
sectionDocs: "/configuration/live",
|
||||
restartRequired: [],
|
||||
fieldOrder: ["stream_name", "height", "quality"],
|
||||
fieldGroups: {},
|
||||
hiddenFields: ["enabled_in_config"],
|
||||
|
||||
@ -2,6 +2,8 @@ import type { SectionConfigOverrides } from "./types";
|
||||
|
||||
const logger: SectionConfigOverrides = {
|
||||
base: {
|
||||
sectionDocs: "/configuration/advanced#logger",
|
||||
restartRequired: [],
|
||||
fieldOrder: ["default", "logs"],
|
||||
advancedFields: ["logs"],
|
||||
},
|
||||
|
||||
@ -2,6 +2,8 @@ import type { SectionConfigOverrides } from "./types";
|
||||
|
||||
const lpr: SectionConfigOverrides = {
|
||||
base: {
|
||||
sectionDocs: "/configuration/license_plate_recognition",
|
||||
restartRequired: [],
|
||||
fieldOrder: ["enabled", "expire_time", "min_area", "enhancement"],
|
||||
hiddenFields: [],
|
||||
advancedFields: ["expire_time", "min_area", "enhancement"],
|
||||
|
||||
@ -2,6 +2,8 @@ import type { SectionConfigOverrides } from "./types";
|
||||
|
||||
const model: SectionConfigOverrides = {
|
||||
base: {
|
||||
sectionDocs: "/configuration/object_detectors#model",
|
||||
restartRequired: [],
|
||||
fieldOrder: [
|
||||
"path",
|
||||
"labelmap_path",
|
||||
|
||||
@ -2,6 +2,8 @@ import type { SectionConfigOverrides } from "./types";
|
||||
|
||||
const motion: SectionConfigOverrides = {
|
||||
base: {
|
||||
sectionDocs: "/configuration/motion_detection",
|
||||
restartRequired: [],
|
||||
fieldOrder: [
|
||||
"enabled",
|
||||
"threshold",
|
||||
|
||||
@ -2,6 +2,8 @@ import type { SectionConfigOverrides } from "./types";
|
||||
|
||||
const mqtt: SectionConfigOverrides = {
|
||||
base: {
|
||||
sectionDocs: "/integrations/mqtt",
|
||||
restartRequired: [],
|
||||
fieldOrder: [
|
||||
"enabled",
|
||||
"timestamp",
|
||||
|
||||
@ -2,6 +2,8 @@ import type { SectionConfigOverrides } from "./types";
|
||||
|
||||
const networking: SectionConfigOverrides = {
|
||||
base: {
|
||||
sectionDocs: "/configuration/reference",
|
||||
restartRequired: [],
|
||||
fieldOrder: [],
|
||||
advancedFields: [],
|
||||
},
|
||||
|
||||
@ -2,6 +2,8 @@ import type { SectionConfigOverrides } from "./types";
|
||||
|
||||
const notifications: SectionConfigOverrides = {
|
||||
base: {
|
||||
sectionDocs: "/configuration/notifications",
|
||||
restartRequired: [],
|
||||
fieldOrder: ["enabled", "email"],
|
||||
fieldGroups: {},
|
||||
hiddenFields: ["enabled_in_config"],
|
||||
|
||||
@ -2,6 +2,8 @@ import type { SectionConfigOverrides } from "./types";
|
||||
|
||||
const objects: SectionConfigOverrides = {
|
||||
base: {
|
||||
sectionDocs: "/configuration/object_filters",
|
||||
restartRequired: [],
|
||||
fieldOrder: ["track", "alert", "detect", "filters"],
|
||||
fieldGroups: {
|
||||
tracking: ["track", "alert", "detect"],
|
||||
@ -14,6 +16,8 @@ const objects: SectionConfigOverrides = {
|
||||
"genai.enabled_in_config",
|
||||
"filters.*.mask",
|
||||
"filters.*.raw_mask",
|
||||
"filters.mask",
|
||||
"filters.raw_mask",
|
||||
],
|
||||
advancedFields: ["filters"],
|
||||
uiSchema: {
|
||||
@ -22,6 +26,11 @@ const objects: SectionConfigOverrides = {
|
||||
suppressMultiSchema: true,
|
||||
},
|
||||
},
|
||||
"filters.*": {
|
||||
"ui:options": {
|
||||
additionalPropertyKeyReadonly: true,
|
||||
},
|
||||
},
|
||||
"filters.*.max_area": {
|
||||
"ui:options": {
|
||||
suppressMultiSchema: true,
|
||||
@ -56,7 +65,17 @@ const objects: SectionConfigOverrides = {
|
||||
},
|
||||
},
|
||||
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",
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@ -2,6 +2,8 @@ import type { SectionConfigOverrides } from "./types";
|
||||
|
||||
const onvif: SectionConfigOverrides = {
|
||||
base: {
|
||||
sectionDocs: "/configuration/cameras#setting-up-camera-ptz-controls",
|
||||
restartRequired: [],
|
||||
fieldOrder: [
|
||||
"host",
|
||||
"port",
|
||||
|
||||
@ -2,6 +2,8 @@ import type { SectionConfigOverrides } from "./types";
|
||||
|
||||
const proxy: SectionConfigOverrides = {
|
||||
base: {
|
||||
sectionDocs: "/configuration/authentication#proxy",
|
||||
restartRequired: [],
|
||||
fieldOrder: [
|
||||
"header_map",
|
||||
"logout_url",
|
||||
|
||||
@ -2,6 +2,8 @@ import type { SectionConfigOverrides } from "./types";
|
||||
|
||||
const record: SectionConfigOverrides = {
|
||||
base: {
|
||||
sectionDocs: "/configuration/record",
|
||||
restartRequired: [],
|
||||
fieldOrder: [
|
||||
"enabled",
|
||||
"expire_interval",
|
||||
|
||||
@ -2,6 +2,8 @@ import type { SectionConfigOverrides } from "./types";
|
||||
|
||||
const review: SectionConfigOverrides = {
|
||||
base: {
|
||||
sectionDocs: "/configuration/review",
|
||||
restartRequired: [],
|
||||
fieldOrder: ["alerts", "detections", "genai"],
|
||||
fieldGroups: {},
|
||||
hiddenFields: [
|
||||
|
||||
@ -2,6 +2,8 @@ import type { SectionConfigOverrides } from "./types";
|
||||
|
||||
const semanticSearch: SectionConfigOverrides = {
|
||||
base: {
|
||||
sectionDocs: "/configuration/semantic_search",
|
||||
restartRequired: [],
|
||||
fieldOrder: ["triggers"],
|
||||
hiddenFields: [],
|
||||
advancedFields: [],
|
||||
|
||||
@ -2,6 +2,8 @@ import type { SectionConfigOverrides } from "./types";
|
||||
|
||||
const snapshots: SectionConfigOverrides = {
|
||||
base: {
|
||||
sectionDocs: "/configuration/snapshots",
|
||||
restartRequired: [],
|
||||
fieldOrder: [
|
||||
"enabled",
|
||||
"bounding_box",
|
||||
|
||||
@ -2,6 +2,8 @@ import type { SectionConfigOverrides } from "./types";
|
||||
|
||||
const telemetry: SectionConfigOverrides = {
|
||||
base: {
|
||||
sectionDocs: "/configuration/reference",
|
||||
restartRequired: [],
|
||||
fieldOrder: ["network_interfaces", "stats", "version_check"],
|
||||
advancedFields: [],
|
||||
},
|
||||
|
||||
@ -2,6 +2,8 @@ import type { SectionConfigOverrides } from "./types";
|
||||
|
||||
const timestampStyle: SectionConfigOverrides = {
|
||||
base: {
|
||||
sectionDocs: "/configuration/reference",
|
||||
restartRequired: [],
|
||||
fieldOrder: ["position", "format", "color", "thickness"],
|
||||
hiddenFields: ["effect", "enabled_in_config"],
|
||||
advancedFields: [],
|
||||
|
||||
@ -2,6 +2,8 @@ import type { SectionConfigOverrides } from "./types";
|
||||
|
||||
const tls: SectionConfigOverrides = {
|
||||
base: {
|
||||
sectionDocs: "/configuration/tls",
|
||||
restartRequired: [],
|
||||
fieldOrder: ["enabled", "cert", "key"],
|
||||
advancedFields: [],
|
||||
},
|
||||
|
||||
@ -2,6 +2,8 @@ import type { SectionConfigOverrides } from "./types";
|
||||
|
||||
const ui: SectionConfigOverrides = {
|
||||
base: {
|
||||
sectionDocs: "/configuration/reference",
|
||||
restartRequired: [],
|
||||
fieldOrder: ["dashboard", "order"],
|
||||
hiddenFields: [],
|
||||
advancedFields: [],
|
||||
|
||||
@ -61,6 +61,12 @@ export interface SectionConfig {
|
||||
advancedFields?: string[];
|
||||
/** Fields to compare for override detection */
|
||||
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 */
|
||||
liveValidate?: boolean;
|
||||
/** 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(() => {
|
||||
isResettingRef.current = true;
|
||||
setPendingData(null);
|
||||
@ -426,8 +453,10 @@ export function ConfigSection({
|
||||
return;
|
||||
}
|
||||
|
||||
const needsRestart = requiresRestartForOverrides(overrides);
|
||||
|
||||
await axios.put("config/sett", {
|
||||
requires_restart: requiresRestart ? 0 : 1,
|
||||
requires_restart: needsRestart ? 1 : 0,
|
||||
update_topic: updateTopic,
|
||||
config_data: {
|
||||
[basePath]: overrides,
|
||||
@ -438,13 +467,15 @@ export function ConfigSection({
|
||||
console.log("Saved config data:", {
|
||||
[basePath]: overrides,
|
||||
update_topic: updateTopic,
|
||||
requires_restart: requiresRestart ? 0 : 1,
|
||||
requires_restart: needsRestart ? 1 : 0,
|
||||
});
|
||||
|
||||
toast.success(
|
||||
t("toast.success", {
|
||||
t(needsRestart ? "toast.successRestartRequired" : "toast.success", {
|
||||
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,
|
||||
level,
|
||||
cameraName,
|
||||
requiresRestart,
|
||||
t,
|
||||
refreshConfig,
|
||||
onSave,
|
||||
@ -504,6 +534,7 @@ export function ConfigSection({
|
||||
schemaDefaults,
|
||||
updateTopic,
|
||||
setPendingData,
|
||||
requiresRestartForOverrides,
|
||||
]);
|
||||
|
||||
// Handle reset to global/defaults - removes camera-level override or resets global to defaults
|
||||
@ -662,6 +693,8 @@ export function ConfigSection({
|
||||
t,
|
||||
renderers:
|
||||
sectionConfig?.renderers ?? sectionRenderers?.[sectionPath],
|
||||
sectionDocs: sectionConfig.sectionDocs,
|
||||
fieldDocs: sectionConfig.fieldDocs,
|
||||
}}
|
||||
/>
|
||||
|
||||
|
||||
@ -13,6 +13,9 @@ import { useTranslation } from "react-i18next";
|
||||
import { isNullableUnionSchema } from "../fields/nullableUtils";
|
||||
import { getTranslatedLabel } from "@/utils/i18n";
|
||||
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
|
||||
@ -114,6 +117,7 @@ export function FieldTemplate(props: FieldTemplateProps) {
|
||||
i18nNamespace || "common",
|
||||
"views/settings",
|
||||
]);
|
||||
const { getLocaleDocUrl } = useDocDomain();
|
||||
|
||||
if (hidden) {
|
||||
return <div className="hidden">{children}</div>;
|
||||
@ -148,6 +152,13 @@ export function FieldTemplate(props: FieldTemplateProps) {
|
||||
const translatedFilterObjectLabel = filterObjectLabel
|
||||
? getTranslatedLabel(filterObjectLabel, "object")
|
||||
: 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)
|
||||
const schemaTitle = schema.title;
|
||||
@ -394,6 +405,19 @@ export function FieldTemplate(props: FieldTemplateProps) {
|
||||
{finalDescription}
|
||||
</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 className="flex items-center gap-2">{children}</div>
|
||||
</div>
|
||||
@ -406,6 +430,19 @@ export function FieldTemplate(props: FieldTemplateProps) {
|
||||
{finalDescription}
|
||||
</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>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import {
|
||||
ADDITIONAL_PROPERTY_FLAG,
|
||||
FormContextType,
|
||||
getUiOptions,
|
||||
RJSFSchema,
|
||||
StrictRJSFSchema,
|
||||
WrapIfAdditionalTemplateProps,
|
||||
@ -30,6 +31,7 @@ export function WrapIfAdditionalTemplate<
|
||||
readonly,
|
||||
required,
|
||||
schema,
|
||||
uiSchema,
|
||||
} = props;
|
||||
|
||||
const { t } = useTranslation(["views/settings"]);
|
||||
@ -57,41 +59,63 @@ export function WrapIfAdditionalTemplate<
|
||||
const removeLabel = t("configForm.additionalProperties.remove", {
|
||||
ns: "views/settings",
|
||||
});
|
||||
const uiOptions = getUiOptions(uiSchema);
|
||||
const keyIsReadonly = uiOptions.additionalPropertyKeyReadonly === true;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn("grid grid-cols-12 items-start gap-2", classNames)}
|
||||
style={style}
|
||||
>
|
||||
<div className="col-span-12 space-y-2 md:col-span-1">
|
||||
{displayLabel && <Label htmlFor={keyId}>{keyLabel}</Label>}
|
||||
<Input
|
||||
id={keyId}
|
||||
name={keyId}
|
||||
required={required}
|
||||
defaultValue={label}
|
||||
placeholder={keyPlaceholder}
|
||||
disabled={disabled || readonly}
|
||||
onBlur={!readonly ? onKeyRenameBlur : undefined}
|
||||
/>
|
||||
</div>
|
||||
<div className="col-span-12 space-y-2 md:col-span-10">
|
||||
{displayLabel && <Label htmlFor={id}>{valueLabel}</Label>}
|
||||
{!keyIsReadonly && (
|
||||
<div className="col-span-12 space-y-2 md:col-span-1">
|
||||
{displayLabel && <Label htmlFor={keyId}>{keyLabel}</Label>}
|
||||
{keyIsReadonly ? (
|
||||
<div
|
||||
id={keyId}
|
||||
className="flex items-center text-sm text-muted-foreground"
|
||||
>
|
||||
{label}
|
||||
</div>
|
||||
) : (
|
||||
<Input
|
||||
id={keyId}
|
||||
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>
|
||||
<div className="col-span-12 flex items-center md:col-span-1 md:justify-center">
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={onRemoveProperty}
|
||||
disabled={disabled || readonly}
|
||||
aria-label={removeLabel}
|
||||
title={removeLabel}
|
||||
>
|
||||
<LuTrash2 className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
{!keyIsReadonly && (
|
||||
<div className="col-span-12 flex items-center md:col-span-1 md:justify-center">
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={onRemoveProperty}
|
||||
disabled={disabled || readonly}
|
||||
aria-label={removeLabel}
|
||||
title={removeLabel}
|
||||
>
|
||||
<LuTrash2 className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@ -22,6 +22,8 @@ export type ConfigFormContext = {
|
||||
fullConfig?: FrigateConfig;
|
||||
i18nNamespace?: string;
|
||||
sectionI18nPrefix?: string;
|
||||
sectionDocs?: string;
|
||||
fieldDocs?: Record<string, string>;
|
||||
t?: (key: string, options?: Record<string, unknown>) => string;
|
||||
renderers?: Record<string, RendererComponent>;
|
||||
};
|
||||
|
||||
@ -1,10 +1,14 @@
|
||||
import { useCallback, useState } from "react";
|
||||
import { useCallback, useMemo, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import type { SectionConfig } from "@/components/config-form/sections";
|
||||
import { ConfigSectionTemplate } from "@/components/config-form/sections";
|
||||
import type { PolygonType } from "@/types/canvas";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
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 = {
|
||||
selectedCamera?: string;
|
||||
@ -58,10 +62,18 @@ export function SingleSectionPage({
|
||||
"views/settings",
|
||||
"common",
|
||||
]);
|
||||
const { getLocaleDocUrl } = useDocDomain();
|
||||
const [sectionStatus, setSectionStatus] = useState<SectionStatus>({
|
||||
hasChanges: false,
|
||||
isOverridden: false,
|
||||
});
|
||||
const resolvedSectionConfig = useMemo(
|
||||
() => sectionConfig ?? getSectionConfig(sectionKey, level),
|
||||
[level, sectionConfig, sectionKey],
|
||||
);
|
||||
const sectionDocsUrl = resolvedSectionConfig.sectionDocs
|
||||
? getLocaleDocUrl(resolvedSectionConfig.sectionDocs)
|
||||
: undefined;
|
||||
|
||||
const handleSectionStatusChange = useCallback(
|
||||
(status: SectionStatus) => {
|
||||
@ -93,6 +105,19 @@ export function SingleSectionPage({
|
||||
{t(`${sectionKey}.description`, { ns: sectionNamespace })}
|
||||
</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 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">
|
||||
@ -127,7 +152,7 @@ export function SingleSectionPage({
|
||||
showOverrideIndicator={showOverrideIndicator}
|
||||
onSave={() => setUnsavedChanges?.(false)}
|
||||
showTitle={false}
|
||||
sectionConfig={sectionConfig}
|
||||
sectionConfig={resolvedSectionConfig}
|
||||
pendingDataBySection={pendingDataBySection}
|
||||
onPendingDataChange={onPendingDataChange}
|
||||
requiresRestart={requiresRestart}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user