mirror of
https://github.com/umami-software/umami.git
synced 2026-02-18 03:25:40 +01:00
Add UserButton to SideNav, refactor NavButton into TeamsButton
- Create UserButton component at bottom of SideNav with Settings, Language, Theme, Admin, and Logout menu items - Move Settings/Logout/Admin/Docs/Support out of NavButton into UserButton - Remove LanguageButton and ThemeButton from SideNav bottom - Refactor NavButton into TeamsButton with simplified team switching - Simplify WebsiteNav and move TeamsButton to App top bar Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
f1b314e90b
commit
04c7216928
9 changed files with 382 additions and 186 deletions
|
|
@ -5,6 +5,7 @@ import { useEffect } from 'react';
|
||||||
import { MobileNav } from '@/app/(main)/MobileNav';
|
import { MobileNav } from '@/app/(main)/MobileNav';
|
||||||
import { SideNav } from '@/app/(main)/SideNav';
|
import { SideNav } from '@/app/(main)/SideNav';
|
||||||
import { useConfig, useLoginQuery, useNavigation } from '@/components/hooks';
|
import { useConfig, useLoginQuery, useNavigation } from '@/components/hooks';
|
||||||
|
import { TeamsButton } from '@/components/input/TeamsButton';
|
||||||
import { LAST_TEAM_CONFIG } from '@/lib/constants';
|
import { LAST_TEAM_CONFIG } from '@/lib/constants';
|
||||||
import { removeItem, setItem } from '@/lib/storage';
|
import { removeItem, setItem } from '@/lib/storage';
|
||||||
import { UpdateNotice } from './UpdateNotice';
|
import { UpdateNotice } from './UpdateNotice';
|
||||||
|
|
@ -50,6 +51,18 @@ export function App({ children }) {
|
||||||
<SideNav />
|
<SideNav />
|
||||||
</Column>
|
</Column>
|
||||||
<Column overflowX="hidden" position="relative">
|
<Column overflowX="hidden" position="relative">
|
||||||
|
<Row
|
||||||
|
position="sticky"
|
||||||
|
top="0"
|
||||||
|
alignItems="center"
|
||||||
|
justifyContent="space-between"
|
||||||
|
paddingY="2"
|
||||||
|
zIndex={100}
|
||||||
|
>
|
||||||
|
<Row backgroundColor="surface-raised">
|
||||||
|
<TeamsButton />
|
||||||
|
</Row>
|
||||||
|
</Row>
|
||||||
<Column alignItems="center">{children}</Column>
|
<Column alignItems="center">{children}</Column>
|
||||||
</Column>
|
</Column>
|
||||||
<UpdateNotice user={user} config={config} />
|
<UpdateNotice user={user} config={config} />
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ import { IconLabel } from '@/components/common/IconLabel';
|
||||||
import { useMessages, useNavigation } from '@/components/hooks';
|
import { useMessages, useNavigation } from '@/components/hooks';
|
||||||
import { Globe, Grid2x2, LinkIcon } from '@/components/icons';
|
import { Globe, Grid2x2, LinkIcon } from '@/components/icons';
|
||||||
import { MobileMenuButton } from '@/components/input/MobileMenuButton';
|
import { MobileMenuButton } from '@/components/input/MobileMenuButton';
|
||||||
import { NavButton } from '@/components/input/NavButton';
|
import { TeamsButton } from '@/components/input/TeamsButton';
|
||||||
import { Logo } from '@/components/svg';
|
import { Logo } from '@/components/svg';
|
||||||
import { AdminNav } from './admin/AdminNav';
|
import { AdminNav } from './admin/AdminNav';
|
||||||
import { SettingsNav } from './settings/SettingsNav';
|
import { SettingsNav } from './settings/SettingsNav';
|
||||||
|
|
@ -44,7 +44,7 @@ export function MobileNav() {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Row padding="3" onClick={close} border="bottom">
|
<Row padding="3" onClick={close} border="bottom">
|
||||||
<NavButton />
|
<TeamsButton />
|
||||||
{links.map(link => {
|
{links.map(link => {
|
||||||
return (
|
return (
|
||||||
<Link key={link.id} href={renderUrl(link.path)}>
|
<Link key={link.id} href={renderUrl(link.path)}>
|
||||||
|
|
|
||||||
|
|
@ -6,26 +6,21 @@ import {
|
||||||
Icon,
|
Icon,
|
||||||
Row,
|
Row,
|
||||||
Text,
|
Text,
|
||||||
ThemeButton,
|
|
||||||
Tooltip,
|
Tooltip,
|
||||||
TooltipTrigger,
|
TooltipTrigger,
|
||||||
} from '@umami/react-zen';
|
} from '@umami/react-zen';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import type { Key } from 'react';
|
|
||||||
import { WebsiteNav } from '@/app/(main)/websites/[websiteId]/WebsiteNav';
|
import { WebsiteNav } from '@/app/(main)/websites/[websiteId]/WebsiteNav';
|
||||||
import { IconLabel } from '@/components/common/IconLabel';
|
import { IconLabel } from '@/components/common/IconLabel';
|
||||||
import { useGlobalState, useMessages, useNavigation } from '@/components/hooks';
|
import { useGlobalState, useMessages, useNavigation } from '@/components/hooks';
|
||||||
import { Globe, Grid2x2, LayoutDashboard, LinkIcon, PanelLeft } from '@/components/icons';
|
import { Globe, Grid2x2, LayoutDashboard, LinkIcon, PanelLeft } from '@/components/icons';
|
||||||
import { LanguageButton } from '@/components/input/LanguageButton';
|
import { UserButton } from '@/components/input/UserButton';
|
||||||
import { NavButton } from '@/components/input/NavButton';
|
|
||||||
import { Logo } from '@/components/svg';
|
import { Logo } from '@/components/svg';
|
||||||
|
|
||||||
export function SideNav(props: any) {
|
export function SideNav(props: any) {
|
||||||
const { t, labels } = useMessages();
|
const { t, labels } = useMessages();
|
||||||
const { pathname, renderUrl, websiteId, router } = useNavigation();
|
const { pathname, renderUrl, websiteId } = useNavigation();
|
||||||
const [isCollapsed, setIsCollapsed] = useGlobalState('sidenav-collapsed', false);
|
const [isCollapsed] = useGlobalState('sidenav-collapsed', false);
|
||||||
|
|
||||||
const hasNav = !!(websiteId || pathname.startsWith('/admin') || pathname.includes('/settings'));
|
|
||||||
|
|
||||||
const links = [
|
const links = [
|
||||||
{
|
{
|
||||||
|
|
@ -54,10 +49,6 @@ export function SideNav(props: any) {
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const handleSelect = (id: Key) => {
|
|
||||||
router.push(id === 'user' ? '/websites' : `/teams/${id}/websites`);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Column
|
<Column
|
||||||
{...props}
|
{...props}
|
||||||
|
|
@ -90,9 +81,6 @@ export function SideNav(props: any) {
|
||||||
<PanelButton />
|
<PanelButton />
|
||||||
</Row>
|
</Row>
|
||||||
</Row>
|
</Row>
|
||||||
<Row marginBottom="4" style={{ flexShrink: 0 }}>
|
|
||||||
<NavButton showText={!isCollapsed} onAction={handleSelect} />
|
|
||||||
</Row>
|
|
||||||
{websiteId ? (
|
{websiteId ? (
|
||||||
<WebsiteNav websiteId={websiteId} isCollapsed={isCollapsed} />
|
<WebsiteNav websiteId={websiteId} isCollapsed={isCollapsed} />
|
||||||
) : (
|
) : (
|
||||||
|
|
@ -126,9 +114,8 @@ export function SideNav(props: any) {
|
||||||
</Column>
|
</Column>
|
||||||
)}
|
)}
|
||||||
</Column>
|
</Column>
|
||||||
<Row alignItems="center" justifyContent="center" wrap="wrap" marginBottom="4" gap>
|
<Row marginBottom="4">
|
||||||
<LanguageButton />
|
<UserButton showText={!isCollapsed} />
|
||||||
<ThemeButton />
|
|
||||||
</Row>
|
</Row>
|
||||||
</Column>
|
</Column>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -32,97 +32,66 @@ export function WebsiteNav({
|
||||||
router.push(renderUrl(`/websites/${value}`));
|
router.push(renderUrl(`/websites/${value}`));
|
||||||
};
|
};
|
||||||
|
|
||||||
const renderValue = (value: any) => {
|
|
||||||
return (
|
|
||||||
<Text truncate style={{ maxWidth: 160, lineHeight: 1 }}>
|
|
||||||
{value?.selectedItem?.name}
|
|
||||||
</Text>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
if (isCollapsed !== undefined) {
|
|
||||||
return (
|
|
||||||
<Column gap="2">
|
|
||||||
<Link href={renderUrl('/websites', false)} role="button">
|
|
||||||
<TooltipTrigger isDisabled={!isCollapsed} delay={0}>
|
|
||||||
<Focusable>
|
|
||||||
<Row
|
|
||||||
alignItems="center"
|
|
||||||
hover={{ backgroundColor: 'surface-sunken' }}
|
|
||||||
borderRadius
|
|
||||||
minHeight="40px"
|
|
||||||
>
|
|
||||||
<IconLabel icon={<ArrowLeft />} label={isCollapsed ? '' : t(labels.back)} padding />
|
|
||||||
</Row>
|
|
||||||
</Focusable>
|
|
||||||
<Tooltip placement="right">{t(labels.back)}</Tooltip>
|
|
||||||
</TooltipTrigger>
|
|
||||||
</Link>
|
|
||||||
{!isCollapsed && (
|
|
||||||
<Box marginBottom="2">
|
|
||||||
<WebsiteSelect
|
|
||||||
websiteId={websiteId}
|
|
||||||
teamId={teamId}
|
|
||||||
onChange={handleChange}
|
|
||||||
renderValue={renderValue}
|
|
||||||
buttonProps={{ style: { outline: 'none' } }}
|
|
||||||
/>
|
|
||||||
</Box>
|
|
||||||
)}
|
|
||||||
{items.map(({ label: sectionLabel, items: sectionItems }, index) => (
|
|
||||||
<Column key={`${sectionLabel}${index}`} gap="1" marginBottom="1">
|
|
||||||
{!isCollapsed && (
|
|
||||||
<Row padding>
|
|
||||||
<Text weight="bold">{sectionLabel}</Text>
|
|
||||||
</Row>
|
|
||||||
)}
|
|
||||||
{sectionItems.map(({ id, path, label, icon }) => {
|
|
||||||
const isSelected = selectedKey === id;
|
|
||||||
return (
|
|
||||||
<Link key={id} href={path} role="button">
|
|
||||||
<TooltipTrigger isDisabled={!isCollapsed} delay={0}>
|
|
||||||
<Focusable>
|
|
||||||
<Row
|
|
||||||
alignItems="center"
|
|
||||||
hover={{ backgroundColor: 'surface-sunken' }}
|
|
||||||
backgroundColor={isSelected ? 'surface-sunken' : undefined}
|
|
||||||
borderRadius
|
|
||||||
minHeight="40px"
|
|
||||||
>
|
|
||||||
<IconLabel
|
|
||||||
icon={icon}
|
|
||||||
label={isCollapsed ? '' : label}
|
|
||||||
weight={isSelected ? 'bold' : undefined}
|
|
||||||
padding
|
|
||||||
/>
|
|
||||||
</Row>
|
|
||||||
</Focusable>
|
|
||||||
<Tooltip placement="right">{label}</Tooltip>
|
|
||||||
</TooltipTrigger>
|
|
||||||
</Link>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</Column>
|
|
||||||
))}
|
|
||||||
</Column>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Column padding="2" position="sticky" top="0" gap>
|
<Column gap="2">
|
||||||
<WebsiteSelect
|
<Link href={renderUrl('/websites', false)} role="button">
|
||||||
websiteId={websiteId}
|
<TooltipTrigger isDisabled={!isCollapsed} delay={0}>
|
||||||
teamId={teamId}
|
<Focusable>
|
||||||
onChange={handleChange}
|
<Row
|
||||||
renderValue={renderValue}
|
alignItems="center"
|
||||||
buttonProps={{ style: { outline: 'none' } }}
|
hover={{ backgroundColor: 'surface-sunken' }}
|
||||||
/>
|
borderRadius
|
||||||
<NavMenu
|
minHeight="40px"
|
||||||
items={items}
|
>
|
||||||
selectedKey={selectedKey}
|
<IconLabel icon={<ArrowLeft />} label={isCollapsed ? '' : t(labels.back)} padding />
|
||||||
allowMinimize={false}
|
</Row>
|
||||||
onItemClick={onItemClick}
|
</Focusable>
|
||||||
/>
|
<Tooltip placement="right">{t(labels.back)}</Tooltip>
|
||||||
|
</TooltipTrigger>
|
||||||
|
</Link>
|
||||||
|
<Box marginBottom="2">
|
||||||
|
<WebsiteSelect
|
||||||
|
websiteId={websiteId}
|
||||||
|
teamId={teamId}
|
||||||
|
onChange={handleChange}
|
||||||
|
buttonProps={{ style: { outline: 'none' } }}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
{items.map(({ label: sectionLabel, items: sectionItems }, index) => (
|
||||||
|
<Column key={`${sectionLabel}${index}`} gap="1" marginBottom="1">
|
||||||
|
{!isCollapsed && (
|
||||||
|
<Row padding>
|
||||||
|
<Text weight="bold">{sectionLabel}</Text>
|
||||||
|
</Row>
|
||||||
|
)}
|
||||||
|
{sectionItems.map(({ id, path, label, icon }) => {
|
||||||
|
const isSelected = selectedKey === id;
|
||||||
|
return (
|
||||||
|
<Link key={id} href={path} role="button">
|
||||||
|
<TooltipTrigger isDisabled={!isCollapsed} delay={0}>
|
||||||
|
<Focusable>
|
||||||
|
<Row
|
||||||
|
alignItems="center"
|
||||||
|
hover={{ backgroundColor: 'surface-sunken' }}
|
||||||
|
backgroundColor={isSelected ? 'surface-sunken' : undefined}
|
||||||
|
borderRadius
|
||||||
|
minHeight="40px"
|
||||||
|
>
|
||||||
|
<IconLabel
|
||||||
|
icon={icon}
|
||||||
|
label={isCollapsed ? '' : label}
|
||||||
|
weight={isSelected ? 'bold' : undefined}
|
||||||
|
padding
|
||||||
|
/>
|
||||||
|
</Row>
|
||||||
|
</Focusable>
|
||||||
|
<Tooltip placement="right">{label}</Tooltip>
|
||||||
|
</TooltipTrigger>
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</Column>
|
||||||
|
))}
|
||||||
</Column>
|
</Column>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -15,26 +15,10 @@ import {
|
||||||
import { ArrowRight } from 'lucide-react';
|
import { ArrowRight } from 'lucide-react';
|
||||||
import type { Key } from 'react';
|
import type { Key } from 'react';
|
||||||
import { IconLabel } from '@/components/common/IconLabel';
|
import { IconLabel } from '@/components/common/IconLabel';
|
||||||
import {
|
import { useLoginQuery, useMessages, useMobile, useNavigation } from '@/components/hooks';
|
||||||
useConfig,
|
import { ChevronRight, User, Users } from '@/components/icons';
|
||||||
useLoginQuery,
|
|
||||||
useMessages,
|
|
||||||
useMobile,
|
|
||||||
useNavigation,
|
|
||||||
} from '@/components/hooks';
|
|
||||||
import {
|
|
||||||
BookText,
|
|
||||||
ChevronRight,
|
|
||||||
ExternalLink,
|
|
||||||
LifeBuoy,
|
|
||||||
LockKeyhole,
|
|
||||||
LogOut,
|
|
||||||
Settings,
|
|
||||||
User,
|
|
||||||
Users,
|
|
||||||
} from '@/components/icons';
|
|
||||||
import { Switch } from '@/components/svg';
|
import { Switch } from '@/components/svg';
|
||||||
import { DOCS_URL, LAST_TEAM_CONFIG } from '@/lib/constants';
|
import { LAST_TEAM_CONFIG } from '@/lib/constants';
|
||||||
import { removeItem } from '@/lib/storage';
|
import { removeItem } from '@/lib/storage';
|
||||||
|
|
||||||
export interface TeamsButtonProps {
|
export interface TeamsButtonProps {
|
||||||
|
|
@ -44,13 +28,13 @@ export interface TeamsButtonProps {
|
||||||
|
|
||||||
export function NavButton({ showText = true }: TeamsButtonProps) {
|
export function NavButton({ showText = true }: TeamsButtonProps) {
|
||||||
const { user } = useLoginQuery();
|
const { user } = useLoginQuery();
|
||||||
const { cloudMode } = useConfig();
|
|
||||||
const { t, labels } = useMessages();
|
const { t, labels } = useMessages();
|
||||||
const { teamId, router } = useNavigation();
|
const { teamId, router } = useNavigation();
|
||||||
const { isMobile } = useMobile();
|
const { isMobile } = useMobile();
|
||||||
const team = user?.teams?.find(({ id }) => id === teamId);
|
const team = user?.teams?.find(({ id }) => id === teamId);
|
||||||
const selectedKeys = new Set([teamId || 'user']);
|
const selectedKeys = new Set([teamId || 'user']);
|
||||||
const label = teamId ? team?.name : user.username;
|
const label = teamId ? team?.name : user.username;
|
||||||
|
const cloudMode = !!process.env.cloudMode;
|
||||||
|
|
||||||
const getUrl = (url: string) => {
|
const getUrl = (url: string) => {
|
||||||
return cloudMode ? `${process.env.cloudUrl}${url}` : url;
|
return cloudMode ? `${process.env.cloudUrl}${url}` : url;
|
||||||
|
|
@ -134,52 +118,6 @@ export function NavButton({ showText = true }: TeamsButtonProps) {
|
||||||
</Column>
|
</Column>
|
||||||
</Popover>
|
</Popover>
|
||||||
</SubmenuTrigger>
|
</SubmenuTrigger>
|
||||||
<MenuSeparator />
|
|
||||||
<MenuItem
|
|
||||||
id="settings"
|
|
||||||
href={getUrl('/settings')}
|
|
||||||
icon={<Settings />}
|
|
||||||
label={t(labels.settings)}
|
|
||||||
/>
|
|
||||||
{cloudMode && (
|
|
||||||
<>
|
|
||||||
<MenuItem
|
|
||||||
id="docs"
|
|
||||||
href={DOCS_URL}
|
|
||||||
target="_blank"
|
|
||||||
icon={<BookText />}
|
|
||||||
label={t(labels.documentation)}
|
|
||||||
>
|
|
||||||
<Icon color="muted">
|
|
||||||
<ExternalLink />
|
|
||||||
</Icon>
|
|
||||||
</MenuItem>
|
|
||||||
<MenuItem
|
|
||||||
id="support"
|
|
||||||
href={getUrl('/settings/support')}
|
|
||||||
icon={<LifeBuoy />}
|
|
||||||
label={t(labels.support)}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
{!cloudMode && user.isAdmin && (
|
|
||||||
<>
|
|
||||||
<MenuSeparator />
|
|
||||||
<MenuItem
|
|
||||||
id="/admin"
|
|
||||||
href="/admin"
|
|
||||||
icon={<LockKeyhole />}
|
|
||||||
label={t(labels.admin)}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
<MenuSeparator />
|
|
||||||
<MenuItem
|
|
||||||
id="logout"
|
|
||||||
href={getUrl('/logout')}
|
|
||||||
icon={<LogOut />}
|
|
||||||
label={t(labels.logout)}
|
|
||||||
/>
|
|
||||||
</Menu>
|
</Menu>
|
||||||
</Column>
|
</Column>
|
||||||
</Popover>
|
</Popover>
|
||||||
|
|
|
||||||
92
src/components/input/TeamsButton.tsx
Normal file
92
src/components/input/TeamsButton.tsx
Normal file
|
|
@ -0,0 +1,92 @@
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
Column,
|
||||||
|
Icon,
|
||||||
|
Menu,
|
||||||
|
MenuItem,
|
||||||
|
MenuSection,
|
||||||
|
MenuSeparator,
|
||||||
|
MenuTrigger,
|
||||||
|
Popover,
|
||||||
|
Row,
|
||||||
|
Text,
|
||||||
|
} from '@umami/react-zen';
|
||||||
|
import { ArrowRight } from 'lucide-react';
|
||||||
|
import type { Key } from 'react';
|
||||||
|
import { IconLabel } from '@/components/common/IconLabel';
|
||||||
|
import { useLoginQuery, useMessages, useMobile, useNavigation } from '@/components/hooks';
|
||||||
|
import { ChevronRight, User, Users } from '@/components/icons';
|
||||||
|
import { LAST_TEAM_CONFIG } from '@/lib/constants';
|
||||||
|
import { removeItem } from '@/lib/storage';
|
||||||
|
|
||||||
|
export function TeamsButton() {
|
||||||
|
const { user } = useLoginQuery();
|
||||||
|
const { t, labels } = useMessages();
|
||||||
|
const { teamId, router } = useNavigation();
|
||||||
|
const team = user?.teams?.find(({ id }) => id === teamId);
|
||||||
|
const selectedKeys = new Set([teamId || 'user']);
|
||||||
|
const label = teamId ? team?.name : user.username;
|
||||||
|
|
||||||
|
const cloudMode = !!process.env.cloudMode;
|
||||||
|
|
||||||
|
const getUrl = (url: string) => {
|
||||||
|
return cloudMode ? `${process.env.cloudUrl}${url}` : url;
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleAction = async (key: Key) => {
|
||||||
|
if (key === 'user') {
|
||||||
|
removeItem(LAST_TEAM_CONFIG);
|
||||||
|
if (cloudMode) {
|
||||||
|
window.location.href = '/';
|
||||||
|
} else {
|
||||||
|
router.push('/');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<MenuTrigger>
|
||||||
|
<Button variant="quiet">
|
||||||
|
<Row alignItems="center" position="relative" gap maxHeight="40px">
|
||||||
|
<Icon>{teamId ? <Users /> : <User />}</Icon>
|
||||||
|
<Text>{label}</Text>
|
||||||
|
</Row>
|
||||||
|
<Icon rotate={90} size="sm">
|
||||||
|
<ChevronRight />
|
||||||
|
</Icon>
|
||||||
|
</Button>
|
||||||
|
<Popover placement="bottom start">
|
||||||
|
<Column minWidth="300px">
|
||||||
|
<Menu selectionMode="single" selectedKeys={selectedKeys} onAction={handleAction}>
|
||||||
|
<MenuSection title={t(labels.myAccount)}>
|
||||||
|
<MenuItem id="user">
|
||||||
|
<IconLabel icon={<User />} label={user.username} />
|
||||||
|
</MenuItem>
|
||||||
|
</MenuSection>
|
||||||
|
<MenuSeparator />
|
||||||
|
<MenuSection title={t(labels.teams)}>
|
||||||
|
{user?.teams?.map(({ id, name }) => (
|
||||||
|
<MenuItem key={id} id={id} href={getUrl(`/teams/${id}`)}>
|
||||||
|
<IconLabel icon={<Users />}>
|
||||||
|
<Text wrap="nowrap">{name}</Text>
|
||||||
|
</IconLabel>
|
||||||
|
</MenuItem>
|
||||||
|
))}
|
||||||
|
<MenuSeparator />
|
||||||
|
<MenuItem id="manage-teams">
|
||||||
|
<a href="/settings/teams" style={{ width: '100%' }}>
|
||||||
|
<Row alignItems="center" justifyContent="space-between" gap>
|
||||||
|
<Text align="center">Manage teams</Text>
|
||||||
|
<Icon>
|
||||||
|
<ArrowRight />
|
||||||
|
</Icon>
|
||||||
|
</Row>
|
||||||
|
</a>
|
||||||
|
</MenuItem>
|
||||||
|
</MenuSection>
|
||||||
|
</Menu>
|
||||||
|
</Column>
|
||||||
|
</Popover>
|
||||||
|
</MenuTrigger>
|
||||||
|
);
|
||||||
|
}
|
||||||
204
src/components/input/UserButton.tsx
Normal file
204
src/components/input/UserButton.tsx
Normal file
|
|
@ -0,0 +1,204 @@
|
||||||
|
import {
|
||||||
|
Column,
|
||||||
|
Icon,
|
||||||
|
Menu,
|
||||||
|
MenuItem,
|
||||||
|
MenuSeparator,
|
||||||
|
MenuTrigger,
|
||||||
|
Popover,
|
||||||
|
Pressable,
|
||||||
|
Row,
|
||||||
|
SubmenuTrigger,
|
||||||
|
Text,
|
||||||
|
Tooltip,
|
||||||
|
TooltipTrigger,
|
||||||
|
useTheme,
|
||||||
|
} from '@umami/react-zen';
|
||||||
|
import { useConfig, useLocale, useLoginQuery, useMessages, useMobile } from '@/components/hooks';
|
||||||
|
import {
|
||||||
|
BookText,
|
||||||
|
ExternalLink,
|
||||||
|
Globe,
|
||||||
|
LifeBuoy,
|
||||||
|
LockKeyhole,
|
||||||
|
LogOut,
|
||||||
|
Moon,
|
||||||
|
Settings,
|
||||||
|
Sun,
|
||||||
|
SunMoon,
|
||||||
|
UserCircle,
|
||||||
|
} from '@/components/icons';
|
||||||
|
import { DOCS_URL } from '@/lib/constants';
|
||||||
|
import { languages } from '@/lib/lang';
|
||||||
|
|
||||||
|
export interface UserButtonProps {
|
||||||
|
showText?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function UserButton({ showText = true }: UserButtonProps) {
|
||||||
|
const { user } = useLoginQuery();
|
||||||
|
const { cloudMode } = useConfig();
|
||||||
|
const { t, labels } = useMessages();
|
||||||
|
const { locale, saveLocale } = useLocale();
|
||||||
|
const { theme, setTheme } = useTheme();
|
||||||
|
const { isMobile } = useMobile();
|
||||||
|
|
||||||
|
const getUrl = (url: string) => {
|
||||||
|
return cloudMode ? `${process.env.cloudUrl}${url}` : url;
|
||||||
|
};
|
||||||
|
|
||||||
|
const languageItems = Object.keys(languages).map(key => ({
|
||||||
|
value: key,
|
||||||
|
label: languages[key].label,
|
||||||
|
}));
|
||||||
|
|
||||||
|
const items = [
|
||||||
|
cloudMode && {
|
||||||
|
id: 'docs',
|
||||||
|
label: t(labels.documentation),
|
||||||
|
path: DOCS_URL,
|
||||||
|
icon: <BookText />,
|
||||||
|
target: '_blank',
|
||||||
|
external: true,
|
||||||
|
},
|
||||||
|
cloudMode && {
|
||||||
|
id: 'support',
|
||||||
|
label: t(labels.support),
|
||||||
|
path: getUrl('/settings/support'),
|
||||||
|
icon: <LifeBuoy />,
|
||||||
|
},
|
||||||
|
!cloudMode &&
|
||||||
|
user.isAdmin && {
|
||||||
|
id: 'admin',
|
||||||
|
label: t(labels.admin),
|
||||||
|
path: '/admin',
|
||||||
|
icon: <LockKeyhole />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'separator',
|
||||||
|
separator: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'logout',
|
||||||
|
label: t(labels.logout),
|
||||||
|
path: getUrl('/logout'),
|
||||||
|
icon: <LogOut />,
|
||||||
|
},
|
||||||
|
].filter(Boolean);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<MenuTrigger>
|
||||||
|
<TooltipTrigger isDisabled={showText} delay={0}>
|
||||||
|
<Pressable>
|
||||||
|
<Row
|
||||||
|
alignItems="center"
|
||||||
|
flexGrow={1}
|
||||||
|
hover={{ backgroundColor: 'surface-sunken' }}
|
||||||
|
borderRadius
|
||||||
|
minHeight="40px"
|
||||||
|
role="button"
|
||||||
|
style={{ cursor: 'pointer', textWrap: 'nowrap', overflow: 'hidden', outline: 'none' }}
|
||||||
|
>
|
||||||
|
<Row alignItems="center" gap padding>
|
||||||
|
<Icon>
|
||||||
|
<UserCircle />
|
||||||
|
</Icon>
|
||||||
|
{showText && <Text>{user.username}</Text>}
|
||||||
|
</Row>
|
||||||
|
</Row>
|
||||||
|
</Pressable>
|
||||||
|
<Tooltip placement="right">{user.username}</Tooltip>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<Popover placement="top start">
|
||||||
|
<Column minWidth="200px">
|
||||||
|
<Menu autoFocus="last">
|
||||||
|
<MenuItem id="settings" href={getUrl('/settings')}>
|
||||||
|
<Row alignItems="center" gap>
|
||||||
|
<Icon>
|
||||||
|
<Settings />
|
||||||
|
</Icon>
|
||||||
|
<Text>{t(labels.settings)}</Text>
|
||||||
|
</Row>
|
||||||
|
</MenuItem>
|
||||||
|
<SubmenuTrigger>
|
||||||
|
<MenuItem id="language" showSubMenuIcon>
|
||||||
|
<Row alignItems="center" gap>
|
||||||
|
<Icon>
|
||||||
|
<Globe />
|
||||||
|
</Icon>
|
||||||
|
<Text>{t(labels.language)}</Text>
|
||||||
|
</Row>
|
||||||
|
</MenuItem>
|
||||||
|
<Popover placement={isMobile ? 'bottom start' : 'right bottom'} isNonModal>
|
||||||
|
<Menu
|
||||||
|
selectionMode="single"
|
||||||
|
selectedKeys={new Set([locale])}
|
||||||
|
onAction={key => saveLocale(key as string)}
|
||||||
|
style={{ maxHeight: 300, overflow: 'auto' }}
|
||||||
|
>
|
||||||
|
{languageItems.map(({ value, label }) => (
|
||||||
|
<MenuItem key={value} id={value}>
|
||||||
|
<Text weight={value === locale ? 'bold' : undefined}>{label}</Text>
|
||||||
|
</MenuItem>
|
||||||
|
))}
|
||||||
|
</Menu>
|
||||||
|
</Popover>
|
||||||
|
</SubmenuTrigger>
|
||||||
|
<SubmenuTrigger>
|
||||||
|
<MenuItem id="theme" showSubMenuIcon>
|
||||||
|
<Row alignItems="center" gap>
|
||||||
|
<Icon>
|
||||||
|
<SunMoon />
|
||||||
|
</Icon>
|
||||||
|
<Text>{t(labels.theme)}</Text>
|
||||||
|
</Row>
|
||||||
|
</MenuItem>
|
||||||
|
<Popover placement={isMobile ? 'bottom start' : 'right bottom'} isNonModal>
|
||||||
|
<Menu
|
||||||
|
selectionMode="single"
|
||||||
|
selectedKeys={new Set([theme])}
|
||||||
|
onAction={key => setTheme(key as 'light' | 'dark')}
|
||||||
|
>
|
||||||
|
<MenuItem id="light">
|
||||||
|
<Row alignItems="center" gap>
|
||||||
|
<Icon>
|
||||||
|
<Sun />
|
||||||
|
</Icon>
|
||||||
|
<Text>Light</Text>
|
||||||
|
</Row>
|
||||||
|
</MenuItem>
|
||||||
|
<MenuItem id="dark">
|
||||||
|
<Row alignItems="center" gap>
|
||||||
|
<Icon>
|
||||||
|
<Moon />
|
||||||
|
</Icon>
|
||||||
|
<Text>Dark</Text>
|
||||||
|
</Row>
|
||||||
|
</MenuItem>
|
||||||
|
</Menu>
|
||||||
|
</Popover>
|
||||||
|
</SubmenuTrigger>
|
||||||
|
{items.map(({ id, path, label, icon, separator, target, external }: any) => {
|
||||||
|
if (separator) {
|
||||||
|
return <MenuSeparator key={id} />;
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<MenuItem key={id} id={id} href={path} target={target}>
|
||||||
|
<Row alignItems="center" gap>
|
||||||
|
<Icon>{icon}</Icon>
|
||||||
|
<Text>{label}</Text>
|
||||||
|
{external && (
|
||||||
|
<Icon color="muted" size="sm">
|
||||||
|
<ExternalLink />
|
||||||
|
</Icon>
|
||||||
|
)}
|
||||||
|
</Row>
|
||||||
|
</MenuItem>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</Menu>
|
||||||
|
</Column>
|
||||||
|
</Popover>
|
||||||
|
</MenuTrigger>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -13,11 +13,13 @@ export function WebsiteSelect({
|
||||||
teamId,
|
teamId,
|
||||||
onChange,
|
onChange,
|
||||||
includeTeams,
|
includeTeams,
|
||||||
|
isCollapsed,
|
||||||
...props
|
...props
|
||||||
}: {
|
}: {
|
||||||
websiteId?: string;
|
websiteId?: string;
|
||||||
teamId?: string;
|
teamId?: string;
|
||||||
includeTeams?: boolean;
|
includeTeams?: boolean;
|
||||||
|
isCollapsed?: boolean;
|
||||||
} & SelectProps) {
|
} & SelectProps) {
|
||||||
const { t, messages } = useMessages();
|
const { t, messages } = useMessages();
|
||||||
const { data: website } = useWebsiteQuery(websiteId);
|
const { data: website } = useWebsiteQuery(websiteId);
|
||||||
|
|
@ -43,14 +45,6 @@ export function WebsiteSelect({
|
||||||
onChange(id);
|
onChange(id);
|
||||||
};
|
};
|
||||||
|
|
||||||
const renderValue = () => {
|
|
||||||
return (
|
|
||||||
<Row maxWidth="160px">
|
|
||||||
<Text truncate>{name ?? website?.name}</Text>
|
|
||||||
</Row>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Select
|
<Select
|
||||||
{...props}
|
{...props}
|
||||||
|
|
@ -61,7 +55,6 @@ export function WebsiteSelect({
|
||||||
onSearch={handleSearch}
|
onSearch={handleSearch}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
onOpenChange={handleOpenChange}
|
onOpenChange={handleOpenChange}
|
||||||
renderValue={renderValue}
|
|
||||||
listProps={{
|
listProps={{
|
||||||
renderEmptyState: () => <Empty message={t(messages.noResultsFound)} />,
|
renderEmptyState: () => <Empty message={t(messages.noResultsFound)} />,
|
||||||
style: { maxHeight: 'calc(42vh - 65px)', width: 280 },
|
style: { maxHeight: 'calc(42vh - 65px)', width: 280 },
|
||||||
|
|
|
||||||
|
|
@ -74,8 +74,8 @@ export * from '@/components/input/DialogButton';
|
||||||
export * from '@/components/input/DownloadButton';
|
export * from '@/components/input/DownloadButton';
|
||||||
export * from '@/components/input/ExportButton';
|
export * from '@/components/input/ExportButton';
|
||||||
export * from '@/components/input/FilterButtons';
|
export * from '@/components/input/FilterButtons';
|
||||||
export * from '@/components/input/NavButton';
|
|
||||||
export * from '@/components/input/ProfileButton';
|
export * from '@/components/input/ProfileButton';
|
||||||
|
export * from '@/components/input/TeamsButton';
|
||||||
export * from '@/components/input/WebsiteSelect';
|
export * from '@/components/input/WebsiteSelect';
|
||||||
export * from '@/components/metrics/ChangeLabel';
|
export * from '@/components/metrics/ChangeLabel';
|
||||||
export * from '@/components/metrics/ListTable';
|
export * from '@/components/metrics/ListTable';
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue