Sql Query Manager yapılandırıldı.

This commit is contained in:
Sedat Öztürk 2025-12-06 01:38:21 +03:00
parent 932ee406b1
commit 81abe549cf
13 changed files with 1152 additions and 365 deletions

View file

@ -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);
} }

View file

@ -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; }
}

View file

@ -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;
@ -160,7 +161,7 @@ public class SqlObjectManagerAppService : ApplicationService, ISqlObjectManagerA
SCHEMA_NAME(t.schema_id), t.name"; SCHEMA_NAME(t.schema_id), t.name";
var result = await _sqlExecutorService.ExecuteQueryAsync(query, dataSourceCode); var result = await _sqlExecutorService.ExecuteQueryAsync(query, dataSourceCode);
var tables = new List<DatabaseTableDto>(); var tables = new List<DatabaseTableDto>();
if (result.Success && result.Data != null) if (result.Success && result.Data != null)
{ {
@ -195,7 +196,7 @@ public class SqlObjectManagerAppService : ApplicationService, ISqlObjectManagerA
public async Task<SqlQueryDto> UpdateQueryAsync(Guid id, UpdateSqlQueryDto input) public async Task<SqlQueryDto> UpdateQueryAsync(Guid id, UpdateSqlQueryDto input)
{ {
var query = await _queryRepository.GetAsync(id); var query = await _queryRepository.GetAsync(id);
query.Name = input.Name; query.Name = input.Name;
query.Description = input.Description; query.Description = input.Description;
query.QueryText = input.QueryText; query.QueryText = input.QueryText;
@ -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);
} }
@ -221,12 +266,12 @@ public class SqlObjectManagerAppService : ApplicationService, ISqlObjectManagerA
{ {
var query = await _queryRepository.GetAsync(id); var query = await _queryRepository.GetAsync(id);
var result = await _sqlExecutorService.ExecuteQueryAsync(query.QueryText, query.DataSourceCode); var result = await _sqlExecutorService.ExecuteQueryAsync(query.QueryText, query.DataSourceCode);
// Update execution statistics // Update execution statistics
query.ExecutionCount++; query.ExecutionCount++;
query.LastExecutedAt = DateTime.UtcNow; query.LastExecutedAt = DateTime.UtcNow;
await _queryRepository.UpdateAsync(query, autoSave: true); await _queryRepository.UpdateAsync(query, autoSave: true);
return MapExecutionResult(result); return MapExecutionResult(result);
} }
@ -237,11 +282,13 @@ public class SqlObjectManagerAppService : ApplicationService, ISqlObjectManagerA
public async Task<SqlStoredProcedureDto> UpdateStoredProcedureAsync(Guid id, UpdateSqlStoredProcedureDto input) public async Task<SqlStoredProcedureDto> UpdateStoredProcedureAsync(Guid id, UpdateSqlStoredProcedureDto input)
{ {
var procedure = await _procedureRepository.GetAsync(id); var procedure = await _procedureRepository.GetAsync(id);
procedure.DisplayName = input.DisplayName; procedure.DisplayName = input.DisplayName;
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, try
procedure.DataSourceCode
);
if (result.Success)
{ {
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);
}
// Sonra CREATE işlemi yap
var result = await _sqlExecutorService.DeployStoredProcedureAsync(
procedure.ProcedureBody,
procedure.DataSourceCode
);
return MapExecutionResult(result); 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
@ -277,11 +357,13 @@ public class SqlObjectManagerAppService : ApplicationService, ISqlObjectManagerA
public async Task<SqlViewDto> UpdateViewAsync(Guid id, UpdateSqlViewDto input) public async Task<SqlViewDto> UpdateViewAsync(Guid id, UpdateSqlViewDto input)
{ {
var view = await _viewRepository.GetAsync(id); var view = await _viewRepository.GetAsync(id);
view.DisplayName = input.DisplayName; view.DisplayName = input.DisplayName;
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, try
view.DataSourceCode
);
if (result.Success)
{ {
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);
}
// Sonra CREATE işlemi yap
var result = await _sqlExecutorService.DeployViewAsync(
view.ViewDefinition,
view.DataSourceCode
);
return MapExecutionResult(result); 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
@ -317,11 +432,13 @@ public class SqlObjectManagerAppService : ApplicationService, ISqlObjectManagerA
public async Task<SqlFunctionDto> UpdateFunctionAsync(Guid id, UpdateSqlFunctionDto input) public async Task<SqlFunctionDto> UpdateFunctionAsync(Guid id, UpdateSqlFunctionDto input)
{ {
var function = await _functionRepository.GetAsync(id); var function = await _functionRepository.GetAsync(id);
function.DisplayName = input.DisplayName; function.DisplayName = input.DisplayName;
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, try
function.DataSourceCode
);
if (result.Success)
{ {
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);
}
// Sonra CREATE işlemi yap
var result = await _sqlExecutorService.DeployFunctionAsync(
function.FunctionBody,
function.DataSourceCode
);
return MapExecutionResult(result); 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
@ -373,7 +523,7 @@ public class SqlObjectManagerAppService : ApplicationService, ISqlObjectManagerA
c.column_id"; c.column_id";
var result = await _sqlExecutorService.ExecuteQueryAsync(query, dataSourceCode); var result = await _sqlExecutorService.ExecuteQueryAsync(query, dataSourceCode);
var columns = new List<DatabaseColumnDto>(); var columns = new List<DatabaseColumnDto>();
if (result.Success && result.Data != null) if (result.Success && result.Data != null)
{ {
@ -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)

View file

@ -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)
{ {

View file

@ -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",
@ -10874,4 +10910,4 @@
"en": "Delete" "en": "Delete"
} }
] ]
} }

