DynamicService düzenlemesi
This commit is contained in:
parent
5cc5000f16
commit
735261cb8a
16 changed files with 773 additions and 506 deletions
|
|
@ -127,6 +127,9 @@ public class PublishAppServiceRequestDto
|
|||
[StringLength(256)]
|
||||
[JsonPropertyName("primaryEntityType")]
|
||||
public string PrimaryEntityType { get; set; }
|
||||
|
||||
[JsonPropertyName("isActive")]
|
||||
public bool IsActive { get; set; } = true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
@ -176,9 +198,11 @@ public class DynamicAppServiceAppService : PlatformAppService, IDynamicServiceAp
|
|||
public virtual async Task DeleteAsync(Guid id)
|
||||
{
|
||||
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)]
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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"]
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
{
|
||||
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;
|
||||
|
||||
foreach (var request in requests)
|
||||
foreach (var request in registrations)
|
||||
{
|
||||
try
|
||||
{
|
||||
|
|
@ -153,12 +186,50 @@ namespace Sozsoft.Platform.DynamicServices
|
|||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Assembly kaydı başarısız. Tenant: {TenantId}, Assembly: {Assembly}",
|
||||
_logger.LogError(ex, "Assembly kaydı başarısız. Tenant: {TenantId}, Assembly: {Assembly}",
|
||||
request.TenantId, request.AssemblyName);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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; }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 _);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -54,6 +54,7 @@ export interface PublishDto {
|
|||
displayName?: string
|
||||
description?: string
|
||||
primaryEntityType?: string
|
||||
isActive?: boolean
|
||||
}
|
||||
|
||||
export interface DynamicAppServiceListResult {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
<h1 className="text-2xl font-bold text-slate-900">
|
||||
{translate('::App.DeveloperKit.DynamicServices')}
|
||||
</h1>
|
||||
<p className="text-slate-600">
|
||||
{translate('::App.DeveloperKit.DynamicServices.Description')}
|
||||
</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 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="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>
|
||||
|
||||
{/* 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 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">
|
||||
<button
|
||||
onClick={newService}
|
||||
className="bg-green-600 text-white px-3 py-1 rounded text-sm hover:bg-green-700"
|
||||
>
|
||||
<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">Açıklama</label>
|
||||
<textarea
|
||||
value={description}
|
||||
onChange={(e) => setDescription(e.target.value)}
|
||||
placeholder="Bu servisin ne yaptığını açı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>
|
||||
{/* 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>
|
||||
)}
|
||||
|
||||
{/* 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">Açı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 */}
|
||||
<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
|
||||
|
|
|
|||
291
ui/src/views/developerKit/DynamicServiceManager.tsx
Normal file
291
ui/src/views/developerKit/DynamicServiceManager.tsx
Normal file
|
|
@ -0,0 +1,291 @@
|
|||
import React, { useState, useEffect } from 'react'
|
||||
import { Link } from 'react-router-dom'
|
||||
import {
|
||||
FaPlus,
|
||||
FaSearch,
|
||||
FaRegEdit,
|
||||
FaTrashAlt,
|
||||
FaCheckCircle,
|
||||
FaTimesCircle,
|
||||
FaFilter,
|
||||
FaCalendarAlt,
|
||||
FaCode,
|
||||
FaSpinner,
|
||||
FaExternalLinkAlt,
|
||||
FaArrowDown,
|
||||
FaArrowUp,
|
||||
} from 'react-icons/fa'
|
||||
import { useLocalization } from '@/utils/hooks/useLocalization'
|
||||
import { dynamicServiceService, type DynamicServiceDto } from '@/services/dynamicService.service'
|
||||
import { Helmet } from 'react-helmet'
|
||||
import { APP_NAME } from '@/constants/app.constant'
|
||||
import { ROUTES_ENUM } from '@/routes/route.constant'
|
||||
|
||||
const DynamicServiceManager: React.FC = () => {
|
||||
const { translate } = useLocalization()
|
||||
const [services, setServices] = useState<DynamicServiceDto[]>([])
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
const [searchTerm, setSearchTerm] = useState('')
|
||||
const [filterStatus, setFilterStatus] = useState<'all' | 'Success' | 'Failed' | 'Pending'>('all')
|
||||
|
||||
useEffect(() => {
|
||||
loadServices()
|
||||
}, [])
|
||||
|
||||
const loadServices = async () => {
|
||||
try {
|
||||
setIsLoading(true)
|
||||
const response = await dynamicServiceService.getList()
|
||||
setServices(response.items || [])
|
||||
} catch (error) {
|
||||
console.error('Servisler yüklenirken hata:', error)
|
||||
} finally {
|
||||
setIsLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const deleteService = async (serviceId: string) => {
|
||||
if (!confirm('Bu servisi silmek istediğinizden emin misiniz?')) return
|
||||
try {
|
||||
await dynamicServiceService.delete(serviceId)
|
||||
await loadServices()
|
||||
} catch (error) {
|
||||
console.error('Servis silinirken hata:', error)
|
||||
alert('Servis silinirken hata oluştu')
|
||||
}
|
||||
}
|
||||
|
||||
const totalServices = services.length
|
||||
const successServices = services.filter((s) => s.compilationStatus === 'Success').length
|
||||
const failedServices = services.filter((s) => s.compilationStatus === 'Failed').length
|
||||
const activeServices = services.filter((s) => s.isActive).length
|
||||
const inactiveServices = services.filter((s) => !s.isActive).length
|
||||
|
||||
const stats = [
|
||||
{
|
||||
name: 'Toplam',
|
||||
value: totalServices,
|
||||
icon: FaCode,
|
||||
color: 'text-purple-600',
|
||||
bgColor: 'bg-purple-100',
|
||||
},
|
||||
{
|
||||
name: 'Başarılı',
|
||||
value: successServices,
|
||||
icon: FaCheckCircle,
|
||||
color: 'text-emerald-600',
|
||||
bgColor: 'bg-emerald-100',
|
||||
},
|
||||
{
|
||||
name: 'Başarısız',
|
||||
value: failedServices,
|
||||
icon: FaTimesCircle,
|
||||
color: 'text-red-600',
|
||||
bgColor: 'bg-red-100',
|
||||
},
|
||||
{
|
||||
name: 'Aktif',
|
||||
value: activeServices,
|
||||
icon: FaArrowUp,
|
||||
color: 'text-emerald-600',
|
||||
bgColor: 'bg-blue-300',
|
||||
},
|
||||
{
|
||||
name: 'Pasif',
|
||||
value: inactiveServices,
|
||||
icon: FaArrowDown,
|
||||
color: 'text-emerald-600',
|
||||
bgColor: 'bg-green-400',
|
||||
},
|
||||
]
|
||||
|
||||
const filteredServices = services.filter((service) => {
|
||||
const matchesSearch =
|
||||
service.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||
(service.displayName || '').toLowerCase().includes(searchTerm.toLowerCase())
|
||||
const matchesFilter = filterStatus === 'all' || service.compilationStatus === filterStatus
|
||||
return matchesSearch && matchesFilter
|
||||
})
|
||||
|
||||
const statusBadge = (status: string) => {
|
||||
if (status === 'Success') return 'bg-emerald-100 text-emerald-700'
|
||||
if (status === 'Failed') return 'bg-red-100 text-red-700'
|
||||
return 'bg-yellow-100 text-yellow-700'
|
||||
}
|
||||
|
||||
const openSwagger = () => {
|
||||
window.open(`${import.meta.env.VITE_API_URL}/swagger/index.html`, '_blank')
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<Helmet
|
||||
titleTemplate={`%s | ${APP_NAME}`}
|
||||
title={translate('::' + 'App.DeveloperKit.DynamicServices')}
|
||||
defaultTitle={APP_NAME}
|
||||
/>
|
||||
|
||||
{/* Stats */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-5 gap-6 mt-2 mb-4">
|
||||
{stats.map((stat, index) => (
|
||||
<div key={index} className="bg-white rounded-lg border border-slate-200 p-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm font-medium text-slate-600 mb-1">{stat.name}</p>
|
||||
<p className="text-3xl font-bold text-slate-900">{stat.value}</p>
|
||||
</div>
|
||||
<div className={`p-3 rounded-lg ${stat.bgColor}`}>
|
||||
<stat.icon className={`w-6 h-6 ${stat.color}`} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Toolbar */}
|
||||
<div className="flex flex-col sm:flex-row gap-4">
|
||||
<div className="flex-1 relative">
|
||||
<FaSearch className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-slate-400" />
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Servis ara..."
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
className="w-full pl-10 pr-4 py-2 border border-slate-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<FaFilter className="w-4 h-4 text-slate-500" />
|
||||
<select
|
||||
value={filterStatus}
|
||||
onChange={(e) =>
|
||||
setFilterStatus(e.target.value as 'all' | 'Success' | 'Failed' | 'Pending')
|
||||
}
|
||||
className="px-3 py-2 border border-slate-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
||||
>
|
||||
<option value="all">Tümü</option>
|
||||
<option value="Success">Başarılı</option>
|
||||
<option value="Failed">Başarısız</option>
|
||||
<option value="Pending">Bekliyor</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<Link
|
||||
to={ROUTES_ENUM.protected.saas.developerKit.dynamicServicesNew}
|
||||
className="flex items-center gap-2 bg-blue-600 text-white px-4 py-2 rounded-lg hover:bg-blue-700 transition-colors"
|
||||
>
|
||||
<FaPlus className="w-4 h-4" />
|
||||
Yeni Servis
|
||||
</Link>
|
||||
</div>
|
||||
<div>
|
||||
<button
|
||||
onClick={openSwagger}
|
||||
className="flex items-center gap-2 bg-green-600 text-white px-4 py-2 rounded-lg hover:bg-green-700 transition-colors"
|
||||
>
|
||||
<FaExternalLinkAlt className="w-3 h-3" />
|
||||
Swagger
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* List */}
|
||||
{isLoading ? (
|
||||
<div className="flex items-center justify-center h-40">
|
||||
<FaSpinner className="w-8 h-8 animate-spin text-slate-400" />
|
||||
</div>
|
||||
) : filteredServices.length > 0 ? (
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 xl:grid-cols-3 gap-6">
|
||||
{filteredServices.map((service) => (
|
||||
<div
|
||||
key={service.id}
|
||||
className="bg-white rounded-lg border border-slate-200 shadow-sm hover:shadow-md transition-shadow"
|
||||
>
|
||||
<div className="p-6">
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<h3 className="text-base font-semibold text-slate-900">{service.name}</h3>
|
||||
<div
|
||||
className={`w-2 h-2 rounded-full ${
|
||||
service.compilationStatus === 'Success'
|
||||
? 'bg-emerald-500'
|
||||
: 'bg-slate-300'
|
||||
}`}
|
||||
/>
|
||||
</div>
|
||||
{service.displayName && (
|
||||
<p className="text-slate-500 text-sm mb-1">{service.displayName}</p>
|
||||
)}
|
||||
<span
|
||||
className={`inline-block px-2 py-0.5 text-xs rounded-full font-medium mb-3 ${statusBadge(service.compilationStatus)}`}
|
||||
>
|
||||
{service.compilationStatus} · v{service.version}
|
||||
</span>
|
||||
{service.description && (
|
||||
<p className="text-slate-500 text-sm">{service.description}</p>
|
||||
)}
|
||||
</div>
|
||||
{service.lastSuccessfulCompilation && (
|
||||
<div className="flex items-center gap-1 text-xs text-slate-400 ml-4 whitespace-nowrap">
|
||||
<FaCalendarAlt className="w-3 h-3" />
|
||||
<span>
|
||||
{new Date(service.lastSuccessfulCompilation).toLocaleDateString()}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Actions */}
|
||||
<div className="flex items-center justify-end pt-3 border-t border-slate-100 gap-1 mt-4">
|
||||
<Link
|
||||
to={ROUTES_ENUM.protected.saas.developerKit.dynamicServicesEdit.replace(
|
||||
':id',
|
||||
service.id,
|
||||
)}
|
||||
className="p-2 text-slate-500 hover:text-blue-600 hover:bg-blue-50 rounded transition-colors"
|
||||
title="Düzenle"
|
||||
>
|
||||
<FaRegEdit className="w-4 h-4" />
|
||||
</Link>
|
||||
<button
|
||||
onClick={() => deleteService(service.id)}
|
||||
className="p-2 text-slate-500 hover:text-red-600 hover:bg-red-50 rounded transition-colors"
|
||||
title="Sil"
|
||||
>
|
||||
<FaTrashAlt className="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-center py-12">
|
||||
<div className="bg-slate-100 rounded-full w-16 h-16 flex items-center justify-center mx-auto mb-4">
|
||||
<FaCode className="w-8 h-8 text-slate-400" />
|
||||
</div>
|
||||
<h3 className="text-lg font-medium text-slate-900 mb-2">
|
||||
{searchTerm || filterStatus !== 'all' ? 'Sonuç bulunamadı' : 'Henüz servis yok'}
|
||||
</h3>
|
||||
<p className="text-slate-500 mb-6">
|
||||
{searchTerm || filterStatus !== 'all'
|
||||
? 'Filtre veya arama kriterlerini değiştirmeyi deneyin.'
|
||||
: 'İlk dinamik servisinizi oluşturmak için başlayın.'}
|
||||
</p>
|
||||
{!searchTerm && filterStatus === 'all' && (
|
||||
<Link
|
||||
to={ROUTES_ENUM.protected.saas.developerKit.dynamicServicesNew}
|
||||
className="inline-flex items-center gap-2 bg-blue-600 text-white px-4 py-2 rounded-lg hover:bg-blue-700 transition-colors"
|
||||
>
|
||||
<FaPlus className="w-4 h-4" />
|
||||
Yeni Servis Oluştur
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default DynamicServiceManager
|
||||
Loading…
Reference in a new issue