feat: Redis查询结果优化 — 复制按钮+JSON自动格式化+语义化列名
1. GET/HGET/TTL等单值命令: columns改为[key,value]替代[command,result] 2. DBSIZE等无key命令保持[command,result]格式 3. 前端添加复制按钮(Clipboard API), 2秒反馈 4. JSON值自动检测并格式化显示(whitespace-pre-wrap) 5. 结果区最大高度350px→500px 6. cost_ms字段兼容处理
This commit is contained in:
1 parent
828a0597c5
commit
18430f3717
2 files changed
+77
-14
No files matched your search
@@ -702,11 +702,25 @@ func executeRedisQuery(instance *model.Instance, dbSelect string, command string
|
||||
}
|
||||
columns = []string{"field", "value"}
|
||||
} else {
|
||||
rows = append(rows, map[string]interface{}{
|
||||
"command": command,
|
||||
"result": fmt.Sprintf("%v", v),
|
||||
})
|
||||
columns = []string{"command", "result"}
|
||||
// Single value result (GET, HGET, TYPE, TTL, etc.)
|
||||
// Try to extract key name from command
|
||||
keyName := ""
|
||||
if len(parts) > 1 {
|
||||
keyName = parts[1]
|
||||
}
|
||||
if keyName != "" {
|
||||
rows = append(rows, map[string]interface{}{
|
||||
"key": keyName,
|
||||
"value": fmt.Sprintf("%v", v),
|
||||
})
|
||||
columns = []string{"key", "value"}
|
||||
} else {
|
||||
rows = append(rows, map[string]interface{}{
|
||||
"command": command,
|
||||
"result": fmt.Sprintf("%v", v),
|
||||
})
|
||||
columns = []string{"command", "result"}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ import {
|
||||
StarOff, Download, Search, ArrowUpDown, Database, Table2,
|
||||
ArrowUp, ArrowDown, Eye, Hash, PanelLeftClose, Columns3,
|
||||
KeyRound, FolderOpen, Server, Zap, ClipboardPaste,
|
||||
History, PanelRightClose, PanelRightOpen,
|
||||
History, PanelRightClose, PanelRightOpen, Copy, Check,
|
||||
} from 'lucide-react';
|
||||
// Card components replaced by design system utility classes
|
||||
import { useActionPermission } from '@/hooks/useAuth';
|
||||
@@ -264,6 +264,46 @@ export default function QueriesPage() {
|
||||
downloadCSV(`query_result_${new Date().toISOString().slice(0, 19).replace(/:/g, '-')}.csv`, exportToCSV(result.columns, result.rows));
|
||||
}, [result]);
|
||||
|
||||
/* ---- Copy Result ---- */
|
||||
const [copied, setCopied] = useState(false);
|
||||
const handleCopyResult = useCallback(() => {
|
||||
if (!result) return;
|
||||
let text = '';
|
||||
if (result.rows?.length && result.columns?.length) {
|
||||
// Format as readable text
|
||||
const lines = result.rows.map((row: Record<string, unknown>) =>
|
||||
result.columns.map((col: string) => {
|
||||
const val = String(row[col] ?? '');
|
||||
// Try to pretty-format JSON values
|
||||
try {
|
||||
const parsed = JSON.parse(val);
|
||||
if (typeof parsed === 'object') return JSON.stringify(parsed, null, 2);
|
||||
} catch { /* not JSON */ }
|
||||
return val;
|
||||
}).join('\t')
|
||||
);
|
||||
text = [result.columns.join('\t'), ...lines].join('\n');
|
||||
} else {
|
||||
text = JSON.stringify(result, null, 2);
|
||||
}
|
||||
navigator.clipboard.writeText(text).then(() => {
|
||||
setCopied(true);
|
||||
setTimeout(() => setCopied(false), 2000);
|
||||
});
|
||||
}, [result]);
|
||||
|
||||
/* ---- Smart cell value formatter (detect JSON) ---- */
|
||||
const formatCellValue = useCallback((val: unknown) => {
|
||||
const str = String(val ?? '');
|
||||
try {
|
||||
const parsed = JSON.parse(str);
|
||||
if (typeof parsed === 'object' && parsed !== null) {
|
||||
return { text: JSON.stringify(parsed, null, 2), isJson: true };
|
||||
}
|
||||
} catch { /* not JSON */ }
|
||||
return { text: str, isJson: false };
|
||||
}, []);
|
||||
|
||||
/* ---- History ---- */
|
||||
const toggleFavorite = useCallback((id: number) => {
|
||||
setHistory(prev => {
|
||||
@@ -439,21 +479,27 @@ export default function QueriesPage() {
|
||||
查询结果
|
||||
</span>
|
||||
<div className="flex items-center gap-3 text-xs dg-text-secondary">
|
||||
<span className="flex items-center gap-1"><Clock className="w-3 h-3" />{result.execution_time_ms}ms</span>
|
||||
<span className="flex items-center gap-1"><Clock className="w-3 h-3" />{result.execution_time_ms || result.cost_ms || 0}ms</span>
|
||||
<span>{result.rows?.length || 0} 行</span>
|
||||
<span>{result.columns?.length || 0} 列</span>
|
||||
{result.rows && result.rows.length > 0 && (
|
||||
<button onClick={handleExportResult}
|
||||
className="dg-btn dg-btn-ghost dg-btn-sm">
|
||||
<Download className="w-3 h-3" />导出 CSV
|
||||
</button>
|
||||
<>
|
||||
<button onClick={handleCopyResult}
|
||||
className="dg-btn dg-btn-ghost dg-btn-sm gap-1">
|
||||
{copied ? <><Check className="w-3 h-3 text-green-400" /><span className="text-green-400">已复制</span></> : <><Copy className="w-3 h-3" />复制</>}
|
||||
</button>
|
||||
<button onClick={handleExportResult}
|
||||
className="dg-btn dg-btn-ghost dg-btn-sm">
|
||||
<Download className="w-3 h-3" />导出 CSV
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="p-0">
|
||||
{result.columns && result.columns.length > 0 ? (
|
||||
<div ref={resultContainerRef} onScroll={handleResultScroll} className="overflow-auto max-h-[350px]">
|
||||
<div ref={resultContainerRef} onScroll={handleResultScroll} className="overflow-auto max-h-[500px]">
|
||||
<table className="w-full text-sm">
|
||||
<thead className="sticky top-0 z-10">
|
||||
<tr className="dg-table-header">
|
||||
@@ -481,9 +527,12 @@ export default function QueriesPage() {
|
||||
{result.columns.map(col => {
|
||||
const isSensitive = sensitiveKeywords.some(k => col.toLowerCase().includes(k));
|
||||
const cellValue = row[col] ?? '';
|
||||
const { text: displayText, isJson } = formatCellValue(cellValue);
|
||||
return (
|
||||
<td key={col} className="px-3 py-2 text-xs max-w-[200px] truncate" title={String(cellValue)}>
|
||||
{isSensitive ? <span className="text-yellow-400 italic">{maskData(String(cellValue))}</span> : String(cellValue)}
|
||||
<td key={col} className={`px-3 py-2 text-xs ${isJson ? 'max-w-[400px] whitespace-pre-wrap break-all font-mono text-[11px] leading-relaxed' : 'max-w-[200px] truncate'}`} title={String(cellValue)}>
|
||||
{isSensitive ? <span className="text-yellow-400 italic">{maskData(String(cellValue))}</span> :
|
||||
isJson ? <span className="text-blue-300">{displayText}</span> :
|
||||
String(cellValue)}
|
||||
</td>
|
||||
);
|
||||
})}
|
||||
|
||||
Reference in new issue
Block a user