feat: ARDM风格查询页优化 — 可拖拽Sidebar + Redis差异化结果展示 + KeyHeader\n\n1. Sidebar可拖拽宽度(200-600px, localStorage持久化)\n2. Redis raw_text展示加ARDM KeyHeader: key名+类型标签+TTL+格式标签+复制按钮\n3. HGETALL结果用field/value紧凑双列展示替代通用表格\n4. 后端raw_text模式自动查询key TYPE和TTL返回raw_type/raw_ttl

This commit is contained in:
logan committed 2026-05-30 18:20:22 +08:00
1 parent 4e6f9eb8fd
commit 433e7e2909
2 files changed
+122 -16

No files matched your search

+23 -2
View File
@@ -915,14 +915,35 @@ func executeRedisQuery(instance *model.Instance, dbSelect string, command string
prettyJSON = strings.TrimRight(buf.String(), "\n")
}
}
return gin.H{
// ARDM-style: auto-fetch key type and TTL for KeyHeader
var keyType, keyTTL string
if keyName != "" && cmd != "TYPE" && cmd != "TTL" && cmd != "PTTL" && cmd != "EXISTS" && cmd != "DBSIZE" && cmd != "INFO" {
if t, err := client.Type(ctx, keyName).Result(); err == nil {
keyType = t
}
if ttl, err := client.TTL(ctx, keyName).Result(); err == nil {
if ttl < 0 {
keyTTL = "永不过期"
} else {
keyTTL = fmt.Sprintf("%vs", int(ttl.Seconds()))
}
}
}
result := gin.H{
"columns": []string{},
"rows": []interface{}{},
"total": 0,
"raw_value": prettyJSON != "",
"raw_key": keyName,
"raw_text": rawValue,
}, nil
}
if keyType != "" {
result["raw_type"] = keyType
}
if keyTTL != "" {
result["raw_ttl"] = keyTTL
}
return result, nil
}
}
+99 -14
View File
@@ -31,6 +31,8 @@ interface QueryResult {
raw_value?: boolean;
raw_key?: string;
raw_text?: string;
raw_type?: string;
raw_ttl?: string;
error?: string;
masked_columns?: string[];
}
@@ -132,6 +134,9 @@ export default function QueriesPage() {
const [rowHeight] = useState(36);
const [visibleRange, setVisibleRange] = useState({ start: 0, end: 50 });
const resultContainerRef = useRef<HTMLDivElement>(null);
// -- Sidebar 拖拽宽度 --
const [sideWidth, setSideWidth] = useState(() => Number(localStorage.getItem('query-side-width')) || 280);
const sidebarDragRef = useRef<HTMLDivElement>(null);
// -- 历史 state --
const [history, setHistory] = useState<QueryHistoryItem[]>(() => {
@@ -316,6 +321,28 @@ export default function QueriesPage() {
setHistory([]); setClearConfirm(false);
}, [clearConfirm]);
// -- Sidebar 拖拽 --
const handleDragStart = useCallback((e: React.MouseEvent) => {
e.preventDefault();
const startX = e.clientX;
const startWidth = sideWidth;
const onMove = (ev: MouseEvent) => {
const delta = ev.clientX - startX;
const next = Math.min(600, Math.max(200, startWidth + delta));
setSideWidth(next);
};
const onUp = () => {
document.removeEventListener('mousemove', onMove);
document.removeEventListener('mouseup', onUp);
setSideWidth(w => {
localStorage.setItem('query-side-width', String(w));
return w;
});
};
document.addEventListener('mousemove', onMove);
document.addEventListener('mouseup', onUp);
}, [sideWidth]);
const filteredHistory = useMemo(() => {
let r = history;
if (favFilter) r = r.filter(h => h.favorite);
@@ -435,16 +462,25 @@ export default function QueriesPage() {
<div className="flex-1 min-h-0 flex gap-0 overflow-hidden">
{/* Schema 侧边栏(左侧,独立) */}
{showSidebar && instanceId && (
<div className="w-[280px] shrink-0 border-r border-[var(--dg-border)] overflow-y-auto">
<SchemaSidebar instanceId={instanceId} database={database} dbType={dbType}
clusterMode={selectedInstance?.cluster_mode}
onInsert={(text) => {
const sep = dbType === 'redis' ? (sql.trim() ? '\\n' : '') : (sql.trim() ? ' ' : '');
setSql(prev => prev + sep + text + (dbType === 'redis' ? '' : ' '));
}}
onDatabaseChange={setDatabase}
singleDb={!!database} />
</div>
<>
<div className="shrink-0 border-r border-[var(--dg-border)] overflow-y-auto" style={{ width: sideWidth + 'px' }}>
<SchemaSidebar instanceId={instanceId} database={database} dbType={dbType}
clusterMode={selectedInstance?.cluster_mode}
onInsert={(text) => {
const sep = dbType === 'redis' ? (sql.trim() ? '\\n' : '') : (sql.trim() ? ' ' : '');
setSql(prev => prev + sep + text + (dbType === 'redis' ? '' : ' '));
}}
onDatabaseChange={setDatabase}
singleDb={!!database} />
</div>
<div
ref={sidebarDragRef}
className="w-1 shrink-0 cursor-col-resize hover:bg-blue-500/30 transition-colors relative group"
onMouseDown={handleDragStart}
>
<div className="absolute inset-y-0 -left-1 -right-1" />
</div>
</>
)}
{/* 编辑器 + 查询结果(右侧主体,垂直排列) */}
@@ -520,13 +556,35 @@ export default function QueriesPage() {
{showResult && (
<div className="flex-1 min-h-0 overflow-auto p-0">
{result.raw_text !== undefined ? (
/* Raw value display (Redis GET/HGET/TTL etc.) */
<div className="p-4">
/* Raw value display — ARDM-style FormatViewer */
<div className="p-4 flex flex-col gap-2">
{/* Key info bar (ARDM KeyHeader style) */}
{result.raw_key && (
<div className="text-xs text-[var(--dg-text-muted)] mb-2 font-mono">{result.raw_key}</div>
<div className="flex items-center gap-2 px-3 py-1.5 bg-[var(--dg-bg-tertiary)] rounded-lg text-xs">
<KeyRound className="w-3 h-3 text-blue-400 shrink-0" />
<span className="font-mono text-[var(--dg-text-primary)] truncate">{result.raw_key}</span>
{result.raw_type && (
<span className="px-1.5 py-0.5 rounded text-[10px] font-medium bg-green-500/15 text-green-400">{result.raw_type}</span>
)}
{result.raw_ttl && (
<span className="px-1.5 py-0.5 rounded text-[10px] font-medium bg-orange-500/15 text-orange-400">{result.raw_ttl}</span>
)}
<div className="flex-1" />
<span className={`px-1.5 py-0.5 rounded text-[10px] font-medium ${
result.raw_value ? 'bg-blue-500/15 text-blue-400' : 'bg-[var(--dg-bg-primary)] text-[var(--dg-text-muted)]'
}`}>{result.raw_value ? 'JSON' : 'String'}</span>
<button onClick={() => navigator.clipboard.writeText(
result.raw_value
? (() => { try { return JSON.stringify(JSON.parse(String(result.raw_text)), null, 2); } catch { return String(result.raw_text); } })()
: String(result.raw_text)
)} className="dg-btn dg-btn-ghost dg-btn-sm p-1 opacity-60 hover:opacity-100">
<ClipboardPaste className="w-3 h-3" />
</button>
</div>
)}
{/* Formatted value */}
<div className="relative group">
<pre className={`text-sm leading-relaxed overflow-auto max-h-[500px] p-4 rounded-lg font-mono ${
<pre className={`text-sm leading-relaxed overflow-auto max-h-[400px] p-4 rounded-lg font-mono ${
result.raw_value
? 'bg-[var(--dg-bg-primary)] text-blue-300 whitespace-pre-wrap break-all'
: 'bg-[var(--dg-bg-primary)] text-[var(--dg-text-primary)] whitespace-pre-wrap break-all'
@@ -538,6 +596,32 @@ export default function QueriesPage() {
</div>
) : result.columns && result.columns.length > 0 ? (
<div ref={resultContainerRef} onScroll={handleResultScroll} className="overflow-auto">
{/* ARDM-style: Hash field/value compact list for Redis HGETALL */}
{dbType === 'redis' && result.columns.length === 2 && result.columns[0] === 'field' && result.columns[1] === 'value' ? (
<div className="divide-y divide-[var(--dg-border)]">
{(sortedResult?.rows || []).map((row, i) => {
const field = String(row.field ?? '');
const value = String(row.value ?? '');
const isJson = (() => { try { const p = JSON.parse(value); return typeof p === 'object'; } catch { return false; } })();
const formatted = isJson ? JSON.stringify(JSON.parse(value), null, 2) : value;
return (
<div key={i} className="flex hover:bg-[var(--dg-bg-hover)] transition-colors">
<div className="w-[200px] shrink-0 px-4 py-2.5 text-xs font-mono text-blue-400 border-r border-[var(--dg-border)] bg-[var(--dg-bg-tertiary)]">
{field}
</div>
<div className="flex-1 px-4 py-2.5 text-xs font-mono whitespace-pre-wrap break-all min-w-0">
{isJson ? (
<pre className="text-blue-300 text-xs whitespace-pre-wrap">{formatted}</pre>
) : (
<span>{value}</span>
)}
</div>
</div>
);
})}
</div>
) : (
/* Standard table for other results */
<table className="w-full text-sm">
<thead className="sticky top-0 z-10">
<tr className="dg-table-header">
@@ -588,6 +672,7 @@ export default function QueriesPage() {
<tr style={{ height: Math.max(0, totalResultHeight - (visibleRange.end * rowHeight)) }} />
</tbody>
</table>
)}
{/* Pagination */}
{result.total > pageSize && (
<div className="flex items-center justify-between px-4 py-2 text-xs dg-text-secondary border-t border-[var(--dg-border)]">