Sql Object AppServis başlığında tüm appservisler birleştirildi.

This commit is contained in:
Sedat ÖZTÜRK 2025-12-05 17:56:39 +03:00
parent cb2e007302
commit 932ee406b1
19 changed files with 840 additions and 1154 deletions

View file

@ -0,0 +1,16 @@
namespace Erp.SqlQueryManager.Application.Contracts;
public class DatabaseTableDto
{
public string SchemaName { get; set; }
public string TableName { get; set; }
public string FullName => $"{SchemaName}.{TableName}";
}
public class DatabaseColumnDto
{
public string ColumnName { get; set; }
public string DataType { get; set; }
public bool IsNullable { get; set; }
public int? MaxLength { get; set; }
}

View file

@ -1,29 +0,0 @@
using System;
using System.Threading.Tasks;
using Volo.Abp.Application.Dtos;
using Volo.Abp.Application.Services;
namespace Erp.SqlQueryManager.Application.Contracts;
public interface ISqlFunctionAppService : ICrudAppService<
SqlFunctionDto,
Guid,
PagedAndSortedResultRequestDto,
CreateSqlFunctionDto,
UpdateSqlFunctionDto>
{
/// <summary>
/// Deploy function to database
/// </summary>
Task<SqlQueryExecutionResultDto> DeployAsync(DeployFunctionDto input);
/// <summary>
/// Check if function exists in database
/// </summary>
Task<bool> CheckExistsAsync(Guid id);
/// <summary>
/// Drop function from database
/// </summary>
Task<SqlQueryExecutionResultDto> DropAsync(Guid id);
}

View file

@ -0,0 +1,44 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Volo.Abp.Application.Services;
namespace Erp.SqlQueryManager.Application.Contracts;
/// <summary>
/// Unified service for SQL Object Explorer and CRUD operations
/// </summary>
public interface ISqlObjectManagerAppService : IApplicationService
{
/// <summary>
/// Get all SQL objects for Object Explorer (Queries, SPs, Views, Functions, Tables, Templates)
/// </summary>
/// <param name="dataSourceCode">Data source code to filter objects</param>
/// <returns>Combined response with all object types</returns>
Task<SqlObjectExplorerDto> GetAllObjectsAsync(string dataSourceCode);
// Query Operations
Task<SqlQueryDto> CreateQueryAsync(CreateSqlQueryDto input);
Task<SqlQueryDto> UpdateQueryAsync(Guid id, UpdateSqlQueryDto input);
Task DeleteQueryAsync(Guid id);
Task<SqlQueryExecutionResultDto> ExecuteQueryAsync(ExecuteSqlQueryDto input);
Task<SqlQueryExecutionResultDto> ExecuteSavedQueryAsync(Guid id);
// Stored Procedure Operations
Task<SqlStoredProcedureDto> UpdateStoredProcedureAsync(Guid id, UpdateSqlStoredProcedureDto input);
Task DeleteStoredProcedureAsync(Guid id);
Task<SqlQueryExecutionResultDto> DeployStoredProcedureAsync(DeployStoredProcedureDto input);
// View Operations
Task<SqlViewDto> UpdateViewAsync(Guid id, UpdateSqlViewDto input);
Task DeleteViewAsync(Guid id);
Task<SqlQueryExecutionResultDto> DeployViewAsync(DeployViewDto input);
// Function Operations
Task<SqlFunctionDto> UpdateFunctionAsync(Guid id, UpdateSqlFunctionDto input);
Task DeleteFunctionAsync(Guid id);
Task<SqlQueryExecutionResultDto> DeployFunctionAsync(DeployFunctionDto input);
// Database Metadata Operations
Task<List<DatabaseColumnDto>> GetTableColumnsAsync(string dataSourceCode, string schemaName, string tableName);
}

View file

@ -1,39 +0,0 @@
using System;
using System.Threading.Tasks;
using Volo.Abp.Application.Dtos;
using Volo.Abp.Application.Services;
namespace Erp.SqlQueryManager.Application.Contracts;
public interface ISqlQueryAppService : ICrudAppService<
SqlQueryDto,
Guid,
PagedAndSortedResultRequestDto,
CreateSqlQueryDto,
UpdateSqlQueryDto>
{
/// <summary>
/// Execute a SQL query
/// </summary>
Task<SqlQueryExecutionResultDto> ExecuteQueryAsync(ExecuteSqlQueryDto input);
/// <summary>
/// Execute a saved query by ID
/// </summary>
Task<SqlQueryExecutionResultDto> ExecuteSavedQueryAsync(Guid id);
/// <summary>
/// Validate SQL query syntax
/// </summary>
Task<(bool IsValid, string ErrorMessage)> ValidateQueryAsync(string sql);
/// <summary>
/// Activate query
/// </summary>
Task ActivateAsync(Guid id);
/// <summary>
/// Archive query
/// </summary>
Task ArchiveAsync(Guid id);
}

View file

@ -1,29 +0,0 @@
using System;
using System.Threading.Tasks;
using Volo.Abp.Application.Dtos;
using Volo.Abp.Application.Services;
namespace Erp.SqlQueryManager.Application.Contracts;
public interface ISqlStoredProcedureAppService : ICrudAppService<
SqlStoredProcedureDto,
Guid,
PagedAndSortedResultRequestDto,
CreateSqlStoredProcedureDto,
UpdateSqlStoredProcedureDto>
{
/// <summary>
/// Deploy stored procedure to database
/// </summary>
Task<SqlQueryExecutionResultDto> DeployAsync(DeployStoredProcedureDto input);
/// <summary>
/// Check if procedure exists in database
/// </summary>
Task<bool> CheckExistsAsync(Guid id);
/// <summary>
/// Drop procedure from database
/// </summary>
Task<SqlQueryExecutionResultDto> DropAsync(Guid id);
}

View file

@ -1,34 +0,0 @@
using System.Collections.Generic;
using System.Threading.Tasks;
using Erp.SqlQueryManager.Domain.Shared;
using Volo.Abp.Application.Services;
namespace Erp.SqlQueryManager.Application.Contracts;
public interface ISqlTemplateAppService : IApplicationService
{
/// <summary>
/// Get all available query templates
/// </summary>
Task<List<SqlTemplateDto>> GetQueryTemplatesAsync();
/// <summary>
/// Get stored procedure template
/// </summary>
Task<string> GetStoredProcedureTemplateAsync(string procedureName, string schemaName = "dbo");
/// <summary>
/// Get view template
/// </summary>
Task<string> GetViewTemplateAsync(string viewName, string schemaName = "dbo", bool withSchemaBinding = false);
/// <summary>
/// Get function template
/// </summary>
Task<string> GetFunctionTemplateAsync(string functionName, SqlFunctionType functionType, string schemaName = "dbo");
/// <summary>
/// Get specific query template
/// </summary>
Task<string> GetQueryTemplateAsync(string templateType);
}

View file

@ -1,29 +0,0 @@
using System;
using System.Threading.Tasks;
using Volo.Abp.Application.Dtos;
using Volo.Abp.Application.Services;
namespace Erp.SqlQueryManager.Application.Contracts;
public interface ISqlViewAppService : ICrudAppService<
SqlViewDto,
Guid,
PagedAndSortedResultRequestDto,
CreateSqlViewDto,
UpdateSqlViewDto>
{
/// <summary>
/// Deploy view to database
/// </summary>
Task<SqlQueryExecutionResultDto> DeployAsync(DeployViewDto input);
/// <summary>
/// Check if view exists in database
/// </summary>
Task<bool> CheckExistsAsync(Guid id);
/// <summary>
/// Drop view from database
/// </summary>
Task<SqlQueryExecutionResultDto> DropAsync(Guid id);
}

View file

@ -0,0 +1,39 @@
using System.Collections.Generic;
namespace Erp.SqlQueryManager.Application.Contracts;
/// <summary>
/// Combined DTO for Object Explorer containing all SQL objects
/// </summary>
public class SqlObjectExplorerDto
{
/// <summary>
/// SQL Queries
/// </summary>
public List<SqlQueryDto> Queries { get; set; } = new();
/// <summary>
/// Stored Procedures
/// </summary>
public List<SqlStoredProcedureDto> StoredProcedures { get; set; } = new();
/// <summary>
/// Views
/// </summary>
public List<SqlViewDto> Views { get; set; } = new();
/// <summary>
/// Functions
/// </summary>
public List<SqlFunctionDto> Functions { get; set; } = new();
/// <summary>
/// Database Tables
/// </summary>
public List<DatabaseTableDto> Tables { get; set; } = new();
/// <summary>
/// Query Templates
/// </summary>
public List<SqlTemplateDto> Templates { get; set; } = new();
}

View file

