import { useState, useEffect, useCallback, useMemo } from 'react'; import request from '../api/request'; import { Search, Filter, ChevronDown, ChevronUp, AlertTriangle, Info, XCircle, Database, Table, FileSearch, Shield, Loader2, Leaf, Server, } from 'lucide-react'; /* ------------------------------------------------------------------ */ /* Types */ /* ------------------------------------------------------------------ */ interface AuditRule { id: number; name: string; description: string | null; rule_type: string; severity: string; db_type: string; pattern: string | null; is_enabled: boolean; message: string; value: string | null; created_at: string; updated_at: string; } type DbType = 'mysql' | 'postgresql' | 'redis' | 'mongodb'; type RuleGroup = 'DDL' | 'DML' | 'DCL' | 'DQL' | 'security' | 'performance'; type SeverityFilter = 'all' | 'error' | 'warning' | 'info'; /* ------------------------------------------------------------------ */ /* Constants */ /* ------------------------------------------------------------------ */ interface DbTab { key: DbType; label: string; icon: React.ElementType; } const DB_TABS: DbTab[] = [ { key: 'mysql', label: 'MySQL', icon: Database }, { key: 'postgresql', label: 'PostgreSQL', icon: Server }, { key: 'redis', label: 'Redis', icon: Table }, { key: 'mongodb', label: 'MongoDB', icon: Leaf }, ]; const GROUP_CONFIG: Record = { DDL: { label: 'DDL 规则', icon: Table, color: 'text-amber-400 bg-amber-500/15' }, DML: { label: 'DML 规则', icon: Shield, color: 'text-blue-400 bg-blue-500/15' }, DCL: { label: 'DCL 规则', icon: Shield, color: 'text-purple-400 bg-purple-500/15' }, DQL: { label: 'DQL 规则', icon: FileSearch, color: 'text-green-400 bg-green-500/15' }, security: { label: '安全规则', icon: AlertTriangle, color: 'text-red-400 bg-red-500/15' }, performance: { label: '性能规则', icon: Info, color: 'text-cyan-400 bg-cyan-500/15' }, }; const SEVERITY_CONFIG: Record = { error: { label: '错误', color: 'text-red-400', dot: 'bg-red-400' }, warning: { label: '警告', color: 'text-amber-400', dot: 'bg-amber-400' }, info: { label: '提示', color: 'text-blue-400', dot: 'bg-blue-400' }, }; /* ------------------------------------------------------------------ */ /* Helpers */ /* ------------------------------------------------------------------ */ const VALID_GROUPS: RuleGroup[] = ['DDL', 'DML', 'DCL', 'DQL', 'security', 'performance']; function getRuleGroup(rule: AuditRule): RuleGroup { const rt = rule.rule_type; if (VALID_GROUPS.includes(rt as RuleGroup)) return rt as RuleGroup; // Fallback: map unknown types if (rt === 'safety' || rt === 'naming' || rt === 'syntax') return 'DDL'; return 'DDL'; } function sortRules(rules: AuditRule[], sortBy: 'severity' | 'name' | 'type'): AuditRule[] { const severityOrder = { error: 0, warning: 1, info: 2 }; const sorted = [...rules]; switch (sortBy) { case 'severity': return sorted.sort((a, b) => (severityOrder[a.severity as keyof typeof severityOrder] ?? 3) - (severityOrder[b.severity as keyof typeof severityOrder] ?? 3)); case 'name': return sorted.sort((a, b) => a.name.localeCompare(b.name, 'zh-CN')); case 'type': return sorted.sort((a, b) => getRuleGroup(a).localeCompare(getRuleGroup(b))); default: return sorted; } } /* ------------------------------------------------------------------ */ /* Component */ /* ------------------------------------------------------------------ */ export default function AuditRulesPage() { const [activeTab, setActiveTab] = useState('mysql'); const [rules, setRules] = useState([]); const [loading, setLoading] = useState(false); const [seeding, setSeeding] = useState(false); const [editingValue, setEditingValue] = useState>({}); const [saving, setSaving] = useState>({}); // -- Search & Filter -- const [searchQuery, setSearchQuery] = useState(''); const [severityFilter, setSeverityFilter] = useState('all'); const [sortBy, setSortBy] = useState<'severity' | 'name' | 'type'>('severity'); // -- Expand & Confirm -- const [expandedRuleId, setExpandedRuleId] = useState(null); const [confirmToggle, setConfirmToggle] = useState(null); /* ---------------------------------------------------------------- */ /* Fetch rules */ /* ---------------------------------------------------------------- */ const fetchRules = useCallback(async (dbType: DbType) => { try { setLoading(true); const res = await request.get('/audit-rules', { params: { db_type: dbType }, }); const data = (res as { data?: unknown[] }).data ?? (res as unknown as unknown[]); setRules(Array.isArray(data) ? data as AuditRule[] : []); } catch { setRules([]); } finally { setLoading(false); } }, []); useEffect(() => { fetchRules(activeTab); setSearchQuery(''); setSeverityFilter('all'); setExpandedRuleId(null); }, [activeTab, fetchRules]); /* ---------------------------------------------------------------- */ /* Handlers */ /* ---------------------------------------------------------------- */ const handleSeed = useCallback(async () => { try { setSeeding(true); await request.post(`/audit-rules/seed/${activeTab}`); fetchRules(activeTab); } catch (err: unknown) { const e = err as { response?: { data?: { detail?: string } } }; alert(e.response?.data?.detail || '初始化失败'); } finally { setSeeding(false); } }, [activeTab, fetchRules]); const handleToggle = useCallback(async (rule: AuditRule) => { if (confirmToggle !== rule.id) { setConfirmToggle(rule.id); return; } try { await request.post(`/audit-rules/${rule.id}/toggle`); setRules((prev) => prev.map((r) => (r.id === rule.id ? { ...r, is_enabled: !r.is_enabled } : r)), ); setConfirmToggle(null); } catch { /* ignore */ } }, [confirmToggle]); const handleSaveValue = useCallback(async (rule: AuditRule) => { const newVal = editingValue[rule.id]; if (newVal === undefined || newVal === rule.value) return; try { setSaving((prev) => ({ ...prev, [rule.id]: true })); await request.put(`/audit-rules/${rule.id}`, { value: newVal }); setRules((prev) => prev.map((r) => (r.id === rule.id ? { ...r, value: newVal } : r)), ); setEditingValue((prev) => { const next = { ...prev }; delete next[rule.id]; return next; }); } catch { /* ignore */ } finally { setSaving((prev) => ({ ...prev, [rule.id]: false })); } }, [editingValue]); /* ---------------------------------------------------------------- */ /* Derived: filtered & grouped rules */ /* ---------------------------------------------------------------- */ const filteredRules = useMemo(() => { let result = [...rules]; // Search if (searchQuery.trim()) { const q = searchQuery.toLowerCase(); result = result.filter( (r) => r.name.toLowerCase().includes(q) || r.message.toLowerCase().includes(q) || (r.description && r.description.toLowerCase().includes(q)), ); } // Severity filter if (severityFilter !== 'all') { result = result.filter((r) => r.severity === severityFilter); } // Sort return sortRules(result, sortBy); }, [rules, searchQuery, severityFilter, sortBy]); const groupedRules = useMemo(() => { const groups: Record = { DDL: [], DML: [], DCL: [], DQL: [], security: [], performance: [], }; for (const rule of filteredRules) { const group = getRuleGroup(rule); groups[group].push(rule); } return groups; }, [filteredRules]); /* ---------------------------------------------------------------- */ /* Render helpers */ /* ---------------------------------------------------------------- */ const renderRuleCard = (rule: AuditRule) => { const hasValue = rule.value !== null && rule.value !== undefined && rule.value !== ''; const currentEditVal = editingValue[rule.id]; const displayValue = currentEditVal !== undefined ? currentEditVal : rule.value ?? ''; const isExpanded = expandedRuleId === rule.id; const severityInfo = SEVERITY_CONFIG[rule.severity] || SEVERITY_CONFIG.info; const isConfirmingToggle = confirmToggle === rule.id; return (
{/* Main row */}
setExpandedRuleId(isExpanded ? null : rule.id)} > {/* Toggle */} {/* Severity dot */} {/* Name + Message */}

