mirror of
https://github.com/toeverything/AFFiNE.git
synced 2024-12-02 15:44:14 +03:00
feat(admin): add server runtime config settings (#7618)
This commit is contained in:
parent
7f7c0519a0
commit
bf6e36de37
@ -59,6 +59,10 @@ export const router = _createBrowserRouter(
|
|||||||
path: '/admin/config',
|
path: '/admin/config',
|
||||||
lazy: () => import('./modules/config'),
|
lazy: () => import('./modules/config'),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: '/admin/settings',
|
||||||
|
lazy: () => import('./modules/settings'),
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
@ -55,7 +55,7 @@ export function CreateUserPanel() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col h-full gap-1">
|
<div className="flex flex-col h-full gap-1">
|
||||||
<div className="flex justify-between items-center py-[10px] px-6">
|
<div className="flex-grow-0 flex-shrink-0 h-[56px] flex justify-between items-center py-[10px] px-6">
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
size="icon"
|
size="icon"
|
||||||
|
@ -61,7 +61,7 @@ export function DataTable<TData, TValue>({
|
|||||||
}, [data]);
|
}, [data]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-4 py-5 px-6 h-full">
|
<div className="flex flex-col gap-4 py-5 px-6 h-full">
|
||||||
<DataTableToolbar setDataTable={setTableData} data={data} />
|
<DataTableToolbar setDataTable={setTableData} data={data} />
|
||||||
<ScrollArea className="rounded-md border max-h-[75vh] h-full">
|
<ScrollArea className="rounded-md border max-h-[75vh] h-full">
|
||||||
<Table>
|
<Table>
|
||||||
|
@ -71,7 +71,7 @@ export function EditPanel({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col h-full gap-1">
|
<div className="flex flex-col h-full gap-1">
|
||||||
<div className="flex justify-between items-center py-[10px] px-6">
|
<div className="flex-grow-0 flex-shrink-0 h-[56px] flex justify-between items-center py-[10px] px-6 ">
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
size="icon"
|
size="icon"
|
||||||
|
@ -29,8 +29,8 @@ export function AccountPage() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className=" h-screen flex-1 space-y-1 flex-col flex">
|
<div className=" h-screen flex-1 flex-col flex">
|
||||||
<div className="flex items-center justify-between px-6 py-3 max-md:ml-9">
|
<div className="flex items-center justify-between px-6 py-3 max-md:ml-9 max-md:mt-[2px]">
|
||||||
<div className="text-base font-medium">Accounts</div>
|
<div className="text-base font-medium">Accounts</div>
|
||||||
</div>
|
</div>
|
||||||
<Separator />
|
<Separator />
|
||||||
|
@ -6,20 +6,13 @@ import {
|
|||||||
import { Separator } from '@affine/admin/components/ui/separator';
|
import { Separator } from '@affine/admin/components/ui/separator';
|
||||||
import { TooltipProvider } from '@affine/admin/components/ui/tooltip';
|
import { TooltipProvider } from '@affine/admin/components/ui/tooltip';
|
||||||
import { cn } from '@affine/admin/utils';
|
import { cn } from '@affine/admin/utils';
|
||||||
import {
|
import { AlignJustifyIcon } from 'lucide-react';
|
||||||
AlignJustifyIcon,
|
|
||||||
ClipboardList,
|
|
||||||
Cpu,
|
|
||||||
Settings,
|
|
||||||
Users,
|
|
||||||
} from 'lucide-react';
|
|
||||||
import type { ReactNode, RefObject } from 'react';
|
import type { ReactNode, RefObject } from 'react';
|
||||||
import {
|
import {
|
||||||
createContext,
|
createContext,
|
||||||
useCallback,
|
useCallback,
|
||||||
useContext,
|
useContext,
|
||||||
useEffect,
|
useEffect,
|
||||||
useMemo,
|
|
||||||
useRef,
|
useRef,
|
||||||
useState,
|
useState,
|
||||||
} from 'react';
|
} from 'react';
|
||||||
@ -35,7 +28,7 @@ import {
|
|||||||
SheetTrigger,
|
SheetTrigger,
|
||||||
} from '../components/ui/sheet';
|
} from '../components/ui/sheet';
|
||||||
import { Logo } from './accounts/components/logo';
|
import { Logo } from './accounts/components/logo';
|
||||||
import type { NavProp } from './nav/nav';
|
import { NavContext } from './nav/context';
|
||||||
import { Nav } from './nav/nav';
|
import { Nav } from './nav/nav';
|
||||||
|
|
||||||
interface LayoutProps {
|
interface LayoutProps {
|
||||||
@ -50,33 +43,11 @@ interface RightPanelContextType {
|
|||||||
openPanel: () => void;
|
openPanel: () => void;
|
||||||
closePanel: () => void;
|
closePanel: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const RightPanelContext = createContext<RightPanelContextType | undefined>(
|
const RightPanelContext = createContext<RightPanelContextType | undefined>(
|
||||||
undefined
|
undefined
|
||||||
);
|
);
|
||||||
|
|
||||||
const navLinks: NavProp[] = [
|
|
||||||
{
|
|
||||||
title: 'Accounts',
|
|
||||||
icon: Users,
|
|
||||||
to: '/admin/accounts',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: 'AI',
|
|
||||||
icon: Cpu,
|
|
||||||
to: '/admin/ai',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: 'Config',
|
|
||||||
icon: ClipboardList,
|
|
||||||
to: '/admin/config',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: 'Settings',
|
|
||||||
icon: Settings,
|
|
||||||
to: '/admin/settings',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
export const useRightPanel = () => {
|
export const useRightPanel = () => {
|
||||||
const context = useContext(RightPanelContext);
|
const context = useContext(RightPanelContext);
|
||||||
|
|
||||||
@ -110,6 +81,10 @@ export function Layout({ content }: LayoutProps) {
|
|||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
const rightPanelRef = useRef<ImperativePanelHandle>(null);
|
const rightPanelRef = useRef<ImperativePanelHandle>(null);
|
||||||
|
|
||||||
|
const [activeTab, setActiveTab] = useState('Accounts');
|
||||||
|
const [activeSubTab, setActiveSubTab] = useState('auth');
|
||||||
|
const [currentModule, setCurrentModule] = useState('auth');
|
||||||
|
|
||||||
const handleExpand = useCallback(() => {
|
const handleExpand = useCallback(() => {
|
||||||
if (rightPanelRef.current?.getSize() === 0) {
|
if (rightPanelRef.current?.getSize() === 0) {
|
||||||
rightPanelRef.current?.resize(30);
|
rightPanelRef.current?.resize(30);
|
||||||
@ -139,15 +114,6 @@ export function Layout({ content }: LayoutProps) {
|
|||||||
[closePanel, openPanel]
|
[closePanel, openPanel]
|
||||||
);
|
);
|
||||||
|
|
||||||
const activeTab = useMemo(() => {
|
|
||||||
const path = window.location.pathname;
|
|
||||||
|
|
||||||
return (
|
|
||||||
navLinks.find(link => path.endsWith(link.title.toLocaleLowerCase()))
|
|
||||||
?.title || ''
|
|
||||||
);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<RightPanelContext.Provider
|
<RightPanelContext.Provider
|
||||||
value={{
|
value={{
|
||||||
@ -159,37 +125,45 @@ export function Layout({ content }: LayoutProps) {
|
|||||||
closePanel,
|
closePanel,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<TooltipProvider delayDuration={0}>
|
<NavContext.Provider
|
||||||
<div className="flex">
|
value={{
|
||||||
<LeftPanel activeTab={activeTab} />
|
activeTab,
|
||||||
<ResizablePanelGroup direction="horizontal">
|
activeSubTab,
|
||||||
<ResizablePanel id="0" order={0} minSize={50}>
|
currentModule,
|
||||||
{content}
|
setActiveTab,
|
||||||
</ResizablePanel>
|
setActiveSubTab,
|
||||||
<RightPanel
|
setCurrentModule,
|
||||||
rightPanelRef={rightPanelRef}
|
}}
|
||||||
onExpand={handleExpand}
|
>
|
||||||
onCollapse={handleCollapse}
|
<TooltipProvider delayDuration={0}>
|
||||||
/>
|
<div className="flex">
|
||||||
</ResizablePanelGroup>
|
<LeftPanel />
|
||||||
</div>
|
<ResizablePanelGroup direction="horizontal">
|
||||||
</TooltipProvider>
|
<ResizablePanel id="0" order={0} minSize={50}>
|
||||||
|
{content}
|
||||||
|
</ResizablePanel>
|
||||||
|
<RightPanel
|
||||||
|
rightPanelRef={rightPanelRef}
|
||||||
|
onExpand={handleExpand}
|
||||||
|
onCollapse={handleCollapse}
|
||||||
|
/>
|
||||||
|
</ResizablePanelGroup>
|
||||||
|
</div>
|
||||||
|
</TooltipProvider>
|
||||||
|
</NavContext.Provider>
|
||||||
</RightPanelContext.Provider>
|
</RightPanelContext.Provider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export const LeftPanel = ({ activeTab }: { activeTab: string }) => {
|
export const LeftPanel = () => {
|
||||||
const isSmallScreen = useMediaQuery('(max-width: 768px)');
|
const isSmallScreen = useMediaQuery('(max-width: 768px)');
|
||||||
|
|
||||||
if (isSmallScreen) {
|
if (isSmallScreen) {
|
||||||
return (
|
return (
|
||||||
<Sheet>
|
<Sheet>
|
||||||
<SheetTrigger asChild>
|
<SheetTrigger asChild>
|
||||||
<Button
|
<Button variant="ghost" className="fixed top-4 left-6 p-0 h-5 w-5">
|
||||||
variant="ghost"
|
<AlignJustifyIcon size={20} />
|
||||||
className="fixed top-[14px] left-6 p-0 h-5 w-5"
|
|
||||||
>
|
|
||||||
<AlignJustifyIcon />
|
|
||||||
</Button>
|
</Button>
|
||||||
</SheetTrigger>
|
</SheetTrigger>
|
||||||
<SheetHeader className="hidden">
|
<SheetHeader className="hidden">
|
||||||
@ -209,7 +183,7 @@ export const LeftPanel = ({ activeTab }: { activeTab: string }) => {
|
|||||||
AFFiNE
|
AFFiNE
|
||||||
</div>
|
</div>
|
||||||
<Separator />
|
<Separator />
|
||||||
<Nav links={navLinks} activeTab={activeTab} />
|
<Nav />
|
||||||
</div>
|
</div>
|
||||||
</SheetContent>
|
</SheetContent>
|
||||||
</Sheet>
|
</Sheet>
|
||||||
@ -227,7 +201,7 @@ export const LeftPanel = ({ activeTab }: { activeTab: string }) => {
|
|||||||
AFFiNE
|
AFFiNE
|
||||||
</div>
|
</div>
|
||||||
<Separator />
|
<Separator />
|
||||||
<Nav links={navLinks} activeTab={activeTab} />
|
<Nav />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
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 { buttonVariants } from '@affine/admin/components/ui/button';
|
||||||
import { cn } from '@affine/admin/utils';
|
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 { 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';
|
import { UserDropdown } from './user-dropdown';
|
||||||
|
|
||||||
export interface NavProp {
|
const TabsMap: { [key: string]: string } = {
|
||||||
title: string;
|
accounts: 'Accounts',
|
||||||
to: string;
|
ai: 'AI',
|
||||||
label?: string;
|
config: 'Config',
|
||||||
icon: LucideIcon;
|
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 (
|
return (
|
||||||
<div className="group flex flex-col gap-4 py-2 justify-between flex-grow">
|
<div className="flex flex-col gap-4 py-2 justify-between flex-grow overflow-hidden">
|
||||||
<nav className="grid gap-1 px-2">
|
<nav className="flex flex-col gap-1 px-2 flex-grow overflow-hidden">
|
||||||
{links.map((link, index) => (
|
<Link
|
||||||
<Link
|
to={'/admin/accounts'}
|
||||||
key={index}
|
className={cn(
|
||||||
to={link.to}
|
buttonVariants({
|
||||||
className={cn(
|
variant: activeTab === 'Accounts' ? 'default' : 'ghost',
|
||||||
buttonVariants({
|
size: 'sm',
|
||||||
variant: activeTab === link.title ? 'default' : 'ghost',
|
}),
|
||||||
size: 'sm',
|
activeTab === 'Accounts' &&
|
||||||
}),
|
'dark:bg-muted dark:text-white dark:hover:bg-muted dark:hover:text-white',
|
||||||
activeTab === link.title &&
|
'justify-start',
|
||||||
'dark:bg-muted dark:text-white dark:hover:bg-muted dark:hover:text-white',
|
'flex-none'
|
||||||
'justify-start'
|
)}
|
||||||
)}
|
>
|
||||||
|
<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 to={'/admin/settings'}>
|
||||||
{link.title}
|
<AccordionTrigger
|
||||||
{link.label && (
|
|
||||||
<span
|
|
||||||
className={cn(
|
className={cn(
|
||||||
'ml-auto',
|
buttonVariants({
|
||||||
activeTab === link.title && 'text-background dark:text-white'
|
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}
|
<div className="flex items-center">
|
||||||
</span>
|
<SettingsIcon className="mr-2 h-4 w-4" />
|
||||||
)}
|
<span>Settings</span>
|
||||||
</Link>
|
</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>
|
</nav>
|
||||||
|
|
||||||
<UserDropdown />
|
<UserDropdown />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -67,7 +67,7 @@ export function UserDropdown() {
|
|||||||
}, [currentUser, navigate, serverConfig.initialized]);
|
}, [currentUser, navigate, serverConfig.initialized]);
|
||||||
|
|
||||||
return (
|
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">
|
<div className="flex items-center gap-2 font-medium text-ellipsis break-words overflow-hidden">
|
||||||
<Avatar className="w-6 h-6">
|
<Avatar className="w-6 h-6">
|
||||||
<AvatarImage src={currentUser?.avatarUrl ?? undefined} />
|
<AvatarImage src={currentUser?.avatarUrl ?? undefined} />
|
||||||
|
@ -0,0 +1,81 @@
|
|||||||
|
import { Button } from '@affine/admin/components/ui/button';
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
|
DialogFooter,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
} from '@affine/admin/components/ui/dialog';
|
||||||
|
|
||||||
|
import type { ModifiedValues } from './index';
|
||||||
|
|
||||||
|
export const ConfirmChanges = ({
|
||||||
|
open,
|
||||||
|
onClose,
|
||||||
|
onConfirm,
|
||||||
|
onOpenChange,
|
||||||
|
modifiedValues,
|
||||||
|
}: {
|
||||||
|
open: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
onConfirm: () => void;
|
||||||
|
onOpenChange: (open: boolean) => void;
|
||||||
|
modifiedValues: ModifiedValues[];
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||||
|
<DialogContent className="sm:w-[460px]">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle className="leading-7">
|
||||||
|
Save Runtime Configurations ?
|
||||||
|
</DialogTitle>
|
||||||
|
<DialogDescription className="leading-6">
|
||||||
|
Are you sure you want to save the following changes?
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
{modifiedValues.length > 0 ? (
|
||||||
|
<pre className="flex flex-col text-sm bg-zinc-100 gap-1 min-h-[64px] rounded-md p-[12px_16px_16px_12px] mt-2 overflow-hidden">
|
||||||
|
<p>{'{'}</p>
|
||||||
|
{modifiedValues.map(({ id, expiredValue, newValue }) => (
|
||||||
|
<p key={id}>
|
||||||
|
{' '} {id}:{' '}
|
||||||
|
<span
|
||||||
|
className="mr-2 line-through "
|
||||||
|
style={{
|
||||||
|
color: 'rgba(198, 34, 34, 1)',
|
||||||
|
backgroundColor: 'rgba(254, 213, 213, 1)',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{JSON.stringify(expiredValue)}
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
color: 'rgba(20, 147, 67, 1)',
|
||||||
|
backgroundColor: 'rgba(225, 250, 177, 1)',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{JSON.stringify(newValue)}
|
||||||
|
</span>
|
||||||
|
,
|
||||||
|
</p>
|
||||||
|
))}
|
||||||
|
<p>{'}'}</p>
|
||||||
|
</pre>
|
||||||
|
) : (
|
||||||
|
'There is no change.'
|
||||||
|
)}
|
||||||
|
<DialogFooter>
|
||||||
|
<div className="flex justify-end items-center w-full space-x-4">
|
||||||
|
<Button type="button" onClick={onClose} variant="outline">
|
||||||
|
<span>Cancel</span>
|
||||||
|
</Button>
|
||||||
|
<Button type="button" onClick={onConfirm}>
|
||||||
|
<span>Save</span>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
};
|
193
packages/frontend/admin/src/modules/settings/index.tsx
Normal file
193
packages/frontend/admin/src/modules/settings/index.tsx
Normal file
@ -0,0 +1,193 @@
|
|||||||
|
import { Button } from '@affine/admin/components/ui/button';
|
||||||
|
import { ScrollArea } from '@affine/admin/components/ui/scroll-area';
|
||||||
|
import { Separator } from '@affine/admin/components/ui/separator';
|
||||||
|
import type { RuntimeConfigType } from '@affine/graphql';
|
||||||
|
import { CheckIcon } from 'lucide-react';
|
||||||
|
import type { Dispatch, SetStateAction } from 'react';
|
||||||
|
import { useCallback, useMemo, useState } from 'react';
|
||||||
|
|
||||||
|
import { Layout } from '../layout';
|
||||||
|
import { useNav } from '../nav/context';
|
||||||
|
import { ConfirmChanges } from './confirm-changes';
|
||||||
|
import { RuntimeSettingRow } from './runtime-setting-row';
|
||||||
|
import { useGetServerRuntimeConfig } from './use-get-server-runtime-config';
|
||||||
|
import { useUpdateServerRuntimeConfigs } from './use-update-server-runtime-config';
|
||||||
|
import {
|
||||||
|
formatValue,
|
||||||
|
formatValueForInput,
|
||||||
|
isEqual,
|
||||||
|
renderInput,
|
||||||
|
} from './utils';
|
||||||
|
|
||||||
|
export type ModifiedValues = {
|
||||||
|
id: string;
|
||||||
|
expiredValue: any;
|
||||||
|
newValue: any;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function Settings() {
|
||||||
|
return <Layout content={<SettingsPage />} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function SettingsPage() {
|
||||||
|
const { trigger } = useUpdateServerRuntimeConfigs();
|
||||||
|
const { serverRuntimeConfig } = useGetServerRuntimeConfig();
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
const [configValues, setConfigValues] = useState(
|
||||||
|
serverRuntimeConfig.reduce(
|
||||||
|
(acc, config) => {
|
||||||
|
acc[config.id] = config.value;
|
||||||
|
return acc;
|
||||||
|
},
|
||||||
|
{} as Record<string, any>
|
||||||
|
)
|
||||||
|
);
|
||||||
|
const modifiedValues: ModifiedValues[] = useMemo(() => {
|
||||||
|
return serverRuntimeConfig
|
||||||
|
.filter(config => !isEqual(config.value, configValues[config.id]))
|
||||||
|
.map(config => ({
|
||||||
|
id: config.id,
|
||||||
|
key: config.key,
|
||||||
|
expiredValue: config.value,
|
||||||
|
newValue: configValues[config.id],
|
||||||
|
}));
|
||||||
|
}, [configValues, serverRuntimeConfig]);
|
||||||
|
const handleSave = useCallback(() => {
|
||||||
|
// post value example: { "key1": "newValue1","key2": "newValue2"}
|
||||||
|
const updates: Record<string, any> = {};
|
||||||
|
|
||||||
|
modifiedValues.forEach(item => {
|
||||||
|
if (item.id && item.newValue !== undefined) {
|
||||||
|
updates[item.id] = item.newValue;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
trigger({ updates });
|
||||||
|
}, [modifiedValues, trigger]);
|
||||||
|
|
||||||
|
const disableSave = modifiedValues.length === 0;
|
||||||
|
const onOpen = useCallback(() => setOpen(true), [setOpen]);
|
||||||
|
const onClose = useCallback(() => setOpen(false), [setOpen]);
|
||||||
|
const onConfirm = useCallback(() => {
|
||||||
|
if (disableSave) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
handleSave();
|
||||||
|
onClose();
|
||||||
|
}, [disableSave, handleSave, onClose]);
|
||||||
|
return (
|
||||||
|
<div className=" h-screen flex-1 flex-col flex">
|
||||||
|
<div className="flex items-center justify-between px-6 py-3 max-md:ml-9">
|
||||||
|
<div className="text-base font-medium">Settings</div>
|
||||||
|
<Button
|
||||||
|
type="submit"
|
||||||
|
size="icon"
|
||||||
|
className="w-7 h-7"
|
||||||
|
variant="ghost"
|
||||||
|
onClick={onOpen}
|
||||||
|
disabled={disableSave}
|
||||||
|
>
|
||||||
|
<CheckIcon size={20} />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<Separator />
|
||||||
|
<AdminPanel
|
||||||
|
configValues={configValues}
|
||||||
|
setConfigValues={setConfigValues}
|
||||||
|
/>
|
||||||
|
<ConfirmChanges
|
||||||
|
modifiedValues={modifiedValues}
|
||||||
|
open={open}
|
||||||
|
onOpenChange={setOpen}
|
||||||
|
onClose={onClose}
|
||||||
|
onConfirm={onConfirm}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const AdminPanel = ({
|
||||||
|
setConfigValues,
|
||||||
|
configValues,
|
||||||
|
}: {
|
||||||
|
setConfigValues: Dispatch<SetStateAction<Record<string, any>>>;
|
||||||
|
configValues: Record<string, any>;
|
||||||
|
}) => {
|
||||||
|
const { configGroup } = useGetServerRuntimeConfig();
|
||||||
|
|
||||||
|
const { currentModule } = useNav();
|
||||||
|
|
||||||
|
const handleInputChange = useCallback(
|
||||||
|
(key: string, value: any, type: RuntimeConfigType) => {
|
||||||
|
const newValue = formatValueForInput(value, type);
|
||||||
|
setConfigValues(prevValues => ({
|
||||||
|
...prevValues,
|
||||||
|
[key]: newValue,
|
||||||
|
}));
|
||||||
|
},
|
||||||
|
[setConfigValues]
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ScrollArea>
|
||||||
|
<div className="flex flex-col h-full gap-3 py-5 px-6 w-full">
|
||||||
|
{configGroup
|
||||||
|
.filter(group => group.moduleName === currentModule)
|
||||||
|
.map(group => {
|
||||||
|
const { moduleName, configs } = group;
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="flex flex-col gap-5"
|
||||||
|
id={moduleName}
|
||||||
|
key={moduleName}
|
||||||
|
>
|
||||||
|
<div className="text-xl font-semibold">{moduleName}</div>
|
||||||
|
{configs?.map((config, index) => {
|
||||||
|
const { id, type, description, updatedAt } = config;
|
||||||
|
const isValueEqual = isEqual(config.value, configValues[id]);
|
||||||
|
const formatServerValue = formatValue(config.value);
|
||||||
|
const formatCurrentValue = formatValue(configValues[id]);
|
||||||
|
return (
|
||||||
|
<div key={id} className="flex flex-col gap-10">
|
||||||
|
{index !== 0 && <Separator />}
|
||||||
|
<RuntimeSettingRow
|
||||||
|
key={id}
|
||||||
|
id={id}
|
||||||
|
description={description}
|
||||||
|
lastUpdatedTime={updatedAt}
|
||||||
|
operation={renderInput(type, configValues[id], value =>
|
||||||
|
handleInputChange(id, value, type)
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div style={{ opacity: isValueEqual ? 0 : 1 }}>
|
||||||
|
<span
|
||||||
|
className="line-through"
|
||||||
|
style={{
|
||||||
|
color: 'rgba(198, 34, 34, 1)',
|
||||||
|
backgroundColor: 'rgba(254, 213, 213, 1)',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{formatServerValue}
|
||||||
|
</span>{' '}
|
||||||
|
=>{' '}
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
color: 'rgba(20, 147, 67, 1)',
|
||||||
|
backgroundColor: 'rgba(225, 250, 177, 1)',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{formatCurrentValue}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</RuntimeSettingRow>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</ScrollArea>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export { Settings as Component };
|
@ -0,0 +1,39 @@
|
|||||||
|
import { type ReactNode } from 'react';
|
||||||
|
|
||||||
|
export const RuntimeSettingRow = ({
|
||||||
|
id,
|
||||||
|
description,
|
||||||
|
lastUpdatedTime,
|
||||||
|
operation,
|
||||||
|
children,
|
||||||
|
}: {
|
||||||
|
id: string;
|
||||||
|
description: string;
|
||||||
|
lastUpdatedTime: string;
|
||||||
|
operation: ReactNode;
|
||||||
|
children: ReactNode;
|
||||||
|
}) => {
|
||||||
|
const formatTime = new Date(lastUpdatedTime).toLocaleString();
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="flex justify-between flex-grow overflow-y-auto space-y-[10px] gap-5"
|
||||||
|
id={id}
|
||||||
|
>
|
||||||
|
<div className="flex flex-col gap-1">
|
||||||
|
<div className="text-base font-bold">{description}</div>
|
||||||
|
<div className="">
|
||||||
|
<code className="text-xs bg-zinc-100 text-gray-500 px-[4px] py-[2px] rounded">
|
||||||
|
{id}
|
||||||
|
</code>
|
||||||
|
</div>
|
||||||
|
<div className="text-xs text-gray-500">
|
||||||
|
last updated at: {formatTime}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col items-end gap-2 mr-1">
|
||||||
|
{operation}
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,57 @@
|
|||||||
|
import { useQuery } from '@affine/core/hooks/use-query';
|
||||||
|
import { getServerRuntimeConfigQuery } from '@affine/graphql';
|
||||||
|
import { useMemo } from 'react';
|
||||||
|
|
||||||
|
export const useGetServerRuntimeConfig = () => {
|
||||||
|
const { data } = useQuery({
|
||||||
|
query: getServerRuntimeConfigQuery,
|
||||||
|
});
|
||||||
|
|
||||||
|
const serverRuntimeConfig = useMemo(
|
||||||
|
() =>
|
||||||
|
data?.serverRuntimeConfig.sort((a, b) => a.id.localeCompare(b.id)) ?? [],
|
||||||
|
[data]
|
||||||
|
);
|
||||||
|
|
||||||
|
// collect all the modules and config keys in each module
|
||||||
|
const moduleList = useMemo(() => {
|
||||||
|
const moduleMap: { [key: string]: string[] } = {};
|
||||||
|
|
||||||
|
serverRuntimeConfig.forEach(config => {
|
||||||
|
if (!moduleMap[config.module]) {
|
||||||
|
moduleMap[config.module] = [];
|
||||||
|
}
|
||||||
|
moduleMap[config.module].push(config.key);
|
||||||
|
});
|
||||||
|
|
||||||
|
return Object.keys(moduleMap)
|
||||||
|
.sort((a, b) => a.localeCompare(b))
|
||||||
|
.map(moduleName => ({
|
||||||
|
moduleName,
|
||||||
|
keys: moduleMap[moduleName].sort((a, b) => a.localeCompare(b)),
|
||||||
|
}));
|
||||||
|
}, [serverRuntimeConfig]);
|
||||||
|
|
||||||
|
// group config by module name
|
||||||
|
const configGroup = useMemo(() => {
|
||||||
|
const configMap = new Map<string, typeof serverRuntimeConfig>();
|
||||||
|
|
||||||
|
serverRuntimeConfig.forEach(config => {
|
||||||
|
if (!configMap.has(config.module)) {
|
||||||
|
configMap.set(config.module, []);
|
||||||
|
}
|
||||||
|
configMap.get(config.module)?.push(config);
|
||||||
|
});
|
||||||
|
|
||||||
|
return Array.from(configMap.entries()).map(([moduleName, configs]) => ({
|
||||||
|
moduleName,
|
||||||
|
configs,
|
||||||
|
}));
|
||||||
|
}, [serverRuntimeConfig]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
serverRuntimeConfig,
|
||||||
|
moduleList,
|
||||||
|
configGroup,
|
||||||
|
};
|
||||||
|
};
|
@ -0,0 +1,41 @@
|
|||||||
|
import { notify } from '@affine/component';
|
||||||
|
import { useAsyncCallback } from '@affine/core/hooks/affine-async-hooks';
|
||||||
|
import {
|
||||||
|
useMutateQueryResource,
|
||||||
|
useMutation,
|
||||||
|
} from '@affine/core/hooks/use-mutation';
|
||||||
|
import {
|
||||||
|
getServerRuntimeConfigQuery,
|
||||||
|
updateServerRuntimeConfigsMutation,
|
||||||
|
} from '@affine/graphql';
|
||||||
|
|
||||||
|
export const useUpdateServerRuntimeConfigs = () => {
|
||||||
|
const { trigger, isMutating } = useMutation({
|
||||||
|
mutation: updateServerRuntimeConfigsMutation,
|
||||||
|
});
|
||||||
|
const revalidate = useMutateQueryResource();
|
||||||
|
|
||||||
|
return {
|
||||||
|
trigger: useAsyncCallback(
|
||||||
|
async (values: any) => {
|
||||||
|
try {
|
||||||
|
await trigger(values);
|
||||||
|
await revalidate(getServerRuntimeConfigQuery);
|
||||||
|
notify.success({
|
||||||
|
title: 'Saved successfully',
|
||||||
|
message: 'Runtime configurations have been saved successfully.',
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
notify.error({
|
||||||
|
title: 'Failed to save',
|
||||||
|
message:
|
||||||
|
'Failed to save runtime configurations, please try again later.',
|
||||||
|
});
|
||||||
|
console.error(e);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[revalidate, trigger]
|
||||||
|
),
|
||||||
|
isMutating,
|
||||||
|
};
|
||||||
|
};
|
73
packages/frontend/admin/src/modules/settings/utils.tsx
Normal file
73
packages/frontend/admin/src/modules/settings/utils.tsx
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
import { Input } from '@affine/admin/components/ui/input';
|
||||||
|
import { Switch } from '@affine/admin/components/ui/switch';
|
||||||
|
import type { RuntimeConfigType } from '@affine/graphql';
|
||||||
|
|
||||||
|
export const renderInput = (
|
||||||
|
type: RuntimeConfigType,
|
||||||
|
value: any,
|
||||||
|
onChange: (value?: any) => void
|
||||||
|
) => {
|
||||||
|
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
onChange(e.target.value);
|
||||||
|
};
|
||||||
|
const handleSwitchChange = (checked: boolean) => {
|
||||||
|
onChange(checked);
|
||||||
|
};
|
||||||
|
switch (type) {
|
||||||
|
case 'Boolean':
|
||||||
|
return <Switch checked={value} onCheckedChange={handleSwitchChange} />;
|
||||||
|
case 'String':
|
||||||
|
return (
|
||||||
|
<Input
|
||||||
|
type="text"
|
||||||
|
minLength={1}
|
||||||
|
value={value}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
case 'Number':
|
||||||
|
return (
|
||||||
|
<div style={{ width: '100%' }}>
|
||||||
|
<Input type="number" value={value} onChange={handleInputChange} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
// TODO(@JimmFly): add more types
|
||||||
|
default:
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const isEqual = (a: any, b: any) => {
|
||||||
|
if (typeof a !== typeof b) return false;
|
||||||
|
if (typeof a === 'object') return JSON.stringify(a) === JSON.stringify(b);
|
||||||
|
return a === b;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const formatValue = (value: any) => {
|
||||||
|
if (typeof value === 'object') return JSON.stringify(value);
|
||||||
|
return value.toString();
|
||||||
|
};
|
||||||
|
|
||||||
|
export const formatValueForInput = (value: any, type: RuntimeConfigType) => {
|
||||||
|
let newValue = null;
|
||||||
|
switch (type) {
|
||||||
|
case 'Boolean':
|
||||||
|
newValue = !!value;
|
||||||
|
break;
|
||||||
|
case 'String':
|
||||||
|
newValue = value;
|
||||||
|
break;
|
||||||
|
case 'Number':
|
||||||
|
newValue = Number(value);
|
||||||
|
break;
|
||||||
|
case 'Array':
|
||||||
|
newValue = value.split(',');
|
||||||
|
break;
|
||||||
|
case 'Object':
|
||||||
|
newValue = JSON.parse(value);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
return newValue;
|
||||||
|
};
|
Loading…
Reference in New Issue
Block a user