{translate('::App.Platform.QueryEditor')}
diff --git a/ui/src/views/sqlQueryManager/components/SqlObjectExplorer.tsx b/ui/src/views/sqlQueryManager/components/SqlObjectExplorer.tsx
index a8fc7460..54d29a7a 100644
--- a/ui/src/views/sqlQueryManager/components/SqlObjectExplorer.tsx
+++ b/ui/src/views/sqlQueryManager/components/SqlObjectExplorer.tsx
@@ -10,7 +10,9 @@ import {
FaSyncAlt,
FaEdit,
FaTrash,
+ FaTable,
} from 'react-icons/fa'
+import { MdViewColumn } from 'react-icons/md'
import type { DataSourceDto } from '@/proxy/data-source'
import type {
SqlFunctionDto,
@@ -20,10 +22,7 @@ import type {
SqlObjectType,
} from '@/proxy/sql-query-manager/models'
import {
- sqlFunctionService,
- sqlQueryService,
- sqlStoredProcedureService,
- sqlViewService,
+ sqlObjectManagerService,
} from '@/services/sql-query-manager.service'
import { useLocalization } from '@/utils/hooks/useLocalization'
@@ -32,11 +31,13 @@ export type SqlObject = SqlFunctionDto | SqlQueryDto | SqlStoredProcedureDto | S
interface TreeNode {
id: string
label: string
- type: 'root' | 'folder' | 'object'
+ type: 'root' | 'folder' | 'object' | 'column'
objectType?: SqlObjectType
- data?: SqlObject
+ data?: SqlObject | any
children?: TreeNode[]
expanded?: boolean
+ isColumn?: boolean
+ parentTable?: { schemaName: string; tableName: string }
}
interface SqlObjectExplorerProps {
@@ -55,9 +56,10 @@ const SqlObjectExplorer = ({
const { translate } = useLocalization()
const [treeData, setTreeData] = useState
([])
const [expandedNodes, setExpandedNodes] = useState>(
- new Set(['root', 'templates', 'queries', 'storedProcedures', 'views', 'functions']),
+ 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
@@ -83,28 +85,10 @@ const SqlObjectExplorer = ({
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,
- }),
- ])
+ // Single API call to get all objects
+ const response = await sqlObjectManagerService.getAllObjects(dataSource.code || '')
+ const allObjects = response.data
+
const tree: TreeNode[] = [
{
id: 'root',
@@ -168,14 +152,27 @@ const SqlObjectExplorer = ({
},
],
},
+ {
+ 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')} (${queries.data.totalCount})`,
+ label: `${translate('::App.Platform.Queries')} (${allObjects.queries.length})`,
type: 'folder',
objectType: 1,
expanded: expandedNodes.has('queries'),
children:
- queries.data.items?.map((q) => ({
+ allObjects.queries.map((q) => ({
id: q.id || '',
label: q.name,
type: 'object' as const,
@@ -185,12 +182,12 @@ const SqlObjectExplorer = ({
},
{
id: 'storedProcedures',
- label: `${translate('::App.Platform.StoredProcedures')} (${storedProcedures.data.totalCount})`,
+ label: `${translate('::App.Platform.StoredProcedures')} (${allObjects.storedProcedures.length})`,
type: 'folder',
objectType: 2,
expanded: expandedNodes.has('storedProcedures'),
children:
- storedProcedures.data.items?.map((sp) => ({
+ allObjects.storedProcedures.map((sp) => ({
id: sp.id || '',
label: sp.displayName || sp.procedureName,
type: 'object' as const,
@@ -200,12 +197,12 @@ const SqlObjectExplorer = ({
},
{
id: 'views',
- label: `${translate('::App.Platform.Views')} (${views.data.totalCount})`,
+ label: `${translate('::App.Platform.Views')} (${allObjects.views.length})`,
type: 'folder',
objectType: 3,
expanded: expandedNodes.has('views'),
children:
- views.data.items?.map((v) => ({
+ allObjects.views.map((v) => ({
id: v.id || '',
label: v.displayName || v.viewName,
type: 'object' as const,
@@ -215,12 +212,12 @@ const SqlObjectExplorer = ({
},
{
id: 'functions',
- label: `${translate('::App.Platform.Functions')} (${functions.data.totalCount})`,
+ label: `${translate('::App.Platform.Functions')} (${allObjects.functions.length})`,
type: 'folder',
objectType: 4,
expanded: expandedNodes.has('functions'),
children:
- functions.data.items?.map((f) => ({
+ allObjects.functions.map((f) => ({
id: f.id || '',
label: f.displayName || f.functionName,
type: 'object' as const,
@@ -245,24 +242,116 @@ const SqlObjectExplorer = ({
}
}
- 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 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
- } else if (node.objectType) {
+ }
+ // 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)
}
}
@@ -270,6 +359,12 @@ const SqlObjectExplorer = ({
const handleContextMenu = (e: React.MouseEvent, node: TreeNode) => {
e.preventDefault()
+
+ // Don't show context menu for columns, templates, or tables
+ if (node.type === 'column' || node.id.startsWith('template-') || node.id.startsWith('table-')) {
+ return
+ }
+
setContextMenu({
show: true,
x: e.clientX,
@@ -286,16 +381,16 @@ const SqlObjectExplorer = ({
switch (type) {
case 1:
- await sqlQueryService.delete(object.id!)
+ await sqlObjectManagerService.deleteQuery(object.id!)
break
case 2:
- await sqlStoredProcedureService.delete(object.id!)
+ await sqlObjectManagerService.deleteStoredProcedure(object.id!)
break
case 3:
- await sqlViewService.delete(object.id!)
+ await sqlObjectManagerService.deleteView(object.id!)
break
case 4:
- await sqlFunctionService.delete(object.id!)
+ await sqlObjectManagerService.deleteFunction(object.id!)
break
}
@@ -337,6 +432,14 @@ const SqlObjectExplorer = ({
)
+ // Tables folder
+ if (node.id === 'tables')
+ return isExpanded ? (
+
+ ) : (
+
+ )
+
if (node.objectType === 1)
return isExpanded ? (
@@ -372,31 +475,41 @@ const SqlObjectExplorer = ({
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 (
handleNodeClick(node)}
- onContextMenu={(e) => handleContextMenu(e, node)}
+ onClick={() => !isColumn && handleNodeClick(node)}
+ onContextMenu={(e) => !isColumn && handleContextMenu(e, node)}
>
{getIcon(node)}
- {node.label}
+ {node.label}
{isExpanded && node.children && (
@@ -406,17 +519,48 @@ const SqlObjectExplorer = ({
)
}
+ const filteredTree = filterTree(treeData, filterText)
+
return (
-
- {loading &&
{translate('::App.Platform.Loading')}
}
- {!loading && treeData.length === 0 && (
-
- {translate('::App.Platform.NoDataSourceSelected')}
+
+ {/* 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"
+ />
+
- )}
- {!loading && treeData.length > 0 && (
-
{treeData.map((node) => renderNode(node))}
- )}
+
+
+ {/* 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 && (
<>