@ -1,139 +0,0 @@
using System;
using System.Threading.Tasks;
using Erp.SqlQueryManager.Application.Contracts;
using Erp.SqlQueryManager.Domain.Entities;
using Erp.SqlQueryManager.Domain.Services;
using Volo.Abp.Application.Dtos;
using Volo.Abp.Application.Services;
using Volo.Abp.Domain.Repositories;
namespace Erp.SqlQueryManager.Application;
public class SqlFunctionAppService : CrudAppService<
SqlFunction,
SqlFunctionDto,
Guid,
PagedAndSortedResultRequestDto,
CreateSqlFunctionDto,
UpdateSqlFunctionDto>, ISqlFunctionAppService
{
private readonly ISqlExecutorService _sqlExecutorService;
public SqlFunctionAppService(
IRepository<SqlFunction, Guid> repository,
ISqlExecutorService sqlExecutorService) : base(repository)
{
_sqlExecutorService = sqlExecutorService;
}
public override async Task<SqlFunctionDto> CreateAsync(CreateSqlFunctionDto input)
{
var entity = new SqlFunction(
GuidGenerator.Create(),
input.FunctionName,
input.SchemaName ?? "dbo",
input.DisplayName,
input.FunctionType,
input.FunctionBody,
input.ReturnType,
input.DataSourceCode,
CurrentTenant.Id)
{
Description = input.Description,
Category = input.Category,
Parameters = input.Parameters
};
await Repository.InsertAsync(entity);
return ObjectMapper.Map<SqlFunction, SqlFunctionDto>(entity);
}
public override async Task<SqlFunctionDto> UpdateAsync(Guid id, UpdateSqlFunctionDto input)
{
var entity = await Repository.GetAsync(id);
entity.DisplayName = input.DisplayName;
entity.Description = input.Description;
entity.UpdateBody(input.FunctionBody);
entity.ReturnType = input.ReturnType;
entity.Category = input.Category;
entity.Parameters = input.Parameters;
await Repository.UpdateAsync(entity);
return ObjectMapper.Map<SqlFunction, SqlFunctionDto>(entity);
}
public async Task<SqlQueryExecutionResultDto> DeployAsync(DeployFunctionDto input)
{
var function = await Repository.GetAsync(input.Id);
// Drop if exists and requested
if (input.DropIfExists)
{
var objectType = function.FunctionType.ToString().ToUpperInvariant().Replace("FUNCTION", "_FUNCTION");
await _sqlExecutorService.DropObjectAsync(
function.FunctionName,
$"SQL_{objectType}",
function.DataSourceCode,
function.SchemaName);
}
// Deploy the function
var result = await _sqlExecutorService.DeployFunctionAsync(
function.FunctionBody,
function.DataSourceCode);
if (result.Success)
{
function.MarkAsDeployed();
await Repository.UpdateAsync(function);
}
return MapExecutionResult(result);
}
public async Task<bool> CheckExistsAsync(Guid id)
{
var function = await Repository.GetAsync(id);
var objectType = function.FunctionType.ToString().ToUpperInvariant().Replace("FUNCTION", "_FUNCTION");
return await _sqlExecutorService.CheckObjectExistsAsync(
function.FunctionName,
$"SQL_{objectType}",
function.DataSourceCode,
function.SchemaName);
}
public async Task<SqlQueryExecutionResultDto> DropAsync(Guid id)
{
var function = await Repository.GetAsync(id);
var objectType = function.FunctionType.ToString().ToUpperInvariant().Replace("FUNCTION", "_FUNCTION");
var result = await _sqlExecutorService.DropObjectAsync(
function.FunctionName,
$"SQL_{objectType}",
function.DataSourceCode,
function.SchemaName);
if (result.Success)
{
function.IsDeployed = false;
await Repository.UpdateAsync(function);
}
return MapExecutionResult(result);
}
private SqlQueryExecutionResultDto MapExecutionResult(SqlExecutionResult result)
{
return new SqlQueryExecutionResultDto
{
Success = result.Success,
Message = result.Message,
Data = result.Data,
RowsAffected = result.RowsAffected,
ExecutionTimeMs = result.ExecutionTimeMs,
Metadata = result.Metadata
};
}
}

View file

