erp-platform/ui/src/views/sqlQueryManager/SqlQueryManager.tsx
2025-12-06 01:38:21 +03:00

984 lines
33 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { useState, useCallback, useEffect, useRef } from 'react'
import { Button, Dialog, Input, Notification, toast } from '@/components/ui'
import Container from '@/components/shared/Container'
import AdaptableCard from '@/components/shared/AdaptableCard'
import { getDataSources } from '@/services/data-source.service'
import type { DataSourceDto } from '@/proxy/data-source'
import type {
SqlFunctionDto,
SqlQueryDto,
SqlStoredProcedureDto,
SqlViewDto,
SqlQueryExecutionResultDto,
} 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 { HiOutlineCheckCircle } from 'react-icons/hi'
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 { Splitter } from '@/components/codeLayout/Splitter'
import { Helmet } from 'react-helmet'
export type SqlObject = SqlFunctionDto | SqlQueryDto | SqlStoredProcedureDto | SqlViewDto
interface SqlManagerState {
dataSources: DataSourceDto[]
selectedDataSource: DataSourceDto | null
selectedObject: SqlObject | null
selectedObjectType: SqlObjectType | null
editorContent: string
isExecuting: boolean
executionResult: SqlQueryExecutionResultDto | null
showProperties: boolean
isDirty: boolean
tableColumns: any | null
isSaved: boolean
refreshTrigger: number
}
const SqlQueryManager = () => {
const { translate } = useLocalization()
const editorRef = useRef<SqlEditorRef>(null)
const [state, setState] = useState<SqlManagerState>({
dataSources: [],
selectedDataSource: null,
selectedObject: null,
selectedObjectType: null,
editorContent: '',
isExecuting: false,
refreshTrigger: 0,
executionResult: null,
showProperties: false,
isDirty: false,
tableColumns: null,
isSaved: false,
})
const [showSaveDialog, setShowSaveDialog] = useState(false)
const [saveDialogData, setSaveDialogData] = useState({
name: '',
description: '',
detectedType: '',
detectedName: '',
isExistingObject: false,
})
const [showTemplateConfirmDialog, setShowTemplateConfirmDialog] = useState(false)
const [pendingTemplate, setPendingTemplate] = useState<{ content: string; type: string } | null>(
null,
)
useEffect(() => {
loadDataSources()
}, [])
const loadDataSources = async () => {
try {
const response = await getDataSources()
const items = response.data.items || []
if (items.length > 0) {
setState((prev) => ({
...prev,
dataSources: items,
selectedDataSource: items[0],
}))
}
} catch (error) {
toast.push(
<Notification type="danger" title={translate('::App.Platform.Error')}>
{translate('::App.Platform.FailedtoloadDatasources')}
</Notification>,
{ placement: 'top-center' },
)
}
}
const handleDataSourceChange = useCallback((dataSource: DataSourceDto) => {
setState((prev) => ({
...prev,
selectedDataSource: dataSource,
selectedObject: null,
editorContent: '',
executionResult: null,
isDirty: false,
}))
}, [])
const handleObjectSelect = useCallback(
(object: SqlObject | null, objectType: SqlObjectType | null) => {
if (state.isDirty) {
if (!confirm(translate('::App.Platform.UnsavedChangesConfirmation'))) {
return
}
}
let content = ''
if (object) {
if (objectType === 1) {
// Query
content = (object as SqlQueryDto).queryText
} else if (objectType === 2) {
// Stored Procedure
content = (object as SqlStoredProcedureDto).procedureBody
} else if (objectType === 3) {
// View
content = (object as SqlViewDto).viewDefinition
} else if (objectType === 4) {
// Function
content = (object as SqlFunctionDto).functionBody
}
}
setState((prev) => ({
...prev,
selectedObject: object,
selectedObjectType: objectType,
editorContent: content,
executionResult: null,
tableColumns: null,
isDirty: false,
isSaved: false,
}))
},
[state.isDirty, translate],
)
const handleEditorChange = useCallback((value: string | undefined) => {
setState((prev) => ({
...prev,
editorContent: value || '',
isDirty: true,
isSaved: false,
}))
}, [])
const getTemplateContent = (templateType: string): string => {
const templates: 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 templates[templateType] || templates['select']
}
// SQL analiz fonksiyonu - SQL metnini analiz edip nesne türünü ve adını tespit eder
const detectSqlObject = (sql: string): { type: string; name: string } => {
const upperSql = sql.trim().toUpperCase()
// VIEW tespiti
if (upperSql.includes('CREATE VIEW') || upperSql.includes('ALTER VIEW')) {
// Son kelimeyi al (schema varsa sonraki kelime, yoksa ilk kelime)
const viewMatch = sql.match(
/(?:CREATE|ALTER)\s+VIEW\s+(?:[\[\]]*\w+[\[\]]*\.)?\s*[\[]?(\w+)[\]]?/i,
)
return {
type: 'View',
name: viewMatch ? viewMatch[1] : '',
}
}
// STORED PROCEDURE tespiti
if (
upperSql.includes('CREATE PROCEDURE') ||
upperSql.includes('CREATE PROC') ||
upperSql.includes('ALTER PROCEDURE') ||
upperSql.includes('ALTER PROC')
) {
const procMatch = sql.match(
/(?:CREATE|ALTER)\s+(?:PROCEDURE|PROC)\s+(?:[\[\]]*\w+[\[\]]*\.)?\s*[\[]?(\w+)[\]]?/i,
)
return {
type: 'StoredProcedure',
name: procMatch ? procMatch[1] : '',
}
}
// FUNCTION tespiti
if (upperSql.includes('CREATE FUNCTION') || upperSql.includes('ALTER FUNCTION')) {
const funcMatch = sql.match(
/(?:CREATE|ALTER)\s+FUNCTION\s+(?:[\[\]]*\w+[\[\]]*\.)?\s*[\[]?(\w+)[\]]?/i,
)
return {
type: 'Function',
name: funcMatch ? funcMatch[1] : '',
}
}
// Default: Query
return {
type: 'Query',
name: '',
}
}
const applyTemplate = useCallback(
(templateContent: string) => {
setState((prev) => ({
...prev,
editorContent: templateContent,
selectedObject: null,
selectedObjectType: null,
executionResult: null,
isDirty: false,
}))
},
[translate],
)
const handleTemplateSelect = useCallback(
(template: string, templateType: string) => {
// If template is already provided (e.g., from table click), use it
const templateContent = template || getTemplateContent(templateType)
// Check if editor has content and it's not from a previous template
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)
}
},
[translate, state.editorContent, state.isDirty, applyTemplate],
)
const handleConfirmTemplateReplace = useCallback(() => {
if (pendingTemplate) {
applyTemplate(pendingTemplate.content)
}
setShowTemplateConfirmDialog(false)
setPendingTemplate(null)
}, [pendingTemplate, applyTemplate])
const handleCancelTemplateReplace = useCallback(() => {
setShowTemplateConfirmDialog(false)
setPendingTemplate(null)
}, [])
const handleExecute = async () => {
if (!state.selectedDataSource) {
toast.push(
<Notification type="warning" title={translate('::App.Platform.Warning')}>
{translate('::App.Platform.PleaseSelectDataSource')}
</Notification>,
{ placement: 'top-center' },
)
return
}
// Seçili text varsa onu, yoksa tüm editor içeriğini kullan
const selectedText = editorRef.current?.getSelectedText() || ''
const queryToExecute = selectedText.trim() || state.editorContent.trim()
if (!queryToExecute) {
toast.push(
<Notification type="warning" title={translate('::App.Platform.Warning')}>
{translate('::App.Platform.PleaseEnterQuery')}
</Notification>,
{ placement: 'top-center' },
)
return
}
// Seçili metni koru
const savedSelection = editorRef.current?.preserveSelection()
setState((prev) => ({ ...prev, isExecuting: true, executionResult: null, tableColumns: null }))
try {
const result = await sqlObjectManagerService.executeQuery({
queryText: queryToExecute,
dataSourceCode: state.selectedDataSource.code || '',
})
setState((prev) => ({
...prev,
executionResult: result.data,
isExecuting: false,
tableColumns: null,
}))
// Seçili metni geri yükle
setTimeout(() => {
if (savedSelection) {
editorRef.current?.restoreSelection(savedSelection)
}
}, 100)
} catch (error: any) {
setState((prev) => ({ ...prev, isExecuting: false }))
// Hata durumunda da seçili metni geri yükle
setTimeout(() => {
if (savedSelection) {
editorRef.current?.restoreSelection(savedSelection)
}
}, 100)
toast.push(
<Notification type="danger" title={translate('::App.Platform.Error')}>
{error.response?.data?.error?.message || translate('::App.Platform.FailedToExecuteQuery')}
</Notification>,
{ placement: 'top-center' },
)
}
}
const handleSave = async () => {
if (!state.selectedDataSource) {
toast.push(
<Notification type="warning" title={translate('::App.Platform.Warning')}>
{translate('::App.Platform.PleaseSelectDataSource')}
</Notification>,
{ placement: 'top-center' },
)
return
}
if (!state.editorContent.trim()) {
toast.push(
<Notification type="warning" title={translate('::App.Platform.Warning')}>
{translate('::App.Platform.PleaseEnterContentToSave')}
</Notification>,
{ placement: 'top-center' },
)
return
}
if (state.selectedObject && state.selectedObjectType) {
// Update existing object - open dialog with existing data
const typeMap: Record<SqlObjectType, string> = {
[SqlObjectType.Query]: 'Query',
[SqlObjectType.View]: 'View',
[SqlObjectType.StoredProcedure]: 'StoredProcedure',
[SqlObjectType.Function]: 'Function',
}
// Get name based on object type
let objectName = ''
if ('viewName' in state.selectedObject) {
objectName = state.selectedObject.viewName || state.selectedObject.displayName || ''
} else if ('procedureName' in state.selectedObject) {
objectName = state.selectedObject.procedureName || state.selectedObject.displayName || ''
} else if ('functionName' in state.selectedObject) {
objectName = state.selectedObject.functionName || state.selectedObject.displayName || ''
} else if ('name' in state.selectedObject) {
objectName = state.selectedObject.name || ''
}
setSaveDialogData({
name: objectName,
description: state.selectedObject.description || '',
detectedType: typeMap[state.selectedObjectType] || '',
detectedName: objectName,
isExistingObject: true,
})
setShowSaveDialog(true)
} else {
// New object - analyze SQL and show dialog with detection
const detection = detectSqlObject(state.editorContent)
setSaveDialogData({
name: detection.name,
description: '',
detectedType: detection.type,
detectedName: detection.name,
isExistingObject: false,
})
setShowSaveDialog(true)
}
}
const handleCreateNewQuery = async () => {
if (!state.selectedDataSource || !saveDialogData.name) return
try {
// Smart save ile kaydet
const result = await sqlObjectManagerService.smartSave({
sqlText: state.editorContent,
dataSourceCode: state.selectedDataSource.code || '',
name: saveDialogData.name,
description: saveDialogData.description,
})
// Kaydedilen objeyi state'e set et
const savedObject: any = {
id: result.data.objectId,
displayName: saveDialogData.name,
description: saveDialogData.description,
isDeployed: result.data.deployed,
}
// ObjectType'a göre ekstra alanlar ekle
let objectType: SqlObjectType | null = null
if (result.data.objectType === 'View') {
objectType = SqlObjectType.View
savedObject.viewName = saveDialogData.name
savedObject.viewDefinition = state.editorContent
} else if (result.data.objectType === 'StoredProcedure') {
objectType = SqlObjectType.StoredProcedure
savedObject.procedureName = saveDialogData.name
savedObject.procedureBody = state.editorContent
} else if (result.data.objectType === 'Function') {
objectType = SqlObjectType.Function
savedObject.functionName = saveDialogData.name
savedObject.functionBody = state.editorContent
} else if (result.data.objectType === 'Query') {
objectType = SqlObjectType.Query
savedObject.queryText = state.editorContent
}
setState((prev) => ({
...prev,
isDirty: false,
isSaved: true,
selectedObject: savedObject,
selectedObjectType: objectType,
refreshTrigger: prev.refreshTrigger + 1,
}))
setShowSaveDialog(false)
toast.push(
<Notification type="success" title={translate('::App.Platform.Success')}>
{result.data.message || translate('::App.Platform.SavedSuccessfully')}
</Notification>,
{ placement: 'top-center' },
)
} catch (error: any) {
toast.push(
<Notification type="danger" title={translate('::App.Platform.Error')}>
{error.response?.data?.error?.message || translate('::App.Platform.FailedToSaveQuery')}
</Notification>,
{ placement: 'top-center' },
)
}
}
const handleDeploy = async () => {
if (!state.selectedObject || !state.selectedObjectType) {
toast.push(
<Notification type="warning" title={translate('::App.Platform.Warning')}>
{translate('::App.Platform.PleaseSelectAnObjectToDeploy')}
</Notification>,
{ placement: 'top-center' },
)
return
}
if (!state.selectedObject.id) return
try {
const objectId = state.selectedObject.id
let result: any
switch (state.selectedObjectType) {
case SqlObjectType.StoredProcedure:
result = await sqlObjectManagerService.deployStoredProcedure({
id: objectId,
dropIfExists: true,
})
break
case SqlObjectType.View:
result = await sqlObjectManagerService.deployView({ id: objectId, dropIfExists: true })
break
case SqlObjectType.Function:
result = await sqlObjectManagerService.deployFunction({
id: objectId,
dropIfExists: true,
})
break
default:
toast.push(
<Notification type="warning" title={translate('::App.Platform.Warning')}>
{translate('::App.Platform.ThisObjectTypeCannotBeDeployed')}
</Notification>,
{ placement: 'top-center' },
)
return
}
// Update selectedObject's isDeployed status
setState((prev) => ({
...prev,
selectedObject: prev.selectedObject ? { ...prev.selectedObject, isDeployed: true } : null,
refreshTrigger: prev.refreshTrigger + 1,
}))
toast.push(
<Notification type="success" title={translate('::App.Platform.Success')}>
{translate('::App.Platform.ObjectDeployedSuccessfully')}
</Notification>,
{ placement: 'top-center' },
)
} catch (error: any) {
toast.push(
<Notification type="danger" title={translate('::App.Platform.Error')}>
{error.response?.data?.error?.message || translate('::App.Platform.FailedToDeployObject')}
</Notification>,
{ placement: 'top-center' },
)
}
}
const handleShowTableColumns = async (schemaName: string, tableName: string) => {
if (!state.selectedDataSource) return
try {
const response = await sqlObjectManagerService.getTableColumns(
state.selectedDataSource.code || '',
schemaName,
tableName,
)
// Transform API response to match display format
const transformedData = response.data.map((col: any) => ({
ColumnName: col.columnName,
DataType: col.dataType,
MaxLength: col.maxLength || '-',
IsNullable: col.isNullable,
IsPrimaryKey: col.isPrimaryKey || false,
}))
// Create a result object that looks like execution result for display
const columnsResult = {
success: true,
message: `Columns for ${schemaName}.${tableName}`,
data: transformedData,
rowsAffected: transformedData.length,
executionTimeMs: 0,
metadata: {
columns: [
{ name: 'ColumnName', dataType: 'string' },
{ name: 'DataType', dataType: 'string' },
{ name: 'MaxLength', dataType: 'string' },
{ name: 'IsNullable', dataType: 'boolean' },
{ name: 'IsPrimaryKey', dataType: 'boolean' },
],
},
}
setState((prev) => ({
...prev,
tableColumns: columnsResult,
executionResult: null, // Clear query results when showing columns
}))
} catch (error: any) {
toast.push(
<Notification type="danger" title={translate('::App.Platform.Error')}>
{error.response?.data?.error?.message || translate('::App.Platform.FailedToLoadColumns')}
</Notification>,
{ placement: 'top-center' },
)
}
}
return (
<Container className="h-full overflow-hidden">
<Helmet
titleTemplate="%s | Erp Platform"
title={translate('::' + 'App.SqlQueryManager')}
defaultTitle="Erp Platform"
></Helmet>
<div className="flex flex-col h-full p-1">
{/* Toolbar */}
<div className="flex-shrink-0 shadow-sm mb-4">
<div className="flex items-center justify-between px-1 py-1">
<div className="flex items-center gap-3">
<FaDatabase className="text-lg text-blue-500" />
<span className="text-sm font-medium text-gray-600 dark:text-gray-400">
{translate('::App.Platform.DataSource')}:
</span>
<select
className="border border-gray-300 rounded px-1 py-1"
value={state.selectedDataSource?.code || ''}
onChange={(e) => {
const ds = state.dataSources.find((d) => d.code === e.target.value)
if (ds) handleDataSourceChange(ds)
}}
>
{state.dataSources.map((ds) => (
<option key={ds.code} value={ds.code}>
{ds.code}
</option>
))}
</select>
</div>
<div className="flex items-center gap-3">
<Button
size="sm"
variant="solid"
color="blue-600"
icon={<FaPlay />}
onClick={handleExecute}
loading={state.isExecuting}
disabled={!state.selectedDataSource}
className="shadow-sm"
>
{translate('::App.Platform.Execute')}
<span className="ml-1 text-xs opacity-75">(F5)</span>
</Button>
<Button
size="sm"
variant="solid"
icon={<FaSave />}
onClick={handleSave}
disabled={
!state.selectedDataSource ||
!state.editorContent.trim() ||
(state.isSaved && !state.isDirty) ||
!state.executionResult?.success
}
className="shadow-sm"
>
{translate('::App.Platform.Save')}
<span className="ml-1 text-xs opacity-75">(Ctrl+S)</span>
</Button>
<Button
size="sm"
variant="solid"
color="green-600"
icon={<FaCloudUploadAlt />}
onClick={handleDeploy}
disabled={
!state.selectedObject ||
!state.selectedObjectType ||
state.selectedObjectType === SqlObjectType.Query ||
(state.selectedObject &&
'isDeployed' in state.selectedObject &&
state.selectedObject.isDeployed)
}
className="shadow-sm"
>
{translate('::App.Platform.Deploy')}
</Button>
</div>
</div>
</div>
{/* Main Content Area */}
<div className="flex-1 flex min-h-0">
{/* Left Panel - Object Explorer */}
<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">
<h6 className="font-semibold text-sm">
{translate('::App.Platform.ObjectExplorer')}
</h6>
</div>
<SqlObjectExplorer
dataSource={state.selectedDataSource}
onObjectSelect={handleObjectSelect}
selectedObject={state.selectedObject}
onTemplateSelect={handleTemplateSelect}
onShowTableColumns={handleShowTableColumns}
refreshTrigger={state.refreshTrigger}
/>
</div>
</AdaptableCard>
</div>
{/* Center Panel - Editor and Results */}
<div className="flex-1 flex flex-col min-h-0 mr-4">
{state.executionResult || state.tableColumns ? (
<Splitter direction="vertical" initialSize={250} minSize={150} maxSize={1200}>
<div className="border rounded-lg shadow-sm bg-white dark:bg-gray-800 flex flex-col h-full">
<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.QueryEditor')}
</h6>
</div>
<div className="flex-1 min-h-0 overflow-hidden">
<SqlEditor
ref={editorRef}
value={state.editorContent}
onChange={handleEditorChange}
onExecute={handleExecute}
onSave={handleSave}
readOnly={state.isExecuting}
/>
</div>
</div>
<div className="border rounded-lg shadow-sm bg-white dark:bg-gray-800 flex flex-col h-full">
<div className="border-b px-4 py-2 bg-gray-50 dark:bg-gray-800 flex-shrink-0 flex items-center justify-between">
<div className="flex items-center gap-2">
<HiOutlineCheckCircle className="text-green-500" />
<span className="text-sm text-green-700 dark:text-green-400">
{state.executionResult?.message ||
state.tableColumns?.message ||
translate('::App.Platform.QueryExecutedSuccessfully')}
</span>
</div>
<div className="flex items-center gap-4">
<div className="flex items-center gap-4 text-sm text-gray-600 dark:text-gray-400">
<span>
{translate('::App.Platform.Rows')}:{' '}
<strong>
{state.executionResult?.rowsAffected ||
state.executionResult?.data?.length ||
state.tableColumns?.rowsAffected ||
0}
</strong>
</span>
{state.executionResult && (
<span>
{translate('::App.Platform.Time')}:{' '}
<strong>{state.executionResult.executionTimeMs}ms</strong>
</span>
)}
</div>
</div>
</div>
<div className="flex-1 overflow-hidden p-2">
<SqlResultsGrid result={(state.executionResult || state.tableColumns)!} />
</div>
</div>
</Splitter>
) : (
<div className="flex-1 border rounded-lg shadow-sm bg-white dark:bg-gray-800 flex flex-col overflow-hidden">
<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.QueryEditor')}
</h6>
</div>
<div className="flex-1 min-h-0">
<SqlEditor
ref={editorRef}
value={state.editorContent}
onChange={handleEditorChange}
onExecute={handleExecute}
onSave={handleSave}
readOnly={state.isExecuting}
/>
</div>
</div>
)}
</div>
{/* Right Panel - Properties (Optional) */}
{state.showProperties && state.selectedObject && (
<div className="w-80 flex-shrink-0 flex flex-col min-h-0">
<SqlObjectProperties object={state.selectedObject} type={state.selectedObjectType} />
</div>
)}
</div>
</div>
{/* Template Confirmation Dialog */}
<Dialog
isOpen={showTemplateConfirmDialog}
onClose={handleCancelTemplateReplace}
onRequestClose={handleCancelTemplateReplace}
>
<h5 className="mb-4">{translate('::App.Platform.ConfirmTemplateReplace')}</h5>
<p className="mb-6 text-gray-600 dark:text-gray-400">
{translate('::App.Platform.TemplateReplaceWarning')}
</p>
<div className="flex justify-end gap-2">
<Button variant="plain" onClick={handleCancelTemplateReplace}>
{translate('::App.Platform.Cancel')}
</Button>
<Button variant="solid" onClick={handleConfirmTemplateReplace}>
{translate('::App.Platform.Replace')}
</Button>
</div>
</Dialog>
{/* Save Dialog */}
<Dialog
isOpen={showSaveDialog}
onClose={() => setShowSaveDialog(false)}
onRequestClose={() => setShowSaveDialog(false)}
>
<h5 className="mb-4">{translate('::App.Platform.SaveQuery')}</h5>
<div className="space-y-4">
{/* Detected Object Type */}
{saveDialogData.detectedType && (
<div className="p-3 bg-blue-50 dark:bg-blue-900/20 rounded-md border border-blue-200 dark:border-blue-800">
<div className="flex items-center gap-2">
<HiOutlineCheckCircle className="text-blue-600 dark:text-blue-400 text-xl" />
<div>
<div className="text-sm font-semibold text-blue-900 dark:text-blue-100">
{translate('::App.Platform.DetectedObjectType')}
</div>
<div className="text-sm text-blue-700 dark:text-blue-300">
{saveDialogData.detectedType === 'View' && translate('::App.Platform.View')}
{saveDialogData.detectedType === 'StoredProcedure' &&
translate('::App.Platform.StoredProcedure')}
{saveDialogData.detectedType === 'Function' &&
translate('::App.Platform.Function')}
{saveDialogData.detectedType === 'Query' && translate('::App.Platform.Query')}
</div>
{saveDialogData.detectedName && (
<div className="text-xs text-blue-600 dark:text-blue-400 mt-1">
{translate('::App.Platform.DetectedName')}:{' '}
<span className="font-mono">{saveDialogData.detectedName}</span>
</div>
)}
</div>
</div>
</div>
)}
<div>
<label className="block mb-2">
{translate('::App.Platform.Name')} <span className="text-red-500">*</span>
{saveDialogData.isExistingObject && (
<span className="text-xs text-gray-500 ml-2">
({translate('::App.Platform.CannotBeChanged')})
</span>
)}
</label>
<Input
autoFocus={!saveDialogData.isExistingObject}
value={saveDialogData.name}
onChange={(e) => setSaveDialogData((prev) => ({ ...prev, name: e.target.value }))}
placeholder={saveDialogData.detectedName || translate('::App.Platform.Name')}
invalid={!saveDialogData.name.trim()}
disabled={saveDialogData.isExistingObject}
/>
</div>
<div>
<label className="block mb-2">{translate('::App.Platform.Description')}</label>
<Input
value={saveDialogData.description}
onChange={(e) =>
setSaveDialogData((prev) => ({ ...prev, description: e.target.value }))
}
placeholder={translate('::App.Platform.Description')}
/>
</div>
<div className="flex justify-end gap-2">
<Button variant="plain" onClick={() => setShowSaveDialog(false)}>
{translate('::App.Platform.Cancel')}
</Button>
<Button
variant="solid"
onClick={handleCreateNewQuery}
disabled={!saveDialogData.name.trim()}
>
{translate('::App.Platform.Save')}
</Button>
</div>
</div>
</Dialog>
</Container>
)
}
export default SqlQueryManager