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. Navigate to <NavPath path="Settings > System > Environment variables" /> to add or edit environment variables.
| Field | Description | | Field | Description |
| --------- | --------------------------------------------------------- | | ----------------- | --------------------------------------------------------- |
| **Key** | The environment variable name (e.g., `FRIGATE_MQTT_USER`) | | **Variable name** | The environment variable name (e.g., `FRIGATE_MQTT_USER`) |
| **Value** | The value for the variable | | **Value** | The value for the variable |
Variables defined here can be referenced elsewhere in your configuration using the `{FRIGATE_VARIABLE_NAME}` syntax. Variables defined here can be referenced elsewhere in your configuration using the `{FRIGATE_VARIABLE_NAME}` syntax.

View File

@ -1490,7 +1490,14 @@
"keyLabel": "Key", "keyLabel": "Key",
"valueLabel": "Value", "valueLabel": "Value",
"keyPlaceholder": "New key", "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": { "knownPlates": {
"namePlaceholder": "e.g., Wife's Car", "namePlaceholder": "e.g., Wife's Car",

View File

@ -7,7 +7,13 @@ const environmentVars: SectionConfigOverrides = {
advancedFields: [], advancedFields: [],
uiSchema: { uiSchema: {
additionalProperties: { 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: { uiSchema: {
"ui:options": { disableNestedCard: true }, "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": [ "ui:order": [
"provider", "provider",
"api_key", "api_key",

View File

@ -12,7 +12,13 @@ const logger: SectionConfigOverrides = {
}, },
logs: { logs: {
additionalProperties: { 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, StrictRJSFSchema,
WrapIfAdditionalTemplateProps, WrapIfAdditionalTemplateProps,
} from "@rjsf/utils"; } from "@rjsf/utils";
import { useEffect, useMemo, useState, type FocusEvent } from "react";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Label } from "@/components/ui/label"; import { Label } from "@/components/ui/label";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { LuTrash2 } from "react-icons/lu"; import { LuTrash2 } from "react-icons/lu";
import type { ConfigFormContext } from "@/types/configForm";
export function WrapIfAdditionalTemplate< export function WrapIfAdditionalTemplate<
T = unknown, T = unknown,
@ -30,6 +32,7 @@ export function WrapIfAdditionalTemplate<
onKeyRenameBlur, onKeyRenameBlur,
readonly, readonly,
required, required,
registry,
schema, schema,
uiSchema, uiSchema,
} = props; } = props;
@ -38,6 +41,55 @@ export function WrapIfAdditionalTemplate<
const additional = ADDITIONAL_PROPERTY_FLAG in schema; 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) { if (!additional) {
return ( return (
<div className={classNames} style={style}> <div className={classNames} style={style}>
@ -47,20 +99,26 @@ export function WrapIfAdditionalTemplate<
} }
const keyId = `${id}-key`; const keyId = `${id}-key`;
const keyLabel = t("configForm.additionalProperties.keyLabel", { const keyLabel = keyLabelKey
ns: "views/settings", ? t(keyLabelKey, { ns: "views/settings" })
}); : t("configForm.additionalProperties.keyLabel", { ns: "views/settings" });
const valueLabel = t("configForm.additionalProperties.valueLabel", { const valueLabel = t("configForm.additionalProperties.valueLabel", {
ns: "views/settings", ns: "views/settings",
}); });
const keyPlaceholder = t("configForm.additionalProperties.keyPlaceholder", { const keyPlaceholder = keyPlaceholderKey
ns: "views/settings", ? t(keyPlaceholderKey, { ns: "views/settings" })
}); : t("configForm.additionalProperties.keyPlaceholder", {
ns: "views/settings",
});
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; const commitKeyRename = (e: FocusEvent<HTMLInputElement>) => {
if (readonly) return;
if (keyError) return;
onKeyRenameBlur?.(e);
};
return ( return (
<div <div
@ -70,23 +128,30 @@ export function WrapIfAdditionalTemplate<
{!keyIsReadonly && ( {!keyIsReadonly && (
<div className="col-span-12 space-y-2 md:col-span-2"> <div className="col-span-12 space-y-2 md:col-span-2">
{displayLabel && <Label htmlFor={keyId}>{keyLabel}</Label>} {displayLabel && <Label htmlFor={keyId}>{keyLabel}</Label>}
{keyIsReadonly ? ( {keyLocked ? (
<div <div
id={keyId} id={keyId}
className="flex items-center text-sm text-muted-foreground" className="flex items-center break-all text-sm text-primary-variant"
> >
{label} {label}
</div> </div>
) : ( ) : (
<Input <>
id={keyId} <Input
name={keyId} id={keyId}
required={required} name={keyId}
defaultValue={label} required={required}
placeholder={keyPlaceholder} value={keyValue}
disabled={disabled || readonly} placeholder={keyPlaceholder}
onBlur={!readonly ? onKeyRenameBlur : undefined} 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> </div>
)} )}