{rule.name}

{severityInfo.label}

{rule.message}

{/* Value input */} {hasValue && (
e.stopPropagation()}> = setEditingValue((prev) => ({ ...prev, [rule.id]: e.target.value })) } onBlur={() => handleSaveValue(rule)} onKeyDown={(e) => { if (e.key === 'Enter') handleSaveValue(rule); }} disabled={saving[rule.id]} className="w-20 px-2 py-1 text-sm font-mono text-center rounded-md border border-[var(--dg-border)] bg-[var(--dg-bg-primary)] text-[var(--dg-text-primary)] focus:border-[var(--dg-blue)] focus:outline-none focus:ring-1 focus:ring-[var(--dg-input-focus-ring)] disabled:opacity-50 transition-all" /> {saving[rule.id] && }
)} {/* Expand icon */} {isExpanded ? ( ) : ( )}
{/* Toggle confirmation */} {isConfirmingToggle && (
确认{rule.is_enabled ? '禁用' : '启用'}规则 "{rule.name}"?
)} {/* Expanded details */} {isExpanded && (
{/* Description */} {rule.description && (
规则说明

{rule.description}

)} {/* Message */}
触发消息

{rule.message}

{/* Pattern */} {rule.pattern && (
匹配模式 {rule.pattern}
)} {/* Metadata */}
类型: {rule.rule_type.toUpperCase()} 更新于: {new Date(rule.updated_at).toLocaleDateString()}
)}
); }; const renderRuleGroup = (group: RuleGroup, groupRules: AuditRule[]) => { if (groupRules.length === 0) return null; const config = GROUP_CONFIG[group]; const GroupIcon = config.icon; return (

{config.label} ({groupRules.length})

{groupRules.map((rule) => renderRuleCard(rule))}
); }; /* ---------------------------------------------------------------- */ /* Main render */ /* ---------------------------------------------------------------- */ return (
{/* Header */}

审核规则

按数据库类型管理 SQL 审核规则模版,支持搜索、筛选和排序

{/* DB Type Tabs */}
{DB_TABS.map((tab) => { const TabIcon = tab.icon; return ( ); })}
{/* Search & Filter Bar */}
{/* Search */}
setSearchQuery(e.target.value)} placeholder="搜索规则名称、消息或描述..." className="w-full pl-9 pr-3 py-2 bg-[var(--dg-bg-primary)] border border-[var(--dg-border)] rounded-lg text-sm text-[var(--dg-text-primary)] placeholder:text-[var(--dg-text-muted)] focus:outline-none focus:border-blue-500 transition-colors" /> {searchQuery && ( )}
{/* Severity filter */}
{([ { value: 'all', label: '全部' }, { value: 'error', label: '错误' }, { value: 'warning', label: '警告' }, { value: 'info', label: '提示' }, ] as { value: SeverityFilter; label: string }[]).map((opt) => ( ))}
{/* Sort */}
排序:
{/* Results summary */} {(searchQuery || severityFilter !== 'all') && (
共 {filteredRules.length} 条规则 {searchQuery && 匹配 "{searchQuery}"} {severityFilter !== 'all' && 严重级别: {SEVERITY_CONFIG[severityFilter]?.label || severityFilter}}
)} {/* Rule List */} {loading ? (
加载中...
) : rules.length === 0 ? (

当前没有 {DB_TABS.find((t) => t.key === activeTab)?.label} 的审核规则

) : filteredRules.length === 0 ? (

没有匹配的规则

) : (
{renderRuleGroup('DDL', groupedRules.DDL)} {renderRuleGroup('DML', groupedRules.DML)} {renderRuleGroup('security', groupedRules.security)} {renderRuleGroup('DCL', groupedRules.DCL)} {renderRuleGroup('DQL', groupedRules.DQL)} {renderRuleGroup('performance', groupedRules.performance)}
)}
); } /* Small icon component */ function MessageSquareIcon(props: React.SVGProps & { className?: string }) { return ( ); }