1657 lines
60 KiB
TypeScript
1657 lines
60 KiB
TypeScript
import { useState, useCallback, useEffect, useRef } from 'react'
|
||
import { Button, Dialog, Notification, toast } from '@/components/ui'
|
||
import Container from '@/components/shared/Container'
|
||
import ConfirmDialog from '@/components/shared/ConfirmDialog'
|
||
import { getDataSources } from '@/services/data-source.service'
|
||
import type { DataSourceDto } from '@/proxy/data-source'
|
||
import { DataSourceTypeEnum } from '@/proxy/form/models'
|
||
import type { SqlQueryExecutionResultDto } from '@/proxy/sql-query-manager/models'
|
||
import { sqlObjectManagerService } from '@/services/sql-query-manager.service'
|
||
import { FaDatabase, FaPlay, FaFileAlt, FaCopy, FaExclamationTriangle } from 'react-icons/fa'
|
||
import { FaCheckCircle } from 'react-icons/fa'
|
||
import { useLocalization } from '@/utils/hooks/useLocalization'
|
||
import SqlObjectExplorer, { type SqlExplorerSelectedObject } from './SqlObjectExplorer'
|
||
import SqlEditor, { SqlEditorRef } from './SqlEditor'
|
||
import SqlResultsGrid from './SqlResultsGrid'
|
||
import SqlTableDesignerDialog from './SqlTableDesignerDialog'
|
||
import { Splitter } from '@/components/codeLayout/Splitter'
|
||
import { Helmet } from 'react-helmet'
|
||
import { useStoreState } from '@/store/store'
|
||
import { APP_NAME } from '@/constants/app.constant'
|
||
import { UiEvalService } from '@/services/UiEvalService'
|
||
import { FcAcceptDatabase } from 'react-icons/fc'
|
||
import { FaFolderOpen } from "react-icons/fa"
|
||
|
||
interface SqlManagerState {
|
||
dataSources: DataSourceDto[]
|
||
selectedDataSource: string | null
|
||
editorContent: string
|
||
isExecuting: boolean
|
||
executionResult: SqlQueryExecutionResultDto | null
|
||
showProperties: boolean
|
||
isDirty: boolean
|
||
tableColumns: any | null
|
||
refreshTrigger: number
|
||
}
|
||
|
||
interface SqlCopyResultItem {
|
||
targetDataSource: string
|
||
objectFullName: string
|
||
objectType: SqlExplorerSelectedObject['objectType']
|
||
status: 'success' | 'error' | 'skipped'
|
||
message: string
|
||
}
|
||
|
||
const SqlQueryManager = () => {
|
||
const { translate } = useLocalization()
|
||
const editorRef = useRef<SqlEditorRef>(null)
|
||
const tenantName = useStoreState((state) => state.locale.currentTenantName)
|
||
|
||
const [state, setState] = useState<SqlManagerState>({
|
||
dataSources: [],
|
||
selectedDataSource: tenantName ?? null,
|
||
editorContent: '',
|
||
isExecuting: false,
|
||
refreshTrigger: 0,
|
||
executionResult: null,
|
||
showProperties: false,
|
||
isDirty: false,
|
||
tableColumns: null,
|
||
})
|
||
|
||
const [showTemplateConfirmDialog, setShowTemplateConfirmDialog] = useState(false)
|
||
const [pendingTemplate, setPendingTemplate] = useState<{ content: string; type: string } | null>(
|
||
null,
|
||
)
|
||
const [showTableDesignerDialog, setShowTableDesignerDialog] = useState(false)
|
||
const [designTableData, setDesignTableData] = useState<{
|
||
schemaName: string
|
||
tableName: string
|
||
} | null>(null)
|
||
const [selectedExplorerObjects, setSelectedExplorerObjects] = useState<
|
||
SqlExplorerSelectedObject[]
|
||
>([])
|
||
const [showDbMigrateConfirmDialog, setShowDbMigrateConfirmDialog] = useState(false)
|
||
const [showCopyDialog, setShowCopyDialog] = useState(false)
|
||
const [copyTargetDataSources, setCopyTargetDataSources] = useState<string[]>([])
|
||
const [overwriteIfExists, setOverwriteIfExists] = useState(false)
|
||
const [isCopyingObjects, setIsCopyingObjects] = useState(false)
|
||
const [copyResults, setCopyResults] = useState<SqlCopyResultItem[]>([])
|
||
const [showCopyResultDialog, setShowCopyResultDialog] = useState(false)
|
||
const [copyDialogMode, setCopyDialogMode] = useState<'objects' | 'sql'>('objects')
|
||
const [sqlScriptForCopy, setSqlScriptForCopy] = useState('')
|
||
const [showSqlDataFilesDialog, setShowSqlDataFilesDialog] = useState(false)
|
||
const [isLoadingSqlDataFiles, setIsLoadingSqlDataFiles] = useState(false)
|
||
const [sqlDataFiles, setSqlDataFiles] = useState<{ fileName: string; createdAt: string }[]>([])
|
||
|
||
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].code ?? null,
|
||
}))
|
||
}
|
||
} 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.code ?? null,
|
||
editorContent: '',
|
||
executionResult: null,
|
||
isDirty: false,
|
||
}))
|
||
}, [])
|
||
|
||
const handleNewTable = useCallback(() => {
|
||
setDesignTableData(null)
|
||
setShowTableDesignerDialog(true)
|
||
}, [])
|
||
|
||
const handleDesignTable = useCallback((schemaName: string, tableName: string) => {
|
||
setDesignTableData({ schemaName, tableName })
|
||
setShowTableDesignerDialog(true)
|
||
}, [])
|
||
|
||
const handleEditorChange = useCallback((value: string | undefined) => {
|
||
setState((prev) => ({
|
||
...prev,
|
||
editorContent: value || '',
|
||
isDirty: true,
|
||
}))
|
||
}, [])
|
||
|
||
const escapeSqlLiteral = (value: string) => value.replace(/'/g, "''")
|
||
|
||
const escapeSqlIdentifier = (value: string) => value.replace(/]/g, ']]')
|
||
const escapePgIdentifier = (value: string) => value.replace(/"/g, '""')
|
||
|
||
const getSafeFullName = (schemaName: string, objectName: string) =>
|
||
`[${escapeSqlIdentifier(schemaName)}].[${escapeSqlIdentifier(objectName)}]`
|
||
const getSafePgFullName = (schemaName: string, objectName: string) =>
|
||
`"${escapePgIdentifier(schemaName)}"."${escapePgIdentifier(objectName)}"`
|
||
const selectedDataSourceType = state.dataSources.find(
|
||
(item) => item.code === state.selectedDataSource,
|
||
)?.dataSourceType
|
||
const isPostgreSql = selectedDataSourceType === DataSourceTypeEnum.Postgresql
|
||
|
||
const buildTableScriptQuery = (schemaName: string, tableName: string) => {
|
||
const fullName = getSafeFullName(schemaName, tableName)
|
||
const escapedFullName = escapeSqlLiteral(fullName)
|
||
|
||
return `DECLARE @ObjectId INT = OBJECT_ID(N'${escapedFullName}');
|
||
|
||
IF @ObjectId IS NULL
|
||
BEGIN
|
||
SELECT CAST('' AS NVARCHAR(MAX)) AS Script;
|
||
RETURN;
|
||
END;
|
||
|
||
;WITH cols AS
|
||
(
|
||
SELECT
|
||
c.column_id,
|
||
' ' + QUOTENAME(c.name) + ' ' +
|
||
CASE
|
||
WHEN t.name IN ('varchar', 'char', 'varbinary', 'binary') THEN
|
||
t.name + '(' + CASE WHEN c.max_length = -1 THEN 'MAX' ELSE CAST(c.max_length AS VARCHAR(10)) END + ')'
|
||
WHEN t.name IN ('nvarchar', 'nchar') THEN
|
||
t.name + '(' + CASE WHEN c.max_length = -1 THEN 'MAX' ELSE CAST(c.max_length / 2 AS VARCHAR(10)) END + ')'
|
||
WHEN t.name IN ('decimal', 'numeric') THEN
|
||
t.name + '(' + CAST(c.precision AS VARCHAR(10)) + ',' + CAST(c.scale AS VARCHAR(10)) + ')'
|
||
WHEN t.name IN ('datetime2', 'datetimeoffset', 'time') THEN
|
||
t.name + '(' + CAST(c.scale AS VARCHAR(10)) + ')'
|
||
ELSE t.name
|
||
END +
|
||
CASE
|
||
WHEN ic.object_id IS NOT NULL
|
||
THEN ' IDENTITY(' + CAST(ic.seed_value AS VARCHAR(30)) + ',' + CAST(ic.increment_value AS VARCHAR(30)) + ')'
|
||
ELSE ''
|
||
END +
|
||
CASE WHEN c.is_nullable = 1 THEN ' NULL' ELSE ' NOT NULL' END +
|
||
ISNULL(' DEFAULT ' + dc.definition, '') AS line
|
||
FROM sys.columns c
|
||
INNER JOIN sys.types t ON c.user_type_id = t.user_type_id
|
||
LEFT JOIN sys.identity_columns ic ON c.object_id = ic.object_id AND c.column_id = ic.column_id
|
||
LEFT JOIN sys.default_constraints dc ON c.default_object_id = dc.object_id
|
||
WHERE c.object_id = @ObjectId
|
||
),
|
||
pk AS
|
||
(
|
||
SELECT
|
||
' CONSTRAINT ' + QUOTENAME(k.name) + ' PRIMARY KEY ' +
|
||
CASE WHEN i.type = 1 THEN 'CLUSTERED' ELSE 'NONCLUSTERED' END +
|
||
CHAR(13) + CHAR(10) + ' (' + CHAR(13) + CHAR(10) +
|
||
(
|
||
SELECT ' ' + QUOTENAME(c.name) + CASE WHEN ic.is_descending_key = 1 THEN ' DESC' ELSE ' ASC' END + CHAR(13) + CHAR(10)
|
||
FROM sys.index_columns ic
|
||
INNER JOIN sys.columns c ON ic.object_id = c.object_id AND ic.column_id = c.column_id
|
||
WHERE ic.object_id = i.object_id
|
||
AND ic.index_id = i.index_id
|
||
AND ic.is_included_column = 0
|
||
ORDER BY ic.key_ordinal
|
||
FOR XML PATH(''), TYPE
|
||
).value('.', 'NVARCHAR(MAX)') +
|
||
' )WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON, OPTIMIZE_FOR_SEQUENTIAL_KEY = OFF) ON [PRIMARY]' AS line
|
||
FROM sys.key_constraints k
|
||
INNER JOIN sys.indexes i ON k.parent_object_id = i.object_id AND k.unique_index_id = i.index_id
|
||
WHERE k.parent_object_id = @ObjectId
|
||
AND k.type = 'PK'
|
||
)
|
||
SELECT
|
||
'IF OBJECT_ID(N''${escapedFullName}'', ''U'') IS NULL' + CHAR(13) + CHAR(10) +
|
||
'BEGIN' + CHAR(13) + CHAR(10) +
|
||
' CREATE TABLE ${fullName}' + CHAR(13) + CHAR(10) +
|
||
' (' + CHAR(13) + CHAR(10) +
|
||
STUFF(
|
||
(
|
||
SELECT ',' + CHAR(13) + CHAR(10) + line
|
||
FROM cols
|
||
ORDER BY column_id
|
||
FOR XML PATH(''), TYPE
|
||
).value('.', 'NVARCHAR(MAX)')
|
||
,1,3,'') +
|
||
ISNULL(
|
||
(
|
||
SELECT ',' + CHAR(13) + CHAR(10) + line
|
||
FROM pk
|
||
),
|
||
''
|
||
) + CHAR(13) + CHAR(10) + ' ) ON [PRIMARY]' + CHAR(13) + CHAR(10) + 'END' + CHAR(13) + CHAR(10) + 'GO' AS Script;`
|
||
}
|
||
|
||
const getTableCreateScript = async (schemaName: string, tableName: string): Promise<string> => {
|
||
if (!state.selectedDataSource) return ''
|
||
|
||
const result = await sqlObjectManagerService.getTableCreateScript(
|
||
state.selectedDataSource,
|
||
schemaName,
|
||
tableName,
|
||
)
|
||
|
||
return result.data || ''
|
||
}
|
||
|
||
const normalizeNativeDefinitionToCreate = (definition: string) => {
|
||
if (!definition?.trim()) return ''
|
||
if (isPostgreSql) return definition
|
||
return definition.replace(/^\s*(?:CREATE|ALTER)\s+(?:OR\s+ALTER\s+)?/i, 'CREATE OR ALTER ')
|
||
}
|
||
|
||
const buildDropIfExistsScript = (obj: SqlExplorerSelectedObject) => {
|
||
if (isPostgreSql) {
|
||
const fullName = getSafePgFullName(obj.schemaName, obj.objectName)
|
||
|
||
if (obj.objectType === 'table') {
|
||
return `DROP TABLE IF EXISTS ${fullName};`
|
||
}
|
||
|
||
if (obj.objectType === 'view') {
|
||
return `DROP VIEW IF EXISTS ${fullName};`
|
||
}
|
||
|
||
if (obj.objectType === 'procedure') {
|
||
return `DROP PROCEDURE IF EXISTS ${fullName};`
|
||
}
|
||
|
||
return `DROP FUNCTION IF EXISTS ${fullName};`
|
||
}
|
||
|
||
const fullName = getSafeFullName(obj.schemaName, obj.objectName)
|
||
|
||
if (obj.objectType === 'table') {
|
||
return `IF OBJECT_ID(N'${escapeSqlLiteral(fullName)}', N'U') IS NOT NULL DROP TABLE ${fullName};`
|
||
}
|
||
|
||
if (obj.objectType === 'view') {
|
||
return `IF OBJECT_ID(N'${escapeSqlLiteral(fullName)}', N'V') IS NOT NULL DROP VIEW ${fullName};`
|
||
}
|
||
|
||
if (obj.objectType === 'procedure') {
|
||
return `IF OBJECT_ID(N'${escapeSqlLiteral(fullName)}', N'P') IS NOT NULL DROP PROCEDURE ${fullName};`
|
||
}
|
||
|
||
return `IF OBJECT_ID(N'${escapeSqlLiteral(fullName)}', N'FN') IS NOT NULL OR OBJECT_ID(N'${escapeSqlLiteral(fullName)}', N'IF') IS NOT NULL OR OBJECT_ID(N'${escapeSqlLiteral(fullName)}', N'TF') IS NOT NULL DROP FUNCTION ${fullName};`
|
||
}
|
||
|
||
const buildObjectExistsCheckQuery = (obj: SqlExplorerSelectedObject) => {
|
||
if (isPostgreSql) {
|
||
const schema = escapeSqlLiteral(obj.schemaName)
|
||
const name = escapeSqlLiteral(obj.objectName)
|
||
|
||
if (obj.objectType === 'table') {
|
||
return `SELECT CASE WHEN EXISTS (
|
||
SELECT 1 FROM information_schema.tables
|
||
WHERE table_schema = '${schema}' AND table_name = '${name}' AND table_type = 'BASE TABLE'
|
||
) THEN 1 ELSE 0 END AS "ExistsFlag";`
|
||
}
|
||
|
||
if (obj.objectType === 'view') {
|
||
return `SELECT CASE WHEN EXISTS (
|
||
SELECT 1 FROM information_schema.views
|
||
WHERE table_schema = '${schema}' AND table_name = '${name}'
|
||
) THEN 1 ELSE 0 END AS "ExistsFlag";`
|
||
}
|
||
|
||
const proKind = obj.objectType === 'procedure' ? 'p' : 'f'
|
||
return `SELECT CASE WHEN EXISTS (
|
||
SELECT 1
|
||
FROM pg_proc p
|
||
INNER JOIN pg_namespace n ON n.oid = p.pronamespace
|
||
WHERE n.nspname = '${schema}' AND p.proname = '${name}' AND p.prokind = '${proKind}'
|
||
) THEN 1 ELSE 0 END AS "ExistsFlag";`
|
||
}
|
||
|
||
const fullName = getSafeFullName(obj.schemaName, obj.objectName)
|
||
const escapedFullName = escapeSqlLiteral(fullName)
|
||
|
||
if (obj.objectType === 'table') {
|
||
return `SELECT CASE WHEN OBJECT_ID(N'${escapedFullName}', N'U') IS NOT NULL THEN 1 ELSE 0 END AS ExistsFlag;`
|
||
}
|
||
|
||
if (obj.objectType === 'view') {
|
||
return `SELECT CASE WHEN OBJECT_ID(N'${escapedFullName}', N'V') IS NOT NULL THEN 1 ELSE 0 END AS ExistsFlag;`
|
||
}
|
||
|
||
if (obj.objectType === 'procedure') {
|
||
return `SELECT CASE WHEN OBJECT_ID(N'${escapedFullName}', N'P') IS NOT NULL THEN 1 ELSE 0 END AS ExistsFlag;`
|
||
}
|
||
|
||
return `SELECT CASE WHEN OBJECT_ID(N'${escapedFullName}', N'FN') IS NOT NULL OR OBJECT_ID(N'${escapedFullName}', N'IF') IS NOT NULL OR OBJECT_ID(N'${escapedFullName}', N'TF') IS NOT NULL THEN 1 ELSE 0 END AS ExistsFlag;`
|
||
}
|
||
|
||
const checkObjectExistsInTarget = async (
|
||
targetDataSource: string,
|
||
obj: SqlExplorerSelectedObject,
|
||
) => {
|
||
const result = await sqlObjectManagerService.executeQuery({
|
||
queryText: buildObjectExistsCheckQuery(obj),
|
||
dataSourceCode: targetDataSource,
|
||
})
|
||
|
||
const firstRow = result.data?.data?.[0]
|
||
const flag = firstRow?.ExistsFlag ?? firstRow?.existsFlag ?? 0
|
||
return Number(flag) === 1
|
||
}
|
||
|
||
const getTemplateContent = (templateType: string): string => {
|
||
if (isPostgreSql) {
|
||
const pgTemplates: Record<string, string> = {
|
||
select: `-- Basic SELECT query
|
||
SELECT
|
||
"Column1",
|
||
"Column2",
|
||
"Column3"
|
||
FROM
|
||
"public"."TableName"
|
||
WHERE
|
||
"Column1" = 'value'
|
||
ORDER BY
|
||
"Column1" ASC
|
||
LIMIT 100;`,
|
||
|
||
insert: `-- Basic INSERT query
|
||
INSERT INTO "public"."TableName" ("Column1", "Column2", "Column3")
|
||
VALUES
|
||
('Value1', 'Value2', 'Value3');`,
|
||
|
||
update: `-- Basic UPDATE query
|
||
UPDATE "public"."TableName"
|
||
SET
|
||
"Column1" = 'NewValue1',
|
||
"Column2" = 'NewValue2'
|
||
WHERE
|
||
"Id" = '00000000-0000-0000-0000-000000000000';`,
|
||
|
||
delete: `-- Basic DELETE query
|
||
DELETE FROM "public"."TableName"
|
||
WHERE
|
||
"Id" = '00000000-0000-0000-0000-000000000000';`,
|
||
|
||
'create-procedure': `-- Create Stored Procedure
|
||
CREATE OR REPLACE PROCEDURE "public"."ProcedureName"(
|
||
"Parameter1" integer,
|
||
"Parameter2" varchar
|
||
)
|
||
LANGUAGE plpgsql
|
||
AS $$
|
||
BEGIN
|
||
-- Add your logic here
|
||
END;
|
||
$$;`,
|
||
|
||
'create-view': `-- Create View
|
||
CREATE OR REPLACE VIEW "public"."ViewName" AS
|
||
SELECT
|
||
t1."Column1",
|
||
t1."Column2"
|
||
FROM "public"."TableName1" t1
|
||
WHERE t1."IsActive" = TRUE;`,
|
||
|
||
'create-scalar-function': `-- Create Scalar Function
|
||
CREATE OR REPLACE FUNCTION "public"."ScalarFunctionName"(
|
||
"Parameter1" integer,
|
||
"Parameter2" varchar
|
||
)
|
||
RETURNS varchar
|
||
LANGUAGE plpgsql
|
||
AS $$
|
||
BEGIN
|
||
RETURN "Parameter2";
|
||
END;
|
||
$$;`,
|
||
|
||
'create-table-function': `-- Create Table-Valued Function
|
||
CREATE OR REPLACE FUNCTION "public"."TableFunctionName"(
|
||
"Parameter1" integer
|
||
)
|
||
RETURNS TABLE("Column1" integer, "Column2" varchar)
|
||
LANGUAGE sql
|
||
AS $$
|
||
SELECT t."Column1", t."Column2"
|
||
FROM "public"."TableName" t
|
||
WHERE t."Id" = "Parameter1";
|
||
$$;`,
|
||
}
|
||
|
||
return pgTemplates[templateType] || pgTemplates.select
|
||
}
|
||
|
||
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']
|
||
}
|
||
|
||
const applyTemplate = useCallback((templateContent: string) => {
|
||
setState((prev) => ({
|
||
...prev,
|
||
editorContent: templateContent,
|
||
executionResult: null,
|
||
isDirty: false,
|
||
}))
|
||
}, [])
|
||
|
||
const handleUseTemplateFromDialog = useCallback(
|
||
(templateContent: string, templateType: string) => {
|
||
// Check if editor has content
|
||
const hasUserContent = state.editorContent?.trim() && state.isDirty
|
||
|
||
if (hasUserContent) {
|
||
// Ask for confirmation
|
||
setPendingTemplate({ content: templateContent, type: templateType })
|
||
setShowTemplateConfirmDialog(true)
|
||
} else {
|
||
// Apply template directly
|
||
applyTemplate(templateContent)
|
||
}
|
||
},
|
||
[state.editorContent, state.isDirty, applyTemplate],
|
||
)
|
||
|
||
const handleTemplateSelect = useCallback(
|
||
(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 handleNewQuery = useCallback(() => {
|
||
setState((prev) => ({
|
||
...prev,
|
||
editorContent: '',
|
||
executionResult: null,
|
||
tableColumns: null,
|
||
isDirty: false,
|
||
}))
|
||
}, [])
|
||
|
||
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 || '',
|
||
})
|
||
|
||
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 handleViewDefinition = async (schemaName: string, objectName: string) => {
|
||
if (!state.selectedDataSource) return
|
||
try {
|
||
const result = await sqlObjectManagerService.getNativeObjectDefinition(
|
||
state.selectedDataSource,
|
||
schemaName,
|
||
objectName,
|
||
)
|
||
if (result.data) {
|
||
const definition = normalizeNativeDefinitionToCreate(result.data)
|
||
setState((prev) => ({
|
||
...prev,
|
||
editorContent: definition,
|
||
executionResult: null,
|
||
tableColumns: null,
|
||
isDirty: false,
|
||
}))
|
||
}
|
||
} catch (error: any) {
|
||
toast.push(
|
||
<Notification type="danger" title={translate('::App.Platform.Error')}>
|
||
{error.response?.data?.error?.message ||
|
||
translate('::App.Platform.FailedToLoadDefinition')}
|
||
</Notification>,
|
||
{ placement: 'top-center' },
|
||
)
|
||
}
|
||
}
|
||
|
||
const handleGenerateTableScript = async (schemaName: string, tableName: string) => {
|
||
if (!state.selectedDataSource) return
|
||
|
||
try {
|
||
const script = await getTableCreateScript(schemaName, tableName)
|
||
|
||
if (!script?.trim()) {
|
||
toast.push(
|
||
<Notification type="warning" title={translate('::App.Platform.Warning')}>
|
||
{translate('::App.Platform.ScriptNotGenerated') || 'Tablo scripti olusturulamadi.'}
|
||
</Notification>,
|
||
{ placement: 'top-center' },
|
||
)
|
||
return
|
||
}
|
||
|
||
setState((prev) => ({
|
||
...prev,
|
||
editorContent: script,
|
||
executionResult: null,
|
||
tableColumns: null,
|
||
isDirty: false,
|
||
}))
|
||
|
||
toast.push(
|
||
<Notification type="success" title={translate('::App.Platform.Success') || 'Basarili'}>
|
||
{'Script Query Editor alanina yuklendi.'}
|
||
</Notification>,
|
||
{ placement: 'top-center' },
|
||
)
|
||
} catch (error: any) {
|
||
toast.push(
|
||
<Notification type="danger" title={translate('::App.Platform.Error')}>
|
||
{error.response?.data?.error?.message || 'Tablo scripti olusturulurken hata olustu.'}
|
||
</Notification>,
|
||
{ placement: 'top-center' },
|
||
)
|
||
}
|
||
}
|
||
|
||
const handleOpenCopyDialog = () => {
|
||
if (!state.selectedDataSource) return
|
||
|
||
setCopyDialogMode('objects')
|
||
setCopyTargetDataSources([])
|
||
setOverwriteIfExists(false)
|
||
setSqlScriptForCopy('')
|
||
setShowCopyDialog(true)
|
||
|
||
// Eğer seçili obje yoksa uyarı göster
|
||
if (selectedExplorerObjects.length === 0) {
|
||
// SQL mode'da obje seçimi zorunlu değil, object mode'da zorunlu
|
||
// Bu uyarı sadece object mode'da gerekirse
|
||
return
|
||
}
|
||
}
|
||
|
||
const handleCopyObjects = async () => {
|
||
if (
|
||
!state.selectedDataSource ||
|
||
selectedExplorerObjects.length === 0 ||
|
||
copyTargetDataSources.length === 0
|
||
) {
|
||
return
|
||
}
|
||
|
||
setIsCopyingObjects(true)
|
||
|
||
const results: SqlCopyResultItem[] = []
|
||
|
||
try {
|
||
const scriptsByObjectId = new Map<string, string>()
|
||
|
||
for (const obj of selectedExplorerObjects) {
|
||
let createScript = ''
|
||
|
||
if (obj.objectType === 'table') {
|
||
createScript = await getTableCreateScript(obj.schemaName, obj.objectName)
|
||
} else {
|
||
const response = await sqlObjectManagerService.getNativeObjectDefinition(
|
||
state.selectedDataSource,
|
||
obj.schemaName,
|
||
obj.objectName,
|
||
)
|
||
createScript = normalizeNativeDefinitionToCreate(response.data || '')
|
||
}
|
||
|
||
if (!createScript?.trim()) {
|
||
results.push({
|
||
targetDataSource: state.selectedDataSource,
|
||
objectFullName: obj.fullName,
|
||
objectType: obj.objectType,
|
||
status: 'error',
|
||
message: translate('::App.Platform.CreateScriptFailed'),
|
||
})
|
||
continue
|
||
}
|
||
|
||
scriptsByObjectId.set(obj.id, createScript)
|
||
}
|
||
|
||
for (const targetDataSource of copyTargetDataSources) {
|
||
for (const obj of selectedExplorerObjects) {
|
||
const createScript = scriptsByObjectId.get(obj.id)
|
||
if (!createScript) continue
|
||
|
||
if (!overwriteIfExists) {
|
||
const exists = await checkObjectExistsInTarget(targetDataSource, obj)
|
||
if (exists) {
|
||
results.push({
|
||
targetDataSource,
|
||
objectFullName: obj.fullName,
|
||
objectType: obj.objectType,
|
||
status: 'skipped',
|
||
message: translate('::App.SqlQueryManager.SkippedDescription'),
|
||
})
|
||
continue
|
||
}
|
||
}
|
||
|
||
const command = overwriteIfExists
|
||
? `${buildDropIfExistsScript(obj)}\n${createScript}`
|
||
: createScript
|
||
|
||
try {
|
||
await sqlObjectManagerService.executeQuery({
|
||
queryText: command,
|
||
dataSourceCode: targetDataSource,
|
||
})
|
||
results.push({
|
||
targetDataSource,
|
||
objectFullName: obj.fullName,
|
||
objectType: obj.objectType,
|
||
status: 'success',
|
||
message: translate('::App.Platform.CopyCompleted'),
|
||
})
|
||
} catch (error: any) {
|
||
results.push({
|
||
targetDataSource,
|
||
objectFullName: obj.fullName,
|
||
objectType: obj.objectType,
|
||
status: 'error',
|
||
message: error.response?.data?.error?.message || 'Kopyalama basarisiz.',
|
||
})
|
||
}
|
||
}
|
||
}
|
||
|
||
const successCount = results.filter((x) => x.status === 'success').length
|
||
const errorCount = results.filter((x) => x.status === 'error').length
|
||
const skippedCount = results.filter((x) => x.status === 'skipped').length
|
||
|
||
const notificationType = errorCount > 0 ? 'warning' : 'success'
|
||
toast.push(
|
||
<Notification
|
||
type={notificationType}
|
||
title={
|
||
errorCount > 0
|
||
? translate('::App.Platform.Warning')
|
||
: translate('::App.Platform.Success')
|
||
}
|
||
>
|
||
{translate('::App.Platform.CopyCompleted') ||
|
||
`translate('::App.Platform.Successful'): ${successCount}, ${translate('::App.Platform.Error')}: ${errorCount}, ${translate('::App.Platform.Skipped')}: ${skippedCount}`}
|
||
</Notification>,
|
||
{ placement: 'top-center' },
|
||
)
|
||
|
||
setCopyResults(results)
|
||
setShowCopyResultDialog(true)
|
||
|
||
setShowCopyDialog(false)
|
||
} finally {
|
||
setIsCopyingObjects(false)
|
||
}
|
||
}
|
||
|
||
const handleExecuteDirectSql = async () => {
|
||
if (!sqlScriptForCopy?.trim()) {
|
||
toast.push(
|
||
<Notification type="warning" title={translate('::App.Platform.Warning')}>
|
||
{translate('::App.Platform.PleaseEnterQuery')}
|
||
</Notification>,
|
||
{ placement: 'top-center' },
|
||
)
|
||
return
|
||
}
|
||
|
||
if (copyTargetDataSources.length === 0) {
|
||
toast.push(
|
||
<Notification type="warning" title={translate('::App.Platform.Warning')}>
|
||
{translate('::App.Platform.PleaseSelectAtLeastOneTarget')}
|
||
</Notification>,
|
||
{ placement: 'top-center' },
|
||
)
|
||
return
|
||
}
|
||
|
||
setIsCopyingObjects(true)
|
||
const results: SqlCopyResultItem[] = []
|
||
|
||
try {
|
||
for (const targetDataSource of copyTargetDataSources) {
|
||
try {
|
||
await sqlObjectManagerService.executeQuery({
|
||
queryText: sqlScriptForCopy,
|
||
dataSourceCode: targetDataSource,
|
||
})
|
||
results.push({
|
||
targetDataSource,
|
||
objectFullName: 'SQL Script',
|
||
objectType: 'script' as any,
|
||
status: 'success',
|
||
message: 'Basariyla calistirildi.',
|
||
})
|
||
} catch (error: any) {
|
||
results.push({
|
||
targetDataSource,
|
||
objectFullName: 'SQL Script',
|
||
objectType: 'script' as any,
|
||
status: 'error',
|
||
message: error.response?.data?.error?.message || 'Calistirma basarisiz.',
|
||
})
|
||
}
|
||
}
|
||
|
||
const successCount = results.filter((x) => x.status === 'success').length
|
||
const errorCount = results.filter((x) => x.status === 'error').length
|
||
|
||
const notificationType = errorCount > 0 ? 'warning' : 'success'
|
||
toast.push(
|
||
<Notification
|
||
type={notificationType}
|
||
title={
|
||
errorCount > 0
|
||
? translate('::App.Platform.Warning')
|
||
: translate('::App.Platform.Success')
|
||
}
|
||
>
|
||
{translate('::App.Platform.ExecutionCompleted') ||
|
||
`translate('::App.Platform.Successful'): ${successCount}, ${translate('::App.Platform.Error')}: ${errorCount}`}
|
||
</Notification>,
|
||
{ placement: 'top-center' },
|
||
)
|
||
|
||
setCopyResults(results)
|
||
setShowCopyResultDialog(true)
|
||
setShowCopyDialog(false)
|
||
} finally {
|
||
setIsCopyingObjects(false)
|
||
}
|
||
}
|
||
|
||
const availableTargetDataSourceCodes = state.dataSources
|
||
.filter((d) => d.code && d.code !== state.selectedDataSource)
|
||
.map((d) => d.code || '')
|
||
|
||
const allTargetsSelected =
|
||
availableTargetDataSourceCodes.length > 0 &&
|
||
availableTargetDataSourceCodes.every((code) => copyTargetDataSources.includes(code))
|
||
|
||
const handleToggleSelectAllTargets = (checked: boolean) => {
|
||
if (checked) {
|
||
setCopyTargetDataSources(availableTargetDataSourceCodes)
|
||
return
|
||
}
|
||
|
||
setCopyTargetDataSources([])
|
||
}
|
||
|
||
const copySuccessCount = copyResults.filter((x) => x.status === 'success').length
|
||
const copyErrorCount = copyResults.filter((x) => x.status === 'error').length
|
||
const copySkippedCount = copyResults.filter((x) => x.status === 'skipped').length
|
||
|
||
const handleOpenSqlDataFilesDialog = async () => {
|
||
setShowSqlDataFilesDialog(true)
|
||
setIsLoadingSqlDataFiles(true)
|
||
|
||
try {
|
||
const response = await sqlObjectManagerService.getSqlDataFiles()
|
||
setSqlDataFiles(response.data || [])
|
||
} catch (error: any) {
|
||
setSqlDataFiles([])
|
||
toast.push(
|
||
<Notification type="danger" title={translate('::App.Platform.Error')}>
|
||
{error.response?.data?.error?.message ||
|
||
translate('::App.Platform.FailedToLoadFiles') ||
|
||
'SQL dosya listesi yuklenemedi.'}
|
||
</Notification>,
|
||
{ placement: 'top-center' },
|
||
)
|
||
} finally {
|
||
setIsLoadingSqlDataFiles(false)
|
||
}
|
||
}
|
||
|
||
return (
|
||
<Container className="flex flex-col overflow-hidden" style={{ height: 'calc(100vh - 130px)' }}>
|
||
<Helmet
|
||
titleTemplate={`%s | ${APP_NAME}`}
|
||
title={translate('::' + 'App.SqlQueryManager')}
|
||
defaultTitle={APP_NAME}
|
||
></Helmet>
|
||
|
||
<div className="flex flex-col flex-1 min-h-0 p-1">
|
||
{/* Toolbar */}
|
||
<div className="flex-shrink-0 shadow-sm mb-4">
|
||
<div className="flex flex-col gap-2 px-1 py-1 lg:flex-row lg:items-center lg:justify-between">
|
||
<div className="flex flex-wrap items-center gap-2 sm:gap-3">
|
||
<FaDatabase className="text-lg text-blue-500" />
|
||
<select
|
||
className="border border-gray-300 rounded px-2 py-1 max-w-full dark:bg-gray-700 dark:border-gray-600"
|
||
disabled={state.selectedDataSource?.length === 0}
|
||
value={state.selectedDataSource || ''}
|
||
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>
|
||
<Button
|
||
size="sm"
|
||
variant="default"
|
||
icon={<FcAcceptDatabase />}
|
||
onClick={() => setShowDbMigrateConfirmDialog(true)}
|
||
title={translate('::App.DbMigrate.StartMessage') || 'Run DB Migration'}
|
||
>
|
||
{translate('::ListForms.ListForm.DbMigrate') || 'DB Migrate'}
|
||
</Button>
|
||
<Button
|
||
size="sm"
|
||
variant="default"
|
||
icon={<FaFolderOpen />}
|
||
onClick={handleOpenSqlDataFilesDialog}
|
||
className="shadow-sm px-2 py-1"
|
||
title={translate('::App.SqlQueryManager.SqlDataFiles') || 'Show SqlData files'}
|
||
>
|
||
{translate('::App.SqlQueryManager.SqlDataFiles') || 'SqlData Files'}
|
||
</Button>
|
||
</div>
|
||
|
||
<div className="flex flex-wrap items-center gap-2 sm:gap-3">
|
||
<Button
|
||
size="sm"
|
||
variant="default"
|
||
icon={<FaCopy />}
|
||
onClick={handleOpenCopyDialog}
|
||
disabled={!state.selectedDataSource}
|
||
className="shadow-sm px-2 py-1"
|
||
title={
|
||
translate('::App.Platform.CopyOrExecuteSql') ||
|
||
'Seçili nesneleri kopyala veya SQL script calistir'
|
||
}
|
||
>
|
||
{translate('::App.Platform.CopySelectedObjects') || 'Copy Selected Objects'}
|
||
</Button>
|
||
<Button
|
||
size="sm"
|
||
variant="default"
|
||
icon={<FaFileAlt />}
|
||
onClick={handleNewQuery}
|
||
className="shadow-sm px-2 py-1"
|
||
>
|
||
{translate('::App.Platform.NewQuery') || 'New Query'}
|
||
</Button>
|
||
<Button
|
||
size="sm"
|
||
variant="solid"
|
||
color="blue-600"
|
||
icon={<FaPlay />}
|
||
onClick={handleExecute}
|
||
loading={state.isExecuting}
|
||
disabled={!state.selectedDataSource}
|
||
className="shadow-sm px-2 py-1"
|
||
>
|
||
{translate('::App.Platform.Execute')}
|
||
<span className="ml-1 text-xs opacity-75">(F5)</span>
|
||
</Button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Main Content Area */}
|
||
<div className="flex-1 flex min-h-0 flex-col gap-3 lg:flex-row lg:gap-4">
|
||
{/* Left Panel - Object Explorer */}
|
||
<div className="w-full lg:w-1/3 flex-shrink-0 flex flex-col h-[35vh] min-h-[260px] max-h-[420px] lg:h-auto lg:min-h-0 lg:max-h-none bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-600 shadow">
|
||
<div className="border-b px-4 py-2 bg-gray-50 dark:bg-gray-800 flex-shrink-0 rounded-t-lg">
|
||
<h6 className="font-semibold text-sm">
|
||
{translate('::App.Platform.ObjectExplorer')}
|
||
</h6>
|
||
</div>
|
||
<div className="flex-1 min-h-0 flex flex-col overflow-hidden">
|
||
<SqlObjectExplorer
|
||
dataSource={state.selectedDataSource}
|
||
dataSourceType={selectedDataSourceType}
|
||
onTemplateSelect={handleTemplateSelect}
|
||
onViewDefinition={handleViewDefinition}
|
||
onGenerateTableScript={handleGenerateTableScript}
|
||
refreshTrigger={state.refreshTrigger}
|
||
onNewTable={handleNewTable}
|
||
onDesignTable={handleDesignTable}
|
||
onSelectedObjectsChange={setSelectedExplorerObjects}
|
||
/>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Center Panel - Editor and Results */}
|
||
<div className="flex-1 flex flex-col min-h-0">
|
||
{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}
|
||
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">
|
||
<FaCheckCircle 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}
|
||
readOnly={state.isExecuting}
|
||
/>
|
||
</div>
|
||
</div>
|
||
)}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* DB Migrate Confirmation Dialog */}
|
||
<ConfirmDialog
|
||
isOpen={showDbMigrateConfirmDialog}
|
||
type="info"
|
||
title={translate('::ListForms.ListForm.DbMigrate') || 'DB Migrate'}
|
||
cancelText={translate('::Cancel')}
|
||
confirmText={translate('::App.Platform.Execute') || 'Çalıştır'}
|
||
onCancel={() => setShowDbMigrateConfirmDialog(false)}
|
||
onClose={() => setShowDbMigrateConfirmDialog(false)}
|
||
onConfirm={() => {
|
||
setShowDbMigrateConfirmDialog(false)
|
||
UiEvalService.ApiDbMigrate()
|
||
}}
|
||
>
|
||
<p className="text-gray-600 dark:text-gray-400">
|
||
{translate('::App.DbMigrate.ConfirmMessage') || 'Are you sure you want to start the database migration process?'}
|
||
</p>
|
||
</ConfirmDialog>
|
||
|
||
<Dialog
|
||
isOpen={showSqlDataFilesDialog}
|
||
onClose={() => setShowSqlDataFilesDialog(false)}
|
||
onRequestClose={() => setShowSqlDataFilesDialog(false)}
|
||
contentClassName="max-h-[90vh] overflow-hidden"
|
||
>
|
||
<div className="flex max-h-[72vh] min-h-[320px] flex-col">
|
||
<h5 className="mb-4 shrink-0">
|
||
{translate('::App.SqlQueryManager.SqlDataFiles') || 'SqlData Files'}
|
||
</h5>
|
||
|
||
{isLoadingSqlDataFiles ? (
|
||
<p className="mb-4 text-gray-600 dark:text-gray-400">
|
||
{translate('::App.Platform.Loading') || 'Loading...'}
|
||
</p>
|
||
) : sqlDataFiles.length === 0 ? (
|
||
<p className="mb-4 text-gray-600 dark:text-gray-400">
|
||
{translate('::App.SqlQueryManager.NoSqlDataFiles') || 'SqlData klasorunde .sql dosyasi bulunamadi.'}
|
||
</p>
|
||
) : (
|
||
<div className="mb-4 h-[70vh] min-h-[220px] overflow-y-auto rounded border border-gray-200 dark:border-gray-700">
|
||
<ul className="divide-y divide-gray-200 dark:divide-gray-700">
|
||
{sqlDataFiles.map((file) => (
|
||
<li key={file.fileName} className="flex items-center justify-between px-3 py-2 text-sm text-gray-700 dark:text-gray-200">
|
||
<span>{file.fileName}</span>
|
||
<span className="ml-4 shrink-0 text-xs text-gray-400 dark:text-gray-500">
|
||
{new Date(file.createdAt).toLocaleString()}
|
||
</span>
|
||
</li>
|
||
))}
|
||
</ul>
|
||
</div>
|
||
)}
|
||
</div>
|
||
</Dialog>
|
||
|
||
{/* 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('::Cancel')}
|
||
</Button>
|
||
<Button variant="solid" onClick={handleConfirmTemplateReplace} icon={<FaExclamationTriangle />} color="red-600">
|
||
{translate('::App.Platform.Replace')}
|
||
</Button>
|
||
</div>
|
||
</Dialog>
|
||
|
||
{/* Table Designer Dialog */}
|
||
<SqlTableDesignerDialog
|
||
isOpen={showTableDesignerDialog}
|
||
onClose={() => {
|
||
setShowTableDesignerDialog(false)
|
||
setDesignTableData(null)
|
||
}}
|
||
dataSource={state.selectedDataSource ?? ''}
|
||
initialTableData={designTableData}
|
||
onDeployed={() => {
|
||
setState((prev) => ({ ...prev, refreshTrigger: prev.refreshTrigger + 1 }))
|
||
}}
|
||
/>
|
||
|
||
<Dialog
|
||
isOpen={showCopyDialog}
|
||
onClose={() => !isCopyingObjects && setShowCopyDialog(false)}
|
||
onRequestClose={() => !isCopyingObjects && setShowCopyDialog(false)}
|
||
width={1050}
|
||
>
|
||
<Dialog.Body className="flex flex-col gap-2">
|
||
<h5 className="mb-1 flex-shrink-0">{translate('::App.Platform.CopySelectedObjects')}</h5>
|
||
|
||
{/* Mode Tabs */}
|
||
<div className="flex gap-2 mb-2 border-b flex-shrink-0">
|
||
<button
|
||
onClick={() => setCopyDialogMode('objects')}
|
||
className={`px-4 py-2 font-medium text-sm border-b-2 transition ${
|
||
copyDialogMode === 'objects'
|
||
? 'border-blue-500 text-blue-600'
|
||
: 'border-transparent text-gray-600 hover:text-gray-900'
|
||
}`}
|
||
disabled={isCopyingObjects}
|
||
>
|
||
{translate('::App.Platform.CopyObjects') || 'Nesneleri Kopyala'}
|
||
</button>
|
||
<button
|
||
onClick={() => setCopyDialogMode('sql')}
|
||
className={`px-4 py-2 font-medium text-sm border-b-2 transition ${
|
||
copyDialogMode === 'sql'
|
||
? 'border-blue-500 text-blue-600'
|
||
: 'border-transparent text-gray-600 hover:text-gray-900'
|
||
}`}
|
||
disabled={isCopyingObjects}
|
||
>
|
||
{translate('::App.Platform.DirectSqlScript') || 'Direkt SQL Script'}
|
||
</button>
|
||
</div>
|
||
|
||
<div className="flex-1 min-h-0 overflow-y-auto pr-1">
|
||
{copyDialogMode === 'objects' ? (
|
||
<>
|
||
{/* Objects Mode */}
|
||
<div className="mb-2 flex items-center justify-between gap-4">
|
||
<p className="text-sm text-gray-600 dark:text-gray-400 mb-0">
|
||
{translate('::App.Platform.SourceDataSource')}:{' '}
|
||
<strong>{state.selectedDataSource}</strong>
|
||
</p>
|
||
<label className="flex items-center gap-2 text-sm cursor-pointer shrink-0">
|
||
<input
|
||
type="checkbox"
|
||
checked={overwriteIfExists}
|
||
onChange={(e) => setOverwriteIfExists(e.target.checked)}
|
||
disabled={isCopyingObjects}
|
||
/>
|
||
<span>{translate('::App.Platform.OverwriteIfExists')}</span>
|
||
</label>
|
||
</div>
|
||
|
||
<div className="mb-4 max-h-36 overflow-auto border rounded p-2">
|
||
{selectedExplorerObjects.length === 0 ? (
|
||
<p className="text-sm text-gray-500">
|
||
{translate('::App.Platform.NoObjectSelected') ||
|
||
'Secili nesne yok. Lutfen Explorer alanından bir nesne secin.'}
|
||
</p>
|
||
) : (
|
||
selectedExplorerObjects.map((obj) => (
|
||
<div key={obj.id} className="text-sm py-0.5">
|
||
{obj.objectType.toUpperCase()} - {obj.fullName}
|
||
</div>
|
||
))
|
||
)}
|
||
</div>
|
||
|
||
<div className="mb-4">
|
||
<div className="text-sm font-medium mb-2">
|
||
{translate('::App.Platform.TargetDataSources')}
|
||
</div>
|
||
<label className="flex items-center gap-2 text-sm cursor-pointer mb-2">
|
||
<input
|
||
type="checkbox"
|
||
checked={allTargetsSelected}
|
||
onChange={(e) => handleToggleSelectAllTargets(e.target.checked)}
|
||
/>
|
||
<span>{translate('::App.Platform.SelectAllTargets') || 'Tumunu sec'}</span>
|
||
</label>
|
||
<div className="max-h-44 overflow-auto border-t pt-2">
|
||
{availableTargetDataSourceCodes.map((code) => {
|
||
const checked = copyTargetDataSources.includes(code)
|
||
return (
|
||
<label
|
||
key={code}
|
||
className="flex items-center gap-2 text-sm cursor-pointer"
|
||
>
|
||
<input
|
||
type="checkbox"
|
||
checked={checked}
|
||
onChange={(e) => {
|
||
setCopyTargetDataSources((prev) => {
|
||
if (e.target.checked) return [...prev, code]
|
||
return prev.filter((x) => x !== code)
|
||
})
|
||
}}
|
||
/>
|
||
<span>{code}</span>
|
||
</label>
|
||
)
|
||
})}
|
||
</div>
|
||
</div>
|
||
</>
|
||
) : (
|
||
<>
|
||
{/* SQL Mode */}
|
||
<div className="mb-2">
|
||
<p className="text-sm text-gray-600 dark:text-gray-400 mb-2">
|
||
{translate('::App.Platform.SourceDataSource')}:{' '}
|
||
<strong>{state.selectedDataSource}</strong>
|
||
</p>
|
||
</div>
|
||
|
||
<div className="mb-4 border rounded bg-white dark:bg-gray-800 h-64 overflow-hidden">
|
||
<SqlEditor
|
||
value={sqlScriptForCopy}
|
||
onChange={(value) => setSqlScriptForCopy(value || '')}
|
||
readOnly={isCopyingObjects}
|
||
/>
|
||
</div>
|
||
|
||
<div className="mb-4">
|
||
<div className="text-sm font-medium mb-2">
|
||
{translate('::App.Platform.TargetDataSources')}
|
||
</div>
|
||
<label className="flex items-center gap-2 text-sm cursor-pointer mb-2">
|
||
<input
|
||
type="checkbox"
|
||
checked={allTargetsSelected}
|
||
onChange={(e) => handleToggleSelectAllTargets(e.target.checked)}
|
||
/>
|
||
<span>{translate('::App.Platform.SelectAllTargets') || 'Tumunu sec'}</span>
|
||
</label>
|
||
<div className="max-h-44 overflow-auto border-t pt-2">
|
||
{availableTargetDataSourceCodes.map((code) => {
|
||
const checked = copyTargetDataSources.includes(code)
|
||
return (
|
||
<label
|
||
key={code}
|
||
className="flex items-center gap-2 text-sm cursor-pointer"
|
||
>
|
||
<input
|
||
type="checkbox"
|
||
checked={checked}
|
||
onChange={(e) => {
|
||
setCopyTargetDataSources((prev) => {
|
||
if (e.target.checked) return [...prev, code]
|
||
return prev.filter((x) => x !== code)
|
||
})
|
||
}}
|
||
/>
|
||
<span>{code}</span>
|
||
</label>
|
||
)
|
||
})}
|
||
</div>
|
||
</div>
|
||
</>
|
||
)}
|
||
</div>
|
||
</Dialog.Body>
|
||
|
||
<Dialog.Footer className="flex justify-end gap-2 border-t pt-3 mt-1">
|
||
<Button
|
||
variant="plain"
|
||
onClick={() => setShowCopyDialog(false)}
|
||
disabled={isCopyingObjects}
|
||
>
|
||
{translate('::Cancel')}
|
||
</Button>
|
||
<Button
|
||
variant="solid"
|
||
onClick={copyDialogMode === 'objects' ? handleCopyObjects : handleExecuteDirectSql}
|
||
loading={isCopyingObjects}
|
||
disabled={
|
||
copyTargetDataSources.length === 0 ||
|
||
(copyDialogMode === 'objects'
|
||
? selectedExplorerObjects.length === 0
|
||
: !sqlScriptForCopy?.trim())
|
||
}
|
||
>
|
||
{copyDialogMode === 'objects'
|
||
? translate('::Copy')
|
||
: translate('::App.Platform.Execute') || 'Calistir'}
|
||
</Button>
|
||
</Dialog.Footer>
|
||
</Dialog>
|
||
|
||
<Dialog
|
||
isOpen={showCopyResultDialog}
|
||
onClose={() => setShowCopyResultDialog(false)}
|
||
onRequestClose={() => setShowCopyResultDialog(false)}
|
||
width={1050}
|
||
>
|
||
<Dialog.Body className="flex flex-col gap-2">
|
||
<h5 className="mb-1 flex-shrink-0">
|
||
{translate('::App.Platform.Results') || 'Kopyalama Sonuc Detaylari'}
|
||
</h5>
|
||
|
||
<div className="grid grid-cols-1 sm:grid-cols-3 gap-2 mb-2 text-xs sm:text-sm flex-shrink-0">
|
||
<div className="rounded border border-green-200 bg-green-50 px-3 py-2 text-green-700">
|
||
{translate('::App.Platform.Success')}: <strong>{copySuccessCount}</strong>
|
||
</div>
|
||
<div className="rounded border border-amber-200 bg-amber-50 px-3 py-2 text-amber-700">
|
||
{translate('::App.Platform.Skipped')}: <strong>{copySkippedCount}</strong>
|
||
</div>
|
||
<div className="rounded border border-red-200 bg-red-50 px-3 py-2 text-red-700">
|
||
{translate('::App.Platform.Error')}: <strong>{copyErrorCount}</strong>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="flex-1 min-h-0 overflow-auto border rounded">
|
||
<div className="md:hidden p-2 space-y-2">
|
||
{copyResults.map((row, idx) => {
|
||
const isError = row.status === 'error'
|
||
const isSkipped = row.status === 'skipped'
|
||
const cardClass = isError
|
||
? 'border-red-200 bg-red-50/60 dark:bg-red-900/20'
|
||
: isSkipped
|
||
? 'border-amber-200 bg-amber-50/60 dark:bg-amber-900/20'
|
||
: 'border-gray-200 bg-white dark:bg-gray-800'
|
||
|
||
return (
|
||
<div
|
||
key={`${row.targetDataSource}-${row.objectFullName}-${idx}`}
|
||
className={`rounded border p-3 ${cardClass}`}
|
||
>
|
||
<div className="flex items-center justify-between gap-2 mb-2">
|
||
<span className="text-xs font-semibold uppercase text-gray-500">
|
||
{row.objectType}
|
||
</span>
|
||
{row.status === 'success' && (
|
||
<span className="inline-flex rounded-full bg-green-100 text-green-700 px-2 py-0.5 text-xs font-semibold">
|
||
{translate('::App.Platform.Success')}
|
||
</span>
|
||
)}
|
||
{row.status === 'error' && (
|
||
<span className="inline-flex rounded-full bg-red-100 text-red-700 px-2 py-0.5 text-xs font-semibold">
|
||
{translate('::App.Platform.Error')}
|
||
</span>
|
||
)}
|
||
{row.status === 'skipped' && (
|
||
<span className="inline-flex rounded-full bg-amber-100 text-amber-700 px-2 py-0.5 text-xs font-semibold">
|
||
{translate('::App.Platform.Skipped')}
|
||
</span>
|
||
)}
|
||
</div>
|
||
<div className="text-sm font-medium break-words mb-1">{row.objectFullName}</div>
|
||
<div className="text-xs text-gray-500 mb-2">Hedef: {row.targetDataSource}</div>
|
||
<div
|
||
className={`text-sm whitespace-normal break-words ${isError ? 'text-red-700 dark:text-red-300 font-medium' : 'text-gray-700 dark:text-gray-200'}`}
|
||
>
|
||
{row.message}
|
||
</div>
|
||
</div>
|
||
)
|
||
})}
|
||
{copyResults.length === 0 && (
|
||
<div className="px-3 py-8 text-center text-gray-500">
|
||
{translate('::App.Platform.NoResults')}
|
||
</div>
|
||
)}
|
||
</div>
|
||
|
||
<table className="hidden md:table w-full table-fixed text-sm">
|
||
<thead className="bg-gray-50 dark:bg-gray-700 sticky top-0 z-10">
|
||
<tr>
|
||
<th className="w-[110px] text-left px-3 py-2 border-b">
|
||
{translate('::App.Platform.Status')}
|
||
</th>
|
||
<th className="w-[270px] text-left px-3 py-2 border-b">
|
||
{translate('::App.Platform.Object')}
|
||
</th>
|
||
<th className="w-[90px] text-left px-3 py-2 border-b">
|
||
{translate('::App.Platform.Type')}
|
||
</th>
|
||
<th className="w-[140px] text-left px-3 py-2 border-b">
|
||
{translate('::App.Platform.Target')}
|
||
</th>
|
||
<th className="text-left px-3 py-2 border-b">
|
||
{translate('::App.Platform.Message')}
|
||
</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
{copyResults.map((row, idx) => {
|
||
const isError = row.status === 'error'
|
||
const isSkipped = row.status === 'skipped'
|
||
const rowClass = isError
|
||
? 'bg-red-50/60 dark:bg-red-900/20'
|
||
: isSkipped
|
||
? 'bg-amber-50/60 dark:bg-amber-900/20'
|
||
: idx % 2 === 0
|
||
? 'bg-white dark:bg-gray-800'
|
||
: 'bg-gray-50/40 dark:bg-gray-800/70'
|
||
|
||
return (
|
||
<tr
|
||
key={`${row.targetDataSource}-${row.objectFullName}-${idx}`}
|
||
className={rowClass}
|
||
>
|
||
<td className="px-3 py-2 border-b align-top">
|
||
{row.status === 'success' && (
|
||
<span className="inline-flex rounded-full bg-green-100 text-green-700 px-2 py-0.5 text-xs font-semibold">
|
||
{translate('::App.Platform.Success')}
|
||
</span>
|
||
)}
|
||
{row.status === 'error' && (
|
||
<span className="inline-flex rounded-full bg-red-100 text-red-700 px-2 py-0.5 text-xs font-semibold">
|
||
{translate('::App.Platform.Error')}
|
||
</span>
|
||
)}
|
||
{row.status === 'skipped' && (
|
||
<span className="inline-flex rounded-full bg-amber-100 text-amber-700 px-2 py-0.5 text-xs font-semibold">
|
||
{translate('::App.Platform.Skipped')}
|
||
</span>
|
||
)}
|
||
</td>
|
||
<td
|
||
className="px-3 py-2 border-b align-top truncate"
|
||
title={row.objectFullName}
|
||
>
|
||
{row.objectFullName}
|
||
</td>
|
||
<td className="px-3 py-2 border-b align-top uppercase">{row.objectType}</td>
|
||
<td className="px-3 py-2 border-b align-top">{row.targetDataSource}</td>
|
||
<td
|
||
className={`px-3 py-2 border-b align-top whitespace-normal break-words leading-5 ${isError ? 'text-red-700 dark:text-red-300 font-medium' : 'text-gray-700 dark:text-gray-200'}`}
|
||
title={row.message}
|
||
>
|
||
{row.message}
|
||
</td>
|
||
</tr>
|
||
)
|
||
})}
|
||
{copyResults.length === 0 && (
|
||
<tr>
|
||
<td colSpan={5} className="px-3 py-8 text-center text-gray-500">
|
||
{translate('::App.Platform.NoResults')}
|
||
</td>
|
||
</tr>
|
||
)}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</Dialog.Body>
|
||
|
||
<Dialog.Footer className="flex justify-end gap-2 border-t pt-3 mt-1">
|
||
<Button variant="solid" onClick={() => setShowCopyResultDialog(false)}>
|
||
{translate('::App.Platform.Close')}
|
||
</Button>
|
||
</Dialog.Footer>
|
||
</Dialog>
|
||
</Container>
|
||
)
|
||
}
|
||
|
||
export default SqlQueryManager
|