fix: 审核引擎优化 + 执行结果结构化展示
后端审核规则修复: - Redis SEC-001: SET/DEL等常规命令不再误报AUTH要求,改为仅对管理级命令WARNING - Redis SEC-003: 降级为INFO,排除已由CMD-001拦截的FLUSHDB/FLUSHALL - Redis key提取: 排除CONFIG/ACL等管理命令,不再把子命令参数误认为key名 - MySQL SEC-006: 新增GRANT ALL/REVOKE ALL全局权限拦截 - MySQL SEC-007: 新增LOAD DATA/LOAD XML拦截 - MySQL SEC-008: 新增SLEEP()/BENCHMARK()时间盲注拦截 - MySQL SEC-009: 新增INTO OUTFILE/DUMPFILE文件写入拦截 前端执行结果结构化: - TicketDetailPage执行结果从原始JSON改为结构化展示 - 解析JSON展示: 状态徽章+影响行数+耗时+验证状态 - Errors/Warnings逐条渲染: 红色错误框+黄色警告框+具体消息 - 删除废弃的executeStats/renderExecuteStats 其他: 审核规则6分组/慢查询移除/查询数据重命名/AuditLogs修复 测试: 1040 passed, TSC零错误
This commit is contained in:
1 parent
9f56e866f2
commit
4ac2528f8e
19 files changed
+1202
-325
No files matched your search
@@ -11,6 +11,7 @@ import { useActionPermission } from '@/hooks/useAuth';
|
||||
import PageHeader from '@/components/PageHeader';
|
||||
import EmptyState from '@/components/EmptyState';
|
||||
import { instanceApi } from '@/api/instance';
|
||||
import EditInstanceDialog from '@/components/EditInstanceDialog';
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Types */
|
||||
@@ -41,11 +42,14 @@ type ViewMode = 'grid' | 'list';
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Constants */
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* DB Brand Icons */
|
||||
|
||||
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' },
|
||||
mysql: { icon: MySQLIcon, color: 'text-[#26a69a]', bg: 'bg-[rgba(0,117,143,0.15)]', label: 'MySQL' },
|
||||
postgres: { icon: PostgreSQLIcon, color: 'text-[#42a5f5]', bg: 'bg-[rgba(51,103,145,0.15)]', label: 'PostgreSQL' },
|
||||
redis: { icon: RedisIcon, color: 'text-[#ef5350]', bg: 'bg-[rgba(220,50,32,0.15)]', label: 'Redis' },
|
||||
mongo: { icon: MongoDBIcon, color: 'text-[#66bb6a]', bg: 'bg-[rgba(71,162,72,0.15)]', label: 'MongoDB' },
|
||||
};
|
||||
|
||||
const envLabel: Record<string, string> = {
|
||||
@@ -132,6 +136,8 @@ export default function InstancePage() {
|
||||
const [instances, setInstances] = useState<InstanceItem[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [showCreate, setShowCreate] = useState(false);
|
||||
const [showEdit, setShowEdit] = useState(false);
|
||||
const [editingInstance, setEditingInstance] = useState<InstanceItem | null>(null);
|
||||
const [selectedIds, setSelectedIds] = useState<Set<number>>(new Set());
|
||||
const [batchDeleting, setBatchDeleting] = useState(false);
|
||||
|
||||
@@ -242,6 +248,28 @@ export default function InstancePage() {
|
||||
);
|
||||
}
|
||||
|
||||
if (showEdit && editingInstance) {
|
||||
return (
|
||||
<>
|
||||
<InstanceDetail
|
||||
instance={editingInstance}
|
||||
onBack={() => { setView('grid'); setShowEdit(false); setEditingInstance(null); }}
|
||||
config={dbTypeConfig[editingInstance.db_type] || dbTypeConfig.mysql}
|
||||
canEdit={canEdit}
|
||||
canDelete={canDelete}
|
||||
onDelete={() => { handleDelete(editingInstance.id); setView('grid'); setShowEdit(false); setEditingInstance(null); }}
|
||||
onEdit={() => {}}
|
||||
/>
|
||||
<EditInstanceDialog
|
||||
instance={editingInstance}
|
||||
open={showEdit}
|
||||
onClose={() => { setShowEdit(false); setEditingInstance(null); }}
|
||||
onSaved={() => { setShowEdit(false); setEditingInstance(null); fetchInstances(); }}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
const selectedConfig = selectedInstance
|
||||
? (dbTypeConfig[selectedInstance.db_type] || dbTypeConfig.mysql)
|
||||
: dbTypeConfig.mysql;
|
||||
@@ -255,6 +283,7 @@ export default function InstancePage() {
|
||||
canEdit={canEdit}
|
||||
canDelete={canDelete}
|
||||
onDelete={() => { handleDelete(selectedInstance.id); setView('grid'); }}
|
||||
onEdit={() => { setEditingInstance(selectedInstance); setShowEdit(true); }}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -428,7 +457,7 @@ export default function InstancePage() {
|
||||
) : filtered.length === 0 ? (
|
||||
<EmptyState type="search" />
|
||||
) : layoutMode === 'grid' ? (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-3">
|
||||
{filtered.map(instance => {
|
||||
const ic = dbTypeConfig[instance.db_type] || dbTypeConfig.mysql;
|
||||
const IconComp = ic.icon;
|
||||
@@ -438,7 +467,7 @@ export default function InstancePage() {
|
||||
return (
|
||||
<div
|
||||
key={instance.id}
|
||||
className={`dg-card p-5 cursor-pointer dg-card-hover group relative transition-all ${
|
||||
className={`dg-card p-3.5 cursor-pointer dg-card-hover group relative transition-all ${
|
||||
isSelected ? 'ring-2 ring-blue-500/50' : ''
|
||||
}`}
|
||||
onClick={() => { setSelectedInstance(instance); setView('detail'); }}
|
||||
@@ -456,8 +485,8 @@ export default function InstancePage() {
|
||||
</div>
|
||||
|
||||
<div className="flex items-start justify-between mb-4 pl-6">
|
||||
<div className={`w-11 h-11 rounded-xl ${ic.bg} flex items-center justify-center`}>
|
||||
<IconComp className={`w-6 h-6 ${ic.color}`} />
|
||||
<div className={`w-9 h-9 rounded-lg ${ic.bg} flex items-center justify-center`}>
|
||||
<IconComp className="w-5 h-5 object-contain" />
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className={`text-[11px] px-2 py-0.5 rounded-md border font-medium ${envColor[instance.env] || 'bg-[var(--dg-bg-tertiary)] text-[var(--dg-text-muted)] border-[var(--dg-border)]'}`}>
|
||||
@@ -472,14 +501,14 @@ export default function InstancePage() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h3 className="text-base font-semibold mb-1 truncate">{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">
|
||||
<h3 className="text-sm font-semibold mb-0.5 truncate">{instance.name}</h3>
|
||||
<p className="text-[11px] text-[var(--dg-text-muted)] mb-0.5">{instance.host}:{instance.port}</p>
|
||||
<p className="text-[11px] text-[var(--dg-text-muted)] mb-2">
|
||||
{ic.label} {instance.version || ''}
|
||||
</p>
|
||||
|
||||
{/* Health Score */}
|
||||
<div className="flex items-center gap-2 mb-3 p-2 rounded-lg bg-[var(--dg-bg-tertiary)]">
|
||||
<div className="flex items-center gap-2 mb-2 p-1.5 rounded-lg bg-[var(--dg-bg-tertiary)]">
|
||||
<Gauge className={`w-4 h-4 ${healthColor(health)}`} />
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center justify-between">
|
||||
@@ -495,35 +524,33 @@ export default function InstancePage() {
|
||||
</div>
|
||||
|
||||
{/* Metrics */}
|
||||
<div className="grid grid-cols-3 gap-2 text-center">
|
||||
<div className="p-1.5 rounded-md bg-[var(--dg-bg-tertiary)]">
|
||||
<div className="grid grid-cols-3 gap-1.5 text-center">
|
||||
<div className="p-1 rounded-md bg-[var(--dg-bg-tertiary)]">
|
||||
<div className="flex items-center justify-center gap-1">
|
||||
<Users className="w-3 h-3 text-[var(--dg-text-muted)]" />
|
||||
<span className="text-xs font-semibold">{instance.connections}</span>
|
||||
<span className="text-[11px] font-semibold">{instance.connections}</span>
|
||||
</div>
|
||||
<span className="text-[10px] text-[var(--dg-text-muted)]">连接</span>
|
||||
<span className="text-[9px] text-[var(--dg-text-muted)]">连接</span>
|
||||
</div>
|
||||
<div className="p-1.5 rounded-md bg-[var(--dg-bg-tertiary)]">
|
||||
<div className="p-1 rounded-md bg-[var(--dg-bg-tertiary)]">
|
||||
<div className="flex items-center justify-center gap-1">
|
||||
<Activity className="w-3 h-3 text-[var(--dg-text-muted)]" />
|
||||
<span className="text-xs font-semibold">{instance.qps ? instance.qps.toLocaleString() : '-'}</span>
|
||||
<span className="text-[11px] font-semibold">{instance.qps ? instance.qps.toLocaleString() : '-'}</span>
|
||||
</div>
|
||||
<span className="text-[10px] text-[var(--dg-text-muted)]">QPS</span>
|
||||
<span className="text-[9px] text-[var(--dg-text-muted)]">QPS</span>
|
||||
</div>
|
||||
<div className="p-1.5 rounded-md bg-[var(--dg-bg-tertiary)]">
|
||||
<div className="p-1 rounded-md bg-[var(--dg-bg-tertiary)]">
|
||||
<div className="flex items-center justify-center gap-1">
|
||||
<Signal className="w-3 h-3 text-[var(--dg-text-muted)]" />
|
||||
<span className="text-xs font-semibold">{instance.latency}ms</span>
|
||||
<span className="text-[11px] font-semibold">{instance.latency}ms</span>
|
||||
</div>
|
||||
<span className="text-[10px] text-[var(--dg-text-muted)]">延迟</span>
|
||||
<span className="text-[9px] text-[var(--dg-text-muted)]">延迟</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{instance.description && (
|
||||
<p className="text-xs text-[var(--dg-text-secondary)] line-clamp-2 mt-3">{instance.description}</p>
|
||||
)}
|
||||
|
||||
<div className="mt-3 opacity-0 group-hover:opacity-100 transition-opacity">
|
||||
|
||||
<div className="mt-2 opacity-0 group-hover:opacity-100 transition-opacity">
|
||||
<span className="text-[11px] text-blue-400 flex items-center gap-1">
|
||||
查看详情
|
||||
<ChevronRight className="w-3 h-3" />
|
||||
@@ -584,7 +611,7 @@ export default function InstancePage() {
|
||||
<td className="px-4 py-3 font-medium">{instance.name}</td>
|
||||
<td className="px-4 py-3">
|
||||
<span className={`flex items-center gap-1.5 ${ic.color}`}>
|
||||
<IconComp className="w-4 h-4" />
|
||||
<IconComp className="w-4 h-4 object-contain" />
|
||||
<span className="text-xs">{ic.label}</span>
|
||||
</span>
|
||||
</td>
|
||||
@@ -633,10 +660,10 @@ export default function InstancePage() {
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* InstanceDetail */
|
||||
/* ------------------------------------------------------------------ */
|
||||
function InstanceDetail({ instance, onBack, config, canEdit, canDelete, onDelete }: {
|
||||
function InstanceDetail({ instance, onBack, config, canEdit, canDelete, onDelete, onEdit }: {
|
||||
instance: InstanceItem; onBack: () => void;
|
||||
config: { icon: React.ElementType; color: string; bg: string };
|
||||
canEdit: boolean; canDelete: boolean; onDelete: () => void;
|
||||
canEdit: boolean; canDelete: boolean; onDelete: () => void; onEdit: () => void;
|
||||
}) {
|
||||
const [testing, setTesting] = useState(false);
|
||||
const [testResult, setTestResult] = useState<{ success: boolean; message: string; version?: string } | null>(null);
|
||||
@@ -694,7 +721,7 @@ function InstanceDetail({ instance, onBack, config, canEdit, canDelete, onDelete
|
||||
{testing ? '测试中...' : '测试连接'}
|
||||
</button>
|
||||
{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">
|
||||
<button onClick={onEdit} 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>
|
||||
@@ -845,8 +872,7 @@ function InstanceDetail({ instance, onBack, config, canEdit, canDelete, onDelete
|
||||
);
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* DB Brand Icons */
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
function MySQLIcon({ className }: { className?: string }) {
|
||||
return <img src="/db-logos/mysql.svg" alt="MySQL" className={className} />;
|
||||
|
||||
Reference in new issue
Block a user