DynamicService düzenlemesi
This commit is contained in:
parent
5cc5000f16
commit
735261cb8a
16 changed files with 773 additions and 506 deletions
|
|
@ -127,6 +127,9 @@ public class PublishAppServiceRequestDto
|
||||||
[StringLength(256)]
|
[StringLength(256)]
|
||||||
[JsonPropertyName("primaryEntityType")]
|
[JsonPropertyName("primaryEntityType")]
|
||||||
public string PrimaryEntityType { get; set; }
|
public string PrimaryEntityType { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("isActive")]
|
||||||
|
public bool IsActive { get; set; } = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Sozsoft.Platform.DeveloperKit;
|
using Sozsoft.Platform.DeveloperKit;
|
||||||
using Sozsoft.Platform.Entities;
|
using Sozsoft.Platform.Entities;
|
||||||
|
|
@ -8,9 +7,11 @@ using Microsoft.EntityFrameworkCore;
|
||||||
using Volo.Abp.Application.Dtos;
|
using Volo.Abp.Application.Dtos;
|
||||||
using Volo.Abp.Application.Services;
|
using Volo.Abp.Application.Services;
|
||||||
using Volo.Abp.Domain.Repositories;
|
using Volo.Abp.Domain.Repositories;
|
||||||
|
using Microsoft.AspNetCore.Authorization;
|
||||||
|
|
||||||
namespace Platform.Api.Application;
|
namespace Platform.Api.Application;
|
||||||
|
|
||||||
|
[Authorize]
|
||||||
public class CrudEndpointGenerateAppService : CrudAppService<
|
public class CrudEndpointGenerateAppService : CrudAppService<
|
||||||
CrudEndpoint,
|
CrudEndpoint,
|
||||||
CrudEndpointDto,
|
CrudEndpointDto,
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using Microsoft.AspNetCore.Authorization;
|
||||||
using Sozsoft.Platform.DeveloperKit;
|
using Sozsoft.Platform.DeveloperKit;
|
||||||
using Sozsoft.Platform.Entities;
|
using Sozsoft.Platform.Entities;
|
||||||
using Volo.Abp.Application.Dtos;
|
using Volo.Abp.Application.Dtos;
|
||||||
|
|
@ -9,6 +10,7 @@ using Volo.Abp.Domain.Repositories;
|
||||||
|
|
||||||
namespace Platform.Api.Application;
|
namespace Platform.Api.Application;
|
||||||
|
|
||||||
|
[Authorize]
|
||||||
public class CustomComponentAppService : CrudAppService<
|
public class CustomComponentAppService : CrudAppService<
|
||||||
CustomComponent,
|
CustomComponent,
|
||||||
CustomComponentDto,
|
CustomComponentDto,
|
||||||
|
|
|
||||||
|
|
@ -70,6 +70,7 @@ public class DynamicAppServiceAppService : PlatformAppService, IDynamicServiceAp
|
||||||
existingService.DisplayName = request.DisplayName;
|
existingService.DisplayName = request.DisplayName;
|
||||||
existingService.Description = request.Description;
|
existingService.Description = request.Description;
|
||||||
existingService.PrimaryEntityType = request.PrimaryEntityType;
|
existingService.PrimaryEntityType = request.PrimaryEntityType;
|
||||||
|
existingService.IsActive = request.IsActive;
|
||||||
|
|
||||||
appService = await _dynamicAppServiceRepository.UpdateAsync(existingService);
|
appService = await _dynamicAppServiceRepository.UpdateAsync(existingService);
|
||||||
}
|
}
|
||||||
|
|
@ -84,13 +85,34 @@ public class DynamicAppServiceAppService : PlatformAppService, IDynamicServiceAp
|
||||||
DisplayName = request.DisplayName,
|
DisplayName = request.DisplayName,
|
||||||
Description = request.Description,
|
Description = request.Description,
|
||||||
PrimaryEntityType = request.PrimaryEntityType,
|
PrimaryEntityType = request.PrimaryEntityType,
|
||||||
ControllerName = GenerateControllerName(request.Name)
|
ControllerName = GenerateControllerName(request.Name),
|
||||||
|
IsActive = request.IsActive
|
||||||
};
|
};
|
||||||
|
|
||||||
appService = await _dynamicAppServiceRepository.InsertAsync(appService);
|
appService = await _dynamicAppServiceRepository.InsertAsync(appService);
|
||||||
}
|
}
|
||||||
|
|
||||||
var assemblyName = $"{appService.Name}_{appService.Version}";
|
var assemblyName = $"{appService.Name}_{appService.Version}";
|
||||||
|
|
||||||
|
// Pasif olarak yayınlanıyorsa mevcut kaydı kaldır, assembly yükleme
|
||||||
|
if (!request.IsActive)
|
||||||
|
{
|
||||||
|
DynamicServiceCompiler.NotifyAssemblyUnregistration?.Invoke(
|
||||||
|
CurrentTenant.Id ?? Guid.Empty,
|
||||||
|
appService.Name);
|
||||||
|
|
||||||
|
appService.MarkCompilationSuccess();
|
||||||
|
await _dynamicAppServiceRepository.UpdateAsync(appService);
|
||||||
|
|
||||||
|
return new PublishResultDto
|
||||||
|
{
|
||||||
|
Success = true,
|
||||||
|
AppServiceId = appService.Id,
|
||||||
|
ControllerName = appService.ControllerName,
|
||||||
|
GeneratedEndpoints = new List<string>()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
var loadResult = await _compiler.CompileAndRegisterForTenantAsync(
|
var loadResult = await _compiler.CompileAndRegisterForTenantAsync(
|
||||||
CurrentTenant.Id ?? Guid.Empty,
|
CurrentTenant.Id ?? Guid.Empty,
|
||||||
request.Code,
|
request.Code,
|
||||||
|
|
@ -177,8 +199,10 @@ public class DynamicAppServiceAppService : PlatformAppService, IDynamicServiceAp
|
||||||
{
|
{
|
||||||
var appService = await _dynamicAppServiceRepository.GetAsync(id);
|
var appService = await _dynamicAppServiceRepository.GetAsync(id);
|
||||||
|
|
||||||
// TODO: Runtime'dan assembly'yi kaldırma işlemi
|
// Runtime'dan assembly ve Swagger endpoint'ini kaldır
|
||||||
// (AssemblyLoadContext.Unload() çağrısı)
|
DynamicServiceCompiler.NotifyAssemblyUnregistration?.Invoke(
|
||||||
|
CurrentTenant.Id ?? Guid.Empty,
|
||||||
|
appService.Name);
|
||||||
|
|
||||||
await _dynamicAppServiceRepository.DeleteAsync(id);
|
await _dynamicAppServiceRepository.DeleteAsync(id);
|
||||||
}
|
}
|
||||||
|
|
@ -189,6 +213,30 @@ public class DynamicAppServiceAppService : PlatformAppService, IDynamicServiceAp
|
||||||
var appService = await _dynamicAppServiceRepository.GetAsync(id);
|
var appService = await _dynamicAppServiceRepository.GetAsync(id);
|
||||||
appService.IsActive = isActive;
|
appService.IsActive = isActive;
|
||||||
await _dynamicAppServiceRepository.UpdateAsync(appService);
|
await _dynamicAppServiceRepository.UpdateAsync(appService);
|
||||||
|
|
||||||
|
if (!isActive)
|
||||||
|
{
|
||||||
|
// Pasif yapılınca Swagger/MVC'den endpoint'i kaldır
|
||||||
|
DynamicServiceCompiler.NotifyAssemblyUnregistration?.Invoke(
|
||||||
|
CurrentTenant.Id ?? Guid.Empty,
|
||||||
|
appService.Name);
|
||||||
|
}
|
||||||
|
else if (appService.CompilationStatus == CompilationStatus.Success)
|
||||||
|
{
|
||||||
|
// Aktif yapılınca yeniden derle ve yayınla
|
||||||
|
var assemblyName = $"{appService.Name}_{appService.Version}";
|
||||||
|
var result = await _compiler.CompileAndRegisterForTenantAsync(
|
||||||
|
CurrentTenant.Id ?? Guid.Empty,
|
||||||
|
appService.Code,
|
||||||
|
assemblyName);
|
||||||
|
|
||||||
|
if (!result.Success)
|
||||||
|
{
|
||||||
|
Logger.LogWarning(
|
||||||
|
"Servis aktif edildi ancak yeniden derleme başarısız. Ad: {Name}, Hata: {Error}",
|
||||||
|
appService.Name, result.ErrorMessage);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
[Authorize(AppCodes.DeveloperKits.DynamicServices.Manage)]
|
[Authorize(AppCodes.DeveloperKits.DynamicServices.Manage)]
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,20 @@ public class DynamicServiceCompiler : ITransientDependency
|
||||||
// Assembly kaydı için delegate
|
// Assembly kaydı için delegate
|
||||||
public static Action<Guid, Assembly, string>? NotifyAssemblyRegistration { get; set; }
|
public static Action<Guid, Assembly, string>? NotifyAssemblyRegistration { get; set; }
|
||||||
|
|
||||||
|
// Assembly silinme bildirimi için delegate
|
||||||
|
public static Action<Guid, string>? NotifyAssemblyUnregistration { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Belirtilen tenant ve assembly adı prefix'ine ait assembly'leri tenant cache'inden kaldırır.
|
||||||
|
/// </summary>
|
||||||
|
public static void UnregisterTenantAssemblyByPrefix(Guid tenantId, string assemblyNamePrefix)
|
||||||
|
{
|
||||||
|
if (_tenantAssemblies.TryGetValue(tenantId, out var assemblies))
|
||||||
|
{
|
||||||
|
assemblies.RemoveAll(a => a.GetName().Name?.StartsWith(assemblyNamePrefix) == true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Güvenlik için yasaklı namespace'ler
|
// Güvenlik için yasaklı namespace'ler
|
||||||
private static readonly string[] ForbiddenNamespaces = {
|
private static readonly string[] ForbiddenNamespaces = {
|
||||||
"System.IO",
|
"System.IO",
|
||||||
|
|
|
||||||
|
|
@ -9,9 +9,11 @@ using System;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using Microsoft.AspNetCore.Authorization;
|
||||||
|
|
||||||
namespace Platform.Api.Application;
|
namespace Platform.Api.Application;
|
||||||
|
|
||||||
|
[Authorize]
|
||||||
public class SqlTableAppService : CrudAppService<
|
public class SqlTableAppService : CrudAppService<
|
||||||
SqlTable,
|
SqlTable,
|
||||||
SqlTableDto,
|
SqlTableDto,
|
||||||
|
|
@ -59,7 +61,7 @@ public class SqlTableAppService : CrudAppService<
|
||||||
.FirstOrDefaultAsync(x => x.Id == id);
|
.FirstOrDefaultAsync(x => x.Id == id);
|
||||||
|
|
||||||
if (entity == null)
|
if (entity == null)
|
||||||
throw new EntityNotFoundException($"CustomEntity with id {id} not found");
|
throw new EntityNotFoundException($"Sql Table with id {id} not found");
|
||||||
|
|
||||||
return ObjectMapper.Map<SqlTable, SqlTableDto>(entity);
|
return ObjectMapper.Map<SqlTable, SqlTableDto>(entity);
|
||||||
}
|
}
|
||||||
|
|
@ -83,7 +85,7 @@ public class SqlTableAppService : CrudAppService<
|
||||||
.FirstOrDefaultAsync(x => x.Id == id);
|
.FirstOrDefaultAsync(x => x.Id == id);
|
||||||
|
|
||||||
if (entity == null)
|
if (entity == null)
|
||||||
throw new EntityNotFoundException($"CustomEntity with id {id} not found");
|
throw new EntityNotFoundException($"Sql Table with id {id} not found");
|
||||||
|
|
||||||
entity.IsActive = !entity.IsActive;
|
entity.IsActive = !entity.IsActive;
|
||||||
await _repository.UpdateAsync(entity, autoSave: true);
|
await _repository.UpdateAsync(entity, autoSave: true);
|
||||||
|
|
|
||||||
|
|
@ -297,6 +297,20 @@
|
||||||
{
|
{
|
||||||
"key": "admin.developerkit.dynamic-services",
|
"key": "admin.developerkit.dynamic-services",
|
||||||
"path": "/admin/developerkit/dynamic-services",
|
"path": "/admin/developerkit/dynamic-services",
|
||||||
|
"componentPath": "@/views/developerKit/DynamicServiceManager",
|
||||||
|
"routeType": "protected",
|
||||||
|
"authority": ["App.DeveloperKit.DynamicServices"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "admin.developerkit.dynamic-services.new",
|
||||||
|
"path": "/admin/developerkit/dynamic-services/new",
|
||||||
|
"componentPath": "@/views/developerKit/DynamicServiceEditor",
|
||||||
|
"routeType": "protected",
|
||||||
|
"authority": ["App.DeveloperKit.DynamicServices"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "admin.developerkit.dynamic-services.edit",
|
||||||
|
"path": "/admin/developerkit/dynamic-services/edit/:id",
|
||||||
"componentPath": "@/views/developerKit/DynamicServiceEditor",
|
"componentPath": "@/views/developerKit/DynamicServiceEditor",
|
||||||
"routeType": "protected",
|
"routeType": "protected",
|
||||||
"authority": ["App.DeveloperKit.DynamicServices"]
|
"authority": ["App.DeveloperKit.DynamicServices"]
|
||||||
|
|
|
||||||
|
|
@ -27,6 +27,8 @@ namespace Sozsoft.Platform.DynamicServices
|
||||||
|
|
||||||
// Bekleyen assembly kayıt istekleri
|
// Bekleyen assembly kayıt istekleri
|
||||||
private static readonly Queue<AssemblyRegistrationRequest> _pendingRegistrations = new();
|
private static readonly Queue<AssemblyRegistrationRequest> _pendingRegistrations = new();
|
||||||
|
// Bekleyen assembly silme istekleri
|
||||||
|
private static readonly Queue<AssemblyUnregistrationRequest> _pendingUnregistrations = new();
|
||||||
private static readonly object _lock = new();
|
private static readonly object _lock = new();
|
||||||
|
|
||||||
public DynamicAssemblyRegistrationService(
|
public DynamicAssemblyRegistrationService(
|
||||||
|
|
@ -60,6 +62,22 @@ namespace Sozsoft.Platform.DynamicServices
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Bir servis adına ait assembly'nin Swagger/MVC'den kaldırılması istemi.
|
||||||
|
/// </summary>
|
||||||
|
public static void RequestAssemblyUnregistration(Guid tenantId, string serviceName)
|
||||||
|
{
|
||||||
|
lock (_lock)
|
||||||
|
{
|
||||||
|
_pendingUnregistrations.Enqueue(new AssemblyUnregistrationRequest
|
||||||
|
{
|
||||||
|
TenantId = tenantId,
|
||||||
|
ServiceName = serviceName,
|
||||||
|
RequestTime = DateTime.UtcNow
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
|
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
|
||||||
{
|
{
|
||||||
await Task.Delay(3000, stoppingToken);
|
await Task.Delay(3000, stoppingToken);
|
||||||
|
|
@ -132,20 +150,35 @@ namespace Sozsoft.Platform.DynamicServices
|
||||||
|
|
||||||
private async Task ProcessPendingRegistrations()
|
private async Task ProcessPendingRegistrations()
|
||||||
{
|
{
|
||||||
var requests = new List<AssemblyRegistrationRequest>();
|
var registrations = new List<AssemblyRegistrationRequest>();
|
||||||
|
var unregistrations = new List<AssemblyUnregistrationRequest>();
|
||||||
|
|
||||||
lock (_lock)
|
lock (_lock)
|
||||||
{
|
{
|
||||||
while (_pendingRegistrations.Count > 0)
|
while (_pendingRegistrations.Count > 0)
|
||||||
|
registrations.Add(_pendingRegistrations.Dequeue());
|
||||||
|
|
||||||
|
while (_pendingUnregistrations.Count > 0)
|
||||||
|
unregistrations.Add(_pendingUnregistrations.Dequeue());
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var request in unregistrations)
|
||||||
{
|
{
|
||||||
requests.Add(_pendingRegistrations.Dequeue());
|
try
|
||||||
|
{
|
||||||
|
UnregisterAssembly(request);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Assembly kaydı silme başarısız. Tenant: {TenantId}, Servis: {Service}",
|
||||||
|
request.TenantId, request.ServiceName);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (requests.Count == 0)
|
if (registrations.Count == 0)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
foreach (var request in requests)
|
foreach (var request in registrations)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
|
@ -159,6 +192,44 @@ namespace Sozsoft.Platform.DynamicServices
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void UnregisterAssembly(AssemblyUnregistrationRequest request)
|
||||||
|
{
|
||||||
|
var servicePrefix = $"{request.ServiceName}_";
|
||||||
|
|
||||||
|
// ApplicationParts'tan kaldır
|
||||||
|
var partsToRemove = _partManager.ApplicationParts
|
||||||
|
.OfType<AssemblyPart>()
|
||||||
|
.Where(p => p.Name.StartsWith(servicePrefix))
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
foreach (var part in partsToRemove)
|
||||||
|
{
|
||||||
|
_partManager.ApplicationParts.Remove(part);
|
||||||
|
_logger.LogInformation("ApplicationPart kaldırıldı: {Name}", part.Name);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ConventionalControllerSettings'tan kaldır
|
||||||
|
var settingsToRemove = _mvcOptions.Value.ConventionalControllers.ConventionalControllerSettings
|
||||||
|
.Where(s => s.Assembly.GetName().Name?.StartsWith(servicePrefix) == true)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
foreach (var setting in settingsToRemove)
|
||||||
|
_mvcOptions.Value.ConventionalControllers.ConventionalControllerSettings.Remove(setting);
|
||||||
|
|
||||||
|
// DynamicServiceTypeRegistry'den kaldır
|
||||||
|
DynamicServiceTypeRegistry.UnregisterByAssemblyNamePrefix(servicePrefix);
|
||||||
|
|
||||||
|
// DynamicServiceCompiler tenant assembly cache'inden kaldır
|
||||||
|
DynamicServiceCompiler.UnregisterTenantAssemblyByPrefix(request.TenantId, servicePrefix);
|
||||||
|
|
||||||
|
// MVC/Swagger'ı yenile
|
||||||
|
_changeProvider.NotifyChanges();
|
||||||
|
|
||||||
|
_logger.LogInformation(
|
||||||
|
"Servis assembly'si başarıyla kaldırıldı: {ServiceName} (Tenant: {TenantId})",
|
||||||
|
request.ServiceName, request.TenantId);
|
||||||
|
}
|
||||||
|
|
||||||
private async Task RegisterAssembly(AssemblyRegistrationRequest request)
|
private async Task RegisterAssembly(AssemblyRegistrationRequest request)
|
||||||
{
|
{
|
||||||
var lastUnderscoreIndex = request.AssemblyName.LastIndexOf('_');
|
var lastUnderscoreIndex = request.AssemblyName.LastIndexOf('_');
|
||||||
|
|
@ -281,5 +352,12 @@ namespace Sozsoft.Platform.DynamicServices
|
||||||
public string AssemblyName { get; set; }
|
public string AssemblyName { get; set; }
|
||||||
public DateTime RequestTime { get; set; }
|
public DateTime RequestTime { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private class AssemblyUnregistrationRequest
|
||||||
|
{
|
||||||
|
public Guid TenantId { get; set; }
|
||||||
|
public string ServiceName { get; set; }
|
||||||
|
public DateTime RequestTime { get; set; }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -14,24 +14,36 @@ namespace Sozsoft.Platform.DynamicServices;
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static class DynamicServiceTypeRegistry
|
public static class DynamicServiceTypeRegistry
|
||||||
{
|
{
|
||||||
private static readonly ConcurrentBag<Type> _registeredTypes = new();
|
private static readonly ConcurrentDictionary<Type, byte> _registeredTypes = new();
|
||||||
|
|
||||||
public static void RegisterType(Type type)
|
public static void RegisterType(Type type)
|
||||||
{
|
{
|
||||||
if (!_registeredTypes.Contains(type))
|
_registeredTypes.TryAdd(type, 0);
|
||||||
{
|
|
||||||
_registeredTypes.Add(type);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static bool IsRegistered(Type type)
|
public static bool IsRegistered(Type type)
|
||||||
{
|
{
|
||||||
return _registeredTypes.Contains(type);
|
return _registeredTypes.ContainsKey(type);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static IEnumerable<Type> GetAllTypes()
|
public static IEnumerable<Type> GetAllTypes()
|
||||||
{
|
{
|
||||||
return _registeredTypes.ToList();
|
return _registeredTypes.Keys.ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Belirtilen assembly adı prefix'iyle başlayan tüm kayıtlı tipleri kaldırır.
|
||||||
|
/// </summary>
|
||||||
|
public static void UnregisterByAssemblyNamePrefix(string assemblyNamePrefix)
|
||||||
|
{
|
||||||
|
var toRemove = _registeredTypes.Keys
|
||||||
|
.Where(t => t.Assembly.GetName().Name?.StartsWith(assemblyNamePrefix) == true)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
foreach (var type in toRemove)
|
||||||
|
{
|
||||||
|
_registeredTypes.TryRemove(type, out _);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -453,6 +453,12 @@ public class PlatformHttpApiHostModule : AbpModule
|
||||||
DynamicAssemblyRegistrationService.RequestAssemblyRegistration(tenantId, assembly, assemblyName);
|
DynamicAssemblyRegistrationService.RequestAssemblyRegistration(tenantId, assembly, assemblyName);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Setup delegate for dynamic service unregistration
|
||||||
|
DynamicServiceCompiler.NotifyAssemblyUnregistration = (tenantId, serviceName) =>
|
||||||
|
{
|
||||||
|
DynamicAssemblyRegistrationService.RequestAssemblyUnregistration(tenantId, serviceName);
|
||||||
|
};
|
||||||
|
|
||||||
if (env.IsDevelopment())
|
if (env.IsDevelopment())
|
||||||
{
|
{
|
||||||
app.UseDeveloperExceptionPage();
|
app.UseDeveloperExceptionPage();
|
||||||
|
|
|
||||||
|
|
@ -154,6 +154,7 @@ public class Program
|
||||||
|
|
||||||
// Dynamic Assembly Registration Delegate Setup
|
// Dynamic Assembly Registration Delegate Setup
|
||||||
DynamicServiceCompiler.NotifyAssemblyRegistration = DynamicAssemblyRegistrationService.RequestAssemblyRegistration;
|
DynamicServiceCompiler.NotifyAssemblyRegistration = DynamicAssemblyRegistrationService.RequestAssemblyRegistration;
|
||||||
|
DynamicServiceCompiler.NotifyAssemblyUnregistration = DynamicAssemblyRegistrationService.RequestAssemblyUnregistration;
|
||||||
await app.InitializeApplicationAsync();
|
await app.InitializeApplicationAsync();
|
||||||
await app.RunAsync();
|
await app.RunAsync();
|
||||||
return 0;
|
return 0;
|
||||||
|
|
|
||||||
|
|
@ -43,6 +43,8 @@ export const ROUTES_ENUM = {
|
||||||
componentsView: '/admin/developerkit/components/view/:id',
|
componentsView: '/admin/developerkit/components/view/:id',
|
||||||
componentsEdit: '/admin/developerkit/components/edit/:id',
|
componentsEdit: '/admin/developerkit/components/edit/:id',
|
||||||
dynamicServices: '/admin/developerkit/dynamic-services',
|
dynamicServices: '/admin/developerkit/dynamic-services',
|
||||||
|
dynamicServicesNew: '/admin/developerkit/dynamic-services/new',
|
||||||
|
dynamicServicesEdit: '/admin/developerkit/dynamic-services/edit/:id',
|
||||||
},
|
},
|
||||||
reports: {
|
reports: {
|
||||||
generator: '/admin/reports/generator',
|
generator: '/admin/reports/generator',
|
||||||
|
|
|
||||||
|
|
@ -54,6 +54,7 @@ export interface PublishDto {
|
||||||
displayName?: string
|
displayName?: string
|
||||||
description?: string
|
description?: string
|
||||||
primaryEntityType?: string
|
primaryEntityType?: string
|
||||||
|
isActive?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface DynamicAppServiceListResult {
|
export interface DynamicAppServiceListResult {
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import React, { useState, useEffect, useCallback } from 'react'
|
import React, { useState, useEffect, useCallback } from 'react'
|
||||||
import { useParams, useNavigate } from 'react-router-dom'
|
import { useParams, useNavigate, Link } from 'react-router-dom'
|
||||||
import { useComponents } from '../../contexts/ComponentContext'
|
import { useComponents } from '../../contexts/ComponentContext'
|
||||||
import {
|
import {
|
||||||
FaRegSave,
|
FaRegSave,
|
||||||
|
|
@ -211,26 +211,25 @@ export default ${pascalCaseName}Component;`
|
||||||
<div className="px-4 py-3">
|
<div className="px-4 py-3">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div className="flex items-center gap-4">
|
<div className="flex items-center gap-4">
|
||||||
<button
|
<Link
|
||||||
type="button"
|
to={ROUTES_ENUM.protected.saas.developerKit.components}
|
||||||
onClick={() => navigate(ROUTES_ENUM.protected.saas.developerKit.components)}
|
className="flex items-center gap-2 text-slate-600 text-black px-4 py-2 rounded-lg hover:text-slate-700 transition-colors"
|
||||||
className="flex items-center gap-2 text-slate-600 hover:text-blue-600 hover:bg-blue-50 px-3 py-2 rounded-lg transition-all duration-200"
|
|
||||||
>
|
>
|
||||||
<FaArrowLeft className="w-4 h-4" />
|
<FaArrowLeft className="w-3.5 h-3.5" />
|
||||||
{translate('::App.DeveloperKit.ComponentEditor.Back')}
|
{translate('::App.DeveloperKit.ComponentEditor.Back')}
|
||||||
</button>
|
</Link>
|
||||||
<div className="h-6 w-px bg-slate-300"></div>
|
<div className="h-6 w-px bg-slate-300"></div>
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<div className="bg-gradient-to-r from-blue-500 to-purple-600 p-2 rounded-lg">
|
<div className="bg-gradient-to-r from-blue-500 to-purple-600 p-2 rounded-lg">
|
||||||
<FaCode className="w-5 h-5 text-white" />
|
<FaCode className="w-5 h-5 text-white" />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-xl font-bold text-slate-900">
|
<h1 className="font-semibold text-slate-800 text-sm leading-tight">
|
||||||
{isEditing
|
{isEditing
|
||||||
? `${translate('::App.DeveloperKit.ComponentEditor.Title.Edit')} - ${values.name || initialValues.name || 'Component'}`
|
? `${translate('::App.DeveloperKit.ComponentEditor.Title.Edit')} - ${values.name || initialValues.name || 'Component'}`
|
||||||
: translate('::App.DeveloperKit.ComponentEditor.Title.Create')}
|
: translate('::App.DeveloperKit.ComponentEditor.Title.Create')}
|
||||||
</h1>
|
</h1>
|
||||||
<p className="text-sm text-slate-600">
|
<p className="text-xs text-slate-500 leading-tight">
|
||||||
{isEditing ? 'Modify your React component' : 'Create a new React component'}
|
{isEditing ? 'Modify your React component' : 'Create a new React component'}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -243,7 +242,7 @@ export default ${pascalCaseName}Component;`
|
||||||
type="button"
|
type="button"
|
||||||
onClick={submitForm}
|
onClick={submitForm}
|
||||||
disabled={isSubmitting || !values.name.trim() || !isValid}
|
disabled={isSubmitting || !values.name.trim() || !isValid}
|
||||||
className="flex items-center gap-1 bg-gradient-to-r from-green-600 to-green-700 hover:from-green-700 hover:to-green-800 text-white font-semibold px-2 py-1.5 rounded shadow transition-all duration-200 disabled:opacity-50 disabled:cursor-not-allowed text-sm"
|
className="flex items-center gap-2 bg-emerald-600 text-white px-4 py-2 rounded-lg hover:bg-emerald-700 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
|
||||||
>
|
>
|
||||||
<FaRegSave className="w-4 h-4" />
|
<FaRegSave className="w-4 h-4" />
|
||||||
{isSubmitting
|
{isSubmitting
|
||||||
|
|
@ -255,7 +254,7 @@ export default ${pascalCaseName}Component;`
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Form className="grid grid-cols-1 lg:grid-cols-3 gap-4 pt-2">
|
<Form className="grid grid-cols-1 lg:grid-cols-3 gap-4 py-3">
|
||||||
{/* Left Side - Component Settings */}
|
{/* Left Side - Component Settings */}
|
||||||
<div className="space-y-3 col-span-1">
|
<div className="space-y-3 col-span-1">
|
||||||
<div className="bg-white rounded-lg shadow-sm border border-slate-200 p-3">
|
<div className="bg-white rounded-lg shadow-sm border border-slate-200 p-3">
|
||||||
|
|
@ -325,7 +324,7 @@ export default ${pascalCaseName}Component;`
|
||||||
component={Input}
|
component={Input}
|
||||||
placeholder="React component code goes here"
|
placeholder="React component code goes here"
|
||||||
textArea={true}
|
textArea={true}
|
||||||
rows={5}
|
rows={10}
|
||||||
onChange={(e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
onChange={(e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
||||||
setFieldValue('code', e.target.value)
|
setFieldValue('code', e.target.value)
|
||||||
}}
|
}}
|
||||||
|
|
|
||||||
|
|
@ -1,53 +1,31 @@
|
||||||
import React, { useState, useRef, useEffect } from 'react'
|
import React, { useState, useEffect } from 'react'
|
||||||
|
import { useParams, useNavigate, Link } from 'react-router-dom'
|
||||||
import { Editor } from '@monaco-editor/react'
|
import { Editor } from '@monaco-editor/react'
|
||||||
import {
|
import {
|
||||||
FaPlay,
|
FaPlay,
|
||||||
FaUpload,
|
FaCopy,
|
||||||
FaCode,
|
|
||||||
FaCheckCircle,
|
FaCheckCircle,
|
||||||
FaExclamationCircle,
|
FaExclamationCircle,
|
||||||
FaSpinner,
|
FaSpinner,
|
||||||
FaCopy,
|
|
||||||
FaExternalLinkAlt,
|
FaExternalLinkAlt,
|
||||||
FaTrash,
|
FaArrowLeft,
|
||||||
FaSync,
|
FaCog,
|
||||||
|
FaCode,
|
||||||
|
FaSave,
|
||||||
} from 'react-icons/fa'
|
} from 'react-icons/fa'
|
||||||
import { useLocalization } from '@/utils/hooks/useLocalization'
|
import { useLocalization } from '@/utils/hooks/useLocalization'
|
||||||
import {
|
import {
|
||||||
dynamicServiceService,
|
dynamicServiceService,
|
||||||
type CompileResult,
|
type CompileResult,
|
||||||
type PublishResult,
|
type PublishResult,
|
||||||
type DynamicServiceDto,
|
|
||||||
postTestCompile,
|
postTestCompile,
|
||||||
TestCompileDto,
|
type TestCompileDto,
|
||||||
} from '@/services/dynamicService.service'
|
} from '@/services/dynamicService.service'
|
||||||
import { Helmet } from 'react-helmet'
|
import { Helmet } from 'react-helmet'
|
||||||
import { APP_NAME } from '@/constants/app.constant'
|
import { APP_NAME } from '@/constants/app.constant'
|
||||||
|
import { ROUTES_ENUM } from '@/routes/route.constant'
|
||||||
|
|
||||||
const DynamicAppServiceEditor: React.FC = () => {
|
const defaultTemplate = `using System;
|
||||||
// State
|
|
||||||
const [code, setCode] = useState('')
|
|
||||||
const [serviceName, setServiceName] = useState('')
|
|
||||||
const [displayName, setDisplayName] = useState('')
|
|
||||||
const [description, setDescription] = useState('')
|
|
||||||
const [primaryEntityType, setPrimaryEntityType] = useState('')
|
|
||||||
|
|
||||||
const [isCompiling, setIsCompiling] = useState(false)
|
|
||||||
const [isPublishing, setIsPublishing] = useState(false)
|
|
||||||
const [isLoading, setIsLoading] = useState(false)
|
|
||||||
|
|
||||||
const [compileResult, setCompileResult] = useState<CompileResult | null>(null)
|
|
||||||
const [publishResult, setPublishResult] = useState<PublishResult | null>(null)
|
|
||||||
|
|
||||||
const [services, setServices] = useState<DynamicServiceDto[]>([])
|
|
||||||
const [selectedService, setSelectedService] = useState<DynamicServiceDto | null>(null)
|
|
||||||
|
|
||||||
const [showServiceList, setShowServiceList] = useState(true)
|
|
||||||
|
|
||||||
const { translate } = useLocalization()
|
|
||||||
|
|
||||||
// Template kod
|
|
||||||
const defaultTemplate = `using System;
|
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Volo.Abp.Application.Services;
|
using Volo.Abp.Application.Services;
|
||||||
|
|
@ -59,14 +37,6 @@ namespace DynamicServices
|
||||||
[Authorize]
|
[Authorize]
|
||||||
public class DynamicCustomerAppService : ApplicationService
|
public class DynamicCustomerAppService : ApplicationService
|
||||||
{
|
{
|
||||||
// Repository injection örneği (kendi entity'nizi kullanın)
|
|
||||||
// private readonly IRepository<Customer, Guid> _customerRepository;
|
|
||||||
|
|
||||||
// public DynamicCustomerAppService(IRepository<Customer, Guid> customerRepository)
|
|
||||||
// {
|
|
||||||
// _customerRepository = customerRepository;
|
|
||||||
// }
|
|
||||||
|
|
||||||
public virtual async Task<string> GetHelloWorldAsync()
|
public virtual async Task<string> GetHelloWorldAsync()
|
||||||
{
|
{
|
||||||
return await Task.FromResult("Hello World from Dynamic AppService!");
|
return await Task.FromResult("Hello World from Dynamic AppService!");
|
||||||
|
|
@ -81,22 +51,29 @@ namespace DynamicServices
|
||||||
"Item 3"
|
"Item 3"
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Repository kullanım örneği:
|
|
||||||
// public virtual async Task<List<Customer>> GetCustomersAsync()
|
|
||||||
// {
|
|
||||||
// return await _customerRepository.GetListAsync();
|
|
||||||
// }
|
|
||||||
}
|
}
|
||||||
}`
|
}`
|
||||||
|
|
||||||
// Component mount
|
const DynamicServiceEditor: React.FC = () => {
|
||||||
useEffect(() => {
|
const { id } = useParams<{ id: string }>()
|
||||||
setCode(defaultTemplate)
|
const navigate = useNavigate()
|
||||||
loadServices()
|
const { translate } = useLocalization()
|
||||||
}, [])
|
|
||||||
|
const [code, setCode] = useState(defaultTemplate)
|
||||||
|
const [serviceName, setServiceName] = useState('')
|
||||||
|
const [displayName, setDisplayName] = useState('')
|
||||||
|
const [description, setDescription] = useState('')
|
||||||
|
const [primaryEntityType, setPrimaryEntityType] = useState('')
|
||||||
|
const [isActive, setIsActive] = useState(true)
|
||||||
|
const [submitted, setSubmitted] = useState(false)
|
||||||
|
|
||||||
|
const [isCompiling, setIsCompiling] = useState(false)
|
||||||
|
const [isPublishing, setIsPublishing] = useState(false)
|
||||||
|
const [isLoading, setIsLoading] = useState(false)
|
||||||
|
|
||||||
|
const [compileResult, setCompileResult] = useState<CompileResult | null>(null)
|
||||||
|
const [publishResult, setPublishResult] = useState<PublishResult | null>(null)
|
||||||
|
|
||||||
// Monaco Editor ayarları
|
|
||||||
const editorOptions = {
|
const editorOptions = {
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
lineNumbers: 'on' as const,
|
lineNumbers: 'on' as const,
|
||||||
|
|
@ -106,39 +83,42 @@ namespace DynamicServices
|
||||||
minimap: { enabled: false },
|
minimap: { enabled: false },
|
||||||
folding: true,
|
folding: true,
|
||||||
wordWrap: 'on' as const,
|
wordWrap: 'on' as const,
|
||||||
theme: 'vs-dark',
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Servisleri yükle
|
useEffect(() => {
|
||||||
const loadServices = async () => {
|
if (id) {
|
||||||
|
loadService(id)
|
||||||
|
}
|
||||||
|
}, [id])
|
||||||
|
|
||||||
|
const loadService = async (serviceId: string) => {
|
||||||
try {
|
try {
|
||||||
setIsLoading(true)
|
setIsLoading(true)
|
||||||
const response = await dynamicServiceService.getList()
|
const data = await dynamicServiceService.getById(serviceId)
|
||||||
setServices(response.items || [])
|
setCode(data.code)
|
||||||
|
setServiceName(data.name)
|
||||||
|
setDisplayName(data.displayName || '')
|
||||||
|
setDescription(data.description || '')
|
||||||
|
setPrimaryEntityType(data.primaryEntityType || '')
|
||||||
|
setIsActive(data.isActive ?? true)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Servisler yüklenirken hata:', error)
|
console.error('Servis yüklenirken hata:', error)
|
||||||
} finally {
|
} finally {
|
||||||
setIsLoading(false)
|
setIsLoading(false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Test compile
|
|
||||||
const handleTestCompile = async () => {
|
const handleTestCompile = async () => {
|
||||||
if (!code.trim()) {
|
if (!code.trim()) {
|
||||||
alert('Lütfen kod girin')
|
alert('Lütfen kod girin')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
setIsCompiling(true)
|
setIsCompiling(true)
|
||||||
setCompileResult(null)
|
setCompileResult(null)
|
||||||
console.log('Test compile code:', code)
|
const result = await postTestCompile({ code } as TestCompileDto)
|
||||||
const input = { code: code } as TestCompileDto
|
|
||||||
const result = await postTestCompile(input)
|
|
||||||
setCompileResult(result.data)
|
setCompileResult(result.data)
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.error('Test compile error:', error)
|
|
||||||
console.error('Error response:', error.response?.data)
|
|
||||||
setCompileResult({
|
setCompileResult({
|
||||||
success: false,
|
success: false,
|
||||||
errorMessage: error.response?.data?.message || 'Derleme sırasında hata oluştu',
|
errorMessage: error.response?.data?.message || 'Derleme sırasında hata oluştu',
|
||||||
|
|
@ -151,34 +131,36 @@ namespace DynamicServices
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Publish
|
|
||||||
const handlePublish = async () => {
|
const handlePublish = async () => {
|
||||||
|
setSubmitted(true)
|
||||||
if (!code.trim() || !serviceName.trim()) {
|
if (!code.trim() || !serviceName.trim()) {
|
||||||
alert('Lütfen kod ve servis adını girin')
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if (isPublishing) return
|
||||||
try {
|
try {
|
||||||
setIsPublishing(true)
|
setIsPublishing(true)
|
||||||
setPublishResult(null)
|
setPublishResult(null)
|
||||||
|
|
||||||
const requestData = {
|
// Edit modunda: önce eskiyi sil, sonra yeniden yayınla
|
||||||
name: serviceName,
|
if (id) {
|
||||||
code: code,
|
await dynamicServiceService.delete(id)
|
||||||
displayName: displayName,
|
|
||||||
description: description,
|
|
||||||
primaryEntityType: primaryEntityType,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = await dynamicServiceService.publish(requestData)
|
const result = await dynamicServiceService.publish({
|
||||||
setPublishResult(result)
|
name: serviceName,
|
||||||
|
code,
|
||||||
|
displayName,
|
||||||
|
description,
|
||||||
|
primaryEntityType,
|
||||||
|
isActive,
|
||||||
|
})
|
||||||
|
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
await loadServices() // Listeyi yenile
|
navigate(ROUTES_ENUM.protected.saas.developerKit.dynamicServices)
|
||||||
|
} else {
|
||||||
|
setPublishResult(result)
|
||||||
}
|
}
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.error('Publish error:', error)
|
|
||||||
console.error('Error response:', error.response?.data)
|
|
||||||
setPublishResult({
|
setPublishResult({
|
||||||
success: false,
|
success: false,
|
||||||
errorMessage: error.response?.data?.message || 'Yayınlama sırasında hata oluştu',
|
errorMessage: error.response?.data?.message || 'Yayınlama sırasında hata oluştu',
|
||||||
|
|
@ -188,417 +170,228 @@ namespace DynamicServices
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Servisi yükle
|
|
||||||
const loadService = async (service: DynamicServiceDto) => {
|
|
||||||
try {
|
|
||||||
const data = await dynamicServiceService.getById(service.id)
|
|
||||||
|
|
||||||
setSelectedService(data)
|
|
||||||
setCode(data.code)
|
|
||||||
setServiceName(data.name)
|
|
||||||
setDisplayName(data.displayName || '')
|
|
||||||
setDescription(data.description || '')
|
|
||||||
setPrimaryEntityType(data.primaryEntityType || '')
|
|
||||||
|
|
||||||
setCompileResult(null)
|
|
||||||
setPublishResult(null)
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Servis yüklenirken hata:', error)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Servisi sil
|
|
||||||
const deleteService = async (serviceId: string) => {
|
|
||||||
if (!confirm('Bu servisi silmek istediğinizden emin misiniz?')) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
await dynamicServiceService.delete(serviceId)
|
|
||||||
await loadServices()
|
|
||||||
|
|
||||||
if (selectedService?.id === serviceId) {
|
|
||||||
setSelectedService(null)
|
|
||||||
setCode(defaultTemplate)
|
|
||||||
setServiceName('')
|
|
||||||
setDisplayName('')
|
|
||||||
setDescription('')
|
|
||||||
setPrimaryEntityType('')
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Servis silinirken hata:', error)
|
|
||||||
alert('Servis silinirken hata oluştu')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Yeni servis
|
|
||||||
const newService = () => {
|
|
||||||
setSelectedService(null)
|
|
||||||
setCode(defaultTemplate)
|
|
||||||
setServiceName('')
|
|
||||||
setDisplayName('')
|
|
||||||
setDescription('')
|
|
||||||
setPrimaryEntityType('')
|
|
||||||
setCompileResult(null)
|
|
||||||
setPublishResult(null)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Swagger aç
|
|
||||||
const openSwagger = () => {
|
|
||||||
window.open(`${import.meta.env.VITE_API_URL}/swagger/index.html`, '_blank')
|
|
||||||
}
|
|
||||||
|
|
||||||
// Kodu kopyala
|
|
||||||
const copyCode = () => {
|
const copyCode = () => {
|
||||||
navigator.clipboard.writeText(code)
|
navigator.clipboard.writeText(code)
|
||||||
alert('Kod panoya kopyalandı')
|
alert('Kod panoya kopyalandı')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const pageTitle = id ? `Servis Düzenle` : `Yeni Servis`
|
||||||
|
|
||||||
|
if (isLoading) {
|
||||||
|
return (
|
||||||
|
<div className="flex items-center justify-center h-64">
|
||||||
|
<FaSpinner className="w-8 h-8 animate-spin text-slate-400" />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const serviceNameError = submitted && !serviceName.trim()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<Helmet
|
<Helmet titleTemplate={`%s | ${APP_NAME}`} title={pageTitle} defaultTitle={APP_NAME} />
|
||||||
titleTemplate={`%s | ${APP_NAME}`}
|
|
||||||
title={translate('::' + 'App.DeveloperKit.DynamicServices')}
|
|
||||||
defaultTitle={APP_NAME}
|
|
||||||
></Helmet>
|
|
||||||
|
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="flex items-center justify-between mb-4">
|
<div className="bg-white shadow-lg border-b border-slate-200 sticky top-0 z-10">
|
||||||
|
<div className="flex items-center justify-between px-4 py-3">
|
||||||
|
{/* Left: back + icon + title */}
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<Link
|
||||||
|
to={ROUTES_ENUM.protected.saas.developerKit.dynamicServices}
|
||||||
|
className="flex items-center gap-2 text-slate-600 text-black px-4 py-2 rounded-lg hover:text-slate-700 transition-colors"
|
||||||
|
>
|
||||||
|
<FaArrowLeft className="w-3.5 h-3.5" />
|
||||||
|
Servislere Dön
|
||||||
|
</Link>
|
||||||
|
<div className="h-6 w-px bg-slate-300"></div>
|
||||||
|
<div className="flex items-center justify-center w-9 h-9 rounded-lg bg-gradient-to-r from-blue-500 to-purple-600 text-white shrink-0">
|
||||||
|
<FaCode className="w-4 h-4" />
|
||||||
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-2xl font-bold text-slate-900">
|
<h1 className="font-semibold text-slate-800 text-sm leading-tight">{pageTitle}</h1>
|
||||||
{translate('::App.DeveloperKit.DynamicServices')}
|
<p className="text-xs text-slate-500 leading-tight">
|
||||||
</h1>
|
{id ? 'Mevcut servisi düzenleyin' : 'Yeni bir dynamic servis oluşturun'}
|
||||||
<p className="text-slate-600">
|
|
||||||
{translate('::App.DeveloperKit.DynamicServices.Description')}
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-3">
|
|
||||||
<button
|
|
||||||
onClick={openSwagger}
|
|
||||||
className="flex items-center gap-2 bg-blue-600 text-white px-4 py-2 rounded-lg hover:bg-blue-700 transition-colors"
|
|
||||||
>
|
|
||||||
<FaExternalLinkAlt className="w-4 h-4" />
|
|
||||||
Swagger
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
onClick={() => setShowServiceList(!showServiceList)}
|
|
||||||
className="flex items-center gap-2 bg-gray-600 text-white px-4 py-2 rounded-lg hover:bg-gray-700 transition-colors"
|
|
||||||
>
|
|
||||||
<FaCode className="w-4 h-4" />
|
|
||||||
{showServiceList ? 'Listeyi Gizle' : 'Listeyi Göster'}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid grid-cols-12 gap-6">
|
{/* Right: action buttons + swagger + publish */}
|
||||||
{/* Service List */}
|
<div className="flex items-center gap-2">
|
||||||
{showServiceList && (
|
|
||||||
<div className="col-span-12 lg:col-span-4">
|
|
||||||
<div className="bg-white rounded-lg shadow-sm border">
|
|
||||||
<div className="p-4 border-b">
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<h3 className="text-lg font-semibold">Mevcut Servisler</h3>
|
|
||||||
<div className="flex gap-2">
|
|
||||||
<button
|
<button
|
||||||
onClick={newService}
|
onClick={copyCode}
|
||||||
className="bg-green-600 text-white px-3 py-1 rounded text-sm hover:bg-green-700"
|
className="flex items-center gap-2 px-4 py-2 border border-slate-300 rounded-lg text-slate-600 hover:bg-slate-50 transition-colors"
|
||||||
>
|
>
|
||||||
<FaCode className="w-4 h-4 inline mr-1" />
|
<FaCopy className="w-3.5 h-3.5" />
|
||||||
Yeni
|
Kodu Kopyala
|
||||||
</button>
|
</button>
|
||||||
<button
|
|
||||||
onClick={loadServices}
|
|
||||||
className="bg-gray-600 text-white px-3 py-1 rounded text-sm hover:bg-gray-700"
|
|
||||||
>
|
|
||||||
<FaSync className="w-4 h-4" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="max-h-96 overflow-y-auto">
|
|
||||||
{isLoading ? (
|
|
||||||
<div className="p-4 text-center">
|
|
||||||
<FaSpinner className="w-6 h-6 animate-spin mx-auto mb-2" />
|
|
||||||
Yükleniyor...
|
|
||||||
</div>
|
|
||||||
) : services.length > 0 ? (
|
|
||||||
services.map((service) => (
|
|
||||||
<div
|
|
||||||
key={service.id}
|
|
||||||
className={`p-3 border-b hover:bg-gray-50 cursor-pointer ${
|
|
||||||
selectedService?.id === service.id
|
|
||||||
? 'bg-blue-50 border-l-4 border-l-blue-500'
|
|
||||||
: ''
|
|
||||||
}`}
|
|
||||||
onClick={() => loadService(service)}
|
|
||||||
>
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<div className="flex-1">
|
|
||||||
<h4 className="font-medium text-gray-900">{service.name}</h4>
|
|
||||||
{service.displayName && (
|
|
||||||
<p className="text-sm text-gray-600">{service.displayName}</p>
|
|
||||||
)}
|
|
||||||
<div className="flex items-center gap-2 mt-1">
|
|
||||||
<span
|
|
||||||
className={`px-2 py-1 text-xs rounded ${
|
|
||||||
service.compilationStatus === 'Success'
|
|
||||||
? 'bg-green-100 text-green-800'
|
|
||||||
: service.compilationStatus === 'Failed'
|
|
||||||
? 'bg-red-100 text-red-800'
|
|
||||||
: 'bg-yellow-100 text-yellow-800'
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{service.compilationStatus}
|
|
||||||
</span>
|
|
||||||
<span className="text-xs text-gray-500">v{service.version}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<button
|
<button
|
||||||
onClick={(e) => {
|
onClick={handleTestCompile}
|
||||||
e.stopPropagation()
|
disabled={isCompiling || !code.trim()}
|
||||||
deleteService(service.id)
|
className="flex items-center gap-2 bg-orange-500 text-white px-4 py-2 rounded-lg hover:bg-orange-600 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
|
||||||
}}
|
|
||||||
className="text-red-600 hover:text-red-800 p-1"
|
|
||||||
>
|
>
|
||||||
<FaTrash className="w-4 h-4" />
|
{isCompiling ? (
|
||||||
</button>
|
<FaSpinner className="w-3.5 h-3.5 animate-spin" />
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))
|
|
||||||
) : (
|
) : (
|
||||||
<div className="p-4 text-center text-gray-500">Henüz servis yok</div>
|
<FaPlay className="w-3.5 h-3.5" />
|
||||||
|
)}
|
||||||
|
{isCompiling ? 'Derleniyor...' : 'Test Compile'}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
onClick={handlePublish}
|
||||||
|
disabled={isPublishing}
|
||||||
|
className="flex items-center gap-2 bg-emerald-600 text-white px-4 py-2 rounded-lg hover:bg-emerald-700 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
|
||||||
|
>
|
||||||
|
{isPublishing ? (
|
||||||
|
<FaSpinner className="w-3.5 h-3.5 animate-spin" />
|
||||||
|
) : (
|
||||||
|
<FaSave className="w-3.5 h-3.5" />
|
||||||
|
)}
|
||||||
|
{isPublishing ? 'Yayınlanıyor...' : 'Yayınla'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Compile / Publish result banners */}
|
||||||
|
{compileResult && (
|
||||||
|
<div
|
||||||
|
className={`flex items-start gap-3 rounded-lg border px-4 py-3 text-sm ${
|
||||||
|
compileResult.success
|
||||||
|
? 'bg-emerald-50 border-emerald-200 text-emerald-800'
|
||||||
|
: 'bg-red-50 border-red-200 text-red-800'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{compileResult.success ? (
|
||||||
|
<FaCheckCircle className="w-4 h-4 mt-0.5 shrink-0 text-emerald-600" />
|
||||||
|
) : (
|
||||||
|
<FaExclamationCircle className="w-4 h-4 mt-0.5 shrink-0 text-red-600" />
|
||||||
|
)}
|
||||||
|
<div className="flex-1">
|
||||||
|
<span className="font-medium">
|
||||||
|
Derleme {compileResult.success ? 'Başarılı' : 'Başarısız'}
|
||||||
|
</span>
|
||||||
|
{!compileResult.success && compileResult.errors && compileResult.errors.length > 0 && (
|
||||||
|
<ul className="mt-1 space-y-0.5">
|
||||||
|
{compileResult.errors.map((e, i) => (
|
||||||
|
<li key={i} className="text-xs font-mono">
|
||||||
|
[{e.code}] Satır {e.line}: {e.message}
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
<span className="text-xs text-slate-400 shrink-0">
|
||||||
|
{compileResult.compilationTimeMs}ms
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{publishResult && !publishResult.success && (
|
||||||
|
<div className="flex items-start gap-3 rounded-lg border bg-red-50 border-red-200 text-red-800 px-4 py-3 text-sm">
|
||||||
|
<FaExclamationCircle className="w-4 h-4 mt-0.5 shrink-0 text-red-600" />
|
||||||
|
<div>
|
||||||
|
<span className="font-medium">Yayınlama Başarısız</span>
|
||||||
|
{publishResult.errorMessage && (
|
||||||
|
<p className="text-xs mt-0.5">{publishResult.errorMessage}</p>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Main Editor */}
|
{/* Two-panel layout */}
|
||||||
<div className={`col-span-12 ${showServiceList ? 'lg:col-span-8' : ''}`}>
|
<div className="flex gap-4 items-start">
|
||||||
{/* Service Info Form */}
|
{/* LEFT PANEL — Servis Ayarları */}
|
||||||
<div className="bg-white rounded-lg shadow-sm border p-4 mb-4">
|
<div className="w-1/4 shrink-0 bg-white rounded-lg border border-slate-200 p-5 space-y-4">
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
{/* Panel header */}
|
||||||
|
<div className="flex items-center gap-2 pb-3 border-b border-slate-100">
|
||||||
|
<FaCog className="w-4 h-4 text-blue-500" />
|
||||||
|
<h2 className="font-semibold text-slate-700 text-sm">Servis Ayarları</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Servis Adı */}
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-1">Servis Adı *</label>
|
<label className="block text-sm font-medium text-slate-700 mb-1">Servis Adı</label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={serviceName}
|
value={serviceName}
|
||||||
onChange={(e) => setServiceName(e.target.value)}
|
onChange={(e) => {
|
||||||
|
setServiceName(e.target.value)
|
||||||
|
setSubmitted(false)
|
||||||
|
}}
|
||||||
placeholder="ör: DynamicCustomerAppService"
|
placeholder="ör: DynamicCustomerAppService"
|
||||||
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
className={`w-full px-3 py-2 border rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-colors ${
|
||||||
|
serviceNameError ? 'border-red-500 bg-red-50' : 'border-slate-300'
|
||||||
|
}`}
|
||||||
/>
|
/>
|
||||||
|
{serviceNameError && <p className="text-red-500 text-xs mt-1">Servis adı zorunludur</p>}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Görünen Ad */}
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-1">Görünen Ad</label>
|
<label className="block text-sm font-medium text-slate-700 mb-1">Görünen Ad</label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={displayName}
|
value={displayName}
|
||||||
onChange={(e) => setDisplayName(e.target.value)}
|
onChange={(e) => setDisplayName(e.target.value)}
|
||||||
placeholder="ör: Müşteri Yönetimi"
|
placeholder="ör: Müşteri Yönetimi"
|
||||||
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
className="w-full px-3 py-2 border border-slate-300 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="md:col-span-2">
|
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-1">Açıklama</label>
|
{/* Açıklama */}
|
||||||
<textarea
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-slate-700 mb-1">Açıklama</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
value={description}
|
value={description}
|
||||||
onChange={(e) => setDescription(e.target.value)}
|
onChange={(e) => setDescription(e.target.value)}
|
||||||
placeholder="Bu servisin ne yaptığını açıklayın..."
|
placeholder="Bu servisin kısa açıklaması"
|
||||||
rows={2}
|
className="w-full px-3 py-2 border border-slate-300 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
||||||
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Ana Entity Türü */}
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
<label className="block text-sm font-medium text-slate-700 mb-1">Ana Entity Türü</label>
|
||||||
Ana Entity Türü
|
|
||||||
</label>
|
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={primaryEntityType}
|
value={primaryEntityType}
|
||||||
onChange={(e) => setPrimaryEntityType(e.target.value)}
|
onChange={(e) => setPrimaryEntityType(e.target.value)}
|
||||||
placeholder="ör: Customer"
|
placeholder="ör: Customer"
|
||||||
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
className="w-full px-3 py-2 border border-slate-300 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Aktif */}
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-slate-700 mb-1">Aktif</label>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={isActive}
|
||||||
|
onChange={(e) => setIsActive(e.target.checked)}
|
||||||
|
className="w-4 h-4 rounded accent-blue-600 cursor-pointer"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Action Buttons */}
|
|
||||||
<div className="bg-white rounded-lg shadow-sm border p-4 mb-4">
|
|
||||||
<div className="flex flex-wrap items-center gap-3">
|
|
||||||
<button
|
|
||||||
onClick={handleTestCompile}
|
|
||||||
disabled={isCompiling || !code.trim()}
|
|
||||||
className="flex items-center gap-2 bg-orange-600 text-white px-4 py-2 rounded-lg hover:bg-orange-700 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
|
|
||||||
>
|
|
||||||
{isCompiling ? (
|
|
||||||
<FaSpinner className="w-4 h-4 animate-spin" />
|
|
||||||
) : (
|
|
||||||
<FaPlay className="w-4 h-4" />
|
|
||||||
)}
|
|
||||||
Test Compile
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<button
|
|
||||||
onClick={handlePublish}
|
|
||||||
disabled={isPublishing || !code.trim() || !serviceName.trim()}
|
|
||||||
className="flex items-center gap-2 bg-green-600 text-white px-4 py-2 rounded-lg hover:bg-green-700 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
|
|
||||||
>
|
|
||||||
{isPublishing ? (
|
|
||||||
<FaSpinner className="w-4 h-4 animate-spin" />
|
|
||||||
) : (
|
|
||||||
<FaUpload className="w-4 h-4" />
|
|
||||||
)}
|
|
||||||
Publish
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<button
|
|
||||||
onClick={copyCode}
|
|
||||||
className="flex items-center gap-2 bg-gray-600 text-white px-4 py-2 rounded-lg hover:bg-gray-700 transition-colors"
|
|
||||||
>
|
|
||||||
<FaCopy className="w-4 h-4" />
|
|
||||||
Kopyala
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Results */}
|
|
||||||
{(compileResult || publishResult) && (
|
|
||||||
<div className="space-y-4 mb-4">
|
|
||||||
{/* Compile Result */}
|
|
||||||
{compileResult && (
|
|
||||||
<div
|
|
||||||
className={`rounded-lg border p-4 ${
|
|
||||||
compileResult.success
|
|
||||||
? 'bg-green-50 border-green-200'
|
|
||||||
: 'bg-red-50 border-red-200'
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
<div className="flex items-center gap-2 mb-2">
|
|
||||||
{compileResult.success ? (
|
|
||||||
<FaCheckCircle className="w-5 h-5 text-green-600" />
|
|
||||||
) : (
|
|
||||||
<FaExclamationCircle className="w-5 h-5 text-red-600" />
|
|
||||||
)}
|
|
||||||
<h4
|
|
||||||
className={`font-medium ${
|
|
||||||
compileResult.success ? 'text-green-800' : 'text-red-800'
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
Derleme {compileResult.success ? 'Başarılı' : 'Başarısız'}
|
|
||||||
</h4>
|
|
||||||
<span className="text-sm text-gray-600">
|
|
||||||
({compileResult.compilationTimeMs}ms)
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{!compileResult.success &&
|
|
||||||
compileResult.errors &&
|
|
||||||
compileResult.errors.length > 0 && (
|
|
||||||
<div className="space-y-2">
|
|
||||||
{compileResult.errors.map((error, index) => (
|
|
||||||
<div key={index} className="bg-white rounded p-3 border">
|
|
||||||
<div className="flex items-start gap-2">
|
|
||||||
<span className="text-red-600 font-mono text-sm">{error.code}</span>
|
|
||||||
<div className="flex-1">
|
|
||||||
<p className="text-sm text-gray-800">{error.message}</p>
|
|
||||||
<p className="text-xs text-gray-600 mt-1">
|
|
||||||
Satır {error.line}, Sütun {error.column}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{compileResult.hasWarnings && compileResult.warnings && (
|
|
||||||
<div className="mt-3">
|
|
||||||
<h5 className="text-sm font-medium text-yellow-800 mb-2">Uyarılar:</h5>
|
|
||||||
<ul className="list-disc list-inside space-y-1">
|
|
||||||
{compileResult.warnings.map((warning, index) => (
|
|
||||||
<li key={index} className="text-sm text-yellow-700">
|
|
||||||
{warning}
|
|
||||||
</li>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Publish Result */}
|
|
||||||
{publishResult && (
|
|
||||||
<div
|
|
||||||
className={`rounded-lg border p-4 ${
|
|
||||||
publishResult.success
|
|
||||||
? 'bg-green-50 border-green-200'
|
|
||||||
: 'bg-red-50 border-red-200'
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
<div className="flex items-center gap-2 mb-2">
|
|
||||||
{publishResult.success ? (
|
|
||||||
<FaCheckCircle className="w-5 h-5 text-green-600" />
|
|
||||||
) : (
|
|
||||||
<FaExclamationCircle className="w-5 h-5 text-red-600" />
|
|
||||||
)}
|
|
||||||
<h4
|
|
||||||
className={`font-medium ${
|
|
||||||
publishResult.success ? 'text-green-800' : 'text-red-800'
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
Yayınlama {publishResult.success ? 'Başarılı' : 'Başarısız'}
|
|
||||||
</h4>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{publishResult.success && (
|
|
||||||
<div className="space-y-2">
|
|
||||||
{publishResult.controllerName && (
|
|
||||||
<p className="text-sm text-gray-600">
|
|
||||||
Controller:{' '}
|
|
||||||
<code className="bg-gray-100 px-2 py-1 rounded">
|
|
||||||
{publishResult.controllerName}
|
|
||||||
</code>
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
{publishResult.generatedEndpoints &&
|
|
||||||
publishResult.generatedEndpoints.length > 0 && (
|
|
||||||
<div>
|
|
||||||
<h5 className="text-sm font-medium text-gray-700 mb-1">
|
|
||||||
Oluşturulan Endpoint'ler:
|
|
||||||
</h5>
|
|
||||||
<ul className="list-disc list-inside space-y-1">
|
|
||||||
{publishResult.generatedEndpoints.map((endpoint, index) => (
|
|
||||||
<li key={index} className="text-sm text-gray-600 font-mono">
|
|
||||||
{endpoint}
|
|
||||||
</li>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{!publishResult.success && publishResult.errorMessage && (
|
|
||||||
<p className="text-red-700 text-sm">{publishResult.errorMessage}</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
|
{/* RIGHT PANEL — Önizleme + Editor */}
|
||||||
|
<div className="flex-1 min-w-0 space-y-4">
|
||||||
{/* Monaco Editor */}
|
{/* Monaco Editor */}
|
||||||
<div className="bg-white rounded-lg shadow-sm border overflow-hidden">
|
<div className="bg-white rounded-lg border border-slate-200 overflow-hidden">
|
||||||
<div className="p-3 bg-gray-50 border-b flex items-center justify-between">
|
<div className="px-5 py-3 bg-slate-50 border-b border-slate-200 flex items-center justify-between">
|
||||||
<h3 className="font-medium text-gray-700">C# Code Editor</h3>
|
<div className="flex items-center gap-2">
|
||||||
<div className="flex items-center gap-2 text-sm text-gray-600">
|
<FaCode className="w-4 h-4 text-slate-500" />
|
||||||
<span>Lines: {code.split('\n').length}</span>
|
<h3 className="font-medium text-slate-700 text-sm">C# Kod Editörü</h3>
|
||||||
<span>|</span>
|
</div>
|
||||||
<span>Characters: {code.length}</span>
|
<div className="flex items-center gap-2 text-xs text-slate-500">
|
||||||
|
<span>Satır: {code.split('\n').length}</span>
|
||||||
|
<span className="text-slate-300">|</span>
|
||||||
|
<span>Karakter: {code.length}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div style={{ height: '600px' }}>
|
<div style={{ height: '560px' }}>
|
||||||
<Editor
|
<Editor
|
||||||
defaultLanguage="csharp"
|
defaultLanguage="csharp"
|
||||||
value={code}
|
value={code}
|
||||||
|
|
@ -614,4 +407,4 @@ namespace DynamicServices
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default DynamicAppServiceEditor
|
export default DynamicServiceEditor
|
||||||
|
|
|
||||||
291
ui/src/views/developerKit/DynamicServiceManager.tsx
Normal file
291
ui/src/views/developerKit/DynamicServiceManager.tsx
Normal file
|
|
@ -0,0 +1,291 @@
|
||||||
|
import React, { useState, useEffect } from 'react'
|
||||||
|
import { Link } from 'react-router-dom'
|
||||||
|
import {
|
||||||
|
FaPlus,
|
||||||
|
FaSearch,
|
||||||
|
FaRegEdit,
|
||||||
|
FaTrashAlt,
|
||||||
|
FaCheckCircle,
|
||||||
|
FaTimesCircle,
|
||||||
|
FaFilter,
|
||||||
|
FaCalendarAlt,
|
||||||
|
FaCode,
|
||||||
|
FaSpinner,
|
||||||
|
FaExternalLinkAlt,
|
||||||
|
FaArrowDown,
|
||||||
|
FaArrowUp,
|
||||||
|
} from 'react-icons/fa'
|
||||||
|
import { useLocalization } from '@/utils/hooks/useLocalization'
|
||||||
|
import { dynamicServiceService, type DynamicServiceDto } from '@/services/dynamicService.service'
|
||||||
|
import { Helmet } from 'react-helmet'
|
||||||
|
import { APP_NAME } from '@/constants/app.constant'
|
||||||
|
import { ROUTES_ENUM } from '@/routes/route.constant'
|
||||||
|
|
||||||
|
const DynamicServiceManager: React.FC = () => {
|
||||||
|
const { translate } = useLocalization()
|
||||||
|
const [services, setServices] = useState<DynamicServiceDto[]>([])
|
||||||
|
const [isLoading, setIsLoading] = useState(false)
|
||||||
|
const [searchTerm, setSearchTerm] = useState('')
|
||||||
|
const [filterStatus, setFilterStatus] = useState<'all' | 'Success' | 'Failed' | 'Pending'>('all')
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
loadServices()
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const loadServices = async () => {
|
||||||
|
try {
|
||||||
|
setIsLoading(true)
|
||||||
|
const response = await dynamicServiceService.getList()
|
||||||
|
setServices(response.items || [])
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Servisler yüklenirken hata:', error)
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const deleteService = async (serviceId: string) => {
|
||||||
|
if (!confirm('Bu servisi silmek istediğinizden emin misiniz?')) return
|
||||||
|
try {
|
||||||
|
await dynamicServiceService.delete(serviceId)
|
||||||
|
await loadServices()
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Servis silinirken hata:', error)
|
||||||
|
alert('Servis silinirken hata oluştu')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const totalServices = services.length
|
||||||
|
const successServices = services.filter((s) => s.compilationStatus === 'Success').length
|
||||||
|
const failedServices = services.filter((s) => s.compilationStatus === 'Failed').length
|
||||||
|
const activeServices = services.filter((s) => s.isActive).length
|
||||||
|
const inactiveServices = services.filter((s) => !s.isActive).length
|
||||||
|
|
||||||
|
const stats = [
|
||||||
|
{
|
||||||
|
name: 'Toplam',
|
||||||
|
value: totalServices,
|
||||||
|
icon: FaCode,
|
||||||
|
color: 'text-purple-600',
|
||||||
|
bgColor: 'bg-purple-100',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Başarılı',
|
||||||
|
value: successServices,
|
||||||
|
icon: FaCheckCircle,
|
||||||
|
color: 'text-emerald-600',
|
||||||
|
bgColor: 'bg-emerald-100',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Başarısız',
|
||||||
|
value: failedServices,
|
||||||
|
icon: FaTimesCircle,
|
||||||
|
color: 'text-red-600',
|
||||||
|
bgColor: 'bg-red-100',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Aktif',
|
||||||
|
value: activeServices,
|
||||||
|
icon: FaArrowUp,
|
||||||
|
color: 'text-emerald-600',
|
||||||
|
bgColor: 'bg-blue-300',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Pasif',
|
||||||
|
value: inactiveServices,
|
||||||
|
icon: FaArrowDown,
|
||||||
|
color: 'text-emerald-600',
|
||||||
|
bgColor: 'bg-green-400',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
const filteredServices = services.filter((service) => {
|
||||||
|
const matchesSearch =
|
||||||
|
service.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||||
|
(service.displayName || '').toLowerCase().includes(searchTerm.toLowerCase())
|
||||||
|
const matchesFilter = filterStatus === 'all' || service.compilationStatus === filterStatus
|
||||||
|
return matchesSearch && matchesFilter
|
||||||
|
})
|
||||||
|
|
||||||
|
const statusBadge = (status: string) => {
|
||||||
|
if (status === 'Success') return 'bg-emerald-100 text-emerald-700'
|
||||||
|
if (status === 'Failed') return 'bg-red-100 text-red-700'
|
||||||
|
return 'bg-yellow-100 text-yellow-700'
|
||||||
|
}
|
||||||
|
|
||||||
|
const openSwagger = () => {
|
||||||
|
window.open(`${import.meta.env.VITE_API_URL}/swagger/index.html`, '_blank')
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-4">
|
||||||
|
<Helmet
|
||||||
|
titleTemplate={`%s | ${APP_NAME}`}
|
||||||
|
title={translate('::' + 'App.DeveloperKit.DynamicServices')}
|
||||||
|
defaultTitle={APP_NAME}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Stats */}
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-5 gap-6 mt-2 mb-4">
|
||||||
|
{stats.map((stat, index) => (
|
||||||
|
<div key={index} className="bg-white rounded-lg border border-slate-200 p-6">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<p className="text-sm font-medium text-slate-600 mb-1">{stat.name}</p>
|
||||||
|
<p className="text-3xl font-bold text-slate-900">{stat.value}</p>
|
||||||
|
</div>
|
||||||
|
<div className={`p-3 rounded-lg ${stat.bgColor}`}>
|
||||||
|
<stat.icon className={`w-6 h-6 ${stat.color}`} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Toolbar */}
|
||||||
|
<div className="flex flex-col sm:flex-row gap-4">
|
||||||
|
<div className="flex-1 relative">
|
||||||
|
<FaSearch className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-slate-400" />
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="Servis ara..."
|
||||||
|
value={searchTerm}
|
||||||
|
onChange={(e) => setSearchTerm(e.target.value)}
|
||||||
|
className="w-full pl-10 pr-4 py-2 border border-slate-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<FaFilter className="w-4 h-4 text-slate-500" />
|
||||||
|
<select
|
||||||
|
value={filterStatus}
|
||||||
|
onChange={(e) =>
|
||||||
|
setFilterStatus(e.target.value as 'all' | 'Success' | 'Failed' | 'Pending')
|
||||||
|
}
|
||||||
|
className="px-3 py-2 border border-slate-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
||||||
|
>
|
||||||
|
<option value="all">Tümü</option>
|
||||||
|
<option value="Success">Başarılı</option>
|
||||||
|
<option value="Failed">Başarısız</option>
|
||||||
|
<option value="Pending">Bekliyor</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Link
|
||||||
|
to={ROUTES_ENUM.protected.saas.developerKit.dynamicServicesNew}
|
||||||
|
className="flex items-center gap-2 bg-blue-600 text-white px-4 py-2 rounded-lg hover:bg-blue-700 transition-colors"
|
||||||
|
>
|
||||||
|
<FaPlus className="w-4 h-4" />
|
||||||
|
Yeni Servis
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<button
|
||||||
|
onClick={openSwagger}
|
||||||
|
className="flex items-center gap-2 bg-green-600 text-white px-4 py-2 rounded-lg hover:bg-green-700 transition-colors"
|
||||||
|
>
|
||||||
|
<FaExternalLinkAlt className="w-3 h-3" />
|
||||||
|
Swagger
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* List */}
|
||||||
|
{isLoading ? (
|
||||||
|
<div className="flex items-center justify-center h-40">
|
||||||
|
<FaSpinner className="w-8 h-8 animate-spin text-slate-400" />
|
||||||
|
</div>
|
||||||
|
) : filteredServices.length > 0 ? (
|
||||||
|
<div className="grid grid-cols-1 lg:grid-cols-2 xl:grid-cols-3 gap-6">
|
||||||
|
{filteredServices.map((service) => (
|
||||||
|
<div
|
||||||
|
key={service.id}
|
||||||
|
className="bg-white rounded-lg border border-slate-200 shadow-sm hover:shadow-md transition-shadow"
|
||||||
|
>
|
||||||
|
<div className="p-6">
|
||||||
|
<div className="flex items-start justify-between">
|
||||||
|
<div className="flex-1">
|
||||||
|
<div className="flex items-center gap-2 mb-1">
|
||||||
|
<h3 className="text-base font-semibold text-slate-900">{service.name}</h3>
|
||||||
|
<div
|
||||||
|
className={`w-2 h-2 rounded-full ${
|
||||||
|
service.compilationStatus === 'Success'
|
||||||
|
? 'bg-emerald-500'
|
||||||
|
: 'bg-slate-300'
|
||||||
|
}`}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{service.displayName && (
|
||||||
|
<p className="text-slate-500 text-sm mb-1">{service.displayName}</p>
|
||||||
|
)}
|
||||||
|
<span
|
||||||
|
className={`inline-block px-2 py-0.5 text-xs rounded-full font-medium mb-3 ${statusBadge(service.compilationStatus)}`}
|
||||||
|
>
|
||||||
|
{service.compilationStatus} · v{service.version}
|
||||||
|
</span>
|
||||||
|
{service.description && (
|
||||||
|
<p className="text-slate-500 text-sm">{service.description}</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{service.lastSuccessfulCompilation && (
|
||||||
|
<div className="flex items-center gap-1 text-xs text-slate-400 ml-4 whitespace-nowrap">
|
||||||
|
<FaCalendarAlt className="w-3 h-3" />
|
||||||
|
<span>
|
||||||
|
{new Date(service.lastSuccessfulCompilation).toLocaleDateString()}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Actions */}
|
||||||
|
<div className="flex items-center justify-end pt-3 border-t border-slate-100 gap-1 mt-4">
|
||||||
|
<Link
|
||||||
|
to={ROUTES_ENUM.protected.saas.developerKit.dynamicServicesEdit.replace(
|
||||||
|
':id',
|
||||||
|
service.id,
|
||||||
|
)}
|
||||||
|
className="p-2 text-slate-500 hover:text-blue-600 hover:bg-blue-50 rounded transition-colors"
|
||||||
|
title="Düzenle"
|
||||||
|
>
|
||||||
|
<FaRegEdit className="w-4 h-4" />
|
||||||
|
</Link>
|
||||||
|
<button
|
||||||
|
onClick={() => deleteService(service.id)}
|
||||||
|
className="p-2 text-slate-500 hover:text-red-600 hover:bg-red-50 rounded transition-colors"
|
||||||
|
title="Sil"
|
||||||
|
>
|
||||||
|
<FaTrashAlt className="w-4 h-4" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="text-center py-12">
|
||||||
|
<div className="bg-slate-100 rounded-full w-16 h-16 flex items-center justify-center mx-auto mb-4">
|
||||||
|
<FaCode className="w-8 h-8 text-slate-400" />
|
||||||
|
</div>
|
||||||
|
<h3 className="text-lg font-medium text-slate-900 mb-2">
|
||||||
|
{searchTerm || filterStatus !== 'all' ? 'Sonuç bulunamadı' : 'Henüz servis yok'}
|
||||||
|
</h3>
|
||||||
|
<p className="text-slate-500 mb-6">
|
||||||
|
{searchTerm || filterStatus !== 'all'
|
||||||
|
? 'Filtre veya arama kriterlerini değiştirmeyi deneyin.'
|
||||||
|
: 'İlk dinamik servisinizi oluşturmak için başlayın.'}
|
||||||
|
</p>
|
||||||
|
{!searchTerm && filterStatus === 'all' && (
|
||||||
|
<Link
|
||||||
|
to={ROUTES_ENUM.protected.saas.developerKit.dynamicServicesNew}
|
||||||
|
className="inline-flex items-center gap-2 bg-blue-600 text-white px-4 py-2 rounded-lg hover:bg-blue-700 transition-colors"
|
||||||
|
>
|
||||||
|
<FaPlus className="w-4 h-4" />
|
||||||
|
Yeni Servis Oluştur
|
||||||
|
</Link>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default DynamicServiceManager
|
||||||
Loading…
Reference in a new issue