add profiles enable toggle and improve empty state

This commit is contained in:
Josh Hawkins 2026-03-11 11:05:18 -05:00
parent 096a13bce9
commit a0849b104c
4 changed files with 96 additions and 53 deletions

View File

@ -1461,7 +1461,10 @@
"deleteProfileConfirm": "Delete profile \"{{profile}}\" from all cameras? This cannot be undone.", "deleteProfileConfirm": "Delete profile \"{{profile}}\" from all cameras? This cannot be undone.",
"deleteSuccess": "Profile '{{profile}}' deleted", "deleteSuccess": "Profile '{{profile}}' deleted",
"deleteSection": "Delete Section Overrides", "deleteSection": "Delete Section Overrides",
"deleteSectionConfirm": "Remove {{profile}}'s overrides for {{section}} on {{camera}}?" "deleteSectionConfirm": "Remove {{profile}}'s overrides for {{section}} on {{camera}}?",
"enableSwitch": "Enable Profiles",
"enabledDescription": "Profiles are enabled. Navigate to a camera config section, create a new profile from the dropdown in the header, and save for changes to take effect.",
"disabledDescription": "Profiles allow you to define named sets of camera config overrides (e.g., armed, away, night) that can be activated on demand. Enable profiles to get started."
}, },
"unsavedChanges": "You have unsaved changes", "unsavedChanges": "You have unsaved changes",
"confirmReset": "Confirm Reset", "confirmReset": "Confirm Reset",

View File

