import { useState, useEffect } from 'react' import { Dialog, Button, Notification, toast } from '@/components/ui' import { FaRegFolder, FaRegFolderOpen, FaRegFileAlt, FaCog, FaColumns, FaCode, FaSyncAlt, FaEdit, FaTrash, FaTable, } from 'react-icons/fa' import type { DataSourceDto } from '@/proxy/data-source' import type { SqlFunctionDto, SqlQueryDto, SqlStoredProcedureDto, SqlViewDto, SqlObjectType, } from '@/proxy/sql-query-manager/models' import { sqlObjectManagerService } from '@/services/sql-query-manager.service' import { useLocalization } from '@/utils/hooks/useLocalization' export type SqlObject = SqlFunctionDto | SqlQueryDto | SqlStoredProcedureDto | SqlViewDto interface TreeNode { id: string label: string type: 'root' | 'folder' | 'object' | 'column' objectType?: SqlObjectType data?: SqlObject | any children?: TreeNode[] expanded?: boolean isColumn?: boolean parentTable?: { schemaName: string; tableName: string } } interface SqlObjectExplorerProps { dataSource: DataSourceDto | null onObjectSelect: (object: SqlObject | null, objectType: SqlObjectType | null) => void selectedObject: SqlObject | null onTemplateSelect?: (template: string, templateType: string) => void onShowTableColumns?: (schemaName: string, tableName: string) => void refreshTrigger?: number } const SqlObjectExplorer = ({ dataSource, onObjectSelect, selectedObject, onTemplateSelect, onShowTableColumns, refreshTrigger, }: SqlObjectExplorerProps) => { const { translate } = useLocalization() const [treeData, setTreeData] = useState([]) const [expandedNodes, setExpandedNodes] = useState>( new Set(['root']), // Only root expanded by default ) const [loading, setLoading] = useState(false) const [filterText, setFilterText] = useState('') const [contextMenu, setContextMenu] = useState<{ show: boolean x: number y: number node: TreeNode | null }>({ show: false, x: 0, y: 0, node: null }) const [showDeleteDialog, setShowDeleteDialog] = useState(false) const [objectToDelete, setObjectToDelete] = useState<{ object: SqlObject type: SqlObjectType } | null>(null) useEffect(() => { if (dataSource) { loadObjects() } else { setTreeData([]) } }, [dataSource, refreshTrigger]) // refreshTrigger değişince de yenile const loadObjects = async () => { if (!dataSource) return setLoading(true) try { // Single API call to get all objects const response = await sqlObjectManagerService.getAllObjects(dataSource.code || '') const allObjects = response.data const tree: TreeNode[] = [ { id: 'root', label: dataSource.code || 'Database', type: 'root', expanded: true, children: [ { id: 'templates', label: translate('::App.Platform.Templates'), type: 'folder', expanded: expandedNodes.has('templates'), children: [ { id: 'template-select', label: 'SELECT Query', type: 'object' as const, data: { templateType: 'select' } as any, }, { id: 'template-insert', label: 'INSERT Query', type: 'object' as const, data: { templateType: 'insert' } as any, }, { id: 'template-update', label: 'UPDATE Query', type: 'object' as const, data: { templateType: 'update' } as any, }, { id: 'template-delete', label: 'DELETE Query', type: 'object' as const, data: { templateType: 'delete' } as any, }, { id: 'template-sp', label: 'Stored Procedure', type: 'object' as const, data: { templateType: 'create-procedure' } as any, }, { id: 'template-view', label: 'View', type: 'object' as const, data: { templateType: 'create-view' } as any, }, { id: 'template-scalar-function', label: 'Scalar Function', type: 'object' as const, data: { templateType: 'create-scalar-function' } as any, }, { id: 'template-table-function', label: 'Table-Valued Function', type: 'object' as const, data: { templateType: 'create-table-function' } as any, }, ], }, { id: 'tables', label: `${translate('::App.Platform.Tables')} (${allObjects.tables.length})`, type: 'folder', expanded: expandedNodes.has('tables'), children: allObjects.tables.map((t) => ({ id: `table-${t.schemaName}-${t.tableName}`, label: t.fullName, type: 'object' as const, data: t, })) || [], }, { id: 'queries', label: `${translate('::App.Platform.Queries')} (${allObjects.queries.length})`, type: 'folder', objectType: 1, expanded: expandedNodes.has('queries'), children: allObjects.queries.map((q) => ({ id: q.id || '', label: q.name, type: 'object' as const, objectType: 1 as SqlObjectType, data: q, })) || [], }, { id: 'procedures', label: `${translate('::App.Platform.StoredProcedures')} (${allObjects.storedProcedures.length})`, type: 'folder', objectType: 2, expanded: expandedNodes.has('procedures'), children: allObjects.storedProcedures.map((p) => { const deployInfo = p.isDeployed && p.lastDeployedAt ? ` (${new Date(p.lastDeployedAt).toLocaleString('tr-TR', { day: '2-digit', month: '2-digit', year: 'numeric', hour: '2-digit', minute: '2-digit' })})` : ''; return { id: p.id || '', label: `${p.displayName || p.procedureName}${p.isDeployed ? ' ✅' : ' ❌'}${deployInfo}`, type: 'object' as const, objectType: 2 as SqlObjectType, data: p, }; }) || [], }, { id: 'views', label: `${translate('::App.Platform.Views')} (${allObjects.views.length})`, type: 'folder', objectType: 3, expanded: expandedNodes.has('views'), children: allObjects.views.map((v) => { const deployInfo = v.isDeployed && v.lastDeployedAt ? ` (${new Date(v.lastDeployedAt).toLocaleString('tr-TR', { day: '2-digit', month: '2-digit', year: 'numeric', hour: '2-digit', minute: '2-digit' })})` : ''; return { id: v.id || '', label: `${v.displayName || v.viewName}${v.isDeployed ? ' ✅' : ' ❌'}${deployInfo}`, type: 'object' as const, objectType: 3 as SqlObjectType, data: v, }; }) || [], }, { id: 'functions', label: `${translate('::App.Platform.Functions')} (${allObjects.functions.length})`, type: 'folder', objectType: 4, expanded: expandedNodes.has('functions'), children: allObjects.functions.map((f) => { const deployInfo = f.isDeployed && f.lastDeployedAt ? ` (${new Date(f.lastDeployedAt).toLocaleString('tr-TR', { day: '2-digit', month: '2-digit', year: 'numeric', hour: '2-digit', minute: '2-digit' })})` : ''; return { id: f.id || '', label: `${f.displayName || f.functionName}${f.isDeployed ? ' ✅' : ' ❌'}${deployInfo}`, type: 'object' as const, objectType: 4 as SqlObjectType, data: f, }; }) || [], }, ], }, ] setTreeData(tree) } catch (error: any) { toast.push( {error.response?.data?.error?.message || translate('::App.Platform.FailedToLoadObjects')} , { placement: 'top-center' }, ) } finally { setLoading(false) } } const loadTableColumns = async (schemaName: string, tableName: string): Promise => { try { const response = await sqlObjectManagerService.getTableColumns( dataSource?.code || '', schemaName, tableName, ) return response.data.map((col) => ({ id: `column-${schemaName}-${tableName}-${col.columnName}`, label: `${col.columnName} (${col.dataType}${col.maxLength ? `(${col.maxLength})` : ''})${col.isNullable ? '' : ' NOT NULL'}`, type: 'column' as const, isColumn: true, data: col, parentTable: { schemaName, tableName }, })) } catch (error) { return [] } } const toggleNode = async (nodeId: string) => { const newSet = new Set(expandedNodes) if (newSet.has(nodeId)) { newSet.delete(nodeId) setExpandedNodes(newSet) } else { newSet.add(nodeId) setExpandedNodes(newSet) // If it's a table node and hasn't loaded columns yet, load them if (nodeId.startsWith('table-') && dataSource) { const tableNode = findNodeById(treeData, nodeId) if (tableNode && (!tableNode.children || tableNode.children.length === 0)) { const tableData = tableNode.data as any const columns = await loadTableColumns(tableData.schemaName, tableData.tableName) // Update tree data with columns setTreeData((prevTree) => { return updateNodeChildren(prevTree, nodeId, columns) }) } } } } const findNodeById = (nodes: TreeNode[], id: string): TreeNode | null => { for (const node of nodes) { if (node.id === id) return node if (node.children) { const found = findNodeById(node.children, id) if (found) return found } } return null } const updateNodeChildren = ( nodes: TreeNode[], nodeId: string, children: TreeNode[], ): TreeNode[] => { return nodes.map((node) => { if (node.id === nodeId) { return { ...node, children } } if (node.children) { return { ...node, children: updateNodeChildren(node.children, nodeId, children) } } return node }) } const filterTree = (nodes: TreeNode[], searchText: string): TreeNode[] => { if (!searchText.trim()) return nodes const search = searchText.toLowerCase() const filtered = nodes .map((node) => { const matchesSearch = node.label.toLowerCase().includes(search) const filteredChildren = node.children ? filterTree(node.children, searchText) : [] if (matchesSearch || filteredChildren.length > 0) { return { ...node, children: filteredChildren.length > 0 ? filteredChildren : node.children, } as TreeNode } return null }) .filter((node) => node !== null) as TreeNode[] return filtered } const handleNodeClick = (node: TreeNode) => { if (node.type === 'folder' || node.type === 'root') { toggleNode(node.id) } else if (node.type === 'column') { // Column clicked - do nothing or show info return } else if (node.type === 'object' && node.data) { // Check if it's a template if ((node.data as any).templateType && onTemplateSelect) { const templateType = (node.data as any).templateType onTemplateSelect('', templateType) // Template content will be generated in parent } // Check if it's a table else if (node.id.startsWith('table-') && onTemplateSelect) { const table = node.data as any const selectQuery = `-- SELECT from ${table.fullName || table.tableName}\nSELECT * \nFROM ${table.fullName || `[${table.schemaName}].[${table.tableName}]`}\nWHERE 1=1;` onTemplateSelect(selectQuery, 'table-select') } else if (node.objectType) { onObjectSelect(node.data, node.objectType) } } } const handleContextMenu = (e: React.MouseEvent, node: TreeNode) => { e.preventDefault() // Don't show context menu for columns or templates if (node.type === 'column' || node.id.startsWith('template-')) { return } setContextMenu({ show: true, x: e.clientX, y: e.clientY, node, }) } const handleDelete = async () => { if (!objectToDelete || !objectToDelete.object.id) return try { const { object, type } = objectToDelete switch (type) { case 1: await sqlObjectManagerService.deleteQuery(object.id!) break case 2: await sqlObjectManagerService.deleteStoredProcedure(object.id!) break case 3: await sqlObjectManagerService.deleteView(object.id!) break case 4: await sqlObjectManagerService.deleteFunction(object.id!) break } toast.push( {translate('::App.Platform.ObjectDeletedSuccessfully')} , { placement: 'top-center' }, ) setShowDeleteDialog(false) setObjectToDelete(null) loadObjects() if (selectedObject?.id === object.id) { onObjectSelect(null, null) } } catch (error: any) { toast.push( {error.response?.data?.error?.message || translate('::App.Platform.FailedToDeleteObject')} , { placement: 'top-center' }, ) } } const getIcon = (node: TreeNode) => { if (node.type === 'root') return if (node.type === 'folder') { const isExpanded = expandedNodes.has(node.id) // Templates folder if (node.id === 'templates') return isExpanded ? ( ) : ( ) // Tables folder if (node.id === 'tables') return isExpanded ? ( ) : ( ) if (node.objectType === 1) return isExpanded ? ( ) : ( ) if (node.objectType === 2) return isExpanded ? ( ) : ( ) if (node.objectType === 3) return isExpanded ? ( ) : ( ) if (node.objectType === 4) return isExpanded ? ( ) : ( ) } if (node.type === 'object') { // Check if it's a template if ((node.data as any)?.templateType) { return } // Check if it's a table if (node.id.startsWith('table-')) { return } if (node.objectType === 1) return if (node.objectType === 2) return if (node.objectType === 3) return if (node.objectType === 4) return } if (node.type === 'column') { return } return } const renderNode = (node: TreeNode, level = 0) => { const isExpanded = expandedNodes.has(node.id) const isSelected = node.type === 'object' && selectedObject?.id === node.id const isColumn = node.type === 'column' return (
!isColumn && handleNodeClick(node)} onContextMenu={(e) => !isColumn && handleContextMenu(e, node)} > {getIcon(node)} {node.label}
{isExpanded && node.children && (
{node.children.map((child) => renderNode(child, level + 1))}
)}
) } const filteredTree = filterTree(treeData, filterText) return (
{/* Filter and Refresh Controls */}
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 Content */}
{loading && (
{translate('::App.Platform.Loading')}
)} {!loading && treeData.length === 0 && (
{translate('::App.Platform.NoDataSourceSelected')}
)} {!loading && filteredTree.length > 0 && (
{filteredTree.map((node) => renderNode(node))}
)} {!loading && treeData.length > 0 && filteredTree.length === 0 && (
{translate('::App.Platform.NoResultsFound')}
)}
{contextMenu.show && ( <>
setContextMenu({ show: false, x: 0, y: 0, node: null })} />
{contextMenu.node?.type === 'object' && !contextMenu.node?.id?.startsWith('table-') && ( <> )} {contextMenu.node?.type === 'folder' && ( )} {contextMenu.node?.id?.startsWith('table-') && contextMenu.node?.data && ( )}
)} setShowDeleteDialog(false)} onRequestClose={() => setShowDeleteDialog(false)} >
{translate('::App.Platform.ConfirmDelete')}

{translate('::App.Platform.DeleteConfirmationMessage')}

) } export default SqlObjectExplorer