From da4037eb52ef81676ef8eaa8867480330bbe93bb Mon Sep 17 00:00:00 2001 From: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com> Date: Sat, 11 Jul 2026 17:30:40 -0500 Subject: [PATCH] UI tweaks (#23679) * lock saved GenAI provider keys and add labels/validation to config map-key fields * fix docs --- docs/docs/configuration/advanced/system.md | 8 +- web/public/locales/en/views/settings.json | 9 +- .../section-configs/environment_vars.ts | 8 +- .../config-form/section-configs/genai.ts | 10 +- .../config-form/section-configs/logger.ts | 8 +- .../templates/WrapIfAdditionalTemplate.tsx | 103 ++++++++++++++---- 6 files changed, 119 insertions(+), 27 deletions(-) diff --git a/docs/docs/configuration/advanced/system.md b/docs/docs/configuration/advanced/system.md index 5f73ccd23b..14979698c7 100644 --- a/docs/docs/configuration/advanced/system.md +++ b/docs/docs/configuration/advanced/system.md @@ -72,10 +72,10 @@ Variables prefixed with `FRIGATE_` can be referenced in config fields that suppo Navigate to to add or edit environment variables. -| Field | Description | -| --------- | --------------------------------------------------------- | -| **Key** | The environment variable name (e.g., `FRIGATE_MQTT_USER`) | -| **Value** | The value for the variable | +| Field | Description | +| ----------------- | --------------------------------------------------------- | +| **Variable name** | The environment variable name (e.g., `FRIGATE_MQTT_USER`) | +| **Value** | The value for the variable | Variables defined here can be referenced elsewhere in your configuration using the `{FRIGATE_VARIABLE_NAME}` syntax. diff --git a/web/public/locales/en/views/settings.json b/web/public/locales/en/views/settings.json index 15f73f81f8..68248241ce 100644 --- a/web/public/locales/en/views/settings.json +++ b/web/public/locales/en/views/settings.json @@ -1490,7 +1490,14 @@ "keyLabel": "Key", "valueLabel": "Value", "keyPlaceholder": "New key", - "remove": "Remove" + "remove": "Remove", + "providerNameLabel": "Provider name", + "providerNamePlaceholder": "e.g., openai", + "variableNameLabel": "Variable name", + "variableNamePlaceholder": "e.g., MY_VARIABLE", + "loggerNameLabel": "Logger name", + "loggerNamePlaceholder": "e.g., frigate.record", + "keyPatternError": "Use only letters, numbers, hyphens, and underscores (no spaces)" }, "knownPlates": { "namePlaceholder": "e.g., Wife's Car", diff --git a/web/src/components/config-form/section-configs/environment_vars.ts b/web/src/components/config-form/section-configs/environment_vars.ts index 969095d075..21048ce35d 100644 --- a/web/src/components/config-form/section-configs/environment_vars.ts +++ b/web/src/components/config-form/section-configs/environment_vars.ts @@ -7,7 +7,13 @@ const environmentVars: SectionConfigOverrides = { advancedFields: [], uiSchema: { additionalProperties: { - "ui:options": { size: "lg" }, + "ui:options": { + size: "lg", + additionalPropertyKeyLabel: + "configForm.additionalProperties.variableNameLabel", + additionalPropertyKeyPlaceholder: + "configForm.additionalProperties.variableNamePlaceholder", + }, }, }, }, diff --git a/web/src/components/config-form/section-configs/genai.ts b/web/src/components/config-form/section-configs/genai.ts index c5645cbbad..552e85c316 100644 --- a/web/src/components/config-form/section-configs/genai.ts +++ b/web/src/components/config-form/section-configs/genai.ts @@ -9,7 +9,15 @@ const genai: SectionConfigOverrides = { uiSchema: { "ui:options": { disableNestedCard: true }, "*": { - "ui:options": { disableNestedCard: true }, + "ui:options": { + disableNestedCard: true, + additionalPropertyKeyLabel: + "configForm.additionalProperties.providerNameLabel", + additionalPropertyKeyPlaceholder: + "configForm.additionalProperties.providerNamePlaceholder", + additionalPropertyKeyPattern: "^[a-zA-Z0-9_-]+$", + preventKeyRename: true, + }, "ui:order": [ "provider", "api_key", diff --git a/web/src/components/config-form/section-configs/logger.ts b/web/src/components/config-form/section-configs/logger.ts index 5cd7f6490b..61c365e034 100644 --- a/web/src/components/config-form/section-configs/logger.ts +++ b/web/src/components/config-form/section-configs/logger.ts @@ -12,7 +12,13 @@ const logger: SectionConfigOverrides = { }, logs: { additionalProperties: { - "ui:options": { enumI18nPrefix: "logger.logLevel" }, + "ui:options": { + enumI18nPrefix: "logger.logLevel", + additionalPropertyKeyLabel: + "configForm.additionalProperties.loggerNameLabel", + additionalPropertyKeyPlaceholder: + "configForm.additionalProperties.loggerNamePlaceholder", + }, }, }, }, diff --git a/web/src/components/config-form/theme/templates/WrapIfAdditionalTemplate.tsx b/web/src/components/config-form/theme/templates/WrapIfAdditionalTemplate.tsx index 6e6a19bfdd..9a769369ce 100644 --- a/web/src/components/config-form/theme/templates/WrapIfAdditionalTemplate.tsx +++ b/web/src/components/config-form/theme/templates/WrapIfAdditionalTemplate.tsx @@ -6,12 +6,14 @@ import { StrictRJSFSchema, WrapIfAdditionalTemplateProps, } from "@rjsf/utils"; +import { useEffect, useMemo, useState, type FocusEvent } from "react"; import { Input } from "@/components/ui/input"; import { Button } from "@/components/ui/button"; import { Label } from "@/components/ui/label"; import { cn } from "@/lib/utils"; import { useTranslation } from "react-i18next"; import { LuTrash2 } from "react-icons/lu"; +import type { ConfigFormContext } from "@/types/configForm"; export function WrapIfAdditionalTemplate< T = unknown, @@ -30,6 +32,7 @@ export function WrapIfAdditionalTemplate< onKeyRenameBlur, readonly, required, + registry, schema, uiSchema, } = props; @@ -38,6 +41,55 @@ export function WrapIfAdditionalTemplate< const additional = ADDITIONAL_PROPERTY_FLAG in schema; + const uiOptions = getUiOptions(uiSchema); + const keyIsReadonly = uiOptions.additionalPropertyKeyReadonly === true; + + const keyLabelKey = + typeof uiOptions.additionalPropertyKeyLabel === "string" + ? uiOptions.additionalPropertyKeyLabel + : undefined; + const keyPlaceholderKey = + typeof uiOptions.additionalPropertyKeyPlaceholder === "string" + ? uiOptions.additionalPropertyKeyPlaceholder + : undefined; + const keyPattern = + typeof uiOptions.additionalPropertyKeyPattern === "string" + ? uiOptions.additionalPropertyKeyPattern + : undefined; + const preventKeyRename = uiOptions.preventKeyRename === true; + + const formContext = registry?.formContext as ConfigFormContext | undefined; + + // optionally, lock the key once it's been saved + const baseline = formContext?.baselineFormData; + const keyLocked = + preventKeyRename && + typeof label === "string" && + !!baseline && + Object.prototype.hasOwnProperty.call(baseline, label); + + // controlled key value so we can validate live and block invalid renames. + const [keyValue, setKeyValue] = useState(label ?? ""); + useEffect(() => { + setKeyValue(label ?? ""); + }, [label]); + + const keyRegex = useMemo( + () => (keyPattern ? new RegExp(keyPattern) : undefined), + [keyPattern], + ); + const keyError = useMemo(() => { + if (!keyRegex || keyLocked) return null; + if (!keyRegex.test(keyValue)) { + return t("configForm.additionalProperties.keyPatternError", { + ns: "views/settings", + defaultValue: + "Use only letters, numbers, hyphens, and underscores (no spaces)", + }); + } + return null; + }, [keyRegex, keyLocked, keyValue, t]); + if (!additional) { return (
@@ -47,20 +99,26 @@ export function WrapIfAdditionalTemplate< } const keyId = `${id}-key`; - const keyLabel = t("configForm.additionalProperties.keyLabel", { - ns: "views/settings", - }); + const keyLabel = keyLabelKey + ? t(keyLabelKey, { ns: "views/settings" }) + : t("configForm.additionalProperties.keyLabel", { ns: "views/settings" }); const valueLabel = t("configForm.additionalProperties.valueLabel", { ns: "views/settings", }); - const keyPlaceholder = t("configForm.additionalProperties.keyPlaceholder", { - ns: "views/settings", - }); + const keyPlaceholder = keyPlaceholderKey + ? t(keyPlaceholderKey, { ns: "views/settings" }) + : t("configForm.additionalProperties.keyPlaceholder", { + ns: "views/settings", + }); const removeLabel = t("configForm.additionalProperties.remove", { ns: "views/settings", }); - const uiOptions = getUiOptions(uiSchema); - const keyIsReadonly = uiOptions.additionalPropertyKeyReadonly === true; + + const commitKeyRename = (e: FocusEvent) => { + if (readonly) return; + if (keyError) return; + onKeyRenameBlur?.(e); + }; return (
{displayLabel && } - {keyIsReadonly ? ( + {keyLocked ? (
{label}
) : ( - + <> + setKeyValue(e.target.value)} + onBlur={!readonly ? commitKeyRename : undefined} + aria-invalid={keyError ? true : undefined} + /> + {keyError && ( +

{keyError}

+ )} + )}
)}