Initial commit: SQL audit platform v0.1.0
FastAPI backend with auth, user, instance, ticket, query, dashboard APIs. React + TypeScript frontend with mock data UI (10 pages). SQLite + async SQLAlchemy, JWT auth, role-based access control. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
commit
439155c92e
290 files changed
+61950
No files matched your search
@@ -0,0 +1,290 @@
|
||||
import { useState, useMemo } from 'react';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import {
|
||||
Plus, RefreshCw, Search, Database, Server, Zap, Leaf,
|
||||
Activity, HardDrive, Cpu, MemoryStick, Clock, ChevronRight, Trash2, Edit2,
|
||||
Undo2,
|
||||
} from 'lucide-react';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { dbInstances } from '@/lib/mock';
|
||||
import { useActionPermission } from '@/hooks/useAuth';
|
||||
import PageHeader from '@/components/PageHeader';
|
||||
import EmptyState from '@/components/EmptyState';
|
||||
import type { DBInstance } from '@/types';
|
||||
|
||||
const dbTypeConfig: Record<string, { icon: React.ElementType; color: string; bg: string; label: string }> = {
|
||||
mysql: { icon: Database, color: 'text-[#26a69a]', bg: 'bg-[rgba(0,117,143,0.15)]', label: 'MySQL' },
|
||||
postgres: { icon: Server, color: 'text-[#42a5f5]', bg: 'bg-[rgba(51,103,145,0.15)]', label: 'PostgreSQL' },
|
||||
redis: { icon: Zap, color: 'text-[#ef5350]', bg: 'bg-[rgba(220,50,32,0.15)]', label: 'Redis' },
|
||||
mongo: { icon: Leaf, color: 'text-[#66bb6a]', bg: 'bg-[rgba(71,162,72,0.15)]', label: 'MongoDB' },
|
||||
};
|
||||
|
||||
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 [searchQuery, setSearchQuery] = useState('');
|
||||
const [filterStatus, setFilterStatus] = useState('');
|
||||
const [filterEnv, setFilterEnv] = useState('');
|
||||
const [selectedInstance, setSelectedInstance] = useState<DBInstance | null>(null);
|
||||
const [view, setView] = useState<'grid' | 'detail'>('grid');
|
||||
|
||||
const config: { icon: React.ElementType; color: string; bg: string; label: string } = dbTypeConfig[type] || dbTypeConfig.mysql;
|
||||
|
||||
const instances = useMemo(() => {
|
||||
return dbInstances.filter(i => {
|
||||
if (i.dbType !== type) 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;
|
||||
return true;
|
||||
});
|
||||
}, [type, filterStatus, filterEnv, searchQuery]);
|
||||
|
||||
const statusCounts = useMemo(() => ({
|
||||
online: instances.filter(i => i.status === 'online').length,
|
||||
warning: instances.filter(i => i.status === 'warning').length,
|
||||
offline: instances.filter(i => i.status === 'offline').length,
|
||||
}), [instances]);
|
||||
|
||||
if (view === 'detail' && selectedInstance) {
|
||||
return <InstanceDetail instance={selectedInstance} onBack={() => setView('grid')} config={config} canEdit={canEdit} canDelete={canDelete} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-5">
|
||||
<PageHeader
|
||||
title={`${config.label} 实例管理`}
|
||||
description={`管理 ${config.label} 数据库实例,监控连接状态和性能指标`}
|
||||
secondaryAction={{ label: '刷新', icon: RefreshCw, onClick: () => {} }}
|
||||
action={canAdd ? { label: '添加实例', icon: Plus, onClick: () => {}, permission: 'instance.add' } : undefined}
|
||||
/>
|
||||
|
||||
{/* 状态概览 */}
|
||||
<div className="grid grid-cols-4 gap-4">
|
||||
<Card className="dg-card">
|
||||
<CardContent className="p-4 flex items-center gap-3">
|
||||
<div className="w-10 h-10 rounded-lg bg-green-500/10 flex items-center justify-center"><Activity className="w-5 h-5 text-green-400" /></div>
|
||||
<div><div className="text-2xl font-bold">{statusCounts.online}</div><div className="text-xs text-[var(--dg-text-muted)]">在线实例</div></div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card className="dg-card">
|
||||
<CardContent className="p-4 flex items-center gap-3">
|
||||
<div className="w-10 h-10 rounded-lg bg-yellow-500/10 flex items-center justify-center"><HardDrive className="w-5 h-5 text-yellow-400" /></div>
|
||||
<div><div className="text-2xl font-bold">{statusCounts.warning}</div><div className="text-xs text-[var(--dg-text-muted)]">告警实例</div></div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card className="dg-card">
|
||||
<CardContent className="p-4 flex items-center gap-3">
|
||||
<div className="w-10 h-10 rounded-lg bg-red-500/10 flex items-center justify-center"><HardDrive className="w-5 h-5 text-red-400" /></div>
|
||||
<div><div className="text-2xl font-bold">{statusCounts.offline}</div><div className="text-xs text-[var(--dg-text-muted)]">离线实例</div></div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card className="dg-card">
|
||||
<CardContent className="p-4 flex items-center gap-3">
|
||||
<div className="w-10 h-10 rounded-lg bg-blue-500/10 flex items-center justify-center"><Server className="w-5 h-5 text-blue-400" /></div>
|
||||
<div><div className="text-2xl font-bold">{instances.length}</div><div className="text-xs text-[var(--dg-text-muted)]">总实例数</div></div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* 筛选栏 */}
|
||||
<div className="flex items-center gap-3 p-4 dg-card flex-wrap">
|
||||
<div className="relative flex-1 max-w-xs">
|
||||
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-[var(--dg-text-muted)]" />
|
||||
<input
|
||||
className="w-full pl-10 pr-4 py-2.5 bg-[var(--dg-bg-tertiary)] border border-[var(--dg-border)] rounded-lg text-sm text-[var(--dg-text-primary)] placeholder:text-[var(--dg-text-muted)] focus:border-blue-400 focus:outline-none focus:ring-1 focus:ring-blue-400/20 transition-all"
|
||||
placeholder="搜索实例名称/地址"
|
||||
value={searchQuery}
|
||||
onChange={e => setSearchQuery(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<select className="px-3 py-2.5 bg-[var(--dg-bg-tertiary)] border border-[var(--dg-border)] rounded-lg text-sm text-[var(--dg-text-primary)] focus:border-blue-400 focus:outline-none" value={filterStatus} onChange={e => setFilterStatus(e.target.value)}>
|
||||
<option value="">全部状态</option>
|
||||
<option value="online">在线</option>
|
||||
<option value="offline">离线</option>
|
||||
<option value="warning">告警</option>
|
||||
</select>
|
||||
<select className="px-3 py-2.5 bg-[var(--dg-bg-tertiary)] border border-[var(--dg-border)] rounded-lg text-sm text-[var(--dg-text-primary)] focus:border-blue-400 focus:outline-none" value={filterEnv} onChange={e => setFilterEnv(e.target.value)}>
|
||||
<option value="">全部环境</option>
|
||||
<option value="prod">生产</option>
|
||||
<option value="test">测试</option>
|
||||
<option value="dev">开发</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{/* 实例卡片网格 */}
|
||||
{instances.length === 0 ? (
|
||||
<EmptyState type="search" />
|
||||
) : (
|
||||
<div className="grid grid-cols-3 gap-4">
|
||||
{instances.map(instance => (
|
||||
<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>
|
||||
<div className="flex items-center gap-2">
|
||||
{instance.tags.map(tag => (
|
||||
<span key={tag} className="px-2 py-0.5 bg-[var(--dg-bg-tertiary)] text-[var(--dg-text-muted)] text-[11px] rounded-md">{tag}</span>
|
||||
))}
|
||||
<div className={`w-2.5 h-2.5 rounded-full ${instance.status === 'online' ? 'bg-green-400 shadow-[0_0_0_3px_rgba(63,185,80,0.2)]' : instance.status === 'warning' ? 'bg-yellow-400' : '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-4">{config.label} {instance.version} · {instance.env === 'prod' ? '生产' : instance.env === 'test' ? '测试' : '开发'}</p>
|
||||
<div className="grid grid-cols-3 gap-3 pt-4 border-t border-[var(--dg-border)]">
|
||||
<div className="text-center">
|
||||
<div className="text-lg font-semibold">{instance.connections}</div>
|
||||
<div className="text-[11px] text-[var(--dg-text-muted)]">连接数</div>
|
||||
</div>
|
||||
<div className="text-center">
|
||||
<div className="text-lg font-semibold">{instance.qps.toLocaleString()}</div>
|
||||
<div className="text-[11px] text-[var(--dg-text-muted)]">QPS</div>
|
||||
</div>
|
||||
<div className="text-center">
|
||||
<div className="text-lg font-semibold">{instance.latency}<span className="text-xs font-normal text-[var(--dg-text-muted)]">ms</span></div>
|
||||
<div className="text-[11px] text-[var(--dg-text-muted)]">延迟</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-4 space-y-2">
|
||||
<div className="flex items-center gap-2 text-[11px]">
|
||||
<Cpu className="w-3 h-3 text-[var(--dg-text-muted)]" />
|
||||
<div className="flex-1 h-1.5 bg-[var(--dg-bg-tertiary)] rounded-full overflow-hidden">
|
||||
<div className="h-full bg-blue-400 rounded-full transition-all" style={{ width: `${instance.cpu}%` }} />
|
||||
</div>
|
||||
<span className="text-[var(--dg-text-muted)] w-8 text-right">{instance.cpu}%</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 text-[11px]">
|
||||
<MemoryStick className="w-3 h-3 text-[var(--dg-text-muted)]" />
|
||||
<div className="flex-1 h-1.5 bg-[var(--dg-bg-tertiary)] rounded-full overflow-hidden">
|
||||
<div className="h-full bg-purple-400 rounded-full transition-all" style={{ width: `${instance.memory}%` }} />
|
||||
</div>
|
||||
<span className="text-[var(--dg-text-muted)] w-8 text-right">{instance.memory}%</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function InstanceDetail({ instance, onBack, config, canEdit, canDelete }: {
|
||||
instance: DBInstance; onBack: () => void; config: { icon: React.ElementType; color: string; bg: string };
|
||||
canEdit: boolean; canDelete: boolean;
|
||||
}) {
|
||||
const DetailIcon = config.icon;
|
||||
return (
|
||||
<div className="space-y-5">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-4">
|
||||
<button onClick={onBack} className="w-9 h-9 rounded-lg bg-[var(--dg-bg-tertiary)] border border-[var(--dg-border)] flex items-center justify-center text-[var(--dg-text-secondary)] hover:text-[var(--dg-text-primary)] transition-all">
|
||||
<ChevronRight className="w-4 h-4 rotate-180" />
|
||||
</button>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className={`w-10 h-10 rounded-xl ${config.bg} flex items-center justify-center`}>
|
||||
<DetailIcon className={`w-5 h-5 ${config.color}`} />
|
||||
</div>
|
||||
<h1 className="text-2xl font-semibold flex items-center gap-3">
|
||||
{instance.name}
|
||||
<span className={`px-2.5 py-1 rounded-full text-xs font-medium ${instance.status === 'online' ? 'bg-green-500/15 text-green-400' : instance.status === 'warning' ? 'bg-yellow-500/15 text-yellow-400' : 'bg-red-500/15 text-red-400'}`}>
|
||||
{instance.status === 'online' ? '在线' : instance.status === 'warning' ? '告警' : '离线'}
|
||||
</span>
|
||||
</h1>
|
||||
<p className="text-sm text-[var(--dg-text-secondary)] mt-1">{instance.host}:{instance.port} · 版本 {instance.version}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
{canEdit && (
|
||||
<button className="flex items-center gap-2 px-4 py-2.5 bg-[var(--dg-bg-tertiary)] border border-[var(--dg-border)] rounded-lg text-sm text-[var(--dg-text-primary)] hover:bg-[var(--dg-bg-hover)] transition-all">
|
||||
<Edit2 className="w-4 h-4" /> 编辑
|
||||
</button>
|
||||
)}
|
||||
{canDelete && (
|
||||
<button className="flex items-center gap-2 px-4 py-2.5 bg-[var(--dg-bg-tertiary)] border border-[var(--dg-border)] rounded-lg text-sm text-red-400 hover:bg-red-500/10 hover:border-red-500/30 transition-all">
|
||||
<Trash2 className="w-4 h-4" /> 删除
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 性能指标 */}
|
||||
<div className="grid grid-cols-6 gap-4">
|
||||
{[
|
||||
{ label: '连接数', value: instance.connections, icon: Activity },
|
||||
{ label: 'QPS', value: instance.qps.toLocaleString(), icon: Zap },
|
||||
{ label: '延迟', value: `${instance.latency}ms`, icon: Clock },
|
||||
{ label: 'CPU', value: `${instance.cpu}%`, icon: Cpu },
|
||||
{ label: '内存', value: `${instance.memory}%`, icon: MemoryStick },
|
||||
{ label: '磁盘', value: `${instance.disk}%`, icon: HardDrive },
|
||||
].map((m, i) => (
|
||||
<Card key={i} className="dg-card">
|
||||
<CardContent className="p-4 text-center">
|
||||
<m.icon className="w-5 h-5 text-[var(--dg-text-muted)] mx-auto mb-2" />
|
||||
<div className="text-xl font-bold">{m.value}</div>
|
||||
<div className="text-xs text-[var(--dg-text-muted)] mt-1">{m.label}</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-5">
|
||||
{/* 连接信息 */}
|
||||
<Card className="dg-card">
|
||||
<CardHeader className="border-b border-[var(--dg-border)]"><CardTitle className="text-[15px] font-semibold">连接信息</CardTitle></CardHeader>
|
||||
<CardContent className="p-5 space-y-3">
|
||||
{[
|
||||
{ label: '主机地址', value: instance.host },
|
||||
{ label: '端口', value: instance.port.toString() },
|
||||
{ label: '数据库版本', value: instance.version },
|
||||
{ label: '运行环境', value: instance.env === 'prod' ? '生产环境' : instance.env === 'test' ? '测试环境' : '开发环境' },
|
||||
{ label: '运行时间', value: instance.uptime },
|
||||
{ label: '标签', value: instance.tags.join(', ') },
|
||||
...(instance.dbType === 'mysql' ? [
|
||||
{ label: 'Binlog 格式', value: <span className="px-2 py-0.5 bg-green-500/15 text-green-400 text-xs rounded">ROW</span> },
|
||||
{ label: 'Binlog 状态', value: <span className="px-2 py-0.5 bg-green-500/15 text-green-400 text-xs rounded">ON</span> },
|
||||
{ label: '回滚支持', value: <span className="flex items-center gap-1"><Undo2 className="w-3 h-3 text-green-400" /> <span className="text-green-400 text-xs">支持</span></span> },
|
||||
] : []),
|
||||
].map(item => (
|
||||
<div key={item.label} className="flex justify-between py-2 border-b border-[var(--dg-border-light)] last:border-0">
|
||||
<span className="text-sm text-[var(--dg-text-secondary)]">{item.label}</span>
|
||||
<span className="text-sm font-medium">{item.value}</span>
|
||||
</div>
|
||||
))}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* 资源使用趋势 */}
|
||||
<Card className="dg-card">
|
||||
<CardHeader className="border-b border-[var(--dg-border)]"><CardTitle className="text-[15px] font-semibold">资源使用</CardTitle></CardHeader>
|
||||
<CardContent className="p-5 space-y-5">
|
||||
{[
|
||||
{ label: 'CPU 使用率', value: instance.cpu, color: 'bg-blue-400' },
|
||||
{ label: '内存使用率', value: instance.memory, color: 'bg-purple-400' },
|
||||
{ label: '磁盘使用率', value: instance.disk, color: 'bg-orange-400' },
|
||||
].map(item => (
|
||||
<div key={item.label}>
|
||||
<div className="flex justify-between text-sm mb-2">
|
||||
<span className="text-[var(--dg-text-secondary)]">{item.label}</span>
|
||||
<span className="font-medium">{item.value}%</span>
|
||||
</div>
|
||||
<div className="h-2.5 bg-[var(--dg-bg-tertiary)] rounded-full overflow-hidden">
|
||||
<div className={`h-full ${item.color} rounded-full transition-all`} style={{ width: `${item.value}%` }} />
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user