import { useState, useEffect } from 'react' import { FaRegFolder, FaRegFolderOpen, FaColumns, FaSyncAlt, FaTable, FaPlus, FaEye, FaCog, FaCode, FaDatabase, FaTrash, FaBolt, FaCopy, } from 'react-icons/fa' import type { DatabaseTableDto, SqlNativeObjectDto } from '@/proxy/sql-query-manager/models' import type { CrudEndpoint } from '@/proxy/developerKit/models' import { DataSourceTypeEnum } from '@/proxy/form/models' import { sqlObjectManagerService } from '@/services/sql-query-manager.service' import { useLocalization } from '@/utils/hooks/useLocalization' import Button from '@/components/ui/Button' import { Notification, toast } from '@/components/ui' import { CrudEndpointBadges, getErrorMessage, toEntityName } from './CrudEndpointDialog' import { developerKitService } from '@/services/developerKit.service' 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 dataSourceType?: DataSourceTypeEnum onTemplateSelect?: (template: string, templateType: string) => void onViewDefinition?: (schemaName: string, objectName: string) => void onGenerateTableScript?: (schemaName: string, tableName: string) => void onDesignTable?: (schemaName: string, tableName: string) => void onNewTable?: () => void onSelectedObjectsChange?: (objects: SqlExplorerSelectedObject[]) => void /** Entity adina gore acilmis CRUD endpoint'leri; tablo satirlarinda ikon olarak gosterilir. */ crudEndpointsByEntity?: Map onManageCrudEndpoints?: (schemaName: string, tableName: string) => void /** Secili tablolar icin CRUD endpoint uretir; verilmezse buton gosterilmez. */ onGenerateCrudEndpoints?: () => void isGeneratingCrudEndpoints?: boolean /** Kopyalama / dogrudan SQL diyalogunu acar; verilmezse buton gosterilmez. */ onCopySelectedObjects?: () => void /** Explorer endpoint'leri degistirdiginde (orn. tablo drop) listeyi tazelemek icin cagrilir. */ onCrudEndpointsChanged?: () => void refreshTrigger?: number } export interface SqlExplorerSelectedObject { id: string objectType: 'table' | 'view' | 'procedure' | 'function' schemaName: string objectName: string fullName: string } 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, dataSourceType, onTemplateSelect, onViewDefinition, onGenerateTableScript, onDesignTable, onNewTable, onSelectedObjectsChange, crudEndpointsByEntity, onManageCrudEndpoints, onGenerateCrudEndpoints, isGeneratingCrudEndpoints, onCopySelectedObjects, onCrudEndpointsChanged, refreshTrigger, }: SqlObjectExplorerProps) => { const { translate } = useLocalization() const isPostgreSql = dataSourceType === DataSourceTypeEnum.Postgresql const [treeData, setTreeData] = useState([]) const [expandedNodes, setExpandedNodes] = useState>(new Set(['root'])) const [loading, setLoading] = useState(false) const [filterText, setFilterText] = useState('') const [selectedObjectIds, setSelectedObjectIds] = useState>(new Set()) /** * Silinmek uzere onay bekleyen nesneler. Satirdaki cop kutusu tek nesneyi, * baslikaki "secilenleri sil" tum secimi ayni listeye koyar; boylece tekil ve * toplu silme tek kod yolundan gecer. */ const [dropTargets, setDropTargets] = useState(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([]) setSelectedObjectIds(new Set()) onSelectedObjectsChange?.([]) } }, [dataSource, refreshTrigger]) useEffect(() => { const selected = getSelectedObjects(treeData, selectedObjectIds) onSelectedObjectsChange?.(selected) }, [treeData, selectedObjectIds, onSelectedObjectsChange]) 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) if (next.has(nodeId)) { next.delete(nodeId) } else { 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[] } /** * Tek tik nesneyi secer/secimi kaldirir (checkbox ile ayni state), klasorlerde acar/kapatir. * Sorgu sablonu ya da tanim yuklemek icin cift tik kullanilir. */ const handleNodeClick = (node: TreeNode) => { if (node.type !== 'object') { toggleNode(node.id) return } toggleObjectSelection(node.id, !selectedObjectIds.has(node.id)) } const handleNodeDoubleClick = (node: TreeNode) => { if (node.type !== 'object') return if (node.folder === 'tables') { // Generate SELECT template for tables const t = node.data as DatabaseTableDto const fullName = t.fullName ?? (isPostgreSql ? `"${t.schemaName}"."${t.tableName}"` : `[${t.schemaName}].[${t.tableName}]`) onTemplateSelect?.( isPostgreSql ? `SELECT *\nFROM ${fullName}\nLIMIT 10;` : `SELECT TOP 10 *\nFROM ${fullName};`, 'table-select', ) } else { // Load native object definition into editor const obj = node.data as SqlNativeObjectDto onViewDefinition?.(obj.schemaName, obj.objectName) } } const mapNodeToSelectedObject = (node: TreeNode): SqlExplorerSelectedObject | null => { if (node.type !== 'object' || !node.folder || !node.data) return null if (node.folder === 'tables') { const t = node.data as DatabaseTableDto return { id: node.id, objectType: 'table', schemaName: t.schemaName, objectName: t.tableName, fullName: t.fullName ?? `[${t.schemaName}].[${t.tableName}]`, } } const obj = node.data as SqlNativeObjectDto const objectType = node.folder === 'views' ? 'view' : node.folder === 'procedures' ? 'procedure' : 'function' return { id: node.id, objectType, schemaName: obj.schemaName, objectName: obj.objectName, fullName: obj.fullName ?? `[${obj.schemaName}].[${obj.objectName}]`, } } const getSelectedNodes = (nodes: TreeNode[], ids: Set): TreeNode[] => { const selected: TreeNode[] = [] const walk = (list: TreeNode[]) => { for (const node of list) { if (node.type === 'object' && ids.has(node.id)) selected.push(node) if (node.children?.length) walk(node.children) } } walk(nodes) return selected } const getSelectedObjects = (nodes: TreeNode[], ids: Set): SqlExplorerSelectedObject[] => getSelectedNodes(nodes, ids) .map(mapNodeToSelectedObject) .filter((obj): obj is SqlExplorerSelectedObject => obj !== null) const toggleObjectSelection = (nodeId: string, checked: boolean) => { setSelectedObjectIds((prev) => { const next = new Set(prev) if (checked) next.add(nodeId) else next.delete(nodeId) return next }) } const buildDropSql = (node: TreeNode): string => { if (node.folder === 'tables') { const t = node.data as DatabaseTableDto const fullName = t.fullName ?? (isPostgreSql ? `"${t.schemaName}"."${t.tableName}"` : `[${t.schemaName}].[${t.tableName}]`) return isPostgreSql ? `DROP TABLE IF EXISTS ${fullName};` : `DROP TABLE ${fullName};` } const obj = node.data as SqlNativeObjectDto const fullName = obj.fullName ?? (isPostgreSql ? `"${obj.schemaName}"."${obj.objectName}"` : `[${obj.schemaName}].[${obj.objectName}]`) const keyword = node.folder === 'views' ? 'VIEW' : node.folder === 'procedures' ? 'PROCEDURE' : 'FUNCTION' return isPostgreSql ? `DROP ${keyword} IF EXISTS ${fullName};` : `DROP ${keyword} ${fullName};` } const getSqlDataFileCandidates = (node: TreeNode): string[] => { if (node.folder === 'tables') { const t = node.data as DatabaseTableDto return [t.tableName, `${t.schemaName}_${t.tableName}`] } const obj = node.data as SqlNativeObjectDto return [obj.objectName, `${obj.schemaName}_${obj.objectName}`] } /** * Onaylanan nesneleri tek tek dusurur; bir nesnenin hatasi digerlerini engellemez. * Basarili olanlar icin SQL seed dosyalari ve (tablolarda) CRUD endpoint'leri de temizlenir. */ const handleDrop = async () => { if (!dropTargets?.length || !dataSource) return setDropping(true) const dropped: TreeNode[] = [] const failures: string[] = [] try { for (const node of dropTargets) { try { const result = await sqlObjectManagerService.executeQuery({ queryText: buildDropSql(node), dataSourceCode: dataSource, }) if (result.data?.success === false) { throw new Error(result.data.error || result.data.message) } dropped.push(node) } catch (err) { failures.push( `${node.label}: ${getErrorMessage(err, translate('::App.Platform.IslemBasarisiz'))}`, ) } } if (dropped.length > 0) { // configs/seeds/SqlData altindaki eslesen seed dosyalari da silinir. try { const fileNames = [ ...new Set(dropped.flatMap(getSqlDataFileCandidates).filter(Boolean)), ] if (fileNames.length > 0) { await sqlObjectManagerService.deleteSqlDataFiles({ fileNames }) } } catch { // Non-blocking: object drop succeeded even if seed file cleanup fails. } // Tablo dusurulduyse ona ait CRUD endpoint'leri de anlamsiz kalir; kayitlar // silinince backend crud/{EntityName}.json seed dosyasini da kaldirir. const droppedTables = dropped.filter((node) => node.folder === 'tables') if (droppedTables.length > 0) { for (const node of droppedTables) { try { const table = node.data as DatabaseTableDto await developerKitService.deleteGeneratedEndpointsByEntity( toEntityName(table.tableName), ) } catch { // Non-blocking: izin yoksa ya da endpoint yoksa drop yine basarilidir. } } onCrudEndpointsChanged?.() } // Silinen nesneler secimde kalmasin; aksi halde toolbar sayaci yaniltir. const droppedIds = new Set(dropped.map((node) => node.id)) setSelectedObjectIds((prev) => new Set([...prev].filter((id) => !droppedIds.has(id)))) } toast.push( 0 ? 'warning' : 'success'} title={ failures.length > 0 ? translate('::App.Platform.Warning') : translate('::App.Platform.Success') } > {failures.length > 0 ? failures.join(' | ') : `${translate('::App.Platform.OperationCompleted')} (${dropped.length})`} , { placement: 'bottom-end' }, ) setDropTargets(null) loadObjects() } 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 getNodeCrudEndpoints = (node: TreeNode): CrudEndpoint[] => { if (node.type !== 'object' || node.folder !== 'tables' || !crudEndpointsByEntity) return [] const table = node.data as DatabaseTableDto return crudEndpointsByEntity.get(toEntityName(table.tableName)) ?? [] } const buildCrudEndpointTitle = (endpoint: CrudEndpoint) => `${endpoint.operationType} — ${endpoint.method} ${endpoint.path} (${ endpoint.isActive ? translate('::App.Listform.ListformField.IsActive') : translate('::App.DeveloperKitCrudEndpoints.InactiveCount') })` const renderNode = (node: TreeNode, level = 0) => { const isExpanded = expandedNodes.has(node.id) const isChecked = selectedObjectIds.has(node.id) const crudEndpoints = getNodeCrudEndpoints(node) return (
handleNodeClick(node)} onDoubleClick={() => handleNodeDoubleClick(node)} onContextMenu={(e) => { e.preventDefault() setContextMenu({ show: true, x: e.clientX, y: e.clientY, node }) }} > {node.type === 'object' && ( toggleObjectSelection(node.id, e.target.checked)} onClick={(e) => e.stopPropagation()} /> )} {getIcon(node)} {node.label} {crudEndpoints.length > 0 && ( { const table = node.data as DatabaseTableDto onManageCrudEndpoints(table.schemaName, table.tableName) } : undefined } /> )} {node.type === 'object' && (
{isExpanded && node.children && (
{node.children.map((c) => renderNode(c, level + 1))}
)}
) } const filteredTree = filterTree(treeData, filterText) /** CRUD endpoint uretimi yalnizca tablolar icin anlamli. */ const selectedTableCount = getSelectedNodes(treeData, selectedObjectIds).filter( (node) => node.folder === 'tables', ).length useEffect(() => { if (filterText.trim()) { const allIds = new Set() const collect = (nodes: TreeNode[]) => { for (const node of nodes) { allIds.add(node.id) if (node.children?.length) collect(node.children) } } collect(filteredTree) setExpandedNodes(allIds) } else { setExpandedNodes(new Set(['root'])) } }, [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 (
{/* Nesne islemleri seridi. Yuksekligi sabit (min-h) — secim yapildikca butonlar gorunse de agac asagi kaymaz. */}
{selectedObjectIds.size > 0 && ( <> {selectedObjectIds.size} {translate('::App.SqlQueryManager.Selected')} )}
{onCopySelectedObjects && selectedObjectIds.size > 0 && ( )} {selectedObjectIds.size > 0 && ( )} {onGenerateCrudEndpoints && selectedTableCount > 0 && ( )}
{/* Search + refresh */}
setFilterText(e.target.value)} className="flex-1 px-3 py-1.5 text-sm border rounded-md bg-white dark:bg-gray-800 border-gray-300 dark:border-gray-700 text-gray-900 dark:text-gray-100" />
{/* Tree */}
{loading && (
{translate('::App.Platform.LoadingWithThreeDot')}
)} {!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.NoResults')}
)}
{/* Context menu */} {contextMenu.show && ( <>
{/* TABLE object � Design */} {isTableObj && ( )} {isTableObj && ( )} {isTableObj && onManageCrudEndpoints && ( )} {/* NATIVE object � View Definition */} {isNativeObj && ( )} {/* FOLDER � New ... */} {isTablesDir && ( )} {isViewsDir && ( )} {isProcsDir && ( )} {isFuncsDir && ( <> )} {/* Separator + Refresh for folders */} {isFolderNode && ( <>
)}
)} {/* Drop Confirm Dialog — tekil ve toplu silmede ayni diyalog */} {dropTargets && dropTargets.length > 0 && (
!dropping && setDropTargets(null)} >
e.stopPropagation()} >
{translate('::App.SqlQueryManager.DropObjects')} ({dropTargets.length})

{translate('::App.SqlQueryManager.DropObjectsWarning')}

{dropTargets.map((node) => ( {buildDropSql(node)} ))}
)}
) } export default SqlObjectExplorer