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)]
[JsonPropertyName("primaryEntityType")]
public string PrimaryEntityType { get; set; }
[JsonPropertyName("isActive")]
public bool IsActive { get; set; } = true;
}
/// <summary>

View file

@ -1,6 +1,5 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Sozsoft.Platform.DeveloperKit;
using Sozsoft.Platform.Entities;
@ -8,9 +7,11 @@ using Microsoft.EntityFrameworkCore;
using Volo.Abp.Application.Dtos;
using Volo.Abp.Application.Services;
using Volo.Abp.Domain.Repositories;
using Microsoft.AspNetCore.Authorization;
namespace Platform.Api.Application;
[Authorize]
public class CrudEndpointGenerateAppService : CrudAppService<
CrudEndpoint,
CrudEndpointDto,

View file

@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization;
using Sozsoft.Platform.DeveloperKit;
using Sozsoft.Platform.Entities;
using Volo.Abp.Application.Dtos;
@ -9,6 +10,7 @@ using Volo.Abp.Domain.Repositories;
namespace Platform.Api.Application;
[Authorize]
public class CustomComponentAppService : CrudAppService<
CustomComponent,
CustomComponentDto,

View file

@ -70,6 +70,7 @@ public class DynamicAppServiceAppService : PlatformAppService, IDynamicServiceAp
existingService.DisplayName = request.DisplayName;
existingService.Description = request.Description;
existingService.PrimaryEntityType = request.PrimaryEntityType;
existingService.IsActive = request.IsActive;
appService = await _dynamicAppServiceRepository.UpdateAsync(existingService);
}
@ -84,13 +85,34 @@ public class DynamicAppServiceAppService : PlatformAppService, IDynamicServiceAp
DisplayName = request.DisplayName,
Description = request.Description,
PrimaryEntityType = request.PrimaryEntityType,
ControllerName = GenerateControllerName(request.Name)
ControllerName = GenerateControllerName(request.Name),
IsActive = request.IsActive
};
appService = await _dynamicAppServiceRepository.InsertAsync(appService);
}
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(
CurrentTenant.Id ?? Guid.Empty,
request.Code,
@ -177,8 +199,10 @@ public class DynamicAppServiceAppService : PlatformAppService, IDynamicServiceAp
{
var appService = await _dynamicAppServiceRepository.GetAsync(id);
// TODO: Runtime'dan assembly'yi kaldırma işlemi
// (AssemblyLoadContext.Unload() çağrısı)
// Runtime'dan assembly ve Swagger endpoint'ini kaldır
DynamicServiceCompiler.NotifyAssemblyUnregistration?.Invoke(
CurrentTenant.Id ?? Guid.Empty,
appService.Name);
await _dynamicAppServiceRepository.DeleteAsync(id);
}
@ -189,6 +213,30 @@ public class DynamicAppServiceAppService : PlatformAppService, IDynamicServiceAp
var appService = await _dynamicAppServiceRepository.GetAsync(id);
appService.IsActive = isActive;
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)]

View file

@ -28,6 +28,20 @@ public class DynamicServiceCompiler : ITransientDependency
// Assembly kaydı için delegate
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
private static readonly string[] ForbiddenNamespaces = {
"System.IO",

View file

@ -9,9 +9,11 @@ using System;
using System.Threading.Tasks;
using System.Collections.Generic;
using System.Linq;
using Microsoft.AspNetCore.Authorization;
namespace Platform.Api.Application;
[Authorize]
public class SqlTableAppService : CrudAppService<
SqlTable,
SqlTableDto,
@ -59,7 +61,7 @@ public class SqlTableAppService : CrudAppService<
.FirstOrDefaultAsync(x => x.Id == id);
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);
}
@ -83,7 +85,7 @@ public class SqlTableAppService : CrudAppService<
.FirstOrDefaultAsync(x => x.Id == id);
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;
await _repository.UpdateAsync(entity, autoSave: true);

View file

