feat: 查询页增强 + 权限体系优化
- Redis DB0~DB15 下拉选择,默认DB0 - 查询历史粘贴按钮(一键填入SQL编辑器) - 去掉导出历史按钮 - SchemaSidebar 按目标数据库筛选 - SQL自动补全 schema 双层注册(db→tables, table→columns) - 后端 db-schema 接口(INFORMATION_SCHEMA.COLUMNS 一次加载) - autocompletion 扩展激活 - Redis key 树形结构(冒号分隔层级) - Redis 查询模板 GET/HGETALL/HGET - guest 角色彻底移除,改为 auditor - 登录页优化:去掉演示账号、placeholder 中文提示
This commit is contained in:
1 parent
aa2e26c827
commit
35a014e571
13 files changed
+345
-111
No files matched your search
@@ -4,7 +4,7 @@ import {
|
||||
ChevronDown, ChevronRight, Clock, FileText, Sparkles, Star,
|
||||
StarOff, Download, Search, ArrowUpDown, Database, Table2,
|
||||
ArrowUp, ArrowDown, Eye, Hash, PanelLeftClose, Columns3,
|
||||
KeyRound, FolderOpen, Server, Zap,
|
||||
KeyRound, FolderOpen, Server, Zap, ClipboardPaste,
|
||||
} from 'lucide-react';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { useActionPermission } from '@/hooks/useAuth';
|
||||
@@ -294,6 +294,7 @@ function NewQueryPanel({
|
||||
const [databases, setDatabases] = useState<string[]>([]);
|
||||
const [loadingDbs, setLoadingDbs] = useState(false);
|
||||
const [instances, setInstances] = useState<Array<{ id: number; name: string; host: string; port: number; db_type: string }>>([]);
|
||||
const [editorSchema, setEditorSchema] = useState<Record<string, Record<string, string[]>>>({});
|
||||
const [executing, setExecuting] = useState(false);
|
||||
const [error, setError] = useState('');
|
||||
const [result, setResult] = useState<QueryResult | null>(null);
|
||||
@@ -314,19 +315,40 @@ function NewQueryPanel({
|
||||
}).catch(() => { setInstances([]); });
|
||||
}, []);
|
||||
|
||||
// 选实例后加载数据库列表
|
||||
// 选实例后加载数据库列表 + 表名(用于 SQL 自动补全)
|
||||
useEffect(() => {
|
||||
if (!instanceId) { setDatabases([]); setDatabase(''); return; }
|
||||
const inst = instances.find(i => String(i.id) === instanceId);
|
||||
if (!instanceId) { setDatabases([]); setDatabase(''); setEditorSchema({}); return; }
|
||||
const inst = instances.find(i => i.id === Number(instanceId));
|
||||
if (inst?.db_type === 'redis') {
|
||||
setDatabases([]); setDatabase('0'); setEditorSchema({}); return;
|
||||
}
|
||||
const type = inst?.db_type || '';
|
||||
if (type === 'mysql' || type === 'postgres' || type === 'postgresql') {
|
||||
setLoadingDbs(true);
|
||||
instanceDetailApi.listDatabases(Number(instanceId))
|
||||
.then(data => setDatabases(Array.isArray(data) ? data : []))
|
||||
.catch(() => setDatabases([]))
|
||||
.then(async data => {
|
||||
const dbs = Array.isArray(data) ? data : [];
|
||||
setDatabases(dbs);
|
||||
// 快速加载所有库的表名(不含列名)
|
||||
const schema: Record<string, Record<string, string[]>> = {};
|
||||
await Promise.all(dbs.map(async db => {
|
||||
try {
|
||||
const tables = await instanceDetailApi.listTables(Number(instanceId), db);
|
||||
if (Array.isArray(tables)) {
|
||||
schema[db] = {};
|
||||
for (const t of tables) {
|
||||
schema[db][t.name] = [];
|
||||
}
|
||||
}
|
||||
} catch { /* ignore */ }
|
||||
}));
|
||||
setEditorSchema(schema);
|
||||
})
|
||||
.catch(() => { setDatabases([]); setEditorSchema({}); })
|
||||
.finally(() => setLoadingDbs(false));
|
||||
} else {
|
||||
setDatabases([]);
|
||||
setEditorSchema({});
|
||||
}
|
||||
}, [instanceId, instances]);
|
||||
|
||||
@@ -337,6 +359,23 @@ function NewQueryPanel({
|
||||
const dbType = (selectedInstance?.db_type || 'mysql') as DBType;
|
||||
const editorLang = dbType === 'redis' ? 'redis' as const : dbType === 'mongodb' ? 'mongodb' as const : 'sql' as const;
|
||||
|
||||
// 选了目标数据库后,加载该库的完整列名用于补全
|
||||
useEffect(() => {
|
||||
if (!instanceId || !database || (dbType !== 'mysql' && dbType !== 'postgres' && dbType !== 'postgresql')) return;
|
||||
// 排除 Redis 残留的纯数字 database(如 "0")
|
||||
if (/^\d+$/.test(database)) return;
|
||||
instanceDetailApi.getDbSchema(Number(instanceId), database)
|
||||
.then(data => {
|
||||
if (data && typeof data === 'object') {
|
||||
setEditorSchema(prev => ({
|
||||
...prev,
|
||||
[database]: data as Record<string, string[]>,
|
||||
}));
|
||||
}
|
||||
})
|
||||
.catch(() => { /* ignore */ });
|
||||
}, [instanceId, database, dbType]);
|
||||
|
||||
const templates = useMemo(() => getTemplatesForType(dbType), [dbType]);
|
||||
const templateCategories = useMemo(() => {
|
||||
const cats = new Set(templates.map(t => t.category));
|
||||
@@ -459,20 +498,6 @@ function NewQueryPanel({
|
||||
downloadCSV(`query_result_${new Date().toISOString().slice(0, 19).replace(/:/g, '-')}.csv`, csv);
|
||||
}, [result]);
|
||||
|
||||
const handleExportHistory = useCallback(() => {
|
||||
const columns = ['序号', 'SQL', '数据库类型', '执行时长(ms)', '状态', '时间'];
|
||||
const rows = history.map((h, i) => ({
|
||||
序号: i + 1,
|
||||
SQL: h.sql,
|
||||
数据库类型: h.db_type,
|
||||
'执行时长(ms)': h.execution_time_ms,
|
||||
状态: h.status === 'success' ? '成功' : '失败',
|
||||
时间: new Date(h.created_at).toLocaleString(),
|
||||
}));
|
||||
const csv = exportToCSV(columns, rows);
|
||||
downloadCSV(`query_history_${new Date().toISOString().slice(0, 19).replace(/:/g, '-')}.csv`, csv);
|
||||
}, [history]);
|
||||
|
||||
const catLabel: Record<string, string> = {
|
||||
all: '全部', '表结构': '表结构', '性能分析': '性能分析', '数据统计': '数据统计', '基础查询': '基础查询',
|
||||
};
|
||||
@@ -490,15 +515,6 @@ function NewQueryPanel({
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
{history.length > 0 && (
|
||||
<button
|
||||
onClick={handleExportHistory}
|
||||
className="flex items-center gap-2 px-3 py-2 text-xs text-[var(--dg-text-secondary)] bg-[var(--dg-bg-tertiary)] border border-[var(--dg-border)] rounded-lg hover:bg-[var(--dg-bg-hover)] transition-colors"
|
||||
>
|
||||
<Download className="w-3.5 h-3.5" />
|
||||
导出历史
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
onClick={() => setShowHistory(!showHistory)}
|
||||
className={`flex items-center gap-2 px-3 py-2 text-xs rounded-lg border transition-colors ${
|
||||
@@ -549,6 +565,13 @@ function NewQueryPanel({
|
||||
>
|
||||
{h.favorite ? <Star className="w-3 h-3 fill-current" /> : <StarOff className="w-3 h-3" />}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setSql(h.sql)}
|
||||
className="p-1 rounded text-[var(--dg-text-muted)] hover:text-blue-400 transition-colors"
|
||||
title="粘贴到编辑器"
|
||||
>
|
||||
<ClipboardPaste className="w-3 h-3" />
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
@@ -576,6 +599,17 @@ function NewQueryPanel({
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium mb-2">目标数据库</label>
|
||||
{dbType === 'redis' ? (
|
||||
<select
|
||||
value={database || '0'}
|
||||
onChange={e => setDatabase(e.target.value)}
|
||||
className="w-full px-4 py-2.5 bg-[var(--dg-bg-tertiary)] border border-[var(--dg-border)] rounded-lg text-sm focus:border-blue-400 focus:outline-none transition-colors"
|
||||
>
|
||||
{Array.from({ length: 16 }, (_, i) => (
|
||||
<option key={i} value={String(i)}>DB{i}</option>
|
||||
))}
|
||||
</select>
|
||||
) : (
|
||||
<select
|
||||
value={database}
|
||||
onChange={e => setDatabase(e.target.value)}
|
||||
@@ -586,6 +620,7 @@ function NewQueryPanel({
|
||||
<option key={db} value={db}>{db}</option>
|
||||
))}
|
||||
</select>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -696,6 +731,7 @@ function NewQueryPanel({
|
||||
language={editorLang}
|
||||
height="240px"
|
||||
onExecute={handleExecute}
|
||||
schema={(dbType === 'mysql' || dbType === 'postgres' || dbType === 'postgresql') ? editorSchema : undefined}
|
||||
/>
|
||||
<div className="flex items-center justify-between px-4 py-2 bg-[var(--dg-bg-tertiary)] border-t border-[var(--dg-border)] text-xs text-[var(--dg-text-muted)]">
|
||||
<span className="flex items-center gap-1.5">
|
||||
|
||||
Reference in new issue
Block a user