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,
|
2025-12-05 14:56:39 +00:00
|
|
|
|
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 {
|
2025-12-05 14:56:39 +00:00
|
|
|
|
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:
|
2025-12-05 14:56:39 +00:00
|
|
|
|
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>
|
|
|
|
|
|
|
2025-12-05 14:56:39 +00:00
|
|
|
|
<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 */}
|
2025-12-05 14:56:39 +00:00
|
|
|
|
<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 */}
|
2025-12-05 14:56:39 +00:00
|
|
|
|
<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
|