feat: M0-03 config management 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
13834af55d
commit
a5483cf37f
@ -100,6 +100,10 @@ function App() {
|
|||||||
<Suspense fallback={<PageLoading />}><GiteaEmbed /></Suspense>
|
<Suspense fallback={<PageLoading />}><GiteaEmbed /></Suspense>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
<Route
|
||||||
|
path="config"
|
||||||
|
element={<PermissionGuard requiredRole="SUPER_ADMIN"><Suspense fallback={<PageLoading />}><ConfigPage /></Suspense></PermissionGuard>}
|
||||||
|
/>
|
||||||
<Route
|
<Route
|
||||||
path="events"
|
path="events"
|
||||||
element={<PermissionGuard requiredRole="SUPER_ADMIN"><Suspense fallback={<PageLoading />}><EventsPage /></Suspense></PermissionGuard>}
|
element={<PermissionGuard requiredRole="SUPER_ADMIN"><Suspense fallback={<PageLoading />}><EventsPage /></Suspense></PermissionGuard>}
|
||||||
|
|||||||
@ -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: '/config', name: '配置管理' },
|
||||||
{ path: '/servers', name: '服务器' },
|
{ path: '/servers', name: '服务器' },
|
||||||
{ path: '/events', name: '事件队列' },
|
{ path: '/events', name: '事件队列' },
|
||||||
]},
|
]},
|
||||||
|
|||||||
@ -24,6 +24,7 @@ const breadcrumbMap: Record<string, string> = {
|
|||||||
'/billing': 'API 用量',
|
'/billing': 'API 用量',
|
||||||
'/git': '代码仓库',
|
'/git': '代码仓库',
|
||||||
'/servers': '服务器运维',
|
'/servers': '服务器运维',
|
||||||
|
'/config': '配置管理',
|
||||||
'/events': '事件队列',
|
'/events': '事件队列',
|
||||||
'/audit': '审计日志',
|
'/audit': '审计日志',
|
||||||
}
|
}
|
||||||
|
|||||||
89
src/pages/Config.tsx
Normal file
89
src/pages/Config.tsx
Normal file
@ -0,0 +1,89 @@
|
|||||||
|
import { useState } from 'react'
|
||||||
|
import { useQuery, useQueryClient } from '@tanstack/react-query'
|
||||||
|
import { Table, Switch, Button, Typography, App, Modal, Input, Tabs, Space } from 'antd'
|
||||||
|
import { ReloadOutlined, PlusOutlined, DeleteOutlined, EditOutlined } from '@ant-design/icons'
|
||||||
|
import { getConfig, setConfig, updateConfig, deleteConfig, toggleFlag, getChangeLog } from '@/services/config-api'
|
||||||
|
import dayjs from 'dayjs'
|
||||||
|
|
||||||
|
const { Title, Text } = Typography
|
||||||
|
|
||||||
|
function ConfigPage() {
|
||||||
|
const { modal, message } = App.useApp()
|
||||||
|
const qc = useQueryClient()
|
||||||
|
const [addOpen, setAddOpen] = useState(false)
|
||||||
|
const [editKey, setEditKey] = useState('')
|
||||||
|
const [newKey, setNewKey] = useState('')
|
||||||
|
const [newValue, setNewValue] = useState('')
|
||||||
|
|
||||||
|
const { data } = useQuery({ queryKey: ['config'], queryFn: getConfig, staleTime: 30_000 })
|
||||||
|
const { data: changelog } = useQuery({ queryKey: ['config', 'changelog'], queryFn: getChangeLog, staleTime: 10_000 })
|
||||||
|
|
||||||
|
const handleSave = async () => {
|
||||||
|
if (editKey) { await updateConfig(editKey, newValue); message.success('已更新') }
|
||||||
|
else { await setConfig(newKey, newValue); message.success('已添加') }
|
||||||
|
setAddOpen(false); setEditKey(''); setNewKey(''); setNewValue('')
|
||||||
|
qc.invalidateQueries({ queryKey: ['config'] })
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleDelete = (key: string) => modal.confirm({
|
||||||
|
title: '删除配置', content: `确定删除 ${key}?`, okType: 'danger',
|
||||||
|
onOk: async () => { await deleteConfig(key); qc.invalidateQueries({ queryKey: ['config'] }) },
|
||||||
|
})
|
||||||
|
|
||||||
|
const configColumns = [
|
||||||
|
{ title: 'Key', dataIndex: 'key', width: 180, ellipsis: true },
|
||||||
|
{ title: 'Value', dataIndex: 'value', width: 300, ellipsis: true, render: (v: string) => <Text code>{v}</Text> },
|
||||||
|
{ title: '环境', dataIndex: 'environment', width: 80 },
|
||||||
|
{ title: '更新', dataIndex: 'updatedAt', width: 140, render: (d: string) => dayjs(d).format('MM-DD HH:mm') },
|
||||||
|
{ title: '操作', width: 100, render: (_: any, r: any) => (
|
||||||
|
<Space>
|
||||||
|
<Button type="link" size="small" icon={<EditOutlined />} onClick={() => { setEditKey(r.key); setNewKey(r.key); setNewValue(r.value); setAddOpen(true) }} />
|
||||||
|
<Button type="link" size="small" danger icon={<DeleteOutlined />} onClick={() => handleDelete(r.key)} />
|
||||||
|
</Space>
|
||||||
|
)},
|
||||||
|
]
|
||||||
|
|
||||||
|
const flagColumns = [
|
||||||
|
{ title: '开关', dataIndex: 'name', width: 180 },
|
||||||
|
{ title: '说明', dataIndex: 'description', ellipsis: true },
|
||||||
|
{ title: '状态', dataIndex: 'enabled', width: 80, render: (v: boolean, r: any) => (
|
||||||
|
<Switch checked={v} onChange={(checked) => { toggleFlag(r.name, checked); message.success('已切换'); qc.invalidateQueries({ queryKey: ['config'] }) }} />
|
||||||
|
)},
|
||||||
|
]
|
||||||
|
|
||||||
|
const logColumns = [
|
||||||
|
{ title: '时间', dataIndex: 'createdAt', width: 130, render: (d: string) => dayjs(d).format('MM-DD HH:mm:ss') },
|
||||||
|
{ title: '类型', dataIndex: 'entityType', width: 100 },
|
||||||
|
{ title: '字段', dataIndex: 'field', width: 100 },
|
||||||
|
{ title: '旧值', dataIndex: 'oldValue', ellipsis: true, render: (v: string) => v ? <Text code style={{ fontSize: 11 }}>{v}</Text> : '-' },
|
||||||
|
{ title: '新值', dataIndex: 'newValue', ellipsis: true, render: (v: string) => <Text code style={{ fontSize: 11 }}>{v}</Text> },
|
||||||
|
{ title: '操作人', dataIndex: 'changedBy', width: 150 },
|
||||||
|
]
|
||||||
|
|
||||||
|
const items = [
|
||||||
|
{ key: 'config', label: '配置', children: <Table dataSource={data?.configs || []} columns={configColumns} rowKey="key" pagination={false} size="small" /> },
|
||||||
|
{ key: 'flags', label: '功能开关', children: <Table dataSource={data?.flags || []} columns={flagColumns} rowKey="name" pagination={false} size="small" /> },
|
||||||
|
{ key: 'log', label: '变更历史', children: <Table dataSource={changelog || []} columns={logColumns} rowKey="id" pagination={{ pageSize: 30 }} size="small" /> },
|
||||||
|
]
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: 16 }}>
|
||||||
|
<Title level={5} style={{ margin: 0 }}>配置管理</Title>
|
||||||
|
<Space>
|
||||||
|
<Button icon={<PlusOutlined />} type="primary" onClick={() => { setEditKey(''); setNewKey(''); setNewValue(''); setAddOpen(true) }}>新增配置</Button>
|
||||||
|
<Button icon={<ReloadOutlined />} onClick={() => qc.invalidateQueries({ queryKey: ['config'] })}>刷新</Button>
|
||||||
|
</Space>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Tabs items={items} />
|
||||||
|
|
||||||
|
<Modal title={editKey ? '编辑配置' : '新增配置'} open={addOpen} onOk={handleSave} onCancel={() => setAddOpen(false)} okText="保存">
|
||||||
|
<Input placeholder="Key" value={newKey} onChange={e => setNewKey(e.target.value)} disabled={!!editKey} style={{ marginBottom: 12 }} />
|
||||||
|
<Input.TextArea placeholder="Value" value={newValue} onChange={e => setNewValue(e.target.value)} rows={3} />
|
||||||
|
</Modal>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ConfigPage
|
||||||
12
src/services/config-api.ts
Normal file
12
src/services/config-api.ts
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
import { api } from './http-client'
|
||||||
|
|
||||||
|
export interface AppConfigItem { id: string; key: string; value: string; description: string | null; environment: string; updatedAt: string }
|
||||||
|
export interface FeatureFlagItem { id: string; name: string; enabled: boolean; description: string | null; rolloutPct: number }
|
||||||
|
export interface ChangeLogItem { id: string; entityType: string; entityId: string; field: string; oldValue: string | null; newValue: string | null; changedBy: string | null; createdAt: string }
|
||||||
|
|
||||||
|
export function getConfig(): Promise<{ configs: AppConfigItem[]; flags: FeatureFlagItem[] }> { return api.get('/admin-api/config') }
|
||||||
|
export function setConfig(key: string, value: string): Promise<any> { return api.post('/admin-api/config', { key, value }) }
|
||||||
|
export function updateConfig(key: string, value: string): Promise<any> { return api.patch(`/admin-api/config/${key}`, { value }) }
|
||||||
|
export function deleteConfig(key: string): Promise<any> { return api.delete(`/admin-api/config/${key}`) }
|
||||||
|
export function toggleFlag(name: string, enabled: boolean): Promise<any> { return api.post(`/admin-api/config/flags/${name}`, { enabled }) }
|
||||||
|
export function getChangeLog(): Promise<ChangeLogItem[]> { return api.get('/admin-api/config/changelog') }
|
||||||
Loading…
x
Reference in New Issue
Block a user