feat: M0-12 secrets admin page
Some checks failed
Deploy Admin Frontend / build-and-deploy (push) Failing after 6s
Some checks failed
Deploy Admin Frontend / build-and-deploy (push) Failing after 6s
This commit is contained in:
parent
a0be7bf832
commit
11297127d6
@ -14,6 +14,7 @@ const KnowledgeBasesPage = lazy(() => import('./pages/KnowledgeBases'))
|
|||||||
const BillingPage = lazy(() => import('./pages/Billing'))
|
const BillingPage = lazy(() => import('./pages/Billing'))
|
||||||
const GiteaEmbed = lazy(() => import('./pages/GiteaEmbed'))
|
const GiteaEmbed = lazy(() => import('./pages/GiteaEmbed'))
|
||||||
const ConfigPage = lazy(() => import("./pages/Config"))
|
const ConfigPage = lazy(() => import("./pages/Config"))
|
||||||
|
const SecretsPage = lazy(() => import('./pages/Secrets'))
|
||||||
const MembershipPage = lazy(() => import("./pages/Membership"))
|
const MembershipPage = lazy(() => import("./pages/Membership"))
|
||||||
const FilesAdminPage = lazy(() => import("./pages/FilesAdmin"))
|
const FilesAdminPage = lazy(() => import("./pages/FilesAdmin"))
|
||||||
const AiGatewayPage = lazy(() => import("./pages/AiGateway"))
|
const AiGatewayPage = lazy(() => import("./pages/AiGateway"))
|
||||||
@ -72,6 +73,10 @@ function App() {
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<Route path="users/members" element={<Placeholder title="普通用户" />} />
|
<Route path="users/members" element={<Placeholder title="普通用户" />} />
|
||||||
|
<Route
|
||||||
|
path="secrets"
|
||||||
|
element={<PermissionGuard requiredRole="SUPER_ADMIN"><Suspense fallback={<PageLoading />}><SecretsPage /></Suspense></PermissionGuard>}
|
||||||
|
/>
|
||||||
<Route
|
<Route
|
||||||
path="membership"
|
path="membership"
|
||||||
element={
|
element={
|
||||||
|
|||||||
@ -30,6 +30,7 @@ export const adminMenuItems: AdminMenuItem[] = [
|
|||||||
{ path: '/billing', name: 'API 用量', icon: <DollarOutlined />, requiredRole: 'SUPER_ADMIN' },
|
{ path: '/billing', name: 'API 用量', icon: <DollarOutlined />, requiredRole: 'SUPER_ADMIN' },
|
||||||
{ path: '/git', name: '代码仓库', icon: <CodeOutlined /> },
|
{ path: '/git', name: '代码仓库', icon: <CodeOutlined /> },
|
||||||
{ path: '/ops', name: '系统运维', icon: <CloudServerOutlined />, requiredRole: 'SUPER_ADMIN', children: [
|
{ path: '/ops', name: '系统运维', icon: <CloudServerOutlined />, requiredRole: 'SUPER_ADMIN', children: [
|
||||||
|
{ path: '/secrets', name: '密钥管理' },
|
||||||
{ path: '/metrics', name: '接口监控' },
|
{ path: '/metrics', name: '接口监控' },
|
||||||
{ path: '/ai-gateway', name: 'AI Gateway' },
|
{ path: '/ai-gateway', name: 'AI Gateway' },
|
||||||
{ path: '/servers', name: '服务器' },
|
{ path: '/servers', name: '服务器' },
|
||||||
|
|||||||
@ -25,6 +25,7 @@ const breadcrumbMap: Record<string, string> = {
|
|||||||
'/git': '代码仓库',
|
'/git': '代码仓库',
|
||||||
'/servers': '服务器运维',
|
'/servers': '服务器运维',
|
||||||
'/config': '配置管理',
|
'/config': '配置管理',
|
||||||
|
'/secrets': '密钥管理',
|
||||||
'/metrics': '接口监控',
|
'/metrics': '接口监控',
|
||||||
'/ai-gateway': 'AI Gateway',
|
'/ai-gateway': 'AI Gateway',
|
||||||
'/safety': '内容安全',
|
'/safety': '内容安全',
|
||||||
|
|||||||
65
src/pages/Secrets.tsx
Normal file
65
src/pages/Secrets.tsx
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
import { useState } from 'react'
|
||||||
|
import { useQuery, useQueryClient } from '@tanstack/react-query'
|
||||||
|
import { Table, Button, Typography, App, Modal, Input, Select, Tag, Tabs, DatePicker } from 'antd'
|
||||||
|
import { ReloadOutlined, PlusOutlined, DeleteOutlined, KeyOutlined } from '@ant-design/icons'
|
||||||
|
import { api } from '@/services/http-client'
|
||||||
|
import dayjs from 'dayjs'
|
||||||
|
|
||||||
|
const { Title, Text } = Typography
|
||||||
|
|
||||||
|
function SecretsPage() {
|
||||||
|
const { modal, message } = App.useApp()
|
||||||
|
const qc = useQueryClient()
|
||||||
|
const [addOpen, setAddOpen] = useState(false)
|
||||||
|
const [form, setForm] = useState({ name: '', provider: 'deepseek', value: '', expiresAt: '' })
|
||||||
|
|
||||||
|
const { data: secrets } = useQuery({ queryKey: ['secrets'], queryFn: (): Promise<any> => api.get('/admin-api/secrets') })
|
||||||
|
const { data: logs } = useQuery({ queryKey: ['secrets', 'logs'], queryFn: (): Promise<any> => api.get('/admin-api/secrets/logs') })
|
||||||
|
|
||||||
|
const addSecret = async () => {
|
||||||
|
await api.post('/admin-api/secrets', form)
|
||||||
|
message.success('已添加')
|
||||||
|
setAddOpen(false); qc.invalidateQueries({ queryKey: ['secrets'] })
|
||||||
|
}
|
||||||
|
|
||||||
|
const deleteSecret = (id: string) => modal.confirm({
|
||||||
|
title: '删除密钥', okType: 'danger',
|
||||||
|
onOk: async () => { await api.delete(`/admin-api/secrets/${id}`); qc.invalidateQueries({ queryKey: ['secrets'] }) },
|
||||||
|
})
|
||||||
|
|
||||||
|
const secCols = [
|
||||||
|
{ title: '名称', dataIndex: 'name', width: 150 },
|
||||||
|
{ title: '服务商', dataIndex: 'provider', width: 100, render: (v: string) => <Tag color={v==='deepseek'?'blue':v==='siliconflow'?'green':v==='minimax'?'purple':'default'}>{v}</Tag> },
|
||||||
|
{ title: '末四位', dataIndex: 'maskLast4', width: 80, render: (v: string) => <Text code>****{v}</Text> },
|
||||||
|
{ title: '状态', dataIndex: 'status', width: 70, render: (v: string) => <Tag color={v==='active'?'green':'red'}>{v}</Tag> },
|
||||||
|
{ title: '到期', dataIndex: 'expiresAt', width: 100, render: (d: string) => d ? dayjs(d).format('MM-DD') : <Text type="secondary">永久</Text> },
|
||||||
|
{ title: '操作', width: 80, render: (_:any, r:any) => <Button type="link" size="small" danger icon={<DeleteOutlined />} onClick={() => deleteSecret(r.id)} /> },
|
||||||
|
]
|
||||||
|
|
||||||
|
const logCols = [
|
||||||
|
{ title: '时间', dataIndex: 'createdAt', width: 150, render: (d: string) => dayjs(d).format('MM-DD HH:mm:ss') },
|
||||||
|
{ title: '密钥', dataIndex: 'secretName', width: 120 },
|
||||||
|
{ title: '访问者', dataIndex: 'accessedBy', width: 120 },
|
||||||
|
]
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: 16 }}>
|
||||||
|
<Title level={5} style={{ margin: 0 }}><KeyOutlined /> 密钥管理</Title>
|
||||||
|
<Button icon={<PlusOutlined />} type="primary" onClick={() => setAddOpen(true)}>新增密钥</Button>
|
||||||
|
</div>
|
||||||
|
<Tabs items={[
|
||||||
|
{ key: 'keys', label: '密钥列表', children: <Table dataSource={secrets || []} columns={secCols} rowKey="id" pagination={false} size="small" /> },
|
||||||
|
{ key: 'logs', label: '访问日志', children: <Table dataSource={logs || []} columns={logCols} rowKey="id" pagination={{ pageSize: 20 }} size="small" /> },
|
||||||
|
]} />
|
||||||
|
<Modal title="新增密钥" open={addOpen} onOk={addSecret} onCancel={() => setAddOpen(false)} okText="添加">
|
||||||
|
<Input placeholder="名称" value={form.name} onChange={e => setForm({...form, name: e.target.value})} style={{ marginBottom: 12 }} />
|
||||||
|
<Select value={form.provider} onChange={v => setForm({...form, provider: v})} style={{ width: '100%', marginBottom: 12 }} options={[{label:'DeepSeek',value:'deepseek'},{label:'硅基流动',value:'siliconflow'},{label:'MiniMax',value:'minimax'},{label:'百度OCR',value:'baidu'},{label:'腾讯COS',value:'cos'}]} />
|
||||||
|
<Input.Password placeholder="Key 值" value={form.value} onChange={e => setForm({...form, value: e.target.value})} style={{ marginBottom: 12 }} />
|
||||||
|
<DatePicker placeholder="到期日期" onChange={d => setForm({...form, expiresAt: d?.toISOString() || ''})} style={{ width: '100%' }} />
|
||||||
|
</Modal>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default SecretsPage
|
||||||
Loading…
x
Reference in New Issue
Block a user