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
|
||||
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.Services;
|
||||
using Erp.SqlQueryManager.Domain.Shared;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Volo.Abp.Application.Services;
|
||||
using Volo.Abp.Domain.Repositories;
|
||||
|
||||
|
|
@ -16,6 +16,7 @@ namespace Erp.SqlQueryManager.Application;
|
|||
/// Unified service for SQL Object Explorer
|
||||
/// Combines all SQL objects into a single endpoint
|
||||
/// </summary>
|
||||
[Authorize("App.SqlQueryManager")]
|
||||
public class SqlObjectManagerAppService : ApplicationService, ISqlObjectManagerAppService
|
||||
{
|
||||
private readonly IRepository<SqlQuery, Guid> _queryRepository;
|
||||
|
|
@ -213,6 +214,50 @@ public class SqlObjectManagerAppService : ApplicationService, ISqlObjectManagerA
|
|||
|
||||
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);
|
||||
return MapExecutionResult(result);
|
||||
}
|
||||
|
|
@ -242,6 +287,8 @@ public class SqlObjectManagerAppService : ApplicationService, ISqlObjectManagerA
|
|||
procedure.Description = input.Description;
|
||||
procedure.ProcedureBody = input.ProcedureBody;
|
||||
procedure.Category = input.Category;
|
||||
procedure.IsDeployed = false;
|
||||
procedure.LastDeployedAt = null;
|
||||
|
||||
var updated = await _procedureRepository.UpdateAsync(procedure, autoSave: true);
|
||||
return ObjectMapper.Map<SqlStoredProcedure, SqlStoredProcedureDto>(updated);
|
||||
|
|
@ -249,12 +296,33 @@ public class SqlObjectManagerAppService : ApplicationService, ISqlObjectManagerA
|
|||
|
||||
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);
|
||||
}
|
||||
|
||||
public async Task<SqlQueryExecutionResultDto> DeployStoredProcedureAsync(DeployStoredProcedureDto input)
|
||||
{
|
||||
var procedure = await _procedureRepository.GetAsync(input.Id);
|
||||
|
||||
try
|
||||
{
|
||||
// Önce DROP işlemi yap (varsa)
|
||||
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);
|
||||
|
||||
// Sonra CREATE işlemi yap
|
||||
var result = await _sqlExecutorService.DeployStoredProcedureAsync(
|
||||
procedure.ProcedureBody,
|
||||
procedure.DataSourceCode
|
||||
|
|
@ -269,6 +337,18 @@ public class SqlObjectManagerAppService : ApplicationService, ISqlObjectManagerA
|
|||
|
||||
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
|
||||
|
||||
|
|
@ -282,6 +362,8 @@ public class SqlObjectManagerAppService : ApplicationService, ISqlObjectManagerA
|
|||
view.Description = input.Description;
|
||||
view.ViewDefinition = input.ViewDefinition;
|
||||
view.Category = input.Category;
|
||||
view.IsDeployed = false;
|
||||
view.LastDeployedAt = null;
|
||||
|
||||
var updated = await _viewRepository.UpdateAsync(view, autoSave: true);
|
||||
return ObjectMapper.Map<SqlView, SqlViewDto>(updated);
|
||||
|
|
@ -289,12 +371,33 @@ public class SqlObjectManagerAppService : ApplicationService, ISqlObjectManagerA
|
|||
|
||||
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);
|
||||
}
|
||||
|
||||
public async Task<SqlQueryExecutionResultDto> DeployViewAsync(DeployViewDto input)
|
||||
{
|
||||
var view = await _viewRepository.GetAsync(input.Id);
|
||||
|
||||
try
|
||||
{
|
||||
// Önce DROP işlemi yap (varsa)
|
||||
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);
|
||||
|
||||
// Sonra CREATE işlemi yap
|
||||
var result = await _sqlExecutorService.DeployViewAsync(
|
||||
view.ViewDefinition,
|
||||
view.DataSourceCode
|
||||
|
|
@ -309,6 +412,18 @@ public class SqlObjectManagerAppService : ApplicationService, ISqlObjectManagerA
|
|||
|
||||
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
|
||||
|
||||
|
|
@ -322,6 +437,8 @@ public class SqlObjectManagerAppService : ApplicationService, ISqlObjectManagerA
|
|||
function.Description = input.Description;
|
||||
function.FunctionBody = input.FunctionBody;
|
||||
function.Category = input.Category;
|
||||
function.IsDeployed = false;
|
||||
function.LastDeployedAt = null;
|
||||
|
||||
var updated = await _functionRepository.UpdateAsync(function, autoSave: true);
|
||||
return ObjectMapper.Map<SqlFunction, SqlFunctionDto>(updated);
|
||||
|
|
@ -329,12 +446,33 @@ public class SqlObjectManagerAppService : ApplicationService, ISqlObjectManagerA
|
|||
|
||||
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);
|
||||
}
|
||||
|
||||
public async Task<SqlQueryExecutionResultDto> DeployFunctionAsync(DeployFunctionDto input)
|
||||
{
|
||||
var function = await _functionRepository.GetAsync(input.Id);
|
||||
|
||||
try
|
||||
{
|
||||
// Önce DROP işlemi yap (varsa)
|
||||
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);
|
||||
|
||||
// Sonra CREATE işlemi yap
|
||||
var result = await _sqlExecutorService.DeployFunctionAsync(
|
||||
function.FunctionBody,
|
||||
function.DataSourceCode
|
||||
|
|
@ -349,6 +487,18 @@ public class SqlObjectManagerAppService : ApplicationService, ISqlObjectManagerA
|
|||
|
||||
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
|
||||
|
||||
|
|
@ -398,6 +548,264 @@ public class SqlObjectManagerAppService : ApplicationService, ISqlObjectManagerA
|
|||
|
||||
#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
|
||||
|
||||
private SqlQueryExecutionResultDto MapExecutionResult(SqlExecutionResult result)
|
||||
|
|
|
|||
|
|
@ -80,7 +80,7 @@ public class SqlExecutorService : DomainService, ISqlExecutorService
|
|||
result.Data = data;
|
||||
result.RowsAffected = data?.Count() ?? 0;
|
||||
result.ExecutionTimeMs = stopwatch.ElapsedMilliseconds;
|
||||
result.Message = $"Query executed successfully. Rows returned: {result.RowsAffected}";
|
||||
result.Message = $"Query executed successfully.";
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -10363,6 +10363,12 @@
|
|||
"tr": "Kaydet",
|
||||
"en": "Save"
|
||||
},
|
||||
{
|
||||
"resourceName": "Platform",
|
||||
"key": "App.Platform.SaveQuery",
|
||||
"tr": "Kaydet Sorgu",
|
||||
"en": "Save Query"
|
||||
},
|
||||
{
|
||||
"resourceName": "Platform",
|
||||
"key": "App.Platform.Deploy",
|
||||
|
|
@ -10443,10 +10449,22 @@
|
|||
},
|
||||
{
|
||||
"resourceName": "Platform",
|
||||
"key": "App.Platform.QuerySavedSuccessfully",
|
||||
"key": "App.Platform.SavedSuccessfully",
|
||||
"tr": "Sorgu başarıyla kaydedildi",
|
||||
"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",
|
||||
"key": "App.Platform.FailedToSaveQuery",
|
||||
|
|
@ -10495,6 +10513,18 @@
|
|||
"tr": "Özellikleri görüntülemek için bir nesne seçin",
|
||||
"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",
|
||||
"key": "App.Platform.Query",
|
||||
|
|
@ -10807,6 +10837,12 @@
|
|||
"tr": "Nesne Gezgini",
|
||||
"en": "Object Explorer"
|
||||
},
|
||||
{
|
||||
"resourceName": "Platform",
|
||||
"key": "App.Platform.ShowColumns",
|
||||
"tr": "Sütunları Göster",
|
||||
"en": "Show Columns"
|
||||
},
|
||||
{
|
||||
"resourceName": "Platform",
|
||||
"key": "App.Platform.QueryEditor",
|
||||
|
|
|
|||
|
|
@ -1,2 +1 @@
|
|||
export * from './data-source.service'
|
||||
export * from './models'
|
||||
|
|
|
|||
|
|
@ -60,7 +60,7 @@ export class SqlObjectManagerService {
|
|||
apiService.fetchData<void, void>(
|
||||
{
|
||||
method: 'DELETE',
|
||||
url: `/api/app/sql-object-manager/query/${id}`,
|
||||
url: `/api/app/sql-object-manager/${id}/query`,
|
||||
},
|
||||
{ apiName: this.apiName, ...config },
|
||||
)
|
||||
|
|
@ -99,7 +99,7 @@ export class SqlObjectManagerService {
|
|||
apiService.fetchData<void, void>(
|
||||
{
|
||||
method: 'DELETE',
|
||||
url: `/api/app/sql-object-manager/stored-procedure/${id}`,
|
||||
url: `/api/app/sql-object-manager/${id}/stored-procedure`,
|
||||
},
|
||||
{ apiName: this.apiName, ...config },
|
||||
)
|
||||
|
|
@ -129,7 +129,7 @@ export class SqlObjectManagerService {
|
|||
apiService.fetchData<void, void>(
|
||||
{
|
||||
method: 'DELETE',
|
||||
url: `/api/app/sql-object-manager/view/${id}`,
|
||||
url: `/api/app/sql-object-manager/${id}/view`,
|
||||
},
|
||||
{ apiName: this.apiName, ...config },
|
||||
)
|
||||
|
|
@ -159,7 +159,7 @@ export class SqlObjectManagerService {
|
|||
apiService.fetchData<void, void>(
|
||||
{
|
||||
method: 'DELETE',
|
||||
url: `/api/app/sql-object-manager/function/${id}`,
|
||||
url: `/api/app/sql-object-manager/${id}/function`,
|
||||
},
|
||||
{ apiName: this.apiName, ...config },
|
||||
)
|
||||
|
|
@ -184,6 +184,17 @@ export class SqlObjectManagerService {
|
|||
},
|
||||
{ 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
|
||||
|
|
|
|||
|
|
@ -10,7 +10,6 @@ import {
|
|||
} from '@/components/ui'
|
||||
import { ROUTES_ENUM } from '@/routes/route.constant'
|
||||
import { ListFormWizardDto } from '@/proxy/admin/list-form/models'
|
||||
import { getDataSources } from '@/proxy/data-source'
|
||||
import { SelectBoxOption } from '@/types/shared'
|
||||
import { useLocalization } from '@/utils/hooks/useLocalization'
|
||||
import { Field, FieldProps, Form, Formik } from 'formik'
|
||||
|
|
@ -24,6 +23,7 @@ import { getMenus } from '@/services/menu.service'
|
|||
import { getPermissions } from '@/services/identity.service'
|
||||
import { DbTypeEnum, SelectCommandTypeEnum } from '@/proxy/form/models'
|
||||
import { postListFormWizard } from '@/services/admin/list-form.service'
|
||||
import { getDataSources } from '@/services/data-source.service'
|
||||
|
||||
const initialValues: ListFormWizardDto = {
|
||||
listFormCode: '',
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
import { Container } from '@/components/shared'
|
||||
import { Button, Card, Checkbox, FormContainer, FormItem, Input, Select } from '@/components/ui'
|
||||
import { ListFormEditTabs } from '@/proxy/admin/list-form/options'
|
||||
import { getDataSources } from '@/proxy/data-source'
|
||||
import { SelectBoxOption } from '@/types/shared'
|
||||
import { useStoreState } from '@/store'
|
||||
import { useLocalization } from '@/utils/hooks/useLocalization'
|
||||
|
|
@ -11,6 +10,7 @@ import * as Yup from 'yup'
|
|||
import { FormEditProps } from './FormEdit'
|
||||
import { dbSourceTypeOptions, selectCommandTypeOptions } from './options'
|
||||
import { DataSourceTypeEnum, SelectCommandTypeEnum } from '@/proxy/form/models'
|
||||
import { getDataSources } from '@/services/data-source.service'
|
||||
|
||||
const schema = Yup.object().shape({
|
||||
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 Container from '@/components/shared/Container'
|
||||
import AdaptableCard from '@/components/shared/AdaptableCard'
|
||||
|
|
@ -9,19 +9,19 @@ import type {
|
|||
SqlQueryDto,
|
||||
SqlStoredProcedureDto,
|
||||
SqlViewDto,
|
||||
SqlObjectType,
|
||||
SqlQueryExecutionResultDto,
|
||||
} from '@/proxy/sql-query-manager/models'
|
||||
import {
|
||||
sqlObjectManagerService,
|
||||
} from '@/services/sql-query-manager.service'
|
||||
import { FaDatabase, FaPlay, FaSave, FaSyncAlt } from 'react-icons/fa'
|
||||
import { SqlObjectType } from '@/proxy/sql-query-manager/models'
|
||||
import { sqlObjectManagerService } from '@/services/sql-query-manager.service'
|
||||
import { FaDatabase, FaPlay, FaSave, FaSyncAlt, FaCloudUploadAlt } from 'react-icons/fa'
|
||||
import { HiOutlineCheckCircle } from 'react-icons/hi'
|
||||
import { useLocalization } from '@/utils/hooks/useLocalization'
|
||||
import SqlObjectExplorer from './components/SqlObjectExplorer'
|
||||
import SqlEditor from './components/SqlEditor'
|
||||
import SqlEditor, { SqlEditorRef } from './components/SqlEditor'
|
||||
import SqlResultsGrid from './components/SqlResultsGrid'
|
||||
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
|
||||
|
||||
|
|
@ -35,10 +35,14 @@ interface SqlManagerState {
|
|||
executionResult: SqlQueryExecutionResultDto | null
|
||||
showProperties: boolean
|
||||
isDirty: boolean
|
||||
tableColumns: any | null
|
||||
isSaved: boolean
|
||||
refreshTrigger: number
|
||||
}
|
||||
|
||||
const SqlQueryManager = () => {
|
||||
const { translate } = useLocalization()
|
||||
const editorRef = useRef<SqlEditorRef>(null)
|
||||
|
||||
const [state, setState] = useState<SqlManagerState>({
|
||||
dataSources: [],
|
||||
|
|
@ -47,15 +51,26 @@ const SqlQueryManager = () => {
|
|||
selectedObjectType: null,
|
||||
editorContent: '',
|
||||
isExecuting: false,
|
||||
refreshTrigger: 0,
|
||||
executionResult: null,
|
||||
showProperties: false,
|
||||
isDirty: false,
|
||||
tableColumns: null,
|
||||
isSaved: 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 [pendingTemplate, setPendingTemplate] = useState<{ content: string; type: string } | null>(null)
|
||||
const [pendingTemplate, setPendingTemplate] = useState<{ content: string; type: string } | null>(
|
||||
null,
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
loadDataSources()
|
||||
|
|
@ -124,7 +139,9 @@ const SqlQueryManager = () => {
|
|||
selectedObjectType: objectType,
|
||||
editorContent: content,
|
||||
executionResult: null,
|
||||
tableColumns: null,
|
||||
isDirty: false,
|
||||
isSaved: false,
|
||||
}))
|
||||
},
|
||||
[state.isDirty, translate],
|
||||
|
|
@ -135,12 +152,13 @@ const SqlQueryManager = () => {
|
|||
...prev,
|
||||
editorContent: value || '',
|
||||
isDirty: true,
|
||||
isSaved: false,
|
||||
}))
|
||||
}, [])
|
||||
|
||||
const getTemplateContent = (templateType: string): string => {
|
||||
const templates: Record<string, string> = {
|
||||
'select': `-- Basic SELECT query
|
||||
select: `-- Basic SELECT query
|
||||
SELECT
|
||||
Column1,
|
||||
Column2,
|
||||
|
|
@ -154,12 +172,12 @@ WHERE
|
|||
ORDER BY
|
||||
Column1 ASC;`,
|
||||
|
||||
'insert': `-- Basic INSERT query
|
||||
insert: `-- Basic INSERT query
|
||||
INSERT INTO TableName (Column1, Column2, Column3)
|
||||
VALUES
|
||||
('Value1', 'Value2', 'Value3');`,
|
||||
|
||||
'update': `-- Basic UPDATE query
|
||||
update: `-- Basic UPDATE query
|
||||
UPDATE TableName
|
||||
SET
|
||||
Column1 = 'NewValue1',
|
||||
|
|
@ -168,7 +186,7 @@ WHERE
|
|||
-- Add your conditions
|
||||
Id = 1;`,
|
||||
|
||||
'delete': `-- Basic DELETE query
|
||||
delete: `-- Basic DELETE query
|
||||
DELETE FROM TableName
|
||||
WHERE
|
||||
-- Add your conditions
|
||||
|
|
@ -250,13 +268,64 @@ RETURN
|
|||
AND t.Column2 LIKE '%' + @Parameter2 + '%'
|
||||
AND t.IsActive = 1
|
||||
)
|
||||
GO`
|
||||
GO`,
|
||||
}
|
||||
|
||||
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
|
||||
const detectSqlObject = (sql: string): { type: string; name: string } => {
|
||||
const upperSql = sql.trim().toUpperCase()
|
||||
|
||||
// VIEW tespiti
|
||||
if (upperSql.includes('CREATE VIEW') || upperSql.includes('ALTER VIEW')) {
|
||||
// Son kelimeyi al (schema varsa sonraki kelime, yoksa ilk kelime)
|
||||
const viewMatch = sql.match(
|
||||
/(?:CREATE|ALTER)\s+VIEW\s+(?:[\[\]]*\w+[\[\]]*\.)?\s*[\[]?(\w+)[\]]?/i,
|
||||
)
|
||||
return {
|
||||
type: 'View',
|
||||
name: viewMatch ? viewMatch[1] : '',
|
||||
}
|
||||
}
|
||||
|
||||
// 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,
|
||||
|
|
@ -265,16 +334,12 @@ GO`
|
|||
executionResult: null,
|
||||
isDirty: false,
|
||||
}))
|
||||
|
||||
toast.push(
|
||||
<Notification type="success" title={translate('::App.Platform.Success')}>
|
||||
{translate('::App.Platform.TemplateLoaded')}
|
||||
</Notification>,
|
||||
{ placement: 'top-center' },
|
||||
},
|
||||
[translate],
|
||||
)
|
||||
}, [translate])
|
||||
|
||||
const handleTemplateSelect = useCallback((template: string, templateType: string) => {
|
||||
const handleTemplateSelect = useCallback(
|
||||
(template: string, templateType: string) => {
|
||||
// If template is already provided (e.g., from table click), use it
|
||||
const templateContent = template || getTemplateContent(templateType)
|
||||
|
||||
|
|
@ -289,7 +354,9 @@ GO`
|
|||
// Apply template directly
|
||||
applyTemplate(templateContent)
|
||||
}
|
||||
}, [translate, state.editorContent, state.isDirty, applyTemplate])
|
||||
},
|
||||
[translate, state.editorContent, state.isDirty, applyTemplate],
|
||||
)
|
||||
|
||||
const handleConfirmTemplateReplace = useCallback(() => {
|
||||
if (pendingTemplate) {
|
||||
|
|
@ -315,7 +382,11 @@ GO`
|
|||
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(
|
||||
<Notification type="warning" title={translate('::App.Platform.Warning')}>
|
||||
{translate('::App.Platform.PleaseEnterQuery')}
|
||||
|
|
@ -325,24 +396,40 @@ GO`
|
|||
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 {
|
||||
const result = await sqlObjectManagerService.executeQuery({
|
||||
queryText: state.editorContent,
|
||||
queryText: queryToExecute,
|
||||
dataSourceCode: state.selectedDataSource.code || '',
|
||||
})
|
||||
|
||||
setState((prev) => ({ ...prev, executionResult: result.data, isExecuting: false }))
|
||||
setState((prev) => ({
|
||||
...prev,
|
||||
executionResult: result.data,
|
||||
isExecuting: false,
|
||||
tableColumns: null,
|
||||
}))
|
||||
|
||||
toast.push(
|
||||
<Notification type="success" title={translate('::App.Platform.Success')}>
|
||||
{translate('::App.Platform.QueryExecutedSuccessfully')} ({result.data.executionTimeMs}ms)
|
||||
</Notification>,
|
||||
{ placement: 'top-center' },
|
||||
)
|
||||
// Seçili metni geri yükle
|
||||
setTimeout(() => {
|
||||
if (savedSelection) {
|
||||
editorRef.current?.restoreSelection(savedSelection)
|
||||
}
|
||||
}, 100)
|
||||
} catch (error: any) {
|
||||
setState((prev) => ({ ...prev, isExecuting: false }))
|
||||
|
||||
// Hata durumunda da seçili metni geri yükle
|
||||
setTimeout(() => {
|
||||
if (savedSelection) {
|
||||
editorRef.current?.restoreSelection(savedSelection)
|
||||
}
|
||||
}, 100)
|
||||
|
||||
toast.push(
|
||||
<Notification type="danger" title={translate('::App.Platform.Error')}>
|
||||
{error.response?.data?.error?.message || translate('::App.Platform.FailedToExecuteQuery')}
|
||||
|
|
@ -374,74 +461,45 @@ GO`
|
|||
}
|
||||
|
||||
if (state.selectedObject && state.selectedObjectType) {
|
||||
// Update existing object
|
||||
await handleUpdate()
|
||||
} else {
|
||||
// Create new object - show dialog to choose type
|
||||
setSaveDialogData({ name: '', description: '' })
|
||||
// Update existing object - open dialog with existing data
|
||||
const typeMap: Record<SqlObjectType, string> = {
|
||||
[SqlObjectType.Query]: 'Query',
|
||||
[SqlObjectType.View]: 'View',
|
||||
[SqlObjectType.StoredProcedure]: 'StoredProcedure',
|
||||
[SqlObjectType.Function]: 'Function',
|
||||
}
|
||||
|
||||
// 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 || ''
|
||||
}
|
||||
|
||||
setSaveDialogData({
|
||||
name: objectName,
|
||||
description: state.selectedObject.description || '',
|
||||
detectedType: typeMap[state.selectedObjectType] || '',
|
||||
detectedName: objectName,
|
||||
isExistingObject: true,
|
||||
})
|
||||
setShowSaveDialog(true)
|
||||
}
|
||||
}
|
||||
|
||||
const handleUpdate = async () => {
|
||||
if (!state.selectedObject || !state.selectedObjectType || !state.selectedDataSource) return
|
||||
if (!state.selectedObject.id) return
|
||||
|
||||
try {
|
||||
const objectId = state.selectedObject.id
|
||||
|
||||
switch (state.selectedObjectType) {
|
||||
case 1: // Query
|
||||
await sqlObjectManagerService.updateQuery(objectId, {
|
||||
...(state.selectedObject as SqlQueryDto),
|
||||
queryText: state.editorContent,
|
||||
} else {
|
||||
// New object - analyze SQL and show dialog with detection
|
||||
const detection = detectSqlObject(state.editorContent)
|
||||
setSaveDialogData({
|
||||
name: detection.name,
|
||||
description: '',
|
||||
detectedType: detection.type,
|
||||
detectedName: detection.name,
|
||||
isExistingObject: false,
|
||||
})
|
||||
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 }))
|
||||
|
||||
toast.push(
|
||||
<Notification type="success" title={translate('::App.Platform.Success')}>
|
||||
{translate('::App.Platform.ObjectUpdatedSuccessfully')}
|
||||
</Notification>,
|
||||
{ placement: 'top-center' },
|
||||
)
|
||||
} catch (error: any) {
|
||||
toast.push(
|
||||
<Notification type="danger" title={translate('::App.Platform.Error')}>
|
||||
{error.response?.data?.error?.message || translate('::App.Platform.FailedToUpdateObject')}
|
||||
</Notification>,
|
||||
{ placement: 'top-center' },
|
||||
)
|
||||
setShowSaveDialog(true)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -449,24 +507,54 @@ GO`
|
|||
if (!state.selectedDataSource || !saveDialogData.name) return
|
||||
|
||||
try {
|
||||
await sqlObjectManagerService.createQuery({
|
||||
code: saveDialogData.name.replace(/\s+/g, '_'),
|
||||
// Smart save ile kaydet
|
||||
const result = await sqlObjectManagerService.smartSave({
|
||||
sqlText: state.editorContent,
|
||||
dataSourceCode: state.selectedDataSource.code || '',
|
||||
name: saveDialogData.name,
|
||||
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)
|
||||
|
||||
toast.push(
|
||||
<Notification type="success" title={translate('::App.Platform.Success')}>
|
||||
{translate('::App.Platform.QuerySavedSuccessfully')}
|
||||
{result.data.message || translate('::App.Platform.SavedSuccessfully')}
|
||||
</Notification>,
|
||||
{ placement: 'top-center' },
|
||||
)
|
||||
|
|
@ -497,14 +585,20 @@ GO`
|
|||
let result: any
|
||||
|
||||
switch (state.selectedObjectType) {
|
||||
case 2: // Stored Procedure
|
||||
result = await sqlObjectManagerService.deployStoredProcedure({ id: objectId, dropIfExists: true })
|
||||
case SqlObjectType.StoredProcedure:
|
||||
result = await sqlObjectManagerService.deployStoredProcedure({
|
||||
id: objectId,
|
||||
dropIfExists: true,
|
||||
})
|
||||
break
|
||||
case 3: // View
|
||||
case SqlObjectType.View:
|
||||
result = await sqlObjectManagerService.deployView({ id: objectId, dropIfExists: true })
|
||||
break
|
||||
case 4: // Function
|
||||
result = await sqlObjectManagerService.deployFunction({ id: objectId, dropIfExists: true })
|
||||
case SqlObjectType.Function:
|
||||
result = await sqlObjectManagerService.deployFunction({
|
||||
id: objectId,
|
||||
dropIfExists: true,
|
||||
})
|
||||
break
|
||||
default:
|
||||
toast.push(
|
||||
|
|
@ -516,6 +610,13 @@ GO`
|
|||
return
|
||||
}
|
||||
|
||||
// Update selectedObject's isDeployed status
|
||||
setState((prev) => ({
|
||||
...prev,
|
||||
selectedObject: prev.selectedObject ? { ...prev.selectedObject, isDeployed: true } : null,
|
||||
refreshTrigger: prev.refreshTrigger + 1,
|
||||
}))
|
||||
|
||||
toast.push(
|
||||
<Notification type="success" title={translate('::App.Platform.Success')}>
|
||||
{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 (
|
||||
<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">
|
||||
{/* 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 gap-3">
|
||||
<FaDatabase className="text-lg text-blue-500" />
|
||||
|
|
@ -578,43 +737,43 @@ GO`
|
|||
variant="solid"
|
||||
icon={<FaSave />}
|
||||
onClick={handleSave}
|
||||
disabled={!state.isDirty || !state.selectedDataSource}
|
||||
disabled={
|
||||
!state.selectedDataSource ||
|
||||
!state.editorContent.trim() ||
|
||||
(state.isSaved && !state.isDirty) ||
|
||||
!state.executionResult?.success
|
||||
}
|
||||
className="shadow-sm"
|
||||
>
|
||||
{translate('::App.Platform.Save')}
|
||||
<span className="ml-1 text-xs opacity-75">(Ctrl+S)</span>
|
||||
</Button>
|
||||
{state.selectedObject &&
|
||||
state.selectedObjectType &&
|
||||
state.selectedObjectType !== 1 && (
|
||||
<Button
|
||||
size="sm"
|
||||
variant="solid"
|
||||
color="green-600"
|
||||
icon={<FaCloudUploadAlt />}
|
||||
onClick={handleDeploy}
|
||||
disabled={!state.selectedDataSource}
|
||||
disabled={
|
||||
!state.selectedObject ||
|
||||
!state.selectedObjectType ||
|
||||
state.selectedObjectType === SqlObjectType.Query ||
|
||||
(state.selectedObject &&
|
||||
'isDeployed' in state.selectedObject &&
|
||||
state.selectedObject.isDeployed)
|
||||
}
|
||||
className="shadow-sm"
|
||||
>
|
||||
{translate('::App.Platform.Deploy')}
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
size="sm"
|
||||
variant="twoTone"
|
||||
icon={<FaSyncAlt />}
|
||||
onClick={() => window.location.reload()}
|
||||
className="shadow-sm"
|
||||
>
|
||||
{translate('::App.Platform.Refresh')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</AdaptableCard>
|
||||
</div>
|
||||
|
||||
{/* Main Content Area */}
|
||||
<div className="flex-1 flex min-h-0">
|
||||
{/* 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">
|
||||
<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">
|
||||
|
|
@ -622,26 +781,31 @@ GO`
|
|||
{translate('::App.Platform.ObjectExplorer')}
|
||||
</h6>
|
||||
</div>
|
||||
<div className="flex-1 min-h-0 overflow-auto">
|
||||
<SqlObjectExplorer
|
||||
dataSource={state.selectedDataSource}
|
||||
onObjectSelect={handleObjectSelect}
|
||||
selectedObject={state.selectedObject}
|
||||
onTemplateSelect={handleTemplateSelect}
|
||||
onShowTableColumns={handleShowTableColumns}
|
||||
refreshTrigger={state.refreshTrigger}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</AdaptableCard>
|
||||
</div>
|
||||
|
||||
{/* Center Panel - Editor and Results */}
|
||||
<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 ? (
|
||||
<Splitter direction="vertical" initialSize={250} minSize={150} maxSize={1200}>
|
||||
<div className="border rounded-lg shadow-sm bg-white dark:bg-gray-800 flex flex-col h-full">
|
||||
<div className="border-b px-4 py-2 bg-gray-50 dark:bg-gray-800 flex-shrink-0">
|
||||
<h6 className="font-semibold text-sm">{translate('::App.Platform.QueryEditor')}</h6>
|
||||
<h6 className="font-semibold text-sm">
|
||||
{translate('::App.Platform.QueryEditor')}
|
||||
</h6>
|
||||
</div>
|
||||
<div className="flex-1 min-h-0">
|
||||
<div className="flex-1 min-h-0 overflow-hidden">
|
||||
<SqlEditor
|
||||
ref={editorRef}
|
||||
value={state.editorContent}
|
||||
onChange={handleEditorChange}
|
||||
onExecute={handleExecute}
|
||||
|
|
@ -650,14 +814,57 @@ GO`
|
|||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{state.executionResult && (
|
||||
<div className="flex-1 mt-4 border rounded-lg shadow-sm bg-white dark:bg-gray-800 flex flex-col overflow-hidden">
|
||||
<div className="border-b px-4 py-2 bg-gray-50 dark:bg-gray-800 flex-shrink-0">
|
||||
<h6 className="font-semibold text-sm">{translate('::App.Platform.Results')}</h6>
|
||||
<div className="border rounded-lg shadow-sm bg-white dark:bg-gray-800 flex flex-col h-full">
|
||||
<div className="border-b px-4 py-2 bg-gray-50 dark:bg-gray-800 flex-shrink-0 flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<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-1 overflow-hidden min-h-0">
|
||||
<SqlResultsGrid result={state.executionResult} />
|
||||
<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>
|
||||
)}
|
||||
|
|
@ -698,13 +905,52 @@ GO`
|
|||
onClose={() => 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">
|
||||
{/* 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>
|
||||
<label className="block mb-2">{translate('::App.Platform.Name')}</label>
|
||||
<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>
|
||||
<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
|
||||
autoFocus={!saveDialogData.isExistingObject}
|
||||
value={saveDialogData.name}
|
||||
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>
|
||||
|
|
@ -714,13 +960,18 @@ GO`
|
|||
onChange={(e) =>
|
||||
setSaveDialogData((prev) => ({ ...prev, description: e.target.value }))
|
||||
}
|
||||
placeholder={translate('::App.Platform.Description')}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex justify-end gap-2">
|
||||
<Button variant="plain" onClick={() => setShowSaveDialog(false)}>
|
||||
{translate('::App.Platform.Cancel')}
|
||||
</Button>
|
||||
<Button variant="solid" onClick={handleCreateNewQuery}>
|
||||
<Button
|
||||
variant="solid"
|
||||
onClick={handleCreateNewQuery}
|
||||
disabled={!saveDialogData.name.trim()}
|
||||
>
|
||||
{translate('::App.Platform.Save')}
|
||||
</Button>
|
||||
</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 { useConfig } from '@/components/ui/ConfigProvider'
|
||||
import type { editor } from 'monaco-editor'
|
||||
|
|
@ -12,18 +12,45 @@ interface SqlEditorProps {
|
|||
height?: string
|
||||
}
|
||||
|
||||
const SqlEditor = ({
|
||||
export interface SqlEditorRef {
|
||||
getSelectedText: () => string
|
||||
preserveSelection: () => any
|
||||
restoreSelection: (selection: any) => void
|
||||
}
|
||||
|
||||
const SqlEditor = forwardRef<SqlEditorRef, SqlEditorProps>((
|
||||
{
|
||||
value,
|
||||
onChange,
|
||||
onExecute,
|
||||
onSave,
|
||||
readOnly = false,
|
||||
height = '100%',
|
||||
}: SqlEditorProps) => {
|
||||
}: SqlEditorProps,
|
||||
ref
|
||||
) => {
|
||||
const { mode } = useConfig()
|
||||
const editorRef = useRef<editor.IStandaloneCodeEditor | 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) => {
|
||||
editorRef.current = editor
|
||||
monacoRef.current = monaco
|
||||
|
|
@ -211,6 +238,8 @@ const SqlEditor = ({
|
|||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
SqlEditor.displayName = '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 {
|
||||
FaRegFolder,
|
||||
|
|
@ -12,7 +12,6 @@ import {
|
|||
FaTrash,
|
||||
FaTable,
|
||||
} from 'react-icons/fa'
|
||||
import { MdViewColumn } from 'react-icons/md'
|
||||
import type { DataSourceDto } from '@/proxy/data-source'
|
||||
import type {
|
||||
SqlFunctionDto,
|
||||
|
|
@ -21,9 +20,7 @@ import type {
|
|||
SqlViewDto,
|
||||
SqlObjectType,
|
||||
} from '@/proxy/sql-query-manager/models'
|
||||
import {
|
||||
sqlObjectManagerService,
|
||||
} from '@/services/sql-query-manager.service'
|
||||
import { sqlObjectManagerService } from '@/services/sql-query-manager.service'
|
||||
import { useLocalization } from '@/utils/hooks/useLocalization'
|
||||
|
||||
export type SqlObject = SqlFunctionDto | SqlQueryDto | SqlStoredProcedureDto | SqlViewDto
|
||||
|
|
@ -45,6 +42,8 @@ interface SqlObjectExplorerProps {
|
|||
onObjectSelect: (object: SqlObject | null, objectType: SqlObjectType | null) => void
|
||||
selectedObject: SqlObject | null
|
||||
onTemplateSelect?: (template: string, templateType: string) => void
|
||||
onShowTableColumns?: (schemaName: string, tableName: string) => void
|
||||
refreshTrigger?: number
|
||||
}
|
||||
|
||||
const SqlObjectExplorer = ({
|
||||
|
|
@ -52,6 +51,8 @@ const SqlObjectExplorer = ({
|
|||
onObjectSelect,
|
||||
selectedObject,
|
||||
onTemplateSelect,
|
||||
onShowTableColumns,
|
||||
refreshTrigger,
|
||||
}: SqlObjectExplorerProps) => {
|
||||
const { translate } = useLocalization()
|
||||
const [treeData, setTreeData] = useState<TreeNode[]>([])
|
||||
|
|
@ -78,7 +79,7 @@ const SqlObjectExplorer = ({
|
|||
} else {
|
||||
setTreeData([])
|
||||
}
|
||||
}, [dataSource])
|
||||
}, [dataSource, refreshTrigger]) // refreshTrigger değişince de yenile
|
||||
|
||||
const loadObjects = async () => {
|
||||
if (!dataSource) return
|
||||
|
|
@ -181,19 +182,24 @@ const SqlObjectExplorer = ({
|
|||
})) || [],
|
||||
},
|
||||
{
|
||||
id: 'storedProcedures',
|
||||
id: 'procedures',
|
||||
label: `${translate('::App.Platform.StoredProcedures')} (${allObjects.storedProcedures.length})`,
|
||||
type: 'folder',
|
||||
objectType: 2,
|
||||
expanded: expandedNodes.has('storedProcedures'),
|
||||
expanded: expandedNodes.has('procedures'),
|
||||
children:
|
||||
allObjects.storedProcedures.map((sp) => ({
|
||||
id: sp.id || '',
|
||||
label: sp.displayName || sp.procedureName,
|
||||
allObjects.storedProcedures.map((p) => {
|
||||
const deployInfo = p.isDeployed && p.lastDeployedAt
|
||||
? ` (${new Date(p.lastDeployedAt).toLocaleString('tr-TR', { day: '2-digit', month: '2-digit', year: 'numeric', hour: '2-digit', minute: '2-digit' })})`
|
||||
: '';
|
||||
return {
|
||||
id: p.id || '',
|
||||
label: `${p.displayName || p.procedureName}${p.isDeployed ? ' ✅' : ' ❌'}${deployInfo}`,
|
||||
type: 'object' as const,
|
||||
objectType: 2 as SqlObjectType,
|
||||
data: sp,
|
||||
})) || [],
|
||||
data: p,
|
||||
};
|
||||
}) || [],
|
||||
},
|
||||
{
|
||||
id: 'views',
|
||||
|
|
@ -202,13 +208,18 @@ const SqlObjectExplorer = ({
|
|||
objectType: 3,
|
||||
expanded: expandedNodes.has('views'),
|
||||
children:
|
||||
allObjects.views.map((v) => ({
|
||||
allObjects.views.map((v) => {
|
||||
const deployInfo = v.isDeployed && v.lastDeployedAt
|
||||
? ` (${new Date(v.lastDeployedAt).toLocaleString('tr-TR', { day: '2-digit', month: '2-digit', year: 'numeric', hour: '2-digit', minute: '2-digit' })})`
|
||||
: '';
|
||||
return {
|
||||
id: v.id || '',
|
||||
label: v.displayName || v.viewName,
|
||||
label: `${v.displayName || v.viewName}${v.isDeployed ? ' ✅' : ' ❌'}${deployInfo}`,
|
||||
type: 'object' as const,
|
||||
objectType: 3 as SqlObjectType,
|
||||
data: v,
|
||||
})) || [],
|
||||
};
|
||||
}) || [],
|
||||
},
|
||||
{
|
||||
id: 'functions',
|
||||
|
|
@ -217,13 +228,18 @@ const SqlObjectExplorer = ({
|
|||
objectType: 4,
|
||||
expanded: expandedNodes.has('functions'),
|
||||
children:
|
||||
allObjects.functions.map((f) => ({
|
||||
allObjects.functions.map((f) => {
|
||||
const deployInfo = f.isDeployed && f.lastDeployedAt
|
||||
? ` (${new Date(f.lastDeployedAt).toLocaleString('tr-TR', { day: '2-digit', month: '2-digit', year: 'numeric', hour: '2-digit', minute: '2-digit' })})`
|
||||
: '';
|
||||
return {
|
||||
id: f.id || '',
|
||||
label: f.displayName || f.functionName,
|
||||
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
|
||||
}
|
||||
|
||||
const updateNodeChildren = (nodes: TreeNode[], nodeId: string, children: TreeNode[]): TreeNode[] => {
|
||||
const updateNodeChildren = (
|
||||
nodes: TreeNode[],
|
||||
nodeId: string,
|
||||
children: TreeNode[],
|
||||
): TreeNode[] => {
|
||||
return nodes.map((node) => {
|
||||
if (node.id === nodeId) {
|
||||
return { ...node, children }
|
||||
|
|
@ -350,8 +370,7 @@ const SqlObjectExplorer = ({
|
|||
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;`
|
||||
onTemplateSelect(selectQuery, 'table-select')
|
||||
}
|
||||
else if (node.objectType) {
|
||||
} else if (node.objectType) {
|
||||
onObjectSelect(node.data, node.objectType)
|
||||
}
|
||||
}
|
||||
|
|
@ -360,8 +379,8 @@ const SqlObjectExplorer = ({
|
|||
const handleContextMenu = (e: React.MouseEvent, node: TreeNode) => {
|
||||
e.preventDefault()
|
||||
|
||||
// Don't show context menu for columns, templates, or tables
|
||||
if (node.type === 'column' || node.id.startsWith('template-') || node.id.startsWith('table-')) {
|
||||
// Don't show context menu for columns or templates
|
||||
if (node.type === 'column' || node.id.startsWith('template-')) {
|
||||
return
|
||||
}
|
||||
|
||||
|
|
@ -487,7 +506,7 @@ const SqlObjectExplorer = ({
|
|||
}
|
||||
|
||||
if (node.type === 'column') {
|
||||
return <MdViewColumn className="text-gray-400 text-sm" />
|
||||
return <FaColumns className="text-gray-400 text-sm" />
|
||||
}
|
||||
|
||||
return <FaRegFolder />
|
||||
|
|
@ -509,7 +528,9 @@ const SqlObjectExplorer = ({
|
|||
onContextMenu={(e) => !isColumn && handleContextMenu(e, 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>
|
||||
|
||||
{isExpanded && node.children && (
|
||||
|
|
@ -545,8 +566,12 @@ const SqlObjectExplorer = ({
|
|||
</div>
|
||||
|
||||
{/* Tree Content */}
|
||||
<div className="flex-1 overflow-auto">
|
||||
{loading && <div className="text-center py-8 text-gray-500">{translate('::App.Platform.Loading')}</div>}
|
||||
<div className="h-[calc(100vh-265px)] overflow-auto">
|
||||
{loading && (
|
||||
<div className="text-center py-8 text-gray-500">
|
||||
{translate('::App.Platform.Loading')}
|
||||
</div>
|
||||
)}
|
||||
{!loading && treeData.length === 0 && (
|
||||
<div className="text-center py-8 text-gray-500">
|
||||
{translate('::App.Platform.NoDataSourceSelected')}
|
||||
|
|
@ -572,7 +597,7 @@ const SqlObjectExplorer = ({
|
|||
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 }}
|
||||
>
|
||||
{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"
|
||||
|
|
@ -618,6 +643,24 @@ const SqlObjectExplorer = ({
|
|||
{translate('::App.Platform.Refresh')}
|
||||
</button>
|
||||
)}
|
||||
|
||||
{contextMenu.node?.id?.startsWith('table-') && contextMenu.node?.data && (
|
||||
<button
|
||||
className="w-full px-4 py-2 text-left hover:bg-gray-100 dark:hover:bg-gray-700 text-sm"
|
||||
onClick={() => {
|
||||
if (onShowTableColumns && contextMenu.node?.data) {
|
||||
onShowTableColumns(
|
||||
contextMenu.node.data.schemaName,
|
||||
contextMenu.node.data.tableName
|
||||
)
|
||||
}
|
||||
setContextMenu({ show: false, x: 0, y: 0, node: null })
|
||||
}}
|
||||
>
|
||||
<FaColumns className="inline mr-2" />
|
||||
{translate('::App.Platform.ShowColumns')}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -55,24 +55,6 @@ const SqlResultsGrid = ({ result }: SqlResultsGridProps) => {
|
|||
|
||||
return (
|
||||
<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 ? (
|
||||
<div className="flex-1 overflow-hidden relative">
|
||||
<div className="absolute inset-0">
|
||||
|
|
|
|||
Loading…
Reference in a new issue