('/db-types/' + dbType),
};
diff --git a/frontend/src/components/EditInstanceDialog.tsx b/frontend/src/components/EditInstanceDialog.tsx
new file mode 100644
index 0000000..9a28a7e
--- /dev/null
+++ b/frontend/src/components/EditInstanceDialog.tsx
@@ -0,0 +1,186 @@
+import { useState, useCallback, useEffect } from 'react';
+import { X, Loader2 } from 'lucide-react';
+import { instanceApi } from '@/api/instance';
+
+/* ------------------------------------------------------------------ */
+/* Types */
+/* ------------------------------------------------------------------ */
+interface InstanceData {
+ id: number;
+ name: string;
+ db_type: string;
+ host: string;
+ port: number;
+ username: string;
+ database: string;
+ env: string;
+ access_mode: string;
+}
+
+interface Props {
+ instance: InstanceData;
+ open: boolean;
+ onClose: () => void;
+ onSaved: () => void;
+}
+
+/* ------------------------------------------------------------------ */
+/* Reusable Edit Instance Dialog */
+/* ------------------------------------------------------------------ */
+export default function EditInstanceDialog({ instance, open, onClose, onSaved }: Props) {
+ const [name, setName] = useState(instance.name);
+ const [host, setHost] = useState(instance.host);
+ const [port, setPort] = useState(String(instance.port));
+ const [username, setUsername] = useState(instance.username);
+ const [password, setPassword] = useState('');
+ const [database, setDatabase] = useState(instance.database || '');
+ const [env, setEnv] = useState(instance.env || 'development');
+ const [accessMode, setAccessMode] = useState(instance.access_mode || 'readwrite');
+ const [submitting, setSubmitting] = useState(false);
+ const [error, setError] = useState('');
+
+ /* Sync when instance changes */
+ useEffect(() => {
+ setName(instance.name);
+ setHost(instance.host);
+ setPort(String(instance.port));
+ setUsername(instance.username);
+ setPassword('');
+ setDatabase(instance.database || '');
+ setEnv(instance.env || 'development');
+ setAccessMode(instance.access_mode || 'readwrite');
+ setError('');
+ }, [instance]);
+
+ const handleSubmit = useCallback(async () => {
+ if (!name.trim()) { setError('请填写名称'); return; }
+ if (!host.trim()) { setError('请填写地址'); return; }
+ setSubmitting(true); setError('');
+ try {
+ await instanceApi.update(instance.id, {
+ name: name.trim(),
+ host: host.trim(),
+ port: Number(port),
+ username: username.trim() || undefined,
+ password: password || undefined,
+ database: database.trim() || undefined,
+ env,
+ access_mode: accessMode,
+ });
+ onSaved();
+ } catch (e: unknown) {
+ const msg = (e as { response?: { data?: { detail?: string } } })?.response?.data?.detail || '保存失败';
+ setError(msg);
+ } finally {
+ setSubmitting(false);
+ }
+ }, [name, host, port, username, password, database, env, accessMode, instance.id, onSaved]);
+
+ if (!open) return null;
+
+ const inputCls = 'w-full px-3 py-2 bg-[var(--dg-bg-tertiary)] border border-[var(--dg-border)] rounded-lg text-sm focus:border-blue-400 focus:outline-none transition-colors';
+ const labelCls = 'block text-xs font-medium text-[var(--dg-text-secondary)] mb-1';
+
+ return (
+
+
e.stopPropagation()}
+ >
+ {/* Header */}
+
+
编辑数据源
+
+
+
+ {/* Body */}
+
+ {error && (
+
{error}
+ )}
+
+ {/* Name */}
+
+
+ setName(e.target.value)} placeholder="实例名称" />
+
+
+ {/* Host + Port */}
+
+
+ {/* Username + Password */}
+
+
+ {/* Database */}
+
+
+ setDatabase(e.target.value)} placeholder="可选" />
+
+
+ {/* Env + Access Mode */}
+
+
+
+
+
+
+
+
+
+
+
+
+ {/* Footer */}
+
+
+
+
+
+
+ );
+}
diff --git a/frontend/src/lib/mock.ts b/frontend/src/lib/mock.ts
index b18869e..178252b 100644
--- a/frontend/src/lib/mock.ts
+++ b/frontend/src/lib/mock.ts
@@ -25,8 +25,7 @@ export const menuItems = [
{ id: 'instances', label: '数据源', icon: 'Database', section: 'datasource', roles: ['admin', 'dba'] },
{ id: 'instance-groups', label: '权限管理', icon: 'Lock', section: 'permission', roles: ['admin'] },
{ id: 'tickets', label: '数据变更', icon: 'ClipboardList', section: 'ticket', roles: ['admin', 'dba', 'developer'] },
- { id: 'queries', label: '查询审计', icon: 'Search', section: 'ticket', roles: ['admin', 'dba', 'developer', 'guest'] },
- { id: 'slow-query', label: '慢查询分析', icon: 'Timer', section: 'ticket', roles: ['admin', 'dba'] },
+ { id: 'queries', label: '查询数据', icon: 'Search', section: 'ticket', roles: ['admin', 'dba', 'developer', 'guest'] },
{ id: 'roles', label: '角色权限', icon: 'UserCog', section: 'permission', roles: ['admin'] },
{ id: 'users', label: '用户管理', icon: 'Users', section: 'permission', roles: ['admin'] },
{ id: 'audit-logs', label: '审计日志', icon: 'FileText', section: 'system', roles: ['admin', 'dba'] },
@@ -192,7 +191,7 @@ export const permissionTreeData: PermissionNode[] = [
{ id: 'ticket_execute', label: '执行工单' },
{ id: 'ticket_rollback', label: '回滚工单' },
]},
- { id: 'queries', label: '查询审计(查看日志、新建查询、导出)', children: [
+ { id: 'queries', label: '查询数据(查看日志、新建查询、导出)', children: [
{ id: 'query_create', label: '执行查询' },
{ id: 'query_export', label: '导出结果' },
{ id: 'query_view_all', label: '查看全部用户查询' },
diff --git a/frontend/src/pages/AuditLogsPage.tsx b/frontend/src/pages/AuditLogsPage.tsx
index 766c01f..51ab4a1 100644
--- a/frontend/src/pages/AuditLogsPage.tsx
+++ b/frontend/src/pages/AuditLogsPage.tsx
@@ -493,9 +493,9 @@ export default function AuditLogsPage() {
- {log.username.charAt(0).toUpperCase()}
+ {(log.username || '?').charAt(0).toUpperCase()}
- {log.username}
+ {log.username || '-'}
|
diff --git a/frontend/src/pages/AuditRulesPage.tsx b/frontend/src/pages/AuditRulesPage.tsx
index d75daf3..5b11f51 100644
--- a/frontend/src/pages/AuditRulesPage.tsx
+++ b/frontend/src/pages/AuditRulesPage.tsx
@@ -26,7 +26,7 @@ interface AuditRule {
}
type DbType = 'mysql' | 'postgresql' | 'redis' | 'mongodb';
-type RuleGroup = 'DDL' | 'DML' | 'DCL' | 'DQL';
+type RuleGroup = 'DDL' | 'DML' | 'DCL' | 'DQL' | 'security' | 'performance';
type SeverityFilter = 'all' | 'error' | 'warning' | 'info';
/* ------------------------------------------------------------------ */
@@ -51,6 +51,8 @@ const GROUP_CONFIG: Record = {
@@ -59,34 +61,17 @@ const SEVERITY_CONFIG: Record upper.includes(kw))) return 'DDL';
- if (DML_KEYWORDS.some((kw) => upper.includes(kw))) return 'DML';
- if (DCL_KEYWORDS.some((kw) => upper.includes(kw))) return 'DCL';
- if (DQL_KEYWORDS.some((kw) => upper.includes(kw))) return 'DQL';
- // MongoDB security rules -> DCL, performance rules -> DQL
- if (upper.includes('SECURITY') || upper.includes('安全') || upper.includes('审计') || upper.includes('注入') || upper.includes('EVAL') || upper.includes('MAPSQL_REDUCE')) return 'DCL';
- if (upper.includes('PERFORMANCE') || upper.includes('性能') || upper.includes('索引') || upper.includes('扫描') || upper.includes('分片') || upper.includes('SHARD')) return 'DQL';
- // Default to DDL for structural rules, DML for data rules
- if (upper.includes('WHERE') || upper.includes('写入') || upper.includes('UPDATE') || upper.includes('DELETE')) return 'DML';
+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';
}
@@ -99,7 +84,7 @@ function sortRules(rules: AuditRule[], sortBy: 'severity' | 'name' | 'type'): Au
case 'name':
return sorted.sort((a, b) => a.name.localeCompare(b.name, 'zh-CN'));
case 'type':
- return sorted.sort((a, b) => getRuleGroup(a.name).localeCompare(getRuleGroup(b.name)));
+ return sorted.sort((a, b) => getRuleGroup(a).localeCompare(getRuleGroup(b)));
default:
return sorted;
}
@@ -239,9 +224,11 @@ export default function AuditRulesPage() {
DML: [],
DCL: [],
DQL: [],
+ security: [],
+ performance: [],
};
for (const rule of filteredRules) {
- const group = getRuleGroup(rule.name);
+ const group = getRuleGroup(rule);
groups[group].push(rule);
}
return groups;
@@ -580,8 +567,10 @@ export default function AuditRulesPage() {
{renderRuleGroup('DDL', groupedRules.DDL)}
{renderRuleGroup('DML', groupedRules.DML)}
+ {renderRuleGroup('security', groupedRules.security)}
{renderRuleGroup('DCL', groupedRules.DCL)}
{renderRuleGroup('DQL', groupedRules.DQL)}
+ {renderRuleGroup('performance', groupedRules.performance)}
)}
diff --git a/frontend/src/pages/InstancePage.tsx b/frontend/src/pages/InstancePage.tsx
index 136dde4..35c62b5 100644
--- a/frontend/src/pages/InstancePage.tsx
+++ b/frontend/src/pages/InstancePage.tsx
@@ -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 = {
- 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 = {
@@ -132,6 +136,8 @@ export default function InstancePage() {
const [instances, setInstances] = useState([]);
const [loading, setLoading] = useState(false);
const [showCreate, setShowCreate] = useState(false);
+ const [showEdit, setShowEdit] = useState(false);
+ const [editingInstance, setEditingInstance] = useState(null);
const [selectedIds, setSelectedIds] = useState>(new Set());
const [batchDeleting, setBatchDeleting] = useState(false);
@@ -242,6 +248,28 @@ export default function InstancePage() {
);
}
+ if (showEdit && editingInstance) {
+ return (
+ <>
+ { 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={() => {}}
+ />
+ { 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 ? (
) : layoutMode === 'grid' ? (
-
+
{filtered.map(instance => {
const ic = dbTypeConfig[instance.db_type] || dbTypeConfig.mysql;
const IconComp = ic.icon;
@@ -438,7 +467,7 @@ export default function InstancePage() {
return (
{ setSelectedInstance(instance); setView('detail'); }}
@@ -456,8 +485,8 @@ export default function InstancePage() {
-
-
+
+
@@ -472,14 +501,14 @@ export default function InstancePage() {
- {instance.name}
- {instance.host}:{instance.port}
-
+ {instance.name}
+ {instance.host}:{instance.port}
+
{ic.label} {instance.version || ''}
{/* Health Score */}
-
+
@@ -495,35 +524,33 @@ export default function InstancePage() {
{/* Metrics */}
-
-
+
+
- {instance.connections}
+ {instance.connections}
- 连接
+ 连接
-
+
- {instance.qps ? instance.qps.toLocaleString() : '-'}
+ {instance.qps ? instance.qps.toLocaleString() : '-'}
- QPS
+ QPS
-
+
- {instance.latency}ms
+ {instance.latency}ms
- 延迟
+ 延迟
- {instance.description && (
- {instance.description}
- )}
-
+
+
查看详情
@@ -584,7 +611,7 @@ export default function InstancePage() {
| {instance.name} |
-
+
{ic.label}
|
@@ -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 ? '测试中...' : '测试连接'}
{canEdit && (
- |