sozsoft-platform/ui/src/views/developerKit/SqlQueryManager.tsx
2026-08-13 11:41:48 +03:00

1535 lines
54 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 { lazy, Suspense, useState, useCallback, useEffect, 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 { sqlObjectManagerService } from '@/services/sql-query-manager.service'
import {
FaDatabase,
FaPlay,
FaFileAlt,
FaCopy,
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'
const SqlResultsGrid = lazy(() => import('./SqlResultsGrid'))
const SqlTableDesignerDialog = lazy(() => import('./SqlTableDesignerDialog'))
interface SqlManagerState {
dataSources: DataSourceDto[]
selectedDataSource: string | null
editorContent: string
isExecuting: boolean
executionResult: SqlQueryExecutionResultDto | null
showProperties: boolean
isDirty: boolean
tableColumns: any | null
refreshTrigger: number
lastExecutedQuery: string
}
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,
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('')
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 {
toast.push(
<Notification type="danger" title={translate('::App.Platform.Error')}>
{translate('::App.Platform.FailedtoloadDatasources')}
</Notification>,
{ placement: 'bottom-end' },
)
}
}
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)
}
},
[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,
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,
tableColumns: null,
lastExecutedQuery: queryToExecute,
}))
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: '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,
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: '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') || 'Tablo scripti olusturulamadi.'}
</Notification>,
{ placement: 'bottom-end' },
)
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: 'bottom-end' },
)
} 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: 'bottom-end' },
)
}
}
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: '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' 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: '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([])
}
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 || 'Record operation failed.'}
</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.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>
<DbMigrateButton />
{/* Seed dosyalari (DbMigrator/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') ||
'DbMigrator Seeds klasorunu Dosya Yoneticisi icinde yonet'
}
>
{translate('::App.SqlQueryManager.OpenFileManager') || 'Seed Dosyalari'}
</Button>
</Link>
</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"
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">
<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 flex-1 flex-col min-h-0 ${state.executionResult || state.tableColumns ? 'min-h-[620px] lg: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">
<Suspense fallback={<div className="p-4">{translate('::App.Loading')}</div>}>
<SqlResultsGrid
result={(state.executionResult || state.tableColumns)!}
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('::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={() => {
setState((prev) => ({ ...prev, refreshTrigger: prev.refreshTrigger + 1 }))
}}
/>
</Suspense>
)}
<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') || 'Nesneleri Kopyala'}
</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') || '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