feat(admin): add server runtime config settings (#7618)

This commit is contained in:
JimmFly
2024-08-13 14:51:31 +08:00
committed by GitHub
parent 7f7c0519a0
commit bf6e36de37
17 changed files with 762 additions and 223 deletions

View 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>
);
};

View 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;
};

View File

@@ -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>
);
}

View File

@@ -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>
);

View File

@@ -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} />