mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-03-19 06:38:21 +03:00
add profiles enable toggle and improve empty state
This commit is contained in:
parent
096a13bce9
commit
a0849b104c
@ -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",
|
||||||
|
|||||||
@ -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}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
})()}
|
})()}
|
||||||
|
|||||||
@ -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">
|
||||||
|
|||||||
@ -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 = {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user