mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-14 21:27:20 +00:00
feat(admin): add server runtime config settings (#7618)
This commit is contained in:
67
packages/frontend/admin/src/modules/nav/collapsible-item.tsx
Normal file
67
packages/frontend/admin/src/modules/nav/collapsible-item.tsx
Normal file
@@ -0,0 +1,67 @@
|
||||
import {
|
||||
Accordion,
|
||||
AccordionContent,
|
||||
AccordionItem,
|
||||
AccordionTrigger,
|
||||
} from '@affine/admin/components/ui/accordion';
|
||||
import { useCallback } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
import { useNav } from './context';
|
||||
|
||||
export const CollapsibleItem = ({
|
||||
items,
|
||||
title,
|
||||
changeModule,
|
||||
}: {
|
||||
title: string;
|
||||
items: string[];
|
||||
changeModule?: (module: string) => void;
|
||||
}) => {
|
||||
const { activeSubTab, setActiveSubTab } = useNav();
|
||||
const handleClick = useCallback(
|
||||
(id: string) => {
|
||||
const targetElement = document.getElementById(id);
|
||||
if (targetElement) {
|
||||
targetElement.scrollIntoView({
|
||||
behavior: 'smooth',
|
||||
block: 'center',
|
||||
});
|
||||
}
|
||||
changeModule?.(title);
|
||||
setActiveSubTab(id);
|
||||
},
|
||||
[changeModule, setActiveSubTab, title]
|
||||
);
|
||||
|
||||
return (
|
||||
<Accordion type="multiple" className="w-full ">
|
||||
<AccordionItem value="item-1" className="border-b-0">
|
||||
<Link to={`/admin/settings#${title}`}>
|
||||
<AccordionTrigger
|
||||
onClick={() => handleClick(title)}
|
||||
className={`py-2 px-3 rounded ${activeSubTab === title ? 'bg-zinc-100' : ''}`}
|
||||
>
|
||||
{title}
|
||||
</AccordionTrigger>
|
||||
</Link>
|
||||
<AccordionContent className=" flex flex-col gap-2">
|
||||
{items.map((item, index) => (
|
||||
<Link
|
||||
key={index}
|
||||
to={`/admin/settings#${item}`}
|
||||
className="px-3 overflow-hidden"
|
||||
>
|
||||
<AccordionContent
|
||||
onClick={() => handleClick(item)}
|
||||
className={`py-1 px-2 rounded text-ellipsis whitespace-nowrap overflow-hidden ${activeSubTab === item ? 'bg-zinc-100' : ''}`}
|
||||
>
|
||||
{item}
|
||||
</AccordionContent>
|
||||
</Link>
|
||||
))}
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
</Accordion>
|
||||
);
|
||||
};
|
||||
21
packages/frontend/admin/src/modules/nav/context.ts
Normal file
21
packages/frontend/admin/src/modules/nav/context.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import { createContext, useContext } from 'react';
|
||||
|
||||
interface NavContextType {
|
||||
activeTab: string;
|
||||
activeSubTab: string;
|
||||
currentModule: string;
|
||||
setActiveTab: (tab: string) => void;
|
||||
setActiveSubTab: (tab: string) => void;
|
||||
setCurrentModule: (module: string) => void;
|
||||
}
|
||||
|
||||
export const NavContext = createContext<NavContextType | undefined>(undefined);
|
||||
export const useNav = () => {
|
||||
const context = useContext(NavContext);
|
||||
|
||||
if (!context) {
|
||||
throw new Error('useNav must be used within a NavProvider');
|
||||
}
|
||||
|
||||
return context;
|
||||
};
|
||||
@@ -1,113 +0,0 @@
|
||||
import { Button } from '@affine/admin/components/ui/button';
|
||||
import {
|
||||
Sheet,
|
||||
SheetContent,
|
||||
SheetTrigger,
|
||||
} from '@affine/admin/components/ui/sheet';
|
||||
import { Menu, Package2 } from 'lucide-react';
|
||||
import type { PropsWithChildren } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
import { UserDropdown } from './user-dropdown';
|
||||
|
||||
export function Nav({ children }: PropsWithChildren<unknown>) {
|
||||
return (
|
||||
<div className="flex min-h-screen w-full flex-col">
|
||||
<header className="sticky top-0 flex h-16 items-center gap-4 border-b bg-background px-4 md:px-6">
|
||||
<nav className="hidden flex-col gap-6 text-lg font-medium md:flex md:flex-row md:items-center md:gap-5 md:text-sm lg:gap-6">
|
||||
<Link
|
||||
to="/"
|
||||
className="flex items-center gap-2 text-lg font-semibold md:text-base"
|
||||
>
|
||||
<Package2 className="h-6 w-6" />
|
||||
<span className="sr-only">AFFiNE</span>
|
||||
</Link>
|
||||
<Link
|
||||
to="/"
|
||||
className="text-foreground transition-colors hover:text-foreground"
|
||||
>
|
||||
Dashboard
|
||||
</Link>
|
||||
<Link
|
||||
to="/admin/users"
|
||||
className="text-muted-foreground transition-colors hover:text-foreground"
|
||||
>
|
||||
Users
|
||||
</Link>
|
||||
<Link
|
||||
to="/"
|
||||
className="text-muted-foreground transition-colors hover:text-foreground"
|
||||
>
|
||||
Configs
|
||||
</Link>
|
||||
<Link
|
||||
to="/"
|
||||
className="text-muted-foreground transition-colors hover:text-foreground"
|
||||
>
|
||||
Backups
|
||||
</Link>
|
||||
<Link
|
||||
to="/"
|
||||
className="text-muted-foreground transition-colors hover:text-foreground"
|
||||
>
|
||||
Analytics
|
||||
</Link>
|
||||
</nav>
|
||||
<Sheet>
|
||||
<SheetTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
className="shrink-0 md:hidden"
|
||||
>
|
||||
<Menu className="h-5 w-5" />
|
||||
<span className="sr-only">Toggle navigation menu</span>
|
||||
</Button>
|
||||
</SheetTrigger>
|
||||
<SheetContent side="left">
|
||||
<nav className="grid gap-6 text-lg font-medium">
|
||||
<Link
|
||||
to="/"
|
||||
className="flex items-center gap-2 text-lg font-semibold"
|
||||
>
|
||||
<Package2 className="h-6 w-6" />
|
||||
<span className="sr-only">Acme Inc</span>
|
||||
</Link>
|
||||
<Link to="/" className="hover:text-foreground">
|
||||
Dashboard
|
||||
</Link>
|
||||
<Link
|
||||
to="/"
|
||||
className="text-muted-foreground hover:text-foreground"
|
||||
>
|
||||
Orders
|
||||
</Link>
|
||||
<Link
|
||||
to="/"
|
||||
className="text-muted-foreground hover:text-foreground"
|
||||
>
|
||||
Products
|
||||
</Link>
|
||||
<Link
|
||||
to="/"
|
||||
className="text-muted-foreground hover:text-foreground"
|
||||
>
|
||||
Customers
|
||||
</Link>
|
||||
<Link
|
||||
to="/"
|
||||
className="text-muted-foreground hover:text-foreground"
|
||||
>
|
||||
Analytics
|
||||
</Link>
|
||||
</nav>
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
<div className="flex w-full items-center justify-end gap-4 md:ml-auto md:gap-2 lg:gap-4">
|
||||
<UserDropdown />
|
||||
</div>
|
||||
</header>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,56 +1,158 @@
|
||||
import {
|
||||
Accordion,
|
||||
AccordionContent,
|
||||
AccordionItem,
|
||||
AccordionTrigger,
|
||||
} from '@affine/admin/components/ui/accordion';
|
||||
import { buttonVariants } from '@affine/admin/components/ui/button';
|
||||
import { cn } from '@affine/admin/utils';
|
||||
import type { LucideIcon } from 'lucide-react';
|
||||
import * as ScrollAreaPrimitive from '@radix-ui/react-scroll-area';
|
||||
import {
|
||||
ClipboardListIcon,
|
||||
CpuIcon,
|
||||
SettingsIcon,
|
||||
UsersIcon,
|
||||
} from 'lucide-react';
|
||||
import { useEffect } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
import { useGetServerRuntimeConfig } from '../settings/use-get-server-runtime-config';
|
||||
import { CollapsibleItem } from './collapsible-item';
|
||||
import { useNav } from './context';
|
||||
import { UserDropdown } from './user-dropdown';
|
||||
|
||||
export interface NavProp {
|
||||
title: string;
|
||||
to: string;
|
||||
label?: string;
|
||||
icon: LucideIcon;
|
||||
}
|
||||
const TabsMap: { [key: string]: string } = {
|
||||
accounts: 'Accounts',
|
||||
ai: 'AI',
|
||||
config: 'Config',
|
||||
settings: 'Settings',
|
||||
};
|
||||
|
||||
const defaultTab = 'Accounts';
|
||||
|
||||
export function Nav() {
|
||||
const { moduleList } = useGetServerRuntimeConfig();
|
||||
const { activeTab, setActiveTab, setCurrentModule } = useNav();
|
||||
|
||||
useEffect(() => {
|
||||
const path = window.location.pathname;
|
||||
for (const key in TabsMap) {
|
||||
if (path.includes(key)) {
|
||||
setActiveTab(TabsMap[key]);
|
||||
return;
|
||||
}
|
||||
}
|
||||
setActiveTab(defaultTab);
|
||||
}, [setActiveTab]);
|
||||
|
||||
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'
|
||||
)}
|
||||
<div className="flex flex-col gap-4 py-2 justify-between flex-grow overflow-hidden">
|
||||
<nav className="flex flex-col gap-1 px-2 flex-grow overflow-hidden">
|
||||
<Link
|
||||
to={'/admin/accounts'}
|
||||
className={cn(
|
||||
buttonVariants({
|
||||
variant: activeTab === 'Accounts' ? 'default' : 'ghost',
|
||||
size: 'sm',
|
||||
}),
|
||||
activeTab === 'Accounts' &&
|
||||
'dark:bg-muted dark:text-white dark:hover:bg-muted dark:hover:text-white',
|
||||
'justify-start',
|
||||
'flex-none'
|
||||
)}
|
||||
>
|
||||
<UsersIcon className="mr-2 h-4 w-4" />
|
||||
Accounts
|
||||
</Link>
|
||||
<Link
|
||||
to={'/admin/ai'}
|
||||
className={cn(
|
||||
buttonVariants({
|
||||
variant: activeTab === 'AI' ? 'default' : 'ghost',
|
||||
size: 'sm',
|
||||
}),
|
||||
activeTab === 'AI' &&
|
||||
'dark:bg-muted dark:text-white dark:hover:bg-muted dark:hover:text-white',
|
||||
'justify-start',
|
||||
'flex-none'
|
||||
)}
|
||||
>
|
||||
<CpuIcon className="mr-2 h-4 w-4" />
|
||||
AI
|
||||
</Link>
|
||||
<Link
|
||||
to={'/admin/config'}
|
||||
className={cn(
|
||||
buttonVariants({
|
||||
variant: activeTab === 'Config' ? 'default' : 'ghost',
|
||||
size: 'sm',
|
||||
}),
|
||||
activeTab === 'Config' &&
|
||||
'dark:bg-muted dark:text-white dark:hover:bg-muted dark:hover:text-white',
|
||||
'justify-start',
|
||||
'flex-none'
|
||||
)}
|
||||
>
|
||||
<ClipboardListIcon className="mr-2 h-4 w-4" />
|
||||
Config
|
||||
</Link>
|
||||
|
||||
<Accordion type="multiple" className="w-full h-full overflow-hidden">
|
||||
<AccordionItem
|
||||
value="item-1"
|
||||
className="border-b-0 h-full flex flex-col gap-1"
|
||||
>
|
||||
<link.icon className="mr-2 h-4 w-4" />
|
||||
{link.title}
|
||||
{link.label && (
|
||||
<span
|
||||
<Link to={'/admin/settings'}>
|
||||
<AccordionTrigger
|
||||
className={cn(
|
||||
'ml-auto',
|
||||
activeTab === link.title && 'text-background dark:text-white'
|
||||
buttonVariants({
|
||||
variant: activeTab === 'Settings' ? 'default' : 'ghost',
|
||||
size: 'sm',
|
||||
}),
|
||||
|
||||
activeTab === 'Settings' &&
|
||||
'dark:bg-muted dark:text-white dark:hover:bg-muted dark:hover:text-white',
|
||||
'justify-between',
|
||||
'hover:no-underline'
|
||||
)}
|
||||
>
|
||||
{link.label}
|
||||
</span>
|
||||
)}
|
||||
</Link>
|
||||
))}
|
||||
<div className="flex items-center">
|
||||
<SettingsIcon className="mr-2 h-4 w-4" />
|
||||
<span>Settings</span>
|
||||
</div>
|
||||
</AccordionTrigger>
|
||||
</Link>
|
||||
|
||||
<AccordionContent className="h-full overflow-hidden w-full">
|
||||
<ScrollAreaPrimitive.Root
|
||||
className={cn('relative overflow-hidden w-full h-full')}
|
||||
>
|
||||
<ScrollAreaPrimitive.Viewport className="h-full w-full rounded-[inherit] [&>div]:!block">
|
||||
{moduleList.map(module => (
|
||||
<CollapsibleItem
|
||||
key={module.moduleName}
|
||||
items={module.keys}
|
||||
title={module.moduleName}
|
||||
changeModule={setCurrentModule}
|
||||
/>
|
||||
))}
|
||||
</ScrollAreaPrimitive.Viewport>
|
||||
<ScrollAreaPrimitive.ScrollAreaScrollbar
|
||||
className={cn(
|
||||
'flex touch-none select-none transition-colors',
|
||||
|
||||
'h-full w-2.5 border-l border-l-transparent p-[1px]'
|
||||
)}
|
||||
>
|
||||
<ScrollAreaPrimitive.ScrollAreaThumb className="relative flex-1 rounded-full bg-border" />
|
||||
</ScrollAreaPrimitive.ScrollAreaScrollbar>
|
||||
<ScrollAreaPrimitive.Corner />
|
||||
</ScrollAreaPrimitive.Root>
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
</Accordion>
|
||||
</nav>
|
||||
|
||||
<UserDropdown />
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -67,7 +67,7 @@ export function UserDropdown() {
|
||||
}, [currentUser, navigate, serverConfig.initialized]);
|
||||
|
||||
return (
|
||||
<div className="flex items-center justify-between px-4 py-3 flex-nowrap">
|
||||
<div className="flex flex-none 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} />
|
||||
|
||||
Reference in New Issue
Block a user