问题:上一轮把导出按钮只加在了「键浏览页」(RedisDetailPage → RedisKeyDetailPanel),
用户在「查询页」(/queries → RedisKeyValuePanel)看结果,那边一个导出按钮都没有。
两个面板是不同组件,只改一个另一个不会生效:
/queries → features/query/components/RedisKeyValuePanel.tsx ← 本次补
/redis(键浏览) → features/redis/components/RedisKeyDetailPanel.tsx ← 上一轮已加
改动:
1. RedisKeyValuePanel 新增 instanceId / db 两个 props(导出需要)
2. 组件内新增导出逻辑:
- buildPageText() 把当前已加载值转 CSV / JSON(纯前端,不发请求)
- exportCurrent() 导出当前页,瞬时完成
- exportAll() 申请一次性票据 → 浏览器原生下载,服务端流式导出全部
3. 选中 key 的头部右侧新增按钮组:CSV / JSON / 导出全部
(搜索框宽度 64 → 56,给按钮腾位置)
4. QueriesPage 传入 instanceId={Number(instanceId)} db={Number(database||0)}
实测:
分页详情 type=zset total=83 本页=100条内 ✅
票据获取 ✅
无 Authorization 的浏览器式下载 HTTP 200,CSV 84 行 ✅
bundle: QueriesPage-*.js 含「导出全部」×1、「Redis 键值可视化」×1 ✅
RedisDetailPage-*.js 含「导出全部」×1(键浏览页仍在)✅
镜像:lghost/opsxsql:latest + v0.1 = sha256:7d72f2a96875
575 lines
21 KiB
TypeScript
575 lines
21 KiB
TypeScript
import { useState, useEffect, useMemo, useCallback, useRef } from 'react';
|
|
import { AlertTriangle } from 'lucide-react';
|
|
import SchemaSidebar from '@/components/SchemaSidebar';
|
|
import { queryApi } from '@/api/query';
|
|
import { instanceApi } from '@/api/instance';
|
|
import { redisApi } from '@/api/redis';
|
|
import { getTemplateSQL, getTemplatesForType } from '@/lib/queryTemplates';
|
|
import { instanceDetailApi } from '@/api/instanceDetail';
|
|
import type { DBType, QueryTemplate } from '@/lib/queryTemplates';
|
|
import { RedisValueViewerDialog } from '@/features/redis/components/RedisValueViewerDialog';
|
|
import type { RedisLineEditorState } from '@/features/redis/types';
|
|
import { QueryHistoryDrawer } from '@/features/query/components/QueryHistoryDrawer';
|
|
import { QueryTemplatePanel } from '@/features/query/components/QueryTemplatePanel';
|
|
import { QueryTopBar } from '@/features/query/components/QueryTopBar';
|
|
import { SqlEditorSection } from '@/features/query/components/SqlEditorSection';
|
|
import type { SqlEditorHandle } from '@/components/SqlEditor';
|
|
import { RedisKeyValuePanel } from '@/features/query/components/RedisKeyValuePanel';
|
|
import { QueryResultPanel } from '@/features/query/components/QueryResultPanel';
|
|
import { dbTypeIcon, normalizeDbType } from '@/features/query/constants';
|
|
import type {
|
|
QueryInstanceOption,
|
|
QueryResult,
|
|
RedisKeyDetail,
|
|
RedisKeyInfo,
|
|
SortDirection,
|
|
SortState,
|
|
} from '@/features/query/types';
|
|
import api from '@/api/request';
|
|
import { readStorage, writeStorage } from '@/lib/storage';
|
|
|
|
export default function QueriesPage() {
|
|
// -- 编辑器 state --
|
|
const [sql, setSql] = useState('');
|
|
const [instanceId, setInstanceId] = useState('');
|
|
const [database, setDatabase] = useState('');
|
|
const [databases, setDatabases] = useState<string[]>([]);
|
|
const [loadingDbs, setLoadingDbs] = useState(false);
|
|
const [instances, setInstances] = useState<QueryInstanceOption[]>([]);
|
|
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);
|
|
const [redisLineViewer, setRedisLineViewer] = useState<RedisLineEditorState>({
|
|
open: false,
|
|
field: '',
|
|
value: '',
|
|
format: 'Json',
|
|
});
|
|
const [redisPattern, setRedisPattern] = useState('');
|
|
const [redisType, setRedisType] = useState('');
|
|
const [redisKeys, setRedisKeys] = useState<RedisKeyInfo[]>([]);
|
|
const [redisSelectedKey, setRedisSelectedKey] = useState<RedisKeyDetail | null>(null);
|
|
const [redisLoading, setRedisLoading] = useState(false);
|
|
const [redisDetailLoading, setRedisDetailLoading] = useState(false);
|
|
const [redisValueSearch, setRedisValueSearch] = useState('');
|
|
|
|
// -- Pagination --
|
|
const [pageSize, setPageSize] = useState(100);
|
|
const [currentPage, setCurrentPage] = useState(1);
|
|
|
|
// -- UI state --
|
|
const [showTemplates, setShowTemplates] = useState(false);
|
|
const [templateSearch, setTemplateSearch] = useState('');
|
|
const [templateCategory, setTemplateCategory] = useState('all');
|
|
const [showSidebar, setShowSidebar] = useState(true);
|
|
const [showResult, setShowResult] = useState(true);
|
|
const [showHistory, setShowHistory] = useState(false);
|
|
const [sortState, setSortState] = useState<SortState>({ column: null, direction: null });
|
|
// -- Sidebar 拖拽宽度 --
|
|
const [sideWidth, setSideWidth] = useState(() => Number(readStorage('querySideWidth')) || 280);
|
|
const sidebarDragRef = useRef<HTMLDivElement>(null);
|
|
|
|
// -- 历史 state --
|
|
const [historyRefreshKey, setHistoryRefreshKey] = useState(0);
|
|
const sqlEditorRef = useRef<SqlEditorHandle>(null);
|
|
/** 最近一次执行的语句(选中片段或全文),分页重放以此为准 */
|
|
const lastExecutedSqlRef = useRef('');
|
|
|
|
/* ---- Data loading ---- */
|
|
useEffect(() => {
|
|
instanceApi
|
|
.accessible('readonly')
|
|
.then((res) => {
|
|
setInstances(Array.isArray(res) ? res : []);
|
|
})
|
|
.catch(() => {
|
|
setInstances([]);
|
|
});
|
|
}, []);
|
|
|
|
useEffect(() => {
|
|
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(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]);
|
|
|
|
const selectedInstance = useMemo(
|
|
() => instances.find((inst) => String(inst.id) === instanceId),
|
|
[instances, instanceId],
|
|
);
|
|
const dbType = normalizeDbType(selectedInstance?.db_type);
|
|
const editorLang =
|
|
dbType === 'redis' ? ('redis' as const) : dbType === 'mongodb' ? ('mongodb' as const) : ('sql' as const);
|
|
|
|
useEffect(() => {
|
|
setRedisKeys([]);
|
|
setRedisSelectedKey(null);
|
|
setRedisValueSearch('');
|
|
}, [instanceId, database]);
|
|
|
|
const handleRedisSearch = useCallback(async () => {
|
|
if (!instanceId) return;
|
|
const pattern = redisPattern.trim();
|
|
if (!pattern) {
|
|
setRedisKeys([]);
|
|
setRedisSelectedKey(null);
|
|
return;
|
|
}
|
|
setRedisLoading(true);
|
|
try {
|
|
const res = await redisApi.scanKeys(Number(instanceId), {
|
|
db: Number(database || 0),
|
|
pattern,
|
|
type: redisType || undefined,
|
|
});
|
|
setRedisKeys((res?.keys || res || []) as RedisKeyInfo[]);
|
|
} catch {
|
|
setRedisKeys([]);
|
|
} finally {
|
|
setRedisLoading(false);
|
|
}
|
|
}, [instanceId, database, redisPattern, redisType]);
|
|
|
|
const loadRedisKeyDetail = useCallback(
|
|
async (keyName: string) => {
|
|
if (!instanceId) return;
|
|
setRedisDetailLoading(true);
|
|
try {
|
|
const detail = await redisApi.getKeyDetail(Number(instanceId), keyName, Number(database || 0));
|
|
setRedisSelectedKey(detail as RedisKeyDetail);
|
|
setRedisValueSearch('');
|
|
} catch {
|
|
setRedisSelectedKey(null);
|
|
} finally {
|
|
setRedisDetailLoading(false);
|
|
}
|
|
},
|
|
[instanceId, database],
|
|
);
|
|
|
|
const copyRedisText = useCallback((text: string) => {
|
|
navigator.clipboard?.writeText(text).catch(() => undefined);
|
|
}, []);
|
|
|
|
const handleInspectRedisValue = useCallback((field: string, value: unknown) => {
|
|
setRedisLineViewer({
|
|
open: true,
|
|
field,
|
|
value: typeof value === 'string' ? value : JSON.stringify(value, null, 2),
|
|
format: 'Json',
|
|
});
|
|
}, []);
|
|
|
|
useEffect(() => {
|
|
if (!instanceId || !database || (dbType !== 'mysql' && dbType !== 'postgresql')) return;
|
|
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]);
|
|
|
|
/* ---- Templates ---- */
|
|
const templates = useMemo(() => getTemplatesForType(dbType), [dbType]);
|
|
const templateCategories = useMemo(() => {
|
|
const cats = new Set(templates.map((t) => t.category));
|
|
return ['all', ...Array.from(cats)];
|
|
}, [templates]);
|
|
const filteredTemplates = useMemo(() => {
|
|
let r = templates;
|
|
if (templateCategory !== 'all') r = r.filter((t) => t.category === templateCategory);
|
|
if (templateSearch) {
|
|
const kw = templateSearch.toLowerCase();
|
|
r = r.filter((t) => t.name.toLowerCase().includes(kw) || t.description.toLowerCase().includes(kw));
|
|
}
|
|
return r;
|
|
}, [templates, templateCategory, templateSearch]);
|
|
|
|
const handleApplyTemplate = useCallback(
|
|
(tpl: QueryTemplate) => {
|
|
setSql(getTemplateSQL(tpl, dbType, { db: database || undefined }));
|
|
setShowTemplates(false);
|
|
},
|
|
[dbType, database],
|
|
);
|
|
|
|
const catLabel: Record<string, string> = {
|
|
all: '全部',
|
|
表结构: '表结构',
|
|
性能分析: '性能分析',
|
|
数据统计: '数据统计',
|
|
基础查询: '基础查询',
|
|
};
|
|
|
|
/* ---- Execute ---- */
|
|
const handleExecute = useCallback(
|
|
async (pageArg: number | unknown = 1, sqlOverride?: string) => {
|
|
const page = typeof pageArg === 'number' && Number.isFinite(pageArg) ? pageArg : 1;
|
|
// 执行范围:显式覆盖 > 选区/光标语句(仅首页) > 上次执行语句(分页) > 编辑器全文
|
|
const effectiveSql =
|
|
sqlOverride ?? (page === 1 ? (sqlEditorRef.current?.getSqlToRun() ?? sql) : lastExecutedSqlRef.current || sql);
|
|
if (!effectiveSql || !instanceId) {
|
|
setError('请选择实例并输入查询语句');
|
|
return;
|
|
}
|
|
if (/\{(?:table|db|collection)\}/.test(effectiveSql)) {
|
|
setError('SQL 中含有未替换的占位符,请先替换或删除');
|
|
return;
|
|
}
|
|
lastExecutedSqlRef.current = effectiveSql;
|
|
setExecuting(true);
|
|
setError('');
|
|
setCurrentPage(page);
|
|
if (page === 1) setResult(null);
|
|
try {
|
|
const offset = (page - 1) * pageSize;
|
|
const res = (await queryApi.execute({
|
|
instance_id: Number(instanceId),
|
|
database: database || undefined,
|
|
sql: effectiveSql,
|
|
limit: pageSize,
|
|
offset,
|
|
})) as unknown as QueryResult;
|
|
const normalized = { ...res, execution_time_ms: res.execution_time_ms || res.cost_ms || 0 };
|
|
if (normalized.error) {
|
|
setError(normalized.error);
|
|
setResult(null);
|
|
} else {
|
|
setResult(normalized);
|
|
}
|
|
if (page === 1) {
|
|
// 历史由后端执行时自动落库,这里只触发抽屉刷新
|
|
setHistoryRefreshKey((k) => k + 1);
|
|
}
|
|
} catch (e: unknown) {
|
|
const raw = (e as { response?: { data?: { detail?: string | Array<{ msg: string }> } } })?.response?.data
|
|
?.detail;
|
|
const msg = Array.isArray(raw)
|
|
? raw.map((v) => v.msg).join('; ')
|
|
: typeof raw === 'string'
|
|
? raw
|
|
: raw
|
|
? JSON.stringify(raw)
|
|
: '查询失败';
|
|
setError(msg);
|
|
} finally {
|
|
setExecuting(false);
|
|
}
|
|
},
|
|
[sql, instanceId, database, dbType, pageSize],
|
|
);
|
|
|
|
/* ---- Result Sorting ---- */
|
|
const sortedResult = useMemo(() => {
|
|
if (!result || !sortState.column || !sortState.direction) return result;
|
|
if (!Array.isArray(result.rows)) return result;
|
|
const col = sortState.column;
|
|
const dir = sortState.direction === 'asc' ? 1 : -1;
|
|
return {
|
|
...result,
|
|
rows: [...result.rows].sort((a, b) => {
|
|
const av = a[col] ?? '';
|
|
const bv = b[col] ?? '';
|
|
if (typeof av === 'number' && typeof bv === 'number') return (av - bv) * dir;
|
|
return String(av).localeCompare(String(bv)) * dir;
|
|
}),
|
|
};
|
|
}, [result, sortState]);
|
|
|
|
const handleSort = useCallback((col: string) => {
|
|
setSortState((prev) => {
|
|
if (prev.column === col) {
|
|
if (prev.direction === 'asc') return { column: col, direction: 'desc' as SortDirection };
|
|
if (prev.direction === 'desc') return { column: null, direction: null };
|
|
}
|
|
return { column: col, direction: 'asc' as SortDirection };
|
|
});
|
|
}, []);
|
|
|
|
const handleExportResult = useCallback(async () => {
|
|
if (!sql || !instanceId) return;
|
|
try {
|
|
const blob = await api.post<unknown, Blob>(
|
|
'/query/export',
|
|
{
|
|
instance_id: Number(instanceId),
|
|
database: database || undefined,
|
|
sql,
|
|
},
|
|
{ responseType: 'blob' },
|
|
);
|
|
const url = URL.createObjectURL(blob);
|
|
const a = document.createElement('a');
|
|
a.href = url;
|
|
a.download = `query_result_${new Date().toISOString().slice(0, 19).replace(/:/g, '-')}.csv`;
|
|
a.click();
|
|
URL.revokeObjectURL(url);
|
|
} catch (e) {
|
|
setError((e as Error).message || '导出失败');
|
|
}
|
|
}, [sql, instanceId, database]);
|
|
|
|
// 修改每页条数后回到第 1 页并重新执行(沿用旧版时序:本次执行仍取旧 pageSize 闭包)
|
|
const handlePageSizeChange = useCallback(
|
|
(size: number) => {
|
|
setPageSize(size);
|
|
setCurrentPage(1);
|
|
handleExecute(1);
|
|
},
|
|
[handleExecute],
|
|
);
|
|
|
|
// -- 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) => {
|
|
writeStorage('querySideWidth', String(w));
|
|
return w;
|
|
});
|
|
};
|
|
document.addEventListener('mousemove', onMove);
|
|
document.addEventListener('mouseup', onUp);
|
|
},
|
|
[sideWidth],
|
|
);
|
|
|
|
const handleToggleSidebar = useCallback(() => setShowSidebar((v) => !v), []);
|
|
const handleToggleTemplates = useCallback(() => setShowTemplates((v) => !v), []);
|
|
const handleToggleHistory = useCallback(() => setShowHistory((v) => !v), []);
|
|
const handleToggleResult = useCallback(() => setShowResult((v) => !v), []);
|
|
|
|
return (
|
|
<div className="flex h-auto min-h-[calc(100vh-7rem)] md:h-[calc(100vh-3rem)] gap-0">
|
|
{/* ====== 主区域 ====== */}
|
|
<div className="flex-1 flex flex-col min-w-0 overflow-visible md:overflow-hidden">
|
|
{/* -- 顶栏: 实例+库选择 -- */}
|
|
<QueryTopBar
|
|
instances={instances}
|
|
instanceId={instanceId}
|
|
selectedInstance={selectedInstance}
|
|
database={database}
|
|
databases={databases}
|
|
loadingDbs={loadingDbs}
|
|
dbType={dbType}
|
|
showSidebar={showSidebar}
|
|
showTemplates={showTemplates}
|
|
showHistory={showHistory}
|
|
executing={executing}
|
|
templateCount={templates.length}
|
|
onInstanceIdChange={setInstanceId}
|
|
onDatabaseChange={setDatabase}
|
|
onToggleSidebar={handleToggleSidebar}
|
|
onToggleTemplates={handleToggleTemplates}
|
|
onToggleHistory={handleToggleHistory}
|
|
onExecute={handleExecute}
|
|
/>
|
|
|
|
{showTemplates && (
|
|
<QueryTemplatePanel
|
|
categories={templateCategories}
|
|
activeCategory={templateCategory}
|
|
categoryLabels={catLabel}
|
|
search={templateSearch}
|
|
templates={filteredTemplates}
|
|
onCategoryChange={setTemplateCategory}
|
|
onSearchChange={setTemplateSearch}
|
|
onApplyTemplate={handleApplyTemplate}
|
|
/>
|
|
)}
|
|
|
|
{/* -- 编辑器 + 侧边栏 + 结果 (主体区) -- */}
|
|
<div className="flex-1 min-h-0 flex gap-0 overflow-visible md:overflow-hidden">
|
|
{/* Schema 侧边栏(左侧,独立) */}
|
|
{showSidebar && instanceId && (
|
|
<>
|
|
<div
|
|
className="hidden md:block 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) => {
|
|
if (dbType === 'redis') {
|
|
setSql(text);
|
|
} else if (/^\s*SELECT\b/i.test(text)) {
|
|
// 左侧栏点表生成的 SELECT 语句:直接替换编辑器内容,
|
|
// 避免和上一条语句拼接成非法 SQL
|
|
setSql(text);
|
|
} else {
|
|
// 插入表名/字段名:追加到现有内容后
|
|
setSql((prev) => prev + (prev.trim() ? ' ' : '') + text + ' ');
|
|
}
|
|
}}
|
|
onExecuteSql={(sqlText) => {
|
|
setSql(sqlText);
|
|
handleExecute(1, sqlText);
|
|
}}
|
|
onDatabaseChange={setDatabase}
|
|
singleDb={!!database}
|
|
/>
|
|
</div>
|
|
<div
|
|
ref={sidebarDragRef}
|
|
className="hidden md:block 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>
|
|
</>
|
|
)}
|
|
|
|
{/* 编辑器 + 查询结果(右侧主体,垂直排列) */}
|
|
<div className="flex-1 min-w-0 flex flex-col overflow-hidden">
|
|
{dbType === 'redis' && instanceId && (
|
|
<RedisKeyValuePanel
|
|
pattern={redisPattern}
|
|
typeFilter={redisType}
|
|
keys={redisKeys}
|
|
selectedKey={redisSelectedKey}
|
|
loading={redisLoading}
|
|
detailLoading={redisDetailLoading}
|
|
valueSearch={redisValueSearch}
|
|
hideOnWide={showSidebar}
|
|
onPatternChange={setRedisPattern}
|
|
onTypeFilterChange={setRedisType}
|
|
onSearch={handleRedisSearch}
|
|
onLoadKeyDetail={loadRedisKeyDetail}
|
|
onValueSearchChange={setRedisValueSearch}
|
|
onCopy={copyRedisText}
|
|
onInspect={handleInspectRedisValue}
|
|
instanceId={Number(instanceId)}
|
|
db={Number(database || 0)}
|
|
/>
|
|
)}
|
|
|
|
{/* SQL 编辑器 */}
|
|
<SqlEditorSection
|
|
sql={sql}
|
|
dbType={dbType}
|
|
editorLang={editorLang}
|
|
schema={editorSchema}
|
|
onSqlChange={setSql}
|
|
onExecute={handleExecute}
|
|
editorRef={sqlEditorRef}
|
|
/>
|
|
|
|
{/* -- 错误提示 -- */}
|
|
{error && (
|
|
<div className="shrink-0 px-4 pb-2">
|
|
<div className="flex items-center gap-2 p-3 bg-red-500/10 border border-red-500/20 rounded-lg text-sm text-red-400">
|
|
<AlertTriangle className="w-4 h-4 shrink-0" />
|
|
{error}
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* -- 查询结果 (编辑器下方,独立滚动,含虚拟滚动表格/分页/raw 展示) -- */}
|
|
{result && (
|
|
<QueryResultPanel
|
|
result={result}
|
|
sortedResult={sortedResult}
|
|
sortState={sortState}
|
|
dbType={dbType}
|
|
sql={sql}
|
|
showResult={showResult}
|
|
pageSize={pageSize}
|
|
currentPage={currentPage}
|
|
onToggleResult={handleToggleResult}
|
|
onExportResult={handleExportResult}
|
|
onSort={handleSort}
|
|
onViewRedisLine={setRedisLineViewer}
|
|
onPageSizeChange={handlePageSizeChange}
|
|
onPageChange={handleExecute}
|
|
/>
|
|
)}
|
|
</div>
|
|
{/* end 编辑器+结果区 */}
|
|
</div>
|
|
{/* end 主体区 (sidebar + 编辑器+结果) */}
|
|
</div>
|
|
{/* end 主区域 */}
|
|
|
|
{showHistory && (
|
|
<QueryHistoryDrawer
|
|
onClose={() => setShowHistory(false)}
|
|
onUseSql={(sqlText, run) => {
|
|
setSql(sqlText);
|
|
if (run) handleExecute(1, sqlText);
|
|
}}
|
|
refreshKey={historyRefreshKey}
|
|
/>
|
|
)}
|
|
<RedisValueViewerDialog
|
|
state={redisLineViewer}
|
|
onChange={setRedisLineViewer}
|
|
onClose={() => setRedisLineViewer({ open: false, field: '', value: '', format: 'Json' })}
|
|
/>
|
|
</div>
|
|
);
|
|
}
|