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

411 lines
16 KiB
TypeScript
Raw Normal View History

2026-03-02 07:36:38 +00:00
import React, { useState, useEffect } from 'react'
import { useParams, useNavigate, Link } from 'react-router-dom'
2026-02-24 20:44:16 +00:00
import { Editor } from '@monaco-editor/react'
import {
FaPlay,
2026-03-02 07:36:38 +00:00
FaCopy,
2026-02-24 20:44:16 +00:00
FaCheckCircle,
FaExclamationCircle,
FaSpinner,
FaExternalLinkAlt,
2026-03-02 07:36:38 +00:00
FaArrowLeft,
FaCog,
FaCode,
FaSave,
2026-02-24 20:44:16 +00:00
} from 'react-icons/fa'
import { useLocalization } from '@/utils/hooks/useLocalization'
import {
dynamicServiceService,
type CompileResult,
type PublishResult,
postTestCompile,
2026-03-02 07:36:38 +00:00
type TestCompileDto,
2026-02-24 20:44:16 +00:00
} 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-03-02 07:36:38 +00:00
import { ROUTES_ENUM } from '@/routes/route.constant'
2026-02-24 20:44:16 +00:00
2026-03-02 07:36:38 +00:00
const defaultTemplate = `using System;
2026-02-24 20:44:16 +00:00
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"
});
}
}
}`
2026-03-02 07:36:38 +00:00
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)
2026-02-24 20:44:16 +00:00
const editorOptions = {
fontSize: 14,
lineNumbers: 'on' as const,
roundedSelection: false,
scrollBeyondLastLine: false,
automaticLayout: true,
minimap: { enabled: false },
folding: true,
wordWrap: 'on' as const,
}
2026-03-02 07:36:38 +00:00
useEffect(() => {
if (id) {
loadService(id)
}
}, [id])
const loadService = async (serviceId: string) => {
2026-02-24 20:44:16 +00:00
try {
setIsLoading(true)
2026-03-02 07:36:38 +00:00
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)
2026-02-24 20:44:16 +00:00
} catch (error) {
2026-03-02 07:36:38 +00:00
console.error('Servis yüklenirken hata:', error)
2026-02-24 20:44:16 +00:00
} finally {
setIsLoading(false)
}
}
const handleTestCompile = async () => {
if (!code.trim()) {
alert(translate('::App.DeveloperKit.DynamicServices.Editor.PleaseEnterCode'))
2026-02-24 20:44:16 +00:00
return
}
try {
setIsCompiling(true)
setCompileResult(null)
2026-03-02 07:36:38 +00:00
const result = await postTestCompile({ code } as TestCompileDto)
2026-02-24 20:44:16 +00:00
setCompileResult(result.data)
} catch (error: any) {
setCompileResult({
success: false,
errorMessage: error.response?.data?.message || translate('::App.DeveloperKit.DynamicServices.Editor.CompileError'),
2026-02-24 20:44:16 +00:00
compilationTimeMs: 0,
hasWarnings: false,
errors: [],
})
} finally {
setIsCompiling(false)
}
}
const handlePublish = async () => {
2026-03-02 07:36:38 +00:00
setSubmitted(true)
2026-02-24 20:44:16 +00:00
if (!code.trim() || !serviceName.trim()) {
return
}
2026-03-02 07:36:38 +00:00
if (isPublishing) return
2026-02-24 20:44:16 +00:00
try {
setIsPublishing(true)
setPublishResult(null)
2026-03-02 07:36:38 +00:00
// Edit modunda: önce eskiyi sil, sonra yeniden yayınla
if (id) {
await dynamicServiceService.delete(id)
2026-02-24 20:44:16 +00:00
}
2026-03-02 07:36:38 +00:00
const result = await dynamicServiceService.publish({
name: serviceName,
code,
displayName,
description,
primaryEntityType,
isActive,
})
2026-02-24 20:44:16 +00:00
if (result.success) {
2026-03-02 07:36:38 +00:00
navigate(ROUTES_ENUM.protected.saas.developerKit.dynamicServices)
} else {
setPublishResult(result)
2026-02-24 20:44:16 +00:00
}
} catch (error: any) {
setPublishResult({
success: false,
errorMessage: error.response?.data?.message || translate('::App.DeveloperKit.DynamicServices.Editor.PublishError'),
2026-02-24 20:44:16 +00:00
})
} finally {
setIsPublishing(false)
}
}
2026-03-02 07:36:38 +00:00
const copyCode = () => {
navigator.clipboard.writeText(code)
alert(translate('::App.DeveloperKit.DynamicServices.Editor.CodeCopied'))
2026-02-24 20:44:16 +00:00
}
const pageTitle = id ? translate('::App.DeveloperKit.DynamicServices.Editor.EditTitle') : translate('::App.DeveloperKit.DynamicServices.Editor.NewTitle')
2026-02-24 20:44:16 +00:00
2026-03-02 07:36:38 +00:00
if (isLoading) {
return (
<div className="flex items-center justify-center h-64">
<FaSpinner className="w-8 h-8 animate-spin text-slate-400" />
</div>
)
2026-02-24 20:44:16 +00:00
}
2026-03-02 07:36:38 +00:00
const serviceNameError = submitted && !serviceName.trim()
2026-02-24 20:44:16 +00:00
return (
<div className="space-y-4">
2026-03-02 07:36:38 +00:00
<Helmet titleTemplate={`%s | ${APP_NAME}`} title={pageTitle} defaultTitle={APP_NAME} />
2026-03-01 20:43:25 +00:00
2026-02-24 20:44:16 +00:00
{/* Header */}
2026-03-02 07:36:38 +00:00
<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')}
2026-03-02 07:36:38 +00:00
</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')}
2026-03-02 07:36:38 +00:00
</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')}
2026-03-02 07:36:38 +00:00
</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')}
2026-03-02 07:36:38 +00:00
</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')}
2026-03-02 07:36:38 +00:00
</button>
</div>
2026-02-24 20:44:16 +00:00
</div>
</div>
2026-03-02 07:36:38 +00:00
{/* 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')}
2026-03-02 07:36:38 +00:00
</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}
2026-03-02 07:36:38 +00:00
</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>
2026-03-02 07:36:38 +00:00
{publishResult.errorMessage && (
<p className="text-xs mt-0.5">{publishResult.errorMessage}</p>
)}
</div>
</div>
)}
{/* Two-panel layout */}
2026-03-18 05:36:36 +00:00
<div className="flex flex-col xl:flex-row gap-4 items-stretch xl:items-start">
2026-03-02 07:36:38 +00:00
{/* LEFT PANEL — Servis Ayarları */}
2026-03-18 05:36:36 +00:00
<div className="w-full xl:w-1/4 shrink-0 bg-white rounded-lg border border-slate-200 p-5 space-y-4">
2026-03-02 07:36:38 +00:00
{/* 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>
2026-03-02 07:36:38 +00:00
</div>
2026-02-24 20:44:16 +00:00
2026-03-02 07:36:38 +00:00
{/* Servis Adı */}
<div>
<label className="block text-sm font-medium text-slate-700 mb-1">{translate('::App.DeveloperKit.DynamicServices.Editor.ServiceName')}</label>
2026-03-02 07:36:38 +00:00
<input
type="text"
value={serviceName}
onChange={(e) => {
setServiceName(e.target.value)
setSubmitted(false)
}}
placeholder={translate('::App.DeveloperKit.DynamicServices.Editor.ServiceNamePlaceholder')}
2026-03-02 07:36:38 +00:00
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>}
2026-02-24 20:44:16 +00:00
</div>
2026-03-02 07:36:38 +00:00
{/* Görünen Ad */}
<div>
<label className="block text-sm font-medium text-slate-700 mb-1">{translate('::App.DeveloperKit.DynamicServices.Editor.DisplayName')}</label>
2026-03-02 07:36:38 +00:00
<input
type="text"
value={displayName}
onChange={(e) => setDisplayName(e.target.value)}
placeholder={translate('::App.DeveloperKit.DynamicServices.Editor.DisplayNamePlaceholder')}
2026-03-02 07:36:38 +00:00
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"
/>
2026-02-24 20:44:16 +00:00
</div>
2026-03-02 07:36:38 +00:00
{/* Açıklama */}
<div>
<label className="block text-sm font-medium text-slate-700 mb-1">{translate('::App.DeveloperKit.DynamicServices.Editor.Description')}</label>
2026-03-02 07:36:38 +00:00
<input
type="text"
value={description}
onChange={(e) => setDescription(e.target.value)}
placeholder={translate('::App.DeveloperKit.DynamicServices.Editor.DescriptionPlaceholder')}
2026-03-02 07:36:38 +00:00
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"
/>
2026-02-24 20:44:16 +00:00
</div>
2026-03-02 07:36:38 +00:00
{/* Ana Entity Türü */}
<div>
<label className="block text-sm font-medium text-slate-700 mb-1">{translate('::App.DeveloperKit.DynamicServices.Editor.PrimaryEntityType')}</label>
2026-03-02 07:36:38 +00:00
<input
type="text"
value={primaryEntityType}
onChange={(e) => setPrimaryEntityType(e.target.value)}
placeholder={translate('::App.DeveloperKit.DynamicServices.Editor.PrimaryEntityTypePlaceholder')}
2026-03-02 07:36:38 +00:00
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>
2026-02-24 20:44:16 +00:00
2026-03-02 07:36:38 +00:00
{/* Aktif */}
<div>
<label className="block text-sm font-medium text-slate-700 mb-1">{translate('::App.DeveloperKit.DynamicServices.Editor.IsActive')}</label>
2026-03-02 07:36:38 +00:00
<input
type="checkbox"
checked={isActive}
onChange={(e) => setIsActive(e.target.checked)}
className="w-4 h-4 rounded accent-blue-600 cursor-pointer"
/>
</div>
</div>
2026-02-24 20:44:16 +00:00
2026-03-02 07:36:38 +00:00
{/* RIGHT PANEL — Önizleme + Editor */}
2026-03-18 05:36:36 +00:00
<div className="w-full flex-1 min-w-0 space-y-4">
2026-02-24 20:44:16 +00:00
{/* Monaco Editor */}
2026-03-02 07:36:38 +00:00
<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>
2026-03-02 07:36:38 +00:00
</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>
2026-03-02 07:36:38 +00:00
<span className="text-slate-300">|</span>
<span>{translate('::App.DeveloperKit.DynamicServices.Editor.CharCount')} {code.length}</span>
2026-02-24 20:44:16 +00:00
</div>
</div>
2026-03-02 07:36:38 +00:00
<div style={{ height: '560px' }}>
2026-02-24 20:44:16 +00:00
<Editor
defaultLanguage="csharp"
value={code}
onChange={(value) => setCode(value || '')}
options={editorOptions}
theme="vs-dark"
/>
</div>
</div>
</div>
</div>
</div>
)
}
2026-03-02 07:36:38 +00:00
export default DynamicServiceEditor