refactor: 数据源菜单合一+布局优化
- 4个数据源菜单(MySQL/PG/Redis/Mongo)合并为1个「数据源」 - InstancePage 改为统一入口,顶部Tab切换类型过滤 - 添加数据源改为弹框Modal(默认MySQL) - 去掉左下角用户区,退出登录移到右上角header - 侧边栏收窄220px,整体布局更紧凑 - 后端202 tests passing - 前端构建零错误
This commit is contained in:
1 parent
bab55d3927
commit
5cbd0e3441
23 files changed
+2881
-94
No files matched your search
@@ -1,5 +1,4 @@
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import {
|
||||
Plus, RefreshCw, Search, Database, Server, Zap, Leaf,
|
||||
Activity, HardDrive, ChevronRight, Trash2, Edit2,
|
||||
@@ -51,11 +50,11 @@ function getStatusInfo(status: string) {
|
||||
}
|
||||
|
||||
export default function InstancePage() {
|
||||
const { type = 'mysql' } = useParams<{ type: string }>();
|
||||
const canAdd = useActionPermission('instance.add');
|
||||
const canEdit = useActionPermission('instance.edit');
|
||||
const canDelete = useActionPermission('instance.delete');
|
||||
|
||||
const [activeType, setActiveType] = useState<string>('all');
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
const [filterStatus, setFilterStatus] = useState('');
|
||||
const [filterEnv, setFilterEnv] = useState('');
|
||||
@@ -65,12 +64,18 @@ export default function InstancePage() {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [showCreate, setShowCreate] = useState(false);
|
||||
|
||||
const config = dbTypeConfig[type] || dbTypeConfig.mysql;
|
||||
const typeTabs = [
|
||||
{ key: 'all', label: '全部' },
|
||||
{ key: 'mysql', label: 'MySQL' },
|
||||
{ key: 'postgres', label: 'PostgreSQL' },
|
||||
{ key: 'redis', label: 'Redis' },
|
||||
{ key: 'mongo', label: 'MongoDB' },
|
||||
];
|
||||
|
||||
const fetchInstances = useCallback(async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const res = await instanceApi.list({ db_type: type });
|
||||
const res = await instanceApi.list({});
|
||||
const data = Array.isArray(res) ? res : [];
|
||||
setInstances(data);
|
||||
} catch {
|
||||
@@ -78,11 +83,12 @@ export default function InstancePage() {
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [type]);
|
||||
}, []);
|
||||
|
||||
useEffect(() => { fetchInstances(); }, [fetchInstances]);
|
||||
|
||||
const filtered = instances.filter(i => {
|
||||
if (activeType !== 'all' && i.db_type !== activeType) return false;
|
||||
if (filterStatus && i.status !== filterStatus) return false;
|
||||
if (filterEnv && i.env !== filterEnv) return false;
|
||||
if (searchQuery && !i.name.includes(searchQuery) && !i.host.includes(searchQuery)) return false;
|
||||
@@ -104,22 +110,37 @@ export default function InstancePage() {
|
||||
};
|
||||
|
||||
if (showCreate) {
|
||||
return <CreateInstanceForm defaultType={type} onCancel={() => setShowCreate(false)} onCreated={() => { setShowCreate(false); fetchInstances(); }} />;
|
||||
return <CreateInstanceForm defaultType={activeType === 'all' ? 'mysql' : activeType} onCancel={() => setShowCreate(false)} onCreated={() => { setShowCreate(false); fetchInstances(); }} />;
|
||||
}
|
||||
|
||||
const selectedConfig = selectedInstance ? (dbTypeConfig[selectedInstance.db_type] || dbTypeConfig.mysql) : dbTypeConfig.mysql;
|
||||
|
||||
if (view === 'detail' && selectedInstance) {
|
||||
return <InstanceDetail instance={selectedInstance} onBack={() => setView('grid')} config={config} canEdit={canEdit} canDelete={canDelete} onDelete={() => { handleDelete(selectedInstance.id); setView('grid'); }} />;
|
||||
return <InstanceDetail instance={selectedInstance} onBack={() => setView('grid')} config={selectedConfig} canEdit={canEdit} canDelete={canDelete} onDelete={() => { handleDelete(selectedInstance.id); setView('grid'); }} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-5">
|
||||
<PageHeader
|
||||
title={`${config.label} 实例管理`}
|
||||
description={`管理 ${config.label} 数据库实例,监控连接状态`}
|
||||
title="数据源管理"
|
||||
description="管理所有数据库实例,监控连接状态"
|
||||
secondaryAction={{ label: '刷新', icon: RefreshCw, onClick: fetchInstances }}
|
||||
action={canAdd ? { label: '添加实例', icon: Plus, onClick: () => setShowCreate(true), permission: 'instance.add' } : undefined}
|
||||
/>
|
||||
|
||||
{/* 类型 Tab */}
|
||||
<div className="flex items-center gap-1 p-1 bg-[var(--dg-bg-tertiary)] rounded-lg w-fit">
|
||||
{typeTabs.map(tab => (
|
||||
<button
|
||||
key={tab.key}
|
||||
onClick={() => setActiveType(tab.key)}
|
||||
className={'px-4 py-1.5 rounded-md text-sm font-medium transition-colors ' + (activeType === tab.key ? 'bg-blue-600 text-white' : 'text-[var(--dg-text-secondary)] hover:bg-[var(--dg-bg-hover)]')}
|
||||
>
|
||||
{tab.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* 状态概览 */}
|
||||
<div className="grid grid-cols-3 gap-4">
|
||||
<Card className="dg-card">
|
||||
@@ -168,23 +189,27 @@ export default function InstancePage() {
|
||||
<EmptyState type="search" />
|
||||
) : (
|
||||
<div className="grid grid-cols-3 gap-4">
|
||||
{filtered.map(instance => (
|
||||
{filtered.map(instance => {
|
||||
const ic = dbTypeConfig[instance.db_type] || dbTypeConfig.mysql;
|
||||
const IconComp = ic.icon;
|
||||
return (
|
||||
<div key={instance.id} className="dg-card p-5 cursor-pointer dg-card-hover group" onClick={() => { setSelectedInstance(instance); setView('detail'); }}>
|
||||
<div className="flex items-start justify-between mb-4">
|
||||
<div className={`w-11 h-11 rounded-xl ${config.bg} flex items-center justify-center`}>
|
||||
<config.icon className={`w-6 h-6 ${config.color}`} />
|
||||
<div className={'w-11 h-11 rounded-xl ' + ic.bg + ' flex items-center justify-center'}>
|
||||
<IconComp className={'w-6 h-6 ' + ic.color} />
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="px-2 py-0.5 bg-[var(--dg-bg-tertiary)] text-[var(--dg-text-muted)] text-[11px] rounded-md">{envLabel[instance.env] || instance.env}</span>
|
||||
<div className={`w-2.5 h-2.5 rounded-full ${getStatusInfo(instance.status).online ? 'bg-green-400 shadow-[0_0_0_3px_rgba(63,185,80,0.2)]' : 'bg-red-400'}`} />
|
||||
<div className={'w-2.5 h-2.5 rounded-full ' + (getStatusInfo(instance.status).online ? 'bg-green-400 shadow-[0_0_0_3px_rgba(63,185,80,0.2)]' : 'bg-red-400')} />
|
||||
</div>
|
||||
</div>
|
||||
<h3 className="text-base font-semibold mb-1">{instance.name}</h3>
|
||||
<p className="text-xs text-[var(--dg-text-muted)] mb-1">{instance.host}:{instance.port}</p>
|
||||
<p className="text-xs text-[var(--dg-text-muted)] mb-3">{config.label} {instance.version || '-'} · {envLabel[instance.env] || instance.env}</p>
|
||||
<p className="text-xs text-[var(--dg-text-muted)] mb-3">{ic.label} {instance.version || ''} {instance.env ? ' · ' + (envLabel[instance.env] || instance.env) : ''}</p>
|
||||
{instance.description && <p className="text-xs text-[var(--dg-text-secondary)] line-clamp-2">{instance.description}</p>}
|
||||
</div>
|
||||
))}
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@@ -360,11 +385,13 @@ function CreateInstanceForm({ defaultType, onCancel, onCreated }: { defaultType:
|
||||
const labelCls = 'block text-sm font-medium mb-1.5';
|
||||
|
||||
return (
|
||||
<div className="max-w-4xl mx-auto space-y-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<h1 className="text-xl font-semibold">添加数据源</h1>
|
||||
<button onClick={onCancel} className="text-[var(--dg-text-secondary)] hover:text-[var(--dg-text-primary)]"><XCircle className="w-5 h-5" /></button>
|
||||
</div>
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50 backdrop-blur-sm" onClick={onCancel}>
|
||||
<div className="bg-[var(--dg-bg-secondary)] rounded-xl shadow-2xl w-full max-w-2xl max-h-[85vh] overflow-y-auto" onClick={e => e.stopPropagation()}>
|
||||
<div className="sticky top-0 z-10 flex items-center justify-between px-6 py-4 bg-[var(--dg-bg-secondary)] border-b border-[var(--dg-border)]">
|
||||
<h2 className="text-lg font-semibold">添加数据源</h2>
|
||||
<button onClick={onCancel} className="text-[var(--dg-text-muted)] hover:text-[var(--dg-text-primary)]"><XCircle className="w-5 h-5" /></button>
|
||||
</div>
|
||||
<div className="p-6 space-y-4">
|
||||
|
||||
{/* 数据源信息 */}
|
||||
<Card className="dg-card">
|
||||
@@ -487,13 +514,14 @@ function CreateInstanceForm({ defaultType, onCancel, onCreated }: { defaultType:
|
||||
</Card>
|
||||
|
||||
{error && <div className="text-sm text-red-400">{error}</div>}
|
||||
|
||||
<div className="flex justify-end gap-3 pb-6">
|
||||
<button onClick={onCancel} className="px-5 py-2.5 bg-[var(--dg-bg-tertiary)] border border-[var(--dg-border)] rounded-lg text-sm font-medium hover:bg-[var(--dg-bg-hover)]">取消</button>
|
||||
<button onClick={handleSubmit} disabled={submitting} className="px-5 py-2.5 bg-gradient-to-r from-blue-500 to-purple-600 rounded-lg text-sm font-medium text-white hover:opacity-90 disabled:opacity-50">
|
||||
<div className="flex justify-end gap-3">
|
||||
<button onClick={onCancel} className="px-4 py-2 bg-[var(--dg-bg-tertiary)] border border-[var(--dg-border)] rounded-lg text-sm hover:bg-[var(--dg-bg-hover)]">取消</button>
|
||||
<button onClick={handleSubmit} disabled={submitting} className="px-4 py-2 bg-blue-600 hover:bg-blue-700 rounded-lg text-sm text-white disabled:opacity-50">
|
||||
{submitting ? '创建中...' : '添加数据源'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user