Rename to Detectors and model, float Modified badge, use ConfigMessageBanner for mismatch

This commit is contained in:
Josh Hawkins 2026-05-16 12:17:46 -05:00
parent 7126428307
commit 73e0d1b0eb
5 changed files with 54 additions and 52 deletions

View File

@ -1,19 +1,19 @@
/**
* Detectors & Model settings page tests -- HIGH tier.
* Detectors and model settings page tests -- HIGH tier.
*
* Tests rendering of the merged page and navigation from the Frigate+ page.
*/
import { test, expect } from "../../fixtures/frigate-test";
test.describe("Detectors & Model Settings @high", () => {
test.describe("Detectors and model Settings @high", () => {
test("page renders with detector and model cards", async ({ frigateApp }) => {
await frigateApp.goto("/settings?page=systemDetectorsAndModel");
await frigateApp.page.waitForTimeout(2000);
await expect(frigateApp.page.locator("#pageRoot")).toBeVisible();
const text = await frigateApp.page.textContent("#pageRoot");
expect(text).toContain("Detectors & Model");
expect(text).toContain("Detectors and model");
expect(text?.toLowerCase()).toContain("detector hardware");
expect(text?.toLowerCase()).toContain("detection model");
});
@ -23,7 +23,7 @@ test.describe("Detectors & Model Settings @high", () => {
await frigateApp.page.waitForTimeout(2000);
const button = frigateApp.page.getByRole("button", {
name: /Change in Detectors & Model/,
name: /Change in Detectors and model/,
});
// Button only appears when Frigate+ is enabled in the test config; skip
@ -32,7 +32,7 @@ test.describe("Detectors & Model Settings @high", () => {
await button.first().click();
await frigateApp.page.waitForURL(/page=systemDetectorsAndModel/);
await expect(frigateApp.page.locator("#pageRoot")).toContainText(
"Detectors & Model",
"Detectors and model",
);
} else {
test.skip(

View File

@ -12,7 +12,7 @@
"globalConfig": "Global Configuration - Frigate",
"cameraConfig": "Camera Configuration - Frigate",
"frigatePlus": "Frigate+ Settings - Frigate",
"detectorsAndModel": "Detectors & Model - Frigate",
"detectorsAndModel": "Detectors and model - Frigate",
"notifications": "Notification Settings - Frigate",
"maintenance": "Maintenance - Frigate",
"profiles": "Profiles - Frigate"
@ -70,7 +70,7 @@
"systemTelemetry": "Telemetry",
"systemBirdseye": "Birdseye",
"systemFfmpeg": "FFmpeg",
"systemDetectorsAndModel": "Detectors & Model",
"systemDetectorsAndModel": "Detectors and model",
"systemMqtt": "MQTT",
"systemGo2rtcStreams": "go2rtc streams",
"integrationSemanticSearch": "Semantic search",
@ -1146,7 +1146,7 @@
},
"modelSelect": "Your available models on Frigate+ can be selected here. Note that only models compatible with your current detector configuration can be selected."
},
"changeInDetectorsAndModel": "Change in Detectors & Model →",
"changeInDetectorsAndModel": "Change model",
"unsavedChanges": "Unsaved Frigate+ settings changes",
"restart_required": "Restart required (Frigate+ model changed)",
"toast": {
@ -1155,7 +1155,7 @@
}
},
"detectorsAndModel": {
"title": "Detectors & Model",
"title": "Detectors and model",
"description": "Configure the detector backend that runs object detection and the model it uses. Changes are saved together so the detector and model stay in sync.",
"cardTitles": {
"detector": "Detector Hardware",
@ -1166,7 +1166,7 @@
"custom": "Custom Model"
},
"mismatch": {
"warning": "The current Frigate+ model <0>{{model}}</0> requires the <1>{{required}}</1> detector. Pick a compatible model below or switch to Custom Model before saving."
"warning": "The current Frigate+ model \"{{model}}\" requires the {{required}} detector. Pick a compatible model below or switch to Custom Model before saving."
},
"plusModel": {
"requiresDetector": "Requires: {{detector}}",

View File

@ -44,7 +44,7 @@ export function ConfigMessageBanner({ messages }: ConfigMessageBannerProps) {
className="flex items-center [&>svg+div]:translate-y-0 [&>svg]:static [&>svg~*]:pl-2"
>
<SeverityIcon severity={msg.severity} />
<AlertDescription>{t(msg.messageKey)}</AlertDescription>
<AlertDescription>{t(msg.messageKey, msg.values)}</AlertDescription>
</Alert>
))}
</div>

View File

@ -24,6 +24,8 @@ export type ConditionalMessage = {
severity: MessageSeverity;
/** Function returning true when the message should be shown */
condition: (ctx: MessageConditionContext) => boolean;
/** Optional interpolation values passed to t() for {{var}} substitution */
values?: Record<string, unknown>;
};
/** Field-level conditional message, adds field targeting */

View File

@ -1,5 +1,5 @@
import { useCallback, useContext, useEffect, useMemo, useState } from "react";
import { Trans, useTranslation } from "react-i18next";
import { useTranslation } from "react-i18next";
import { Link } from "react-router-dom";
import { LuExternalLink, LuFilter } from "react-icons/lu";
import { toast } from "sonner";
@ -39,6 +39,7 @@ import type {
import type { ConfigSectionData } from "@/types/configForm";
import { SettingsGroupCard } from "@/components/card/SettingsGroupCard";
import { ConfigSectionTemplate } from "@/components/config-form/sections";
import { ConfigMessageBanner } from "@/components/config-form/ConfigMessageBanner";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
type ModelTab = "plus" | "custom";
@ -358,35 +359,34 @@ export default function DetectorsAndModelSettingsView({
return (
<div className="flex size-full flex-col md:pr-2">
<Toaster position="top-center" closeButton={true} />
<div className="w-full max-w-5xl space-y-6 pt-2">
<div className="mb-1 flex items-center justify-between gap-4">
<div className="flex flex-col">
<Heading as="h4">{t("detectorsAndModel.title")}</Heading>
<div className="my-1 text-sm text-muted-foreground">
{t("detectorsAndModel.description")}
</div>
<div className="flex items-center text-sm text-primary-variant">
<Link
to={getLocaleDocUrl("/configuration/object_detectors")}
target="_blank"
rel="noopener noreferrer"
className="inline"
>
{t("readTheDocumentation", { ns: "common" })}
<LuExternalLink className="ml-2 inline-flex size-3" />
</Link>
</div>
<div className="mb-1 flex items-center justify-between gap-4 pt-2">
<div className="flex max-w-5xl flex-col">
<Heading as="h4">{t("detectorsAndModel.title")}</Heading>
<div className="my-1 text-sm text-muted-foreground">
{t("detectorsAndModel.description")}
</div>
{isDirty && (
<Badge
variant="secondary"
className="cursor-default bg-unsaved text-xs text-black hover:bg-unsaved"
<div className="flex items-center text-sm text-primary-variant">
<Link
to={getLocaleDocUrl("/configuration/object_detectors")}
target="_blank"
rel="noopener noreferrer"
className="inline"
>
{t("button.modified", { ns: "common", defaultValue: "Modified" })}
</Badge>
)}
{t("readTheDocumentation", { ns: "common" })}
<LuExternalLink className="ml-2 inline-flex size-3" />
</Link>
</div>
</div>
{isDirty && (
<Badge
variant="secondary"
className="cursor-default bg-unsaved text-xs text-black hover:bg-unsaved"
>
{t("button.modified", { ns: "common", defaultValue: "Modified" })}
</Badge>
)}
</div>
<div className="w-full max-w-5xl space-y-6 pt-4">
<div className="space-y-6">
<SettingsGroupCard title={t("detectorsAndModel.cardTitles.detector")}>
<ConfigSectionTemplate
@ -401,20 +401,20 @@ export default function DetectorsAndModelSettingsView({
/>
</SettingsGroupCard>
{plusMismatch && selectedPlusModel && (
<div className="rounded-md border border-danger bg-danger/10 px-4 py-3 text-sm text-danger">
<Trans
ns="views/settings"
i18nKey="detectorsAndModel.mismatch.warning"
values={{
model: selectedPlusModel.name,
required: selectedPlusModel.supportedDetectors.join(", "),
}}
components={{
0: <strong />,
1: <strong />,
}}
/>
</div>
<ConfigMessageBanner
messages={[
{
key: "plus-mismatch",
messageKey: "detectorsAndModel.mismatch.warning",
severity: "warning",
condition: () => true,
values: {
model: selectedPlusModel.name,
required: selectedPlusModel.supportedDetectors.join(", "),
},
},
]}
/>
)}
<SettingsGroupCard title={t("detectorsAndModel.cardTitles.model")}>
<Tabs