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

1581 lines
56 KiB
TypeScript

import { lazy, Suspense, useState, useCallback, useEffect, useMemo, useRef } from 'react'
import { Button, Dialog, Notification, toast } from '@/components/ui'
import Container from '@/components/shared/Container'
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 type { CrudEndpoint } from '@/proxy/developerKit/models'
import { sqlObjectManagerService } from '@/services/sql-query-manager.service'
import { developerKitService } from '@/services/developerKit.service'
import {
FaDatabase,
FaPlay,
FaFileAlt,
FaExclamationTriangle,
FaCheckCircle,
FaFolderOpen,
} from 'react-icons/fa'
import { Link } from 'react-router-dom'
import { ROUTES_ENUM } from '@/routes/route.constant'
import { useLocalization } from '@/utils/hooks/useLocalization'
import SqlObjectExplorer, { type SqlExplorerSelectedObject } from './SqlObjectExplorer'
import SqlEditor, { SqlEditorRef } from './SqlEditor'
import { Splitter } from '@/components/codeLayout/Splitter'
import PageTitle from '@/components/shared/PageTitle'
import { useStoreState } from '@/store/store'
import DbMigrateButton from '@/components/shared/DbMigrateButton'
import CrudEndpointDialog, {
CRUD_ENDPOINT_PERMISSION,
getErrorMessage,
groupEndpointsByEntity,
toEntityName,
} from './CrudEndpointDialog'
import { usePermission } from '@/utils/hooks/usePermission'
const SqlResultsGrid = lazy(() => import('./SqlResultsGrid'))
const SqlTableDesignerDialog = lazy(() => import('./SqlTableDesignerDialog'))
interface SqlManagerState {
dataSources: DataSourceDto[]
selectedDataSource: string | null
editorContent: string
isExecuting: boolean
executionResult: SqlQueryExecutionResultDto | null
isDirty: boolean
refreshTrigger: number
lastExecutedQuery: string
}
interface SqlCopyResultItem {
targetDataSource: string
objectFullName: string
/** Dogrudan SQL modunda tek bir 'script' satiri raporlanir. */
objectType: SqlExplorerSelectedObject['objectType'] | 'script'
status: 'success' | 'error' | 'skipped'
message: string
}
const SqlQueryManager = () => {
const { translate } = useLocalization()
const editorRef = useRef<SqlEditorRef>(null)
const tenantName = useStoreState((state) => state.locale.currentTenantName)
const { checkPermission } = usePermission()
/** CRUD endpoint uretimi/yonetimi yalnizca bu izin verildiginde gorunur. */
const canManageCrudEndpoints = checkPermission(CRUD_ENDPOINT_PERMISSION)
const [state, setState] = useState<SqlManagerState>({
dataSources: [],
selectedDataSource: tenantName ?? null,
editorContent: '',
isExecuting: false,
refreshTrigger: 0,
executionResult: null,
isDirty: false,
lastExecutedQuery: '',
})
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 [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 [crudEndpoints, setCrudEndpoints] = useState<CrudEndpoint[]>([])
const [crudDialogTable, setCrudDialogTable] = useState<{
schemaName: string
tableName: string
} | null>(null)
const [isGeneratingCrud, setIsGeneratingCrud] = useState(false)
useEffect(() => {
loadDataSources()
if (canManageCrudEndpoints) {
loadCrudEndpoints()
}
}, [canManageCrudEndpoints])
const loadDataSources = async () => {
try {
const response = await getDataSources()
const items = response.data.items || []
if (items.length > 0) {
setState((prev) => ({
...prev,
dataSources: items,
// Tenant'in kendi datasource'u varsa onunla baslar, yoksa ilk kayit secilir.
selectedDataSource:
items.find((item) => item.code === prev.selectedDataSource)?.code ??
items[0].code ??
null,
}))
}
} catch {
toast.push(
<Notification type="danger" title={translate('::App.Platform.Error')}>
{translate('::App.Platform.FailedtoloadDatasources')}
</Notification>,
{ placement: 'bottom-end' },
)
}
}
const loadCrudEndpoints = async () => {
try {
const result = await developerKitService.getGeneratedListEndpoints()
setCrudEndpoints(result.items || [])
} catch {
// CRUD endpoint listesi okunamazsa SQL yonetimi calismaya devam eder;
// yalnizca tablo satirlarindaki ikonlar gosterilemez.
}
}
const crudEndpointsByEntity = useMemo(
() => groupEndpointsByEntity(crudEndpoints),
[crudEndpoints],
)
/** Dialog ya da toplu uretim sonrasi ilgili entity'nin endpoint'lerini state'te tazeler. */
const applyEntityEndpoints = useCallback((entityName: string, endpoints: CrudEndpoint[]) => {
setCrudEndpoints((prev) => [...prev.filter((ep) => ep.entityName !== entityName), ...endpoints])
}, [])
const selectedExplorerTables = useMemo(
() => selectedExplorerObjects.filter((obj) => obj.objectType === 'table'),
[selectedExplorerObjects],
)
/** Explorer'da secili tum tablolar icin CRUD endpoint'lerini uretir. */
const handleGenerateCrudEndpoints = async () => {
if (!canManageCrudEndpoints || selectedExplorerTables.length === 0) return
setIsGeneratingCrud(true)
try {
const failures: string[] = []
for (const table of selectedExplorerTables) {
const entityName = toEntityName(table.objectName)
try {
const result = await developerKitService.generateCrudEndpoints(entityName)
applyEntityEndpoints(entityName, result.items || [])
} catch (error) {
failures.push(`${table.fullName}: ${getErrorMessage(error, entityName)}`)
}
}
toast.push(
<Notification
type={failures.length > 0 ? 'warning' : 'success'}
title={
failures.length > 0
? translate('::App.Platform.Warning')
: translate('::App.Platform.Success')
}
>
{failures.length > 0
? failures.join(' | ')
: `${translate('::App.Platform.OperationCompleted')} (${selectedExplorerTables.length})`}
</Notification>,
{ placement: 'bottom-end' },
)
} finally {
setIsGeneratingCrud(false)
}
}
const handleManageCrudEndpoints = useCallback((schemaName: string, tableName: string) => {
setCrudDialogTable({ schemaName, tableName })
}, [])
const handleDataSourceChange = useCallback((dataSource: DataSourceDto) => {
setState((prev) => ({
...prev,
selectedDataSource: dataSource.code ?? null,
editorContent: '',
executionResult: null,
lastExecutedQuery: '',
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 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 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)
}
},
[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,
lastExecutedQuery: '',
isDirty: false,
}))
}, [])
const handleExecute = async () => {
if (!state.selectedDataSource) {
toast.push(
<Notification type="warning" title={translate('::App.Platform.Warning')}>
{translate('::App.Platform.PleaseSelectDataSource')}
</Notification>,
{ placement: 'bottom-end' },
)
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: 'bottom-end' },
)
return
}
// Seçili metni koru
const savedSelection = editorRef.current?.preserveSelection()
setState((prev) => ({
...prev,
isExecuting: true,
executionResult: null,
lastExecutedQuery: queryToExecute,
}))
try {
const result = await sqlObjectManagerService.executeQuery({
queryText: queryToExecute,
dataSourceCode: state.selectedDataSource || '',
})
setState((prev) => ({
...prev,
executionResult: result.data,
isExecuting: false,
}))
// 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: 'bottom-end' },
)
}
}
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,
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: 'bottom-end' },
)
}
}
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')}
</Notification>,
{ placement: 'bottom-end' },
)
return
}
setState((prev) => ({
...prev,
editorContent: script,
executionResult: null,
isDirty: false,
}))
toast.push(
<Notification type="success" title={translate('::App.Platform.Success')}>
{translate('::App.SqlQueryManager.ScriptLoadedToEditor')}
</Notification>,
{ placement: 'bottom-end' },
)
} catch (error: any) {
toast.push(
<Notification type="danger" title={translate('::App.Platform.Error')}>
{error.response?.data?.error?.message ||
translate('::App.SqlQueryManager.TableScriptGenerationFailed')}
</Notification>,
{ placement: 'bottom-end' },
)
}
}
const handleOpenCopyDialog = () => {
if (!state.selectedDataSource) return
setCopyDialogMode('objects')
setCopyTargetDataSources([])
setOverwriteIfExists(false)
setSqlScriptForCopy('')
setShowCopyDialog(true)
}
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 ||
translate('::App.Platform.IslemBasarisiz'),
})
}
}
}
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
toast.push(
<Notification
type={errorCount > 0 ? 'warning' : 'success'}
title={
errorCount > 0
? translate('::App.Platform.Warning')
: translate('::App.Platform.Success')
}
>
{`${translate('::App.Platform.CopyCompleted')}${translate('::App.Platform.Success')}: ${successCount}, ${translate('::App.Platform.Skipped')}: ${skippedCount}, ${translate('::App.Platform.Error')}: ${errorCount}`}
</Notification>,
{ placement: 'bottom-end' },
)
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: 'bottom-end' },
)
return
}
if (copyTargetDataSources.length === 0) {
toast.push(
<Notification type="warning" title={translate('::App.Platform.Warning')}>
{translate('::App.Platform.PleaseSelectAtLeastOneTarget')}
</Notification>,
{ placement: 'bottom-end' },
)
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',
status: 'success',
message: translate('::App.Platform.OperationCompleted'),
})
} catch (error: any) {
results.push({
targetDataSource,
objectFullName: 'SQL Script',
objectType: 'script',
status: 'error',
message:
error.response?.data?.error?.message ||
translate('::App.Platform.IslemBasarisiz'),
})
}
}
const successCount = results.filter((x) => x.status === 'success').length
const errorCount = results.filter((x) => x.status === 'error').length
toast.push(
<Notification
type={errorCount > 0 ? 'warning' : 'success'}
title={
errorCount > 0
? translate('::App.Platform.Warning')
: translate('::App.Platform.Success')
}
>
{`${translate('::App.Platform.ExecutionCompleted')}${translate('::App.Platform.Success')}: ${successCount}, ${translate('::App.Platform.Error')}: ${errorCount}`}
</Notification>,
{ placement: 'bottom-end' },
)
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([])
}
/** Hedef veri kaynagi secici; kopyalama ve dogrudan SQL sekmelerinde ayni. */
const renderTargetDataSourcePicker = () => (
<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')}</span>
</label>
<div className="max-h-44 overflow-auto border-t pt-2">
{availableTargetDataSourceCodes.map((code) => (
<label key={code} className="flex items-center gap-2 text-sm cursor-pointer">
<input
type="checkbox"
checked={copyTargetDataSources.includes(code)}
onChange={(e) =>
setCopyTargetDataSources((prev) =>
e.target.checked ? [...prev, code] : prev.filter((x) => x !== code),
)
}
/>
<span>{code}</span>
</label>
))}
</div>
</div>
)
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 handleResultsMutation = async (mutationQuery: string) => {
if (!state.selectedDataSource || !state.lastExecutedQuery) return
try {
const mutationResult = await sqlObjectManagerService.executeQuery({
queryText: mutationQuery,
dataSourceCode: state.selectedDataSource,
})
if (!mutationResult.data.success) {
throw new Error(mutationResult.data.error || mutationResult.data.message)
}
const refreshedResult = await sqlObjectManagerService.executeQuery({
queryText: state.lastExecutedQuery,
dataSourceCode: state.selectedDataSource,
})
setState((prev) => ({ ...prev, executionResult: refreshedResult.data }))
} catch (error: any) {
toast.push(
<Notification type="danger" title={translate('::App.Platform.Error')}>
{error.response?.data?.error?.message ||
error.message ||
translate('::App.Platform.IslemBasarisiz')}
</Notification>,
{ placement: 'bottom-end' },
)
throw error
}
}
return (
<Container
className="flex flex-col overflow-x-hidden overflow-y-auto lg:overflow-hidden"
style={{ height: 'calc(100vh - 130px)' }}
>
<PageTitle title={translate('::' + 'App.SqlQueryManager')} />
<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.dataSources.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>
<DbMigrateButton />
{/* Seed dosyalari (configs/seeds) File Manager uzerinden yonetilir. */}
<Link to={ROUTES_ENUM.protected.admin.files} target="_blank">
<Button
size="sm"
variant="default"
icon={<FaFolderOpen />}
className="shadow-sm px-2 py-1"
title={translate('::App.SqlQueryManager.OpenFileManagerHint')}
>
{translate('::App.SqlQueryManager.OpenFileManager')}
</Button>
</Link>
</div>
<div className="flex flex-wrap items-center gap-2 sm:gap-3">
{/* Nesne bazli islemler (kopyala, sil, CRUD endpoint) explorer seridinde. */}
<Button
size="sm"
variant="default"
icon={<FaFileAlt />}
onClick={handleNewQuery}
className="shadow-sm px-2 py-1"
>
{translate('::App.Platform.NewQuery')}
</Button>
<Button
size="sm"
variant="solid"
icon={<FaPlay />}
onClick={handleExecute}
loading={state.isExecuting}
disabled={!state.selectedDataSource || !state.editorContent.trim()}
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-[220px] min-h-[220px] max-h-[260px] sm:h-[260px] sm:min-h-[260px] sm:max-h-[300px] 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">
{/* Baslik explorer'in kendi header seridinde; secim islemleri de orada. */}
<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}
crudEndpointsByEntity={crudEndpointsByEntity}
onManageCrudEndpoints={
canManageCrudEndpoints ? handleManageCrudEndpoints : undefined
}
onGenerateCrudEndpoints={
canManageCrudEndpoints ? handleGenerateCrudEndpoints : undefined
}
isGeneratingCrudEndpoints={isGeneratingCrud}
onCopySelectedObjects={handleOpenCopyDialog}
onCrudEndpointsChanged={canManageCrudEndpoints ? loadCrudEndpoints : undefined}
/>
</div>
</div>
{/* Center Panel - Editor and Results */}
<div
className={`flex flex-1 flex-col min-h-0 ${state.executionResult ? 'min-h-[620px] lg:min-h-0' : ''}`}
>
{state.executionResult ? (
<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 ||
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.Row')}:{' '}
<strong>
{state.executionResult?.rowsAffected ||
state.executionResult?.data?.length ||
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">
<Suspense fallback={<div className="p-4">{translate('::App.Platform.LoadingWithThreeDot')}</div>}>
<SqlResultsGrid
result={state.executionResult}
queryText={state.lastExecutedQuery}
dataSourceCode={state.selectedDataSource}
isPostgreSql={isPostgreSql}
onExecuteMutation={handleResultsMutation}
/>
</Suspense>
</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>
{/* Template Confirmation Dialog */}
<Dialog
isOpen={showTemplateConfirmDialog}
onClose={handleCancelTemplateReplace}
onRequestClose={handleCancelTemplateReplace}
>
<h5 className="mb-4">{translate('::App.Platform.ConfirmTemplateReplace')}</h5>
<p className="mb-6 text-gray-600 dark:text-gray-400">
{translate('::App.Platform.TemplateReplaceWarning')}
</p>
<div className="flex justify-end gap-2">
<Button variant="plain" onClick={handleCancelTemplateReplace}>
{translate('::App.Platform.Cancel')}
</Button>
<Button
variant="solid"
onClick={handleConfirmTemplateReplace}
icon={<FaExclamationTriangle />}
>
{translate('::App.Platform.Replace')}
</Button>
</div>
</Dialog>
{/* Table Designer Dialog */}
{showTableDesignerDialog && (
<Suspense fallback={null}>
<SqlTableDesignerDialog
isOpen
onClose={() => {
setShowTableDesignerDialog(false)
setDesignTableData(null)
}}
dataSource={state.selectedDataSource ?? ''}
initialTableData={designTableData}
onDeployed={async () => {
setState((prev) => ({ ...prev, refreshTrigger: prev.refreshTrigger + 1 }))
// Tasarimci deploy sonrasi CRUD endpoint uretmis olabilir; explorer
// rozetlerinin gorunmesi icin liste tazelenir.
if (canManageCrudEndpoints) {
await loadCrudEndpoints()
}
}}
/>
</Suspense>
)}
{/* CRUD Endpoint Dialog */}
{crudDialogTable && canManageCrudEndpoints && (
<CrudEndpointDialog
isOpen
schemaName={crudDialogTable.schemaName}
tableName={crudDialogTable.tableName}
dataSourceCode={state.selectedDataSource}
onClose={() => setCrudDialogTable(null)}
onChanged={applyEntityEndpoints}
/>
)}
<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')}
variant="plain"
shape="none"
className={`!h-auto !rounded-none border-b-2 !px-4 !py-2 text-sm font-medium transition hover:!bg-transparent active:!bg-transparent focus:!bg-transparent ${
copyDialogMode === 'objects'
? 'border-blue-500 !text-blue-600'
: 'border-transparent text-gray-600 hover:text-gray-900'
}`}
disabled={isCopyingObjects}
>
{translate('::App.Platform.CopyObjects')}
</Button>
<Button
onClick={() => setCopyDialogMode('sql')}
variant="plain"
shape="none"
className={`!h-auto !rounded-none border-b-2 !px-4 !py-2 text-sm font-medium transition hover:!bg-transparent active:!bg-transparent focus:!bg-transparent ${
copyDialogMode === 'sql'
? 'border-blue-500 !text-blue-600'
: 'border-transparent text-gray-600 hover:text-gray-900'
}`}
disabled={isCopyingObjects}
>
{translate('::App.Platform.DirectSqlScript')}
</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')}
</p>
) : (
selectedExplorerObjects.map((obj) => (
<div key={obj.id} className="text-sm py-0.5">
{obj.objectType.toUpperCase()} - {obj.fullName}
</div>
))
)}
</div>
{renderTargetDataSourcePicker()}
</>
) : (
<>
{/* 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>
{renderTargetDataSourcePicker()}
</>
)}
</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('::App.Platform.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('::App.Platform.Copy')
: translate('::App.Platform.Execute')}
</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')}</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">
{translate('::App.Listform.ListformField.Target')}: {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.Listform.ListformField.Status')}
</th>
<th className="w-[270px] text-left px-3 py-2 border-b">
{translate('::App.StaticLookup.Object')}
</th>
<th className="w-[90px] text-left px-3 py-2 border-b">
{translate('::App.Platform.PlatformType')}
</th>
<th className="w-[140px] text-left px-3 py-2 border-b">
{translate('::App.Listform.ListformField.Target')}
</th>
<th className="text-left px-3 py-2 border-b">
{translate('::App.Listform.ListformField.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