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

591 lines
19 KiB
TypeScript
Raw Normal View History

2026-02-24 20:44:16 +00:00
import { useState, useCallback, useEffect, useRef } from 'react'
import { Button, Dialog, Notification, toast } from '@/components/ui'
2026-02-24 20:44:16 +00:00
import Container from '@/components/shared/Container'
import { getDataSources } from '@/services/data-source.service'
import type { DataSourceDto } from '@/proxy/data-source'
import type { SqlQueryExecutionResultDto } from '@/proxy/sql-query-manager/models'
2026-02-24 20:44:16 +00:00
import { sqlObjectManagerService } from '@/services/sql-query-manager.service'
import { FaDatabase, FaPlay, FaFileAlt } from 'react-icons/fa'
2026-02-24 20:44:16 +00:00
import { FaCheckCircle } from 'react-icons/fa'
import { useLocalization } from '@/utils/hooks/useLocalization'
2026-03-01 20:43:25 +00:00
import SqlObjectExplorer from './SqlObjectExplorer'
import SqlEditor, { SqlEditorRef } from './SqlEditor'
import SqlResultsGrid from './SqlResultsGrid'
import SqlTableDesignerDialog from './SqlTableDesignerDialog'
2026-02-24 20:44:16 +00:00
import { Splitter } from '@/components/codeLayout/Splitter'
import { Helmet } from 'react-helmet'
import { useStoreState } from '@/store/store'
import { APP_NAME } from '@/constants/app.constant'
interface SqlManagerState {
dataSources: DataSourceDto[]
selectedDataSource: string | null
editorContent: string
isExecuting: boolean
executionResult: SqlQueryExecutionResultDto | null
showProperties: boolean
isDirty: boolean
tableColumns: any | null
refreshTrigger: number
}
const SqlQueryManager = () => {
const { translate } = useLocalization()
const editorRef = useRef<SqlEditorRef>(null)
const tenantName = useStoreState((state) => state.locale.currentTenantName)
const [state, setState] = useState<SqlManagerState>({
dataSources: [],
selectedDataSource: tenantName ?? null,
editorContent: '',
isExecuting: false,
refreshTrigger: 0,
executionResult: null,
showProperties: false,
isDirty: false,
tableColumns: null,
})
const [showTemplateConfirmDialog, setShowTemplateConfirmDialog] = useState(false)
const [pendingTemplate, setPendingTemplate] = useState<{ content: string; type: string } | null>(
null,
)
2026-03-01 20:43:25 +00:00
const [showTableDesignerDialog, setShowTableDesignerDialog] = useState(false)
const [designTableData, setDesignTableData] = useState<{
schemaName: string
tableName: string
} | null>(null)
2026-03-01 17:40:25 +00:00
2026-02-24 20:44:16 +00:00
useEffect(() => {
loadDataSources()
}, [])
const loadDataSources = async () => {
try {
const response = await getDataSources()
const items = response.data.items || []
if (items.length > 0) {
setState((prev) => ({
...prev,
dataSources: items,
selectedDataSource: items[0].code ?? null,
}))
}
} catch (error) {
toast.push(
<Notification type="danger" title={translate('::App.Platform.Error')}>
{translate('::App.Platform.FailedtoloadDatasources')}
</Notification>,
{ placement: 'top-center' },
)
}
}
const handleDataSourceChange = useCallback((dataSource: DataSourceDto) => {
setState((prev) => ({
...prev,
selectedDataSource: dataSource.code ?? null,
editorContent: '',
executionResult: null,
isDirty: false,
}))
}, [])
2026-03-01 20:43:25 +00:00
const handleNewTable = useCallback(() => {
setDesignTableData(null)
setShowTableDesignerDialog(true)
}, [])
const handleDesignTable = useCallback((schemaName: string, tableName: string) => {
setDesignTableData({ schemaName, tableName })
setShowTableDesignerDialog(true)
}, [])
2026-02-24 20:44:16 +00:00
const handleEditorChange = useCallback((value: string | undefined) => {
setState((prev) => ({
...prev,
editorContent: value || '',
isDirty: true,
}))
}, [])
const getTemplateContent = (templateType: string): string => {
const templates: Record<string, string> = {
select: `-- Basic SELECT query
SELECT
Column1,
Column2,
Column3
FROM
TableName
WHERE
-- Add your conditions
Column1 = 'value'
AND IsActive = 1
ORDER BY
Column1 ASC;`,
insert: `-- Basic INSERT query
INSERT INTO TableName (Column1, Column2, Column3)
VALUES
('Value1', 'Value2', 'Value3');`,
update: `-- Basic UPDATE query
UPDATE TableName
SET
Column1 = 'NewValue1',
Column2 = 'NewValue2'
WHERE
-- Add your conditions
Id = 1;`,
delete: `-- Basic DELETE query
DELETE FROM TableName
WHERE
-- Add your conditions
Id = 1;`,
'create-procedure': `-- Create Stored Procedure
CREATE PROCEDURE [dbo].[ProcedureName]
@Parameter1 INT,
@Parameter2 NVARCHAR(100)
AS
BEGIN
SET NOCOUNT ON;
-- Add your logic here
SELECT
Column1,
Column2
FROM
TableName
WHERE
Column1 = @Parameter1
AND Column2 = @Parameter2;
END
GO`,
'create-view': `-- Create View
CREATE VIEW [dbo].[ViewName]
AS
SELECT
t1.Column1,
t1.Column2,
t2.Column3
FROM
TableName1 t1
INNER JOIN TableName2 t2 ON t1.Id = t2.TableName1Id
WHERE
t1.IsActive = 1;
GO`,
'create-scalar-function': `-- Create Scalar Function
CREATE FUNCTION [dbo].[ScalarFunctionName]
(
@Parameter1 INT,
@Parameter2 NVARCHAR(100)
)
RETURNS NVARCHAR(200)
AS
BEGIN
DECLARE @Result NVARCHAR(200);
-- Add your logic here
SELECT @Result = Column1 + ' ' + @Parameter2
FROM TableName
WHERE Id = @Parameter1;
RETURN @Result;
END
GO`,
'create-table-function': `-- Create Table-Valued Function
CREATE FUNCTION [dbo].[TableFunctionName]
(
@Parameter1 INT,
@Parameter2 NVARCHAR(100)
)
RETURNS TABLE
AS
RETURN
(
SELECT
t.Column1,
t.Column2,
t.Column3,
t.Column4
FROM
TableName t
WHERE
t.Id = @Parameter1
AND t.Column2 LIKE '%' + @Parameter2 + '%'
AND t.IsActive = 1
)
GO`,
}
return templates[templateType] || templates['select']
}
const applyTemplate = useCallback((templateContent: string) => {
setState((prev) => ({
...prev,
editorContent: templateContent,
executionResult: null,
isDirty: false,
}))
}, [])
2026-02-24 20:44:16 +00:00
const handleUseTemplateFromDialog = useCallback(
(templateContent: string, templateType: string) => {
// Check if editor has content
const hasUserContent = state.editorContent?.trim() && state.isDirty
if (hasUserContent) {
// Ask for confirmation
setPendingTemplate({ content: templateContent, type: templateType })
setShowTemplateConfirmDialog(true)
} else {
// Apply template directly
applyTemplate(templateContent)
}
},
[state.editorContent, state.isDirty, applyTemplate],
)
const handleTemplateSelect = useCallback(
(template: string, templateType: string) => {
// If template is already provided (e.g., from table click), use it
const templateContent = template || getTemplateContent(templateType)
// Check if editor has content and it's not from a previous template
const hasUserContent = state.editorContent?.trim() && state.isDirty
if (hasUserContent) {
// Ask for confirmation
setPendingTemplate({ content: templateContent, type: templateType })
setShowTemplateConfirmDialog(true)
} else {
// Apply template directly
applyTemplate(templateContent)
}
},
[translate, state.editorContent, state.isDirty, applyTemplate],
)
const handleConfirmTemplateReplace = useCallback(() => {
if (pendingTemplate) {
applyTemplate(pendingTemplate.content)
}
setShowTemplateConfirmDialog(false)
setPendingTemplate(null)
}, [pendingTemplate, applyTemplate])
const handleCancelTemplateReplace = useCallback(() => {
setShowTemplateConfirmDialog(false)
setPendingTemplate(null)
}, [])
const handleNewQuery = useCallback(() => {
setState((prev) => ({
...prev,
editorContent: '',
executionResult: null,
tableColumns: null,
isDirty: false,
}))
}, [])
2026-02-24 20:44:16 +00:00
const handleExecute = async () => {
if (!state.selectedDataSource) {
toast.push(
<Notification type="warning" title={translate('::App.Platform.Warning')}>
{translate('::App.Platform.PleaseSelectDataSource')}
</Notification>,
{ placement: 'top-center' },
)
return
}
// Seçili text varsa onu, yoksa tüm editor içeriğini kullan
const selectedText = editorRef.current?.getSelectedText() || ''
const queryToExecute = selectedText.trim() || state.editorContent?.trim() || ''
if (!queryToExecute) {
toast.push(
<Notification type="warning" title={translate('::App.Platform.Warning')}>
{translate('::App.Platform.PleaseEnterQuery')}
</Notification>,
{ placement: 'top-center' },
)
return
}
// Seçili metni koru
const savedSelection = editorRef.current?.preserveSelection()
setState((prev) => ({ ...prev, isExecuting: true, executionResult: null, tableColumns: null }))
try {
const result = await sqlObjectManagerService.executeQuery({
queryText: queryToExecute,
dataSourceCode: state.selectedDataSource || '',
})
setState((prev) => ({
...prev,
executionResult: result.data,
isExecuting: false,
tableColumns: null,
}))
// Seçili metni geri yükle
setTimeout(() => {
if (savedSelection) {
editorRef.current?.restoreSelection(savedSelection)
}
}, 100)
} catch (error: any) {
setState((prev) => ({ ...prev, isExecuting: false }))
// Hata durumunda da seçili metni geri yükle
setTimeout(() => {
if (savedSelection) {
editorRef.current?.restoreSelection(savedSelection)
}
}, 100)
toast.push(
<Notification type="danger" title={translate('::App.Platform.Error')}>
{error.response?.data?.error?.message || translate('::App.Platform.FailedToExecuteQuery')}
</Notification>,
{ placement: 'top-center' },
)
}
}
const handleViewDefinition = async (schemaName: string, objectName: string) => {
2026-02-24 20:44:16 +00:00
if (!state.selectedDataSource) return
try {
const result = await sqlObjectManagerService.getNativeObjectDefinition(
state.selectedDataSource,
2026-02-24 20:44:16 +00:00
schemaName,
objectName,
2026-02-24 20:44:16 +00:00
)
if (result.data) {
const definition = result.data.replace(/\bCREATE\b/i, 'ALTER')
setState((prev) => ({
...prev,
editorContent: definition,
executionResult: null,
tableColumns: null,
isDirty: false,
}))
2026-02-24 20:44:16 +00:00
}
} catch (error: any) {
toast.push(
<Notification type="danger" title={translate('::App.Platform.Error')}>
{error.response?.data?.error?.message ||
translate('::App.Platform.FailedToLoadDefinition')}
2026-02-24 20:44:16 +00:00
</Notification>,
{ placement: 'top-center' },
)
}
}
return (
<Container className="flex flex-col overflow-hidden" style={{ height: 'calc(100vh - 130px)' }}>
2026-02-24 20:44:16 +00:00
<Helmet
titleTemplate={`%s | ${APP_NAME}`}
title={translate('::' + 'App.SqlQueryManager')}
defaultTitle={APP_NAME}
></Helmet>
<div className="flex flex-col flex-1 min-h-0 p-1">
2026-02-24 20:44:16 +00:00
{/* Toolbar */}
<div className="flex-shrink-0 shadow-sm mb-4">
2026-03-18 05:36:36 +00:00
<div className="flex flex-col gap-2 px-1 py-1 lg:flex-row lg:items-center lg:justify-between">
<div className="flex flex-wrap items-center gap-2 sm:gap-3">
2026-02-24 20:44:16 +00:00
<FaDatabase className="text-lg text-blue-500" />
<select
2026-03-18 05:36:36 +00:00
className="border border-gray-300 rounded px-2 py-1 max-w-full"
2026-02-24 20:44:16 +00:00
disabled={state.selectedDataSource?.length === 0}
value={state.selectedDataSource || ''}
onChange={(e) => {
const ds = state.dataSources.find((d) => d.code === e.target.value)
if (ds) handleDataSourceChange(ds)
}}
>
{state.dataSources.map((ds) => (
<option key={ds.code} value={ds.code}>
{ds.code}
</option>
))}
</select>
</div>
2026-03-18 05:36:36 +00:00
<div className="flex flex-wrap items-center gap-2 sm:gap-3">
<Button
size="sm"
variant="default"
icon={<FaFileAlt />}
onClick={handleNewQuery}
className="shadow-sm"
>
{translate('::App.Platform.NewQuery') || 'New Query'}
</Button>
2026-02-24 20:44:16 +00:00
<Button
size="sm"
variant="solid"
color="blue-600"
icon={<FaPlay />}
onClick={handleExecute}
loading={state.isExecuting}
disabled={!state.selectedDataSource}
className="shadow-sm"
>
{translate('::App.Platform.Execute')}
<span className="ml-1 text-xs opacity-75">(F5)</span>
</Button>
</div>
</div>
2026-03-18 05:36:36 +00:00
2026-02-24 20:44:16 +00:00
</div>
{/* Main Content Area */}
2026-03-18 05:36:36 +00:00
<div className="flex-1 flex min-h-0 flex-col gap-3 lg:flex-row lg:gap-4">
2026-02-24 20:44:16 +00:00
{/* Left Panel - Object Explorer */}
2026-03-18 05:36:36 +00:00
<div className="w-full lg:w-1/3 flex-shrink-0 flex flex-col h-[35vh] min-h-[260px] max-h-[420px] lg:h-auto lg:min-h-0 lg:max-h-none bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-600 shadow">
<div className="border-b px-4 py-2 bg-gray-50 dark:bg-gray-800 flex-shrink-0 rounded-t-lg">
<h6 className="font-semibold text-sm">
{translate('::App.Platform.ObjectExplorer')}
</h6>
</div>
<div className="flex-1 min-h-0 flex flex-col overflow-hidden">
<SqlObjectExplorer
dataSource={state.selectedDataSource}
onTemplateSelect={handleTemplateSelect}
onViewDefinition={handleViewDefinition}
refreshTrigger={state.refreshTrigger}
onNewTable={handleNewTable}
onDesignTable={handleDesignTable}
/>
</div>
2026-02-24 20:44:16 +00:00
</div>
{/* Center Panel - Editor and Results */}
2026-03-18 05:36:36 +00:00
<div className="flex-1 flex flex-col min-h-0">
2026-02-24 20:44:16 +00:00
{state.executionResult || state.tableColumns ? (
<Splitter direction="vertical" initialSize={250} minSize={150} maxSize={1200}>
<div className="border rounded-lg shadow-sm bg-white dark:bg-gray-800 flex flex-col h-full">
<div className="border-b px-4 py-2 bg-gray-50 dark:bg-gray-800 flex-shrink-0">
<h6 className="font-semibold text-sm">
{translate('::App.Platform.QueryEditor')}
</h6>
</div>
<div className="flex-1 min-h-0 overflow-hidden">
<SqlEditor
ref={editorRef}
value={state.editorContent}
onChange={handleEditorChange}
onExecute={handleExecute}
readOnly={state.isExecuting}
/>
</div>
</div>
<div className="border rounded-lg shadow-sm bg-white dark:bg-gray-800 flex flex-col h-full">
<div className="border-b px-4 py-2 bg-gray-50 dark:bg-gray-800 flex-shrink-0 flex items-center justify-between">
<div className="flex items-center gap-2">
<FaCheckCircle className="text-green-500" />
<span className="text-sm text-green-700 dark:text-green-400">
{state.executionResult?.message ||
state.tableColumns?.message ||
translate('::App.Platform.QueryExecutedSuccessfully')}
</span>
</div>
<div className="flex items-center gap-4">
<div className="flex items-center gap-4 text-sm text-gray-600 dark:text-gray-400">
<span>
{translate('::App.Platform.Rows')}:{' '}
<strong>
{state.executionResult?.rowsAffected ||
state.executionResult?.data?.length ||
state.tableColumns?.rowsAffected ||
0}
</strong>
</span>
{state.executionResult && (
<span>
{translate('::App.Platform.Time')}:{' '}
<strong>{state.executionResult.executionTimeMs}ms</strong>
</span>
)}
</div>
</div>
</div>
<div className="flex-1 overflow-hidden p-2">
<SqlResultsGrid result={(state.executionResult || state.tableColumns)!} />
</div>
</div>
</Splitter>
) : (
<div className="flex-1 border rounded-lg shadow-sm bg-white dark:bg-gray-800 flex flex-col overflow-hidden">
<div className="border-b px-4 py-2 bg-gray-50 dark:bg-gray-800 flex-shrink-0">
<h6 className="font-semibold text-sm">
{translate('::App.Platform.QueryEditor')}
</h6>
</div>
<div className="flex-1 min-h-0">
<SqlEditor
ref={editorRef}
value={state.editorContent}
onChange={handleEditorChange}
onExecute={handleExecute}
readOnly={state.isExecuting}
/>
</div>
</div>
)}
</div>
</div>
</div>
{/* 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}>
{translate('::App.Platform.Replace')}
</Button>
</div>
</Dialog>
2026-03-01 20:43:25 +00:00
{/* Table Designer Dialog */}
<SqlTableDesignerDialog
isOpen={showTableDesignerDialog}
onClose={() => {
setShowTableDesignerDialog(false)
setDesignTableData(null)
}}
dataSource={state.selectedDataSource ?? ''}
initialTableData={designTableData}
onDeployed={() => {
setState((prev) => ({ ...prev, refreshTrigger: prev.refreshTrigger + 1 }))
}}
/>
2026-02-24 20:44:16 +00:00
</Container>
)
}
export default SqlQueryManager