2026-02-24 20:44:16 +00:00
|
|
|
|
import { useState, useEffect } from 'react'
|
|
|
|
|
|
import {
|
|
|
|
|
|
FaRegFolder,
|
|
|
|
|
|
FaRegFolderOpen,
|
|
|
|
|
|
FaColumns,
|
|
|
|
|
|
FaSyncAlt,
|
|
|
|
|
|
FaTable,
|
2026-03-01 17:40:25 +00:00
|
|
|
|
FaPlus,
|
2026-03-02 18:31:49 +00:00
|
|
|
|
FaEye,
|
|
|
|
|
|
FaCog,
|
|
|
|
|
|
FaCode,
|
|
|
|
|
|
FaDatabase,
|
|
|
|
|
|
FaTrash,
|
2026-02-24 20:44:16 +00:00
|
|
|
|
} from 'react-icons/fa'
|
2026-03-02 18:31:49 +00:00
|
|
|
|
import type { DatabaseTableDto, SqlNativeObjectDto } from '@/proxy/sql-query-manager/models'
|
2026-02-24 20:44:16 +00:00
|
|
|
|
import { sqlObjectManagerService } from '@/services/sql-query-manager.service'
|
|
|
|
|
|
import { useLocalization } from '@/utils/hooks/useLocalization'
|
|
|
|
|
|
|
2026-03-02 18:31:49 +00:00
|
|
|
|
type FolderKey = 'tables' | 'views' | 'procedures' | 'functions'
|
2026-02-24 20:44:16 +00:00
|
|
|
|
|
|
|
|
|
|
interface TreeNode {
|
|
|
|
|
|
id: string
|
|
|
|
|
|
label: string
|
2026-03-02 18:31:49 +00:00
|
|
|
|
type: 'root' | 'folder' | 'object'
|
|
|
|
|
|
folder?: FolderKey
|
|
|
|
|
|
data?: DatabaseTableDto | SqlNativeObjectDto
|
2026-02-24 20:44:16 +00:00
|
|
|
|
children?: TreeNode[]
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
interface SqlObjectExplorerProps {
|
|
|
|
|
|
dataSource: string | null
|
|
|
|
|
|
onTemplateSelect?: (template: string, templateType: string) => void
|
2026-03-02 18:31:49 +00:00
|
|
|
|
onViewDefinition?: (schemaName: string, objectName: string) => void
|
2026-03-18 09:00:11 +00:00
|
|
|
|
onGenerateTableScript?: (schemaName: string, tableName: string) => void
|
2026-03-01 17:40:25 +00:00
|
|
|
|
onDesignTable?: (schemaName: string, tableName: string) => void
|
|
|
|
|
|
onNewTable?: () => void
|
2026-03-18 09:00:11 +00:00
|
|
|
|
onSelectedObjectsChange?: (objects: SqlExplorerSelectedObject[]) => void
|
2026-02-24 20:44:16 +00:00
|
|
|
|
refreshTrigger?: number
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-18 09:00:11 +00:00
|
|
|
|
export interface SqlExplorerSelectedObject {
|
|
|
|
|
|
id: string
|
|
|
|
|
|
objectType: 'table' | 'view' | 'procedure' | 'function'
|
|
|
|
|
|
schemaName: string
|
|
|
|
|
|
objectName: string
|
|
|
|
|
|
fullName: string
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-02 18:31:49 +00:00
|
|
|
|
const FOLDER_META: Record<FolderKey, { label: string; color: string }> = {
|
|
|
|
|
|
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' },
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-24 20:44:16 +00:00
|
|
|
|
const SqlObjectExplorer = ({
|
|
|
|
|
|
dataSource,
|
|
|
|
|
|
onTemplateSelect,
|
2026-03-02 18:31:49 +00:00
|
|
|
|
onViewDefinition,
|
2026-03-18 09:00:11 +00:00
|
|
|
|
onGenerateTableScript,
|
2026-03-01 17:40:25 +00:00
|
|
|
|
onDesignTable,
|
|
|
|
|
|
onNewTable,
|
2026-03-18 09:00:11 +00:00
|
|
|
|
onSelectedObjectsChange,
|
2026-02-24 20:44:16 +00:00
|
|
|
|
refreshTrigger,
|
|
|
|
|
|
}: SqlObjectExplorerProps) => {
|
|
|
|
|
|
const { translate } = useLocalization()
|
|
|
|
|
|
const [treeData, setTreeData] = useState<TreeNode[]>([])
|
2026-03-02 18:31:49 +00:00
|
|
|
|
const [expandedNodes, setExpandedNodes] = useState<Set<string>>(new Set(['root']))
|
2026-02-24 20:44:16 +00:00
|
|
|
|
const [loading, setLoading] = useState(false)
|
|
|
|
|
|
const [filterText, setFilterText] = useState('')
|
2026-03-18 09:00:11 +00:00
|
|
|
|
const [selectedObjectIds, setSelectedObjectIds] = useState<Set<string>>(new Set())
|
2026-03-02 18:31:49 +00:00
|
|
|
|
const [dropConfirm, setDropConfirm] = useState<{ node: TreeNode } | null>(null)
|
|
|
|
|
|
const [dropping, setDropping] = useState(false)
|
2026-02-24 20:44:16 +00:00
|
|
|
|
const [contextMenu, setContextMenu] = useState<{
|
2026-03-02 18:31:49 +00:00
|
|
|
|
show: boolean; x: number; y: number; node: TreeNode | null
|
2026-02-24 20:44:16 +00:00
|
|
|
|
}>({ show: false, x: 0, y: 0, node: null })
|
|
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
2026-03-18 09:00:11 +00:00
|
|
|
|
if (dataSource) {
|
|
|
|
|
|
loadObjects()
|
|
|
|
|
|
} else {
|
|
|
|
|
|
setTreeData([])
|
|
|
|
|
|
setSelectedObjectIds(new Set())
|
|
|
|
|
|
onSelectedObjectsChange?.([])
|
|
|
|
|
|
}
|
2026-03-02 18:31:49 +00:00
|
|
|
|
}, [dataSource, refreshTrigger])
|
2026-02-24 20:44:16 +00:00
|
|
|
|
|
2026-03-18 09:00:11 +00:00
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
const selected = getSelectedObjects(treeData, selectedObjectIds)
|
|
|
|
|
|
onSelectedObjectsChange?.(selected)
|
|
|
|
|
|
}, [treeData, selectedObjectIds, onSelectedObjectsChange])
|
|
|
|
|
|
|
2026-02-24 20:44:16 +00:00
|
|
|
|
const loadObjects = async () => {
|
|
|
|
|
|
if (!dataSource) return
|
|
|
|
|
|
setLoading(true)
|
|
|
|
|
|
try {
|
2026-03-02 18:31:49 +00:00
|
|
|
|
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,
|
|
|
|
|
|
})
|
2026-02-24 20:44:16 +00:00
|
|
|
|
|
2026-03-02 18:31:49 +00:00
|
|
|
|
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)),
|
|
|
|
|
|
},
|
|
|
|
|
|
],
|
|
|
|
|
|
}]
|
2026-02-24 20:44:16 +00:00
|
|
|
|
|
|
|
|
|
|
setTreeData(tree)
|
|
|
|
|
|
} catch (error: any) {
|
2026-03-02 18:31:49 +00:00
|
|
|
|
console.error('Failed to load objects', error)
|
2026-02-24 20:44:16 +00:00
|
|
|
|
} finally {
|
|
|
|
|
|
setLoading(false)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-02 18:31:49 +00:00
|
|
|
|
const toggleNode = (nodeId: string) => {
|
|
|
|
|
|
setExpandedNodes((prev) => {
|
|
|
|
|
|
const next = new Set(prev)
|
|
|
|
|
|
next.has(nodeId) ? next.delete(nodeId) : next.add(nodeId)
|
|
|
|
|
|
return next
|
2026-02-24 20:44:16 +00:00
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-02 18:31:49 +00:00
|
|
|
|
const filterTree = (nodes: TreeNode[], search: string): TreeNode[] => {
|
|
|
|
|
|
if (!search.trim()) return nodes
|
|
|
|
|
|
const q = search.toLowerCase()
|
|
|
|
|
|
return nodes
|
2026-02-24 20:44:16 +00:00
|
|
|
|
.map((node) => {
|
2026-03-02 18:31:49 +00:00
|
|
|
|
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
|
2026-02-24 20:44:16 +00:00
|
|
|
|
return null
|
|
|
|
|
|
})
|
2026-03-02 18:31:49 +00:00
|
|
|
|
.filter(Boolean) as TreeNode[]
|
2026-02-24 20:44:16 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const handleNodeClick = (node: TreeNode) => {
|
2026-03-02 18:31:49 +00:00
|
|
|
|
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)
|
2026-02-24 20:44:16 +00:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-18 09:00:11 +00:00
|
|
|
|
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 getSelectedObjects = (nodes: TreeNode[], ids: Set<string>): SqlExplorerSelectedObject[] => {
|
|
|
|
|
|
const selected: SqlExplorerSelectedObject[] = []
|
|
|
|
|
|
|
|
|
|
|
|
const walk = (list: TreeNode[]) => {
|
|
|
|
|
|
for (const node of list) {
|
|
|
|
|
|
if (node.type === 'object' && ids.has(node.id)) {
|
|
|
|
|
|
const mapped = mapNodeToSelectedObject(node)
|
|
|
|
|
|
if (mapped) selected.push(mapped)
|
|
|
|
|
|
}
|
|
|
|
|
|
if (node.children?.length) walk(node.children)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
walk(nodes)
|
|
|
|
|
|
return selected
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const toggleObjectSelection = (nodeId: string, checked: boolean) => {
|
|
|
|
|
|
setSelectedObjectIds((prev) => {
|
|
|
|
|
|
const next = new Set(prev)
|
|
|
|
|
|
if (checked) next.add(nodeId)
|
|
|
|
|
|
else next.delete(nodeId)
|
|
|
|
|
|
return next
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-02 18:31:49 +00:00
|
|
|
|
const buildDropSql = (node: TreeNode): string => {
|
|
|
|
|
|
if (node.folder === 'tables') {
|
|
|
|
|
|
const t = node.data as DatabaseTableDto
|
|
|
|
|
|
return `DROP TABLE ${t.fullName ?? `[${t.schemaName}].[${t.tableName}]`};`
|
2026-02-24 20:44:16 +00:00
|
|
|
|
}
|
2026-03-02 18:31:49 +00:00
|
|
|
|
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}]`};`
|
2026-02-24 20:44:16 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-02 18:31:49 +00:00
|
|
|
|
const handleDrop = async () => {
|
|
|
|
|
|
if (!dropConfirm || !dataSource) return
|
|
|
|
|
|
setDropping(true)
|
2026-02-24 20:44:16 +00:00
|
|
|
|
try {
|
2026-03-02 18:31:49 +00:00
|
|
|
|
await sqlObjectManagerService.executeQuery({
|
|
|
|
|
|
queryText: buildDropSql(dropConfirm.node),
|
|
|
|
|
|
dataSourceCode: dataSource,
|
|
|
|
|
|
})
|
|
|
|
|
|
setDropConfirm(null)
|
2026-02-24 20:44:16 +00:00
|
|
|
|
loadObjects()
|
2026-03-02 18:31:49 +00:00
|
|
|
|
} catch (err: any) {
|
|
|
|
|
|
console.error('Drop failed', err)
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
setDropping(false)
|
2026-02-24 20:44:16 +00:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-02 18:31:49 +00:00
|
|
|
|
const closeCtx = () => setContextMenu({ show: false, x: 0, y: 0, node: null })
|
2026-02-24 20:44:16 +00:00
|
|
|
|
|
2026-03-02 18:31:49 +00:00
|
|
|
|
const getIcon = (node: TreeNode) => {
|
|
|
|
|
|
if (node.type === 'root') return <FaDatabase className="text-blue-500" />
|
2026-02-24 20:44:16 +00:00
|
|
|
|
if (node.type === 'folder') {
|
2026-03-02 18:31:49 +00:00
|
|
|
|
const open = expandedNodes.has(node.id)
|
|
|
|
|
|
const cls = FOLDER_META[node.folder!]?.color ?? 'text-blue-500'
|
|
|
|
|
|
return open
|
|
|
|
|
|
? <FaRegFolderOpen className={cls} />
|
|
|
|
|
|
: <FaRegFolder className={cls} />
|
2026-02-24 20:44:16 +00:00
|
|
|
|
}
|
2026-03-02 18:31:49 +00:00
|
|
|
|
if (node.folder === 'tables') return <FaTable className="text-teal-500" />
|
|
|
|
|
|
if (node.folder === 'views') return <FaEye className="text-purple-500" />
|
|
|
|
|
|
if (node.folder === 'procedures') return <FaCog className="text-green-600" />
|
|
|
|
|
|
if (node.folder === 'functions') return <FaCode className="text-orange-500" />
|
|
|
|
|
|
return <FaColumns className="text-gray-400" />
|
2026-02-24 20:44:16 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const renderNode = (node: TreeNode, level = 0) => {
|
|
|
|
|
|
const isExpanded = expandedNodes.has(node.id)
|
2026-03-18 09:00:11 +00:00
|
|
|
|
const isChecked = selectedObjectIds.has(node.id)
|
2026-02-24 20:44:16 +00:00
|
|
|
|
return (
|
|
|
|
|
|
<div key={node.id}>
|
|
|
|
|
|
<div
|
2026-03-02 18:31:49 +00:00
|
|
|
|
className="group flex items-center gap-2 py-1 px-2 cursor-pointer hover:bg-gray-100 dark:hover:bg-gray-700 rounded"
|
2026-02-24 20:44:16 +00:00
|
|
|
|
style={{ paddingLeft: `${level * 16 + 8}px` }}
|
2026-03-02 18:31:49 +00:00
|
|
|
|
onClick={() => handleNodeClick(node)}
|
|
|
|
|
|
onContextMenu={(e) => { e.preventDefault(); setContextMenu({ show: true, x: e.clientX, y: e.clientY, node }) }}
|
2026-02-24 20:44:16 +00:00
|
|
|
|
>
|
2026-03-18 09:00:11 +00:00
|
|
|
|
{node.type === 'object' && (
|
|
|
|
|
|
<input
|
|
|
|
|
|
type="checkbox"
|
|
|
|
|
|
className="h-4 w-4"
|
|
|
|
|
|
checked={isChecked}
|
|
|
|
|
|
onChange={(e) => toggleObjectSelection(node.id, e.target.checked)}
|
|
|
|
|
|
onClick={(e) => e.stopPropagation()}
|
|
|
|
|
|
/>
|
|
|
|
|
|
)}
|
2026-02-24 20:44:16 +00:00
|
|
|
|
{getIcon(node)}
|
2026-03-02 18:31:49 +00:00
|
|
|
|
<span className="text-sm flex-1 truncate">{node.label}</span>
|
|
|
|
|
|
{node.type === 'object' && (
|
|
|
|
|
|
<button
|
|
|
|
|
|
title="Drop"
|
|
|
|
|
|
className="opacity-0 group-hover:opacity-100 p-1 rounded hover:bg-red-100 dark:hover:bg-red-900 text-red-500 transition-opacity flex-shrink-0"
|
|
|
|
|
|
onClick={(e) => { e.stopPropagation(); setDropConfirm({ node }) }}
|
|
|
|
|
|
>
|
|
|
|
|
|
<FaTrash className="text-xs" />
|
|
|
|
|
|
</button>
|
|
|
|
|
|
)}
|
2026-02-24 20:44:16 +00:00
|
|
|
|
</div>
|
|
|
|
|
|
{isExpanded && node.children && (
|
2026-03-02 18:31:49 +00:00
|
|
|
|
<div>{node.children.map((c) => renderNode(c, level + 1))}</div>
|
2026-02-24 20:44:16 +00:00
|
|
|
|
)}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const filteredTree = filterTree(treeData, filterText)
|
|
|
|
|
|
|
2026-03-02 18:31:49 +00:00
|
|
|
|
// 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'
|
|
|
|
|
|
|
2026-02-24 20:44:16 +00:00
|
|
|
|
return (
|
2026-03-02 18:31:49 +00:00
|
|
|
|
<div className="flex-1 flex flex-col min-h-0">
|
|
|
|
|
|
{/* Search + refresh */}
|
|
|
|
|
|
<div className="p-2 border-b flex gap-2 flex-shrink-0">
|
|
|
|
|
|
<input
|
|
|
|
|
|
type="text"
|
|
|
|
|
|
placeholder={translate('::App.Platform.Search')}
|
|
|
|
|
|
value={filterText}
|
|
|
|
|
|
onChange={(e) => 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"
|
|
|
|
|
|
/>
|
|
|
|
|
|
<button
|
|
|
|
|
|
onClick={loadObjects}
|
|
|
|
|
|
disabled={loading || !dataSource}
|
|
|
|
|
|
className="px-3 py-1.5 text-sm border rounded-md hover:bg-gray-100 dark:hover:bg-gray-700 disabled:opacity-50"
|
|
|
|
|
|
title={translate('::App.Platform.Refresh')}
|
|
|
|
|
|
>
|
|
|
|
|
|
<FaSyncAlt className={loading ? 'animate-spin' : ''} />
|
|
|
|
|
|
</button>
|
2026-02-24 20:44:16 +00:00
|
|
|
|
</div>
|
|
|
|
|
|
|
2026-03-02 18:31:49 +00:00
|
|
|
|
{/* Tree */}
|
|
|
|
|
|
<div className="flex-1 overflow-auto">
|
|
|
|
|
|
{loading && <div className="text-center py-8 text-gray-500 text-sm">{translate('::App.Platform.Loading')}</div>}
|
|
|
|
|
|
{!loading && treeData.length === 0 && <div className="text-center py-8 text-gray-500 text-sm">{translate('::App.Platform.NoDataSourceSelected')}</div>}
|
|
|
|
|
|
{!loading && filteredTree.length > 0 && <div className="p-1">{filteredTree.map((n) => renderNode(n))}</div>}
|
2026-03-16 09:52:15 +00:00
|
|
|
|
{!loading && treeData.length > 0 && filteredTree.length === 0 && <div className="text-center py-8 text-gray-500 text-sm">{translate('::App.Platform.NoResults')}</div>}
|
2026-02-24 20:44:16 +00:00
|
|
|
|
</div>
|
|
|
|
|
|
|
2026-03-02 18:31:49 +00:00
|
|
|
|
{/* Context menu */}
|
2026-02-24 20:44:16 +00:00
|
|
|
|
{contextMenu.show && (
|
|
|
|
|
|
<>
|
2026-03-02 18:31:49 +00:00
|
|
|
|
<div className="fixed inset-0 z-40" onClick={closeCtx} />
|
2026-02-24 20:44:16 +00:00
|
|
|
|
<div
|
2026-03-02 18:31:49 +00:00
|
|
|
|
className="fixed z-50 bg-white dark:bg-gray-800 shadow-lg rounded border border-gray-200 dark:border-gray-700 py-1 min-w-[180px]"
|
|
|
|
|
|
style={{ top: contextMenu.y, left: contextMenu.x }}
|
|
|
|
|
|
>
|
|
|
|
|
|
{/* TABLE object <20> Design */}
|
|
|
|
|
|
{isTableObj && (
|
2026-03-18 09:00:11 +00:00
|
|
|
|
<button
|
|
|
|
|
|
className="w-full px-4 py-2 text-left hover:bg-gray-100 dark:hover:bg-gray-700 text-sm flex items-center gap-2"
|
|
|
|
|
|
onClick={() => {
|
|
|
|
|
|
const t = ctxNode!.data as DatabaseTableDto
|
|
|
|
|
|
onGenerateTableScript?.(t.schemaName, t.tableName)
|
|
|
|
|
|
closeCtx()
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
|
|
|
|
|
<FaCode className="text-blue-600" /> Script olustur
|
|
|
|
|
|
</button>
|
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
|
|
{isTableObj && (
|
|
|
|
|
|
<button
|
|
|
|
|
|
className="w-full px-4 py-2 text-left hover:bg-gray-100 dark:hover:bg-gray-700 text-sm flex items-center gap-2"
|
2026-03-02 18:31:49 +00:00
|
|
|
|
onClick={() => {
|
|
|
|
|
|
const t = ctxNode!.data as DatabaseTableDto
|
|
|
|
|
|
onDesignTable?.(t.schemaName, t.tableName)
|
|
|
|
|
|
closeCtx()
|
2026-03-18 09:00:11 +00:00
|
|
|
|
}}
|
|
|
|
|
|
>
|
2026-03-02 18:31:49 +00:00
|
|
|
|
<FaTable className="text-teal-600" /> Design Table
|
|
|
|
|
|
</button>
|
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
|
|
{/* NATIVE object <20> View Definition */}
|
|
|
|
|
|
{isNativeObj && (
|
|
|
|
|
|
<button className="w-full px-4 py-2 text-left hover:bg-gray-100 dark:hover:bg-gray-700 text-sm flex items-center gap-2"
|
|
|
|
|
|
onClick={() => {
|
|
|
|
|
|
const obj = ctxNode!.data as SqlNativeObjectDto
|
|
|
|
|
|
onViewDefinition?.(obj.schemaName, obj.objectName)
|
|
|
|
|
|
closeCtx()
|
|
|
|
|
|
}}>
|
|
|
|
|
|
{ctxNode!.folder === 'views' && <FaEye className="text-purple-500" />}
|
|
|
|
|
|
{ctxNode!.folder === 'procedures' && <FaCog className="text-green-600" />}
|
|
|
|
|
|
{ctxNode!.folder === 'functions' && <FaCode className="text-orange-500" />}
|
|
|
|
|
|
View Definition
|
|
|
|
|
|
</button>
|
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
|
|
{/* FOLDER <20> New ... */}
|
|
|
|
|
|
{isTablesDir && (
|
|
|
|
|
|
<button className="w-full px-4 py-2 text-left hover:bg-gray-100 dark:hover:bg-gray-700 text-sm flex items-center gap-2"
|
|
|
|
|
|
onClick={() => { onNewTable?.(); closeCtx() }}>
|
|
|
|
|
|
<FaPlus className="text-teal-600" /> New Table
|
|
|
|
|
|
</button>
|
|
|
|
|
|
)}
|
|
|
|
|
|
{isViewsDir && (
|
|
|
|
|
|
<button className="w-full px-4 py-2 text-left hover:bg-gray-100 dark:hover:bg-gray-700 text-sm flex items-center gap-2"
|
|
|
|
|
|
onClick={() => { onTemplateSelect?.('', 'create-view'); closeCtx() }}>
|
|
|
|
|
|
<FaPlus className="text-purple-500" /> New View
|
|
|
|
|
|
</button>
|
|
|
|
|
|
)}
|
|
|
|
|
|
{isProcsDir && (
|
|
|
|
|
|
<button className="w-full px-4 py-2 text-left hover:bg-gray-100 dark:hover:bg-gray-700 text-sm flex items-center gap-2"
|
|
|
|
|
|
onClick={() => { onTemplateSelect?.('', 'create-procedure'); closeCtx() }}>
|
|
|
|
|
|
<FaPlus className="text-green-600" /> New Stored Procedure
|
|
|
|
|
|
</button>
|
|
|
|
|
|
)}
|
|
|
|
|
|
{isFuncsDir && (
|
|
|
|
|
|
<>
|
|
|
|
|
|
<button className="w-full px-4 py-2 text-left hover:bg-gray-100 dark:hover:bg-gray-700 text-sm flex items-center gap-2"
|
|
|
|
|
|
onClick={() => { onTemplateSelect?.('', 'create-scalar-function'); closeCtx() }}>
|
|
|
|
|
|
<FaPlus className="text-orange-500" /> New Scalar Function
|
|
|
|
|
|
</button>
|
|
|
|
|
|
<button className="w-full px-4 py-2 text-left hover:bg-gray-100 dark:hover:bg-gray-700 text-sm flex items-center gap-2"
|
|
|
|
|
|
onClick={() => { onTemplateSelect?.('', 'create-table-function'); closeCtx() }}>
|
|
|
|
|
|
<FaPlus className="text-orange-500" /> New Table-Valued Function
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</>
|
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
|
|
{/* Separator + Refresh for folders */}
|
|
|
|
|
|
{isFolderNode && (
|
|
|
|
|
|
<>
|
|
|
|
|
|
<div className="my-1 border-t border-gray-100 dark:border-gray-700" />
|
|
|
|
|
|
<button className="w-full px-4 py-2 text-left hover:bg-gray-100 dark:hover:bg-gray-700 text-sm flex items-center gap-2"
|
|
|
|
|
|
onClick={() => { loadObjects(); closeCtx() }}>
|
|
|
|
|
|
<FaSyncAlt /> {translate('::App.Platform.Refresh')}
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</>
|
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
|
|
{/* Drop Confirm Dialog */}
|
|
|
|
|
|
{dropConfirm && (
|
|
|
|
|
|
<>
|
|
|
|
|
|
<div className="fixed inset-0 z-50 bg-black/40 flex items-center justify-center" onClick={() => !dropping && setDropConfirm(null)}>
|
|
|
|
|
|
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-xl p-6 max-w-sm w-full mx-4" onClick={(e) => e.stopPropagation()}>
|
|
|
|
|
|
<div className="flex items-center gap-3 mb-3">
|
|
|
|
|
|
<FaTrash className="text-red-500 text-lg flex-shrink-0" />
|
|
|
|
|
|
<h6 className="font-semibold text-gray-900 dark:text-gray-100">Drop Object</h6>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<p className="text-sm text-gray-600 dark:text-gray-400 mb-1">
|
|
|
|
|
|
The following object will be permanently dropped:
|
|
|
|
|
|
</p>
|
|
|
|
|
|
<code className="block text-sm bg-gray-100 dark:bg-gray-700 rounded px-3 py-2 mb-4 break-all">
|
|
|
|
|
|
{buildDropSql(dropConfirm.node)}
|
|
|
|
|
|
</code>
|
|
|
|
|
|
<div className="flex justify-end gap-2">
|
2026-03-01 17:40:25 +00:00
|
|
|
|
<button
|
2026-03-02 18:31:49 +00:00
|
|
|
|
disabled={dropping}
|
|
|
|
|
|
className="px-4 py-1.5 text-sm rounded border hover:bg-gray-100 dark:hover:bg-gray-700 disabled:opacity-50"
|
|
|
|
|
|
onClick={() => setDropConfirm(null)}
|
2026-03-01 17:40:25 +00:00
|
|
|
|
>
|
2026-03-02 18:31:49 +00:00
|
|
|
|
Cancel
|
2026-03-01 17:40:25 +00:00
|
|
|
|
</button>
|
2026-03-02 18:31:49 +00:00
|
|
|
|
<button
|
|
|
|
|
|
disabled={dropping}
|
|
|
|
|
|
className="px-4 py-1.5 text-sm rounded bg-red-600 text-white hover:bg-red-700 disabled:opacity-50 flex items-center gap-2"
|
|
|
|
|
|
onClick={handleDrop}
|
|
|
|
|
|
>
|
|
|
|
|
|
{dropping && <FaSyncAlt className="animate-spin text-xs" />}
|
|
|
|
|
|
Drop
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
2026-02-24 20:44:16 +00:00
|
|
|
|
</div>
|
2026-03-02 18:31:49 +00:00
|
|
|
|
</div>
|
2026-02-24 20:44:16 +00:00
|
|
|
|
</>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
export default SqlObjectExplorer
|