sozsoft-platform/ui/src/views/developerKit/DynamicServiceEditor.tsx

618 lines
22 KiB
TypeScript
Raw Normal View History

2026-02-24 20:44:16 +00:00
import React, { useState, useRef, useEffect } from 'react'
import { Editor } from '@monaco-editor/react'
import {
FaPlay,
FaUpload,
FaCode,
FaCheckCircle,
FaExclamationCircle,
FaSpinner,
FaCopy,
FaExternalLinkAlt,
FaTrash,
FaSync,
} from 'react-icons/fa'
import { useLocalization } from '@/utils/hooks/useLocalization'
import {
dynamicServiceService,
type CompileResult,
type PublishResult,
type DynamicServiceDto,
postTestCompile,
TestCompileDto,
} from '@/services/dynamicService.service'
2026-03-01 20:43:25 +00:00
import { Helmet } from 'react-helmet'
import { APP_NAME } from '@/constants/app.constant'
2026-02-24 20:44:16 +00:00
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;
using System.Collections.Generic;
using System.Threading.Tasks;
using Volo.Abp.Application.Services;
using Volo.Abp.Domain.Repositories;
using Microsoft.AspNetCore.Authorization;
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!");
}
public virtual async Task<List<string>> GetSampleDataAsync()
{
return await Task.FromResult(new List<string>
{
"Item 1",
"Item 2",
"Item 3"
});
}
// Repository kullanım örneği:
// public virtual async Task<List<Customer>> GetCustomersAsync()
// {
// return await _customerRepository.GetListAsync();
// }
}
}`
// Component mount
useEffect(() => {
setCode(defaultTemplate)
loadServices()
}, [])
// Monaco Editor ayarları
const editorOptions = {
fontSize: 14,
lineNumbers: 'on' as const,
roundedSelection: false,
scrollBeyondLastLine: false,
automaticLayout: true,
minimap: { enabled: false },
folding: true,
wordWrap: 'on' as const,
theme: 'vs-dark',
}
// Servisleri yükle
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)
}
}
// 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)
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',
compilationTimeMs: 0,
hasWarnings: false,
errors: [],
})
} finally {
setIsCompiling(false)
}
}
// Publish
const handlePublish = async () => {
if (!code.trim() || !serviceName.trim()) {
alert('Lütfen kod ve servis adını girin')
return
}
try {
setIsPublishing(true)
setPublishResult(null)
const requestData = {
name: serviceName,
code: code,
displayName: displayName,
description: description,
primaryEntityType: primaryEntityType,
}
const result = await dynamicServiceService.publish(requestData)
setPublishResult(result)
if (result.success) {
await loadServices() // Listeyi yenile
}
} 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',
})
} finally {
setIsPublishing(false)
}
}
// 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ı')
}
return (
<div className="space-y-4">
2026-03-01 20:43:25 +00:00
<Helmet
titleTemplate={`%s | ${APP_NAME}`}
title={translate('::' + 'App.DeveloperKit.DynamicServices')}
defaultTitle={APP_NAME}
></Helmet>
2026-02-24 20:44:16 +00:00
{/* 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>
</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">ıklama</label>
<textarea
value={description}
onChange={(e) => setDescription(e.target.value)}
placeholder="Bu servisin ne yaptığınııklayın..."
rows={2}
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Ana Entity Türü
</label>
<input
type="text"
value={primaryEntityType}
onChange={(e) => setPrimaryEntityType(e.target.value)}
placeholder="ör: Customer"
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
/>
</div>
</div>
</div>
{/* Action Buttons */}
<div className="bg-white rounded-lg shadow-sm border p-4 mb-4">
<div className="flex flex-wrap items-center gap-3">
<button
onClick={handleTestCompile}
disabled={isCompiling || !code.trim()}
className="flex items-center gap-2 bg-orange-600 text-white px-4 py-2 rounded-lg hover:bg-orange-700 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
>
{isCompiling ? (
<FaSpinner className="w-4 h-4 animate-spin" />
) : (
<FaPlay className="w-4 h-4" />
)}
Test Compile
</button>
<button
onClick={handlePublish}
disabled={isPublishing || !code.trim() || !serviceName.trim()}
className="flex items-center gap-2 bg-green-600 text-white px-4 py-2 rounded-lg hover:bg-green-700 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
>
{isPublishing ? (
<FaSpinner className="w-4 h-4 animate-spin" />
) : (
<FaUpload className="w-4 h-4" />
)}
Publish
</button>
<button
onClick={copyCode}
className="flex items-center gap-2 bg-gray-600 text-white px-4 py-2 rounded-lg hover:bg-gray-700 transition-colors"
>
<FaCopy className="w-4 h-4" />
Kopyala
</button>
</div>
</div>
{/* Results */}
{(compileResult || publishResult) && (
<div className="space-y-4 mb-4">
{/* Compile Result */}
{compileResult && (
<div
className={`rounded-lg border p-4 ${
compileResult.success
? 'bg-green-50 border-green-200'
: 'bg-red-50 border-red-200'
}`}
>
<div className="flex items-center gap-2 mb-2">
{compileResult.success ? (
<FaCheckCircle className="w-5 h-5 text-green-600" />
) : (
<FaExclamationCircle className="w-5 h-5 text-red-600" />
)}
<h4
className={`font-medium ${
compileResult.success ? 'text-green-800' : 'text-red-800'
}`}
>
Derleme {compileResult.success ? 'Başarılı' : 'Başarısız'}
</h4>
<span className="text-sm text-gray-600">
({compileResult.compilationTimeMs}ms)
</span>
</div>
{!compileResult.success &&
compileResult.errors &&
compileResult.errors.length > 0 && (
<div className="space-y-2">
{compileResult.errors.map((error, index) => (
<div key={index} className="bg-white rounded p-3 border">
<div className="flex items-start gap-2">
<span className="text-red-600 font-mono text-sm">{error.code}</span>
<div className="flex-1">
<p className="text-sm text-gray-800">{error.message}</p>
<p className="text-xs text-gray-600 mt-1">
Satır {error.line}, Sütun {error.column}
</p>
</div>
</div>
</div>
))}
</div>
)}
{compileResult.hasWarnings && compileResult.warnings && (
<div className="mt-3">
<h5 className="text-sm font-medium text-yellow-800 mb-2">Uyarılar:</h5>
<ul className="list-disc list-inside space-y-1">
{compileResult.warnings.map((warning, index) => (
<li key={index} className="text-sm text-yellow-700">
{warning}
</li>
))}
</ul>
</div>
)}
</div>
)}
{/* Publish Result */}
{publishResult && (
<div
className={`rounded-lg border p-4 ${
publishResult.success
? 'bg-green-50 border-green-200'
: 'bg-red-50 border-red-200'
}`}
>
<div className="flex items-center gap-2 mb-2">
{publishResult.success ? (
<FaCheckCircle className="w-5 h-5 text-green-600" />
) : (
<FaExclamationCircle className="w-5 h-5 text-red-600" />
)}
<h4
className={`font-medium ${
publishResult.success ? 'text-green-800' : 'text-red-800'
}`}
>
Yayınlama {publishResult.success ? 'Başarılı' : 'Başarısız'}
</h4>
</div>
{publishResult.success && (
<div className="space-y-2">
{publishResult.controllerName && (
<p className="text-sm text-gray-600">
Controller:{' '}
<code className="bg-gray-100 px-2 py-1 rounded">
{publishResult.controllerName}
</code>
</p>
)}
{publishResult.generatedEndpoints &&
publishResult.generatedEndpoints.length > 0 && (
<div>
<h5 className="text-sm font-medium text-gray-700 mb-1">
Oluşturulan Endpoint'ler:
</h5>
<ul className="list-disc list-inside space-y-1">
{publishResult.generatedEndpoints.map((endpoint, index) => (
<li key={index} className="text-sm text-gray-600 font-mono">
{endpoint}
</li>
))}
</ul>
</div>
)}
</div>
)}
{!publishResult.success && publishResult.errorMessage && (
<p className="text-red-700 text-sm">{publishResult.errorMessage}</p>
)}
</div>
)}
</div>
)}
{/* 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>
</div>
<div style={{ height: '600px' }}>
<Editor
defaultLanguage="csharp"
value={code}
onChange={(value) => setCode(value || '')}
options={editorOptions}
theme="vs-dark"
/>
</div>
</div>
</div>
</div>
</div>
)
}
export default DynamicAppServiceEditor