mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-03-23 16:48:23 +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": {
|
"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",
|
||||||
|
|||||||
@ -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",
|
||||||
|
|||||||
@ -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"],
|
||||||
|
|||||||
@ -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",
|
||||||
|
|||||||
@ -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: [],
|
||||||
|
|||||||
@ -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: [],
|
||||||
},
|
},
|
||||||
|
|||||||
@ -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: [],
|
||||||
},
|
},
|
||||||
|
|||||||
@ -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",
|
||||||
|
|||||||
@ -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: [
|
||||||
|
|||||||
@ -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: [],
|
||||||
},
|
},
|
||||||
|
|||||||
@ -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"],
|
||||||
|
|||||||
@ -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",
|
||||||
|
|||||||
@ -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",
|
||||||
|
|||||||
@ -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"],
|
||||||
|
|||||||
@ -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"],
|
||||||
},
|
},
|
||||||
|
|||||||
@ -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"],
|
||||||
|
|||||||
@ -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",
|
||||||
|
|||||||
@ -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",
|
||||||
|
|||||||
@ -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",
|
||||||
|
|||||||
@ -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: [],
|
||||||
},
|
},
|
||||||
|
|||||||
@ -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"],
|
||||||
|
|||||||
@ -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",
|
||||||
|
],
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -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",
|
||||||
|
|||||||
@ -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",
|
||||||
|
|||||||
@ -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",
|
||||||
|
|||||||
@ -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: [
|
||||||
|
|||||||
@ -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: [],
|
||||||
|
|||||||
@ -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",
|
||||||
|
|||||||
@ -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: [],
|
||||||
},
|
},
|
||||||
|
|||||||
@ -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: [],
|
||||||
|
|||||||
@ -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: [],
|
||||||
},
|
},
|
||||||
|
|||||||
@ -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: [],
|
||||||
|
|||||||
@ -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,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|||||||
@ -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>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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>;
|
||||||
};
|
};
|
||||||
|
|||||||
@ -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}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user