@ -659,6 +659,7 @@ export default function Settings() {
Record<string, string | null> Record<string, string | null>
>({}); >({});
const [newProfiles, setNewProfiles] = useState<string[]>([]); const [newProfiles, setNewProfiles] = useState<string[]>([]);
const [profilesUIEnabled, setProfilesUIEnabled] = useState(false);
const allProfileNames = useMemo(() => { const allProfileNames = useMemo(() => {
if (!config) return []; if (!config) return [];
@ -1127,7 +1128,7 @@ export default function Settings() {
const showProfileDropdown = const showProfileDropdown =
PROFILE_DROPDOWN_PAGES.has(pageToggle) && PROFILE_DROPDOWN_PAGES.has(pageToggle) &&
!!selectedCamera && !!selectedCamera &&
allProfileNames.length > 0; (allProfileNames.length > 0 || profilesUIEnabled);
const headerHasProfileData = useCallback( const headerHasProfileData = useCallback(
(profileName: string): boolean => { (profileName: string): boolean => {
@ -1527,6 +1528,8 @@ export default function Settings() {
pendingDataBySection={pendingDataBySection} pendingDataBySection={pendingDataBySection}
onPendingDataChange={handlePendingDataChange} onPendingDataChange={handlePendingDataChange}
profileState={profileState} profileState={profileState}
profilesUIEnabled={profilesUIEnabled}
setProfilesUIEnabled={setProfilesUIEnabled}
/> />
); );
})()} })()}

View File

@ -29,6 +29,8 @@ import {
AlertDialogHeader, AlertDialogHeader,
AlertDialogTitle, AlertDialogTitle,
} from "@/components/ui/alert-dialog"; } from "@/components/ui/alert-dialog";
import { Switch } from "@/components/ui/switch";
import { Label } from "@/components/ui/label";
type ProfilesApiResponse = { type ProfilesApiResponse = {
profiles: string[]; profiles: string[];
@ -38,9 +40,15 @@ type ProfilesApiResponse = {
type ProfilesViewProps = { type ProfilesViewProps = {
setUnsavedChanges?: React.Dispatch<React.SetStateAction<boolean>>; setUnsavedChanges?: React.Dispatch<React.SetStateAction<boolean>>;
profileState?: ProfileState; profileState?: ProfileState;
profilesUIEnabled?: boolean;
setProfilesUIEnabled?: React.Dispatch<React.SetStateAction<boolean>>;
}; };
export default function ProfilesView({ profileState }: ProfilesViewProps) { export default function ProfilesView({
profileState,
profilesUIEnabled,
setProfilesUIEnabled,
}: ProfilesViewProps) {
const { t } = useTranslation(["views/settings", "common"]); const { t } = useTranslation(["views/settings", "common"]);
const { data: config, mutate: updateConfig } = const { data: config, mutate: updateConfig } =
useSWR<FrigateConfig>("config"); useSWR<FrigateConfig>("config");
@ -182,68 +190,95 @@ export default function ProfilesView({ profileState }: ProfilesViewProps) {
return null; return null;
} }
const hasProfiles = allProfileNames.length > 0;
return ( return (
<div className="flex size-full flex-col lg:pr-2"> <div className="flex size-full flex-col lg:pr-2">
<Heading as="h4" className="mb-5"> <Heading as="h4" className="mb-5">
{t("profiles.title", { ns: "views/settings" })} {t("profiles.title", { ns: "views/settings" })}
</Heading> </Heading>
{/* Active Profile Section */} {/* Enable Profiles Toggle — shown only when no profiles exist */}
<div className="mb-6 rounded-lg border border-border/70 bg-card/30 p-4"> {!hasProfiles && setProfilesUIEnabled && (
<div className="mb-3 text-sm font-semibold text-primary-variant"> <div className="mb-6 rounded-lg border border-border/70 bg-card/30 p-4">
{t("profiles.activeProfile", { ns: "views/settings" })} <div className="flex items-center justify-between">
<Label htmlFor="profiles-toggle" className="cursor-pointer">
{t("profiles.enableSwitch", { ns: "views/settings" })}
</Label>
<Switch
id="profiles-toggle"
checked={profilesUIEnabled ?? false}
onCheckedChange={setProfilesUIEnabled}
/>
</div>
<p className="mt-3 text-sm text-muted-foreground">
{profilesUIEnabled
? t("profiles.enabledDescription", { ns: "views/settings" })
: t("profiles.disabledDescription", { ns: "views/settings" })}
</p>
</div> </div>
<div className="flex items-center gap-3"> )}
<Select
value={activeProfile ?? "__none__"} {/* Active Profile Section — only when profiles exist */}
onValueChange={(v) => {hasProfiles && (
handleActivateProfile(v === "__none__" ? null : v) <div className="mb-6 rounded-lg border border-border/70 bg-card/30 p-4">
} <div className="mb-3 text-sm font-semibold text-primary-variant">
disabled={activating} {t("profiles.activeProfile", { ns: "views/settings" })}
> </div>
<SelectTrigger className="w-[200px]"> <div className="flex items-center gap-3">
<SelectValue /> <Select
</SelectTrigger> value={activeProfile ?? "__none__"}
<SelectContent> onValueChange={(v) =>
<SelectItem value="__none__"> handleActivateProfile(v === "__none__" ? null : v)
{t("profiles.noActiveProfile", { ns: "views/settings" })} }
</SelectItem> disabled={activating}
{allProfileNames.map((profile) => {
const color = getProfileColor(profile, allProfileNames);
return (
<SelectItem key={profile} value={profile}>
<div className="flex items-center gap-2">
<span
className={cn(
"h-2 w-2 shrink-0 rounded-full",
color.dot,
)}
/>
{profile}
</div>
</SelectItem>
);
})}
</SelectContent>
</Select>
{activeProfile && (
<Badge
className={cn(
"cursor-default",
getProfileColor(activeProfile, allProfileNames).bg,
"text-white",
)}
> >
{t("profiles.active", { ns: "views/settings" })} <SelectTrigger className="w-[200px]">
</Badge> <SelectValue />
)} </SelectTrigger>
<SelectContent>
<SelectItem value="__none__">
{t("profiles.noActiveProfile", { ns: "views/settings" })}
</SelectItem>
{allProfileNames.map((profile) => {
const color = getProfileColor(profile, allProfileNames);
return (
<SelectItem key={profile} value={profile}>
<div className="flex items-center gap-2">
<span
className={cn(
"h-2 w-2 shrink-0 rounded-full",
color.dot,
)}
/>
{profile}
</div>
</SelectItem>
);
})}
</SelectContent>
</Select>
{activeProfile && (
<Badge
className={cn(
"cursor-default",
getProfileColor(activeProfile, allProfileNames).bg,
"text-white",
)}
>
{t("profiles.active", { ns: "views/settings" })}
</Badge>
)}
</div>
</div> </div>
</div> )}
{/* Profile Cards */} {/* Profile Cards */}
{allProfileNames.length === 0 ? ( {!hasProfiles ? (
<div className="flex flex-col items-center justify-center py-12 text-muted-foreground"> <div className="flex flex-col items-center justify-center py-12 text-muted-foreground">
<p>{t("profiles.noProfiles", { ns: "views/settings" })}</p> {!profilesUIEnabled && (
<p>{t("profiles.noProfiles", { ns: "views/settings" })}</p>
)}
</div> </div>
) : ( ) : (
<div className="flex flex-col gap-3"> <div className="flex flex-col gap-3">

View File

@ -28,6 +28,8 @@ export type SettingsPageProps = {
data: ConfigSectionData | null, data: ConfigSectionData | null,
) => void; ) => void;
profileState?: ProfileState; profileState?: ProfileState;
profilesUIEnabled?: boolean;
setProfilesUIEnabled?: React.Dispatch<React.SetStateAction<boolean>>;
}; };
export type SectionStatus = { export type SectionStatus = {