Add UI config messages framework (#22692)

* add config messages to sections and fields

* add alert variants

* add messages to types

* add detect fps, review, and audio messages

* add a basic set of messages

* remove emptySelectionHintKey from switches widget

use the new messages framework and revert the changes made in #22664
This commit is contained in:
Josh Hawkins
2026-03-29 15:25:40 -06:00
committed by GitHub
parent 257dae11c1
commit 953d244c52
18 changed files with 475 additions and 18 deletions
@@ -3,6 +3,21 @@ import type { SectionConfigOverrides } from "./types";
const audio: SectionConfigOverrides = {
base: {
sectionDocs: "/configuration/audio_detectors",
messages: [
{
key: "no-audio-role",
messageKey: "configMessages.audio.noAudioRole",
severity: "warning",
condition: (ctx) => {
if (ctx.level === "camera" && ctx.fullCameraConfig) {
return !ctx.fullCameraConfig.ffmpeg?.inputs?.some((input) =>
input.roles?.includes("audio"),
);
}
return false;
},
},
],
restartRequired: [],
fieldOrder: [
"enabled",
@@ -3,6 +3,19 @@ import type { SectionConfigOverrides } from "./types";
const audioTranscription: SectionConfigOverrides = {
base: {
sectionDocs: "/configuration/audio_detectors#audio-transcription",
messages: [
{
key: "audio-detection-disabled",
messageKey: "configMessages.audioTranscription.audioDetectionDisabled",
severity: "warning",
condition: (ctx) => {
if (ctx.level === "camera" && ctx.fullCameraConfig) {
return ctx.fullCameraConfig.audio.enabled === false;
}
return false;
},
},
],
restartRequired: [],
fieldOrder: ["enabled", "language", "device", "model_size"],
hiddenFields: ["enabled_in_config", "live_enabled"],
@@ -3,6 +3,20 @@ import type { SectionConfigOverrides } from "./types";
const birdseye: SectionConfigOverrides = {
base: {
sectionDocs: "/configuration/birdseye",
messages: [
{
key: "objects-mode-detect-disabled",
messageKey: "configMessages.birdseye.objectsModeDetectDisabled",
severity: "info",
condition: (ctx) => {
if (ctx.level !== "camera" || !ctx.fullCameraConfig) return false;
return (
ctx.formData?.mode === "objects" &&
ctx.fullCameraConfig.detect?.enabled === false
);
},
},
],
restartRequired: [],
fieldOrder: ["enabled", "mode", "order"],
hiddenFields: [],
@@ -3,6 +3,21 @@ import type { SectionConfigOverrides } from "./types";
const detect: SectionConfigOverrides = {
base: {
sectionDocs: "/configuration/camera_specific",
fieldMessages: [
{
key: "fps-greater-than-five",
field: "fps",
messageKey: "configMessages.detect.fpsGreaterThanFive",
severity: "info",
position: "after",
condition: (ctx) => {
if (ctx.level !== "camera" || !ctx.fullCameraConfig) return false;
const detectFps = ctx.formData?.fps as number | undefined;
const streamFps = ctx.fullCameraConfig.detect?.fps;
return detectFps != null && streamFps != null && detectFps > 5;
},
},
],
fieldOrder: [
"enabled",
"width",
@@ -3,6 +3,26 @@ import type { SectionConfigOverrides } from "./types";
const faceRecognition: SectionConfigOverrides = {
base: {
sectionDocs: "/configuration/face_recognition",
messages: [
{
key: "global-disabled",
messageKey: "configMessages.faceRecognition.globalDisabled",
severity: "warning",
condition: (ctx) => {
if (ctx.level !== "camera") return false;
return ctx.fullConfig.face_recognition?.enabled === false;
},
},
{
key: "person-not-tracked",
messageKey: "configMessages.faceRecognition.personNotTracked",
severity: "info",
condition: (ctx) => {
if (ctx.level !== "camera" || !ctx.fullCameraConfig) return false;
return !ctx.fullCameraConfig.objects?.track?.includes("person");
},
},
],
restartRequired: [],
fieldOrder: ["enabled", "min_area"],
hiddenFields: [],
@@ -3,6 +3,28 @@ import type { SectionConfigOverrides } from "./types";
const lpr: SectionConfigOverrides = {
base: {
sectionDocs: "/configuration/license_plate_recognition",
messages: [
{
key: "global-disabled",
messageKey: "configMessages.lpr.globalDisabled",
severity: "warning",
condition: (ctx) => {
if (ctx.level !== "camera") return false;
return ctx.fullConfig.lpr?.enabled === false;
},
},
{
key: "vehicle-not-tracked",
messageKey: "configMessages.lpr.vehicleNotTracked",
severity: "info",
condition: (ctx) => {
if (ctx.level !== "camera" || !ctx.fullCameraConfig) return false;
if (ctx.fullCameraConfig.type === "lpr") return false;
const tracked = ctx.fullCameraConfig.objects?.track ?? [];
return !tracked.some((o) => ["car", "motorcycle"].includes(o));
},
},
],
fieldDocs: {
enhancement: "/configuration/license_plate_recognition#enhancement",
},
@@ -3,6 +3,19 @@ import type { SectionConfigOverrides } from "./types";
const record: SectionConfigOverrides = {
base: {
sectionDocs: "/configuration/record",
messages: [
{
key: "no-record-role",
messageKey: "configMessages.record.noRecordRole",
severity: "warning",
condition: (ctx) => {
if (ctx.level !== "camera" || !ctx.fullCameraConfig) return false;
return !ctx.fullCameraConfig.ffmpeg?.inputs?.some((i) =>
i.roles?.includes("record"),
);
},
},
],
restartRequired: [],
fieldOrder: [
"enabled",
@@ -3,6 +3,45 @@ import type { SectionConfigOverrides } from "./types";
const review: SectionConfigOverrides = {
base: {
sectionDocs: "/configuration/review",
messages: [
{
key: "record-disabled",
messageKey: "configMessages.review.recordDisabled",
severity: "warning",
condition: (ctx) => {
if (ctx.level === "camera" && ctx.fullCameraConfig) {
return ctx.fullCameraConfig.record.enabled === false;
}
return ctx.fullConfig.record?.enabled === false;
},
},
{
key: "detect-disabled",
messageKey: "configMessages.review.detectDisabled",
severity: "info",
condition: (ctx) => {
if (ctx.level === "camera" && ctx.fullCameraConfig) {
return ctx.fullCameraConfig.detect?.enabled === false;
}
return false;
},
},
],
fieldMessages: [
{
key: "detections-all-non-alert",
field: "detections.labels",
messageKey: "configMessages.review.allNonAlertDetections",
severity: "info",
position: "after",
condition: (ctx) => {
const labels = (
ctx.formData?.detections as Record<string, unknown> | undefined
)?.labels;
return !Array.isArray(labels) || labels.length === 0;
},
},
],
fieldDocs: {
"alerts.labels": "/configuration/review/#alerts-and-detections",
"detections.labels": "/configuration/review/#alerts-and-detections",
@@ -35,8 +74,6 @@ const review: SectionConfigOverrides = {
"ui:widget": "reviewLabels",
"ui:options": {
suppressMultiSchema: true,
emptySelectionHintKey:
"configForm.reviewLabels.allNonAlertDetections",
},
},
required_zones: {
@@ -3,6 +3,17 @@ import type { SectionConfigOverrides } from "./types";
const snapshots: SectionConfigOverrides = {
base: {
sectionDocs: "/configuration/snapshots",
messages: [
{
key: "detect-disabled",
messageKey: "configMessages.snapshots.detectDisabled",
severity: "info",
condition: (ctx) => {
if (ctx.level !== "camera" || !ctx.fullCameraConfig) return false;
return ctx.fullCameraConfig.detect?.enabled === false;
},
},
],
restartRequired: [],
fieldOrder: [
"enabled",
@@ -1,5 +1,39 @@
import type { FrigateConfig, CameraConfig } from "@/types/frigateConfig";
import type { ConfigSectionData } from "@/types/configForm";
import type { SectionConfig } from "../sections/BaseSection";
/** Context provided to message condition functions */
export type MessageConditionContext = {
fullConfig: FrigateConfig;
fullCameraConfig?: CameraConfig;
level: "global" | "camera";
cameraName?: string;
formData: ConfigSectionData;
};
/** Severity levels for conditional messages */
export type MessageSeverity = "info" | "warning" | "error";
/** A conditional message definition */
export type ConditionalMessage = {
/** Unique key for React list rendering and deduplication */
key: string;
/** Translation key resolved via t() in the views/settings namespace */
messageKey: string;
/** Severity level controlling visual styling */
severity: MessageSeverity;
/** Function returning true when the message should be shown */
condition: (ctx: MessageConditionContext) => boolean;
};
/** Field-level conditional message, adds field targeting */
export type FieldConditionalMessage = ConditionalMessage & {
/** Dot-separated field path (e.g., "enabled", "alerts.labels") */
field: string;
/** Whether to render before or after the field (default: "before") */
position?: "before" | "after";
};
export type SectionConfigOverrides = {
base?: SectionConfig;
global?: Partial<SectionConfig>;