sozsoft-platform/ui/src/views/developerKit/DynamicServiceEditor.tsx
2026-03-18 08:36:36 +03:00

410 lines
16 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import React, { useState, useEffect } from 'react'
import { useParams, useNavigate, Link } from 'react-router-dom'
import { Editor } from '@monaco-editor/react'
import {
FaPlay,
FaCopy,
FaCheckCircle,
FaExclamationCircle,
FaSpinner,
FaExternalLinkAlt,
FaArrowLeft,
FaCog,
FaCode,
FaSave,
} from 'react-icons/fa'
import { useLocalization } from '@/utils/hooks/useLocalization'
import {
dynamicServiceService,
type CompileResult,
type PublishResult,
postTestCompile,
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 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
{
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"
});
}
}
}`
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)
const editorOptions = {
fontSize: 14,
lineNumbers: 'on' as const,
roundedSelection: false,
scrollBeyondLastLine: false,
automaticLayout: true,
minimap: { enabled: false },
folding: true,
wordWrap: 'on' as const,
}
useEffect(() => {
if (id) {
loadService(id)
}
}, [id])
const loadService = async (serviceId: string) => {
try {
setIsLoading(true)
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('Servis yüklenirken hata:', error)
} finally {
setIsLoading(false)
}
}
const handleTestCompile = async () => {
if (!code.trim()) {
alert(translate('::App.DeveloperKit.DynamicServices.Editor.PleaseEnterCode'))
return
}
try {
setIsCompiling(true)
setCompileResult(null)
const result = await postTestCompile({ code } as TestCompileDto)
setCompileResult(result.data)
} catch (error: any) {
setCompileResult({
success: false,
errorMessage: error.response?.data?.message || translate('::App.DeveloperKit.DynamicServices.Editor.CompileError'),
compilationTimeMs: 0,
hasWarnings: false,
errors: [],
})
} finally {
setIsCompiling(false)
}
}
const handlePublish = async () => {
setSubmitted(true)
if (!code.trim() || !serviceName.trim()) {
return
}
if (isPublishing) return
try {
setIsPublishing(true)
setPublishResult(null)
// Edit modunda: önce eskiyi sil, sonra yeniden yayınla
if (id) {
await dynamicServiceService.delete(id)
}
const result = await dynamicServiceService.publish({
name: serviceName,
code,
displayName,
description,
primaryEntityType,
isActive,
})
if (result.success) {
navigate(ROUTES_ENUM.protected.saas.developerKit.dynamicServices)
} else {
setPublishResult(result)
}
} catch (error: any) {
setPublishResult({
success: false,
errorMessage: error.response?.data?.message || translate('::App.DeveloperKit.DynamicServices.Editor.PublishError'),
})
} finally {
setIsPublishing(false)
}
}
const copyCode = () => {
navigator.clipboard.writeText(code)
alert(translate('::App.DeveloperKit.DynamicServices.Editor.CodeCopied'))
}
const pageTitle = id ? translate('::App.DeveloperKit.DynamicServices.Editor.EditTitle') : translate('::App.DeveloperKit.DynamicServices.Editor.NewTitle')
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={pageTitle} defaultTitle={APP_NAME} />
{/* Header */}
<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" />
{translate('::App.DeveloperKit.DynamicServices.Editor.BackToServices')}
</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 ? translate('::App.DeveloperKit.DynamicServices.Editor.EditSubtitle') : translate('::App.DeveloperKit.DynamicServices.Editor.NewSubtitle')}
</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" />
{translate('::App.DeveloperKit.DynamicServices.Editor.CopyCode')}
</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 ? translate('::App.DeveloperKit.DynamicServices.Editor.Compiling') : translate('::App.DeveloperKit.DynamicServices.Editor.TestCompile')}
</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 ? translate('::App.DeveloperKit.DynamicServices.Editor.Publishing') : translate('::App.DeveloperKit.DynamicServices.Editor.Publish')}
</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">
{compileResult.success ? translate('::App.DeveloperKit.DynamicServices.Editor.CompileSuccess') : translate('::App.DeveloperKit.DynamicServices.Editor.CompileFailed')}
</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}] {translate('::App.DeveloperKit.DynamicServices.Editor.Line')} {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">{translate('::App.DeveloperKit.DynamicServices.Editor.PublishFailed')}</span>
{publishResult.errorMessage && (
<p className="text-xs mt-0.5">{publishResult.errorMessage}</p>
)}
</div>
</div>
)}
{/* Two-panel layout */}
<div className="flex flex-col xl:flex-row gap-4 items-stretch xl:items-start">
{/* LEFT PANEL — Servis Ayarları */}
<div className="w-full xl: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">{translate('::App.DeveloperKit.DynamicServices.Editor.ServiceSettings')}</h2>
</div>
{/* Servis Adı */}
<div>
<label className="block text-sm font-medium text-slate-700 mb-1">{translate('::App.DeveloperKit.DynamicServices.Editor.ServiceName')}</label>
<input
type="text"
value={serviceName}
onChange={(e) => {
setServiceName(e.target.value)
setSubmitted(false)
}}
placeholder={translate('::App.DeveloperKit.DynamicServices.Editor.ServiceNamePlaceholder')}
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">{translate('::App.DeveloperKit.DynamicServices.Editor.ServiceNameRequired')}</p>}
</div>
{/* Görünen Ad */}
<div>
<label className="block text-sm font-medium text-slate-700 mb-1">{translate('::App.DeveloperKit.DynamicServices.Editor.DisplayName')}</label>
<input
type="text"
value={displayName}
onChange={(e) => setDisplayName(e.target.value)}
placeholder={translate('::App.DeveloperKit.DynamicServices.Editor.DisplayNamePlaceholder')}
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">{translate('::App.DeveloperKit.DynamicServices.Editor.Description')}</label>
<input
type="text"
value={description}
onChange={(e) => setDescription(e.target.value)}
placeholder={translate('::App.DeveloperKit.DynamicServices.Editor.DescriptionPlaceholder')}
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">{translate('::App.DeveloperKit.DynamicServices.Editor.PrimaryEntityType')}</label>
<input
type="text"
value={primaryEntityType}
onChange={(e) => setPrimaryEntityType(e.target.value)}
placeholder={translate('::App.DeveloperKit.DynamicServices.Editor.PrimaryEntityTypePlaceholder')}
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">{translate('::App.DeveloperKit.DynamicServices.Editor.IsActive')}</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="w-full flex-1 min-w-0 space-y-4">
{/* Monaco Editor */}
<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">{translate('::App.DeveloperKit.DynamicServices.Editor.CodeEditor')}</h3>
</div>
<div className="flex items-center gap-2 text-xs text-slate-500">
<span>{translate('::App.DeveloperKit.DynamicServices.Editor.LineCount')} {code.split('\n').length}</span>
<span className="text-slate-300">|</span>
<span>{translate('::App.DeveloperKit.DynamicServices.Editor.CharCount')} {code.length}</span>
</div>
</div>
<div style={{ height: '560px' }}>
<Editor
defaultLanguage="csharp"
value={code}
onChange={(value) => setCode(value || '')}
options={editorOptions}
theme="vs-dark"
/>
</div>
</div>
</div>
</div>
</div>
)
}
export default DynamicServiceEditor