UI tweaks (#23679)
Some checks are pending
CI / AMD64 Build (push) Waiting to run
CI / ARM Build (push) Waiting to run
CI / Jetson Jetpack 6 (push) Waiting to run
CI / AMD64 Extra Build (push) Blocked by required conditions
CI / ARM Extra Build (push) Blocked by required conditions
CI / Synaptics Build (push) Blocked by required conditions
CI / Assemble and push default build (push) Blocked by required conditions

* lock saved GenAI provider keys and add labels/validation to config map-key fields

* fix docs
This commit is contained in:
Josh Hawkins 2026-07-11 17:30:40 -05:00 committed by GitHub
parent f3c09ae169
commit da4037eb52
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 119 additions and 27 deletions

View File

@ -72,10 +72,10 @@ Variables prefixed with `FRIGATE_` can be referenced in config fields that suppo
Navigate to <NavPath path="Settings > System > Environment variables" /> 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.

View File

@ -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",

View File

@ -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",
},
},
},
},

View File

@ -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",

View File

@ -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",
},
},
},
},

View File

@ -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<string>(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 (
<div className={classNames} style={style}>
@ -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<HTMLInputElement>) => {
if (readonly) return;
if (keyError) return;
onKeyRenameBlur?.(e);
};
return (
<div
@ -70,23 +128,30 @@ export function WrapIfAdditionalTemplate<
{!keyIsReadonly && (
<div className="col-span-12 space-y-2 md:col-span-2">
{displayLabel && <Label htmlFor={keyId}>{keyLabel}</Label>}
{keyIsReadonly ? (
{keyLocked ? (
<div
id={keyId}
className="flex items-center text-sm text-muted-foreground"
className="flex items-center break-all text-sm text-primary-variant"
>
{label}
</div>
) : (
<Input
id={keyId}
name={keyId}
required={required}
defaultValue={label}
placeholder={keyPlaceholder}
disabled={disabled || readonly}
onBlur={!readonly ? onKeyRenameBlur : undefined}
/>
<>
<Input
id={keyId}
name={keyId}
required={required}
value={keyValue}
placeholder={keyPlaceholder}
disabled={disabled || readonly}
onChange={(e) => setKeyValue(e.target.value)}
onBlur={!readonly ? commitKeyRename : undefined}
aria-invalid={keyError ? true : undefined}
/>
{keyError && (
<p className="text-xs text-destructive">{keyError}</p>
)}
</>
)}
</div>
)}