DynamicService düzenlemesi

This commit is contained in:
Sedat ÖZTÜRK 2026-03-02 10:36:38 +03:00
parent 5cc5000f16
commit 735261cb8a
16 changed files with 773 additions and 506 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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)
{
try
{ {
requests.Add(_pendingRegistrations.Dequeue()); 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; }
}
} }
} }

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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> <div className="flex items-center justify-between px-4 py-3">
<h1 className="text-2xl font-bold text-slate-900"> {/* Left: back + icon + title */}
{translate('::App.DeveloperKit.DynamicServices')} <div className="flex items-center gap-3">
</h1> <Link
<p className="text-slate-600"> to={ROUTES_ENUM.protected.saas.developerKit.dynamicServices}
{translate('::App.DeveloperKit.DynamicServices.Description')} className="flex items-center gap-2 text-slate-600 text-black px-4 py-2 rounded-lg hover:text-slate-700 transition-colors"
</p> >
</div> <FaArrowLeft className="w-3.5 h-3.5" />
<div className="flex items-center gap-3"> Servislere Dön
<button </Link>
onClick={openSwagger} <div className="h-6 w-px bg-slate-300"></div>
className="flex items-center gap-2 bg-blue-600 text-white px-4 py-2 rounded-lg hover:bg-blue-700 transition-colors" <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" />
<FaExternalLinkAlt className="w-4 h-4" /> </div>
Swagger <div>
</button> <h1 className="font-semibold text-slate-800 text-sm leading-tight">{pageTitle}</h1>
<button <p className="text-xs text-slate-500 leading-tight">
onClick={() => setShowServiceList(!showServiceList)} {id ? 'Mevcut servisi düzenleyin' : 'Yeni bir dynamic servis oluşturun'}
className="flex items-center gap-2 bg-gray-600 text-white px-4 py-2 rounded-lg hover:bg-gray-700 transition-colors" </p>
> </div>
<FaCode className="w-4 h-4" /> </div>
{showServiceList ? 'Listeyi Gizle' : 'Listeyi Göster'}
</button> {/* Right: action buttons + swagger + publish */}
<div className="flex items-center gap-2">
<button
onClick={copyCode}
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"
>
<FaCopy className="w-3.5 h-3.5" />
Kodu Kopyala
</button>
<button
onClick={handleTestCompile}
disabled={isCompiling || !code.trim()}
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"
>
{isCompiling ? (
<FaSpinner className="w-3.5 h-3.5 animate-spin" />
) : (
<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>
</div> </div>
<div className="grid grid-cols-12 gap-6"> {/* Compile / Publish result banners */}
{/* Service List */} {compileResult && (
{showServiceList && ( <div
<div className="col-span-12 lg:col-span-4"> className={`flex items-start gap-3 rounded-lg border px-4 py-3 text-sm ${
<div className="bg-white rounded-lg shadow-sm border"> compileResult.success
<div className="p-4 border-b"> ? 'bg-emerald-50 border-emerald-200 text-emerald-800'
<div className="flex items-center justify-between"> : 'bg-red-50 border-red-200 text-red-800'
<h3 className="text-lg font-semibold">Mevcut Servisler</h3> }`}
<div className="flex gap-2"> >
<button {compileResult.success ? (
onClick={newService} <FaCheckCircle className="w-4 h-4 mt-0.5 shrink-0 text-emerald-600" />
className="bg-green-600 text-white px-3 py-1 rounded text-sm hover:bg-green-700" ) : (
> <FaExclamationCircle className="w-4 h-4 mt-0.5 shrink-0 text-red-600" />
<FaCode className="w-4 h-4 inline mr-1" />
Yeni
</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
onClick={(e) => {
e.stopPropagation()
deleteService(service.id)
}}
className="text-red-600 hover:text-red-800 p-1"
>
<FaTrash className="w-4 h-4" />
</button>
</div>
</div>
))
) : (
<div className="p-4 text-center text-gray-500">Henüz servis yok</div>
)}
</div>
</div>
</div>
)}
{/* Main Editor */}
<div className={`col-span-12 ${showServiceList ? 'lg:col-span-8' : ''}`}>
{/* Service Info Form */}
<div className="bg-white rounded-lg shadow-sm border p-4 mb-4">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Servis Adı *</label>
<input
type="text"
value={serviceName}
onChange={(e) => setServiceName(e.target.value)}
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"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Görünen Ad</label>
<input
type="text"
value={displayName}
onChange={(e) => setDisplayName(e.target.value)}
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"
/>
</div>
<div className="md:col-span-2">
<label className="block text-sm font-medium text-gray-700 mb-1">ıklama</label>
<textarea
value={description}
onChange={(e) => setDescription(e.target.value)}
placeholder="Bu servisin ne yaptığınııklayın..."
rows={2}
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>
<label className="block text-sm font-medium text-gray-700 mb-1">
Ana Entity Türü
</label>
<input
type="text"
value={primaryEntityType}
onChange={(e) => setPrimaryEntityType(e.target.value)}
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"
/>
</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>
)} )}
<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>
<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>
)}
{/* Two-panel layout */}
<div className="flex gap-4 items-start">
{/* LEFT PANEL — Servis Ayarları */}
<div className="w-1/4 shrink-0 bg-white rounded-lg border border-slate-200 p-5 space-y-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>
<label className="block text-sm font-medium text-slate-700 mb-1">Servis Adı</label>
<input
type="text"
value={serviceName}
onChange={(e) => {
setServiceName(e.target.value)
setSubmitted(false)
}}
placeholder="ör: DynamicCustomerAppService"
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>
{/* Görünen Ad */}
<div>
<label className="block text-sm font-medium text-slate-700 mb-1">Görünen Ad</label>
<input
type="text"
value={displayName}
onChange={(e) => setDisplayName(e.target.value)}
placeholder="ör: Müşteri Yönetimi"
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>
{/* Açıklama */}
<div>
<label className="block text-sm font-medium text-slate-700 mb-1">ıklama</label>
<input
type="text"
value={description}
onChange={(e) => setDescription(e.target.value)}
placeholder="Bu servisin kısa açıklaması"
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>
{/* Ana Entity Türü */}
<div>
<label className="block text-sm font-medium text-slate-700 mb-1">Ana Entity Türü</label>
<input
type="text"
value={primaryEntityType}
onChange={(e) => setPrimaryEntityType(e.target.value)}
placeholder="ör: Customer"
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>
{/* 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

View 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