Sql Query Manager düzenlemesi

This commit is contained in:
Sedat Öztürk 2025-12-06 14:15:32 +03:00
parent 27c298c245
commit 4a7f2ee853
4 changed files with 356 additions and 138 deletions

View file

@ -10321,6 +10321,12 @@
"tr": "Ajanda",
"en": "Agenda"
},
{
"resourceName": "Platform",
"key": "App.Platform.UseTemplate",
"tr": "Şablon Kullan",
"en": "Use Template"
},
{
"resourceName": "Platform",
"key": "App.Platform.Error",

View file

@ -13,13 +13,14 @@ import type {
} from '@/proxy/sql-query-manager/models'
import { SqlObjectType } from '@/proxy/sql-query-manager/models'
import { sqlObjectManagerService } from '@/services/sql-query-manager.service'
import { FaDatabase, FaPlay, FaSave, FaSyncAlt, FaCloudUploadAlt } from 'react-icons/fa'
import { FaDatabase, FaPlay, FaSave, FaSyncAlt, FaCloudUploadAlt, FaCode } from 'react-icons/fa'
import { FaCheckCircle } from 'react-icons/fa'
import { useLocalization } from '@/utils/hooks/useLocalization'
import SqlObjectExplorer from './components/SqlObjectExplorer'
import SqlEditor, { SqlEditorRef } from './components/SqlEditor'
import SqlResultsGrid from './components/SqlResultsGrid'
import SqlObjectProperties from './components/SqlObjectProperties'
import TemplateDialog from './components/TemplateDialog'
import { Splitter } from '@/components/codeLayout/Splitter'
import { Helmet } from 'react-helmet'
import { useStoreState } from '@/store/store'
@ -69,6 +70,7 @@ const SqlQueryManager = () => {
detectedName: '',
isExistingObject: false,
})
const [showTemplateDialog, setShowTemplateDialog] = useState(false)
const [showTemplateConfirmDialog, setShowTemplateConfirmDialog] = useState(false)
const [pendingTemplate, setPendingTemplate] = useState<{ content: string; type: string } | null>(
null,
@ -337,7 +339,24 @@ GO`,
isDirty: false,
}))
},
[translate],
[],
)
const handleUseTemplateFromDialog = useCallback(
(templateContent: string, templateType: string) => {
// Check if editor has content
const hasUserContent = state.editorContent.trim() && state.isDirty
if (hasUserContent) {
// Ask for confirmation
setPendingTemplate({ content: templateContent, type: templateType })
setShowTemplateConfirmDialog(true)
} else {
// Apply template directly
applyTemplate(templateContent)
}
},
[state.editorContent, state.isDirty, applyTemplate],
)
const handleTemplateSelect = useCallback(
@ -722,6 +741,16 @@ GO`,
</div>
<div className="flex items-center gap-3">
<Button
size="sm"
variant="solid"
color="orange-500"
icon={<FaCode />}
onClick={() => setShowTemplateDialog(true)}
className="shadow-sm"
>
{translate('::App.Platform.Templates')}
</Button>
<Button
size="sm"
variant="solid"
@ -779,7 +808,7 @@ GO`,
<div className="w-1/3 flex-shrink-0 flex flex-col min-h-0 mr-4">
<AdaptableCard className="h-full" bodyClass="p-0">
<div className="h-full flex flex-col">
<div className="border-b px-4 py-3 bg-gray-50 dark:bg-gray-800 flex-shrink-0">
<div className="border-b px-4 py-2 bg-gray-50 dark:bg-gray-800 flex-shrink-0">
<h6 className="font-semibold text-sm">
{translate('::App.Platform.ObjectExplorer')}
</h6>
@ -882,6 +911,13 @@ GO`,
</div>
</div>
{/* Template Dialog */}
<TemplateDialog
isOpen={showTemplateDialog}
onClose={() => setShowTemplateDialog(false)}
onUseTemplate={handleUseTemplateFromDialog}
/>
{/* Template Confirmation Dialog */}
<Dialog
isOpen={showTemplateConfirmDialog}

View file

@ -96,62 +96,6 @@ const SqlObjectExplorer = ({
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})`,
@ -165,21 +109,6 @@ const SqlObjectExplorer = ({
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})`,
@ -240,6 +169,21 @@ const SqlObjectExplorer = ({
};
}) || [],
},
{
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,
})) || [],
},
],
},
]
@ -286,20 +230,6 @@ const SqlObjectExplorer = ({
} 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)
})
}
}
}
}
@ -359,17 +289,7 @@ const SqlObjectExplorer = ({
// 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 TOP 10 * \nFROM ${table.fullName || `[${table.schemaName}].[${table.tableName}]`};`
onTemplateSelect(selectQuery, 'table-select')
} else if (node.objectType) {
if (node.objectType) {
onObjectSelect(node.data, node.objectType)
}
}
@ -378,8 +298,8 @@ const SqlObjectExplorer = ({
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-')) {
// Don't show context menu for columns
if (node.type === 'column') {
return
}
@ -442,14 +362,6 @@ const SqlObjectExplorer = ({
if (node.type === 'folder') {
const isExpanded = expandedNodes.has(node.id)
// Templates folder
if (node.id === 'templates')
return isExpanded ? (
<FaRegFolderOpen className="text-orange-500" />
) : (
<FaRegFolder className="text-orange-500" />
)
// Tables folder
if (node.id === 'tables')
return isExpanded ? (
@ -488,16 +400,6 @@ const SqlObjectExplorer = ({
}
if (node.type === 'object') {
// Check if it's a template
if ((node.data as any)?.templateType) {
return <FaCode className="text-orange-500" />
}
// Check if it's a table
if (node.id.startsWith('table-')) {
return <FaTable className="text-blue-500" />
}
if (node.objectType === 1) return <FaRegFileAlt className="text-gray-500" />
if (node.objectType === 2) return <FaCog className="text-gray-500" />
if (node.objectType === 3) return <FaColumns className="text-gray-500" />
@ -596,7 +498,7 @@ const SqlObjectExplorer = ({
className="fixed z-50 bg-white dark:bg-gray-800 shadow-lg rounded border border-gray-200 dark:border-gray-700 py-1"
style={{ top: contextMenu.y, left: contextMenu.x }}
>
{contextMenu.node?.type === 'object' && !contextMenu.node?.id?.startsWith('table-') && (
{contextMenu.node?.type === 'object' && contextMenu.node?.objectType && (
<>
<button
className="w-full px-4 py-2 text-left hover:bg-gray-100 dark:hover:bg-gray-700 text-sm"
@ -642,24 +544,6 @@ const SqlObjectExplorer = ({
{translate('::App.Platform.Refresh')}
</button>
)}
{contextMenu.node?.id?.startsWith('table-') && contextMenu.node?.data && (
<button
className="w-full px-4 py-2 text-left hover:bg-gray-100 dark:hover:bg-gray-700 text-sm"
onClick={() => {
if (onShowTableColumns && contextMenu.node?.data) {
onShowTableColumns(
contextMenu.node.data.schemaName,
contextMenu.node.data.tableName
)
}
setContextMenu({ show: false, x: 0, y: 0, node: null })
}}
>
<FaColumns className="inline mr-2" />
{translate('::App.Platform.ShowColumns')}
</button>
)}
</div>
</>
)}

View file

@ -0,0 +1,292 @@
import { useState } from 'react'
import { Dialog, Button } from '@/components/ui'
import { FaCode } from 'react-icons/fa'
import { useLocalization } from '@/utils/hooks/useLocalization'
interface TemplateDialogProps {
isOpen: boolean
onClose: () => void
onUseTemplate: (templateContent: string, templateType: string) => void
}
interface Template {
id: string
label: string
description: string
type: string
}
const templates: Template[] = [
{
id: 'select',
label: 'SELECT Query',
description: 'Basic SELECT query template',
type: 'select',
},
{
id: 'insert',
label: 'INSERT Query',
description: 'Basic INSERT query template',
type: 'insert',
},
{
id: 'update',
label: 'UPDATE Query',
description: 'Basic UPDATE query template',
type: 'update',
},
{
id: 'delete',
label: 'DELETE Query',
description: 'Basic DELETE query template',
type: 'delete',
},
{
id: 'create-procedure',
label: 'Stored Procedure',
description: 'Create Stored Procedure template',
type: 'create-procedure',
},
{
id: 'create-view',
label: 'View',
description: 'Create View template',
type: 'create-view',
},
{
id: 'create-scalar-function',
label: 'Scalar Function',
description: 'Create Scalar Function template',
type: 'create-scalar-function',
},
{
id: 'create-table-function',
label: 'Table-Valued Function',
description: 'Create Table-Valued Function template',
type: 'create-table-function',
},
]
const getTemplateContent = (templateType: string): string => {
const templateContents: Record<string, string> = {
select: `-- Basic SELECT query
SELECT
Column1,
Column2,
Column3
FROM
TableName
WHERE
-- Add your conditions
Column1 = 'value'
AND IsActive = 1
ORDER BY
Column1 ASC;`,
insert: `-- Basic INSERT query
INSERT INTO TableName (Column1, Column2, Column3)
VALUES
('Value1', 'Value2', 'Value3');`,
update: `-- Basic UPDATE query
UPDATE TableName
SET
Column1 = 'NewValue1',
Column2 = 'NewValue2'
WHERE
-- Add your conditions
Id = 1;`,
delete: `-- Basic DELETE query
DELETE FROM TableName
WHERE
-- Add your conditions
Id = 1;`,
'create-procedure': `-- Create Stored Procedure
CREATE PROCEDURE [dbo].[ProcedureName]
@Parameter1 INT,
@Parameter2 NVARCHAR(100)
AS
BEGIN
SET NOCOUNT ON;
-- Add your logic here
SELECT
Column1,
Column2
FROM
TableName
WHERE
Column1 = @Parameter1
AND Column2 = @Parameter2;
END
GO`,
'create-view': `-- Create View
CREATE VIEW [dbo].[ViewName]
AS
SELECT
t1.Column1,
t1.Column2,
t2.Column3
FROM
TableName1 t1
INNER JOIN TableName2 t2 ON t1.Id = t2.TableName1Id
WHERE
t1.IsActive = 1;
GO`,
'create-scalar-function': `-- Create Scalar Function
CREATE FUNCTION [dbo].[ScalarFunctionName]
(
@Parameter1 INT,
@Parameter2 NVARCHAR(100)
)
RETURNS NVARCHAR(200)
AS
BEGIN
DECLARE @Result NVARCHAR(200);
-- Add your logic here
SELECT @Result = Column1 + ' ' + @Parameter2
FROM TableName
WHERE Id = @Parameter1;
RETURN @Result;
END
GO`,
'create-table-function': `-- Create Table-Valued Function
CREATE FUNCTION [dbo].[TableFunctionName]
(
@Parameter1 INT,
@Parameter2 NVARCHAR(100)
)
RETURNS TABLE
AS
RETURN
(
SELECT
t.Column1,
t.Column2,
t.Column3,
t.Column4
FROM
TableName t
WHERE
t.Id = @Parameter1
AND t.Column2 LIKE '%' + @Parameter2 + '%'
AND t.IsActive = 1
)
GO`,
}
return templateContents[templateType] || templateContents['select']
}
const TemplateDialog = ({ isOpen, onClose, onUseTemplate }: TemplateDialogProps) => {
const { translate } = useLocalization()
const [selectedTemplate, setSelectedTemplate] = useState<Template | null>(null)
const handleTemplateSelect = (template: Template) => {
setSelectedTemplate(template)
}
const handleUseTemplate = () => {
if (selectedTemplate) {
const content = getTemplateContent(selectedTemplate.type)
onUseTemplate(content, selectedTemplate.type)
onClose()
setSelectedTemplate(null)
}
}
const handleClose = () => {
onClose()
setSelectedTemplate(null)
}
return (
<Dialog isOpen={isOpen} onClose={handleClose} onRequestClose={handleClose} width={900}>
<div className="flex flex-col h-[600px]">
<h5 className="mb-4 text-lg font-semibold">
{translate('::App.Platform.SelectTemplate')}
</h5>
<div className="flex flex-1 gap-4 min-h-0">
{/* Template List */}
<div className="w-1/3 border rounded-lg p-2 overflow-y-auto">
<div className="space-y-1">
{templates.map((template) => (
<div
key={template.id}
className={`flex items-start gap-3 p-3 rounded-lg cursor-pointer transition-colors ${
selectedTemplate?.id === template.id
? 'bg-blue-100 dark:bg-blue-900 border-2 border-blue-500'
: 'bg-gray-50 dark:bg-gray-800 hover:bg-gray-100 dark:hover:bg-gray-700 border-2 border-transparent'
}`}
onClick={() => handleTemplateSelect(template)}
>
<FaCode
className={`text-lg mt-1 ${
selectedTemplate?.id === template.id
? 'text-blue-600 dark:text-blue-400'
: 'text-gray-500'
}`}
/>
<div className="flex-1">
<div className="font-medium text-sm">{template.label}</div>
<div className="text-xs text-gray-600 dark:text-gray-400 mt-1">
{template.description}
</div>
</div>
</div>
))}
</div>
</div>
{/* Template Preview */}
<div className="flex-1 border rounded-lg p-4 overflow-hidden flex flex-col">
{selectedTemplate ? (
<>
<div className="mb-3">
<h6 className="font-semibold text-sm mb-1">{selectedTemplate.label}</h6>
<p className="text-xs text-gray-600 dark:text-gray-400">
{selectedTemplate.description}
</p>
</div>
<div className="flex-1 overflow-auto bg-gray-900 rounded p-3">
<pre className="text-sm text-gray-100 font-mono">
{getTemplateContent(selectedTemplate.type)}
</pre>
</div>
</>
) : (
<div className="flex items-center justify-center h-full text-gray-500 dark:text-gray-400">
{translate('::App.Platform.SelectATemplateToPreview')}
</div>
)}
</div>
</div>
{/* Action Buttons */}
<div className="flex justify-end gap-2 mt-4 pt-4 border-t">
<Button variant="plain" onClick={handleClose}>
{translate('::App.Platform.Cancel')}
</Button>
<Button
variant="solid"
onClick={handleUseTemplate}
disabled={!selectedTemplate}
color="blue-600"
>
{translate('::App.Platform.UseTemplate')}
</Button>
</div>
</div>
</Dialog>
)
}
export default TemplateDialog