import { useState, useEffect, useCallback } from 'react' import { Dialog, Button, Notification, toast } from '@/components/ui' import { FaRegFolder, FaRegFolderOpen, FaRegFileAlt, FaCog, FaColumns, FaCode, FaSyncAlt, FaEdit, FaTrash, } 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 { sqlFunctionService, sqlQueryService, sqlStoredProcedureService, sqlViewService, } 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' objectType?: SqlObjectType data?: SqlObject children?: TreeNode[] expanded?: boolean } interface SqlObjectExplorerProps { dataSource: DataSourceDto | null onObjectSelect: (object: SqlObject | null, objectType: SqlObjectType | null) => void selectedObject: SqlObject | null onTemplateSelect?: (template: string, templateType: string) => void } const SqlObjectExplorer = ({ dataSource, onObjectSelect, selectedObject, onTemplateSelect, }: SqlObjectExplorerProps) => { const { translate } = useLocalization() const [treeData, setTreeData] = useState([]) const [expandedNodes, setExpandedNodes] = useState>( new Set(['root', 'templates', 'queries', 'storedProcedures', 'views', 'functions']), ) const [loading, setLoading] = useState(false) 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]) const loadObjects = async () => { if (!dataSource) return setLoading(true) try { const [queries, storedProcedures, views, functions] = await Promise.all([ sqlQueryService.getList({ skipCount: 0, maxResultCount: 1000, dataSourceCode: dataSource.code, }), sqlStoredProcedureService.getList({ skipCount: 0, maxResultCount: 1000, dataSourceCode: dataSource.code, }), sqlViewService.getList({ skipCount: 0, maxResultCount: 1000, dataSourceCode: dataSource.code, }), sqlFunctionService.getList({ skipCount: 0, maxResultCount: 1000, dataSourceCode: dataSource.code, }), ]) 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-function', label: 'Function', type: 'object' as const, data: { templateType: 'create-function' } as any, }, ], }, { id: 'queries', label: `${translate('::App.Platform.Queries')} (${queries.data.totalCount})`, type: 'folder', objectType: 1, expanded: expandedNodes.has('queries'), children: queries.data.items?.map((q) => ({ id: q.id || '', label: q.name, type: 'object' as const, objectType: 1 as SqlObjectType, data: q, })) || [], }, { id: 'storedProcedures', label: `${translate('::App.Platform.StoredProcedures')} (${storedProcedures.data.totalCount})`, type: 'folder', objectType: 2, expanded: expandedNodes.has('storedProcedures'), children: storedProcedures.data.items?.map((sp) => ({ id: sp.id || '', label: sp.displayName || sp.procedureName, type: 'object' as const, objectType: 2 as SqlObjectType, data: sp, })) || [], }, { id: 'views', label: `${translate('::App.Platform.Views')} (${views.data.totalCount})`, type: 'folder', objectType: 3, expanded: expandedNodes.has('views'), children: views.data.items?.map((v) => ({ id: v.id || '', label: v.displayName || v.viewName, type: 'object' as const, objectType: 3 as SqlObjectType, data: v, })) || [], }, { id: 'functions', label: `${translate('::App.Platform.Functions')} (${functions.data.totalCount})`, type: 'folder', objectType: 4, expanded: expandedNodes.has('functions'), children: functions.data.items?.map((f) => ({ id: f.id || '', label: f.displayName || f.functionName, 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 toggleNode = (nodeId: string) => { setExpandedNodes((prev) => { const newSet = new Set(prev) if (newSet.has(nodeId)) newSet.delete(nodeId) else newSet.add(nodeId) return newSet }) } const handleNodeClick = (node: TreeNode) => { if (node.type === 'folder' || node.type === 'root') { toggleNode(node.id) } 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 } else if (node.objectType) { onObjectSelect(node.data, node.objectType) } } } const handleContextMenu = (e: React.MouseEvent, node: TreeNode) => { e.preventDefault() 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 sqlQueryService.delete(object.id!) break case 2: await sqlStoredProcedureService.delete(object.id!) break case 3: await sqlViewService.delete(object.id!) break case 4: await sqlFunctionService.delete(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 ? ( ) : ( ) 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 } if (node.objectType === 1) return if (node.objectType === 2) return if (node.objectType === 3) return if (node.objectType === 4) return } return } const renderNode = (node: TreeNode, level = 0) => { const isExpanded = expandedNodes.has(node.id) const isSelected = node.type === 'object' && selectedObject?.id === node.id return (
handleNodeClick(node)} onContextMenu={(e) => handleContextMenu(e, node)} > {getIcon(node)} {node.label}
{isExpanded && node.children && (
{node.children.map((child) => renderNode(child, level + 1))}
)}
) } return (
{loading &&
{translate('::App.Platform.Loading')}
} {!loading && treeData.length === 0 && (
{translate('::App.Platform.NoDataSourceSelected')}
)} {!loading && treeData.length > 0 && (
{treeData.map((node) => renderNode(node))}
)} {contextMenu.show && ( <>
setContextMenu({ show: false, x: 0, y: 0, node: null })} />
{contextMenu.node?.type === 'object' && ( <> )} {contextMenu.node?.type === 'folder' && ( )}
)} setShowDeleteDialog(false)} onRequestClose={() => setShowDeleteDialog(false)} >
{translate('::App.Platform.ConfirmDelete')}

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

) } export default SqlObjectExplorer