feat(admin): add config page to admin (#7619)
This commit is contained in:
@@ -55,6 +55,10 @@ export const router = _createBrowserRouter(
|
|||||||
path: '/admin/setup',
|
path: '/admin/setup',
|
||||||
lazy: () => import('./modules/setup'),
|
lazy: () => import('./modules/setup'),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: '/admin/config',
|
||||||
|
lazy: () => import('./modules/config'),
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|||||||
88
packages/frontend/admin/src/modules/config/about.tsx
Normal file
88
packages/frontend/admin/src/modules/config/about.tsx
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
import { buttonVariants } from '@affine/admin/components/ui/button';
|
||||||
|
import { Separator } from '@affine/admin/components/ui/separator';
|
||||||
|
import { cn } from '@affine/admin/utils';
|
||||||
|
import {
|
||||||
|
AlbumIcon,
|
||||||
|
ChevronRightIcon,
|
||||||
|
GithubIcon,
|
||||||
|
MailWarningIcon,
|
||||||
|
UploadCloudIcon,
|
||||||
|
} from 'lucide-react';
|
||||||
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
const appChannelSchema = z.enum(['stable', 'canary', 'beta', 'internal']);
|
||||||
|
|
||||||
|
type Channel = z.infer<typeof appChannelSchema>;
|
||||||
|
|
||||||
|
const appNames = {
|
||||||
|
stable: 'AFFiNE',
|
||||||
|
canary: 'AFFiNE Canary',
|
||||||
|
beta: 'AFFiNE Beta',
|
||||||
|
internal: 'AFFiNE Internal',
|
||||||
|
} satisfies Record<Channel, string>;
|
||||||
|
|
||||||
|
const links = [
|
||||||
|
{
|
||||||
|
href: runtimeConfig.githubUrl,
|
||||||
|
icon: <GithubIcon size={20} />,
|
||||||
|
label: 'Star AFFiNE on GitHub',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
href: runtimeConfig.githubUrl,
|
||||||
|
icon: <MailWarningIcon size={20} />,
|
||||||
|
label: 'Report an Issue',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
href: 'https://docs.affine.pro/docs/self-host-affine',
|
||||||
|
icon: <AlbumIcon size={20} />,
|
||||||
|
label: 'Self-host Document',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
href: 'https://affine.pro/pricing',
|
||||||
|
icon: <UploadCloudIcon size={20} />,
|
||||||
|
label: 'Upgrade to Pro',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export function AboutAFFiNE() {
|
||||||
|
const { appBuildType, appVersion, editorVersion } = runtimeConfig;
|
||||||
|
const appName = appNames[appBuildType];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col h-full gap-3 py-5 px-6 w-full">
|
||||||
|
<div className="flex items-center">
|
||||||
|
<span className="text-xl font-semibold">About AFFiNE</span>
|
||||||
|
</div>
|
||||||
|
<div className="overflow-y-auto space-y-[10px]">
|
||||||
|
<div className="flex flex-col rounded-md border">
|
||||||
|
{links.map(({ href, icon, label }, index) => (
|
||||||
|
<div key={label + index}>
|
||||||
|
<a
|
||||||
|
className={cn(
|
||||||
|
buttonVariants({ variant: 'ghost' }),
|
||||||
|
'justify-between cursor-pointer w-full'
|
||||||
|
)}
|
||||||
|
href={href}
|
||||||
|
target="_blank"
|
||||||
|
rel="noreferrer"
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
{icon}
|
||||||
|
<span>{label}</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<ChevronRightIcon size={20} />
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
{index < links.length - 1 && <Separator />}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-3 text-sm font-normal text-gray-500">
|
||||||
|
<div>{`App Version: ${appName} ${appVersion}`}</div>
|
||||||
|
<div>{`Editor Version: ${editorVersion}`}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
221
packages/frontend/admin/src/modules/config/index.tsx
Normal file
221
packages/frontend/admin/src/modules/config/index.tsx
Normal file
@@ -0,0 +1,221 @@
|
|||||||
|
import {
|
||||||
|
Card,
|
||||||
|
CardContent,
|
||||||
|
CardHeader,
|
||||||
|
CardTitle,
|
||||||
|
} from '@affine/admin/components/ui/card';
|
||||||
|
import { ScrollArea } from '@affine/admin/components/ui/scroll-area';
|
||||||
|
import { Separator } from '@affine/admin/components/ui/separator';
|
||||||
|
import { useQuery } from '@affine/core/hooks/use-query';
|
||||||
|
import { getServerServiceConfigsQuery } from '@affine/graphql';
|
||||||
|
|
||||||
|
import { Layout } from '../layout';
|
||||||
|
import { AboutAFFiNE } from './about';
|
||||||
|
|
||||||
|
type ServerConfig = {
|
||||||
|
externalUrl: string;
|
||||||
|
https: boolean;
|
||||||
|
host: string;
|
||||||
|
port: number;
|
||||||
|
path: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type MailerConfig = {
|
||||||
|
host: string;
|
||||||
|
port: number;
|
||||||
|
sender: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type DatabaseConfig = {
|
||||||
|
host: string;
|
||||||
|
port: number;
|
||||||
|
user: string;
|
||||||
|
database: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type ServerServiceConfig = {
|
||||||
|
name: string;
|
||||||
|
config: ServerConfig | MailerConfig | DatabaseConfig;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function Config() {
|
||||||
|
return <Layout content={<ConfigPage />} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ConfigPage() {
|
||||||
|
return (
|
||||||
|
<div className=" h-screen flex-1 space-y-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">Config</div>
|
||||||
|
</div>
|
||||||
|
<Separator />
|
||||||
|
<ScrollArea>
|
||||||
|
<ServerServiceConfig />
|
||||||
|
<AboutAFFiNE />
|
||||||
|
</ScrollArea>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const ServerCard = ({ serverConfig }: { serverConfig?: ServerConfig }) => {
|
||||||
|
if (!serverConfig) return null;
|
||||||
|
return (
|
||||||
|
<Card className="px-5 py-4">
|
||||||
|
<CardHeader className="p-0">
|
||||||
|
<CardTitle className="text-base font-semibold mb-3">Server</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="grid gap-4 p-0">
|
||||||
|
<div className="space-y-5">
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<div className="text-sm font-medium">Domain</div>
|
||||||
|
<div className="text-sm text-zinc-500 font-normal">
|
||||||
|
{serverConfig.host}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<div className="text-sm font-medium">Port</div>
|
||||||
|
<div className="text-sm text-zinc-500 font-normal">
|
||||||
|
{serverConfig.port}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<div className="text-sm font-medium">HTTPS Prefix</div>
|
||||||
|
<div className="text-sm text-zinc-500 font-normal">
|
||||||
|
{serverConfig.https.toString()}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<div className="text-sm font-medium">External Url</div>
|
||||||
|
<div className="text-sm text-zinc-500 font-normal">
|
||||||
|
{serverConfig.externalUrl}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
const DatabaseCard = ({
|
||||||
|
databaseConfig,
|
||||||
|
}: {
|
||||||
|
databaseConfig?: DatabaseConfig;
|
||||||
|
}) => {
|
||||||
|
if (!databaseConfig) return null;
|
||||||
|
return (
|
||||||
|
<Card className="px-5 py-4">
|
||||||
|
<CardHeader className="p-0">
|
||||||
|
<CardTitle className="text-base font-semibold mb-3">Database</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="grid gap-4 p-0">
|
||||||
|
<div className="space-y-5">
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<div className="text-sm font-medium">Domain</div>
|
||||||
|
<div className="text-sm text-zinc-500 font-normal">
|
||||||
|
{databaseConfig.host}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<div className="text-sm font-medium">Port</div>
|
||||||
|
<div className="text-sm text-zinc-500 font-normal">
|
||||||
|
{databaseConfig.port}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<div className="text-sm font-medium">User</div>
|
||||||
|
<div className="text-sm text-zinc-500 font-normal">
|
||||||
|
{databaseConfig.user}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<div className="text-sm font-medium">Database</div>
|
||||||
|
<div className="text-sm text-zinc-500 font-normal">
|
||||||
|
{databaseConfig.database}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
const MailerCard = ({ mailerConfig }: { mailerConfig?: MailerConfig }) => {
|
||||||
|
if (!mailerConfig) return null;
|
||||||
|
return (
|
||||||
|
<Card className="px-5 py-4">
|
||||||
|
<CardHeader className="p-0">
|
||||||
|
<CardTitle className="text-base font-semibold mb-3">Email</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="grid gap-4 p-0">
|
||||||
|
<div className="space-y-5">
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<div className="text-sm font-medium">Provider Domain</div>
|
||||||
|
<div className="text-sm text-zinc-500 font-normal">
|
||||||
|
{mailerConfig.host}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<div className="text-sm font-medium">Port</div>
|
||||||
|
<div className="text-sm text-zinc-500 font-normal">
|
||||||
|
{mailerConfig.port}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<div className="text-sm font-medium">Sender</div>
|
||||||
|
<div className="text-sm text-zinc-500 font-normal">
|
||||||
|
{mailerConfig.sender}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export function ServerServiceConfig() {
|
||||||
|
const { data } = useQuery({
|
||||||
|
query: getServerServiceConfigsQuery,
|
||||||
|
});
|
||||||
|
const server = data.serverServiceConfigs.find(
|
||||||
|
(service: ServerServiceConfig) => service.name === 'server'
|
||||||
|
);
|
||||||
|
const mailer = data.serverServiceConfigs.find(
|
||||||
|
(service: ServerServiceConfig) => service.name === 'mailer'
|
||||||
|
);
|
||||||
|
const database = data.serverServiceConfigs.find(
|
||||||
|
(service: ServerServiceConfig) => service.name === 'database'
|
||||||
|
);
|
||||||
|
|
||||||
|
const serverConfig = server?.config as ServerConfig | undefined;
|
||||||
|
const mailerConfig = mailer?.config as MailerConfig | undefined;
|
||||||
|
const databaseConfig = database?.config as DatabaseConfig | undefined;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col py-5 px-6">
|
||||||
|
<div className="flex items-center mb-5">
|
||||||
|
<span className="text-2xl font-semibold">Server Config</span>
|
||||||
|
</div>
|
||||||
|
<div className=" items-start justify-center gap-6 rounded-lg grid grid-cols-2">
|
||||||
|
<div className="col-span-2 grid items-start gap-6 lg:col-span-1">
|
||||||
|
<ServerCard serverConfig={serverConfig} />
|
||||||
|
<MailerCard mailerConfig={mailerConfig} />
|
||||||
|
</div>
|
||||||
|
<div className="col-span-2 grid items-start gap-6 lg:col-span-1">
|
||||||
|
<DatabaseCard databaseConfig={databaseConfig} />
|
||||||
|
<div className="px-5 py-4 border rounded text-sm text-zinc-500 font-normal">
|
||||||
|
<span className="mr-1">
|
||||||
|
These settings are controlled by Docker environment variables.
|
||||||
|
Refer to the
|
||||||
|
</span>
|
||||||
|
<a
|
||||||
|
href="https://docs.affine.pro/docs/self-host-affine"
|
||||||
|
className="text-black underline"
|
||||||
|
>
|
||||||
|
Selfhost documentation.
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Config as Component };
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
query getServerServiceConfigs {
|
||||||
|
serverServiceConfigs {
|
||||||
|
name
|
||||||
|
config
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -515,6 +515,20 @@ query getServerRuntimeConfig {
|
|||||||
}`,
|
}`,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const getServerServiceConfigsQuery = {
|
||||||
|
id: 'getServerServiceConfigsQuery' as const,
|
||||||
|
operationName: 'getServerServiceConfigs',
|
||||||
|
definitionName: 'serverServiceConfigs',
|
||||||
|
containsFile: false,
|
||||||
|
query: `
|
||||||
|
query getServerServiceConfigs {
|
||||||
|
serverServiceConfigs {
|
||||||
|
name
|
||||||
|
config
|
||||||
|
}
|
||||||
|
}`,
|
||||||
|
};
|
||||||
|
|
||||||
export const getUserByEmailQuery = {
|
export const getUserByEmailQuery = {
|
||||||
id: 'getUserByEmailQuery' as const,
|
id: 'getUserByEmailQuery' as const,
|
||||||
operationName: 'getUserByEmail',
|
operationName: 'getUserByEmail',
|
||||||
|
|||||||
@@ -1702,6 +1702,19 @@ export type GetServerRuntimeConfigQuery = {
|
|||||||
}>;
|
}>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type GetServerServiceConfigsQueryVariables = Exact<{
|
||||||
|
[key: string]: never;
|
||||||
|
}>;
|
||||||
|
|
||||||
|
export type GetServerServiceConfigsQuery = {
|
||||||
|
__typename?: 'Query';
|
||||||
|
serverServiceConfigs: Array<{
|
||||||
|
__typename?: 'ServerServiceConfig';
|
||||||
|
name: string;
|
||||||
|
config: any;
|
||||||
|
}>;
|
||||||
|
};
|
||||||
|
|
||||||
export type GetUserByEmailQueryVariables = Exact<{
|
export type GetUserByEmailQueryVariables = Exact<{
|
||||||
email: Scalars['String']['input'];
|
email: Scalars['String']['input'];
|
||||||
}>;
|
}>;
|
||||||
@@ -2424,6 +2437,11 @@ export type Queries =
|
|||||||
variables: GetServerRuntimeConfigQueryVariables;
|
variables: GetServerRuntimeConfigQueryVariables;
|
||||||
response: GetServerRuntimeConfigQuery;
|
response: GetServerRuntimeConfigQuery;
|
||||||
}
|
}
|
||||||
|
| {
|
||||||
|
name: 'getServerServiceConfigsQuery';
|
||||||
|
variables: GetServerServiceConfigsQueryVariables;
|
||||||
|
response: GetServerServiceConfigsQuery;
|
||||||
|
}
|
||||||
| {
|
| {
|
||||||
name: 'getUserByEmailQuery';
|
name: 'getUserByEmailQuery';
|
||||||
variables: GetUserByEmailQueryVariables;
|
variables: GetUserByEmailQueryVariables;
|
||||||
|
|||||||
Reference in New Issue
Block a user