mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-07-27 14:19:01 +03:00
Merge detector and model in settings UI (#23216)
* add embedded mode to BaseSection so parents can host the save action * add optional action slot to current Frigate+ model summary * add w-full to action slot flex wrapper for explicit width contract * i18n * merged detectors and model settings view * fix document title * Embed detector form in merged settings view * add detection model card with tabs and custom model embed * add Frigate+ model selector with filter popover to merged page * Add mismatch banner and gate save on detector and model compatibility * Wire atomic save, restart toast, and undo on detectors and model page * Clear child pending data on undo * route merged detectors and model view in settings * trim Frigate+ page to account-only and remove old detection model view * basic e2e * Fix unsaved-changes guard, custom path leak, and post-failure cache resync * Rename to Detectors and model, float Modified badge, use ConfigMessageBanner for mismatch * Hide Plus/Custom tabs when Frigate+ is not enabled * Detect active Plus model via model.plus.id instead of path prefix * Sync state back to snapshot when child form un-modifies and remount on undo * Always require restart on save since model changes also need one * Wrap Frigate+ model selector in SplitCardRow with label and description * rename tab * update docs * sync top-level model with default detector's resolved model when the user doesn't define a top-level `model:` block, `FrigateConfig.model` stayed at pydantic field defaults (320×320, /labelmap.txt) while the per-detector model picked up `DEFAULT_MODEL` for openvino on cpu (300×300, coco_91cl_bkgr.txt introduced in #23127), causing `RemoteObjectDetector` to fail with "buffer is too small for requested array" because the SHM was sized from the per-detector model but mapped using the top-level one. After the detector loop, copy the first detector's resolved model up to `self.model` so both sides agree on dimensions and labelmap * revert to cpu detector by default use openvino cpu for new configs only * add defaults
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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 */
|
||||
|
||||
@@ -175,6 +175,9 @@ export interface BaseSectionProps {
|
||||
isSavingAll?: boolean;
|
||||
/** Callback when this section's saving state changes */
|
||||
onSavingChange?: (isSaving: boolean) => void;
|
||||
/** When true, render the form fields only; suppress the internal save/undo bar.
|
||||
* The parent owns the save action and reads pending data via `onPendingDataChange`. */
|
||||
embedded?: boolean;
|
||||
}
|
||||
|
||||
export interface CreateSectionOptions {
|
||||
@@ -211,6 +214,7 @@ export function ConfigSection({
|
||||
onDeleteProfileSection,
|
||||
isSavingAll = false,
|
||||
onSavingChange,
|
||||
embedded = false,
|
||||
}: ConfigSectionProps) {
|
||||
// For replay level, treat as camera-level config access
|
||||
const effectiveLevel = level === "replay" ? "camera" : level;
|
||||
@@ -1048,121 +1052,123 @@ export function ConfigSection({
|
||||
}}
|
||||
/>
|
||||
|
||||
<div
|
||||
className={cn(
|
||||
"w-full border-t border-secondary bg-background pt-0",
|
||||
!noStickyButtons && "sticky bottom-0 z-50",
|
||||
)}
|
||||
>
|
||||
{!embedded && (
|
||||
<div
|
||||
className={cn(
|
||||
"flex flex-col items-center gap-4 pt-2 md:flex-row",
|
||||
hasChanges ? "justify-between" : "justify-end",
|
||||
"w-full border-t border-secondary bg-background pt-0",
|
||||
!noStickyButtons && "sticky bottom-0 z-50",
|
||||
)}
|
||||
>
|
||||
{hasChanges && (
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-sm text-unsaved">
|
||||
{t("unsavedChanges", {
|
||||
ns: "views/settings",
|
||||
defaultValue: "You have unsaved changes",
|
||||
})}
|
||||
</span>
|
||||
<SaveAllPreviewPopover
|
||||
items={sectionPreviewItems}
|
||||
className="h-7 w-7"
|
||||
align="start"
|
||||
side="top"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex w-full flex-col gap-2 sm:flex-row sm:items-center md:w-auto">
|
||||
{((effectiveLevel === "camera" && isOverridden) ||
|
||||
effectiveLevel === "global") &&
|
||||
!hasChanges &&
|
||||
!skipSave &&
|
||||
!profileName && (
|
||||
<Button
|
||||
onClick={() => setIsResetDialogOpen(true)}
|
||||
variant="outline"
|
||||
disabled={isSaving || isResettingToDefault || disabled}
|
||||
className="flex flex-1 gap-2"
|
||||
>
|
||||
{isResettingToDefault && (
|
||||
<ActivityIndicator className="h-4 w-4" />
|
||||
)}
|
||||
{effectiveLevel === "global"
|
||||
? t("button.resetToDefault", {
|
||||
ns: "common",
|
||||
defaultValue: "Reset to Default",
|
||||
})
|
||||
: t("button.resetToGlobal", {
|
||||
ns: "common",
|
||||
defaultValue: "Reset to Global",
|
||||
})}
|
||||
</Button>
|
||||
)}
|
||||
{profileName &&
|
||||
profileOverridesSection &&
|
||||
!hasChanges &&
|
||||
!skipSave &&
|
||||
onDeleteProfileSection && (
|
||||
<Button
|
||||
onClick={() => setIsDeleteProfileDialogOpen(true)}
|
||||
variant="outline"
|
||||
disabled={isSaving || disabled}
|
||||
className="flex flex-1 gap-2"
|
||||
>
|
||||
{t("profiles.removeOverride", {
|
||||
ns: "views/settings",
|
||||
defaultValue: "Remove Profile Override",
|
||||
})}
|
||||
</Button>
|
||||
)}
|
||||
<div
|
||||
className={cn(
|
||||
"flex flex-col items-center gap-4 pt-2 md:flex-row",
|
||||
hasChanges ? "justify-between" : "justify-end",
|
||||
)}
|
||||
>
|
||||
{hasChanges && (
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-sm text-unsaved">
|
||||
{t("unsavedChanges", {
|
||||
ns: "views/settings",
|
||||
defaultValue: "You have unsaved changes",
|
||||
})}
|
||||
</span>
|
||||
<SaveAllPreviewPopover
|
||||
items={sectionPreviewItems}
|
||||
className="h-7 w-7"
|
||||
align="start"
|
||||
side="top"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex w-full flex-col gap-2 sm:flex-row sm:items-center md:w-auto">
|
||||
{((effectiveLevel === "camera" && isOverridden) ||
|
||||
effectiveLevel === "global") &&
|
||||
!hasChanges &&
|
||||
!skipSave &&
|
||||
!profileName && (
|
||||
<Button
|
||||
onClick={() => setIsResetDialogOpen(true)}
|
||||
variant="outline"
|
||||
disabled={isSaving || isResettingToDefault || disabled}
|
||||
className="flex flex-1 gap-2"
|
||||
>
|
||||
{isResettingToDefault && (
|
||||
<ActivityIndicator className="h-4 w-4" />
|
||||
)}
|
||||
{effectiveLevel === "global"
|
||||
? t("button.resetToDefault", {
|
||||
ns: "common",
|
||||
defaultValue: "Reset to Default",
|
||||
})
|
||||
: t("button.resetToGlobal", {
|
||||
ns: "common",
|
||||
defaultValue: "Reset to Global",
|
||||
})}
|
||||
</Button>
|
||||
)}
|
||||
{profileName &&
|
||||
profileOverridesSection &&
|
||||
!hasChanges &&
|
||||
!skipSave &&
|
||||
onDeleteProfileSection && (
|
||||
<Button
|
||||
onClick={() => setIsDeleteProfileDialogOpen(true)}
|
||||
variant="outline"
|
||||
disabled={isSaving || disabled}
|
||||
className="flex flex-1 gap-2"
|
||||
>
|
||||
{t("profiles.removeOverride", {
|
||||
ns: "views/settings",
|
||||
defaultValue: "Remove Profile Override",
|
||||
})}
|
||||
</Button>
|
||||
)}
|
||||
{hasChanges && (
|
||||
<Button
|
||||
onClick={handleReset}
|
||||
variant="outline"
|
||||
disabled={isSaving || isSavingAll || disabled}
|
||||
className="flex min-w-36 flex-1 gap-2"
|
||||
>
|
||||
{t("button.undo", { ns: "common", defaultValue: "Undo" })}
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
onClick={handleReset}
|
||||
variant="outline"
|
||||
disabled={isSaving || isSavingAll || disabled}
|
||||
onClick={handleSave}
|
||||
variant="select"
|
||||
disabled={
|
||||
!hasChanges ||
|
||||
hasValidationErrors ||
|
||||
isSaving ||
|
||||
isSavingAll ||
|
||||
disabled
|
||||
}
|
||||
className="flex min-w-36 flex-1 gap-2"
|
||||
>
|
||||
{t("button.undo", { ns: "common", defaultValue: "Undo" })}
|
||||
{isSaving ? (
|
||||
<>
|
||||
<ActivityIndicator className="h-4 w-4" />
|
||||
{skipSave
|
||||
? t("button.applying", {
|
||||
ns: "common",
|
||||
defaultValue: "Applying...",
|
||||
})
|
||||
: t("button.saving", {
|
||||
ns: "common",
|
||||
defaultValue: "Saving...",
|
||||
})}
|
||||
</>
|
||||
) : skipSave ? (
|
||||
t("button.apply", { ns: "common", defaultValue: "Apply" })
|
||||
) : (
|
||||
t("button.save", { ns: "common", defaultValue: "Save" })
|
||||
)}
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
onClick={handleSave}
|
||||
variant="select"
|
||||
disabled={
|
||||
!hasChanges ||
|
||||
hasValidationErrors ||
|
||||
isSaving ||
|
||||
isSavingAll ||
|
||||
disabled
|
||||
}
|
||||
className="flex min-w-36 flex-1 gap-2"
|
||||
>
|
||||
{isSaving ? (
|
||||
<>
|
||||
<ActivityIndicator className="h-4 w-4" />
|
||||
{skipSave
|
||||
? t("button.applying", {
|
||||
ns: "common",
|
||||
defaultValue: "Applying...",
|
||||
})
|
||||
: t("button.saving", {
|
||||
ns: "common",
|
||||
defaultValue: "Saving...",
|
||||
})}
|
||||
</>
|
||||
) : skipSave ? (
|
||||
t("button.apply", { ns: "common", defaultValue: "Apply" })
|
||||
) : (
|
||||
t("button.save", { ns: "common", defaultValue: "Save" })
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<AlertDialog open={isResetDialogOpen} onOpenChange={setIsResetDialogOpen}>
|
||||
<AlertDialogContent>
|
||||
|
||||
Reference in New Issue
Block a user