@ -0,0 +1,417 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
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;
namespace Erp.SqlQueryManager.Application;
/// <summary>
/// Unified service for SQL Object Explorer
/// Combines all SQL objects into a single endpoint
/// </summary>
public class SqlObjectManagerAppService : ApplicationService, ISqlObjectManagerAppService
{
private readonly IRepository<SqlQuery, Guid> _queryRepository;
private readonly IRepository<SqlStoredProcedure, Guid> _procedureRepository;
private readonly IRepository<SqlView, Guid> _viewRepository;
private readonly IRepository<SqlFunction, Guid> _functionRepository;
private readonly ISqlExecutorService _sqlExecutorService;
private readonly ISqlTemplateProvider _templateProvider;
public SqlObjectManagerAppService(
IRepository<SqlQuery, Guid> queryRepository,
IRepository<SqlStoredProcedure, Guid> procedureRepository,
IRepository<SqlView, Guid> viewRepository,
IRepository<SqlFunction, Guid> functionRepository,
ISqlExecutorService sqlExecutorService,
ISqlTemplateProvider templateProvider)
{
_queryRepository = queryRepository;
_procedureRepository = procedureRepository;
_viewRepository = viewRepository;
_functionRepository = functionRepository;
_sqlExecutorService = sqlExecutorService;
_templateProvider = templateProvider;
}
public async Task<SqlObjectExplorerDto> GetAllObjectsAsync(string dataSourceCode)
{
var result = new SqlObjectExplorerDto();
// Get all queries for this data source
var queries = await _queryRepository.GetListAsync();
result.Queries = queries
.Where(q => q.DataSourceCode == dataSourceCode)
.Select(q => new SqlQueryDto
{
Id = q.Id,
Code = q.Code,
Name = q.Name,
Description = q.Description,
QueryText = q.QueryText,
DataSourceCode = q.DataSourceCode,
Status = q.Status,
Category = q.Category,
Tags = q.Tags,
IsModifyingData = q.IsModifyingData,
Parameters = q.Parameters,
ExecutionCount = q.ExecutionCount,
LastExecutedAt = q.LastExecutedAt
})
.ToList();
// Get all stored procedures for this data source
var procedures = await _procedureRepository.GetListAsync();
result.StoredProcedures = procedures
.Where(p => p.DataSourceCode == dataSourceCode)
.Select(p => new SqlStoredProcedureDto
{
Id = p.Id,
ProcedureName = p.ProcedureName,
SchemaName = p.SchemaName,
DisplayName = p.DisplayName,
Description = p.Description,
ProcedureBody = p.ProcedureBody,
DataSourceCode = p.DataSourceCode,
Category = p.Category,
Parameters = p.Parameters,
IsDeployed = p.IsDeployed,
LastDeployedAt = p.LastDeployedAt
})
.ToList();
// Get all views for this data source
var views = await _viewRepository.GetListAsync();
result.Views = views
.Where(v => v.DataSourceCode == dataSourceCode)
.Select(v => new SqlViewDto
{
Id = v.Id,
ViewName = v.ViewName,
SchemaName = v.SchemaName,
DisplayName = v.DisplayName,
Description = v.Description,
ViewDefinition = v.ViewDefinition,
DataSourceCode = v.DataSourceCode,
Category = v.Category,
WithSchemaBinding = v.WithSchemaBinding,
IsDeployed = v.IsDeployed,
LastDeployedAt = v.LastDeployedAt
})
.ToList();
// Get all functions for this data source
var functions = await _functionRepository.GetListAsync();
result.Functions = functions
.Where(f => f.DataSourceCode == dataSourceCode)
.Select(f => new SqlFunctionDto
{
Id = f.Id,
FunctionName = f.FunctionName,
SchemaName = f.SchemaName,
DisplayName = f.DisplayName,
Description = f.Description,
FunctionType = f.FunctionType,
FunctionBody = f.FunctionBody,
ReturnType = f.ReturnType,
DataSourceCode = f.DataSourceCode,
Category = f.Category,
Parameters = f.Parameters,
IsDeployed = f.IsDeployed,
LastDeployedAt = f.LastDeployedAt
})
.ToList();
// Get all database tables
result.Tables = await GetTablesAsync(dataSourceCode);
// Get all templates
result.Templates = _templateProvider.GetAvailableQueryTemplates()
.Select(t => new SqlTemplateDto
{
Type = t.Type,
Name = t.Name,
Description = t.Description,
Template = _templateProvider.GetQueryTemplate(t.Type)
})
.ToList();
return result;
}
private async Task<List<DatabaseTableDto>> GetTablesAsync(string dataSourceCode)
{
var query = @"
SELECT
SCHEMA_NAME(t.schema_id) AS SchemaName,
t.name AS TableName
FROM
sys.tables t
WHERE
t.is_ms_shipped = 0
ORDER BY
SCHEMA_NAME(t.schema_id), t.name";
var result = await _sqlExecutorService.ExecuteQueryAsync(query, dataSourceCode);
var tables = new List<DatabaseTableDto>();
if (result.Success && result.Data != null)
{
foreach (var row in result.Data)
{
var dict = row as System.Collections.Generic.IDictionary<string, object>;
if (dict != null)
{
tables.Add(new DatabaseTableDto
{
SchemaName = dict["SchemaName"]?.ToString() ?? "dbo",
TableName = dict["TableName"]?.ToString() ?? ""
});
}
}
}
return tables;
}
#region Query Operations
public async Task<SqlQueryDto> CreateQueryAsync(CreateSqlQueryDto input)
{
var query = ObjectMapper.Map<CreateSqlQueryDto, SqlQuery>(input);
query.Status = SqlQueryStatus.Draft;
var created = await _queryRepository.InsertAsync(query, autoSave: true);
return ObjectMapper.Map<SqlQuery, SqlQueryDto>(created);
}
public async Task<SqlQueryDto> UpdateQueryAsync(Guid id, UpdateSqlQueryDto input)
{
var query = await _queryRepository.GetAsync(id);
query.Name = input.Name;
query.Description = input.Description;
query.QueryText = input.QueryText;
query.Category = input.Category;
query.Tags = input.Tags;
var updated = await _queryRepository.UpdateAsync(query, autoSave: true);
return ObjectMapper.Map<SqlQuery, SqlQueryDto>(updated);
}
public async Task DeleteQueryAsync(Guid id)
{
await _queryRepository.DeleteAsync(id);
}
public async Task<SqlQueryExecutionResultDto> ExecuteQueryAsync(ExecuteSqlQueryDto input)
{
var result = await _sqlExecutorService.ExecuteQueryAsync(input.QueryText, input.DataSourceCode);
return MapExecutionResult(result);
}
public async Task<SqlQueryExecutionResultDto> ExecuteSavedQueryAsync(Guid id)
{
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);
}
#endregion
#region Stored Procedure Operations
public async Task<SqlStoredProcedureDto> 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;
var updated = await _procedureRepository.UpdateAsync(procedure, autoSave: true);
return ObjectMapper.Map<SqlStoredProcedure, SqlStoredProcedureDto>(updated);
}
public async Task DeleteStoredProcedureAsync(Guid id)
{
await _procedureRepository.DeleteAsync(id);
}
public async Task<SqlQueryExecutionResultDto> DeployStoredProcedureAsync(DeployStoredProcedureDto input)
{
var procedure = await _procedureRepository.GetAsync(input.Id);
var result = await _sqlExecutorService.DeployStoredProcedureAsync(
procedure.ProcedureBody,
procedure.DataSourceCode
);
if (result.Success)
{
procedure.IsDeployed = true;
procedure.LastDeployedAt = DateTime.UtcNow;
await _procedureRepository.UpdateAsync(procedure, autoSave: true);
}
return MapExecutionResult(result);
}
#endregion
#region View Operations
public async Task<SqlViewDto> 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;
var updated = await _viewRepository.UpdateAsync(view, autoSave: true);
return ObjectMapper.Map<SqlView, SqlViewDto>(updated);
}
public async Task DeleteViewAsync(Guid id)
{
await _viewRepository.DeleteAsync(id);
}
public async Task<SqlQueryExecutionResultDto> DeployViewAsync(DeployViewDto input)
{
var view = await _viewRepository.GetAsync(input.Id);
var result = await _sqlExecutorService.DeployViewAsync(
view.ViewDefinition,
view.DataSourceCode
);
if (result.Success)
{
view.IsDeployed = true;
view.LastDeployedAt = DateTime.UtcNow;
await _viewRepository.UpdateAsync(view, autoSave: true);
}
return MapExecutionResult(result);
}
#endregion
#region Function Operations
public async Task<SqlFunctionDto> 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;
var updated = await _functionRepository.UpdateAsync(function, autoSave: true);
return ObjectMapper.Map<SqlFunction, SqlFunctionDto>(updated);
}
public async Task DeleteFunctionAsync(Guid id)
{
await _functionRepository.DeleteAsync(id);
}
public async Task<SqlQueryExecutionResultDto> DeployFunctionAsync(DeployFunctionDto input)
{
var function = await _functionRepository.GetAsync(input.Id);
var result = await _sqlExecutorService.DeployFunctionAsync(
function.FunctionBody,
function.DataSourceCode
);
if (result.Success)
{
function.IsDeployed = true;
function.LastDeployedAt = DateTime.UtcNow;
await _functionRepository.UpdateAsync(function, autoSave: true);
}
return MapExecutionResult(result);
}
#endregion
#region Database Metadata Operations
public async Task<List<DatabaseColumnDto>> GetTableColumnsAsync(string dataSourceCode, string schemaName, string tableName)
{
var query = $@"
SELECT
c.name AS ColumnName,
TYPE_NAME(c.user_type_id) AS DataType,
c.is_nullable AS IsNullable,
c.max_length AS MaxLength
FROM
sys.columns c
INNER JOIN sys.tables t ON c.object_id = t.object_id
INNER JOIN sys.schemas s ON t.schema_id = s.schema_id
WHERE
s.name = '{schemaName}'
AND t.name = '{tableName}'
ORDER BY
c.column_id";
var result = await _sqlExecutorService.ExecuteQueryAsync(query, dataSourceCode);
var columns = new List<DatabaseColumnDto>();
if (result.Success && result.Data != null)
{
foreach (var row in result.Data)
{
var dict = row as System.Collections.Generic.IDictionary<string, object>;
if (dict != null)
{
columns.Add(new DatabaseColumnDto
{
ColumnName = dict["ColumnName"]?.ToString() ?? "",
DataType = dict["DataType"]?.ToString() ?? "",
IsNullable = dict["IsNullable"] is bool b && b,
MaxLength = dict["MaxLength"] != null ? int.Parse(dict["MaxLength"].ToString()) : null
});
}
}
}
return columns;
}
#endregion
#region Helper Methods
private SqlQueryExecutionResultDto MapExecutionResult(SqlExecutionResult result)
{
return new SqlQueryExecutionResultDto
{
Success = result.Success,
Message = result.Message,
Data = result.Data,
RowsAffected = result.RowsAffected,
ExecutionTimeMs = result.ExecutionTimeMs,
Metadata = result.Metadata
};
}
#endregion
}

View file

@ -1,124 +0,0 @@
using System;
using System.Linq;
using System.Threading.Tasks;
using Erp.SqlQueryManager.Application.Contracts;
using Erp.SqlQueryManager.Domain.Entities;
using Erp.SqlQueryManager.Domain.Services;
using Microsoft.AspNetCore.Mvc;
using Volo.Abp.Application.Dtos;
using Volo.Abp.Application.Services;
using Volo.Abp.Domain.Repositories;
namespace Erp.SqlQueryManager.Application;
public class SqlQueryAppService : CrudAppService<
SqlQuery,
SqlQueryDto,
Guid,
PagedAndSortedResultRequestDto,
CreateSqlQueryDto,
UpdateSqlQueryDto>, ISqlQueryAppService
{
private readonly ISqlExecutorService _sqlExecutorService;
public SqlQueryAppService(
IRepository<SqlQuery, Guid> repository,
ISqlExecutorService sqlExecutorService) : base(repository)
{
_sqlExecutorService = sqlExecutorService;
}
public override async Task<SqlQueryDto> CreateAsync(CreateSqlQueryDto input)
{
var entity = new SqlQuery(
GuidGenerator.Create(),
input.Code,
input.Name,
input.QueryText,
input.DataSourceCode,
CurrentTenant.Id)
{
Description = input.Description,
Category = input.Category,
Tags = input.Tags,
IsModifyingData = input.IsModifyingData,
Parameters = input.Parameters
};
await Repository.InsertAsync(entity);
return ObjectMapper.Map<SqlQuery, SqlQueryDto>(entity);
}
public override async Task<SqlQueryDto> UpdateAsync(Guid id, UpdateSqlQueryDto input)
{
var entity = await Repository.GetAsync(id);
entity.Name = input.Name;
entity.Description = input.Description;
entity.UpdateQueryText(input.QueryText);
entity.DataSourceCode = input.DataSourceCode;
entity.Category = input.Category;
entity.Tags = input.Tags;
entity.IsModifyingData = input.IsModifyingData;
entity.Parameters = input.Parameters;
await Repository.UpdateAsync(entity);
return ObjectMapper.Map<SqlQuery, SqlQueryDto>(entity);
}
public async Task<SqlQueryExecutionResultDto> ExecuteQueryAsync(ExecuteSqlQueryDto input)
{
var result = input.QueryText.TrimStart().StartsWith("SELECT", StringComparison.OrdinalIgnoreCase)
? await _sqlExecutorService.ExecuteQueryAsync(input.QueryText, input.DataSourceCode, input.Parameters)
: await _sqlExecutorService.ExecuteNonQueryAsync(input.QueryText, input.DataSourceCode, input.Parameters);
return MapExecutionResult(result);
}
public async Task<SqlQueryExecutionResultDto> ExecuteSavedQueryAsync(Guid id)
{
var query = await Repository.GetAsync(id);
var result = query.IsModifyingData
? await _sqlExecutorService.ExecuteNonQueryAsync(query.QueryText, query.DataSourceCode)
: await _sqlExecutorService.ExecuteQueryAsync(query.QueryText, query.DataSourceCode);
// Update execution statistics
query.MarkAsExecuted();
await Repository.UpdateAsync(query);
return MapExecutionResult(result);
}
public async Task<(bool IsValid, string ErrorMessage)> ValidateQueryAsync(string sql)
{
return await _sqlExecutorService.ValidateSqlAsync(sql);
}
public async Task ActivateAsync(Guid id)
{
var entity = await Repository.GetAsync(id);
entity.Activate();
await Repository.UpdateAsync(entity);
}
public async Task ArchiveAsync(Guid id)
{
var entity = await Repository.GetAsync(id);
entity.Archive();
await Repository.UpdateAsync(entity);
}
private SqlQueryExecutionResultDto MapExecutionResult(SqlExecutionResult result)
{
return new SqlQueryExecutionResultDto
{
Success = result.Success,
Message = result.Message,
Data = result.Data,
RowsAffected = result.RowsAffected,
ExecutionTimeMs = result.ExecutionTimeMs,
Metadata = result.Metadata
};
}
}

