Sql Query Manager yapılandırıldı.
This commit is contained in:
parent
932ee406b1
commit
81abe549cf
13 changed files with 1152 additions and 365 deletions
|
|
@ -41,4 +41,7 @@ public interface ISqlObjectManagerAppService : IApplicationService
|
||||||
|
|
||||||
// Database Metadata Operations
|
// Database Metadata Operations
|
||||||
Task<List<DatabaseColumnDto>> GetTableColumnsAsync(string dataSourceCode, string schemaName, string tableName);
|
Task<List<DatabaseColumnDto>> GetTableColumnsAsync(string dataSourceCode, string schemaName, string tableName);
|
||||||
|
|
||||||
|
// Smart Save - Analyzes SQL and saves to appropriate table with auto-deploy
|
||||||
|
Task<SmartSaveResultDto> SmartSaveAsync(SmartSaveInputDto input);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,25 @@
|
||||||
|
using System;
|
||||||
|
|
||||||
|
namespace Erp.SqlQueryManager.Application.Contracts;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Input for smart save operation
|
||||||
|
/// </summary>
|
||||||
|
public class SmartSaveInputDto
|
||||||
|
{
|
||||||
|
public string SqlText { get; set; }
|
||||||
|
public string DataSourceCode { get; set; }
|
||||||
|
public string Name { get; set; }
|
||||||
|
public string Description { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Result of smart save operation
|
||||||
|
/// </summary>
|
||||||
|
public class SmartSaveResultDto
|
||||||
|
{
|
||||||
|
public string ObjectType { get; set; }
|
||||||
|
public Guid ObjectId { get; set; }
|
||||||
|
public bool Deployed { get; set; }
|
||||||
|
public string Message { get; set; }
|
||||||
|
}
|
||||||
|
|
@ -6,7 +6,7 @@ using Erp.SqlQueryManager.Application.Contracts;
|
||||||
using Erp.SqlQueryManager.Domain.Entities;
|
using Erp.SqlQueryManager.Domain.Entities;
|
||||||
using Erp.SqlQueryManager.Domain.Services;
|
using Erp.SqlQueryManager.Domain.Services;
|
||||||
using Erp.SqlQueryManager.Domain.Shared;
|
using Erp.SqlQueryManager.Domain.Shared;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
using Volo.Abp.Application.Services;
|
using Volo.Abp.Application.Services;
|
||||||
using Volo.Abp.Domain.Repositories;
|
using Volo.Abp.Domain.Repositories;
|
||||||
|
|
||||||
|
|
@ -16,6 +16,7 @@ namespace Erp.SqlQueryManager.Application;
|
||||||
/// Unified service for SQL Object Explorer
|
/// Unified service for SQL Object Explorer
|
||||||
/// Combines all SQL objects into a single endpoint
|
/// Combines all SQL objects into a single endpoint
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
[Authorize("App.SqlQueryManager")]
|
||||||
public class SqlObjectManagerAppService : ApplicationService, ISqlObjectManagerAppService
|
public class SqlObjectManagerAppService : ApplicationService, ISqlObjectManagerAppService
|
||||||
{
|
{
|
||||||
private readonly IRepository<SqlQuery, Guid> _queryRepository;
|
private readonly IRepository<SqlQuery, Guid> _queryRepository;
|
||||||
|
|
@ -213,6 +214,50 @@ public class SqlObjectManagerAppService : ApplicationService, ISqlObjectManagerA
|
||||||
|
|
||||||
public async Task<SqlQueryExecutionResultDto> ExecuteQueryAsync(ExecuteSqlQueryDto input)
|
public async Task<SqlQueryExecutionResultDto> ExecuteQueryAsync(ExecuteSqlQueryDto input)
|
||||||
{
|
{
|
||||||
|
var sqlText = input.QueryText.Trim();
|
||||||
|
var sqlUpper = sqlText.ToUpperInvariant();
|
||||||
|
|
||||||
|
// Check if this is a DDL command (CREATE/ALTER/DROP for VIEW/PROCEDURE/FUNCTION)
|
||||||
|
bool isDDLCommand =
|
||||||
|
sqlUpper.Contains("CREATE VIEW") || sqlUpper.Contains("ALTER VIEW") ||
|
||||||
|
sqlUpper.Contains("CREATE PROCEDURE") || sqlUpper.Contains("CREATE PROC") ||
|
||||||
|
sqlUpper.Contains("ALTER PROCEDURE") || sqlUpper.Contains("ALTER PROC") ||
|
||||||
|
sqlUpper.Contains("CREATE FUNCTION") || sqlUpper.Contains("ALTER FUNCTION") ||
|
||||||
|
sqlUpper.Contains("DROP VIEW") || sqlUpper.Contains("DROP PROCEDURE") ||
|
||||||
|
sqlUpper.Contains("DROP PROC") || sqlUpper.Contains("DROP FUNCTION");
|
||||||
|
|
||||||
|
if (isDDLCommand)
|
||||||
|
{
|
||||||
|
// For DDL commands, only validate syntax without executing
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// Try to parse/validate the SQL using SET PARSEONLY
|
||||||
|
var validationSql = $"SET PARSEONLY ON;\n{sqlText}\nSET PARSEONLY OFF;";
|
||||||
|
await _sqlExecutorService.ExecuteNonQueryAsync(validationSql, input.DataSourceCode);
|
||||||
|
|
||||||
|
return new SqlQueryExecutionResultDto
|
||||||
|
{
|
||||||
|
Success = true,
|
||||||
|
Message = "SQL syntax is valid. Use Save button to save and Deploy button to create in SQL Server.",
|
||||||
|
Data = new List<object>(),
|
||||||
|
RowsAffected = 0,
|
||||||
|
ExecutionTimeMs = 0
|
||||||
|
};
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
return new SqlQueryExecutionResultDto
|
||||||
|
{
|
||||||
|
Success = false,
|
||||||
|
Message = $"SQL syntax error: {ex.Message}",
|
||||||
|
Data = new List<object>(),
|
||||||
|
RowsAffected = 0,
|
||||||
|
ExecutionTimeMs = 0
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// For DML commands (SELECT, INSERT, UPDATE, DELETE), execute normally
|
||||||
var result = await _sqlExecutorService.ExecuteQueryAsync(input.QueryText, input.DataSourceCode);
|
var result = await _sqlExecutorService.ExecuteQueryAsync(input.QueryText, input.DataSourceCode);
|
||||||
return MapExecutionResult(result);
|
return MapExecutionResult(result);
|
||||||
}
|
}
|
||||||
|
|
@ -242,6 +287,8 @@ public class SqlObjectManagerAppService : ApplicationService, ISqlObjectManagerA
|
||||||
procedure.Description = input.Description;
|
procedure.Description = input.Description;
|
||||||
procedure.ProcedureBody = input.ProcedureBody;
|
procedure.ProcedureBody = input.ProcedureBody;
|
||||||
procedure.Category = input.Category;
|
procedure.Category = input.Category;
|
||||||
|
procedure.IsDeployed = false;
|
||||||
|
procedure.LastDeployedAt = null;
|
||||||
|
|
||||||
var updated = await _procedureRepository.UpdateAsync(procedure, autoSave: true);
|
var updated = await _procedureRepository.UpdateAsync(procedure, autoSave: true);
|
||||||
return ObjectMapper.Map<SqlStoredProcedure, SqlStoredProcedureDto>(updated);
|
return ObjectMapper.Map<SqlStoredProcedure, SqlStoredProcedureDto>(updated);
|
||||||
|
|
@ -249,25 +296,58 @@ public class SqlObjectManagerAppService : ApplicationService, ISqlObjectManagerA
|
||||||
|
|
||||||
public async Task DeleteStoredProcedureAsync(Guid id)
|
public async Task DeleteStoredProcedureAsync(Guid id)
|
||||||
{
|
{
|
||||||
|
var procedure = await _procedureRepository.GetAsync(id);
|
||||||
|
|
||||||
|
// Drop stored procedure from SQL Server (always try, regardless of IsDeployed flag)
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var dropSql = $"IF OBJECT_ID('[{procedure.SchemaName}].[{procedure.ProcedureName}]', 'P') IS NOT NULL DROP PROCEDURE [{procedure.SchemaName}].[{procedure.ProcedureName}]";
|
||||||
|
await _sqlExecutorService.ExecuteNonQueryAsync(dropSql, procedure.DataSourceCode);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// Ignore errors if object doesn't exist in database
|
||||||
|
}
|
||||||
|
|
||||||
await _procedureRepository.DeleteAsync(id);
|
await _procedureRepository.DeleteAsync(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<SqlQueryExecutionResultDto> DeployStoredProcedureAsync(DeployStoredProcedureDto input)
|
public async Task<SqlQueryExecutionResultDto> DeployStoredProcedureAsync(DeployStoredProcedureDto input)
|
||||||
{
|
{
|
||||||
var procedure = await _procedureRepository.GetAsync(input.Id);
|
var procedure = await _procedureRepository.GetAsync(input.Id);
|
||||||
var result = await _sqlExecutorService.DeployStoredProcedureAsync(
|
|
||||||
procedure.ProcedureBody,
|
|
||||||
procedure.DataSourceCode
|
|
||||||
);
|
|
||||||
|
|
||||||
if (result.Success)
|
try
|
||||||
{
|
{
|
||||||
procedure.IsDeployed = true;
|
// Önce DROP işlemi yap (varsa)
|
||||||
procedure.LastDeployedAt = DateTime.UtcNow;
|
var dropSql = $"IF OBJECT_ID('[{procedure.SchemaName}].[{procedure.ProcedureName}]', 'P') IS NOT NULL DROP PROCEDURE [{procedure.SchemaName}].[{procedure.ProcedureName}]";
|
||||||
await _procedureRepository.UpdateAsync(procedure, autoSave: true);
|
await _sqlExecutorService.ExecuteNonQueryAsync(dropSql, procedure.DataSourceCode);
|
||||||
}
|
|
||||||
|
|
||||||
return MapExecutionResult(result);
|
// Sonra CREATE işlemi yap
|
||||||
|
var result = await _sqlExecutorService.DeployStoredProcedureAsync(
|
||||||
|
procedure.ProcedureBody,
|
||||||
|
procedure.DataSourceCode
|
||||||
|
);
|
||||||
|
|
||||||
|
if (result.Success)
|
||||||
|
{
|
||||||
|
procedure.IsDeployed = true;
|
||||||
|
procedure.LastDeployedAt = DateTime.UtcNow;
|
||||||
|
await _procedureRepository.UpdateAsync(procedure, autoSave: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
return MapExecutionResult(result);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
return new SqlQueryExecutionResultDto
|
||||||
|
{
|
||||||
|
Success = false,
|
||||||
|
Message = $"Deploy failed: {ex.Message}",
|
||||||
|
Data = new List<object>(),
|
||||||
|
RowsAffected = 0,
|
||||||
|
ExecutionTimeMs = 0
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
@ -282,6 +362,8 @@ public class SqlObjectManagerAppService : ApplicationService, ISqlObjectManagerA
|
||||||
view.Description = input.Description;
|
view.Description = input.Description;
|
||||||
view.ViewDefinition = input.ViewDefinition;
|
view.ViewDefinition = input.ViewDefinition;
|
||||||
view.Category = input.Category;
|
view.Category = input.Category;
|
||||||
|
view.IsDeployed = false;
|
||||||
|
view.LastDeployedAt = null;
|
||||||
|
|
||||||
var updated = await _viewRepository.UpdateAsync(view, autoSave: true);
|
var updated = await _viewRepository.UpdateAsync(view, autoSave: true);
|
||||||
return ObjectMapper.Map<SqlView, SqlViewDto>(updated);
|
return ObjectMapper.Map<SqlView, SqlViewDto>(updated);
|
||||||
|
|
@ -289,25 +371,58 @@ public class SqlObjectManagerAppService : ApplicationService, ISqlObjectManagerA
|
||||||
|
|
||||||
public async Task DeleteViewAsync(Guid id)
|
public async Task DeleteViewAsync(Guid id)
|
||||||
{
|
{
|
||||||
|
var view = await _viewRepository.GetAsync(id);
|
||||||
|
|
||||||
|
// Drop view from SQL Server (always try, regardless of IsDeployed flag)
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var dropSql = $"IF OBJECT_ID('[{view.SchemaName}].[{view.ViewName}]', 'V') IS NOT NULL DROP VIEW [{view.SchemaName}].[{view.ViewName}]";
|
||||||
|
await _sqlExecutorService.ExecuteNonQueryAsync(dropSql, view.DataSourceCode);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// Ignore errors if object doesn't exist in database
|
||||||
|
}
|
||||||
|
|
||||||
await _viewRepository.DeleteAsync(id);
|
await _viewRepository.DeleteAsync(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<SqlQueryExecutionResultDto> DeployViewAsync(DeployViewDto input)
|
public async Task<SqlQueryExecutionResultDto> DeployViewAsync(DeployViewDto input)
|
||||||
{
|
{
|
||||||
var view = await _viewRepository.GetAsync(input.Id);
|
var view = await _viewRepository.GetAsync(input.Id);
|
||||||
var result = await _sqlExecutorService.DeployViewAsync(
|
|
||||||
view.ViewDefinition,
|
|
||||||
view.DataSourceCode
|
|
||||||
);
|
|
||||||
|
|
||||||
if (result.Success)
|
try
|
||||||
{
|
{
|
||||||
view.IsDeployed = true;
|
// Önce DROP işlemi yap (varsa)
|
||||||
view.LastDeployedAt = DateTime.UtcNow;
|
var dropSql = $"IF OBJECT_ID('[{view.SchemaName}].[{view.ViewName}]', 'V') IS NOT NULL DROP VIEW [{view.SchemaName}].[{view.ViewName}]";
|
||||||
await _viewRepository.UpdateAsync(view, autoSave: true);
|
await _sqlExecutorService.ExecuteNonQueryAsync(dropSql, view.DataSourceCode);
|
||||||
}
|
|
||||||
|
|
||||||
return MapExecutionResult(result);
|
// Sonra CREATE işlemi yap
|
||||||
|
var result = await _sqlExecutorService.DeployViewAsync(
|
||||||
|
view.ViewDefinition,
|
||||||
|
view.DataSourceCode
|
||||||
|
);
|
||||||
|
|
||||||
|
if (result.Success)
|
||||||
|
{
|
||||||
|
view.IsDeployed = true;
|
||||||
|
view.LastDeployedAt = DateTime.UtcNow;
|
||||||
|
await _viewRepository.UpdateAsync(view, autoSave: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
return MapExecutionResult(result);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
return new SqlQueryExecutionResultDto
|
||||||
|
{
|
||||||
|
Success = false,
|
||||||
|
Message = $"Deploy failed: {ex.Message}",
|
||||||
|
Data = new List<object>(),
|
||||||
|
RowsAffected = 0,
|
||||||
|
ExecutionTimeMs = 0
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
@ -322,6 +437,8 @@ public class SqlObjectManagerAppService : ApplicationService, ISqlObjectManagerA
|
||||||
function.Description = input.Description;
|
function.Description = input.Description;
|
||||||
function.FunctionBody = input.FunctionBody;
|
function.FunctionBody = input.FunctionBody;
|
||||||
function.Category = input.Category;
|
function.Category = input.Category;
|
||||||
|
function.IsDeployed = false;
|
||||||
|
function.LastDeployedAt = null;
|
||||||
|
|
||||||
var updated = await _functionRepository.UpdateAsync(function, autoSave: true);
|
var updated = await _functionRepository.UpdateAsync(function, autoSave: true);
|
||||||
return ObjectMapper.Map<SqlFunction, SqlFunctionDto>(updated);
|
return ObjectMapper.Map<SqlFunction, SqlFunctionDto>(updated);
|
||||||
|
|
@ -329,25 +446,58 @@ public class SqlObjectManagerAppService : ApplicationService, ISqlObjectManagerA
|
||||||
|
|
||||||
public async Task DeleteFunctionAsync(Guid id)
|
public async Task DeleteFunctionAsync(Guid id)
|
||||||
{
|
{
|
||||||
|
var function = await _functionRepository.GetAsync(id);
|
||||||
|
|
||||||
|
// Drop function from SQL Server (always try, regardless of IsDeployed flag)
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var dropSql = $"IF OBJECT_ID('[{function.SchemaName}].[{function.FunctionName}]', 'FN') IS NOT NULL DROP FUNCTION [{function.SchemaName}].[{function.FunctionName}]";
|
||||||
|
await _sqlExecutorService.ExecuteNonQueryAsync(dropSql, function.DataSourceCode);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// Ignore errors if object doesn't exist in database
|
||||||
|
}
|
||||||
|
|
||||||
await _functionRepository.DeleteAsync(id);
|
await _functionRepository.DeleteAsync(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<SqlQueryExecutionResultDto> DeployFunctionAsync(DeployFunctionDto input)
|
public async Task<SqlQueryExecutionResultDto> DeployFunctionAsync(DeployFunctionDto input)
|
||||||
{
|
{
|
||||||
var function = await _functionRepository.GetAsync(input.Id);
|
var function = await _functionRepository.GetAsync(input.Id);
|
||||||
var result = await _sqlExecutorService.DeployFunctionAsync(
|
|
||||||
function.FunctionBody,
|
|
||||||
function.DataSourceCode
|
|
||||||
);
|
|
||||||
|
|
||||||
if (result.Success)
|
try
|
||||||
{
|
{
|
||||||
function.IsDeployed = true;
|
// Önce DROP işlemi yap (varsa)
|
||||||
function.LastDeployedAt = DateTime.UtcNow;
|
var dropSql = $"IF OBJECT_ID('[{function.SchemaName}].[{function.FunctionName}]', 'FN') IS NOT NULL DROP FUNCTION [{function.SchemaName}].[{function.FunctionName}]";
|
||||||
await _functionRepository.UpdateAsync(function, autoSave: true);
|
await _sqlExecutorService.ExecuteNonQueryAsync(dropSql, function.DataSourceCode);
|
||||||
}
|
|
||||||
|
|
||||||
return MapExecutionResult(result);
|
// Sonra CREATE işlemi yap
|
||||||
|
var result = await _sqlExecutorService.DeployFunctionAsync(
|
||||||
|
function.FunctionBody,
|
||||||
|
function.DataSourceCode
|
||||||
|
);
|
||||||
|
|
||||||
|
if (result.Success)
|
||||||
|
{
|
||||||
|
function.IsDeployed = true;
|
||||||
|
function.LastDeployedAt = DateTime.UtcNow;
|
||||||
|
await _functionRepository.UpdateAsync(function, autoSave: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
return MapExecutionResult(result);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
return new SqlQueryExecutionResultDto
|
||||||
|
{
|
||||||
|
Success = false,
|
||||||
|
Message = $"Deploy failed: {ex.Message}",
|
||||||
|
Data = new List<object>(),
|
||||||
|
RowsAffected = 0,
|
||||||
|
ExecutionTimeMs = 0
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
@ -398,6 +548,264 @@ public class SqlObjectManagerAppService : ApplicationService, ISqlObjectManagerA
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
|
public async Task<SmartSaveResultDto> SmartSaveAsync(SmartSaveInputDto input)
|
||||||
|
{
|
||||||
|
var result = new SmartSaveResultDto();
|
||||||
|
var sqlText = input.SqlText.Trim();
|
||||||
|
var sqlUpper = sqlText.ToUpperInvariant();
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// Analyze SQL to determine object type
|
||||||
|
if (sqlUpper.Contains("CREATE VIEW") || sqlUpper.Contains("ALTER VIEW"))
|
||||||
|
{
|
||||||
|
// Extract view name
|
||||||
|
var viewName = input.Name;
|
||||||
|
var schemaName = "dbo";
|
||||||
|
var displayName = input.Name;
|
||||||
|
|
||||||
|
// Check if view already exists
|
||||||
|
var existingView = (await _viewRepository.GetListAsync())
|
||||||
|
.FirstOrDefault(v => v.ViewName == viewName && v.DataSourceCode == input.DataSourceCode);
|
||||||
|
|
||||||
|
SqlView view;
|
||||||
|
if (existingView != null)
|
||||||
|
{
|
||||||
|
// Update existing view
|
||||||
|
existingView.DisplayName = displayName;
|
||||||
|
existingView.ViewDefinition = sqlText;
|
||||||
|
existingView.IsDeployed = false;
|
||||||
|
existingView.LastDeployedAt = null;
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(input.Description))
|
||||||
|
{
|
||||||
|
existingView.Description = input.Description;
|
||||||
|
}
|
||||||
|
view = await _viewRepository.UpdateAsync(existingView, autoSave: true);
|
||||||
|
result.Message = $"View '{viewName}' updated successfully. Use Deploy button to deploy changes to SQL Server.";
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Create new view
|
||||||
|
view = new SqlView(
|
||||||
|
GuidGenerator.Create(),
|
||||||
|
viewName, // ViewName from SQL
|
||||||
|
schemaName,
|
||||||
|
displayName, // DisplayName from user input
|
||||||
|
sqlText,
|
||||||
|
input.DataSourceCode
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(input.Description))
|
||||||
|
{
|
||||||
|
view.Description = input.Description;
|
||||||
|
}
|
||||||
|
|
||||||
|
await _viewRepository.InsertAsync(view, autoSave: true);
|
||||||
|
result.Message = $"View '{viewName}' saved successfully. Use Deploy button to deploy to SQL Server.";
|
||||||
|
}
|
||||||
|
|
||||||
|
result.ObjectType = "View";
|
||||||
|
result.ObjectId = view.Id;
|
||||||
|
result.Deployed = view.IsDeployed;
|
||||||
|
}
|
||||||
|
else if (sqlUpper.Contains("CREATE PROCEDURE") || sqlUpper.Contains("CREATE PROC") ||
|
||||||
|
sqlUpper.Contains("ALTER PROCEDURE") || sqlUpper.Contains("ALTER PROC"))
|
||||||
|
{
|
||||||
|
// Extract procedure name
|
||||||
|
var procName = input.Name;
|
||||||
|
var schemaName = "dbo";
|
||||||
|
var displayName = input.Name;
|
||||||
|
|
||||||
|
// Check if procedure already exists
|
||||||
|
var existingProcedure = (await _procedureRepository.GetListAsync())
|
||||||
|
.FirstOrDefault(p => p.ProcedureName == procName && p.DataSourceCode == input.DataSourceCode);
|
||||||
|
|
||||||
|
SqlStoredProcedure procedure;
|
||||||
|
if (existingProcedure != null)
|
||||||
|
{
|
||||||
|
// Update existing procedure
|
||||||
|
existingProcedure.DisplayName = displayName;
|
||||||
|
existingProcedure.ProcedureBody = sqlText;
|
||||||
|
existingProcedure.IsDeployed = false;
|
||||||
|
existingProcedure.LastDeployedAt = null;
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(input.Description))
|
||||||
|
{
|
||||||
|
existingProcedure.Description = input.Description;
|
||||||
|
}
|
||||||
|
procedure = await _procedureRepository.UpdateAsync(existingProcedure, autoSave: true);
|
||||||
|
result.Message = $"Stored Procedure '{procName}' updated successfully. Use Deploy button to deploy changes to SQL Server.";
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Create new procedure
|
||||||
|
procedure = new SqlStoredProcedure(
|
||||||
|
GuidGenerator.Create(),
|
||||||
|
procName, // ProcedureName from SQL
|
||||||
|
schemaName,
|
||||||
|
displayName, // DisplayName from user input
|
||||||
|
sqlText,
|
||||||
|
input.DataSourceCode
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(input.Description))
|
||||||
|
{
|
||||||
|
procedure.Description = input.Description;
|
||||||
|
}
|
||||||
|
|
||||||
|
await _procedureRepository.InsertAsync(procedure, autoSave: true);
|
||||||
|
result.Message = $"Stored Procedure '{procName}' saved successfully. Use Deploy button to deploy to SQL Server.";
|
||||||
|
}
|
||||||
|
|
||||||
|
result.ObjectType = "StoredProcedure";
|
||||||
|
result.ObjectId = procedure.Id;
|
||||||
|
result.Deployed = procedure.IsDeployed;
|
||||||
|
}
|
||||||
|
else if (sqlUpper.Contains("CREATE FUNCTION") || sqlUpper.Contains("ALTER FUNCTION"))
|
||||||
|
{
|
||||||
|
// Extract function name
|
||||||
|
var funcName = input.Name;
|
||||||
|
var schemaName = "dbo";
|
||||||
|
var returnType = "NVARCHAR(MAX)"; // Default, can be extracted from SQL
|
||||||
|
var displayName = input.Name;
|
||||||
|
|
||||||
|
// Check if function already exists
|
||||||
|
var existingFunction = (await _functionRepository.GetListAsync())
|
||||||
|
.FirstOrDefault(f => f.FunctionName == funcName && f.DataSourceCode == input.DataSourceCode);
|
||||||
|
|
||||||
|
SqlFunction function;
|
||||||
|
if (existingFunction != null)
|
||||||
|
{
|
||||||
|
// Update existing function
|
||||||
|
existingFunction.DisplayName = displayName;
|
||||||
|
existingFunction.FunctionBody = sqlText;
|
||||||
|
existingFunction.IsDeployed = false;
|
||||||
|
existingFunction.LastDeployedAt = null;
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(input.Description))
|
||||||
|
{
|
||||||
|
existingFunction.Description = input.Description;
|
||||||
|
}
|
||||||
|
function = await _functionRepository.UpdateAsync(existingFunction, autoSave: true);
|
||||||
|
result.Message = $"Function '{funcName}' updated successfully. Use Deploy button to deploy changes to SQL Server.";
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Create new function
|
||||||
|
function = new SqlFunction(
|
||||||
|
GuidGenerator.Create(),
|
||||||
|
funcName, // FunctionName from SQL
|
||||||
|
schemaName,
|
||||||
|
displayName, // DisplayName from user input
|
||||||
|
SqlFunctionType.ScalarFunction,
|
||||||
|
sqlText,
|
||||||
|
returnType,
|
||||||
|
input.DataSourceCode
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(input.Description))
|
||||||
|
{
|
||||||
|
function.Description = input.Description;
|
||||||
|
}
|
||||||
|
|
||||||
|
await _functionRepository.InsertAsync(function, autoSave: true);
|
||||||
|
result.Message = $"Function '{funcName}' saved successfully. Use Deploy button to deploy to SQL Server.";
|
||||||
|
}
|
||||||
|
|
||||||
|
result.ObjectType = "Function";
|
||||||
|
result.ObjectId = function.Id;
|
||||||
|
result.Deployed = function.IsDeployed;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Default to Query (SELECT, INSERT, UPDATE, DELETE, etc.)
|
||||||
|
var queryName = input.Name ?? $"Query_{DateTime.Now:yyyyMMddHHmmss}";
|
||||||
|
var queryCode = queryName.Replace(" ", "_");
|
||||||
|
|
||||||
|
// Check if query already exists
|
||||||
|
var existingQuery = (await _queryRepository.GetListAsync())
|
||||||
|
.FirstOrDefault(q => q.Code == queryCode && q.DataSourceCode == input.DataSourceCode);
|
||||||
|
|
||||||
|
SqlQuery query;
|
||||||
|
if (existingQuery != null)
|
||||||
|
{
|
||||||
|
// Update existing query
|
||||||
|
existingQuery.Name = queryName;
|
||||||
|
existingQuery.QueryText = sqlText;
|
||||||
|
if (!string.IsNullOrEmpty(input.Description))
|
||||||
|
{
|
||||||
|
existingQuery.Description = input.Description;
|
||||||
|
}
|
||||||
|
query = await _queryRepository.UpdateAsync(existingQuery, autoSave: true);
|
||||||
|
result.Message = $"Query '{queryName}' updated successfully";
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Create new query
|
||||||
|
query = new SqlQuery(
|
||||||
|
GuidGenerator.Create(),
|
||||||
|
queryCode,
|
||||||
|
queryName,
|
||||||
|
sqlText,
|
||||||
|
input.DataSourceCode
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(input.Description))
|
||||||
|
{
|
||||||
|
query.Description = input.Description;
|
||||||
|
}
|
||||||
|
|
||||||
|
await _queryRepository.InsertAsync(query, autoSave: true);
|
||||||
|
result.Message = $"Query '{queryName}' saved successfully";
|
||||||
|
}
|
||||||
|
|
||||||
|
result.ObjectType = "Query";
|
||||||
|
result.ObjectId = query.Id;
|
||||||
|
result.Deployed = false; // Queries are not deployed;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
throw new Volo.Abp.UserFriendlyException($"Failed to save SQL object: {ex.Message}");
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private string ExtractObjectName(string sql, params string[] keywords)
|
||||||
|
{
|
||||||
|
var sqlUpper = sql.ToUpperInvariant();
|
||||||
|
|
||||||
|
foreach (var keyword in keywords)
|
||||||
|
{
|
||||||
|
var createIndex = sqlUpper.IndexOf($"CREATE {keyword}", StringComparison.OrdinalIgnoreCase);
|
||||||
|
var alterIndex = sqlUpper.IndexOf($"ALTER {keyword}", StringComparison.OrdinalIgnoreCase);
|
||||||
|
var startIndex = Math.Max(createIndex, alterIndex);
|
||||||
|
|
||||||
|
if (startIndex >= 0)
|
||||||
|
{
|
||||||
|
startIndex = sqlUpper.IndexOf(keyword, startIndex, StringComparison.OrdinalIgnoreCase) + keyword.Length;
|
||||||
|
var endIndex = sql.IndexOfAny(new[] { ' ', '\r', '\n', '\t', '(', '[' }, startIndex);
|
||||||
|
|
||||||
|
if (endIndex > startIndex)
|
||||||
|
{
|
||||||
|
var name = sql.Substring(startIndex, endIndex - startIndex).Trim();
|
||||||
|
// Remove schema prefix if exists
|
||||||
|
if (name.Contains("."))
|
||||||
|
{
|
||||||
|
name = name.Substring(name.LastIndexOf('.') + 1);
|
||||||
|
}
|
||||||
|
// Remove square brackets
|
||||||
|
name = name.Replace("[", "").Replace("]", "");
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return "UnnamedObject";
|
||||||
|
}
|
||||||
|
|
||||||
#region Helper Methods
|
#region Helper Methods
|
||||||
|
|
||||||
private SqlQueryExecutionResultDto MapExecutionResult(SqlExecutionResult result)
|
private SqlQueryExecutionResultDto MapExecutionResult(SqlExecutionResult result)
|
||||||
|
|
|
||||||
|
|
@ -80,7 +80,7 @@ public class SqlExecutorService : DomainService, ISqlExecutorService
|
||||||
result.Data = data;
|
result.Data = data;
|
||||||
result.RowsAffected = data?.Count() ?? 0;
|
result.RowsAffected = data?.Count() ?? 0;
|
||||||
result.ExecutionTimeMs = stopwatch.ElapsedMilliseconds;
|
result.ExecutionTimeMs = stopwatch.ElapsedMilliseconds;
|
||||||
result.Message = $"Query executed successfully. Rows returned: {result.RowsAffected}";
|
result.Message = $"Query executed successfully.";
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -10363,6 +10363,12 @@
|
||||||
"tr": "Kaydet",
|
"tr": "Kaydet",
|
||||||
"en": "Save"
|
"en": "Save"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"resourceName": "Platform",
|
||||||
|
"key": "App.Platform.SaveQuery",
|
||||||
|
"tr": "Kaydet Sorgu",
|
||||||
|
"en": "Save Query"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"resourceName": "Platform",
|
"resourceName": "Platform",
|
||||||
"key": "App.Platform.Deploy",
|
"key": "App.Platform.Deploy",
|
||||||
|
|
@ -10443,10 +10449,22 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"resourceName": "Platform",
|
"resourceName": "Platform",
|
||||||
"key": "App.Platform.QuerySavedSuccessfully",
|
"key": "App.Platform.SavedSuccessfully",
|
||||||
"tr": "Sorgu başarıyla kaydedildi",
|
"tr": "Sorgu başarıyla kaydedildi",
|
||||||
"en": "Query saved successfully"
|
"en": "Query saved successfully"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"resourceName": "Platform",
|
||||||
|
"key": "App.Platform.FailedToSave",
|
||||||
|
"tr": "Sorgu kaydedilemedi",
|
||||||
|
"en": "Failed to save query"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"resourceName": "Platform",
|
||||||
|
"key": "App.Platform.AndDeployedToSqlServer",
|
||||||
|
"tr": "Ve SQL Server'a dağıtıldı",
|
||||||
|
"en": "And deployed to SQL Server"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"resourceName": "Platform",
|
"resourceName": "Platform",
|
||||||
"key": "App.Platform.FailedToSaveQuery",
|
"key": "App.Platform.FailedToSaveQuery",
|
||||||
|
|
@ -10495,6 +10513,18 @@
|
||||||
"tr": "Özellikleri görüntülemek için bir nesne seçin",
|
"tr": "Özellikleri görüntülemek için bir nesne seçin",
|
||||||
"en": "Select an object to view properties"
|
"en": "Select an object to view properties"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"resourceName": "Platform",
|
||||||
|
"key": "App.Platform.DetectedObjectType",
|
||||||
|
"tr": "Tespit Edilen Nesne Türü",
|
||||||
|
"en": "Detected Object Type"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"resourceName": "Platform",
|
||||||
|
"key": "App.Platform.DetectedName",
|
||||||
|
"tr": "Tespit Edilen Ad",
|
||||||
|
"en": "Detected Name"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"resourceName": "Platform",
|
"resourceName": "Platform",
|
||||||
"key": "App.Platform.Query",
|
"key": "App.Platform.Query",
|
||||||
|
|
@ -10807,6 +10837,12 @@
|
||||||
"tr": "Nesne Gezgini",
|
"tr": "Nesne Gezgini",
|
||||||
"en": "Object Explorer"
|
"en": "Object Explorer"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"resourceName": "Platform",
|
||||||
|
"key": "App.Platform.ShowColumns",
|
||||||
|
"tr": "Sütunları Göster",
|
||||||
|
"en": "Show Columns"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"resourceName": "Platform",
|
"resourceName": "Platform",
|
||||||
"key": "App.Platform.QueryEditor",
|
"key": "App.Platform.QueryEditor",
|
||||||
|
|
|
||||||
|
|
@ -1,2 +1 @@
|
||||||
export * from './data-source.service'
|
|
||||||
export * from './models'
|
export * from './models'
|
||||||
|
|
|
||||||
|
|
@ -60,7 +60,7 @@ export class SqlObjectManagerService {
|
||||||
apiService.fetchData<void, void>(
|
apiService.fetchData<void, void>(
|
||||||
{
|
{
|
||||||
method: 'DELETE',
|
method: 'DELETE',
|
||||||
url: `/api/app/sql-object-manager/query/${id}`,
|
url: `/api/app/sql-object-manager/${id}/query`,
|
||||||
},
|
},
|
||||||
{ apiName: this.apiName, ...config },
|
{ apiName: this.apiName, ...config },
|
||||||
)
|
)
|
||||||
|
|
@ -99,7 +99,7 @@ export class SqlObjectManagerService {
|
||||||
apiService.fetchData<void, void>(
|
apiService.fetchData<void, void>(
|
||||||
{
|
{
|
||||||
method: 'DELETE',
|
method: 'DELETE',
|
||||||
url: `/api/app/sql-object-manager/stored-procedure/${id}`,
|
url: `/api/app/sql-object-manager/${id}/stored-procedure`,
|
||||||
},
|
},
|
||||||
{ apiName: this.apiName, ...config },
|
{ apiName: this.apiName, ...config },
|
||||||
)
|
)
|
||||||
|
|
@ -129,7 +129,7 @@ export class SqlObjectManagerService {
|
||||||
apiService.fetchData<void, void>(
|
apiService.fetchData<void, void>(
|
||||||
{
|
{
|
||||||
method: 'DELETE',
|
method: 'DELETE',
|
||||||
url: `/api/app/sql-object-manager/view/${id}`,
|
url: `/api/app/sql-object-manager/${id}/view`,
|
||||||
},
|
},
|
||||||
{ apiName: this.apiName, ...config },
|
{ apiName: this.apiName, ...config },
|
||||||
)
|
)
|
||||||
|
|
@ -159,7 +159,7 @@ export class SqlObjectManagerService {
|
||||||
apiService.fetchData<void, void>(
|
apiService.fetchData<void, void>(
|
||||||
{
|
{
|
||||||
method: 'DELETE',
|
method: 'DELETE',
|
||||||
url: `/api/app/sql-object-manager/function/${id}`,
|
url: `/api/app/sql-object-manager/${id}/function`,
|
||||||
},
|
},
|
||||||
{ apiName: this.apiName, ...config },
|
{ apiName: this.apiName, ...config },
|
||||||
)
|
)
|
||||||
|
|
@ -184,6 +184,17 @@ export class SqlObjectManagerService {
|
||||||
},
|
},
|
||||||
{ apiName: this.apiName, ...config },
|
{ apiName: this.apiName, ...config },
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Smart Save - Analyzes SQL and saves to appropriate table with auto-deploy
|
||||||
|
smartSave = (input: { sqlText: string; dataSourceCode: string; name?: string; description?: string }, config?: Partial<Config>) =>
|
||||||
|
apiService.fetchData<{ objectType: string; objectId: string; deployed: boolean; message: string }, typeof input>(
|
||||||
|
{
|
||||||
|
method: 'POST',
|
||||||
|
url: '/api/app/sql-object-manager/smart-save',
|
||||||
|
data: input,
|
||||||
|
},
|
||||||
|
{ apiName: this.apiName, ...config },
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Export service instance
|
// Export service instance
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,6 @@ import {
|
||||||
} from '@/components/ui'
|
} from '@/components/ui'
|
||||||
import { ROUTES_ENUM } from '@/routes/route.constant'
|
import { ROUTES_ENUM } from '@/routes/route.constant'
|
||||||
import { ListFormWizardDto } from '@/proxy/admin/list-form/models'
|
import { ListFormWizardDto } from '@/proxy/admin/list-form/models'
|
||||||
import { getDataSources } from '@/proxy/data-source'
|
|
||||||
import { SelectBoxOption } from '@/types/shared'
|
import { SelectBoxOption } from '@/types/shared'
|
||||||
import { useLocalization } from '@/utils/hooks/useLocalization'
|
import { useLocalization } from '@/utils/hooks/useLocalization'
|
||||||
import { Field, FieldProps, Form, Formik } from 'formik'
|
import { Field, FieldProps, Form, Formik } from 'formik'
|
||||||
|
|
@ -24,6 +23,7 @@ import { getMenus } from '@/services/menu.service'
|
||||||
import { getPermissions } from '@/services/identity.service'
|
import { getPermissions } from '@/services/identity.service'
|
||||||
import { DbTypeEnum, SelectCommandTypeEnum } from '@/proxy/form/models'
|
import { DbTypeEnum, SelectCommandTypeEnum } from '@/proxy/form/models'
|
||||||
import { postListFormWizard } from '@/services/admin/list-form.service'
|
import { postListFormWizard } from '@/services/admin/list-form.service'
|
||||||
|
import { getDataSources } from '@/services/data-source.service'
|
||||||
|
|
||||||
const initialValues: ListFormWizardDto = {
|
const initialValues: ListFormWizardDto = {
|
||||||
listFormCode: '',
|
listFormCode: '',
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
import { Container } from '@/components/shared'
|
import { Container } from '@/components/shared'
|
||||||
import { Button, Card, Checkbox, FormContainer, FormItem, Input, Select } from '@/components/ui'
|
import { Button, Card, Checkbox, FormContainer, FormItem, Input, Select } from '@/components/ui'
|
||||||
import { ListFormEditTabs } from '@/proxy/admin/list-form/options'
|
import { ListFormEditTabs } from '@/proxy/admin/list-form/options'
|
||||||
import { getDataSources } from '@/proxy/data-source'
|
|
||||||
import { SelectBoxOption } from '@/types/shared'
|
import { SelectBoxOption } from '@/types/shared'
|
||||||
import { useStoreState } from '@/store'
|
import { useStoreState } from '@/store'
|
||||||
import { useLocalization } from '@/utils/hooks/useLocalization'
|
import { useLocalization } from '@/utils/hooks/useLocalization'
|
||||||
|
|
@ -11,6 +10,7 @@ import * as Yup from 'yup'
|
||||||
import { FormEditProps } from './FormEdit'
|
import { FormEditProps } from './FormEdit'
|
||||||
import { dbSourceTypeOptions, selectCommandTypeOptions } from './options'
|
import { dbSourceTypeOptions, selectCommandTypeOptions } from './options'
|
||||||
import { DataSourceTypeEnum, SelectCommandTypeEnum } from '@/proxy/form/models'
|
import { DataSourceTypeEnum, SelectCommandTypeEnum } from '@/proxy/form/models'
|
||||||
|
import { getDataSources } from '@/services/data-source.service'
|
||||||
|
|
||||||
const schema = Yup.object().shape({
|
const schema = Yup.object().shape({
|
||||||
isOrganizationUnit: Yup.bool(),
|
isOrganizationUnit: Yup.bool(),
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { useState, useCallback, useEffect } from 'react'
|
import { useState, useCallback, useEffect, useRef } from 'react'
|
||||||
import { Button, Dialog, Input, Notification, toast } from '@/components/ui'
|
import { Button, Dialog, Input, Notification, toast } from '@/components/ui'
|
||||||
import Container from '@/components/shared/Container'
|
import Container from '@/components/shared/Container'
|
||||||
import AdaptableCard from '@/components/shared/AdaptableCard'
|
import AdaptableCard from '@/components/shared/AdaptableCard'
|
||||||
|
|
@ -9,19 +9,19 @@ import type {
|
||||||
SqlQueryDto,
|
SqlQueryDto,
|
||||||
SqlStoredProcedureDto,
|
SqlStoredProcedureDto,
|
||||||
SqlViewDto,
|
SqlViewDto,
|
||||||
SqlObjectType,
|
|
||||||
SqlQueryExecutionResultDto,
|
SqlQueryExecutionResultDto,
|
||||||
} from '@/proxy/sql-query-manager/models'
|
} from '@/proxy/sql-query-manager/models'
|
||||||
import {
|
import { SqlObjectType } from '@/proxy/sql-query-manager/models'
|
||||||
sqlObjectManagerService,
|
import { sqlObjectManagerService } from '@/services/sql-query-manager.service'
|
||||||
} from '@/services/sql-query-manager.service'
|
import { FaDatabase, FaPlay, FaSave, FaSyncAlt, FaCloudUploadAlt } from 'react-icons/fa'
|
||||||
import { FaDatabase, FaPlay, FaSave, FaSyncAlt } from 'react-icons/fa'
|
import { HiOutlineCheckCircle } from 'react-icons/hi'
|
||||||
import { useLocalization } from '@/utils/hooks/useLocalization'
|
import { useLocalization } from '@/utils/hooks/useLocalization'
|
||||||
import SqlObjectExplorer from './components/SqlObjectExplorer'
|
import SqlObjectExplorer from './components/SqlObjectExplorer'
|
||||||
import SqlEditor from './components/SqlEditor'
|
import SqlEditor, { SqlEditorRef } from './components/SqlEditor'
|
||||||
import SqlResultsGrid from './components/SqlResultsGrid'
|
import SqlResultsGrid from './components/SqlResultsGrid'
|
||||||
import SqlObjectProperties from './components/SqlObjectProperties'
|
import SqlObjectProperties from './components/SqlObjectProperties'
|
||||||
import { FaCloudUploadAlt } from 'react-icons/fa'
|
import { Splitter } from '@/components/codeLayout/Splitter'
|
||||||
|
import { Helmet } from 'react-helmet'
|
||||||
|
|
||||||
export type SqlObject = SqlFunctionDto | SqlQueryDto | SqlStoredProcedureDto | SqlViewDto
|
export type SqlObject = SqlFunctionDto | SqlQueryDto | SqlStoredProcedureDto | SqlViewDto
|
||||||
|
|
||||||
|
|
@ -35,10 +35,14 @@ interface SqlManagerState {
|
||||||
executionResult: SqlQueryExecutionResultDto | null
|
executionResult: SqlQueryExecutionResultDto | null
|
||||||
showProperties: boolean
|
showProperties: boolean
|
||||||
isDirty: boolean
|
isDirty: boolean
|
||||||
|
tableColumns: any | null
|
||||||
|
isSaved: boolean
|
||||||
|
refreshTrigger: number
|
||||||
}
|
}
|
||||||
|
|
||||||
const SqlQueryManager = () => {
|
const SqlQueryManager = () => {
|
||||||
const { translate } = useLocalization()
|
const { translate } = useLocalization()
|
||||||
|
const editorRef = useRef<SqlEditorRef>(null)
|
||||||
|
|
||||||
const [state, setState] = useState<SqlManagerState>({
|
const [state, setState] = useState<SqlManagerState>({
|
||||||
dataSources: [],
|
dataSources: [],
|
||||||
|
|
@ -47,15 +51,26 @@ const SqlQueryManager = () => {
|
||||||
selectedObjectType: null,
|
selectedObjectType: null,
|
||||||
editorContent: '',
|
editorContent: '',
|
||||||
isExecuting: false,
|
isExecuting: false,
|
||||||
|
refreshTrigger: 0,
|
||||||
executionResult: null,
|
executionResult: null,
|
||||||
showProperties: false,
|
showProperties: false,
|
||||||
isDirty: false,
|
isDirty: false,
|
||||||
|
tableColumns: null,
|
||||||
|
isSaved: false,
|
||||||
})
|
})
|
||||||
|
|
||||||
const [showSaveDialog, setShowSaveDialog] = useState(false)
|
const [showSaveDialog, setShowSaveDialog] = useState(false)
|
||||||
const [saveDialogData, setSaveDialogData] = useState({ name: '', description: '' })
|
const [saveDialogData, setSaveDialogData] = useState({
|
||||||
|
name: '',
|
||||||
|
description: '',
|
||||||
|
detectedType: '',
|
||||||
|
detectedName: '',
|
||||||
|
isExistingObject: false,
|
||||||
|
})
|
||||||
const [showTemplateConfirmDialog, setShowTemplateConfirmDialog] = useState(false)
|
const [showTemplateConfirmDialog, setShowTemplateConfirmDialog] = useState(false)
|
||||||
const [pendingTemplate, setPendingTemplate] = useState<{ content: string; type: string } | null>(null)
|
const [pendingTemplate, setPendingTemplate] = useState<{ content: string; type: string } | null>(
|
||||||
|
null,
|
||||||
|
)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
loadDataSources()
|
loadDataSources()
|
||||||
|
|
@ -124,7 +139,9 @@ const SqlQueryManager = () => {
|
||||||
selectedObjectType: objectType,
|
selectedObjectType: objectType,
|
||||||
editorContent: content,
|
editorContent: content,
|
||||||
executionResult: null,
|
executionResult: null,
|
||||||
|
tableColumns: null,
|
||||||
isDirty: false,
|
isDirty: false,
|
||||||
|
isSaved: false,
|
||||||
}))
|
}))
|
||||||
},
|
},
|
||||||
[state.isDirty, translate],
|
[state.isDirty, translate],
|
||||||
|
|
@ -135,12 +152,13 @@ const SqlQueryManager = () => {
|
||||||
...prev,
|
...prev,
|
||||||
editorContent: value || '',
|
editorContent: value || '',
|
||||||
isDirty: true,
|
isDirty: true,
|
||||||
|
isSaved: false,
|
||||||
}))
|
}))
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
const getTemplateContent = (templateType: string): string => {
|
const getTemplateContent = (templateType: string): string => {
|
||||||
const templates: Record<string, string> = {
|
const templates: Record<string, string> = {
|
||||||
'select': `-- Basic SELECT query
|
select: `-- Basic SELECT query
|
||||||
SELECT
|
SELECT
|
||||||
Column1,
|
Column1,
|
||||||
Column2,
|
Column2,
|
||||||
|
|
@ -154,12 +172,12 @@ WHERE
|
||||||
ORDER BY
|
ORDER BY
|
||||||
Column1 ASC;`,
|
Column1 ASC;`,
|
||||||
|
|
||||||
'insert': `-- Basic INSERT query
|
insert: `-- Basic INSERT query
|
||||||
INSERT INTO TableName (Column1, Column2, Column3)
|
INSERT INTO TableName (Column1, Column2, Column3)
|
||||||
VALUES
|
VALUES
|
||||||
('Value1', 'Value2', 'Value3');`,
|
('Value1', 'Value2', 'Value3');`,
|
||||||
|
|
||||||
'update': `-- Basic UPDATE query
|
update: `-- Basic UPDATE query
|
||||||
UPDATE TableName
|
UPDATE TableName
|
||||||
SET
|
SET
|
||||||
Column1 = 'NewValue1',
|
Column1 = 'NewValue1',
|
||||||
|
|
@ -168,7 +186,7 @@ WHERE
|
||||||
-- Add your conditions
|
-- Add your conditions
|
||||||
Id = 1;`,
|
Id = 1;`,
|
||||||
|
|
||||||
'delete': `-- Basic DELETE query
|
delete: `-- Basic DELETE query
|
||||||
DELETE FROM TableName
|
DELETE FROM TableName
|
||||||
WHERE
|
WHERE
|
||||||
-- Add your conditions
|
-- Add your conditions
|
||||||
|
|
@ -250,46 +268,95 @@ RETURN
|
||||||
AND t.Column2 LIKE '%' + @Parameter2 + '%'
|
AND t.Column2 LIKE '%' + @Parameter2 + '%'
|
||||||
AND t.IsActive = 1
|
AND t.IsActive = 1
|
||||||
)
|
)
|
||||||
GO`
|
GO`,
|
||||||
}
|
}
|
||||||
|
|
||||||
return templates[templateType] || templates['select']
|
return templates[templateType] || templates['select']
|
||||||
}
|
}
|
||||||
|
|
||||||
const applyTemplate = useCallback((templateContent: string) => {
|
// SQL analiz fonksiyonu - SQL metnini analiz edip nesne türünü ve adını tespit eder
|
||||||
setState((prev) => ({
|
const detectSqlObject = (sql: string): { type: string; name: string } => {
|
||||||
...prev,
|
const upperSql = sql.trim().toUpperCase()
|
||||||
editorContent: templateContent,
|
|
||||||
selectedObject: null,
|
|
||||||
selectedObjectType: null,
|
|
||||||
executionResult: null,
|
|
||||||
isDirty: false,
|
|
||||||
}))
|
|
||||||
|
|
||||||
toast.push(
|
// VIEW tespiti
|
||||||
<Notification type="success" title={translate('::App.Platform.Success')}>
|
if (upperSql.includes('CREATE VIEW') || upperSql.includes('ALTER VIEW')) {
|
||||||
{translate('::App.Platform.TemplateLoaded')}
|
// Son kelimeyi al (schema varsa sonraki kelime, yoksa ilk kelime)
|
||||||
</Notification>,
|
const viewMatch = sql.match(
|
||||||
{ placement: 'top-center' },
|
/(?:CREATE|ALTER)\s+VIEW\s+(?:[\[\]]*\w+[\[\]]*\.)?\s*[\[]?(\w+)[\]]?/i,
|
||||||
)
|
)
|
||||||
}, [translate])
|
return {
|
||||||
|
type: 'View',
|
||||||
const handleTemplateSelect = useCallback((template: string, templateType: string) => {
|
name: viewMatch ? viewMatch[1] : '',
|
||||||
// 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])
|
|
||||||
|
// STORED PROCEDURE tespiti
|
||||||
|
if (
|
||||||
|
upperSql.includes('CREATE PROCEDURE') ||
|
||||||
|
upperSql.includes('CREATE PROC') ||
|
||||||
|
upperSql.includes('ALTER PROCEDURE') ||
|
||||||
|
upperSql.includes('ALTER PROC')
|
||||||
|
) {
|
||||||
|
const procMatch = sql.match(
|
||||||
|
/(?:CREATE|ALTER)\s+(?:PROCEDURE|PROC)\s+(?:[\[\]]*\w+[\[\]]*\.)?\s*[\[]?(\w+)[\]]?/i,
|
||||||
|
)
|
||||||
|
return {
|
||||||
|
type: 'StoredProcedure',
|
||||||
|
name: procMatch ? procMatch[1] : '',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// FUNCTION tespiti
|
||||||
|
if (upperSql.includes('CREATE FUNCTION') || upperSql.includes('ALTER FUNCTION')) {
|
||||||
|
const funcMatch = sql.match(
|
||||||
|
/(?:CREATE|ALTER)\s+FUNCTION\s+(?:[\[\]]*\w+[\[\]]*\.)?\s*[\[]?(\w+)[\]]?/i,
|
||||||
|
)
|
||||||
|
return {
|
||||||
|
type: 'Function',
|
||||||
|
name: funcMatch ? funcMatch[1] : '',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default: Query
|
||||||
|
return {
|
||||||
|
type: 'Query',
|
||||||
|
name: '',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const applyTemplate = useCallback(
|
||||||
|
(templateContent: string) => {
|
||||||
|
setState((prev) => ({
|
||||||
|
...prev,
|
||||||
|
editorContent: templateContent,
|
||||||
|
selectedObject: null,
|
||||||
|
selectedObjectType: null,
|
||||||
|
executionResult: null,
|
||||||
|
isDirty: false,
|
||||||
|
}))
|
||||||
|
},
|
||||||
|
[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)
|
||||||
|
|
||||||
|
// 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(() => {
|
const handleConfirmTemplateReplace = useCallback(() => {
|
||||||
if (pendingTemplate) {
|
if (pendingTemplate) {
|
||||||
|
|
@ -315,7 +382,11 @@ GO`
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!state.editorContent.trim()) {
|
// 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(
|
toast.push(
|
||||||
<Notification type="warning" title={translate('::App.Platform.Warning')}>
|
<Notification type="warning" title={translate('::App.Platform.Warning')}>
|
||||||
{translate('::App.Platform.PleaseEnterQuery')}
|
{translate('::App.Platform.PleaseEnterQuery')}
|
||||||
|
|
@ -325,24 +396,40 @@ GO`
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
setState((prev) => ({ ...prev, isExecuting: true, executionResult: null }))
|
// Seçili metni koru
|
||||||
|
const savedSelection = editorRef.current?.preserveSelection()
|
||||||
|
|
||||||
|
setState((prev) => ({ ...prev, isExecuting: true, executionResult: null, tableColumns: null }))
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const result = await sqlObjectManagerService.executeQuery({
|
const result = await sqlObjectManagerService.executeQuery({
|
||||||
queryText: state.editorContent,
|
queryText: queryToExecute,
|
||||||
dataSourceCode: state.selectedDataSource.code || '',
|
dataSourceCode: state.selectedDataSource.code || '',
|
||||||
})
|
})
|
||||||
|
|
||||||
setState((prev) => ({ ...prev, executionResult: result.data, isExecuting: false }))
|
setState((prev) => ({
|
||||||
|
...prev,
|
||||||
|
executionResult: result.data,
|
||||||
|
isExecuting: false,
|
||||||
|
tableColumns: null,
|
||||||
|
}))
|
||||||
|
|
||||||
toast.push(
|
// Seçili metni geri yükle
|
||||||
<Notification type="success" title={translate('::App.Platform.Success')}>
|
setTimeout(() => {
|
||||||
{translate('::App.Platform.QueryExecutedSuccessfully')} ({result.data.executionTimeMs}ms)
|
if (savedSelection) {
|
||||||
</Notification>,
|
editorRef.current?.restoreSelection(savedSelection)
|
||||||
{ placement: 'top-center' },
|
}
|
||||||
)
|
}, 100)
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
setState((prev) => ({ ...prev, isExecuting: false }))
|
setState((prev) => ({ ...prev, isExecuting: false }))
|
||||||
|
|
||||||
|
// Hata durumunda da seçili metni geri yükle
|
||||||
|
setTimeout(() => {
|
||||||
|
if (savedSelection) {
|
||||||
|
editorRef.current?.restoreSelection(savedSelection)
|
||||||
|
}
|
||||||
|
}, 100)
|
||||||
|
|
||||||
toast.push(
|
toast.push(
|
||||||
<Notification type="danger" title={translate('::App.Platform.Error')}>
|
<Notification type="danger" title={translate('::App.Platform.Error')}>
|
||||||
{error.response?.data?.error?.message || translate('::App.Platform.FailedToExecuteQuery')}
|
{error.response?.data?.error?.message || translate('::App.Platform.FailedToExecuteQuery')}
|
||||||
|
|
@ -374,74 +461,45 @@ GO`
|
||||||
}
|
}
|
||||||
|
|
||||||
if (state.selectedObject && state.selectedObjectType) {
|
if (state.selectedObject && state.selectedObjectType) {
|
||||||
// Update existing object
|
// Update existing object - open dialog with existing data
|
||||||
await handleUpdate()
|
const typeMap: Record<SqlObjectType, string> = {
|
||||||
} else {
|
[SqlObjectType.Query]: 'Query',
|
||||||
// Create new object - show dialog to choose type
|
[SqlObjectType.View]: 'View',
|
||||||
setSaveDialogData({ name: '', description: '' })
|
[SqlObjectType.StoredProcedure]: 'StoredProcedure',
|
||||||
setShowSaveDialog(true)
|
[SqlObjectType.Function]: 'Function',
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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, {
|
|
||||||
...(state.selectedObject as SqlQueryDto),
|
|
||||||
queryText: state.editorContent,
|
|
||||||
})
|
|
||||||
break
|
|
||||||
case 2: // Stored Procedure
|
|
||||||
await sqlObjectManagerService.updateStoredProcedure(objectId, {
|
|
||||||
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, {
|
|
||||||
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, {
|
|
||||||
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 }))
|
// Get name based on object type
|
||||||
|
let objectName = ''
|
||||||
|
if ('viewName' in state.selectedObject) {
|
||||||
|
objectName = state.selectedObject.viewName || state.selectedObject.displayName || ''
|
||||||
|
} else if ('procedureName' in state.selectedObject) {
|
||||||
|
objectName = state.selectedObject.procedureName || state.selectedObject.displayName || ''
|
||||||
|
} else if ('functionName' in state.selectedObject) {
|
||||||
|
objectName = state.selectedObject.functionName || state.selectedObject.displayName || ''
|
||||||
|
} else if ('name' in state.selectedObject) {
|
||||||
|
objectName = state.selectedObject.name || ''
|
||||||
|
}
|
||||||
|
|
||||||
toast.push(
|
setSaveDialogData({
|
||||||
<Notification type="success" title={translate('::App.Platform.Success')}>
|
name: objectName,
|
||||||
{translate('::App.Platform.ObjectUpdatedSuccessfully')}
|
description: state.selectedObject.description || '',
|
||||||
</Notification>,
|
detectedType: typeMap[state.selectedObjectType] || '',
|
||||||
{ placement: 'top-center' },
|
detectedName: objectName,
|
||||||
)
|
isExistingObject: true,
|
||||||
} catch (error: any) {
|
})
|
||||||
toast.push(
|
setShowSaveDialog(true)
|
||||||
<Notification type="danger" title={translate('::App.Platform.Error')}>
|
} else {
|
||||||
{error.response?.data?.error?.message || translate('::App.Platform.FailedToUpdateObject')}
|
// New object - analyze SQL and show dialog with detection
|
||||||
</Notification>,
|
const detection = detectSqlObject(state.editorContent)
|
||||||
{ placement: 'top-center' },
|
setSaveDialogData({
|
||||||
)
|
name: detection.name,
|
||||||
|
description: '',
|
||||||
|
detectedType: detection.type,
|
||||||
|
detectedName: detection.name,
|
||||||
|
isExistingObject: false,
|
||||||
|
})
|
||||||
|
setShowSaveDialog(true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -449,24 +507,54 @@ GO`
|
||||||
if (!state.selectedDataSource || !saveDialogData.name) return
|
if (!state.selectedDataSource || !saveDialogData.name) return
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await sqlObjectManagerService.createQuery({
|
// Smart save ile kaydet
|
||||||
code: saveDialogData.name.replace(/\s+/g, '_'),
|
const result = await sqlObjectManagerService.smartSave({
|
||||||
|
sqlText: state.editorContent,
|
||||||
|
dataSourceCode: state.selectedDataSource.code || '',
|
||||||
name: saveDialogData.name,
|
name: saveDialogData.name,
|
||||||
description: saveDialogData.description,
|
description: saveDialogData.description,
|
||||||
queryText: state.editorContent,
|
|
||||||
dataSourceCode: state.selectedDataSource.code || '',
|
|
||||||
category: '',
|
|
||||||
tags: '',
|
|
||||||
isModifyingData: false,
|
|
||||||
parameters: '',
|
|
||||||
})
|
})
|
||||||
|
|
||||||
setState((prev) => ({ ...prev, isDirty: false }))
|
// Kaydedilen objeyi state'e set et
|
||||||
|
const savedObject: any = {
|
||||||
|
id: result.data.objectId,
|
||||||
|
displayName: saveDialogData.name,
|
||||||
|
description: saveDialogData.description,
|
||||||
|
isDeployed: result.data.deployed,
|
||||||
|
}
|
||||||
|
|
||||||
|
// ObjectType'a göre ekstra alanlar ekle
|
||||||
|
let objectType: SqlObjectType | null = null
|
||||||
|
if (result.data.objectType === 'View') {
|
||||||
|
objectType = SqlObjectType.View
|
||||||
|
savedObject.viewName = saveDialogData.name
|
||||||
|
savedObject.viewDefinition = state.editorContent
|
||||||
|
} else if (result.data.objectType === 'StoredProcedure') {
|
||||||
|
objectType = SqlObjectType.StoredProcedure
|
||||||
|
savedObject.procedureName = saveDialogData.name
|
||||||
|
savedObject.procedureBody = state.editorContent
|
||||||
|
} else if (result.data.objectType === 'Function') {
|
||||||
|
objectType = SqlObjectType.Function
|
||||||
|
savedObject.functionName = saveDialogData.name
|
||||||
|
savedObject.functionBody = state.editorContent
|
||||||
|
} else if (result.data.objectType === 'Query') {
|
||||||
|
objectType = SqlObjectType.Query
|
||||||
|
savedObject.queryText = state.editorContent
|
||||||
|
}
|
||||||
|
|
||||||
|
setState((prev) => ({
|
||||||
|
...prev,
|
||||||
|
isDirty: false,
|
||||||
|
isSaved: true,
|
||||||
|
selectedObject: savedObject,
|
||||||
|
selectedObjectType: objectType,
|
||||||
|
refreshTrigger: prev.refreshTrigger + 1,
|
||||||
|
}))
|
||||||
setShowSaveDialog(false)
|
setShowSaveDialog(false)
|
||||||
|
|
||||||
toast.push(
|
toast.push(
|
||||||
<Notification type="success" title={translate('::App.Platform.Success')}>
|
<Notification type="success" title={translate('::App.Platform.Success')}>
|
||||||
{translate('::App.Platform.QuerySavedSuccessfully')}
|
{result.data.message || translate('::App.Platform.SavedSuccessfully')}
|
||||||
</Notification>,
|
</Notification>,
|
||||||
{ placement: 'top-center' },
|
{ placement: 'top-center' },
|
||||||
)
|
)
|
||||||
|
|
@ -497,14 +585,20 @@ GO`
|
||||||
let result: any
|
let result: any
|
||||||
|
|
||||||
switch (state.selectedObjectType) {
|
switch (state.selectedObjectType) {
|
||||||
case 2: // Stored Procedure
|
case SqlObjectType.StoredProcedure:
|
||||||
result = await sqlObjectManagerService.deployStoredProcedure({ id: objectId, dropIfExists: true })
|
result = await sqlObjectManagerService.deployStoredProcedure({
|
||||||
|
id: objectId,
|
||||||
|
dropIfExists: true,
|
||||||
|
})
|
||||||
break
|
break
|
||||||
case 3: // View
|
case SqlObjectType.View:
|
||||||
result = await sqlObjectManagerService.deployView({ id: objectId, dropIfExists: true })
|
result = await sqlObjectManagerService.deployView({ id: objectId, dropIfExists: true })
|
||||||
break
|
break
|
||||||
case 4: // Function
|
case SqlObjectType.Function:
|
||||||
result = await sqlObjectManagerService.deployFunction({ id: objectId, dropIfExists: true })
|
result = await sqlObjectManagerService.deployFunction({
|
||||||
|
id: objectId,
|
||||||
|
dropIfExists: true,
|
||||||
|
})
|
||||||
break
|
break
|
||||||
default:
|
default:
|
||||||
toast.push(
|
toast.push(
|
||||||
|
|
@ -516,6 +610,13 @@ GO`
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Update selectedObject's isDeployed status
|
||||||
|
setState((prev) => ({
|
||||||
|
...prev,
|
||||||
|
selectedObject: prev.selectedObject ? { ...prev.selectedObject, isDeployed: true } : null,
|
||||||
|
refreshTrigger: prev.refreshTrigger + 1,
|
||||||
|
}))
|
||||||
|
|
||||||
toast.push(
|
toast.push(
|
||||||
<Notification type="success" title={translate('::App.Platform.Success')}>
|
<Notification type="success" title={translate('::App.Platform.Success')}>
|
||||||
{translate('::App.Platform.ObjectDeployedSuccessfully')}
|
{translate('::App.Platform.ObjectDeployedSuccessfully')}
|
||||||
|
|
@ -532,11 +633,69 @@ GO`
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handleShowTableColumns = async (schemaName: string, tableName: string) => {
|
||||||
|
if (!state.selectedDataSource) return
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await sqlObjectManagerService.getTableColumns(
|
||||||
|
state.selectedDataSource.code || '',
|
||||||
|
schemaName,
|
||||||
|
tableName,
|
||||||
|
)
|
||||||
|
|
||||||
|
// Transform API response to match display format
|
||||||
|
const transformedData = response.data.map((col: any) => ({
|
||||||
|
ColumnName: col.columnName,
|
||||||
|
DataType: col.dataType,
|
||||||
|
MaxLength: col.maxLength || '-',
|
||||||
|
IsNullable: col.isNullable,
|
||||||
|
IsPrimaryKey: col.isPrimaryKey || false,
|
||||||
|
}))
|
||||||
|
|
||||||
|
// Create a result object that looks like execution result for display
|
||||||
|
const columnsResult = {
|
||||||
|
success: true,
|
||||||
|
message: `Columns for ${schemaName}.${tableName}`,
|
||||||
|
data: transformedData,
|
||||||
|
rowsAffected: transformedData.length,
|
||||||
|
executionTimeMs: 0,
|
||||||
|
metadata: {
|
||||||
|
columns: [
|
||||||
|
{ name: 'ColumnName', dataType: 'string' },
|
||||||
|
{ name: 'DataType', dataType: 'string' },
|
||||||
|
{ name: 'MaxLength', dataType: 'string' },
|
||||||
|
{ name: 'IsNullable', dataType: 'boolean' },
|
||||||
|
{ name: 'IsPrimaryKey', dataType: 'boolean' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
setState((prev) => ({
|
||||||
|
...prev,
|
||||||
|
tableColumns: columnsResult,
|
||||||
|
executionResult: null, // Clear query results when showing columns
|
||||||
|
}))
|
||||||
|
} catch (error: any) {
|
||||||
|
toast.push(
|
||||||
|
<Notification type="danger" title={translate('::App.Platform.Error')}>
|
||||||
|
{error.response?.data?.error?.message || translate('::App.Platform.FailedToLoadColumns')}
|
||||||
|
</Notification>,
|
||||||
|
{ placement: 'top-center' },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Container className="h-full overflow-hidden">
|
<Container className="h-full overflow-hidden">
|
||||||
|
<Helmet
|
||||||
|
titleTemplate="%s | Erp Platform"
|
||||||
|
title={translate('::' + 'App.SqlQueryManager')}
|
||||||
|
defaultTitle="Erp Platform"
|
||||||
|
></Helmet>
|
||||||
|
|
||||||
<div className="flex flex-col h-full p-1">
|
<div className="flex flex-col h-full p-1">
|
||||||
{/* Toolbar */}
|
{/* Toolbar */}
|
||||||
<AdaptableCard className="flex-shrink-0 shadow-sm mb-4">
|
<div className="flex-shrink-0 shadow-sm mb-4">
|
||||||
<div className="flex items-center justify-between px-1 py-1">
|
<div className="flex items-center justify-between px-1 py-1">
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<FaDatabase className="text-lg text-blue-500" />
|
<FaDatabase className="text-lg text-blue-500" />
|
||||||
|
|
@ -578,43 +737,43 @@ GO`
|
||||||
variant="solid"
|
variant="solid"
|
||||||
icon={<FaSave />}
|
icon={<FaSave />}
|
||||||
onClick={handleSave}
|
onClick={handleSave}
|
||||||
disabled={!state.isDirty || !state.selectedDataSource}
|
disabled={
|
||||||
|
!state.selectedDataSource ||
|
||||||
|
!state.editorContent.trim() ||
|
||||||
|
(state.isSaved && !state.isDirty) ||
|
||||||
|
!state.executionResult?.success
|
||||||
|
}
|
||||||
className="shadow-sm"
|
className="shadow-sm"
|
||||||
>
|
>
|
||||||
{translate('::App.Platform.Save')}
|
{translate('::App.Platform.Save')}
|
||||||
<span className="ml-1 text-xs opacity-75">(Ctrl+S)</span>
|
<span className="ml-1 text-xs opacity-75">(Ctrl+S)</span>
|
||||||
</Button>
|
</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
|
<Button
|
||||||
size="sm"
|
size="sm"
|
||||||
variant="twoTone"
|
variant="solid"
|
||||||
icon={<FaSyncAlt />}
|
color="green-600"
|
||||||
onClick={() => window.location.reload()}
|
icon={<FaCloudUploadAlt />}
|
||||||
|
onClick={handleDeploy}
|
||||||
|
disabled={
|
||||||
|
!state.selectedObject ||
|
||||||
|
!state.selectedObjectType ||
|
||||||
|
state.selectedObjectType === SqlObjectType.Query ||
|
||||||
|
(state.selectedObject &&
|
||||||
|
'isDeployed' in state.selectedObject &&
|
||||||
|
state.selectedObject.isDeployed)
|
||||||
|
}
|
||||||
className="shadow-sm"
|
className="shadow-sm"
|
||||||
>
|
>
|
||||||
{translate('::App.Platform.Refresh')}
|
{translate('::App.Platform.Deploy')}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</AdaptableCard>
|
</div>
|
||||||
|
|
||||||
{/* Main Content Area */}
|
{/* Main Content Area */}
|
||||||
<div className="flex-1 flex min-h-0">
|
<div className="flex-1 flex min-h-0">
|
||||||
{/* Left Panel - Object Explorer */}
|
{/* Left Panel - Object Explorer */}
|
||||||
<div className="w-80 flex-shrink-0 flex flex-col min-h-0 mr-4">
|
<div className="w-1/3 flex-shrink-0 flex flex-col min-h-0 mr-4">
|
||||||
<AdaptableCard className="h-full" bodyClass="p-0">
|
<AdaptableCard className="h-full" bodyClass="p-0">
|
||||||
<div className="h-full flex flex-col">
|
<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">
|
<div className="border-b px-4 py-3 bg-gray-50 dark:bg-gray-800 flex-shrink-0">
|
||||||
|
|
@ -622,42 +781,90 @@ GO`
|
||||||
{translate('::App.Platform.ObjectExplorer')}
|
{translate('::App.Platform.ObjectExplorer')}
|
||||||
</h6>
|
</h6>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-1 min-h-0 overflow-auto">
|
<SqlObjectExplorer
|
||||||
<SqlObjectExplorer
|
dataSource={state.selectedDataSource}
|
||||||
dataSource={state.selectedDataSource}
|
onObjectSelect={handleObjectSelect}
|
||||||
onObjectSelect={handleObjectSelect}
|
selectedObject={state.selectedObject}
|
||||||
selectedObject={state.selectedObject}
|
onTemplateSelect={handleTemplateSelect}
|
||||||
onTemplateSelect={handleTemplateSelect}
|
onShowTableColumns={handleShowTableColumns}
|
||||||
/>
|
refreshTrigger={state.refreshTrigger}
|
||||||
</div>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</AdaptableCard>
|
</AdaptableCard>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Center Panel - Editor and Results */}
|
{/* Center Panel - Editor and Results */}
|
||||||
<div className="flex-1 flex flex-col min-h-0 mr-4">
|
<div className="flex-1 flex flex-col min-h-0 mr-4">
|
||||||
<div className="flex-1 border rounded-lg shadow-sm bg-white dark:bg-gray-800 flex flex-col overflow-hidden">
|
{state.executionResult || state.tableColumns ? (
|
||||||
<div className="border-b px-4 py-2 bg-gray-50 dark:bg-gray-800 flex-shrink-0">
|
<Splitter direction="vertical" initialSize={250} minSize={150} maxSize={1200}>
|
||||||
<h6 className="font-semibold text-sm">{translate('::App.Platform.QueryEditor')}</h6>
|
<div className="border rounded-lg shadow-sm bg-white dark:bg-gray-800 flex flex-col h-full">
|
||||||
</div>
|
<div className="border-b px-4 py-2 bg-gray-50 dark:bg-gray-800 flex-shrink-0">
|
||||||
<div className="flex-1 min-h-0">
|
<h6 className="font-semibold text-sm">
|
||||||
<SqlEditor
|
{translate('::App.Platform.QueryEditor')}
|
||||||
value={state.editorContent}
|
</h6>
|
||||||
onChange={handleEditorChange}
|
</div>
|
||||||
onExecute={handleExecute}
|
<div className="flex-1 min-h-0 overflow-hidden">
|
||||||
onSave={handleSave}
|
<SqlEditor
|
||||||
readOnly={state.isExecuting}
|
ref={editorRef}
|
||||||
/>
|
value={state.editorContent}
|
||||||
</div>
|
onChange={handleEditorChange}
|
||||||
</div>
|
onExecute={handleExecute}
|
||||||
|
onSave={handleSave}
|
||||||
{state.executionResult && (
|
readOnly={state.isExecuting}
|
||||||
<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">
|
</div>
|
||||||
<h6 className="font-semibold text-sm">{translate('::App.Platform.Results')}</h6>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-1 overflow-hidden min-h-0">
|
<div className="border rounded-lg shadow-sm bg-white dark:bg-gray-800 flex flex-col h-full">
|
||||||
<SqlResultsGrid result={state.executionResult} />
|
<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">
|
||||||
|
<HiOutlineCheckCircle 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}
|
||||||
|
onSave={handleSave}
|
||||||
|
readOnly={state.isExecuting}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
@ -698,13 +905,52 @@ GO`
|
||||||
onClose={() => setShowSaveDialog(false)}
|
onClose={() => setShowSaveDialog(false)}
|
||||||
onRequestClose={() => setShowSaveDialog(false)}
|
onRequestClose={() => setShowSaveDialog(false)}
|
||||||
>
|
>
|
||||||
<h5 className="mb-4">{translate('::App.Platform.SaveAsNewQuery')}</h5>
|
<h5 className="mb-4">{translate('::App.Platform.SaveQuery')}</h5>
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
|
{/* Detected Object Type */}
|
||||||
|
{saveDialogData.detectedType && (
|
||||||
|
<div className="p-3 bg-blue-50 dark:bg-blue-900/20 rounded-md border border-blue-200 dark:border-blue-800">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<HiOutlineCheckCircle className="text-blue-600 dark:text-blue-400 text-xl" />
|
||||||
|
<div>
|
||||||
|
<div className="text-sm font-semibold text-blue-900 dark:text-blue-100">
|
||||||
|
{translate('::App.Platform.DetectedObjectType')}
|
||||||
|
</div>
|
||||||
|
<div className="text-sm text-blue-700 dark:text-blue-300">
|
||||||
|
{saveDialogData.detectedType === 'View' && translate('::App.Platform.View')}
|
||||||
|
{saveDialogData.detectedType === 'StoredProcedure' &&
|
||||||
|
translate('::App.Platform.StoredProcedure')}
|
||||||
|
{saveDialogData.detectedType === 'Function' &&
|
||||||
|
translate('::App.Platform.Function')}
|
||||||
|
{saveDialogData.detectedType === 'Query' && translate('::App.Platform.Query')}
|
||||||
|
</div>
|
||||||
|
{saveDialogData.detectedName && (
|
||||||
|
<div className="text-xs text-blue-600 dark:text-blue-400 mt-1">
|
||||||
|
{translate('::App.Platform.DetectedName')}:{' '}
|
||||||
|
<span className="font-mono">{saveDialogData.detectedName}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label className="block mb-2">{translate('::App.Platform.Name')}</label>
|
<label className="block mb-2">
|
||||||
|
{translate('::App.Platform.Name')} <span className="text-red-500">*</span>
|
||||||
|
{saveDialogData.isExistingObject && (
|
||||||
|
<span className="text-xs text-gray-500 ml-2">
|
||||||
|
({translate('::App.Platform.CannotBeChanged')})
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</label>
|
||||||
<Input
|
<Input
|
||||||
|
autoFocus={!saveDialogData.isExistingObject}
|
||||||
value={saveDialogData.name}
|
value={saveDialogData.name}
|
||||||
onChange={(e) => setSaveDialogData((prev) => ({ ...prev, name: e.target.value }))}
|
onChange={(e) => setSaveDialogData((prev) => ({ ...prev, name: e.target.value }))}
|
||||||
|
placeholder={saveDialogData.detectedName || translate('::App.Platform.Name')}
|
||||||
|
invalid={!saveDialogData.name.trim()}
|
||||||
|
disabled={saveDialogData.isExistingObject}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
|
|
@ -714,13 +960,18 @@ GO`
|
||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
setSaveDialogData((prev) => ({ ...prev, description: e.target.value }))
|
setSaveDialogData((prev) => ({ ...prev, description: e.target.value }))
|
||||||
}
|
}
|
||||||
|
placeholder={translate('::App.Platform.Description')}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex justify-end gap-2">
|
<div className="flex justify-end gap-2">
|
||||||
<Button variant="plain" onClick={() => setShowSaveDialog(false)}>
|
<Button variant="plain" onClick={() => setShowSaveDialog(false)}>
|
||||||
{translate('::App.Platform.Cancel')}
|
{translate('::App.Platform.Cancel')}
|
||||||
</Button>
|
</Button>
|
||||||
<Button variant="solid" onClick={handleCreateNewQuery}>
|
<Button
|
||||||
|
variant="solid"
|
||||||
|
onClick={handleCreateNewQuery}
|
||||||
|
disabled={!saveDialogData.name.trim()}
|
||||||
|
>
|
||||||
{translate('::App.Platform.Save')}
|
{translate('::App.Platform.Save')}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { useEffect, useRef } from 'react'
|
import { useEffect, useRef, forwardRef, useImperativeHandle } from 'react'
|
||||||
import Editor, { Monaco } from '@monaco-editor/react'
|
import Editor, { Monaco } from '@monaco-editor/react'
|
||||||
import { useConfig } from '@/components/ui/ConfigProvider'
|
import { useConfig } from '@/components/ui/ConfigProvider'
|
||||||
import type { editor } from 'monaco-editor'
|
import type { editor } from 'monaco-editor'
|
||||||
|
|
@ -12,18 +12,45 @@ interface SqlEditorProps {
|
||||||
height?: string
|
height?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
const SqlEditor = ({
|
export interface SqlEditorRef {
|
||||||
value,
|
getSelectedText: () => string
|
||||||
onChange,
|
preserveSelection: () => any
|
||||||
onExecute,
|
restoreSelection: (selection: any) => void
|
||||||
onSave,
|
}
|
||||||
readOnly = false,
|
|
||||||
height = '100%',
|
const SqlEditor = forwardRef<SqlEditorRef, SqlEditorProps>((
|
||||||
}: SqlEditorProps) => {
|
{
|
||||||
|
value,
|
||||||
|
onChange,
|
||||||
|
onExecute,
|
||||||
|
onSave,
|
||||||
|
readOnly = false,
|
||||||
|
height = '100%',
|
||||||
|
}: SqlEditorProps,
|
||||||
|
ref
|
||||||
|
) => {
|
||||||
const { mode } = useConfig()
|
const { mode } = useConfig()
|
||||||
const editorRef = useRef<editor.IStandaloneCodeEditor | null>(null)
|
const editorRef = useRef<editor.IStandaloneCodeEditor | null>(null)
|
||||||
const monacoRef = useRef<Monaco | null>(null)
|
const monacoRef = useRef<Monaco | null>(null)
|
||||||
|
|
||||||
|
useImperativeHandle(ref, () => ({
|
||||||
|
getSelectedText: () => {
|
||||||
|
if (!editorRef.current) return ''
|
||||||
|
const selection = editorRef.current.getSelection()
|
||||||
|
if (!selection) return ''
|
||||||
|
return editorRef.current.getModel()?.getValueInRange(selection) || ''
|
||||||
|
},
|
||||||
|
preserveSelection: () => {
|
||||||
|
if (!editorRef.current) return null
|
||||||
|
return editorRef.current.getSelection()
|
||||||
|
},
|
||||||
|
restoreSelection: (selection: any) => {
|
||||||
|
if (!editorRef.current || !selection) return
|
||||||
|
editorRef.current.setSelection(selection)
|
||||||
|
editorRef.current.revealRangeInCenter(selection)
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
|
||||||
const handleEditorDidMount = (editor: editor.IStandaloneCodeEditor, monaco: Monaco) => {
|
const handleEditorDidMount = (editor: editor.IStandaloneCodeEditor, monaco: Monaco) => {
|
||||||
editorRef.current = editor
|
editorRef.current = editor
|
||||||
monacoRef.current = monaco
|
monacoRef.current = monaco
|
||||||
|
|
@ -211,6 +238,8 @@ const SqlEditor = ({
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
})
|
||||||
|
|
||||||
|
SqlEditor.displayName = 'SqlEditor'
|
||||||
|
|
||||||
export default SqlEditor
|
export default SqlEditor
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { useState, useEffect, useCallback } from 'react'
|
import { useState, useEffect } from 'react'
|
||||||
import { Dialog, Button, Notification, toast } from '@/components/ui'
|
import { Dialog, Button, Notification, toast } from '@/components/ui'
|
||||||
import {
|
import {
|
||||||
FaRegFolder,
|
FaRegFolder,
|
||||||
|
|
@ -12,7 +12,6 @@ import {
|
||||||
FaTrash,
|
FaTrash,
|
||||||
FaTable,
|
FaTable,
|
||||||
} from 'react-icons/fa'
|
} from 'react-icons/fa'
|
||||||
import { MdViewColumn } from 'react-icons/md'
|
|
||||||
import type { DataSourceDto } from '@/proxy/data-source'
|
import type { DataSourceDto } from '@/proxy/data-source'
|
||||||
import type {
|
import type {
|
||||||
SqlFunctionDto,
|
SqlFunctionDto,
|
||||||
|
|
@ -21,9 +20,7 @@ import type {
|
||||||
SqlViewDto,
|
SqlViewDto,
|
||||||
SqlObjectType,
|
SqlObjectType,
|
||||||
} from '@/proxy/sql-query-manager/models'
|
} from '@/proxy/sql-query-manager/models'
|
||||||
import {
|
import { sqlObjectManagerService } from '@/services/sql-query-manager.service'
|
||||||
sqlObjectManagerService,
|
|
||||||
} from '@/services/sql-query-manager.service'
|
|
||||||
import { useLocalization } from '@/utils/hooks/useLocalization'
|
import { useLocalization } from '@/utils/hooks/useLocalization'
|
||||||
|
|
||||||
export type SqlObject = SqlFunctionDto | SqlQueryDto | SqlStoredProcedureDto | SqlViewDto
|
export type SqlObject = SqlFunctionDto | SqlQueryDto | SqlStoredProcedureDto | SqlViewDto
|
||||||
|
|
@ -45,6 +42,8 @@ interface SqlObjectExplorerProps {
|
||||||
onObjectSelect: (object: SqlObject | null, objectType: SqlObjectType | null) => void
|
onObjectSelect: (object: SqlObject | null, objectType: SqlObjectType | null) => void
|
||||||
selectedObject: SqlObject | null
|
selectedObject: SqlObject | null
|
||||||
onTemplateSelect?: (template: string, templateType: string) => void
|
onTemplateSelect?: (template: string, templateType: string) => void
|
||||||
|
onShowTableColumns?: (schemaName: string, tableName: string) => void
|
||||||
|
refreshTrigger?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
const SqlObjectExplorer = ({
|
const SqlObjectExplorer = ({
|
||||||
|
|
@ -52,6 +51,8 @@ const SqlObjectExplorer = ({
|
||||||
onObjectSelect,
|
onObjectSelect,
|
||||||
selectedObject,
|
selectedObject,
|
||||||
onTemplateSelect,
|
onTemplateSelect,
|
||||||
|
onShowTableColumns,
|
||||||
|
refreshTrigger,
|
||||||
}: SqlObjectExplorerProps) => {
|
}: SqlObjectExplorerProps) => {
|
||||||
const { translate } = useLocalization()
|
const { translate } = useLocalization()
|
||||||
const [treeData, setTreeData] = useState<TreeNode[]>([])
|
const [treeData, setTreeData] = useState<TreeNode[]>([])
|
||||||
|
|
@ -78,7 +79,7 @@ const SqlObjectExplorer = ({
|
||||||
} else {
|
} else {
|
||||||
setTreeData([])
|
setTreeData([])
|
||||||
}
|
}
|
||||||
}, [dataSource])
|
}, [dataSource, refreshTrigger]) // refreshTrigger değişince de yenile
|
||||||
|
|
||||||
const loadObjects = async () => {
|
const loadObjects = async () => {
|
||||||
if (!dataSource) return
|
if (!dataSource) return
|
||||||
|
|
@ -181,19 +182,24 @@ const SqlObjectExplorer = ({
|
||||||
})) || [],
|
})) || [],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'storedProcedures',
|
id: 'procedures',
|
||||||
label: `${translate('::App.Platform.StoredProcedures')} (${allObjects.storedProcedures.length})`,
|
label: `${translate('::App.Platform.StoredProcedures')} (${allObjects.storedProcedures.length})`,
|
||||||
type: 'folder',
|
type: 'folder',
|
||||||
objectType: 2,
|
objectType: 2,
|
||||||
expanded: expandedNodes.has('storedProcedures'),
|
expanded: expandedNodes.has('procedures'),
|
||||||
children:
|
children:
|
||||||
allObjects.storedProcedures.map((sp) => ({
|
allObjects.storedProcedures.map((p) => {
|
||||||
id: sp.id || '',
|
const deployInfo = p.isDeployed && p.lastDeployedAt
|
||||||
label: sp.displayName || sp.procedureName,
|
? ` (${new Date(p.lastDeployedAt).toLocaleString('tr-TR', { day: '2-digit', month: '2-digit', year: 'numeric', hour: '2-digit', minute: '2-digit' })})`
|
||||||
type: 'object' as const,
|
: '';
|
||||||
objectType: 2 as SqlObjectType,
|
return {
|
||||||
data: sp,
|
id: p.id || '',
|
||||||
})) || [],
|
label: `${p.displayName || p.procedureName}${p.isDeployed ? ' ✅' : ' ❌'}${deployInfo}`,
|
||||||
|
type: 'object' as const,
|
||||||
|
objectType: 2 as SqlObjectType,
|
||||||
|
data: p,
|
||||||
|
};
|
||||||
|
}) || [],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'views',
|
id: 'views',
|
||||||
|
|
@ -202,13 +208,18 @@ const SqlObjectExplorer = ({
|
||||||
objectType: 3,
|
objectType: 3,
|
||||||
expanded: expandedNodes.has('views'),
|
expanded: expandedNodes.has('views'),
|
||||||
children:
|
children:
|
||||||
allObjects.views.map((v) => ({
|
allObjects.views.map((v) => {
|
||||||
id: v.id || '',
|
const deployInfo = v.isDeployed && v.lastDeployedAt
|
||||||
label: v.displayName || v.viewName,
|
? ` (${new Date(v.lastDeployedAt).toLocaleString('tr-TR', { day: '2-digit', month: '2-digit', year: 'numeric', hour: '2-digit', minute: '2-digit' })})`
|
||||||
type: 'object' as const,
|
: '';
|
||||||
objectType: 3 as SqlObjectType,
|
return {
|
||||||
data: v,
|
id: v.id || '',
|
||||||
})) || [],
|
label: `${v.displayName || v.viewName}${v.isDeployed ? ' ✅' : ' ❌'}${deployInfo}`,
|
||||||
|
type: 'object' as const,
|
||||||
|
objectType: 3 as SqlObjectType,
|
||||||
|
data: v,
|
||||||
|
};
|
||||||
|
}) || [],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'functions',
|
id: 'functions',
|
||||||
|
|
@ -217,13 +228,18 @@ const SqlObjectExplorer = ({
|
||||||
objectType: 4,
|
objectType: 4,
|
||||||
expanded: expandedNodes.has('functions'),
|
expanded: expandedNodes.has('functions'),
|
||||||
children:
|
children:
|
||||||
allObjects.functions.map((f) => ({
|
allObjects.functions.map((f) => {
|
||||||
id: f.id || '',
|
const deployInfo = f.isDeployed && f.lastDeployedAt
|
||||||
label: f.displayName || f.functionName,
|
? ` (${new Date(f.lastDeployedAt).toLocaleString('tr-TR', { day: '2-digit', month: '2-digit', year: 'numeric', hour: '2-digit', minute: '2-digit' })})`
|
||||||
type: 'object' as const,
|
: '';
|
||||||
objectType: 4 as SqlObjectType,
|
return {
|
||||||
data: f,
|
id: f.id || '',
|
||||||
})) || [],
|
label: `${f.displayName || f.functionName}${f.isDeployed ? ' ✅' : ' ❌'}${deployInfo}`,
|
||||||
|
type: 'object' as const,
|
||||||
|
objectType: 4 as SqlObjectType,
|
||||||
|
data: f,
|
||||||
|
};
|
||||||
|
}) || [],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|
@ -299,7 +315,11 @@ const SqlObjectExplorer = ({
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
const updateNodeChildren = (nodes: TreeNode[], nodeId: string, children: TreeNode[]): TreeNode[] => {
|
const updateNodeChildren = (
|
||||||
|
nodes: TreeNode[],
|
||||||
|
nodeId: string,
|
||||||
|
children: TreeNode[],
|
||||||
|
): TreeNode[] => {
|
||||||
return nodes.map((node) => {
|
return nodes.map((node) => {
|
||||||
if (node.id === nodeId) {
|
if (node.id === nodeId) {
|
||||||
return { ...node, children }
|
return { ...node, children }
|
||||||
|
|
@ -350,8 +370,7 @@ const SqlObjectExplorer = ({
|
||||||
const table = node.data as any
|
const table = node.data as any
|
||||||
const selectQuery = `-- SELECT from ${table.fullName || table.tableName}\nSELECT * \nFROM ${table.fullName || `[${table.schemaName}].[${table.tableName}]`}\nWHERE 1=1;`
|
const selectQuery = `-- SELECT from ${table.fullName || table.tableName}\nSELECT * \nFROM ${table.fullName || `[${table.schemaName}].[${table.tableName}]`}\nWHERE 1=1;`
|
||||||
onTemplateSelect(selectQuery, 'table-select')
|
onTemplateSelect(selectQuery, 'table-select')
|
||||||
}
|
} else if (node.objectType) {
|
||||||
else if (node.objectType) {
|
|
||||||
onObjectSelect(node.data, node.objectType)
|
onObjectSelect(node.data, node.objectType)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -360,8 +379,8 @@ const SqlObjectExplorer = ({
|
||||||
const handleContextMenu = (e: React.MouseEvent, node: TreeNode) => {
|
const handleContextMenu = (e: React.MouseEvent, node: TreeNode) => {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
|
|
||||||
// Don't show context menu for columns, templates, or tables
|
// Don't show context menu for columns or templates
|
||||||
if (node.type === 'column' || node.id.startsWith('template-') || node.id.startsWith('table-')) {
|
if (node.type === 'column' || node.id.startsWith('template-')) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -487,7 +506,7 @@ const SqlObjectExplorer = ({
|
||||||
}
|
}
|
||||||
|
|
||||||
if (node.type === 'column') {
|
if (node.type === 'column') {
|
||||||
return <MdViewColumn className="text-gray-400 text-sm" />
|
return <FaColumns className="text-gray-400 text-sm" />
|
||||||
}
|
}
|
||||||
|
|
||||||
return <FaRegFolder />
|
return <FaRegFolder />
|
||||||
|
|
@ -509,7 +528,9 @@ const SqlObjectExplorer = ({
|
||||||
onContextMenu={(e) => !isColumn && handleContextMenu(e, node)}
|
onContextMenu={(e) => !isColumn && handleContextMenu(e, node)}
|
||||||
>
|
>
|
||||||
{getIcon(node)}
|
{getIcon(node)}
|
||||||
<span className={`text-sm flex-1 ${isColumn ? 'text-gray-600 dark:text-gray-400' : ''}`}>{node.label}</span>
|
<span className={`text-sm flex-1 ${isColumn ? 'text-gray-600 dark:text-gray-400' : ''}`}>
|
||||||
|
{node.label}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{isExpanded && node.children && (
|
{isExpanded && node.children && (
|
||||||
|
|
@ -545,8 +566,12 @@ const SqlObjectExplorer = ({
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Tree Content */}
|
{/* Tree Content */}
|
||||||
<div className="flex-1 overflow-auto">
|
<div className="h-[calc(100vh-265px)] overflow-auto">
|
||||||
{loading && <div className="text-center py-8 text-gray-500">{translate('::App.Platform.Loading')}</div>}
|
{loading && (
|
||||||
|
<div className="text-center py-8 text-gray-500">
|
||||||
|
{translate('::App.Platform.Loading')}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
{!loading && treeData.length === 0 && (
|
{!loading && treeData.length === 0 && (
|
||||||
<div className="text-center py-8 text-gray-500">
|
<div className="text-center py-8 text-gray-500">
|
||||||
{translate('::App.Platform.NoDataSourceSelected')}
|
{translate('::App.Platform.NoDataSourceSelected')}
|
||||||
|
|
@ -568,57 +593,75 @@ const SqlObjectExplorer = ({
|
||||||
className="fixed inset-0 z-40"
|
className="fixed inset-0 z-40"
|
||||||
onClick={() => setContextMenu({ show: false, x: 0, y: 0, node: null })}
|
onClick={() => setContextMenu({ show: false, x: 0, y: 0, node: null })}
|
||||||
/>
|
/>
|
||||||
<div
|
<div
|
||||||
className="fixed z-50 bg-white dark:bg-gray-800 shadow-lg rounded border border-gray-200 dark:border-gray-700 py-1"
|
className="fixed z-50 bg-white dark:bg-gray-800 shadow-lg rounded border border-gray-200 dark:border-gray-700 py-1"
|
||||||
style={{ top: contextMenu.y, left: contextMenu.x }}
|
style={{ top: contextMenu.y, left: contextMenu.x }}
|
||||||
>
|
>
|
||||||
{contextMenu.node?.type === 'object' && (
|
{contextMenu.node?.type === 'object' && !contextMenu.node?.id?.startsWith('table-') && (
|
||||||
<>
|
<>
|
||||||
|
<button
|
||||||
|
className="w-full px-4 py-2 text-left hover:bg-gray-100 dark:hover:bg-gray-700 text-sm"
|
||||||
|
onClick={() => {
|
||||||
|
if (contextMenu.node?.data && contextMenu.node?.objectType) {
|
||||||
|
onObjectSelect(contextMenu.node.data, contextMenu.node.objectType)
|
||||||
|
}
|
||||||
|
setContextMenu({ show: false, x: 0, y: 0, node: null })
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<FaEdit className="inline mr-2" />
|
||||||
|
{translate('::App.Platform.Edit')}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
className="w-full px-4 py-2 text-left hover:bg-gray-100 dark:hover:bg-gray-700 text-sm text-red-600"
|
||||||
|
onClick={() => {
|
||||||
|
if (contextMenu.node?.data && contextMenu.node?.objectType) {
|
||||||
|
setObjectToDelete({
|
||||||
|
object: contextMenu.node.data,
|
||||||
|
type: contextMenu.node.objectType,
|
||||||
|
})
|
||||||
|
setShowDeleteDialog(true)
|
||||||
|
}
|
||||||
|
setContextMenu({ show: false, x: 0, y: 0, node: null })
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<FaTrash className="inline mr-2" />
|
||||||
|
{translate('::App.Platform.Delete')}
|
||||||
|
</button>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{contextMenu.node?.type === 'folder' && (
|
||||||
<button
|
<button
|
||||||
className="w-full px-4 py-2 text-left hover:bg-gray-100 dark:hover:bg-gray-700 text-sm"
|
className="w-full px-4 py-2 text-left hover:bg-gray-100 dark:hover:bg-gray-700 text-sm"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (contextMenu.node?.data && contextMenu.node?.objectType) {
|
loadObjects()
|
||||||
onObjectSelect(contextMenu.node.data, contextMenu.node.objectType)
|
|
||||||
}
|
|
||||||
setContextMenu({ show: false, x: 0, y: 0, node: null })
|
setContextMenu({ show: false, x: 0, y: 0, node: null })
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<FaEdit className="inline mr-2" />
|
<FaSyncAlt className="inline mr-2" />
|
||||||
{translate('::App.Platform.Edit')}
|
{translate('::App.Platform.Refresh')}
|
||||||
</button>
|
</button>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{contextMenu.node?.id?.startsWith('table-') && contextMenu.node?.data && (
|
||||||
<button
|
<button
|
||||||
className="w-full px-4 py-2 text-left hover:bg-gray-100 dark:hover:bg-gray-700 text-sm text-red-600"
|
className="w-full px-4 py-2 text-left hover:bg-gray-100 dark:hover:bg-gray-700 text-sm"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (contextMenu.node?.data && contextMenu.node?.objectType) {
|
if (onShowTableColumns && contextMenu.node?.data) {
|
||||||
setObjectToDelete({
|
onShowTableColumns(
|
||||||
object: contextMenu.node.data,
|
contextMenu.node.data.schemaName,
|
||||||
type: contextMenu.node.objectType,
|
contextMenu.node.data.tableName
|
||||||
})
|
)
|
||||||
setShowDeleteDialog(true)
|
|
||||||
}
|
}
|
||||||
setContextMenu({ show: false, x: 0, y: 0, node: null })
|
setContextMenu({ show: false, x: 0, y: 0, node: null })
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<FaTrash className="inline mr-2" />
|
<FaColumns className="inline mr-2" />
|
||||||
{translate('::App.Platform.Delete')}
|
{translate('::App.Platform.ShowColumns')}
|
||||||
</button>
|
</button>
|
||||||
</>
|
)}
|
||||||
)}
|
</div>
|
||||||
|
|
||||||
{contextMenu.node?.type === 'folder' && (
|
|
||||||
<button
|
|
||||||
className="w-full px-4 py-2 text-left hover:bg-gray-100 dark:hover:bg-gray-700 text-sm"
|
|
||||||
onClick={() => {
|
|
||||||
loadObjects()
|
|
||||||
setContextMenu({ show: false, x: 0, y: 0, node: null })
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<FaSyncAlt className="inline mr-2" />
|
|
||||||
{translate('::App.Platform.Refresh')}
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -55,24 +55,6 @@ const SqlResultsGrid = ({ result }: SqlResultsGridProps) => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="h-full flex flex-col">
|
<div className="h-full flex flex-col">
|
||||||
|
|
||||||
<div className="flex items-center justify-between mb-2 p-2 bg-green-50 dark:bg-green-900/20 border border-green-200 dark:border-green-800 rounded">
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<HiOutlineCheckCircle className="text-green-500" />
|
|
||||||
<span className="text-sm text-green-700 dark:text-green-400">
|
|
||||||
{result.message || translate('::App.Platform.QueryExecutedSuccessfully')}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center gap-4 text-sm text-gray-600 dark:text-gray-400">
|
|
||||||
<span>
|
|
||||||
{translate('::App.Platform.Rows')}: <strong>{result.rowsAffected || dataSource.length}</strong>
|
|
||||||
</span>
|
|
||||||
<span>
|
|
||||||
{translate('::App.Platform.Time')}: <strong>{result.executionTimeMs}ms</strong>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{dataSource.length > 0 ? (
|
{dataSource.length > 0 ? (
|
||||||
<div className="flex-1 overflow-hidden relative">
|
<div className="flex-1 overflow-hidden relative">
|
||||||
<div className="absolute inset-0">
|
<div className="absolute inset-0">
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue