import { useState, useEffect } from 'react' import { FaRegFolder, FaRegFolderOpen, FaColumns, FaSyncAlt, FaTable, FaPlus, FaEye, FaCog, FaCode, FaDatabase, FaTrash, } from 'react-icons/fa' import type { DatabaseTableDto, SqlNativeObjectDto } from '@/proxy/sql-query-manager/models' import { sqlObjectManagerService } from '@/services/sql-query-manager.service' import { useLocalization } from '@/utils/hooks/useLocalization' type FolderKey = 'tables' | 'views' | 'procedures' | 'functions' interface TreeNode { id: string label: string type: 'root' | 'folder' | 'object' folder?: FolderKey data?: DatabaseTableDto | SqlNativeObjectDto children?: TreeNode[] } interface SqlObjectExplorerProps { dataSource: string | null onTemplateSelect?: (template: string, templateType: string) => void onViewDefinition?: (schemaName: string, objectName: string) => void onDesignTable?: (schemaName: string, tableName: string) => void onNewTable?: () => void refreshTrigger?: number } const FOLDER_META: Record = { tables: { label: 'Tables', color: 'text-teal-500' }, views: { label: 'Views', color: 'text-purple-500' }, procedures: { label: 'Stored Procedures', color: 'text-green-600' }, functions: { label: 'Functions', color: 'text-orange-500' }, } const SqlObjectExplorer = ({ dataSource, onTemplateSelect, onViewDefinition, onDesignTable, onNewTable, refreshTrigger, }: SqlObjectExplorerProps) => { const { translate } = useLocalization() const [treeData, setTreeData] = useState([]) const [expandedNodes, setExpandedNodes] = useState>(new Set(['root'])) const [loading, setLoading] = useState(false) const [filterText, setFilterText] = useState('') const [dropConfirm, setDropConfirm] = useState<{ node: TreeNode } | null>(null) const [dropping, setDropping] = useState(false) const [contextMenu, setContextMenu] = useState<{ show: boolean; x: number; y: number; node: TreeNode | null }>({ show: false, x: 0, y: 0, node: null }) useEffect(() => { if (dataSource) loadObjects() else setTreeData([]) }, [dataSource, refreshTrigger]) const loadObjects = async () => { if (!dataSource) return setLoading(true) try { const { data } = await sqlObjectManagerService.getAllObjects(dataSource) const makeObjectNode = (folder: FolderKey, obj: SqlNativeObjectDto): TreeNode => ({ id: `${folder}-${obj.schemaName}-${obj.objectName}`, label: obj.fullName ?? `[${obj.schemaName}].[${obj.objectName}]`, type: 'object', folder, data: obj, }) const tree: TreeNode[] = [{ id: 'root', label: dataSource, type: 'root', children: [ { id: 'tables', label: `Tables (${data.tables.length})`, type: 'folder', folder: 'tables', children: data.tables.map((t) => ({ id: `tables-${t.schemaName}-${t.tableName}`, label: t.fullName ?? `[${t.schemaName}].[${t.tableName}]`, type: 'object' as const, folder: 'tables' as FolderKey, data: t, })), }, { id: 'views', label: `Views (${data.views.length})`, type: 'folder', folder: 'views', children: data.views.map((v) => makeObjectNode('views', v)), }, { id: 'procedures', label: `Stored Procedures (${data.storedProcedures.length})`, type: 'folder', folder: 'procedures', children: data.storedProcedures.map((p) => makeObjectNode('procedures', p)), }, { id: 'functions', label: `Functions (${data.functions.length})`, type: 'folder', folder: 'functions', children: data.functions.map((f) => makeObjectNode('functions', f)), }, ], }] setTreeData(tree) } catch (error: any) { console.error('Failed to load objects', error) } finally { setLoading(false) } } const toggleNode = (nodeId: string) => { setExpandedNodes((prev) => { const next = new Set(prev) next.has(nodeId) ? next.delete(nodeId) : next.add(nodeId) return next }) } const filterTree = (nodes: TreeNode[], search: string): TreeNode[] => { if (!search.trim()) return nodes const q = search.toLowerCase() return nodes .map((node) => { const match = node.label.toLowerCase().includes(q) const kids = node.children ? filterTree(node.children, search) : [] if (match || kids.length > 0) return { ...node, children: kids.length > 0 ? kids : node.children } as TreeNode return null }) .filter(Boolean) as TreeNode[] } const handleNodeClick = (node: TreeNode) => { if (node.type !== 'object') { toggleNode(node.id); return } if (node.folder === 'tables') { // Generate SELECT template for tables const t = node.data as DatabaseTableDto onTemplateSelect?.(`SELECT TOP 10 *\nFROM ${t.fullName ?? `[${t.schemaName}].[${t.tableName}]`};`, 'table-select') } else { // Load native object definition into editor const obj = node.data as SqlNativeObjectDto onViewDefinition?.(obj.schemaName, obj.objectName) } } const buildDropSql = (node: TreeNode): string => { if (node.folder === 'tables') { const t = node.data as DatabaseTableDto return `DROP TABLE ${t.fullName ?? `[${t.schemaName}].[${t.tableName}]`};` } const obj = node.data as SqlNativeObjectDto const keyword = node.folder === 'views' ? 'VIEW' : node.folder === 'procedures' ? 'PROCEDURE' : 'FUNCTION' return `DROP ${keyword} ${obj.fullName ?? `[${obj.schemaName}].[${obj.objectName}]`};` } const handleDrop = async () => { if (!dropConfirm || !dataSource) return setDropping(true) try { await sqlObjectManagerService.executeQuery({ queryText: buildDropSql(dropConfirm.node), dataSourceCode: dataSource, }) setDropConfirm(null) loadObjects() } catch (err: any) { console.error('Drop failed', err) } finally { setDropping(false) } } const closeCtx = () => setContextMenu({ show: false, x: 0, y: 0, node: null }) const getIcon = (node: TreeNode) => { if (node.type === 'root') return if (node.type === 'folder') { const open = expandedNodes.has(node.id) const cls = FOLDER_META[node.folder!]?.color ?? 'text-blue-500' return open ? : } if (node.folder === 'tables') return if (node.folder === 'views') return if (node.folder === 'procedures') return if (node.folder === 'functions') return return } const renderNode = (node: TreeNode, level = 0) => { const isExpanded = expandedNodes.has(node.id) return (
handleNodeClick(node)} onContextMenu={(e) => { e.preventDefault(); setContextMenu({ show: true, x: e.clientX, y: e.clientY, node }) }} > {getIcon(node)} {node.label} {node.type === 'object' && ( )}
{isExpanded && node.children && (
{node.children.map((c) => renderNode(c, level + 1))}
)}
) } const filteredTree = filterTree(treeData, filterText) // Context menu items per folder const ctxNode = contextMenu.node const isTableObj = ctxNode?.type === 'object' && ctxNode.folder === 'tables' const isNativeObj = ctxNode?.type === 'object' && ctxNode.folder !== 'tables' const isTablesDir = ctxNode?.id === 'tables' const isViewsDir = ctxNode?.id === 'views' const isProcsDir = ctxNode?.id === 'procedures' const isFuncsDir = ctxNode?.id === 'functions' const isFolderNode = ctxNode?.type === 'folder' return (
{/* Search + refresh */}
setFilterText(e.target.value)} className="flex-1 px-3 py-1.5 text-sm border rounded-md bg-white dark:bg-gray-700 dark:border-gray-600" />
{/* Tree */}
{loading &&
{translate('::App.Platform.Loading')}
} {!loading && treeData.length === 0 &&
{translate('::App.Platform.NoDataSourceSelected')}
} {!loading && filteredTree.length > 0 &&
{filteredTree.map((n) => renderNode(n))}
} {!loading && treeData.length > 0 && filteredTree.length === 0 &&
{translate('::App.Platform.NoResultsFound')}
}
{/* Context menu */} {contextMenu.show && ( <>
{/* TABLE object � Design */} {isTableObj && ( )} {/* NATIVE object � View Definition */} {isNativeObj && ( )} {/* FOLDER � New ... */} {isTablesDir && ( )} {isViewsDir && ( )} {isProcsDir && ( )} {isFuncsDir && ( <> )} {/* Separator + Refresh for folders */} {isFolderNode && ( <>
)}
)} {/* Drop Confirm Dialog */} {dropConfirm && ( <>
!dropping && setDropConfirm(null)}>
e.stopPropagation()}>
Drop Object

The following object will be permanently dropped:

{buildDropSql(dropConfirm.node)}
)}
) } export default SqlObjectExplorer