sozsoft-platform/ui/src/views/developerKit/SqlQueryManager.tsx
2026-05-25 17:31:54 +03:00

1657 lines
60 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

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

import { useState, useCallback, useEffect, useRef } from 'react'
import { Button, Dialog, 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