erp-platform/ui/src/views/sqlQueryManager/SqlQueryManager.tsx

734 lines
24 KiB
TypeScript
Raw Normal View History

2025-12-05 13:45:45 +00:00
import { useState, useCallback, useEffect } from 'react'
import { Button, Dialog, Input, Notification, toast } from '@/components/ui'
import Container from '@/components/shared/Container'
import AdaptableCard from '@/components/shared/AdaptableCard'
import { getDataSources } from '@/services/data-source.service'
import type { DataSourceDto } from '@/proxy/data-source'
import type {
SqlFunctionDto,
SqlQueryDto,
SqlStoredProcedureDto,
SqlViewDto,
SqlObjectType,
SqlQueryExecutionResultDto,
} from '@/proxy/sql-query-manager/models'
import {
sqlObjectManagerService,
2025-12-05 13:45:45 +00:00
} from '@/services/sql-query-manager.service'
import { FaDatabase, FaPlay, FaSave, FaSyncAlt } from 'react-icons/fa'
import { useLocalization } from '@/utils/hooks/useLocalization'
import SqlObjectExplorer from './components/SqlObjectExplorer'
import SqlEditor from './components/SqlEditor'
import SqlResultsGrid from './components/SqlResultsGrid'
import SqlObjectProperties from './components/SqlObjectProperties'
import { FaCloudUploadAlt } from 'react-icons/fa'
export type SqlObject = SqlFunctionDto | SqlQueryDto | SqlStoredProcedureDto | SqlViewDto
interface SqlManagerState {
dataSources: DataSourceDto[]
selectedDataSource: DataSourceDto | null
selectedObject: SqlObject | null
selectedObjectType: SqlObjectType | null
editorContent: string
isExecuting: boolean
executionResult: SqlQueryExecutionResultDto | null
showProperties: boolean
isDirty: boolean
}
const SqlQueryManager = () => {
const { translate } = useLocalization()
const [state, setState] = useState<SqlManagerState>({
dataSources: [],
selectedDataSource: null,
selectedObject: null,
selectedObjectType: null,
editorContent: '',
2025-12-05 13:45:45 +00:00
isExecuting: false,
executionResult: null,
showProperties: false,
isDirty: false,
})
const [showSaveDialog, setShowSaveDialog] = useState(false)
const [saveDialogData, setSaveDialogData] = useState({ name: '', description: '' })
const [showTemplateConfirmDialog, setShowTemplateConfirmDialog] = useState(false)
const [pendingTemplate, setPendingTemplate] = useState<{ content: string; type: string } | null>(null)
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],
}))
}
} 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,
selectedObject: null,
editorContent: '',
executionResult: null,
isDirty: false,
}))
}, [])
const handleObjectSelect = useCallback(
(object: SqlObject | null, objectType: SqlObjectType | null) => {
if (state.isDirty) {
if (!confirm(translate('::App.Platform.UnsavedChangesConfirmation'))) {
return
}
}
let content = ''
if (object) {
if (objectType === 1) {
// Query
content = (object as SqlQueryDto).queryText
} else if (objectType === 2) {
// Stored Procedure
content = (object as SqlStoredProcedureDto).procedureBody
} else if (objectType === 3) {
// View
content = (object as SqlViewDto).viewDefinition
} else if (objectType === 4) {
// Function
content = (object as SqlFunctionDto).functionBody
}
}
setState((prev) => ({
...prev,
selectedObject: object,
selectedObjectType: objectType,
editorContent: content,
executionResult: null,
isDirty: false,
}))
},
[state.isDirty, translate],
)
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`,
2025-12-05 13:48:32 +00:00
'create-scalar-function': `-- Create Scalar Function
CREATE FUNCTION [dbo].[ScalarFunctionName]
2025-12-05 13:45:45 +00:00
(
@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
2025-12-05 13:48:32 +00:00
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
)
2025-12-05 13:45:45 +00:00
GO`
}
return templates[templateType] || templates['select']
}
const applyTemplate = useCallback((templateContent: string) => {
setState((prev) => ({
...prev,
editorContent: templateContent,
selectedObject: null,
selectedObjectType: null,
executionResult: null,
isDirty: false,
}))
toast.push(
<Notification type="success" title={translate('::App.Platform.Success')}>
{translate('::App.Platform.TemplateLoaded')}
</Notification>,
{ placement: 'top-center' },
)
}, [translate])
const handleTemplateSelect = useCallback((template: string, templateType: string) => {
// If template is already provided (e.g., from table click), use it
const templateContent = template || getTemplateContent(templateType)
2025-12-05 13:45:45 +00:00
// 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 handleExecute = async () => {
if (!state.selectedDataSource) {
toast.push(
<Notification type="warning" title={translate('::App.Platform.Warning')}>
{translate('::App.Platform.PleaseSelectDataSource')}
</Notification>,
{ placement: 'top-center' },
)
return
}
if (!state.editorContent.trim()) {
toast.push(
<Notification type="warning" title={translate('::App.Platform.Warning')}>
{translate('::App.Platform.PleaseEnterQuery')}
</Notification>,
{ placement: 'top-center' },
)
return
}
setState((prev) => ({ ...prev, isExecuting: true, executionResult: null }))
try {
const result = await sqlObjectManagerService.executeQuery({
2025-12-05 13:45:45 +00:00
queryText: state.editorContent,
dataSourceCode: state.selectedDataSource.code || '',
})
setState((prev) => ({ ...prev, executionResult: result.data, isExecuting: false }))
toast.push(
<Notification type="success" title={translate('::App.Platform.Success')}>
{translate('::App.Platform.QueryExecutedSuccessfully')} ({result.data.executionTimeMs}ms)
</Notification>,
{ placement: 'top-center' },
)
} catch (error: any) {
setState((prev) => ({ ...prev, isExecuting: false }))
toast.push(
<Notification type="danger" title={translate('::App.Platform.Error')}>
{error.response?.data?.error?.message || translate('::App.Platform.FailedToExecuteQuery')}
</Notification>,
{ placement: 'top-center' },
)
}
}
const handleSave = async () => {
if (!state.selectedDataSource) {
toast.push(
<Notification type="warning" title={translate('::App.Platform.Warning')}>
{translate('::App.Platform.PleaseSelectDataSource')}
</Notification>,
{ placement: 'top-center' },
)
return
}
if (!state.editorContent.trim()) {
toast.push(
<Notification type="warning" title={translate('::App.Platform.Warning')}>
{translate('::App.Platform.PleaseEnterContentToSave')}
</Notification>,
{ placement: 'top-center' },
)
return
}
if (state.selectedObject && state.selectedObjectType) {
// Update existing object
await handleUpdate()
} else {
// Create new object - show dialog to choose type
setSaveDialogData({ name: '', description: '' })
setShowSaveDialog(true)
}
}
const handleUpdate = async () => {
if (!state.selectedObject || !state.selectedObjectType || !state.selectedDataSource) return
if (!state.selectedObject.id) return
try {
const objectId = state.selectedObject.id
switch (state.selectedObjectType) {
case 1: // Query
await sqlObjectManagerService.updateQuery(objectId, {
2025-12-05 13:45:45 +00:00
...(state.selectedObject as SqlQueryDto),
queryText: state.editorContent,
})
break
case 2: // Stored Procedure
await sqlObjectManagerService.updateStoredProcedure(objectId, {
2025-12-05 13:45:45 +00:00
displayName: (state.selectedObject as SqlStoredProcedureDto).displayName,
description: (state.selectedObject as SqlStoredProcedureDto).description,
procedureBody: state.editorContent,
category: (state.selectedObject as SqlStoredProcedureDto).category,
parameters: (state.selectedObject as SqlStoredProcedureDto).parameters,
})
break
case 3: // View
await sqlObjectManagerService.updateView(objectId, {
2025-12-05 13:45:45 +00:00
displayName: (state.selectedObject as SqlViewDto).displayName,
description: (state.selectedObject as SqlViewDto).description,
viewDefinition: state.editorContent,
category: (state.selectedObject as SqlViewDto).category,
withSchemaBinding: (state.selectedObject as SqlViewDto).withSchemaBinding,
})
break
case 4: // Function
await sqlObjectManagerService.updateFunction(objectId, {
2025-12-05 13:45:45 +00:00
displayName: (state.selectedObject as SqlFunctionDto).displayName,
description: (state.selectedObject as SqlFunctionDto).description,
functionBody: state.editorContent,
returnType: (state.selectedObject as SqlFunctionDto).returnType,
category: (state.selectedObject as SqlFunctionDto).category,
parameters: (state.selectedObject as SqlFunctionDto).parameters,
})
break
}
setState((prev) => ({ ...prev, isDirty: false }))
toast.push(
<Notification type="success" title={translate('::App.Platform.Success')}>
{translate('::App.Platform.ObjectUpdatedSuccessfully')}
</Notification>,
{ placement: 'top-center' },
)
} catch (error: any) {
toast.push(
<Notification type="danger" title={translate('::App.Platform.Error')}>
{error.response?.data?.error?.message || translate('::App.Platform.FailedToUpdateObject')}
</Notification>,
{ placement: 'top-center' },
)
}
}
const handleCreateNewQuery = async () => {
if (!state.selectedDataSource || !saveDialogData.name) return
try {
await sqlObjectManagerService.createQuery({
2025-12-05 13:45:45 +00:00
code: saveDialogData.name.replace(/\s+/g, '_'),
name: saveDialogData.name,
description: saveDialogData.description,
queryText: state.editorContent,
dataSourceCode: state.selectedDataSource.code || '',
category: '',
tags: '',
isModifyingData: false,
parameters: '',
})
setState((prev) => ({ ...prev, isDirty: false }))
setShowSaveDialog(false)
toast.push(
<Notification type="success" title={translate('::App.Platform.Success')}>
{translate('::App.Platform.QuerySavedSuccessfully')}
</Notification>,
{ placement: 'top-center' },
)
} catch (error: any) {
toast.push(
<Notification type="danger" title={translate('::App.Platform.Error')}>
{error.response?.data?.error?.message || translate('::App.Platform.FailedToSaveQuery')}
</Notification>,
{ placement: 'top-center' },
)
}
}
const handleDeploy = async () => {
if (!state.selectedObject || !state.selectedObjectType) {
toast.push(
<Notification type="warning" title={translate('::App.Platform.Warning')}>
{translate('::App.Platform.PleaseSelectAnObjectToDeploy')}
</Notification>,
{ placement: 'top-center' },
)
return
}
if (!state.selectedObject.id) return
try {
const objectId = state.selectedObject.id
let result: any
switch (state.selectedObjectType) {
case 2: // Stored Procedure
result = await sqlObjectManagerService.deployStoredProcedure({ id: objectId, dropIfExists: true })
2025-12-05 13:45:45 +00:00
break
case 3: // View
result = await sqlObjectManagerService.deployView({ id: objectId, dropIfExists: true })
2025-12-05 13:45:45 +00:00
break
case 4: // Function
result = await sqlObjectManagerService.deployFunction({ id: objectId, dropIfExists: true })
2025-12-05 13:45:45 +00:00
break
default:
toast.push(
<Notification type="warning" title={translate('::App.Platform.Warning')}>
{translate('::App.Platform.ThisObjectTypeCannotBeDeployed')}
</Notification>,
{ placement: 'top-center' },
)
return
}
toast.push(
<Notification type="success" title={translate('::App.Platform.Success')}>
{translate('::App.Platform.ObjectDeployedSuccessfully')}
</Notification>,
{ placement: 'top-center' },
)
} catch (error: any) {
toast.push(
<Notification type="danger" title={translate('::App.Platform.Error')}>
{error.response?.data?.error?.message || translate('::App.Platform.FailedToDeployObject')}
</Notification>,
{ placement: 'top-center' },
)
}
}
return (
<Container className="h-full overflow-hidden">
<div className="flex flex-col h-full p-1">
2025-12-05 13:45:45 +00:00
{/* Toolbar */}
<AdaptableCard className="flex-shrink-0 shadow-sm mb-4">
2025-12-05 13:45:45 +00:00
<div className="flex items-center justify-between px-1 py-1">
<div className="flex items-center gap-3">
<FaDatabase className="text-lg text-blue-500" />
<span className="text-sm font-medium text-gray-600 dark:text-gray-400">
{translate('::App.Platform.DataSource')}:
</span>
<select
className="border border-gray-300 rounded px-1 py-1"
value={state.selectedDataSource?.code || ''}
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>
<div className="flex items-center gap-3">
<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>
<Button
size="sm"
variant="solid"
icon={<FaSave />}
onClick={handleSave}
disabled={!state.isDirty || !state.selectedDataSource}
className="shadow-sm"
>
{translate('::App.Platform.Save')}
<span className="ml-1 text-xs opacity-75">(Ctrl+S)</span>
</Button>
{state.selectedObject &&
state.selectedObjectType &&
state.selectedObjectType !== 1 && (
<Button
size="sm"
variant="solid"
icon={<FaCloudUploadAlt />}
onClick={handleDeploy}
disabled={!state.selectedDataSource}
className="shadow-sm"
>
{translate('::App.Platform.Deploy')}
</Button>
)}
<Button
size="sm"
variant="twoTone"
icon={<FaSyncAlt />}
onClick={() => window.location.reload()}
className="shadow-sm"
>
{translate('::App.Platform.Refresh')}
</Button>
</div>
</div>
</AdaptableCard>
{/* Main Content Area */}
<div className="flex-1 flex min-h-0">
2025-12-05 13:45:45 +00:00
{/* Left Panel - Object Explorer */}
<div className="w-80 flex-shrink-0 flex flex-col min-h-0 mr-4">
2025-12-05 13:45:45 +00:00
<AdaptableCard className="h-full" bodyClass="p-0">
<div className="h-full flex flex-col">
<div className="border-b px-4 py-3 bg-gray-50 dark:bg-gray-800 flex-shrink-0">
<h6 className="font-semibold text-sm">
{translate('::App.Platform.ObjectExplorer')}
</h6>
</div>
<div className="flex-1 min-h-0 overflow-auto">
2025-12-05 13:45:45 +00:00
<SqlObjectExplorer
dataSource={state.selectedDataSource}
onObjectSelect={handleObjectSelect}
selectedObject={state.selectedObject}
onTemplateSelect={handleTemplateSelect}
/>
</div>
</div>
</AdaptableCard>
</div>
{/* Center Panel - Editor and Results */}
<div className="flex-1 flex flex-col min-h-0 mr-4">
2025-12-05 13:45:45 +00:00
<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
value={state.editorContent}
onChange={handleEditorChange}
onExecute={handleExecute}
onSave={handleSave}
readOnly={state.isExecuting}
/>
</div>
</div>
{state.executionResult && (
<div className="flex-1 mt-4 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.Results')}</h6>
</div>
<div className="flex-1 overflow-hidden min-h-0">
<SqlResultsGrid result={state.executionResult} />
</div>
</div>
)}
</div>
{/* Right Panel - Properties (Optional) */}
{state.showProperties && state.selectedObject && (
<div className="w-80 flex-shrink-0 flex flex-col min-h-0">
<SqlObjectProperties object={state.selectedObject} type={state.selectedObjectType} />
</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}>
{translate('::App.Platform.Replace')}
</Button>
</div>
</Dialog>
{/* Save Dialog */}
<Dialog
isOpen={showSaveDialog}
onClose={() => setShowSaveDialog(false)}
onRequestClose={() => setShowSaveDialog(false)}
>
<h5 className="mb-4">{translate('::App.Platform.SaveAsNewQuery')}</h5>
<div className="space-y-4">
<div>
<label className="block mb-2">{translate('::App.Platform.Name')}</label>
<Input
value={saveDialogData.name}
onChange={(e) => setSaveDialogData((prev) => ({ ...prev, name: e.target.value }))}
/>
</div>
<div>
<label className="block mb-2">{translate('::App.Platform.Description')}</label>
<Input
value={saveDialogData.description}
onChange={(e) =>
setSaveDialogData((prev) => ({ ...prev, description: e.target.value }))
}
/>
</div>
<div className="flex justify-end gap-2">
<Button variant="plain" onClick={() => setShowSaveDialog(false)}>
{translate('::App.Platform.Cancel')}
</Button>
<Button variant="solid" onClick={handleCreateNewQuery}>
{translate('::App.Platform.Save')}
</Button>
</div>
</div>
</Dialog>
</Container>
)
}
export default SqlQueryManager