View file

@ -1,134 +0,0 @@
using System;
using System.Threading.Tasks;
using Erp.SqlQueryManager.Application.Contracts;
using Erp.SqlQueryManager.Domain.Entities;
using Erp.SqlQueryManager.Domain.Services;
using Microsoft.AspNetCore.Mvc;
using Volo.Abp.Application.Dtos;
using Volo.Abp.Application.Services;
using Volo.Abp.Domain.Repositories;
namespace Erp.SqlQueryManager.Application;
public class SqlStoredProcedureAppService : CrudAppService<
SqlStoredProcedure,
SqlStoredProcedureDto,
Guid,
PagedAndSortedResultRequestDto,
CreateSqlStoredProcedureDto,
UpdateSqlStoredProcedureDto>, ISqlStoredProcedureAppService
{
private readonly ISqlExecutorService _sqlExecutorService;
public SqlStoredProcedureAppService(
IRepository<SqlStoredProcedure, Guid> repository,
ISqlExecutorService sqlExecutorService) : base(repository)
{
_sqlExecutorService = sqlExecutorService;
}
public override async Task<SqlStoredProcedureDto> CreateAsync(CreateSqlStoredProcedureDto input)
{
var entity = new SqlStoredProcedure(
GuidGenerator.Create(),
input.ProcedureName,
input.SchemaName ?? "dbo",
input.DisplayName,
input.ProcedureBody,
input.DataSourceCode,
CurrentTenant.Id)
{
Description = input.Description,
Category = input.Category,
Parameters = input.Parameters
};
await Repository.InsertAsync(entity);
return ObjectMapper.Map<SqlStoredProcedure, SqlStoredProcedureDto>(entity);
}
public override async Task<SqlStoredProcedureDto> UpdateAsync(Guid id, UpdateSqlStoredProcedureDto input)
{
var entity = await Repository.GetAsync(id);
entity.DisplayName = input.DisplayName;
entity.Description = input.Description;
entity.UpdateBody(input.ProcedureBody);
entity.Category = input.Category;
entity.Parameters = input.Parameters;
await Repository.UpdateAsync(entity);
return ObjectMapper.Map<SqlStoredProcedure, SqlStoredProcedureDto>(entity);
}
public async Task<SqlQueryExecutionResultDto> DeployAsync(DeployStoredProcedureDto input)
{
var procedure = await Repository.GetAsync(input.Id);
// Drop if exists and requested
if (input.DropIfExists)
{
await _sqlExecutorService.DropObjectAsync(
procedure.ProcedureName,
"SQL_STORED_PROCEDURE",
procedure.DataSourceCode,
procedure.SchemaName);
}
// Deploy the procedure
var result = await _sqlExecutorService.DeployStoredProcedureAsync(
procedure.ProcedureBody,
procedure.DataSourceCode);
if (result.Success)
{
procedure.MarkAsDeployed();
await Repository.UpdateAsync(procedure);
}
return MapExecutionResult(result);
}
public async Task<bool> CheckExistsAsync(Guid id)
{
var procedure = await Repository.GetAsync(id);
return await _sqlExecutorService.CheckObjectExistsAsync(
procedure.ProcedureName,
"SQL_STORED_PROCEDURE",
procedure.DataSourceCode,
procedure.SchemaName);
}
public async Task<SqlQueryExecutionResultDto> DropAsync(Guid id)
{
var procedure = await Repository.GetAsync(id);
var result = await _sqlExecutorService.DropObjectAsync(
procedure.ProcedureName,
"SQL_STORED_PROCEDURE",
procedure.DataSourceCode,
procedure.SchemaName);
if (result.Success)
{
procedure.IsDeployed = false;
await Repository.UpdateAsync(procedure);
}
return MapExecutionResult(result);
}
private SqlQueryExecutionResultDto MapExecutionResult(SqlExecutionResult result)
{
return new SqlQueryExecutionResultDto
{
Success = result.Success,
Message = result.Message,
Data = result.Data,
RowsAffected = result.RowsAffected,
ExecutionTimeMs = result.ExecutionTimeMs,
Metadata = result.Metadata
};
}
}

View file

@ -1,59 +0,0 @@
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Erp.SqlQueryManager.Application.Contracts;
using Erp.SqlQueryManager.Domain.Services;
using Erp.SqlQueryManager.Domain.Shared;
using Microsoft.AspNetCore.Mvc;
using Volo.Abp.Application.Services;
namespace Erp.SqlQueryManager.Application;
public class SqlTemplateAppService : ApplicationService, ISqlTemplateAppService
{
private readonly ISqlTemplateProvider _templateProvider;
public SqlTemplateAppService(ISqlTemplateProvider templateProvider)
{
_templateProvider = templateProvider;
}
public Task<List<SqlTemplateDto>> GetQueryTemplatesAsync()
{
var templates = _templateProvider.GetAvailableQueryTemplates()
.Select(t => new SqlTemplateDto
{
Type = t.Type,
Name = t.Name,
Description = t.Description,
Template = _templateProvider.GetQueryTemplate(t.Type)
})
.ToList();
return Task.FromResult(templates);
}
public Task<string> GetStoredProcedureTemplateAsync(string procedureName, string schemaName = "dbo")
{
var template = _templateProvider.GetStoredProcedureTemplate(procedureName, schemaName);
return Task.FromResult(template);
}
public Task<string> GetViewTemplateAsync(string viewName, string schemaName = "dbo", bool withSchemaBinding = false)
{
var template = _templateProvider.GetViewTemplate(viewName, schemaName, withSchemaBinding);
return Task.FromResult(template);
}
public Task<string> GetFunctionTemplateAsync(string functionName, SqlFunctionType functionType, string schemaName = "dbo")
{
var template = _templateProvider.GetFunctionTemplate(functionName, functionType, schemaName);
return Task.FromResult(template);
}
public Task<string> GetQueryTemplateAsync(string templateType)
{
var template = _templateProvider.GetQueryTemplate(templateType);
return Task.FromResult(template);
}
}

View file

@ -1,133 +0,0 @@
using System;
using System.Threading.Tasks;
using Erp.SqlQueryManager.Application.Contracts;
using Erp.SqlQueryManager.Domain.Entities;
using Erp.SqlQueryManager.Domain.Services;
using Volo.Abp.Application.Dtos;
using Volo.Abp.Application.Services;
using Volo.Abp.Domain.Repositories;
namespace Erp.SqlQueryManager.Application;
public class SqlViewAppService : CrudAppService<
SqlView,
SqlViewDto,
Guid,
PagedAndSortedResultRequestDto,
CreateSqlViewDto,
UpdateSqlViewDto>, ISqlViewAppService
{
private readonly ISqlExecutorService _sqlExecutorService;
public SqlViewAppService(
IRepository<SqlView, Guid> repository,
ISqlExecutorService sqlExecutorService) : base(repository)
{
_sqlExecutorService = sqlExecutorService;
}
public override async Task<SqlViewDto> CreateAsync(CreateSqlViewDto input)
{
var entity = new SqlView(
GuidGenerator.Create(),
input.ViewName,
input.SchemaName ?? "dbo",
input.DisplayName,
input.ViewDefinition,
input.DataSourceCode,
CurrentTenant.Id)
{
Description = input.Description,
Category = input.Category,
WithSchemaBinding = input.WithSchemaBinding
};
await Repository.InsertAsync(entity);
return ObjectMapper.Map<SqlView, SqlViewDto>(entity);
}
public override async Task<SqlViewDto> UpdateAsync(Guid id, UpdateSqlViewDto input)
{
var entity = await Repository.GetAsync(id);
entity.DisplayName = input.DisplayName;
entity.Description = input.Description;
entity.UpdateDefinition(input.ViewDefinition);
entity.Category = input.Category;
entity.WithSchemaBinding = input.WithSchemaBinding;
await Repository.UpdateAsync(entity);
return ObjectMapper.Map<SqlView, SqlViewDto>(entity);
}
public async Task<SqlQueryExecutionResultDto> DeployAsync(DeployViewDto input)
{
var view = await Repository.GetAsync(input.Id);
// Drop if exists and requested
if (input.DropIfExists)
{
await _sqlExecutorService.DropObjectAsync(
view.ViewName,
"VIEW",
view.DataSourceCode,
view.SchemaName);
}
// Deploy the view
var result = await _sqlExecutorService.DeployViewAsync(
view.ViewDefinition,
view.DataSourceCode);
if (result.Success)
{
view.MarkAsDeployed();
await Repository.UpdateAsync(view);
}
return MapExecutionResult(result);
}
public async Task<bool> CheckExistsAsync(Guid id)
{
var view = await Repository.GetAsync(id);
return await _sqlExecutorService.CheckObjectExistsAsync(
view.ViewName,
"VIEW",
view.DataSourceCode,
view.SchemaName);
}
public async Task<SqlQueryExecutionResultDto> DropAsync(Guid id)
{
var view = await Repository.GetAsync(id);
var result = await _sqlExecutorService.DropObjectAsync(
view.ViewName,
"VIEW",
view.DataSourceCode,
view.SchemaName);
if (result.Success)
{
view.IsDeployed = false;
await Repository.UpdateAsync(view);
}
return MapExecutionResult(result);
}
private SqlQueryExecutionResultDto MapExecutionResult(SqlExecutionResult result)
{
return new SqlQueryExecutionResultDto
{
Success = result.Success,
Message = result.Message,
Data = result.Data,
RowsAffected = result.RowsAffected,
ExecutionTimeMs = result.ExecutionTimeMs,
Metadata = result.Metadata
};
}
}