@ -297,6 +297,20 @@
{
"key": "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",
"routeType": "protected",
"authority": ["App.DeveloperKit.DynamicServices"]

View file

@ -27,6 +27,8 @@ namespace Sozsoft.Platform.DynamicServices
// Bekleyen assembly kayıt istekleri
private static readonly Queue<AssemblyRegistrationRequest> _pendingRegistrations = new();
// Bekleyen assembly silme istekleri
private static readonly Queue<AssemblyUnregistrationRequest> _pendingUnregistrations = new();
private static readonly object _lock = new();
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)
{
await Task.Delay(3000, stoppingToken);
@ -132,20 +150,35 @@ namespace Sozsoft.Platform.DynamicServices
private async Task ProcessPendingRegistrations()
{
var requests = new List<AssemblyRegistrationRequest>();
var registrations = new List<AssemblyRegistrationRequest>();
var unregistrations = new List<AssemblyUnregistrationRequest>();
lock (_lock)
{
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;
foreach (var request in requests)
foreach (var request in registrations)
{
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)
{
var lastUnderscoreIndex = request.AssemblyName.LastIndexOf('_');
@ -281,5 +352,12 @@ namespace Sozsoft.Platform.DynamicServices
public string AssemblyName { 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>
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)
{
if (!_registeredTypes.Contains(type))
{
_registeredTypes.Add(type);
}
_registeredTypes.TryAdd(type, 0);
}
public static bool IsRegistered(Type type)
{
return _registeredTypes.Contains(type);
return _registeredTypes.ContainsKey(type);
}
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);
};
// Setup delegate for dynamic service unregistration
DynamicServiceCompiler.NotifyAssemblyUnregistration = (tenantId, serviceName) =>
{
DynamicAssemblyRegistrationService.RequestAssemblyUnregistration(tenantId, serviceName);
};
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();

View file

@ -154,6 +154,7 @@ public class Program
// Dynamic Assembly Registration Delegate Setup
DynamicServiceCompiler.NotifyAssemblyRegistration = DynamicAssemblyRegistrationService.RequestAssemblyRegistration;
DynamicServiceCompiler.NotifyAssemblyUnregistration = DynamicAssemblyRegistrationService.RequestAssemblyUnregistration;
await app.InitializeApplicationAsync();
await app.RunAsync();
return 0;

View file

@ -43,6 +43,8 @@ export const ROUTES_ENUM = {
componentsView: '/admin/developerkit/components/view/:id',
componentsEdit: '/admin/developerkit/components/edit/:id',
dynamicServices: '/admin/developerkit/dynamic-services',
dynamicServicesNew: '/admin/developerkit/dynamic-services/new',
dynamicServicesEdit: '/admin/developerkit/dynamic-services/edit/:id',
},
reports: {
generator: '/admin/reports/generator',

View file

@ -54,6 +54,7 @@ export interface PublishDto {
displayName?: string
description?: string
primaryEntityType?: string
isActive?: boolean
}
export interface DynamicAppServiceListResult {

View file

@ -1,5 +1,5 @@
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 {
FaRegSave,
@ -211,26 +211,25 @@ export default ${pascalCaseName}Component;`
<div className="px-4 py-3">
<div className="flex items-center justify-between">
<div className="flex items-center gap-4">
<button
type="button"
onClick={() => navigate(ROUTES_ENUM.protected.saas.developerKit.components)}
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"
<Link
to={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"
>
<FaArrowLeft className="w-4 h-4" />
<FaArrowLeft className="w-3.5 h-3.5" />
{translate('::App.DeveloperKit.ComponentEditor.Back')}
</button>
</Link>
<div className="h-6 w-px bg-slate-300"></div>
<div className="flex items-center gap-3">
<div className="bg-gradient-to-r from-blue-500 to-purple-600 p-2 rounded-lg">
<FaCode className="w-5 h-5 text-white" />
</div>
<div>
<h1 className="text-xl font-bold text-slate-900">
<h1 className="font-semibold text-slate-800 text-sm leading-tight">
{isEditing
? `${translate('::App.DeveloperKit.ComponentEditor.Title.Edit')} - ${values.name || initialValues.name || 'Component'}`
: translate('::App.DeveloperKit.ComponentEditor.Title.Create')}
</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'}
</p>
</div>
@ -243,7 +242,7 @@ export default ${pascalCaseName}Component;`
type="button"
onClick={submitForm}
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" />
{isSubmitting
@ -255,7 +254,7 @@ export default ${pascalCaseName}Component;`
</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 */}
<div className="space-y-3 col-span-1">
<div className="bg-white rounded-lg shadow-sm border border-slate-200 p-3">
@ -325,7 +324,7 @@ export default ${pascalCaseName}Component;`
component={Input}
placeholder="React component code goes here"
textArea={true}
rows={5}
rows={10}
onChange={(e: React.ChangeEvent<HTMLTextAreaElement>) => {
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 {
FaPlay,
FaUpload,
FaCode,
FaCopy,
FaCheckCircle,
FaExclamationCircle,
FaSpinner,
FaCopy,
FaExternalLinkAlt,
FaTrash,
FaSync,
FaArrowLeft,
FaCog,
FaCode,
FaSave,
} from 'react-icons/fa'
import { useLocalization } from '@/utils/hooks/useLocalization'
import {
dynamicServiceService,
type CompileResult,
type PublishResult,
type DynamicServiceDto,
postTestCompile,
TestCompileDto,
type TestCompileDto,
} from '@/services/dynamicService.service'
import { Helmet } from 'react-helmet'
import { APP_NAME } from '@/constants/app.constant'
import { ROUTES_ENUM } from '@/routes/route.constant'
const DynamicAppServiceEditor: React.FC = () => {
// 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;
const defaultTemplate = `using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Volo.Abp.Application.Services;
@ -59,14 +37,6 @@ namespace DynamicServices
[Authorize]
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()
{
return await Task.FromResult("Hello World from Dynamic AppService!");
@ -81,22 +51,29 @@ namespace DynamicServices
"Item 3"
});
}
// Repository kullanım örneği:
// public virtual async Task<List<Customer>> GetCustomersAsync()
// {
// return await _customerRepository.GetListAsync();
// }
}
}`
// Component mount
useEffect(() => {
setCode(defaultTemplate)
loadServices()
}, [])
const DynamicServiceEditor: React.FC = () => {
const { id } = useParams<{ id: string }>()
const navigate = useNavigate()
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 = {
fontSize: 14,
lineNumbers: 'on' as const,
@ -106,39 +83,42 @@ namespace DynamicServices
minimap: { enabled: false },
folding: true,
wordWrap: 'on' as const,
theme: 'vs-dark',
}
// Servisleri yükle
const loadServices = async () => {
useEffect(() => {
if (id) {
loadService(id)
}
}, [id])
const loadService = async (serviceId: string) => {
try {
setIsLoading(true)
const response = await dynamicServiceService.getList()
setServices(response.items || [])
const data = await dynamicServiceService.getById(serviceId)
setCode(data.code)
setServiceName(data.name)
setDisplayName(data.displayName || '')
setDescription(data.description || '')
setPrimaryEntityType(data.primaryEntityType || '')
setIsActive(data.isActive ?? true)
} catch (error) {
console.error('Servisler yüklenirken hata:', error)
console.error('Servis yüklenirken hata:', error)
} finally {
setIsLoading(false)
}
}
// Test compile
const handleTestCompile = async () => {
if (!code.trim()) {
alert('Lütfen kod girin')
return
}
try {
setIsCompiling(true)
setCompileResult(null)
console.log('Test compile code:', code)
const input = { code: code } as TestCompileDto
const result = await postTestCompile(input)
const result = await postTestCompile({ code } as TestCompileDto)
setCompileResult(result.data)
} catch (error: any) {
console.error('Test compile error:', error)
console.error('Error response:', error.response?.data)
setCompileResult({
success: false,
errorMessage: error.response?.data?.message || 'Derleme sırasında hata oluştu',
@ -151,34 +131,36 @@ namespace DynamicServices
}
}
// Publish
const handlePublish = async () => {
setSubmitted(true)
if (!code.trim() || !serviceName.trim()) {
alert('Lütfen kod ve servis adını girin')
return
}
if (isPublishing) return
try {
setIsPublishing(true)
setPublishResult(null)
const requestData = {
name: serviceName,
code: code,
displayName: displayName,
description: description,
primaryEntityType: primaryEntityType,
// Edit modunda: önce eskiyi sil, sonra yeniden yayınla
if (id) {
await dynamicServiceService.delete(id)
}
const result = await dynamicServiceService.publish(requestData)
setPublishResult(result)
const result = await dynamicServiceService.publish({
name: serviceName,
code,
displayName,
description,
primaryEntityType,
isActive,
})
if (result.success) {
await loadServices() // Listeyi yenile
navigate(ROUTES_ENUM.protected.saas.developerKit.dynamicServices)
} else {
setPublishResult(result)
}
} catch (error: any) {
console.error('Publish error:', error)
console.error('Error response:', error.response?.data)
setPublishResult({
success: false,
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 = () => {
navigator.clipboard.writeText(code)
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 (
<div className="space-y-4">
<Helmet
titleTemplate={`%s | ${APP_NAME}`}
title={translate('::' + 'App.DeveloperKit.DynamicServices')}
defaultTitle={APP_NAME}
></Helmet>
<Helmet titleTemplate={`%s | ${APP_NAME}`} title={pageTitle} defaultTitle={APP_NAME} />
{/* 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>
<h1 className="text-2xl font-bold text-slate-900">
{translate('::App.DeveloperKit.DynamicServices')}
</h1>
<p className="text-slate-600">
{translate('::App.DeveloperKit.DynamicServices.Description')}
<h1 className="font-semibold text-slate-800 text-sm leading-tight">{pageTitle}</h1>
<p className="text-xs text-slate-500 leading-tight">
{id ? 'Mevcut servisi düzenleyin' : 'Yeni bir dynamic servis oluşturun'}
</p>
</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 className="grid grid-cols-12 gap-6">
{/* Service List */}
{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">
{/* Right: action buttons + swagger + publish */}
<div className="flex items-center gap-2">
<button
onClick={newService}
className="bg-green-600 text-white px-3 py-1 rounded text-sm hover:bg-green-700"
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"
>
<FaCode className="w-4 h-4 inline mr-1" />
Yeni
<FaCopy className="w-3.5 h-3.5" />
Kodu Kopyala
</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"
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"
>
<FaTrash className="w-4 h-4" />
</button>
</div>
</div>
))
{isCompiling ? (
<FaSpinner className="w-3.5 h-3.5 animate-spin" />
) : (
<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>
<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>
)}
{/* 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">
{/* 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-gray-700 mb-1">Servis Adı *</label>
<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)}
onChange={(e) => {
setServiceName(e.target.value)
setSubmitted(false)
}}
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>
{/* Görünen Ad */}
<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
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"
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 className="md:col-span-2">
<label className="block text-sm font-medium text-gray-700 mb-1">ıklama</label>
<textarea
{/* 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 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"
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-gray-700 mb-1">
Ana Entity Türü
</label>
<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-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>
{/* 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 */}
<div className="bg-white rounded-lg shadow-sm border overflow-hidden">
<div className="p-3 bg-gray-50 border-b flex items-center justify-between">
<h3 className="font-medium text-gray-700">C# Code Editor</h3>
<div className="flex items-center gap-2 text-sm text-gray-600">
<span>Lines: {code.split('\n').length}</span>
<span>|</span>
<span>Characters: {code.length}</span>
<div className="bg-white rounded-lg border border-slate-200 overflow-hidden">
<div className="px-5 py-3 bg-slate-50 border-b border-slate-200 flex items-center justify-between">
<div className="flex items-center gap-2">
<FaCode className="w-4 h-4 text-slate-500" />
<h3 className="font-medium text-slate-700 text-sm">C# Kod Editörü</h3>
</div>
<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 style={{ height: '600px' }}>
<div style={{ height: '560px' }}>
<Editor
defaultLanguage="csharp"
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