refactor(frontend): split 7 oversized pages into feature co-location

Per guide ch.7 page split pattern (skeleton orchestrates, private
components live in features/<domain>/):
- InstancePage 1716->328, UsersPage 1309->240, TicketsPage 1076->195,
  TicketDetailPage 911->287, QueriesPage 883->479, LdapConfigPage 860->393,
  WorkflowPage 852->194
- new feature modules: datasource/ rbac/ ticket/ query/ system/ldap/
  governance/workflow/ (components + hooks + types + utils)
- stable keys replace index keys in dynamic lists; polling intervals
  and toast call sites preserved 1:1 (verified by agents)
- filter/pagination state moved to URL query via new useQueryState hook
  (Instance/Users/Tickets/AuditLogs pages): refresh/share/back-button
  restore state; composite updates applied atomically
- permission pure functions extracted to lib/permissions.ts (used by
  useAuth + useLogin); page-size audit: 0 pages over 700-line budget
This commit is contained in:
Logan.Li committed 2026-10-26 17:54:15 +08:00
1 parent ac563d1c5e
commit ac498c7ba2
92 files changed
+8224 -6146

No files matched your search

@@ -0,0 +1,126 @@
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';
interface QueryTopBarProps {
/** 可选实例列表 */
instances: QueryInstanceOption[];
/** 当前选中实例 id(字符串形式) */
instanceId: string;
/** 已选实例(用于展示库类型图标) */
selectedInstance: QueryInstanceOption | undefined;
/** 当前选中数据库 */
database: string;
/** 可选数据库列表(MySQL/PG) */
databases: string[];
/** 数据库列表加载中 */
loadingDbs: boolean;
/** 归一化后的库类型 */
dbType: DBType;
/** Schema 侧栏是否可见 */
showSidebar: boolean;
/** 模板面板是否展开 */
showTemplates: boolean;
/** 历史抽屉是否展开 */
showHistory: boolean;
/** 是否正在执行查询 */
executing: boolean;
/** 模板总数 */
templateCount: number;
/** 历史记录总数 */
historyCount: number;
onInstanceIdChange: (instanceId: string) => void;
onDatabaseChange: (database: string) => void;
onToggleSidebar: () => void;
onToggleTemplates: () => void;
onToggleHistory: () => void;
/** 执行查询(等价于第 1 页) */
onExecute: () => void;
}
/** 查询页顶栏:实例/数据库选择 + 模板/历史/导航/执行工具按钮 */
export function QueryTopBar({
instances,
instanceId,
selectedInstance,
database,
databases,
loadingDbs,
dbType,
showSidebar,
showTemplates,
showHistory,
executing,
templateCount,
historyCount,
onInstanceIdChange,
onDatabaseChange,
onToggleSidebar,
onToggleTemplates,
onToggleHistory,
onExecute,
}: QueryTopBarProps) {
return (
<div className="shrink-0 px-0 md:px-5 pt-1 md:pt-4 pb-2">
<div className="flex items-center gap-3 flex-wrap">
<div className="flex items-center gap-2 flex-1 min-w-full md:min-w-0 flex-wrap md:flex-nowrap">
{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]">
<option value="">选择实例</option>
{instances.map(inst => (
<option key={inst.id} value={inst.id}>
{inst.name} ({dbTypeLabel[inst.db_type] || inst.db_type})
</option>
))}
</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>
) : (
<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>)}
</select>
)}
</div>
{/* 右侧工具按钮组 */}
<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' : ''}`}>
<Database className="w-3.5 h-3.5" />
{showSidebar ? '隐藏' : '导航'}
</button>
)}
<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' : ''}`}>
{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">
{executing ? <Loader2 className="w-4 h-4 animate-spin" /> : <Play className="w-4 h-4" />}
{executing ? '执行中...' : '执行'}
</button>
</div>
</div>
</div>
);
}