fix(admin): adjust admin panel style (#11065)

close AF-2353 AF-2354 AF-2355 AF-2356 AF-2358 AF-2360 AF-2362 AF-2363 AF-2364 AF-2389
Fixed some UI issues.
This commit is contained in:
JimmFly
2025-03-26 03:16:14 +00:00
parent ce7e3330f4
commit 28854ef274
16 changed files with 180 additions and 139 deletions

View File

@@ -5,16 +5,10 @@ import {
} from '@affine/admin/components/ui/avatar';
import type { UserType } from '@affine/graphql';
import { FeatureType } from '@affine/graphql';
import { AccountIcon, LockIcon, UnlockIcon } from '@blocksuite/icons/rc';
import type { ColumnDef } from '@tanstack/react-table';
import { cssVarV2 } from '@toeverything/theme/v2';
import clsx from 'clsx';
import {
LockIcon,
MailIcon,
MailWarningIcon,
UnlockIcon,
UserIcon,
} from 'lucide-react';
import { MailIcon } from 'lucide-react';
import type { ReactNode } from 'react';
import { Checkbox } from '../../../components/ui/checkbox';
@@ -35,10 +29,10 @@ const StatusItem = ({
textFalse: string;
}) => (
<div
className={clsx(
'flex gap-2 items-center',
!condition ? 'text-red-500 opacity-100' : 'opacity-25'
)}
className="flex gap-1 items-center"
style={{
color: condition ? cssVarV2('text/secondary') : cssVarV2('status/error'),
}}
>
{condition ? (
<>
@@ -89,7 +83,7 @@ export const columns: ColumnDef<UserType>[] = [
<Avatar className="w-10 h-10">
<AvatarImage src={row.original.avatarUrl ?? undefined} />
<AvatarFallback>
<UserIcon size={20} />
<AccountIcon fontSize={20} />
</AvatarFallback>
</Avatar>
<div className="flex flex-col gap-1 max-w-full overflow-hidden">
@@ -97,7 +91,7 @@ export const columns: ColumnDef<UserType>[] = [
<span>{row.original.name}</span>
{row.original.features.includes(FeatureType.Admin) && (
<span
className="ml-2 rounded px-2 py-0.5 text-xs h-5 border"
className="ml-2 rounded px-2 py-0.5 text-xs h-5 border text-center inline-flex items-center font-normal"
style={{
borderRadius: '4px',
backgroundColor: cssVarV2('chip/label/blue'),
@@ -120,7 +114,12 @@ export const columns: ColumnDef<UserType>[] = [
</span>
)}
</div>
<div className="text-xs font-medium opacity-50 max-w-full overflow-hidden">
<div
className="text-xs font-medium max-w-full overflow-hidden"
style={{
color: cssVarV2('text/secondary'),
}}
>
{row.original.email}
</div>
</div>
@@ -141,20 +140,29 @@ export const columns: ColumnDef<UserType>[] = [
cell: ({ row: { original: user } }) => (
<div className="flex items-center gap-2">
<div className="flex flex-col gap-2 text-xs max-md:hidden">
<div className="flex justify-end opacity-25">{user.id}</div>
<div className="flex gap-3 items-center justify-end">
<div className="flex justify-start">{user.id}</div>
<div className="flex gap-3 items-center justify-start">
<StatusItem
condition={user.hasPassword}
IconTrue={<LockIcon size={10} />}
IconFalse={<UnlockIcon size={10} />}
IconTrue={
<LockIcon
fontSize={16}
color={cssVarV2('selfhost/icon/tertiary')}
/>
}
IconFalse={<UnlockIcon fontSize={16} />}
textTrue="Password Set"
textFalse="No Password"
/>
<StatusItem
condition={user.emailVerified}
IconTrue={<MailIcon size={10} />}
IconFalse={<MailWarningIcon size={10} />}
IconTrue={
<MailIcon
size={16}
color={cssVarV2('selfhost/icon/tertiary')}
/>
}
IconFalse={<MailIcon size={16} />}
textTrue="Email Verified"
textFalse="Email Not Verified"
/>

View File

@@ -170,25 +170,21 @@ export function DataTableRowActions({ user }: DataTableRowActionsProps) {
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-[214px] p-[5px] gap-2">
<div className="px-2 py-[6px] text-sm font-semibold overflow-hidden text-ellipsis text-nowrap">
{user.name}
</div>
<DropdownMenuSeparator />
<DropdownMenuItem
className="px-2 py-[6px] text-sm font-medium gap-2 cursor-pointer"
className="px-2 py-[6px] text-sm font-normal gap-2 cursor-pointer"
onSelect={openResetPasswordDialog}
>
<LockIcon fontSize={20} /> Reset Password
</DropdownMenuItem>
<DropdownMenuItem
onSelect={handleEdit}
className="px-2 py-[6px] text-sm font-medium gap-2 cursor-pointer"
className="px-2 py-[6px] text-sm font-normal gap-2 cursor-pointer"
>
<EditIcon fontSize={20} /> Edit
</DropdownMenuItem>
{user.disabled && (
<DropdownMenuItem
className="px-2 py-[6px] text-sm font-medium gap-2 cursor-pointer"
className="px-2 py-[6px] text-sm font-normal gap-2 cursor-pointer"
onSelect={openEnableDialog}
>
<AccountBanIcon fontSize={20} /> Enable Email
@@ -197,14 +193,14 @@ export function DataTableRowActions({ user }: DataTableRowActionsProps) {
<DropdownMenuSeparator />
{!user.disabled && (
<DropdownMenuItem
className="px-2 py-[6px] text-sm font-medium gap-2 text-red-500 cursor-pointer focus:text-red-500"
className="px-2 py-[6px] text-sm font-normal gap-2 text-red-500 cursor-pointer focus:text-red-500"
onSelect={openDisableDialog}
>
<AccountBanIcon fontSize={20} /> Disable & Delete data
</DropdownMenuItem>
)}
<DropdownMenuItem
className="px-2 py-[6px] text-sm font-medium gap-2 text-red-500 cursor-pointer focus:text-red-500"
className="px-2 py-[6px] text-sm font-normal gap-2 text-red-500 cursor-pointer focus:text-red-500"
onSelect={openDeleteDialog}
>
<DeleteIcon fontSize={20} /> Delete

View File

@@ -69,9 +69,9 @@ export function DataTable<TData, TValue>({
}, [data]);
return (
<div className="flex flex-col gap-4 py-5 px-6 h-full">
<div className="flex flex-col gap-4 py-5 px-6 h-full overflow-auto">
<DataTableToolbar setDataTable={setTableData} data={data} table={table} />
<div className="rounded-md border max-h-[75vh] h-full overflow-auto">
<div className="rounded-md border h-full flex flex-col overflow-auto">
<Table>
<TableHeader>
{table.getHeaderGroups().map(headerGroup => (
@@ -107,49 +107,54 @@ export function DataTable<TData, TValue>({
</TableRow>
))}
</TableHeader>
<TableBody>
{table.getRowModel().rows?.length ? (
table.getRowModel().rows.map(row => (
<TableRow key={row.id} className="flex items-center">
{row.getVisibleCells().map(cell => {
let columnClassName = '';
if (cell.column.id === 'select') {
columnClassName = 'w-[40px] flex-shrink-0';
} else if (cell.column.id === 'info') {
columnClassName = 'flex-1';
} else if (cell.column.id === 'property') {
columnClassName = 'flex-1';
} else if (cell.column.id === 'actions') {
columnClassName =
'w-[40px] flex-shrink-0 justify-center mr-6';
}
return (
<TableCell
key={cell.id}
className={`${columnClassName} flex items-center`}
>
{flexRender(
cell.column.columnDef.cell,
cell.getContext()
)}
</TableCell>
);
})}
</TableRow>
))
) : (
<TableRow>
<TableCell
colSpan={columns.length}
className="h-24 text-center"
>
No results.
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
<div className="overflow-auto flex-1">
<Table>
<TableBody>
{table.getRowModel().rows?.length ? (
table.getRowModel().rows.map(row => (
<TableRow key={row.id} className="flex items-center">
{row.getVisibleCells().map(cell => {
let columnClassName = '';
if (cell.column.id === 'select') {
columnClassName = 'w-[40px] flex-shrink-0';
} else if (cell.column.id === 'info') {
columnClassName = 'flex-1';
} else if (cell.column.id === 'property') {
columnClassName = 'flex-1';
} else if (cell.column.id === 'actions') {
columnClassName =
'w-[40px] flex-shrink-0 justify-center mr-6';
}
return (
<TableCell
key={cell.id}
className={`${columnClassName} flex items-center`}
>
{flexRender(
cell.column.columnDef.cell,
cell.getContext()
)}
</TableCell>
);
})}
</TableRow>
))
) : (
<TableRow>
<TableCell
colSpan={columns.length}
className="h-24 text-center"
>
No results.
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
</div>
</div>
<DataTablePagination table={table} />

View File

@@ -56,7 +56,7 @@ export const DeleteAccountDialog = ({
className="placeholder:opacity-50"
/>
<DialogFooter>
<div className="flex justify-between items-center w-full">
<div className="flex justify-end gap-2 items-center w-full">
<Button type="button" variant="outline" size="sm" onClick={onClose}>
Cancel
</Button>

View File

@@ -56,7 +56,7 @@ export const DisableAccountDialog = ({
className="placeholder:opacity-50"
/>
<DialogFooter>
<div className="flex justify-between items-center w-full">
<div className="flex justify-end gap-2 items-center w-full">
<Button type="button" variant="outline" size="sm" onClick={onClose}>
Cancel
</Button>

View File

@@ -23,13 +23,13 @@ export const DiscardChanges = ({
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="sm:w-[460px]">
<DialogHeader>
<DialogTitle className="leading-7">Discard Changes</DialogTitle>
<DialogDescription className="leading-6">
<DialogTitle className="leading-6.5">Discard Changes</DialogTitle>
<DialogDescription className="leading-6 text-[15px]">
Changes to this user will not be saved.
</DialogDescription>
</DialogHeader>
<DialogFooter>
<div className="flex justify-end items-center w-full space-x-4">
<div className="flex justify-end gap-2 items-center w-full">
<Button type="button" onClick={onClose} variant="outline">
<span>Cancel</span>
</Button>

View File

@@ -33,7 +33,7 @@ export const EnableAccountDialog = ({
</DialogDescription>
</DialogHeader>
<DialogFooter>
<div className="flex justify-end items-center w-full space-x-4">
<div className="flex justify-end gap-2 items-center w-full">
<Button type="button" onClick={onClose} variant="outline">
<span>Cancel</span>
</Button>

View File

@@ -2,6 +2,7 @@ import { Button } from '@affine/admin/components/ui/button';
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
@@ -198,7 +199,9 @@ export function ImportUsersDialog({
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent
className={isPreviewMode ? 'sm:max-w-[600px]' : 'sm:max-w-[425px]'}
className={
isPreviewMode ? 'sm:max-w-[600px] flex-col gap-3' : 'sm:max-w-[425px]'
}
>
<DialogHeader>
<DialogTitle>
@@ -211,35 +214,32 @@ export function ImportUsersDialog({
: 'Import'}
</DialogTitle>
</DialogHeader>
{isFormatError ? (
<div className="grid gap-4 py-4">
<p className="text-sm text-gray-500">
You need to import the accounts by importing a CSV file in the
correct format. Please download the CSV template.
</p>
</div>
) : isPreviewMode ? (
<div className="grid gap-4 py-4">
<p className="text-sm text-gray-500">
{parsedUsers.length} users detected from the CSV file. Please
confirm the user list below and import.
</p>
<UserTable users={parsedUsers} />
</div>
) : (
<div className="grid gap-4 py-4">
<p className="text-sm text-gray-500">
You need to import the accounts by importing a CSV file in the
correct format. Please download the CSV template.
</p>
<FileUploadArea
ref={fileUploadRef}
onFileSelected={handleFileSelected}
/>
</div>
)}
<DialogDescription className="text-[15px]">
{isFormatError ? (
'You need to import the accounts by importing a CSV file in the correct format. Please download the CSV template.'
) : isPreviewMode ? (
<div className="grid gap-3">
{isImported ? null : (
<p>
{parsedUsers.length} users detected from the CSV file. Please
confirm the user list below and import.
</p>
)}
<UserTable users={parsedUsers} />
</div>
) : (
<div className="grid gap-3">
<p>
You need to import the accounts by importing a CSV file in the
correct format. Please download the CSV template.
</p>
<FileUploadArea
ref={fileUploadRef}
onFileSelected={handleFileSelected}
/>
</div>
)}
</DialogDescription>
<DialogFooter
className={`flex-col sm:flex-row sm:justify-between items-center ${isPreviewMode ? 'sm:justify-end' : 'sm:justify-between'}`}
@@ -265,9 +265,8 @@ export function ImportUsersDialog({
<>
<Button
variant="outline"
size="sm"
onClick={cancelImport}
className="mb-2 sm:mb-0"
className="w-full mb-2 sm:mb-0 sm:w-auto"
disabled={isImporting}
>
Cancel

View File

@@ -32,7 +32,7 @@ export const ResetPasswordDialog = ({
</DialogDescription>
</DialogHeader>
<DialogFooter>
<div className="flex justify-between items-center w-full space-x-4">
<div className="flex justify-end gap-2 items-center w-full">
<Input
type="text"
value={link}

View File

@@ -29,7 +29,7 @@ export const DiscardChanges = ({
</DialogDescription>
</DialogHeader>
<DialogFooter>
<div className="flex justify-end items-center w-full space-x-4">
<div className="flex justify-end gap-2 items-center w-full">
<Button type="button" onClick={onClose} variant="outline">
<span>Cancel</span>
</Button>

View File

@@ -216,7 +216,7 @@ export const LeftPanel = ({
onExpand={onExpand}
onCollapse={onCollapse}
className={cn(
isCollapsed ? 'min-w-[56px] max-w-[56px]' : 'min-w-56 max-w-56',
isCollapsed ? 'min-w-[57px] max-w-[57px]' : 'min-w-56 max-w-56',
'border-r h-dvh'
)}
style={{ overflow: 'visible' }}
@@ -231,11 +231,18 @@ export const LeftPanel = ({
>
<div
className={cn(
'flex h-[56px] items-center gap-2 px-4 text-base font-medium',
'flex h-[56px] items-center px-4 text-base font-medium',
isCollapsed && 'justify-center px-2'
)}
>
<Logo />
<span
className={cn(
'flex items-center p-0.5 mr-2',
isCollapsed && 'justify-center px-2 mr-0'
)}
>
<Logo />
</span>
{!isCollapsed && 'AFFiNE'}
</div>
<Nav isCollapsed={isCollapsed} />
@@ -283,12 +290,12 @@ export const RightPanel = ({
order={2}
ref={panelRef}
defaultSize={0}
maxSize={30}
maxSize={20}
collapsible={true}
collapsedSize={0}
onExpand={onExpand}
onCollapse={onCollapse}
className="border-l"
className="border-l max-w-96"
>
{panelContent}
</ResizablePanel>

View File

@@ -60,7 +60,7 @@ const NavItem = ({ icon, label, to, isCollapsed }: NavItemProps) => {
},
})}
>
{icon}
<span className="flex items-center p-0.5 mr-2">{icon}</span>
{label}
</NavLink>
);
@@ -86,25 +86,19 @@ export function Nav({ isCollapsed = false }: NavProps) {
>
<NavItem
to="/admin/config"
icon={
<SelfhostIcon className={cn(!isCollapsed && 'mr-2', 'h-5 w-5')} />
}
icon={<SelfhostIcon fontSize={20} />}
label="Server"
isCollapsed={isCollapsed}
/>
<NavItem
to="/admin/accounts"
icon={
<AccountIcon className={cn(!isCollapsed && 'mr-2', 'h-5 w-5')} />
}
icon={<AccountIcon fontSize={20} />}
label="Accounts"
isCollapsed={isCollapsed}
/>
<NavItem
to="/admin/ai"
icon={
<AiOutlineIcon className={cn(!isCollapsed && 'mr-2', 'h-5 w-5')} />
}
icon={<AiOutlineIcon fontSize={20} />}
label="AI"
isCollapsed={isCollapsed}
/>

View File

@@ -17,10 +17,15 @@ export const ServerVersion = () => {
return (
<Button
variant="outline"
className="flex items-center justify-center gap-1 text-xs p-2 font-medium"
className="flex items-center justify-center gap-1 text-xs p-2 font-medium w-full overflow-hidden"
onClick={handleClick}
title={`New Version ${availableUpgrade.version} Available`}
>
New Version <span>{availableUpgrade.version}</span>Available
<span className="overflow-hidden text-ellipsis">
New Version
<span>{availableUpgrade.version}</span>
Available
</span>
</Button>
);
}

View File

@@ -52,7 +52,7 @@ export const SettingsItem = ({ isCollapsed }: { isCollapsed: boolean }) => {
: undefined,
})}
>
<SettingsIcon className="h-5 w-5" />
<SettingsIcon fontSize={20} />
</NavLink>
</NavigationMenuPrimitive.Trigger>
<NavigationMenuPrimitive.Content>
@@ -119,7 +119,9 @@ export const SettingsItem = ({ isCollapsed }: { isCollapsed: boolean }) => {
}
>
<div className="flex items-center">
<SettingsIcon className={cn(!isCollapsed && 'mr-2', 'h-5 w-5')} />
<span className="flex items-center p-0.5 mr-2">
<SettingsIcon fontSize={20} />
</span>
<span>Settings</span>
</div>
</AccordionTrigger>

View File

@@ -73,15 +73,20 @@ export function UserDropdown({ isCollapsed }: UserDropdownProps) {
</AvatarFallback>
</Avatar>
{currentUser?.name ? (
<span className="text-sm text-nowrap text-ellipsis break-words overflow-hidden">
<span
className="text-sm text-nowrap text-ellipsis break-words overflow-hidden"
title={currentUser?.name}
>
{currentUser?.name}
</span>
) : (
// Fallback to email prefix if name is not available
<span className="text-sm">{currentUser?.email.split('@')[0]}</span>
<span className="text-sm" title={currentUser?.email.split('@')[0]}>
{currentUser?.email.split('@')[0]}
</span>
)}
<span
className="ml-2 rounded px-2 py-0.5 text-xs h-5 border"
className="ml-2 rounded px-2 py-0.5 text-xs h-5 border text-center inline-flex items-center font-normal"
style={{
borderRadius: '4px',
backgroundColor: cssVarV2('chip/label/blue'),
@@ -98,7 +103,27 @@ export function UserDropdown({ isCollapsed }: UserDropdownProps) {
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" side="right">
<DropdownMenuLabel>{currentUser?.name}</DropdownMenuLabel>
<DropdownMenuLabel className="flex items-center gap-2">
<Avatar className="w-8 h-8">
<AvatarImage src={currentUser?.avatarUrl ?? undefined} />
<AvatarFallback>
<CircleUser size={32} />
</AvatarFallback>
</Avatar>
<div className="flex flex-col font-medium gap-1">
{currentUser?.name ?? currentUser?.email.split('@')[0]}
<span
className="w-fit rounded px-2 py-0.5 text-xs h-5 border text-center inline-flex items-center font-normal"
style={{
borderRadius: '4px',
backgroundColor: cssVarV2('chip/label/blue'),
borderColor: cssVarV2('layer/insideBorder/border'),
}}
>
Admin
</span>
</div>
</DropdownMenuLabel>
<DropdownMenuSeparator />
<DropdownMenuItem onSelect={handleLogout}>Logout</DropdownMenuItem>
</DropdownMenuContent>

View File

@@ -66,7 +66,7 @@ export const ConfirmChanges = ({
'There is no change.'
)}
<DialogFooter>
<div className="flex justify-end items-center w-full space-x-4">
<div className="flex justify-end items-center w-full gap-2">
<Button type="button" onClick={onClose} variant="outline">
<span>Cancel</span>
</Button>