diff --git a/api/modules/Erp.SqlQueryManager/Erp.SqlQueryManager.Application.Contracts/ISqlObjectManagerAppService.cs b/api/modules/Erp.SqlQueryManager/Erp.SqlQueryManager.Application.Contracts/ISqlObjectManagerAppService.cs index 432cbe9b..e69301ae 100644 --- a/api/modules/Erp.SqlQueryManager/Erp.SqlQueryManager.Application.Contracts/ISqlObjectManagerAppService.cs +++ b/api/modules/Erp.SqlQueryManager/Erp.SqlQueryManager.Application.Contracts/ISqlObjectManagerAppService.cs @@ -41,4 +41,7 @@ public interface ISqlObjectManagerAppService : IApplicationService // Database Metadata Operations Task> GetTableColumnsAsync(string dataSourceCode, string schemaName, string tableName); + + // Smart Save - Analyzes SQL and saves to appropriate table with auto-deploy + Task SmartSaveAsync(SmartSaveInputDto input); } diff --git a/api/modules/Erp.SqlQueryManager/Erp.SqlQueryManager.Application.Contracts/SmartSaveDto.cs b/api/modules/Erp.SqlQueryManager/Erp.SqlQueryManager.Application.Contracts/SmartSaveDto.cs new file mode 100644 index 00000000..903dd9d1 --- /dev/null +++ b/api/modules/Erp.SqlQueryManager/Erp.SqlQueryManager.Application.Contracts/SmartSaveDto.cs @@ -0,0 +1,25 @@ +using System; + +namespace Erp.SqlQueryManager.Application.Contracts; + +/// +/// Input for smart save operation +/// +public class SmartSaveInputDto +{ + public string SqlText { get; set; } + public string DataSourceCode { get; set; } + public string Name { get; set; } + public string Description { get; set; } +} + +/// +/// Result of smart save operation +/// +public class SmartSaveResultDto +{ + public string ObjectType { get; set; } + public Guid ObjectId { get; set; } + public bool Deployed { get; set; } + public string Message { get; set; } +} diff --git a/api/modules/Erp.SqlQueryManager/Erp.SqlQueryManager.Application/SqlObjectManagerAppService.cs b/api/modules/Erp.SqlQueryManager/Erp.SqlQueryManager.Application/SqlObjectManagerAppService.cs index b0fdba7e..055af202 100644 --- a/api/modules/Erp.SqlQueryManager/Erp.SqlQueryManager.Application/SqlObjectManagerAppService.cs +++ b/api/modules/Erp.SqlQueryManager/Erp.SqlQueryManager.Application/SqlObjectManagerAppService.cs @@ -6,7 +6,6 @@ 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 Volo.Abp.Application.Services; using Volo.Abp.Domain.Repositories; @@ -160,7 +159,7 @@ public class SqlObjectManagerAppService : ApplicationService, ISqlObjectManagerA SCHEMA_NAME(t.schema_id), t.name"; var result = await _sqlExecutorService.ExecuteQueryAsync(query, dataSourceCode); - + var tables = new List(); if (result.Success && result.Data != null) { @@ -195,7 +194,7 @@ public class SqlObjectManagerAppService : ApplicationService, ISqlObjectManagerA public async Task UpdateQueryAsync(Guid id, UpdateSqlQueryDto input) { var query = await _queryRepository.GetAsync(id); - + query.Name = input.Name; query.Description = input.Description; query.QueryText = input.QueryText; @@ -213,6 +212,50 @@ public class SqlObjectManagerAppService : ApplicationService, ISqlObjectManagerA public async Task 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(), + RowsAffected = 0, + ExecutionTimeMs = 0 + }; + } + catch (Exception ex) + { + return new SqlQueryExecutionResultDto + { + Success = false, + Message = $"SQL syntax error: {ex.Message}", + Data = new List(), + 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); } @@ -221,12 +264,12 @@ public class SqlObjectManagerAppService : ApplicationService, ISqlObjectManagerA { var query = await _queryRepository.GetAsync(id); var result = await _sqlExecutorService.ExecuteQueryAsync(query.QueryText, query.DataSourceCode); - + // Update execution statistics query.ExecutionCount++; query.LastExecutedAt = DateTime.UtcNow; await _queryRepository.UpdateAsync(query, autoSave: true); - + return MapExecutionResult(result); } @@ -237,11 +280,13 @@ public class SqlObjectManagerAppService : ApplicationService, ISqlObjectManagerA public async Task UpdateStoredProcedureAsync(Guid id, UpdateSqlStoredProcedureDto input) { var procedure = await _procedureRepository.GetAsync(id); - + procedure.DisplayName = input.DisplayName; 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(updated); @@ -249,25 +294,58 @@ 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 DeployStoredProcedureAsync(DeployStoredProcedureDto input) { var procedure = await _procedureRepository.GetAsync(input.Id); - var result = await _sqlExecutorService.DeployStoredProcedureAsync( - procedure.ProcedureBody, - procedure.DataSourceCode - ); - - if (result.Success) + + try { - procedure.IsDeployed = true; - procedure.LastDeployedAt = DateTime.UtcNow; - await _procedureRepository.UpdateAsync(procedure, autoSave: true); - } + // Ö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 + ); - 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(), + RowsAffected = 0, + ExecutionTimeMs = 0 + }; + } } #endregion @@ -277,11 +355,13 @@ public class SqlObjectManagerAppService : ApplicationService, ISqlObjectManagerA public async Task UpdateViewAsync(Guid id, UpdateSqlViewDto input) { var view = await _viewRepository.GetAsync(id); - + view.DisplayName = input.DisplayName; 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(updated); @@ -289,25 +369,58 @@ 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 DeployViewAsync(DeployViewDto input) { var view = await _viewRepository.GetAsync(input.Id); - var result = await _sqlExecutorService.DeployViewAsync( - view.ViewDefinition, - view.DataSourceCode - ); - - if (result.Success) + + try { - view.IsDeployed = true; - view.LastDeployedAt = DateTime.UtcNow; - await _viewRepository.UpdateAsync(view, autoSave: true); - } + // Ö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 + ); - 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(), + RowsAffected = 0, + ExecutionTimeMs = 0 + }; + } } #endregion @@ -317,11 +430,13 @@ public class SqlObjectManagerAppService : ApplicationService, ISqlObjectManagerA public async Task UpdateFunctionAsync(Guid id, UpdateSqlFunctionDto input) { var function = await _functionRepository.GetAsync(id); - + function.DisplayName = input.DisplayName; 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(updated); @@ -329,25 +444,58 @@ 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 DeployFunctionAsync(DeployFunctionDto input) { var function = await _functionRepository.GetAsync(input.Id); - var result = await _sqlExecutorService.DeployFunctionAsync( - function.FunctionBody, - function.DataSourceCode - ); - - if (result.Success) + + try { - function.IsDeployed = true; - function.LastDeployedAt = DateTime.UtcNow; - await _functionRepository.UpdateAsync(function, autoSave: true); - } + // Ö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 + ); - 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(), + RowsAffected = 0, + ExecutionTimeMs = 0 + }; + } } #endregion @@ -373,7 +521,7 @@ public class SqlObjectManagerAppService : ApplicationService, ISqlObjectManagerA c.column_id"; var result = await _sqlExecutorService.ExecuteQueryAsync(query, dataSourceCode); - + var columns = new List(); if (result.Success && result.Data != null) { @@ -398,6 +546,264 @@ public class SqlObjectManagerAppService : ApplicationService, ISqlObjectManagerA #endregion + public async Task 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) diff --git a/api/modules/Erp.SqlQueryManager/Erp.SqlQueryManager.Domain/Services/SqlExecutorService.cs b/api/modules/Erp.SqlQueryManager/Erp.SqlQueryManager.Domain/Services/SqlExecutorService.cs index f94baffd..83276370 100644 --- a/api/modules/Erp.SqlQueryManager/Erp.SqlQueryManager.Domain/Services/SqlExecutorService.cs +++ b/api/modules/Erp.SqlQueryManager/Erp.SqlQueryManager.Domain/Services/SqlExecutorService.cs @@ -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) { diff --git a/api/src/Erp.Platform.DbMigrator/Seeds/LanguagesData.json b/api/src/Erp.Platform.DbMigrator/Seeds/LanguagesData.json index 09708996..6d7c4e92 100644 --- a/api/src/Erp.Platform.DbMigrator/Seeds/LanguagesData.json +++ b/api/src/Erp.Platform.DbMigrator/Seeds/LanguagesData.json @@ -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", @@ -10874,4 +10910,4 @@ "en": "Delete" } ] -} +} \ No newline at end of file diff --git a/ui/src/proxy/data-source/index.ts b/ui/src/proxy/data-source/index.ts index 4c548b92..ad200c53 100644 --- a/ui/src/proxy/data-source/index.ts +++ b/ui/src/proxy/data-source/index.ts @@ -1,2 +1 @@ -export * from './data-source.service' export * from './models' diff --git a/ui/src/services/sql-query-manager.service.ts b/ui/src/services/sql-query-manager.service.ts index 961ad416..e89e2699 100644 --- a/ui/src/services/sql-query-manager.service.ts +++ b/ui/src/services/sql-query-manager.service.ts @@ -60,7 +60,7 @@ export class SqlObjectManagerService { apiService.fetchData( { 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( { 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( { 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( { 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) => + 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 diff --git a/ui/src/views/admin/listForm/Wizard.tsx b/ui/src/views/admin/listForm/Wizard.tsx index 38f15cab..2eb4a163 100644 --- a/ui/src/views/admin/listForm/Wizard.tsx +++ b/ui/src/views/admin/listForm/Wizard.tsx @@ -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: '', diff --git a/ui/src/views/admin/listForm/edit/FormTabDatabaseDataSource.tsx b/ui/src/views/admin/listForm/edit/FormTabDatabaseDataSource.tsx index 144f06d0..08b344c5 100644 --- a/ui/src/views/admin/listForm/edit/FormTabDatabaseDataSource.tsx +++ b/ui/src/views/admin/listForm/edit/FormTabDatabaseDataSource.tsx @@ -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(), diff --git a/ui/src/views/sqlQueryManager/SqlQueryManager.tsx b/ui/src/views/sqlQueryManager/SqlQueryManager.tsx index ce973c61..f89d966c 100644 --- a/ui/src/views/sqlQueryManager/SqlQueryManager.tsx +++ b/ui/src/views/sqlQueryManager/SqlQueryManager.tsx @@ -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(null) const [state, setState] = useState({ 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 = { - 'select': `-- Basic SELECT query + select: `-- Basic SELECT query SELECT Column1, Column2, @@ -153,13 +171,13 @@ WHERE AND IsActive = 1 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', @@ -167,13 +185,13 @@ SET WHERE -- Add your conditions Id = 1;`, - - 'delete': `-- Basic DELETE query + + delete: `-- Basic DELETE query DELETE FROM TableName WHERE -- Add your conditions Id = 1;`, - + 'create-procedure': `-- Create Stored Procedure CREATE PROCEDURE [dbo].[ProcedureName] @Parameter1 INT, @@ -193,7 +211,7 @@ BEGIN AND Column2 = @Parameter2; END GO`, - + 'create-view': `-- Create View CREATE VIEW [dbo].[ViewName] AS @@ -207,7 +225,7 @@ FROM WHERE t1.IsActive = 1; GO`, - + 'create-scalar-function': `-- Create Scalar Function CREATE FUNCTION [dbo].[ScalarFunctionName] ( @@ -227,7 +245,7 @@ BEGIN RETURN @Result; END GO`, - + 'create-table-function': `-- Create Table-Valued Function CREATE FUNCTION [dbo].[TableFunctionName] ( @@ -250,46 +268,95 @@ RETURN AND t.Column2 LIKE '%' + @Parameter2 + '%' AND t.IsActive = 1 ) -GO` +GO`, } - + return templates[templateType] || templates['select'] } - const applyTemplate = useCallback((templateContent: string) => { - setState((prev) => ({ - ...prev, - editorContent: templateContent, - selectedObject: null, - selectedObjectType: null, - executionResult: null, - isDirty: false, - })) + // 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() - toast.push( - - {translate('::App.Platform.TemplateLoaded')} - , - { placement: 'top-center' }, - ) - }, [translate]) - - const handleTemplateSelect = useCallback((template: string, templateType: string) => { - // If template is already provided (e.g., from table click), use it - const templateContent = template || getTemplateContent(templateType) - - // 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) + // 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] : '', + } } - }, [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(() => { 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( {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( - - {translate('::App.Platform.QueryExecutedSuccessfully')} ({result.data.executionTimeMs}ms) - , - { 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( {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: '' }) - 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, - }) - 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 + // Update existing object - open dialog with existing data + const typeMap: Record = { + [SqlObjectType.Query]: 'Query', + [SqlObjectType.View]: 'View', + [SqlObjectType.StoredProcedure]: 'StoredProcedure', + [SqlObjectType.Function]: 'Function', } - 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( - - {translate('::App.Platform.ObjectUpdatedSuccessfully')} - , - { placement: 'top-center' }, - ) - } catch (error: any) { - toast.push( - - {error.response?.data?.error?.message || translate('::App.Platform.FailedToUpdateObject')} - , - { placement: 'top-center' }, - ) + setSaveDialogData({ + name: objectName, + description: state.selectedObject.description || '', + detectedType: typeMap[state.selectedObjectType] || '', + detectedName: objectName, + isExistingObject: true, + }) + setShowSaveDialog(true) + } 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, + }) + 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( - {translate('::App.Platform.QuerySavedSuccessfully')} + {result.data.message || translate('::App.Platform.SavedSuccessfully')} , { 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( {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( + + {error.response?.data?.error?.message || translate('::App.Platform.FailedToLoadColumns')} + , + { placement: 'top-center' }, + ) + } + } + return ( + +
{/* Toolbar */} - +
@@ -578,43 +737,43 @@ GO` variant="solid" icon={} 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')} (Ctrl+S) - {state.selectedObject && - state.selectedObjectType && - state.selectedObjectType !== 1 && ( - - )}
- +
{/* Main Content Area */}
{/* Left Panel - Object Explorer */} -
+
@@ -622,42 +781,90 @@ GO` {translate('::App.Platform.ObjectExplorer')}
-
- -
+
{/* Center Panel - Editor and Results */}
-
-
-
{translate('::App.Platform.QueryEditor')}
-
-
- -
-
- - {state.executionResult && ( -
-
-
{translate('::App.Platform.Results')}
+ {state.executionResult || state.tableColumns ? ( + +
+
+
+ {translate('::App.Platform.QueryEditor')} +
+
+
+ +
-
- +
+
+
+ + + {state.executionResult?.message || + state.tableColumns?.message || + translate('::App.Platform.QueryExecutedSuccessfully')} + +
+
+
+ + {translate('::App.Platform.Rows')}:{' '} + + {state.executionResult?.rowsAffected || + state.executionResult?.data?.length || + state.tableColumns?.rowsAffected || + 0} + + + {state.executionResult && ( + + {translate('::App.Platform.Time')}:{' '} + {state.executionResult.executionTimeMs}ms + + )} +
+
+
+
+ +
+
+ + ) : ( +
+
+
+ {translate('::App.Platform.QueryEditor')} +
+
+
+
)} @@ -698,13 +905,52 @@ GO` onClose={() => setShowSaveDialog(false)} onRequestClose={() => setShowSaveDialog(false)} > -
{translate('::App.Platform.SaveAsNewQuery')}
+
{translate('::App.Platform.SaveQuery')}
+ {/* Detected Object Type */} + {saveDialogData.detectedType && ( +
+
+ +
+
+ {translate('::App.Platform.DetectedObjectType')} +
+
+ {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')} +
+ {saveDialogData.detectedName && ( +
+ {translate('::App.Platform.DetectedName')}:{' '} + {saveDialogData.detectedName} +
+ )} +
+
+
+ )} +
- + setSaveDialogData((prev) => ({ ...prev, name: e.target.value }))} + placeholder={saveDialogData.detectedName || translate('::App.Platform.Name')} + invalid={!saveDialogData.name.trim()} + disabled={saveDialogData.isExistingObject} />
@@ -714,13 +960,18 @@ GO` onChange={(e) => setSaveDialogData((prev) => ({ ...prev, description: e.target.value })) } + placeholder={translate('::App.Platform.Description')} />
-
diff --git a/ui/src/views/sqlQueryManager/components/SqlEditor.tsx b/ui/src/views/sqlQueryManager/components/SqlEditor.tsx index 8ee943b5..ff23cbac 100644 --- a/ui/src/views/sqlQueryManager/components/SqlEditor.tsx +++ b/ui/src/views/sqlQueryManager/components/SqlEditor.tsx @@ -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 = ({ - value, - onChange, - onExecute, - onSave, - readOnly = false, - height = '100%', -}: SqlEditorProps) => { +export interface SqlEditorRef { + getSelectedText: () => string + preserveSelection: () => any + restoreSelection: (selection: any) => void +} + +const SqlEditor = forwardRef(( + { + value, + onChange, + onExecute, + onSave, + readOnly = false, + height = '100%', + }: SqlEditorProps, + ref +) => { const { mode } = useConfig() const editorRef = useRef(null) const monacoRef = useRef(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 = ({
) -} +}) + +SqlEditor.displayName = 'SqlEditor' export default SqlEditor diff --git a/ui/src/views/sqlQueryManager/components/SqlObjectExplorer.tsx b/ui/src/views/sqlQueryManager/components/SqlObjectExplorer.tsx index 54d29a7a..aae2960b 100644 --- a/ui/src/views/sqlQueryManager/components/SqlObjectExplorer.tsx +++ b/ui/src/views/sqlQueryManager/components/SqlObjectExplorer.tsx @@ -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([]) @@ -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, - type: 'object' as const, - objectType: 2 as SqlObjectType, - data: sp, - })) || [], + 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: p, + }; + }) || [], }, { id: 'views', @@ -202,13 +208,18 @@ const SqlObjectExplorer = ({ objectType: 3, expanded: expandedNodes.has('views'), children: - allObjects.views.map((v) => ({ - id: v.id || '', - label: v.displayName || v.viewName, - type: 'object' as const, - objectType: 3 as SqlObjectType, - data: 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}${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) => ({ - id: f.id || '', - label: f.displayName || f.functionName, - type: 'object' as const, - objectType: 4 as SqlObjectType, - data: 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}${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 newSet = new Set(expandedNodes) - + if (newSet.has(nodeId)) { newSet.delete(nodeId) setExpandedNodes(newSet) } else { newSet.add(nodeId) setExpandedNodes(newSet) - + // If it's a table node and hasn't loaded columns yet, load them if (nodeId.startsWith('table-') && dataSource) { const tableNode = findNodeById(treeData, nodeId) if (tableNode && (!tableNode.children || tableNode.children.length === 0)) { const tableData = tableNode.data as any const columns = await loadTableColumns(tableData.schemaName, tableData.tableName) - + // Update tree data with columns setTreeData((prevTree) => { return updateNodeChildren(prevTree, nodeId, columns) @@ -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 } @@ -329,7 +349,7 @@ const SqlObjectExplorer = ({ return null }) .filter((node) => node !== null) as TreeNode[] - + return filtered } @@ -344,14 +364,13 @@ const SqlObjectExplorer = ({ if ((node.data as any).templateType && onTemplateSelect) { const templateType = (node.data as any).templateType onTemplateSelect('', templateType) // Template content will be generated in parent - } + } // Check if it's a table else if (node.id.startsWith('table-') && onTemplateSelect) { 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) } } @@ -359,12 +378,12 @@ 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 } - + setContextMenu({ show: true, x: e.clientX, @@ -423,7 +442,7 @@ const SqlObjectExplorer = ({ if (node.type === 'folder') { const isExpanded = expandedNodes.has(node.id) - + // Templates folder if (node.id === 'templates') return isExpanded ? ( @@ -431,7 +450,7 @@ const SqlObjectExplorer = ({ ) : ( ) - + // Tables folder if (node.id === 'tables') return isExpanded ? ( @@ -439,7 +458,7 @@ const SqlObjectExplorer = ({ ) : ( ) - + if (node.objectType === 1) return isExpanded ? ( @@ -474,12 +493,12 @@ const SqlObjectExplorer = ({ if ((node.data as any)?.templateType) { return } - + // Check if it's a table if (node.id.startsWith('table-')) { return } - + if (node.objectType === 1) return if (node.objectType === 2) return if (node.objectType === 3) return @@ -487,7 +506,7 @@ const SqlObjectExplorer = ({ } if (node.type === 'column') { - return + return } return @@ -509,7 +528,9 @@ const SqlObjectExplorer = ({ onContextMenu={(e) => !isColumn && handleContextMenu(e, node)} > {getIcon(node)} - {node.label} + + {node.label} +
{isExpanded && node.children && ( @@ -545,8 +566,12 @@ const SqlObjectExplorer = ({
{/* Tree Content */} -
- {loading &&
{translate('::App.Platform.Loading')}
} +
+ {loading && ( +
+ {translate('::App.Platform.Loading')} +
+ )} {!loading && treeData.length === 0 && (
{translate('::App.Platform.NoDataSourceSelected')} @@ -568,57 +593,75 @@ const SqlObjectExplorer = ({ className="fixed inset-0 z-40" onClick={() => setContextMenu({ show: false, x: 0, y: 0, node: null })} /> -
- {contextMenu.node?.type === 'object' && ( - <> +
+ {contextMenu.node?.type === 'object' && !contextMenu.node?.id?.startsWith('table-') && ( + <> + + + + + )} + + {contextMenu.node?.type === 'folder' && ( + )} + {contextMenu.node?.id?.startsWith('table-') && contextMenu.node?.data && ( - - )} - - {contextMenu.node?.type === 'folder' && ( - - )} -
+ )} +
)} diff --git a/ui/src/views/sqlQueryManager/components/SqlResultsGrid.tsx b/ui/src/views/sqlQueryManager/components/SqlResultsGrid.tsx index 738ad4dc..075ca69f 100644 --- a/ui/src/views/sqlQueryManager/components/SqlResultsGrid.tsx +++ b/ui/src/views/sqlQueryManager/components/SqlResultsGrid.tsx @@ -55,24 +55,6 @@ const SqlResultsGrid = ({ result }: SqlResultsGridProps) => { return (
- -
-
- - - {result.message || translate('::App.Platform.QueryExecutedSuccessfully')} - -
-
- - {translate('::App.Platform.Rows')}: {result.rowsAffected || dataSource.length} - - - {translate('::App.Platform.Time')}: {result.executionTimeMs}ms - -
-
- {dataSource.length > 0 ? (