Skip to content
Clone
f0a1552
195 lines 6.9 KB
import { useCallback, useEffect, useState } from 'react'
import { adminSystem } from '@/shared/api'
import type { SystemConfig } from '@/shared/api'
import { ApiError } from '@/shared/api'
import { formatDateTime } from '@/shared/format'
import { Badge, Button, Card, ErrorBanner, Loading } from '@/shared/ui'
function Rows({ entries }: { entries: [string, string | number | boolean | null | undefined][] }) {
return (
<dl className="divide-y divide-slate-100 text-sm dark:divide-slate-800">
{entries.map(([label, value]) => (
<div key={label} className="flex items-center justify-between gap-3 py-1.5">
<dt className="text-slate-500 dark:text-slate-400">{label}</dt>
<dd className="break-all text-right text-slate-800 dark:text-slate-100">
{value === null || value === undefined || value === '' ? '-' : String(value)}
</dd>
</div>
))}
</dl>
)
}
function onOff(value: boolean | null | undefined): string {
if (value === null || value === undefined) {
return '-'
}
return value ? '开启' : '关闭'
}
export function SystemPage() {
const [config, setConfig] = useState<SystemConfig | null>(null)
const [error, setError] = useState<string | null>(null)
const [busy, setBusy] = useState(false)
const load = useCallback(async () => {
setBusy(true)
setError(null)
try {
setConfig(await adminSystem.config())
} catch (err) {
setError(err instanceof ApiError ? err.message : '配置快照加载失败')
} finally {
setBusy(false)
}
}, [])
useEffect(() => {
void load()
}, [load])
if (!config) {
return (
<div className="space-y-3">
<ErrorBanner message={error} />
<Loading />
</div>
)
}
return (
<div className="space-y-4">
<div className="flex items-center justify-between">
<div>
<h1 className="text-lg font-semibold text-slate-900 dark:text-slate-100">系统配置</h1>
<p className="text-xs text-slate-500">
adminhost v{config.version} · 快照于 {formatDateTime(config.generated_at)} · 不含任何密钥
</p>
</div>
<Button variant="secondary" size="sm" disabled={busy} onClick={() => void load()}>
刷新
</Button>
</div>
<ErrorBanner message={error} />
<div className="grid gap-3 lg:grid-cols-2">
<Card title="进程与监听">
<Rows
entries={[
...config.hosts.map(
(host) =>
[
host.name,
`${host.bind_addr ?? '-'}:${host.port ?? '-'} · ${host.enabled ? '启用' : '停用'}`,
] as [string, string],
),
['adminhost', `${config.admin.bind_addr}:${config.admin.port}`],
['管理端 Cookie', config.admin.session_cookie_name],
['管理端会话表', config.admin.session_table_name],
['管理端跨域来源', config.admin.cors_allow_origins.join(', ') || '未配置(同源部署)'],
['跨域携带凭据', onOff(config.admin.cors_allow_credentials)],
]}
/>
</Card>
<Card title="功能开关">
<Rows
entries={[
['搜索', onOff(config.features.search_enabled)],
['导入', onOff(config.features.import_enabled)],
['邮件', onOff(config.features.email_enabled)],
['邮件队列', onOff(config.features.email_queue_enabled)],
]}
/>
</Card>
<Card title="数据库">
<Rows
entries={[
['端点', config.database.endpoint],
['主机', `${config.database.host ?? '-'}:${config.database.port ?? '-'}`],
['库名', config.database.database],
['SSL 模式', config.database.ssl_mode],
['连接池', `${config.database.min_connections ?? '-'} ~ ${config.database.max_connections ?? '-'}`],
['只读副本', config.database.replica_count],
]}
/>
</Card>
<Card title="Redis">
<Rows
entries={[
['端点', config.redis.endpoint],
['模式', config.redis.mode],
['库', config.redis.database],
['连接池', config.redis.pool_size],
]}
/>
</Card>
<Card title="会话">
<Rows
entries={[
['模式', config.session.mode],
['Cookie', config.session.cookie_name],
['Cookie 域', config.session.cookie_domain],
['Secure', onOff(config.session.cookie_secure)],
['SameSite', config.session.cookie_same_site],
['生命周期(秒)', config.session.lifespan_secs],
['会话表', config.session.table_name],
]}
/>
</Card>
<Card title="存储与邮件">
<Rows
entries={[
['Bucket', config.storage.bucket],
['Region', config.storage.region],
['Path style', onOff(config.storage.path_style)],
['SMTP', `${config.email.smtp_host}:${config.email.smtp_port}`],
['发件人', config.email.from_address],
['发件人名称', config.email.from_name],
]}
/>
</Card>
<Card title="检索与导入">
<Rows
entries={[
['索引前缀', config.search.index_prefix],
['单文件上限(字节)', config.search.max_file_bytes],
['批大小', config.search.batch_size],
['轮询间隔(秒)', config.search.poll_interval_secs],
['导入并发', config.import.max_concurrent_jobs],
['仓库体积上限(字节)', config.import.max_repo_bytes],
['最大尝试次数', config.import.max_attempts],
['允许主机', config.import.allowed_hosts.join(', ') || '未配置'],
]}
/>
{config.import.allow_insecure_tls && (
<p className="mt-2">
<Badge tone="warning">导入允许不安全 TLS</Badge>
</p>
)}
</Card>
<Card title="Git">
<Rows
entries={[
['仓库根目录', config.git.repo_root],
['对象目录', config.git.objects_root],
['单对象上限(字节)', config.git.max_blob_size],
['同步 topic', config.git.sync_topic],
['同步并发', config.git.sync_concurrency],
['基础地址', config.git.base_url],
['GraphQL 复杂度上限', config.git.graphql_complexity_limit],
['GraphQL introspection', onOff(config.git.graphql_introspection)],
]}
/>
</Card>
</div>
</div>
)
}