feat(admin): add self-host setup and user management page (#7537)

This commit is contained in:
JimmFly
2024-08-13 14:11:03 +08:00
committed by GitHub
parent dc519348c5
commit ccf225c8f9
47 changed files with 2793 additions and 551 deletions
@@ -0,0 +1,57 @@
import { buttonVariants } from '@affine/admin/components/ui/button';
import { cn } from '@affine/admin/utils';
import type { LucideIcon } from 'lucide-react';
import { Link } from 'react-router-dom';
import { UserDropdown } from './user-dropdown';
export interface NavProp {
title: string;
to: string;
label?: string;
icon: LucideIcon;
}
export function Nav({
links,
activeTab,
}: {
links: NavProp[];
activeTab: string;
}) {
return (
<div className="group flex flex-col gap-4 py-2 justify-between flex-grow">
<nav className="grid gap-1 px-2">
{links.map((link, index) => (
<Link
key={index}
to={link.to}
className={cn(
buttonVariants({
variant: activeTab === link.title ? 'default' : 'ghost',
size: 'sm',
}),
activeTab === link.title &&
'dark:bg-muted dark:text-white dark:hover:bg-muted dark:hover:text-white',
'justify-start'
)}
>
<link.icon className="mr-2 h-4 w-4" />
{link.title}
{link.label && (
<span
className={cn(
'ml-auto',
activeTab === link.title && 'text-background dark:text-white'
)}
>
{link.label}
</span>
)}
</Link>
))}
</nav>
<UserDropdown />
</div>
);
}
@@ -1,3 +1,8 @@
import {
Avatar,
AvatarFallback,
AvatarImage,
} from '@affine/admin/components/ui/avatar';
import { Button } from '@affine/admin/components/ui/button';
import {
DropdownMenu,
@@ -8,9 +13,13 @@ import {
DropdownMenuTrigger,
} from '@affine/admin/components/ui/dropdown-menu';
import { useQuery } from '@affine/core/hooks/use-query';
import { FeatureType, getCurrentUserFeaturesQuery } from '@affine/graphql';
import { CircleUser } from 'lucide-react';
import { useEffect } from 'react';
import {
FeatureType,
getCurrentUserFeaturesQuery,
serverConfigQuery,
} from '@affine/graphql';
import { CircleUser, MoreVertical } from 'lucide-react';
import { useCallback, useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
import { toast } from 'sonner';
@@ -20,9 +29,32 @@ export function UserDropdown() {
} = useQuery({
query: getCurrentUserFeaturesQuery,
});
const {
data: { serverConfig },
} = useQuery({
query: serverConfigQuery,
});
const navigate = useNavigate();
const handleLogout = useCallback(() => {
fetch('/api/auth/sign-out', {
method: 'POST',
})
.then(() => {
navigate('/admin/auth');
})
.catch(err => {
toast.error(`Failed to logout: ${err.message}`);
});
}, [navigate]);
useEffect(() => {
if (serverConfig.initialized === false) {
navigate('/admin/setup');
return;
}
if (!currentUser) {
navigate('/admin/auth');
return;
@@ -32,28 +64,47 @@ export function UserDropdown() {
navigate('/admin/auth');
return;
}
}, [currentUser, navigate]);
const avatar = currentUser?.avatarUrl ? (
<img src={currentUser?.avatarUrl} />
) : (
<CircleUser size={24} />
);
}, [currentUser, navigate, serverConfig.initialized]);
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="secondary" size="icon" className="rounded-full">
{avatar}
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuLabel>{currentUser?.name}</DropdownMenuLabel>
<DropdownMenuSeparator />
<DropdownMenuItem>Settings</DropdownMenuItem>
<DropdownMenuItem>Support</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem>Logout</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
<div className="flex items-center justify-between px-4 py-3 flex-nowrap">
<div className="flex items-center gap-2 font-medium text-ellipsis break-words overflow-hidden">
<Avatar className="w-6 h-6">
<AvatarImage src={currentUser?.avatarUrl ?? undefined} />
<AvatarFallback>
<CircleUser size={24} />
</AvatarFallback>
</Avatar>
{currentUser?.name ? (
<span className="text-sm text-nowrap text-ellipsis break-words overflow-hidden">
{currentUser?.name}
</span>
) : (
// Fallback to email prefix if name is not available
<span className="text-sm">{currentUser?.email.split('@')[0]}</span>
)}
<span
className="rounded p-1 text-xs"
style={{
backgroundColor: 'rgba(30, 150, 235, 0.20)',
color: 'rgba(30, 150, 235, 1)',
}}
>
Admin
</span>
</div>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" className="ml-2 p-1 h-6">
<MoreVertical size={20} />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuLabel>{currentUser?.name}</DropdownMenuLabel>
<DropdownMenuSeparator />
<DropdownMenuItem onSelect={handleLogout}>Logout</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
);
}