View file

@ -1,2 +1 @@
export * from './data-source.service'
export * from './models' export * from './models'

View file

@ -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

View file

@ -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: '',

View file

@ -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(),

View file

@ -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,
@ -153,13 +171,13 @@ WHERE
AND IsActive = 1 AND IsActive = 1
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',
@ -167,13 +185,13 @@ SET
WHERE 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
Id = 1;`, Id = 1;`,
'create-procedure': `-- Create Stored Procedure 'create-procedure': `-- Create Stored Procedure
CREATE PROCEDURE [dbo].[ProcedureName] CREATE PROCEDURE [dbo].[ProcedureName]
@Parameter1 INT, @Parameter1 INT,
@ -193,7 +211,7 @@ BEGIN
AND Column2 = @Parameter2; AND Column2 = @Parameter2;
END END
GO`, GO`,
'create-view': `-- Create View 'create-view': `-- Create View
CREATE VIEW [dbo].[ViewName] CREATE VIEW [dbo].[ViewName]
AS AS
@ -207,7 +225,7 @@ FROM
WHERE WHERE
t1.IsActive = 1; t1.IsActive = 1;
GO`, GO`,
'create-scalar-function': `-- Create Scalar Function 'create-scalar-function': `-- Create Scalar Function
CREATE FUNCTION [dbo].[ScalarFunctionName] CREATE FUNCTION [dbo].[ScalarFunctionName]
( (
@ -227,7 +245,7 @@ BEGIN
RETURN @Result; RETURN @Result;
END END
GO`, GO`,
'create-table-function': `-- Create Table-Valued Function 'create-table-function': `-- Create Table-Valued Function
CREATE FUNCTION [dbo].[TableFunctionName] CREATE FUNCTION [dbo].[TableFunctionName]
( (
@ -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>

View file

@ -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

View file

@ -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,
};
}) || [],
}, },
], ],
}, },
@ -264,21 +280,21 @@ const SqlObjectExplorer = ({
const toggleNode = async (nodeId: string) => { const toggleNode = async (nodeId: string) => {
const newSet = new Set(expandedNodes) const newSet = new Set(expandedNodes)
if (newSet.has(nodeId)) { if (newSet.has(nodeId)) {
newSet.delete(nodeId) newSet.delete(nodeId)
setExpandedNodes(newSet) setExpandedNodes(newSet)
} else { } else {
newSet.add(nodeId) newSet.add(nodeId)
setExpandedNodes(newSet) setExpandedNodes(newSet)
// If it's a table node and hasn't loaded columns yet, load them // If it's a table node and hasn't loaded columns yet, load them
if (nodeId.startsWith('table-') && dataSource) { if (nodeId.startsWith('table-') && dataSource) {
const tableNode = findNodeById(treeData, nodeId) const tableNode = findNodeById(treeData, nodeId)
if (tableNode && (!tableNode.children || tableNode.children.length === 0)) { if (tableNode && (!tableNode.children || tableNode.children.length === 0)) {
const tableData = tableNode.data as any const tableData = tableNode.data as any
const columns = await loadTableColumns(tableData.schemaName, tableData.tableName) const columns = await loadTableColumns(tableData.schemaName, tableData.tableName)
// Update tree data with columns // Update tree data with columns
setTreeData((prevTree) => { setTreeData((prevTree) => {
return updateNodeChildren(prevTree, nodeId, columns) return updateNodeChildren(prevTree, nodeId, columns)
@ -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 }
@ -329,7 +349,7 @@ const SqlObjectExplorer = ({
return null return null
}) })
.filter((node) => node !== null) as TreeNode[] .filter((node) => node !== null) as TreeNode[]
return filtered return filtered
} }
@ -344,14 +364,13 @@ const SqlObjectExplorer = ({
if ((node.data as any).templateType && onTemplateSelect) { if ((node.data as any).templateType && onTemplateSelect) {
const templateType = (node.data as any).templateType const templateType = (node.data as any).templateType
onTemplateSelect('', templateType) // Template content will be generated in parent onTemplateSelect('', templateType) // Template content will be generated in parent
} }
// Check if it's a table // Check if it's a table
else if (node.id.startsWith('table-') && onTemplateSelect) { else if (node.id.startsWith('table-') && onTemplateSelect) {
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)
} }
} }
@ -359,12 +378,12 @@ 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
} }
setContextMenu({ setContextMenu({
show: true, show: true,
x: e.clientX, x: e.clientX,
@ -423,7 +442,7 @@ const SqlObjectExplorer = ({
if (node.type === 'folder') { if (node.type === 'folder') {
const isExpanded = expandedNodes.has(node.id) const isExpanded = expandedNodes.has(node.id)
// Templates folder // Templates folder
if (node.id === 'templates') if (node.id === 'templates')
return isExpanded ? ( return isExpanded ? (
@ -431,7 +450,7 @@ const SqlObjectExplorer = ({
) : ( ) : (
<FaRegFolder className="text-orange-500" /> <FaRegFolder className="text-orange-500" />
) )
// Tables folder // Tables folder
if (node.id === 'tables') if (node.id === 'tables')
return isExpanded ? ( return isExpanded ? (
@ -439,7 +458,7 @@ const SqlObjectExplorer = ({
) : ( ) : (
<FaRegFolder className="text-blue-500" /> <FaRegFolder className="text-blue-500" />
) )
if (node.objectType === 1) if (node.objectType === 1)
return isExpanded ? ( return isExpanded ? (
<FaRegFolderOpen className="text-yellow-500" /> <FaRegFolderOpen className="text-yellow-500" />
@ -474,12 +493,12 @@ const SqlObjectExplorer = ({
if ((node.data as any)?.templateType) { if ((node.data as any)?.templateType) {
return <FaCode className="text-orange-500" /> return <FaCode className="text-orange-500" />
} }
// Check if it's a table // Check if it's a table
if (node.id.startsWith('table-')) { if (node.id.startsWith('table-')) {
return <FaTable className="text-blue-500" /> return <FaTable className="text-blue-500" />
} }
if (node.objectType === 1) return <FaRegFileAlt className="text-gray-500" /> if (node.objectType === 1) return <FaRegFileAlt className="text-gray-500" />
if (node.objectType === 2) return <FaCog className="text-gray-500" /> if (node.objectType === 2) return <FaCog className="text-gray-500" />
if (node.objectType === 3) return <FaColumns className="text-gray-500" /> if (node.objectType === 3) return <FaColumns className="text-gray-500" />
@ -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>
</> </>
)} )}

View file

@ -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">