remove profile badge in settings and add profiles to main menu

This commit is contained in:
Josh Hawkins 2026-03-12 08:32:23 -05:00
parent 39500b20a0
commit 5a1ec5d729
3 changed files with 154 additions and 31 deletions

View File

@ -168,6 +168,7 @@
"systemMetrics": "System metrics",
"configuration": "Configuration",
"systemLogs": "System logs",
"profiles": "Profiles",
"settings": "Settings",
"configurationEditor": "Configuration Editor",
"languages": "Languages",

View File

@ -2,6 +2,7 @@ import {
LuActivity,
LuGithub,
LuLanguages,
LuLayers,
LuLifeBuoy,
LuList,
LuLogOut,
@ -69,6 +70,9 @@ import SetPasswordDialog from "../overlay/SetPasswordDialog";
import { toast } from "sonner";
import axios from "axios";
import { FrigateConfig } from "@/types/frigateConfig";
import type { ProfilesApiResponse } from "@/types/profile";
import { getProfileColor } from "@/utils/profileColors";
import { Badge } from "@/components/ui/badge";
import { useTranslation } from "react-i18next";
import { supportedLanguageKeys } from "@/lib/const";
@ -84,6 +88,8 @@ export default function GeneralSettings({ className }: GeneralSettingsProps) {
const { getLocaleDocUrl } = useDocDomain();
const { data: profile } = useSWR("profile");
const { data: config } = useSWR<FrigateConfig>("config");
const { data: profilesData, mutate: updateProfiles } =
useSWR<ProfilesApiResponse>("profiles");
const logoutUrl = config?.proxy?.logout_url || "/api/logout";
// languages
@ -105,6 +111,41 @@ export default function GeneralSettings({ className }: GeneralSettingsProps) {
});
}, [t]);
// profiles
const allProfileNames = useMemo(
() => profilesData?.profiles?.map((p) => p.name) ?? [],
[profilesData],
);
const profileFriendlyNames = useMemo(() => {
const map = new Map<string, string>();
profilesData?.profiles?.forEach((p) => map.set(p.name, p.friendly_name));
return map;
}, [profilesData]);
const hasProfiles = allProfileNames.length > 0;
const handleActivateProfile = async (profileName: string | null) => {
try {
await axios.put("profile/set", { profile: profileName || null });
await updateProfiles();
toast.success(
profileName
? t("profiles.activated", {
ns: "views/settings",
profile: profileFriendlyNames.get(profileName) ?? profileName,
})
: t("profiles.deactivated", { ns: "views/settings" }),
{ position: "top-center" },
);
} catch {
toast.error(t("profiles.activateFailed", { ns: "views/settings" }), {
position: "top-center",
});
}
};
// settings
const { language, setLanguage } = useLanguage();
@ -285,6 +326,118 @@ export default function GeneralSettings({ className }: GeneralSettingsProps) {
<span>{t("menu.systemLogs")}</span>
</MenuItem>
</Link>
{hasProfiles && (
<SubItem>
<SubItemTrigger
className={
isDesktop
? "cursor-pointer"
: "flex items-center p-2 text-sm"
}
>
<LuLayers className="mr-2 size-4" />
<span>{t("menu.profiles")}</span>
</SubItemTrigger>
<Portal>
<SubItemContent
className={
isDesktop ? "" : "w-[92%] rounded-lg md:rounded-2xl"
}
>
{!isDesktop && (
<>
<DialogTitle className="sr-only">
{t("menu.profiles")}
</DialogTitle>
<DialogDescription className="sr-only">
{t("menu.profiles")}
</DialogDescription>
</>
)}
<span tabIndex={0} className="sr-only" />
<MenuItem
className={
isDesktop
? "cursor-pointer"
: "flex items-center p-2 text-sm"
}
aria-label={t("profiles.baseConfig", {
ns: "views/settings",
})}
onClick={() => handleActivateProfile(null)}
>
<div className="flex w-full items-center justify-between gap-2">
<span className="ml-6 mr-2">
{t("profiles.baseConfig", {
ns: "views/settings",
})}
</span>
{!profilesData?.active_profile && (
<Badge
variant="secondary"
className="text-xs text-primary-variant"
>
{t("profiles.active", {
ns: "views/settings",
})}
</Badge>
)}
</div>
</MenuItem>
{allProfileNames.map((profileName) => {
const color = getProfileColor(
profileName,
allProfileNames,
);
const isActive =
profilesData?.active_profile === profileName;
return (
<MenuItem
key={profileName}
className={
isDesktop
? "cursor-pointer"
: "flex items-center p-2 text-sm"
}
aria-label={
profileFriendlyNames.get(profileName) ??
profileName
}
onClick={() =>
handleActivateProfile(profileName)
}
>
<div className="flex w-full items-center justify-between gap-2">
<div className="flex items-center gap-2">
<span
className={cn(
"ml-2 size-2 shrink-0 rounded-full",
color.dot,
)}
/>
<span>
{profileFriendlyNames.get(profileName) ??
profileName}
</span>
</div>
{isActive && (
<Badge
variant="secondary"
className="text-xs text-primary-variant"
>
{t("profiles.active", {
ns: "views/settings",
})}
</Badge>
)}
</div>
</MenuItem>
);
})}
</SubItemContent>
</Portal>
</SubItem>
)}
</DropdownMenuGroup>
</>
)}

View File

@ -99,7 +99,6 @@ import {
import type { ProfileState, ProfilesApiResponse } from "@/types/profile";
import { getProfileColor } from "@/utils/profileColors";
import { ProfileSectionDropdown } from "@/components/settings/ProfileSectionDropdown";
import { Badge } from "@/components/ui/badge";
import ActivityIndicator from "@/components/indicators/activity-indicator";
import RestartDialog from "@/components/overlay/dialog/RestartDialog";
import SaveAllPreviewPopover, {
@ -1524,24 +1523,6 @@ export default function Settings() {
<h2 className="ml-2 text-lg">
{t("menu.settings", { ns: "common" })}
</h2>
{profilesData?.active_profile && (
<Badge
className={cn(
"ml-2 cursor-pointer text-white",
getProfileColor(
profilesData.active_profile,
allProfileNames,
).bg,
)}
onClick={() => {
setPage("profiles");
setContentMobileOpen(true);
}}
>
{profileFriendlyNames.get(profilesData.active_profile) ??
profilesData.active_profile}
</Badge>
)}
</div>
</div>
@ -1750,18 +1731,6 @@ export default function Settings() {
<Heading as="h3" className="mb-0">
{t("menu.settings", { ns: "common" })}
</Heading>
{profilesData?.active_profile && (
<Badge
className={cn(
"cursor-pointer text-white",
getProfileColor(profilesData.active_profile, allProfileNames)
.bg,
)}
onClick={() => setPage("profiles")}
>
{profilesData.active_profile}
</Badge>
)}
</div>
<div className="flex items-center gap-2">
{hasPendingChanges && (