sozsoft-platform/ui/src/views/developerKit/SqlQueryManager.tsx

1658 lines
60 KiB
TypeScript
Raw Normal View History

2026-02-24 20:44:16 +00:00
import { useState, useCallback, useEffect, useRef } from 'react'
import { Button, Dialog, Notification, toast } from '@/components/ui'
2026-02-24 20:44:16 +00:00
import Container from '@/components/shared/Container'
2026-04-30 10:09:05 +00:00
import ConfirmDialog from '@/components/shared/ConfirmDialog'
2026-02-24 20:44:16 +00:00
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'
2026-02-24 20:44:16 +00:00
import { sqlObjectManagerService } from '@/services/sql-query-manager.service'
2026-05-03 09:52:22 +00:00
import { FaDatabase, FaPlay, FaFileAlt, FaCopy, FaExclamationTriangle } from 'react-icons/fa'
2026-02-24 20:44:16 +00:00
import { FaCheckCircle } from 'react-icons/fa'
import { useLocalization } from '@/utils/hooks/useLocalization'
2026-03-18 09:00:11 +00:00
import SqlObjectExplorer, { type SqlExplorerSelectedObject } from './SqlObjectExplorer'
2026-03-01 20:43:25 +00:00
import SqlEditor, { SqlEditorRef } from './SqlEditor'
import SqlResultsGrid from './SqlResultsGrid'
import SqlTableDesignerDialog from './SqlTableDesignerDialog'
2026-02-24 20:44:16 +00:00
import { Splitter } from '@/components/codeLayout/Splitter'
import { Helmet } from 'react-helmet'
import { useStoreState } from '@/store/store'
import { APP_NAME } from '@/constants/app.constant'
2026-04-30 10:09:05 +00:00
import { UiEvalService } from '@/services/UiEvalService'
import { FcAcceptDatabase } from 'react-icons/fc'
import { FaFolderOpen } from "react-icons/fa"
2026-02-24 20:44:16 +00:00
interface SqlManagerState {
dataSources: DataSourceDto[]
selectedDataSource: string | null
editorContent: string
isExecuting: boolean
executionResult: SqlQueryExecutionResultDto | null
showProperties: boolean
isDirty: boolean
tableColumns: any | null
refreshTrigger: number
}
2026-03-18 09:00:11 +00:00
interface SqlCopyResultItem {
targetDataSource: string
objectFullName: string
objectType: SqlExplorerSelectedObject['objectType']
status: 'success' | 'error' | 'skipped'
message: string
}
2026-02-24 20:44:16 +00:00
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,
)
2026-03-01 20:43:25 +00:00
const [showTableDesignerDialog, setShowTableDesignerDialog] = useState(false)
const [designTableData, setDesignTableData] = useState<{
schemaName: string
tableName: string
} | null>(null)
2026-03-18 09:00:11 +00:00
const [selectedExplorerObjects, setSelectedExplorerObjects] = useState<
SqlExplorerSelectedObject[]
>([])
2026-04-30 10:09:05 +00:00
const [showDbMigrateConfirmDialog, setShowDbMigrateConfirmDialog] = useState(false)
2026-03-18 09:00:11 +00:00
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)
2026-03-18 18:53:51 +00:00
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 }[]>([])
2026-03-01 17:40:25 +00:00
2026-02-24 20:44:16 +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].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,
}))
}, [])
2026-03-01 20:43:25 +00:00
const handleNewTable = useCallback(() => {
setDesignTableData(null)
setShowTableDesignerDialog(true)
}, [])
const handleDesignTable = useCallback((schemaName: string, tableName: string) => {
setDesignTableData({ schemaName, tableName })
setShowTableDesignerDialog(true)
}, [])
2026-02-24 20:44:16 +00:00
const handleEditorChange = useCallback((value: string | undefined) => {
setState((prev) => ({
...prev,
editorContent: value || '',
isDirty: true,
}))
}, [])
2026-03-18 09:00:11 +00:00
const escapeSqlLiteral = (value: string) => value.replace(/'/g, "''")
const escapeSqlIdentifier = (value: string) => value.replace(/]/g, ']]')
const escapePgIdentifier = (value: string) => value.replace(/"/g, '""')
2026-03-18 09:00:11 +00:00
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
2026-03-18 09:00:11 +00:00
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) + ' ' +
2026-03-18 09:00:11 +00:00
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 ' +
2026-03-18 09:00:11 +00:00
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
2026-03-18 09:00:11 +00:00
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) +
2026-03-18 09:00:11 +00:00
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;`
2026-03-18 09:00:11 +00:00
}
const getTableCreateScript = async (schemaName: string, tableName: string): Promise<string> => {
if (!state.selectedDataSource) return ''
const result = await sqlObjectManagerService.getTableCreateScript(
state.selectedDataSource,
schemaName,
tableName,
)
2026-03-18 09:00:11 +00:00
return result.data || ''
2026-03-18 09:00:11 +00:00
}
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 ')
2026-03-18 09:00:11 +00:00
}
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};`
}
2026-03-18 09:00:11 +00:00
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";`
}
2026-03-18 09:00:11 +00:00
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
}
2026-02-24 20:44:16 +00:00
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
}
2026-02-24 20:44:16 +00:00
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,
}))
}, [])
2026-02-24 20:44:16 +00:00
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,
}))
}, [])
2026-02-24 20:44:16 +00:00
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) => {
2026-02-24 20:44:16 +00:00
if (!state.selectedDataSource) return
try {
const result = await sqlObjectManagerService.getNativeObjectDefinition(
state.selectedDataSource,
2026-02-24 20:44:16 +00:00
schemaName,
objectName,
2026-02-24 20:44:16 +00:00
)
if (result.data) {
const definition = normalizeNativeDefinitionToCreate(result.data)
setState((prev) => ({
...prev,
editorContent: definition,
executionResult: null,
tableColumns: null,
isDirty: false,
}))
2026-02-24 20:44:16 +00:00
}
} catch (error: any) {
toast.push(
<Notification type="danger" title={translate('::App.Platform.Error')}>
{error.response?.data?.error?.message ||
translate('::App.Platform.FailedToLoadDefinition')}
2026-02-24 20:44:16 +00:00
</Notification>,
{ placement: 'top-center' },
)
}
}
2026-03-18 09:00:11 +00:00
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
2026-03-18 18:53:51 +00:00
setCopyDialogMode('objects')
2026-03-18 09:00:11 +00:00
setCopyTargetDataSources([])
setOverwriteIfExists(false)
2026-03-18 18:53:51 +00:00
setSqlScriptForCopy('')
2026-03-18 09:00:11 +00:00
setShowCopyDialog(true)
2026-03-18 18:53:51 +00:00
// 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
}
2026-03-18 09:00:11 +00:00
}
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',
2026-03-18 18:53:51 +00:00
message: translate('::App.Platform.CreateScriptFailed'),
2026-03-18 09:00:11 +00:00
})
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',
2026-03-18 18:53:51 +00:00
message: translate('::App.SqlQueryManager.SkippedDescription'),
2026-03-18 09:00:11 +00:00
})
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',
2026-03-18 18:53:51 +00:00
message: translate('::App.Platform.CopyCompleted'),
2026-03-18 09:00:11 +00:00
})
} 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')
}
>
2026-03-18 18:53:51 +00:00
{translate('::App.Platform.CopyCompleted') ||
`translate('::App.Platform.Successful'): ${successCount}, ${translate('::App.Platform.Error')}: ${errorCount}, ${translate('::App.Platform.Skipped')}: ${skippedCount}`}
2026-03-18 09:00:11 +00:00
</Notification>,
{ placement: 'top-center' },
)
setCopyResults(results)
setShowCopyResultDialog(true)
setShowCopyDialog(false)
} finally {
setIsCopyingObjects(false)
}
}
2026-03-18 18:53:51 +00:00
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)
}
}
2026-03-18 09:00:11 +00:00
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)
}
}
2026-02-24 20:44:16 +00:00
return (
<Container className="flex flex-col overflow-hidden" style={{ height: 'calc(100vh - 130px)' }}>
2026-02-24 20:44:16 +00:00
<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">
2026-02-24 20:44:16 +00:00
{/* Toolbar */}
<div className="flex-shrink-0 shadow-sm mb-4">
2026-03-18 05:36:36 +00:00
<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">
2026-02-24 20:44:16 +00:00
<FaDatabase className="text-lg text-blue-500" />
<select
2026-05-19 20:22:25 +00:00
className="border border-gray-300 rounded px-2 py-1 max-w-full dark:bg-gray-700 dark:border-gray-600"
2026-02-24 20:44:16 +00:00
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>
2026-04-30 10:09:05 +00:00
<Button
size="sm"
2026-04-30 10:09:05 +00:00
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>
2026-02-24 20:44:16 +00:00
</div>
2026-03-18 05:36:36 +00:00
<div className="flex flex-wrap items-center gap-2 sm:gap-3">
2026-03-18 09:00:11 +00:00
<Button
size="sm"
2026-03-18 09:00:11 +00:00
variant="default"
2026-04-30 10:09:05 +00:00
icon={<FaCopy />}
2026-03-18 09:00:11 +00:00
onClick={handleOpenCopyDialog}
2026-03-18 18:53:51 +00:00
disabled={!state.selectedDataSource}
2026-05-03 12:24:46 +00:00
className="shadow-sm px-2 py-1"
2026-03-18 18:53:51 +00:00
title={
translate('::App.Platform.CopyOrExecuteSql') ||
'Seçili nesneleri kopyala veya SQL script calistir'
}
2026-03-18 09:00:11 +00:00
>
{translate('::App.Platform.CopySelectedObjects') || 'Copy Selected Objects'}
</Button>
<Button
size="sm"
variant="default"
icon={<FaFileAlt />}
onClick={handleNewQuery}
2026-05-03 12:24:46 +00:00
className="shadow-sm px-2 py-1"
>
{translate('::App.Platform.NewQuery') || 'New Query'}
</Button>
2026-02-24 20:44:16 +00:00
<Button
size="sm"
2026-02-24 20:44:16 +00:00
variant="solid"
color="blue-600"
icon={<FaPlay />}
onClick={handleExecute}
loading={state.isExecuting}
disabled={!state.selectedDataSource}
2026-05-03 12:24:46 +00:00
className="shadow-sm px-2 py-1"
2026-02-24 20:44:16 +00:00
>
{translate('::App.Platform.Execute')}
<span className="ml-1 text-xs opacity-75">(F5)</span>
</Button>
</div>
</div>
</div>
{/* Main Content Area */}
2026-03-18 05:36:36 +00:00
<div className="flex-1 flex min-h-0 flex-col gap-3 lg:flex-row lg:gap-4">
2026-02-24 20:44:16 +00:00
{/* Left Panel - Object Explorer */}
2026-03-18 05:36:36 +00:00
<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}
2026-03-18 09:00:11 +00:00
onGenerateTableScript={handleGenerateTableScript}
refreshTrigger={state.refreshTrigger}
onNewTable={handleNewTable}
onDesignTable={handleDesignTable}
2026-03-18 09:00:11 +00:00
onSelectedObjectsChange={setSelectedExplorerObjects}
/>
</div>
2026-02-24 20:44:16 +00:00
</div>
{/* Center Panel - Editor and Results */}
2026-03-18 05:36:36 +00:00
<div className="flex-1 flex flex-col min-h-0">
2026-02-24 20:44:16 +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}
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>
2026-04-30 10:09:05 +00:00
{/* 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?'}
2026-04-30 10:09:05 +00:00
</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>
2026-02-24 20:44:16 +00:00
{/* 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>
2026-05-03 09:52:22 +00:00
<Button variant="solid" onClick={handleConfirmTemplateReplace} icon={<FaExclamationTriangle />} color="red-600">
2026-02-24 20:44:16 +00:00
{translate('::App.Platform.Replace')}
</Button>
</div>
</Dialog>
2026-03-01 20:43:25 +00:00
{/* Table Designer Dialog */}
<SqlTableDesignerDialog
isOpen={showTableDesignerDialog}
onClose={() => {
setShowTableDesignerDialog(false)
setDesignTableData(null)
}}
dataSource={state.selectedDataSource ?? ''}
initialTableData={designTableData}
onDeployed={() => {
setState((prev) => ({ ...prev, refreshTrigger: prev.refreshTrigger + 1 }))
}}
/>
2026-03-18 09:00:11 +00:00
<Dialog
isOpen={showCopyDialog}
onClose={() => !isCopyingObjects && setShowCopyDialog(false)}
onRequestClose={() => !isCopyingObjects && setShowCopyDialog(false)}
2026-05-15 10:48:07 +00:00
width={1050}
2026-03-18 09:00:11 +00:00
>
2026-05-15 10:48:07 +00:00
<Dialog.Body className="flex flex-col gap-2">
<h5 className="mb-1 flex-shrink-0">{translate('::App.Platform.CopySelectedObjects')}</h5>
2026-03-18 18:53:51 +00:00
{/* Mode Tabs */}
2026-05-15 10:48:07 +00:00
<div className="flex gap-2 mb-2 border-b flex-shrink-0">
2026-03-18 18:53:51 +00:00
<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>
2026-05-15 10:48:07 +00:00
<div className="flex-1 min-h-0 overflow-y-auto pr-1">
2026-03-18 18:53:51 +00:00
{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>
2026-03-18 09:00:11 +00:00
2026-03-18 18:53:51 +00:00
<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>
))
)}
2026-03-18 09:00:11 +00:00
</div>
2026-03-18 18:53:51 +00:00
<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>
</>
)}
2026-03-18 09:00:11 +00:00
</div>
2026-05-15 10:48:07 +00:00
</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>
2026-03-18 09:00:11 +00:00
</Dialog>
<Dialog
isOpen={showCopyResultDialog}
onClose={() => setShowCopyResultDialog(false)}
onRequestClose={() => setShowCopyResultDialog(false)}
width={1050}
>
2026-05-15 10:48:07 +00:00
<Dialog.Body className="flex flex-col gap-2">
<h5 className="mb-1 flex-shrink-0">
2026-03-18 09:00:11 +00:00
{translate('::App.Platform.Results') || 'Kopyalama Sonuc Detaylari'}
</h5>
2026-05-15 10:48:07 +00:00
<div className="grid grid-cols-1 sm:grid-cols-3 gap-2 mb-2 text-xs sm:text-sm flex-shrink-0">
2026-03-18 09:00:11 +00:00
<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>
2026-05-15 10:48:07 +00:00
<div className="flex-1 min-h-0 overflow-auto border rounded">
2026-03-18 09:00:11 +00:00
<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>
2026-03-18 18:53:51 +00:00
<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>
2026-03-18 09:00:11 +00:00
</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>
2026-05-15 10:48:07 +00:00
</Dialog.Body>
2026-03-18 09:00:11 +00:00
2026-05-15 10:48:07 +00:00
<Dialog.Footer className="flex justify-end gap-2 border-t pt-3 mt-1">
2026-03-18 09:00:11 +00:00
<Button variant="solid" onClick={() => setShowCopyResultDialog(false)}>
2026-03-18 18:53:51 +00:00
{translate('::App.Platform.Close')}
2026-03-18 09:00:11 +00:00
</Button>
2026-05-15 10:48:07 +00:00
</Dialog.Footer>
2026-03-18 09:00:11 +00:00
</Dialog>
2026-02-24 20:44:16 +00:00
</Container>
)
}
export default SqlQueryManager