feat(query): sql workspace conveniences per opsxdb patterns
- generic ContextMenu component (viewport flip, esc/outside close) - SchemaSidebar table right-click menu: preview data, generate SELECT/INSERT/UPDATE/DELETE templates with pk-aware WHERE, copy table name; double-click table runs preview SELECT; column right-click copy/insert; dialect-aware quoting; preview SELECT carries no LIMIT - SqlEditor selection-first execution (Mod-Enter runs selection else statement at cursor via quote/comment-aware splitter); pagination replays last executed statement - query history persisted server-side: drawer fetches query-histories with keyword/mine/favorites filters, star toggle (new PUT favorite + mine filter endpoints), insert and insert-and-run - custom templates in localStorage alongside built-ins - unit tests: genSql + statement splitter (28 tests green)
This commit is contained in:
1 parent
ff539b47e0
commit
f6d636fa44
16 files changed
+1392
-457
No files matched your search
@@ -1,6 +1,4 @@
|
||||
import {
|
||||
ChevronDown, Database, Loader2, PanelRightClose, PanelRightOpen, Play, Sparkles,
|
||||
} from 'lucide-react';
|
||||
import { ChevronDown, Database, Loader2, PanelRightClose, PanelRightOpen, Play, Sparkles } from 'lucide-react';
|
||||
import type { DBType } from '@/lib/queryTemplates';
|
||||
import { dbTypeIcon, dbTypeLabel } from '../constants';
|
||||
import type { QueryInstanceOption } from '../types';
|
||||
@@ -31,7 +29,6 @@ interface QueryTopBarProps {
|
||||
/** 模板总数 */
|
||||
templateCount: number;
|
||||
/** 历史记录总数 */
|
||||
historyCount: number;
|
||||
onInstanceIdChange: (instanceId: string) => void;
|
||||
onDatabaseChange: (database: string) => void;
|
||||
onToggleSidebar: () => void;
|
||||
@@ -55,7 +52,6 @@ export function QueryTopBar({
|
||||
showHistory,
|
||||
executing,
|
||||
templateCount,
|
||||
historyCount,
|
||||
onInstanceIdChange,
|
||||
onDatabaseChange,
|
||||
onToggleSidebar,
|
||||
@@ -70,10 +66,13 @@ export function QueryTopBar({
|
||||
{selectedInstance && dbTypeIcon[dbType] && (
|
||||
<img src={dbTypeIcon[dbType]} alt="" className="w-5 h-5 object-contain" />
|
||||
)}
|
||||
<select value={instanceId} onChange={e => onInstanceIdChange(e.target.value)}
|
||||
className="px-3 py-2 bg-[var(--dg-bg-tertiary)] border border-[var(--dg-border)] rounded-lg text-sm focus:border-blue-400 focus:outline-none min-w-0 w-full md:w-auto md:min-w-[200px]">
|
||||
<select
|
||||
value={instanceId}
|
||||
onChange={(e) => onInstanceIdChange(e.target.value)}
|
||||
className="px-3 py-2 bg-[var(--dg-bg-tertiary)] border border-[var(--dg-border)] rounded-lg text-sm focus:border-blue-400 focus:outline-none min-w-0 w-full md:w-auto md:min-w-[200px]"
|
||||
>
|
||||
<option value="">选择实例</option>
|
||||
{instances.map(inst => (
|
||||
{instances.map((inst) => (
|
||||
<option key={inst.id} value={inst.id}>
|
||||
{inst.name} ({dbTypeLabel[inst.db_type] || inst.db_type})
|
||||
</option>
|
||||
@@ -81,15 +80,29 @@ export function QueryTopBar({
|
||||
</select>
|
||||
|
||||
{dbType === 'redis' ? (
|
||||
<select value={database || '0'} onChange={e => onDatabaseChange(e.target.value)}
|
||||
className="px-3 py-2 bg-[var(--dg-bg-tertiary)] border border-[var(--dg-border)] rounded-lg text-sm focus:border-blue-400 focus:outline-none w-full md:w-24">
|
||||
{Array.from({ length: 16 }, (_, i) => <option key={i} value={String(i)}>DB{i}</option>)}
|
||||
<select
|
||||
value={database || '0'}
|
||||
onChange={(e) => onDatabaseChange(e.target.value)}
|
||||
className="px-3 py-2 bg-[var(--dg-bg-tertiary)] border border-[var(--dg-border)] rounded-lg text-sm focus:border-blue-400 focus:outline-none w-full md:w-24"
|
||||
>
|
||||
{Array.from({ length: 16 }, (_, i) => (
|
||||
<option key={i} value={String(i)}>
|
||||
DB{i}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
) : (
|
||||
<select value={database} onChange={e => onDatabaseChange(e.target.value)}
|
||||
className="px-3 py-2 bg-[var(--dg-bg-tertiary)] border border-[var(--dg-border)] rounded-lg text-sm focus:border-blue-400 focus:outline-none min-w-0 w-full md:w-auto md:min-w-[160px]">
|
||||
<select
|
||||
value={database}
|
||||
onChange={(e) => onDatabaseChange(e.target.value)}
|
||||
className="px-3 py-2 bg-[var(--dg-bg-tertiary)] border border-[var(--dg-border)] rounded-lg text-sm focus:border-blue-400 focus:outline-none min-w-0 w-full md:w-auto md:min-w-[160px]"
|
||||
>
|
||||
<option value="">{loadingDbs ? '加载中...' : '选择数据库'}</option>
|
||||
{databases.map(db => <option key={db} value={db}>{db}</option>)}
|
||||
{databases.map((db) => (
|
||||
<option key={db} value={db}>
|
||||
{db}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
)}
|
||||
</div>
|
||||
@@ -97,25 +110,31 @@ export function QueryTopBar({
|
||||
{/* 右侧工具按钮组 */}
|
||||
<div className="flex items-center gap-2 w-full md:w-auto overflow-x-auto pb-1 md:pb-0">
|
||||
{instanceId && (
|
||||
<button onClick={onToggleSidebar}
|
||||
className={`dg-btn dg-btn-ghost dg-btn-sm ${showSidebar ? 'bg-blue-500/15 text-blue-400 border-blue-500/30' : ''}`}>
|
||||
<button
|
||||
onClick={onToggleSidebar}
|
||||
className={`dg-btn dg-btn-ghost dg-btn-sm ${showSidebar ? 'bg-blue-500/15 text-blue-400 border-blue-500/30' : ''}`}
|
||||
>
|
||||
<Database className="w-3.5 h-3.5" />
|
||||
{showSidebar ? '隐藏' : '导航'}
|
||||
</button>
|
||||
)}
|
||||
<button onClick={onToggleTemplates}
|
||||
className="dg-btn dg-btn-ghost dg-btn-sm">
|
||||
<button onClick={onToggleTemplates} className="dg-btn dg-btn-ghost dg-btn-sm">
|
||||
<Sparkles className="w-3.5 h-3.5 text-blue-400" />
|
||||
模板 ({templateCount})
|
||||
<ChevronDown className={`w-3 h-3 transition-transform ${showTemplates ? 'rotate-180' : ''}`} />
|
||||
</button>
|
||||
<button onClick={onToggleHistory}
|
||||
className={`dg-btn dg-btn-ghost dg-btn-sm ${showHistory ? 'bg-blue-500/15 text-blue-400 border-blue-500/30' : ''}`}>
|
||||
<button
|
||||
onClick={onToggleHistory}
|
||||
className={`dg-btn dg-btn-ghost dg-btn-sm ${showHistory ? 'bg-blue-500/15 text-blue-400 border-blue-500/30' : ''}`}
|
||||
>
|
||||
{showHistory ? <PanelRightClose className="w-3.5 h-3.5" /> : <PanelRightOpen className="w-3.5 h-3.5" />}
|
||||
历史 {historyCount > 0 && <span className="text-[10px] px-1 py-0.5 rounded-full bg-[var(--dg-bg-primary)]">{historyCount}</span>}
|
||||
历史
|
||||
</button>
|
||||
<button onClick={onExecute} disabled={executing || !instanceId}
|
||||
className="dg-btn dg-btn-primary ml-auto md:ml-0">
|
||||
<button
|
||||
onClick={onExecute}
|
||||
disabled={executing || !instanceId}
|
||||
className="dg-btn dg-btn-primary ml-auto md:ml-0"
|
||||
>
|
||||
{executing ? <Loader2 className="w-4 h-4 animate-spin" /> : <Play className="w-4 h-4" />}
|
||||
{executing ? '执行中...' : '执行'}
|
||||
</button>
|
||||
|
||||
Reference in new issue
Block a user