View file

@ -10513,6 +10513,12 @@
"tr": "View",
"en": "View"
},
{
"resourceName": "Platform",
"key": "App.Platform.Tables",
"tr": "Tablolar",
"en": "Tables"
},
{
"resourceName": "Platform",
"key": "App.Platform.Function",

View file

@ -244,3 +244,27 @@ export interface GetSqlViewsInput extends PagedAndSortedResultRequestDto {
status?: SqlQueryStatus
category?: string
}
// Database Metadata DTOs
export interface DatabaseTableDto {
schemaName: string
tableName: string
fullName: string
}
export interface DatabaseColumnDto {
columnName: string
dataType: string
isNullable: boolean
maxLength?: number
}
// Unified Object Explorer Response
export interface SqlObjectExplorerDto {
queries: SqlQueryDto[]
storedProcedures: SqlStoredProcedureDto[]
views: SqlViewDto[]
functions: SqlFunctionDto[]
tables: DatabaseTableDto[]
templates: SqlTemplateDto[]
}

View file

@ -1,159 +1,66 @@
import apiService, { Config } from '@/services/api.service'
import type { PagedResultDto } from '@/proxy'
import type {
SqlFunctionDto,
CreateSqlFunctionDto,
UpdateSqlFunctionDto,
DeployFunctionDto,
SqlQueryDto,
CreateSqlQueryDto,
UpdateSqlQueryDto,
CreateSqlQueryDto,
ExecuteSqlQueryDto,
ExecuteSavedQueryDto,
ValidateQueryDto,
SqlStoredProcedureDto,
CreateSqlStoredProcedureDto,
UpdateSqlStoredProcedureDto,
DeployStoredProcedureDto,
SqlViewDto,
CreateSqlViewDto,
UpdateSqlViewDto,
DeployViewDto,
SqlTemplateDto,
DeployFunctionDto,
SqlQueryExecutionResultDto,
GetSqlFunctionsInput,
GetSqlQueriesInput,
GetSqlStoredProceduresInput,
GetSqlViewsInput,
DatabaseColumnDto,
DatabaseTableDto,
SqlObjectExplorerDto,
} from '@/proxy/sql-query-manager/models'
export class SqlFunctionService {
export class SqlObjectManagerService {
apiName = 'Default'
create = (input: CreateSqlFunctionDto, config?: Partial<Config>) =>
apiService.fetchData<SqlFunctionDto, CreateSqlFunctionDto>(
{
method: 'POST',
url: '/api/app/sql-function',
data: input,
},
{ apiName: this.apiName, ...config },
)
getList = (input: GetSqlFunctionsInput, config?: Partial<Config>) =>
apiService.fetchData<PagedResultDto<SqlFunctionDto>, GetSqlFunctionsInput>(
/**
* Get all SQL objects for Object Explorer in a single call
*/
getAllObjects = (dataSourceCode: string, config?: Partial<Config>) =>
apiService.fetchData<SqlObjectExplorerDto, void>(
{
method: 'GET',
url: '/api/app/sql-function',
params: input,
url: '/api/app/sql-object-manager/objects',
params: { dataSourceCode },
},
{ apiName: this.apiName, ...config },
)
get = (id: string, config?: Partial<Config>) =>
apiService.fetchData<SqlFunctionDto, void>(
{
method: 'GET',
url: `/api/app/sql-function/${id}`,
},
{ apiName: this.apiName, ...config },
)
update = (id: string, input: UpdateSqlFunctionDto, config?: Partial<Config>) =>
apiService.fetchData<SqlFunctionDto, UpdateSqlFunctionDto>(
{
method: 'PUT',
url: `/api/app/sql-function/${id}`,
data: input,
},
{ apiName: this.apiName, ...config },
)
delete = (id: string, config?: Partial<Config>) =>
apiService.fetchData<void, void>(
{
method: 'DELETE',
url: `/api/app/sql-function/${id}`,
},
{ apiName: this.apiName, ...config },
)
deploy = (input: DeployFunctionDto, config?: Partial<Config>) =>
apiService.fetchData<SqlQueryExecutionResultDto, DeployFunctionDto>(
{
method: 'POST',
url: '/api/app/sql-function/deploy',
data: input,
},
{ apiName: this.apiName, ...config },
)
checkExists = (id: string, config?: Partial<Config>) =>
apiService.fetchData<boolean, void>(
{
method: 'POST',
url: `/api/app/sql-function/${id}/check-exists`,
},
{ apiName: this.apiName, ...config },
)
drop = (id: string, config?: Partial<Config>) =>
apiService.fetchData<SqlQueryExecutionResultDto, void>(
{
method: 'POST',
url: `/api/app/sql-function/${id}/drop`,
},
{ apiName: this.apiName, ...config },
)
}
export class SqlQueryService {
apiName = 'Default'
create = (input: CreateSqlQueryDto, config?: Partial<Config>) =>
// Query Operations
createQuery = (input: CreateSqlQueryDto, config?: Partial<Config>) =>
apiService.fetchData<SqlQueryDto, CreateSqlQueryDto>(
{
method: 'POST',
url: '/api/app/sql-query',
url: '/api/app/sql-object-manager/query',
data: input,
},
{ apiName: this.apiName, ...config },
)
getList = (input: GetSqlQueriesInput, config?: Partial<Config>) =>
apiService.fetchData<PagedResultDto<SqlQueryDto>, GetSqlQueriesInput>(
{
method: 'GET',
url: '/api/app/sql-query',
params: input,
},
{ apiName: this.apiName, ...config },
)
get = (id: string, config?: Partial<Config>) =>
apiService.fetchData<SqlQueryDto, void>(
{
method: 'GET',
url: `/api/app/sql-query/${id}`,
},
{ apiName: this.apiName, ...config },
)
update = (id: string, input: UpdateSqlQueryDto, config?: Partial<Config>) =>
updateQuery = (id: string, input: UpdateSqlQueryDto, config?: Partial<Config>) =>
apiService.fetchData<SqlQueryDto, UpdateSqlQueryDto>(
{
method: 'PUT',
url: `/api/app/sql-query/${id}`,
url: `/api/app/sql-object-manager/query/${id}`,
data: input,
},
{ apiName: this.apiName, ...config },
)
delete = (id: string, config?: Partial<Config>) =>
deleteQuery = (id: string, config?: Partial<Config>) =>
apiService.fetchData<void, void>(
{
method: 'DELETE',
url: `/api/app/sql-query/${id}`,
url: `/api/app/sql-object-manager/query/${id}`,
},
{ apiName: this.apiName, ...config },
)
@ -162,281 +69,122 @@ export class SqlQueryService {
apiService.fetchData<SqlQueryExecutionResultDto, ExecuteSqlQueryDto>(
{
method: 'POST',
url: '/api/app/sql-query/execute-query',
url: '/api/app/sql-object-manager/execute-query',
data: input,
},
{ apiName: this.apiName, ...config },
)
executeSavedQuery = (id: string, parameters?: Record<string, any>, config?: Partial<Config>) =>
apiService.fetchData<SqlQueryExecutionResultDto, ExecuteSavedQueryDto>(
executeSavedQuery = (id: string, config?: Partial<Config>) =>
apiService.fetchData<SqlQueryExecutionResultDto, void>(
{
method: 'POST',
url: `/api/app/sql-query/${id}/execute-saved-query`,
data: { id, parameters },
url: `/api/app/sql-object-manager/execute-saved-query/${id}`,
},
{ apiName: this.apiName, ...config },
)
validateQuery = (input: ValidateQueryDto, config?: Partial<Config>) =>
apiService.fetchData<SqlQueryExecutionResultDto, ValidateQueryDto>(
{
method: 'POST',
url: '/api/app/sql-query/validate-query',
data: input,
},
{ apiName: this.apiName, ...config },
)
activate = (id: string, config?: Partial<Config>) =>
apiService.fetchData<void, void>(
{
method: 'POST',
url: `/api/app/sql-query/${id}/activate`,
},
{ apiName: this.apiName, ...config },
)
archive = (id: string, config?: Partial<Config>) =>
apiService.fetchData<void, void>(
{
method: 'POST',
url: `/api/app/sql-query/${id}/archive`,
},
{ apiName: this.apiName, ...config },
)
}
export class SqlStoredProcedureService {
apiName = 'Default'
create = (input: CreateSqlStoredProcedureDto, config?: Partial<Config>) =>
apiService.fetchData<SqlStoredProcedureDto, CreateSqlStoredProcedureDto>(
{
method: 'POST',
url: '/api/app/sql-stored-procedure',
data: input,
},
{ apiName: this.apiName, ...config },
)
getList = (input: GetSqlStoredProceduresInput, config?: Partial<Config>) =>
apiService.fetchData<PagedResultDto<SqlStoredProcedureDto>, GetSqlStoredProceduresInput>(
{
method: 'GET',
url: '/api/app/sql-stored-procedure',
params: input,
},
{ apiName: this.apiName, ...config },
)
get = (id: string, config?: Partial<Config>) =>
apiService.fetchData<SqlStoredProcedureDto, void>(
{
method: 'GET',
url: `/api/app/sql-stored-procedure/${id}`,
},
{ apiName: this.apiName, ...config },
)
update = (id: string, input: UpdateSqlStoredProcedureDto, config?: Partial<Config>) =>
// Stored Procedure Operations
updateStoredProcedure = (id: string, input: UpdateSqlStoredProcedureDto, config?: Partial<Config>) =>
apiService.fetchData<SqlStoredProcedureDto, UpdateSqlStoredProcedureDto>(
{
method: 'PUT',
url: `/api/app/sql-stored-procedure/${id}`,
url: `/api/app/sql-object-manager/stored-procedure/${id}`,
data: input,
},
{ apiName: this.apiName, ...config },
)
delete = (id: string, config?: Partial<Config>) =>
deleteStoredProcedure = (id: string, config?: Partial<Config>) =>
apiService.fetchData<void, void>(
{
method: 'DELETE',
url: `/api/app/sql-stored-procedure/${id}`,
url: `/api/app/sql-object-manager/stored-procedure/${id}`,
},
{ apiName: this.apiName, ...config },
)
deploy = (input: DeployStoredProcedureDto, config?: Partial<Config>) =>
deployStoredProcedure = (input: DeployStoredProcedureDto, config?: Partial<Config>) =>
apiService.fetchData<SqlQueryExecutionResultDto, DeployStoredProcedureDto>(
{
method: 'POST',
url: '/api/app/sql-stored-procedure/deploy',
url: '/api/app/sql-object-manager/deploy-stored-procedure',
data: input,
},
{ apiName: this.apiName, ...config },
)
checkExists = (id: string, config?: Partial<Config>) =>
apiService.fetchData<boolean, void>(
{
method: 'POST',
url: `/api/app/sql-stored-procedure/${id}/check-exists`,
},
{ apiName: this.apiName, ...config },
)
drop = (id: string, config?: Partial<Config>) =>
apiService.fetchData<SqlQueryExecutionResultDto, void>(
{
method: 'POST',
url: `/api/app/sql-stored-procedure/${id}/drop`,
},
{ apiName: this.apiName, ...config },
)
}
export class SqlViewService {
apiName = 'Default'
create = (input: CreateSqlViewDto, config?: Partial<Config>) =>
apiService.fetchData<SqlViewDto, CreateSqlViewDto>(
{
method: 'POST',
url: '/api/app/sql-view',
data: input,
},
{ apiName: this.apiName, ...config },
)
getList = (input: GetSqlViewsInput, config?: Partial<Config>) =>
apiService.fetchData<PagedResultDto<SqlViewDto>, GetSqlViewsInput>(
{
method: 'GET',
url: '/api/app/sql-view',
params: input,
},
{ apiName: this.apiName, ...config },
)
get = (id: string, config?: Partial<Config>) =>
apiService.fetchData<SqlViewDto, void>(
{
method: 'GET',
url: `/api/app/sql-view/${id}`,
},
{ apiName: this.apiName, ...config },
)
update = (id: string, input: UpdateSqlViewDto, config?: Partial<Config>) =>
// View Operations
updateView = (id: string, input: UpdateSqlViewDto, config?: Partial<Config>) =>
apiService.fetchData<SqlViewDto, UpdateSqlViewDto>(
{
method: 'PUT',
url: `/api/app/sql-view/${id}`,
url: `/api/app/sql-object-manager/view/${id}`,
data: input,
},
{ apiName: this.apiName, ...config },
)
delete = (id: string, config?: Partial<Config>) =>
deleteView = (id: string, config?: Partial<Config>) =>
apiService.fetchData<void, void>(
{
method: 'DELETE',
url: `/api/app/sql-view/${id}`,
url: `/api/app/sql-object-manager/view/${id}`,
},
{ apiName: this.apiName, ...config },
)
deploy = (input: DeployViewDto, config?: Partial<Config>) =>
deployView = (input: DeployViewDto, config?: Partial<Config>) =>
apiService.fetchData<SqlQueryExecutionResultDto, DeployViewDto>(
{
method: 'POST',
url: '/api/app/sql-view/deploy',
url: '/api/app/sql-object-manager/deploy-view',
data: input,
},
{ apiName: this.apiName, ...config },
)
checkExists = (id: string, config?: Partial<Config>) =>
apiService.fetchData<boolean, void>(
// Function Operations
updateFunction = (id: string, input: UpdateSqlFunctionDto, config?: Partial<Config>) =>
apiService.fetchData<SqlFunctionDto, UpdateSqlFunctionDto>(
{
method: 'POST',
url: `/api/app/sql-view/${id}/check-exists`,
method: 'PUT',
url: `/api/app/sql-object-manager/function/${id}`,
data: input,
},
{ apiName: this.apiName, ...config },
)
drop = (id: string, config?: Partial<Config>) =>
apiService.fetchData<SqlQueryExecutionResultDto, void>(
deleteFunction = (id: string, config?: Partial<Config>) =>
apiService.fetchData<void, void>(
{
method: 'DELETE',
url: `/api/app/sql-object-manager/function/${id}`,
},
{ apiName: this.apiName, ...config },
)
deployFunction = (input: DeployFunctionDto, config?: Partial<Config>) =>
apiService.fetchData<SqlQueryExecutionResultDto, DeployFunctionDto>(
{
method: 'POST',
url: `/api/app/sql-view/${id}/drop`,
url: '/api/app/sql-object-manager/deploy-function',
data: input,
},
{ apiName: this.apiName, ...config },
)
// Database Metadata Operations
getTableColumns = (dataSourceCode: string, schemaName: string, tableName: string, config?: Partial<Config>) =>
apiService.fetchData<DatabaseColumnDto[], void>(
{
method: 'GET',
url: '/api/app/sql-object-manager/table-columns',
params: { dataSourceCode, schemaName, tableName },
},
{ apiName: this.apiName, ...config },
)
}
export class SqlTemplateService {
apiName = 'Default'
getQueryTemplates = (config?: Partial<Config>) =>
apiService.fetchData<SqlTemplateDto[], void>(
{
method: 'GET',
url: '/api/app/sql-template/query-templates',
},
{ apiName: this.apiName, ...config },
)
getStoredProcedureTemplate = (
procedureName: string,
schemaName = 'dbo',
config?: Partial<Config>,
) =>
apiService.fetchData<string, void>(
{
method: 'GET',
url: '/api/app/sql-template/stored-procedure-template',
params: { procedureName, schemaName },
},
{ apiName: this.apiName, ...config },
)
getViewTemplate = (
viewName: string,
schemaName = 'dbo',
withSchemaBinding = false,
config?: Partial<Config>,
) =>
apiService.fetchData<string, void>(
{
method: 'GET',
url: '/api/app/sql-template/view-template',
params: { viewName, schemaName, withSchemaBinding },
},
{ apiName: this.apiName, ...config },
)
getFunctionTemplate = (
functionName: string,
functionType: number,
schemaName = 'dbo',
config?: Partial<Config>,
) =>
apiService.fetchData<string, void>(
{
method: 'GET',
url: '/api/app/sql-template/function-template',
params: { functionName, functionType, schemaName },
},
{ apiName: this.apiName, ...config },
)
getQueryTemplate = (templateType: string, config?: Partial<Config>) =>
apiService.fetchData<string, void>(
{
method: 'GET',
url: '/api/app/sql-template/query-template',
params: { templateType },
},
{ apiName: this.apiName, ...config },
)
}
// Export service instances
export const sqlFunctionService = new SqlFunctionService()
export const sqlQueryService = new SqlQueryService()
export const sqlStoredProcedureService = new SqlStoredProcedureService()
export const sqlViewService = new SqlViewService()
export const sqlTemplateService = new SqlTemplateService()
// Export service instance
export const sqlObjectManagerService = new SqlObjectManagerService()

View file

@ -13,10 +13,7 @@ import type {
SqlQueryExecutionResultDto,
} from '@/proxy/sql-query-manager/models'
import {
sqlFunctionService,
sqlQueryService,
sqlStoredProcedureService,
sqlViewService,
sqlObjectManagerService,
} from '@/services/sql-query-manager.service'
import { FaDatabase, FaPlay, FaSave, FaSyncAlt } from 'react-icons/fa'
import { useLocalization } from '@/utils/hooks/useLocalization'
@ -48,8 +45,7 @@ const SqlQueryManager = () => {
selectedDataSource: null,
selectedObject: null,
selectedObjectType: null,
editorContent:
'-- SQL Query Editor\n-- Write your SQL query here and press F5 or click Execute to run\n\nSELECT * FROM YourTable\nWHERE 1=1',
editorContent: '',
isExecuting: false,
executionResult: null,
showProperties: false,
@ -279,7 +275,8 @@ GO`
}, [translate])
const handleTemplateSelect = useCallback((template: string, templateType: string) => {
const templateContent = getTemplateContent(templateType)
// 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
@ -331,7 +328,7 @@ GO`
setState((prev) => ({ ...prev, isExecuting: true, executionResult: null }))
try {
const result = await sqlQueryService.executeQuery({
const result = await sqlObjectManagerService.executeQuery({
queryText: state.editorContent,
dataSourceCode: state.selectedDataSource.code || '',
})
@ -395,13 +392,13 @@ GO`
switch (state.selectedObjectType) {
case 1: // Query
await sqlQueryService.update(objectId, {
await sqlObjectManagerService.updateQuery(objectId, {
...(state.selectedObject as SqlQueryDto),
queryText: state.editorContent,
})
break
case 2: // Stored Procedure
await sqlStoredProcedureService.update(objectId, {
await sqlObjectManagerService.updateStoredProcedure(objectId, {
displayName: (state.selectedObject as SqlStoredProcedureDto).displayName,
description: (state.selectedObject as SqlStoredProcedureDto).description,
procedureBody: state.editorContent,
@ -410,7 +407,7 @@ GO`
})
break
case 3: // View
await sqlViewService.update(objectId, {
await sqlObjectManagerService.updateView(objectId, {
displayName: (state.selectedObject as SqlViewDto).displayName,
description: (state.selectedObject as SqlViewDto).description,
viewDefinition: state.editorContent,
@ -419,7 +416,7 @@ GO`
})
break
case 4: // Function
await sqlFunctionService.update(objectId, {
await sqlObjectManagerService.updateFunction(objectId, {
displayName: (state.selectedObject as SqlFunctionDto).displayName,
description: (state.selectedObject as SqlFunctionDto).description,
functionBody: state.editorContent,
@ -452,7 +449,7 @@ GO`
if (!state.selectedDataSource || !saveDialogData.name) return
try {
await sqlQueryService.create({
await sqlObjectManagerService.createQuery({
code: saveDialogData.name.replace(/\s+/g, '_'),
name: saveDialogData.name,
description: saveDialogData.description,
@ -501,13 +498,13 @@ GO`
switch (state.selectedObjectType) {
case 2: // Stored Procedure
result = await sqlStoredProcedureService.deploy({ id: objectId, dropIfExists: true })
result = await sqlObjectManagerService.deployStoredProcedure({ id: objectId, dropIfExists: true })
break
case 3: // View
result = await sqlViewService.deploy({ id: objectId, dropIfExists: true })
result = await sqlObjectManagerService.deployView({ id: objectId, dropIfExists: true })
break
case 4: // Function
result = await sqlFunctionService.deploy({ id: objectId, dropIfExists: true })
result = await sqlObjectManagerService.deployFunction({ id: objectId, dropIfExists: true })
break
default:
toast.push(
@ -537,9 +534,9 @@ GO`
return (
<Container className="h-full overflow-hidden">
<div className="flex flex-col h-full gap-4 p-1">
<div className="flex flex-col h-full p-1">
{/* Toolbar */}
<AdaptableCard className="flex-shrink-0 shadow-sm">
<AdaptableCard className="flex-shrink-0 shadow-sm mb-4">
<div className="flex items-center justify-between px-1 py-1">
<div className="flex items-center gap-3">
<FaDatabase className="text-lg text-blue-500" />
@ -615,9 +612,9 @@ GO`
</AdaptableCard>
{/* Main Content Area */}
<div className="flex-1 flex gap-4 min-h-0">
<div className="flex-1 flex min-h-0">
{/* Left Panel - Object Explorer */}
<div className="w-80 flex-shrink-0 flex flex-col min-h-0">
<div className="w-80 flex-shrink-0 flex flex-col min-h-0 mr-4">
<AdaptableCard className="h-full" bodyClass="p-0">
<div className="h-full flex flex-col">
<div className="border-b px-4 py-3 bg-gray-50 dark:bg-gray-800 flex-shrink-0">
@ -625,7 +622,7 @@ GO`
{translate('::App.Platform.ObjectExplorer')}
</h6>
</div>
<div className="flex-1 overflow-auto p-2 min-h-0">
<div className="flex-1 min-h-0 overflow-auto">
<SqlObjectExplorer
dataSource={state.selectedDataSource}
onObjectSelect={handleObjectSelect}
@ -638,7 +635,7 @@ GO`
</div>
{/* Center Panel - Editor and Results */}
<div className="flex-1 flex flex-col min-h-0">
<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">
<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>

View file

@ -10,7 +10,9 @@ import {
FaSyncAlt,
FaEdit,
FaTrash,
FaTable,
} from 'react-icons/fa'
import { MdViewColumn } from 'react-icons/md'
import type { DataSourceDto } from '@/proxy/data-source'
import type {
SqlFunctionDto,
@ -20,10 +22,7 @@ import type {
SqlObjectType,
} from '@/proxy/sql-query-manager/models'
import {
sqlFunctionService,
sqlQueryService,
sqlStoredProcedureService,
sqlViewService,
sqlObjectManagerService,
} from '@/services/sql-query-manager.service'
import { useLocalization } from '@/utils/hooks/useLocalization'
@ -32,11 +31,13 @@ export type SqlObject = SqlFunctionDto | SqlQueryDto | SqlStoredProcedureDto | S
interface TreeNode {
id: string
label: string
type: 'root' | 'folder' | 'object'
type: 'root' | 'folder' | 'object' | 'column'
objectType?: SqlObjectType
data?: SqlObject
data?: SqlObject | any
children?: TreeNode[]
expanded?: boolean
isColumn?: boolean
parentTable?: { schemaName: string; tableName: string }
}
interface SqlObjectExplorerProps {
@ -55,9 +56,10 @@ const SqlObjectExplorer = ({
const { translate } = useLocalization()
const [treeData, setTreeData] = useState<TreeNode[]>([])
const [expandedNodes, setExpandedNodes] = useState<Set<string>>(
new Set(['root', 'templates', 'queries', 'storedProcedures', 'views', 'functions']),
new Set(['root']), // Only root expanded by default
)
const [loading, setLoading] = useState(false)
const [filterText, setFilterText] = useState('')
const [contextMenu, setContextMenu] = useState<{
show: boolean
x: number
@ -83,28 +85,10 @@ const SqlObjectExplorer = ({
setLoading(true)
try {
const [queries, storedProcedures, views, functions] = await Promise.all([
sqlQueryService.getList({
skipCount: 0,
maxResultCount: 1000,
dataSourceCode: dataSource.code,
}),
sqlStoredProcedureService.getList({
skipCount: 0,
maxResultCount: 1000,
dataSourceCode: dataSource.code,
}),
sqlViewService.getList({
skipCount: 0,
maxResultCount: 1000,
dataSourceCode: dataSource.code,
}),
sqlFunctionService.getList({
skipCount: 0,
maxResultCount: 1000,
dataSourceCode: dataSource.code,
}),
])
// Single API call to get all objects
const response = await sqlObjectManagerService.getAllObjects(dataSource.code || '')
const allObjects = response.data
const tree: TreeNode[] = [
{
id: 'root',
@ -168,14 +152,27 @@ const SqlObjectExplorer = ({
},
],
},
{
id: 'tables',
label: `${translate('::App.Platform.Tables')} (${allObjects.tables.length})`,
type: 'folder',
expanded: expandedNodes.has('tables'),
children:
allObjects.tables.map((t) => ({
id: `table-${t.schemaName}-${t.tableName}`,
label: t.fullName,
type: 'object' as const,
data: t,
})) || [],
},
{
id: 'queries',
label: `${translate('::App.Platform.Queries')} (${queries.data.totalCount})`,
label: `${translate('::App.Platform.Queries')} (${allObjects.queries.length})`,
type: 'folder',
objectType: 1,
expanded: expandedNodes.has('queries'),
children:
queries.data.items?.map((q) => ({
allObjects.queries.map((q) => ({
id: q.id || '',
label: q.name,
type: 'object' as const,
@ -185,12 +182,12 @@ const SqlObjectExplorer = ({
},
{
id: 'storedProcedures',
label: `${translate('::App.Platform.StoredProcedures')} (${storedProcedures.data.totalCount})`,
label: `${translate('::App.Platform.StoredProcedures')} (${allObjects.storedProcedures.length})`,
type: 'folder',
objectType: 2,
expanded: expandedNodes.has('storedProcedures'),
children:
storedProcedures.data.items?.map((sp) => ({
allObjects.storedProcedures.map((sp) => ({
id: sp.id || '',
label: sp.displayName || sp.procedureName,
type: 'object' as const,
@ -200,12 +197,12 @@ const SqlObjectExplorer = ({
},
{
id: 'views',
label: `${translate('::App.Platform.Views')} (${views.data.totalCount})`,
label: `${translate('::App.Platform.Views')} (${allObjects.views.length})`,
type: 'folder',
objectType: 3,
expanded: expandedNodes.has('views'),
children:
views.data.items?.map((v) => ({
allObjects.views.map((v) => ({
id: v.id || '',
label: v.displayName || v.viewName,
type: 'object' as const,
@ -215,12 +212,12 @@ const SqlObjectExplorer = ({
},
{
id: 'functions',
label: `${translate('::App.Platform.Functions')} (${functions.data.totalCount})`,
label: `${translate('::App.Platform.Functions')} (${allObjects.functions.length})`,
type: 'folder',
objectType: 4,
expanded: expandedNodes.has('functions'),
children:
functions.data.items?.map((f) => ({
allObjects.functions.map((f) => ({
id: f.id || '',
label: f.displayName || f.functionName,
type: 'object' as const,
@ -245,24 +242,116 @@ const SqlObjectExplorer = ({
}
}
const toggleNode = (nodeId: string) => {
setExpandedNodes((prev) => {
const newSet = new Set(prev)
if (newSet.has(nodeId)) newSet.delete(nodeId)
else newSet.add(nodeId)
return newSet
const loadTableColumns = async (schemaName: string, tableName: string): Promise<TreeNode[]> => {
try {
const response = await sqlObjectManagerService.getTableColumns(
dataSource?.code || '',
schemaName,
tableName,
)
return response.data.map((col) => ({
id: `column-${schemaName}-${tableName}-${col.columnName}`,
label: `${col.columnName} (${col.dataType}${col.maxLength ? `(${col.maxLength})` : ''})${col.isNullable ? '' : ' NOT NULL'}`,
type: 'column' as const,
isColumn: true,
data: col,
parentTable: { schemaName, tableName },
}))
} catch (error) {
return []
}
}
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)
})
}
}
}
}
const findNodeById = (nodes: TreeNode[], id: string): TreeNode | null => {
for (const node of nodes) {
if (node.id === id) return node
if (node.children) {
const found = findNodeById(node.children, id)
if (found) return found
}
}
return null
}
const updateNodeChildren = (nodes: TreeNode[], nodeId: string, children: TreeNode[]): TreeNode[] => {
return nodes.map((node) => {
if (node.id === nodeId) {
return { ...node, children }
}
if (node.children) {
return { ...node, children: updateNodeChildren(node.children, nodeId, children) }
}
return node
})
}
const filterTree = (nodes: TreeNode[], searchText: string): TreeNode[] => {
if (!searchText.trim()) return nodes
const search = searchText.toLowerCase()
const filtered = nodes
.map((node) => {
const matchesSearch = node.label.toLowerCase().includes(search)
const filteredChildren = node.children ? filterTree(node.children, searchText) : []
if (matchesSearch || filteredChildren.length > 0) {
return {
...node,
children: filteredChildren.length > 0 ? filteredChildren : node.children,
} as TreeNode
}
return null
})
.filter((node) => node !== null) as TreeNode[]
return filtered
}
const handleNodeClick = (node: TreeNode) => {
if (node.type === 'folder' || node.type === 'root') {
toggleNode(node.id)
} else if (node.type === 'column') {
// Column clicked - do nothing or show info
return
} else if (node.type === 'object' && node.data) {
// Check if it's a template
if ((node.data as any).templateType && onTemplateSelect) {
const templateType = (node.data as any).templateType
onTemplateSelect('', templateType) // Template content will be generated in parent
} else if (node.objectType) {
}
// 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) {
onObjectSelect(node.data, node.objectType)
}
}
@ -270,6 +359,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-')) {
return
}
setContextMenu({
show: true,
x: e.clientX,
@ -286,16 +381,16 @@ const SqlObjectExplorer = ({
switch (type) {
case 1:
await sqlQueryService.delete(object.id!)
await sqlObjectManagerService.deleteQuery(object.id!)
break
case 2:
await sqlStoredProcedureService.delete(object.id!)
await sqlObjectManagerService.deleteStoredProcedure(object.id!)
break
case 3:
await sqlViewService.delete(object.id!)
await sqlObjectManagerService.deleteView(object.id!)
break
case 4:
await sqlFunctionService.delete(object.id!)
await sqlObjectManagerService.deleteFunction(object.id!)
break
}
@ -337,6 +432,14 @@ const SqlObjectExplorer = ({
<FaRegFolder className="text-orange-500" />
)
// Tables folder
if (node.id === 'tables')
return isExpanded ? (
<FaRegFolderOpen className="text-blue-500" />
) : (
<FaRegFolder className="text-blue-500" />
)
if (node.objectType === 1)
return isExpanded ? (
<FaRegFolderOpen className="text-yellow-500" />
@ -372,31 +475,41 @@ const SqlObjectExplorer = ({
return <FaCode className="text-orange-500" />
}
// Check if it's a table
if (node.id.startsWith('table-')) {
return <FaTable className="text-blue-500" />
}
if (node.objectType === 1) return <FaRegFileAlt 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 === 4) return <FaCode className="text-gray-500" />
}
if (node.type === 'column') {
return <MdViewColumn className="text-gray-400 text-sm" />
}
return <FaRegFolder />
}
const renderNode = (node: TreeNode, level = 0) => {
const isExpanded = expandedNodes.has(node.id)
const isSelected = node.type === 'object' && selectedObject?.id === node.id
const isColumn = node.type === 'column'
return (
<div key={node.id}>
<div
className={`flex items-center gap-2 py-1 px-2 cursor-pointer hover:bg-gray-100 dark:hover:bg-gray-700 rounded ${
className={`flex items-center gap-2 py-1 px-2 ${isColumn ? 'cursor-default' : 'cursor-pointer hover:bg-gray-100 dark:hover:bg-gray-700'} rounded ${
isSelected ? 'bg-blue-100 dark:bg-blue-900' : ''
}`}
style={{ paddingLeft: `${level * 16 + 8}px` }}
onClick={() => handleNodeClick(node)}
onContextMenu={(e) => handleContextMenu(e, node)}
onClick={() => !isColumn && handleNodeClick(node)}
onContextMenu={(e) => !isColumn && handleContextMenu(e, node)}
>
{getIcon(node)}
<span className="text-sm flex-1">{node.label}</span>
<span className={`text-sm flex-1 ${isColumn ? 'text-gray-600 dark:text-gray-400' : ''}`}>{node.label}</span>
</div>
{isExpanded && node.children && (
@ -406,17 +519,48 @@ const SqlObjectExplorer = ({
)
}
const filteredTree = filterTree(treeData, filterText)
return (
<div className="h-full">
{loading && <div className="text-center py-8 text-gray-500">{translate('::App.Platform.Loading')}</div>}
{!loading && treeData.length === 0 && (
<div className="text-center py-8 text-gray-500">
{translate('::App.Platform.NoDataSourceSelected')}
<div className="h-full flex flex-col">
{/* Filter and Refresh Controls */}
<div className="p-2 border-b space-y-2 flex-shrink-0">
<div className="flex gap-2">
<input
type="text"
placeholder={translate('::App.Platform.Search')}
value={filterText}
onChange={(e) => setFilterText(e.target.value)}
className="flex-1 px-3 py-1.5 text-sm border rounded-md bg-white dark:bg-gray-700 dark:border-gray-600"
/>
<button
onClick={loadObjects}
disabled={loading || !dataSource}
className="px-3 py-1.5 text-sm border rounded-md hover:bg-gray-100 dark:hover:bg-gray-700 disabled:opacity-50"
title={translate('::App.Platform.Refresh')}
>
<FaSyncAlt className={loading ? 'animate-spin' : ''} />
</button>
</div>
)}
{!loading && treeData.length > 0 && (
<div className="space-y-1">{treeData.map((node) => renderNode(node))}</div>
)}
</div>
{/* Tree Content */}
<div className="flex-1 overflow-auto">
{loading && <div className="text-center py-8 text-gray-500">{translate('::App.Platform.Loading')}</div>}
{!loading && treeData.length === 0 && (
<div className="text-center py-8 text-gray-500">
{translate('::App.Platform.NoDataSourceSelected')}
</div>
)}
{!loading && filteredTree.length > 0 && (
<div className="space-y-1 p-2">{filteredTree.map((node) => renderNode(node))}</div>
)}
{!loading && treeData.length > 0 && filteredTree.length === 0 && (
<div className="text-center py-8 text-gray-500">
{translate('::App.Platform.NoResultsFound')}
</div>
)}
</div>
{contextMenu.show && (
<>