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:
1 parent
4e6f9eb8fd
commit
433e7e2909
2 files changed
+122
-16
No files matched your search
@@ -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)]">
|
||||
|
||||
Reference in new issue
Block a user