erp-platform/ui/src/views/sqlQueryManager/SqlQueryManager.tsx

985 lines
33 KiB
TypeScript
Raw Normal View History

2025-12-05 22:38:21 +00:00
import { useState, useCallback, useEffect, useRef } from 'react'
2025-12-05 13:45:45 +00:00
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'
2025-12-05 22:38:21 +00:00
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'
2025-12-05 13:45:45 +00:00
import { useLocalization } from '@/utils/hooks/useLocalization'
import SqlObjectExplorer from './components/SqlObjectExplorer'
2025-12-05 22:38:21 +00:00
import SqlEditor, { SqlEditorRef } from './components/SqlEditor'
2025-12-05 13:45:45 +00:00
import SqlResultsGrid from './components/SqlResultsGrid'
import SqlObjectProperties from './components/SqlObjectProperties'
2025-12-05 22:38:21 +00:00
import { Splitter } from '@/components/codeLayout/Splitter'
import { Helmet } from 'react-helmet'
2025-12-05 13:45:45 +00:00
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
2025-12-05 22:38:21 +00:00
tableColumns: any | null
isSaved: boolean
refreshTrigger: number
2025-12-05 13:45:45 +00:00
}
const SqlQueryManager = () => {
const { translate } = useLocalization()
2025-12-05 22:38:21 +00:00
const editorRef = useRef<SqlEditorRef>(null)
2025-12-05 13:45:45 +00:00
const [state, setState] = useState<SqlManagerState>({
dataSources: [],
selectedDataSource: null,
selectedObject: null,
selectedObjectType: null,
editorContent: '',
2025-12-05 13:45:45 +00:00
isExecuting: false,
2025-12-05 22:38:21 +00:00
refreshTrigger: 0,
2025-12-05 13:45:45 +00:00
executionResult: null,
showProperties: false,
isDirty: false,
2025-12-05 22:38:21 +00:00
tableColumns: null,
isSaved: false,
2025-12-05 13:45:45 +00:00
})
const [showSaveDialog, setShowSaveDialog] = useState(false)
2025-12-05 22:38:21 +00:00
const [saveDialogData, setSaveDialogData] = useState({
name: '',
description: '',
detectedType: '',
detectedName: '',
isExistingObject: false,
})
2025-12-05 13:45:45 +00:00
const [showTemplateConfirmDialog, setShowTemplateConfirmDialog] = useState(false)
2025-12-05 22:38:21 +00:00
const [pendingTemplate, setPendingTemplate] = useState<{ content: string; type: string } | null>(
null,
)
2025-12-05 13:45:45 +00:00
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,
2025-12-05 22:38:21 +00:00
tableColumns: null,
2025-12-05 13:45:45 +00:00
isDirty: false,
2025-12-05 22:38:21 +00:00
isSaved: false,
2025-12-05 13:45:45 +00:00
}))
},
[state.isDirty, translate],
)
const handleEditorChange = useCallback((value: string | undefined) => {
setState((prev) => ({
...prev,
editorContent: value || '',
isDirty: true,
2025-12-05 22:38:21 +00:00
isSaved: false,
2025-12-05 13:45:45 +00:00
}))
}, [])
const getTemplateContent = (templateType: string): string => {
const templates: Record<string, string> = {
2025-12-05 22:38:21 +00:00
select: `-- Basic SELECT query
2025-12-05 13:45:45 +00:00
SELECT
Column1,
Column2,
Column3
FROM
TableName
WHERE
-- Add your conditions
Column1 = 'value'
AND IsActive = 1
ORDER BY
Column1 ASC;`,
2025-12-05 22:38:21 +00:00
insert: `-- Basic INSERT query
2025-12-05 13:45:45 +00:00
INSERT INTO TableName (Column1, Column2, Column3)
VALUES
('Value1', 'Value2', 'Value3');`,
2025-12-05 22:38:21 +00:00
update: `-- Basic UPDATE query
2025-12-05 13:45:45 +00:00
UPDATE TableName
SET
Column1 = 'NewValue1',
Column2 = 'NewValue2'
WHERE
-- Add your conditions
Id = 1;`,
2025-12-05 22:38:21 +00:00
delete: `-- Basic DELETE query
2025-12-05 13:45:45 +00:00
DELETE FROM TableName
WHERE
-- Add your conditions
Id = 1;`,
2025-12-05 22:38:21 +00:00
2025-12-05 13:45:45 +00:00
'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`,
2025-12-05 22:38:21 +00:00
2025-12-05 13:45:45 +00:00
'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`,
2025-12-05 22:38:21 +00:00
2025-12-05 13:48:32 +00:00
'create-scalar-function': `-- Create Scalar Function
CREATE FUNCTION [dbo].[ScalarFunctionName]
2025-12-05 13:45:45 +00:00
(
@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
2025-12-05 13:48:32 +00:00
GO`,
2025-12-05 22:38:21 +00:00
2025-12-05 13:48:32 +00:00
'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
)
2025-12-05 22:38:21 +00:00
GO`,
2025-12-05 13:45:45 +00:00
}
2025-12-05 22:38:21 +00:00
2025-12-05 13:45:45 +00:00
return templates[templateType] || templates['select']
}
2025-12-05 22:38:21 +00:00
// 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()
2025-12-05 13:45:45 +00:00
2025-12-05 22:38:21 +00:00
// 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] : '',
}
2025-12-05 13:45:45 +00:00
}
2025-12-05 22:38:21 +00:00
// 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],
)
2025-12-05 13:45:45 +00:00
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
}
2025-12-05 22:38:21 +00:00
// 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) {
2025-12-05 13:45:45 +00:00
toast.push(
<Notification type="warning" title={translate('::App.Platform.Warning')}>
{translate('::App.Platform.PleaseEnterQuery')}
</Notification>,
{ placement: 'top-center' },
)
return
}
2025-12-05 22:38:21 +00:00
// Seçili metni koru
const savedSelection = editorRef.current?.preserveSelection()
setState((prev) => ({ ...prev, isExecuting: true, executionResult: null, tableColumns: null }))
2025-12-05 13:45:45 +00:00
try {
const result = await sqlObjectManagerService.executeQuery({
2025-12-05 22:38:21 +00:00
queryText: queryToExecute,
2025-12-05 13:45:45 +00:00
dataSourceCode: state.selectedDataSource.code || '',
})
2025-12-05 22:38:21 +00:00
setState((prev) => ({
...prev,
executionResult: result.data,
isExecuting: false,
tableColumns: null,
}))
2025-12-05 13:45:45 +00:00
2025-12-05 22:38:21 +00:00
// Seçili metni geri yükle
setTimeout(() => {
if (savedSelection) {
editorRef.current?.restoreSelection(savedSelection)
}
}, 100)
2025-12-05 13:45:45 +00:00
} catch (error: any) {
setState((prev) => ({ ...prev, isExecuting: false }))
2025-12-05 22:38:21 +00:00
// Hata durumunda da seçili metni geri yükle
setTimeout(() => {
if (savedSelection) {
editorRef.current?.restoreSelection(savedSelection)
}
}, 100)
2025-12-05 13:45:45 +00:00
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) {
2025-12-05 22:38:21 +00:00
// 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',
2025-12-05 13:45:45 +00:00
}
2025-12-05 22:38:21 +00:00
// 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 || ''
}
2025-12-05 13:45:45 +00:00
2025-12-05 22:38:21 +00:00
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)
2025-12-05 13:45:45 +00:00
}
}
const handleCreateNewQuery = async () => {
if (!state.selectedDataSource || !saveDialogData.name) return
try {
2025-12-05 22:38:21 +00:00
// Smart save ile kaydet
const result = await sqlObjectManagerService.smartSave({
sqlText: state.editorContent,
dataSourceCode: state.selectedDataSource.code || '',
2025-12-05 13:45:45 +00:00
name: saveDialogData.name,
description: saveDialogData.description,
})
2025-12-05 22:38:21 +00:00
// 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,
}))
2025-12-05 13:45:45 +00:00
setShowSaveDialog(false)
toast.push(
<Notification type="success" title={translate('::App.Platform.Success')}>
2025-12-05 22:38:21 +00:00
{result.data.message || translate('::App.Platform.SavedSuccessfully')}
2025-12-05 13:45:45 +00:00
</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) {
2025-12-05 22:38:21 +00:00
case SqlObjectType.StoredProcedure:
result = await sqlObjectManagerService.deployStoredProcedure({
id: objectId,
dropIfExists: true,
})
2025-12-05 13:45:45 +00:00
break
2025-12-05 22:38:21 +00:00
case SqlObjectType.View:
result = await sqlObjectManagerService.deployView({ id: objectId, dropIfExists: true })
2025-12-05 13:45:45 +00:00
break
2025-12-05 22:38:21 +00:00
case SqlObjectType.Function:
result = await sqlObjectManagerService.deployFunction({
id: objectId,
dropIfExists: true,
})
2025-12-05 13:45:45 +00:00
break
default:
toast.push(
<Notification type="warning" title={translate('::App.Platform.Warning')}>
{translate('::App.Platform.ThisObjectTypeCannotBeDeployed')}
</Notification>,
{ placement: 'top-center' },
)
return
}
2025-12-05 22:38:21 +00:00
// Update selectedObject's isDeployed status
setState((prev) => ({
...prev,
selectedObject: prev.selectedObject ? { ...prev.selectedObject, isDeployed: true } : null,
refreshTrigger: prev.refreshTrigger + 1,
}))
2025-12-05 13:45:45 +00:00
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' },
)
}
}
2025-12-05 22:38:21 +00:00
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' },
)
}
}
2025-12-05 13:45:45 +00:00
return (
<Container className="h-full overflow-hidden">
2025-12-05 22:38:21 +00:00
<Helmet
titleTemplate="%s | Erp Platform"
title={translate('::' + 'App.SqlQueryManager')}
defaultTitle="Erp Platform"
></Helmet>
<div className="flex flex-col h-full p-1">
2025-12-05 13:45:45 +00:00
{/* Toolbar */}
2025-12-05 22:38:21 +00:00
<div className="flex-shrink-0 shadow-sm mb-4">
2025-12-05 13:45:45 +00:00
<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}
2025-12-05 22:38:21 +00:00
disabled={
!state.selectedDataSource ||
!state.editorContent.trim() ||
(state.isSaved && !state.isDirty) ||
!state.executionResult?.success
}
2025-12-05 13:45:45 +00:00
className="shadow-sm"
>
{translate('::App.Platform.Save')}
<span className="ml-1 text-xs opacity-75">(Ctrl+S)</span>
</Button>
<Button
size="sm"
2025-12-05 22:38:21 +00:00
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)
}
2025-12-05 13:45:45 +00:00
className="shadow-sm"
>
2025-12-05 22:38:21 +00:00
{translate('::App.Platform.Deploy')}
2025-12-05 13:45:45 +00:00
</Button>
</div>
</div>
2025-12-05 22:38:21 +00:00
</div>
2025-12-05 13:45:45 +00:00
{/* Main Content Area */}
<div className="flex-1 flex min-h-0">
2025-12-05 13:45:45 +00:00
{/* Left Panel - Object Explorer */}
2025-12-05 22:38:21 +00:00
<div className="w-1/3 flex-shrink-0 flex flex-col min-h-0 mr-4">
2025-12-05 13:45:45 +00:00
<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>
2025-12-05 22:38:21 +00:00
<SqlObjectExplorer
dataSource={state.selectedDataSource}
onObjectSelect={handleObjectSelect}
selectedObject={state.selectedObject}
onTemplateSelect={handleTemplateSelect}
onShowTableColumns={handleShowTableColumns}
refreshTrigger={state.refreshTrigger}
/>
2025-12-05 13:45:45 +00:00
</div>
</AdaptableCard>
</div>
{/* Center Panel - Editor and Results */}
<div className="flex-1 flex flex-col min-h-0 mr-4">
2025-12-05 22:38:21 +00:00
{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">
2025-12-05 13:45:45 +00:00
<div className="border-b px-4 py-2 bg-gray-50 dark:bg-gray-800 flex-shrink-0">
2025-12-05 22:38:21 +00:00
<h6 className="font-semibold text-sm">
{translate('::App.Platform.QueryEditor')}
</h6>
2025-12-05 13:45:45 +00:00
</div>
2025-12-05 22:38:21 +00:00
<div className="flex-1 min-h-0">
<SqlEditor
ref={editorRef}
value={state.editorContent}
onChange={handleEditorChange}
onExecute={handleExecute}
onSave={handleSave}
readOnly={state.isExecuting}
/>
2025-12-05 13:45:45 +00:00
</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)}
>
2025-12-05 22:38:21 +00:00
<h5 className="mb-4">{translate('::App.Platform.SaveQuery')}</h5>
2025-12-05 13:45:45 +00:00
<div className="space-y-4">
2025-12-05 22:38:21 +00:00
{/* 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>
)}
2025-12-05 13:45:45 +00:00
<div>
2025-12-05 22:38:21 +00:00
<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>
2025-12-05 13:45:45 +00:00
<Input
2025-12-05 22:38:21 +00:00
autoFocus={!saveDialogData.isExistingObject}
2025-12-05 13:45:45 +00:00
value={saveDialogData.name}
onChange={(e) => setSaveDialogData((prev) => ({ ...prev, name: e.target.value }))}
2025-12-05 22:38:21 +00:00
placeholder={saveDialogData.detectedName || translate('::App.Platform.Name')}
invalid={!saveDialogData.name.trim()}
disabled={saveDialogData.isExistingObject}
2025-12-05 13:45:45 +00:00
/>
</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 }))
}
2025-12-05 22:38:21 +00:00
placeholder={translate('::App.Platform.Description')}
2025-12-05 13:45:45 +00:00
/>
</div>
<div className="flex justify-end gap-2">
<Button variant="plain" onClick={() => setShowSaveDialog(false)}>
{translate('::App.Platform.Cancel')}
</Button>
2025-12-05 22:38:21 +00:00
<Button
variant="solid"
onClick={handleCreateNewQuery}
disabled={!saveDialogData.name.trim()}
>
2025-12-05 13:45:45 +00:00
{translate('::App.Platform.Save')}
</Button>
</div>
</div>
</Dialog>
</Container>
)
}
export default SqlQueryManager