291 lines
12 KiB
TypeScript
291 lines
12 KiB
TypeScript
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(translate('::App.DeveloperKit.DynamicServices.DeleteConfirm'))) return
|
|
try {
|
|
await dynamicServiceService.delete(serviceId)
|
|
await loadServices()
|
|
} catch (error) {
|
|
console.error('Servis silinirken hata:', error)
|
|
alert(translate('::App.DeveloperKit.DynamicServices.DeleteError'))
|
|
}
|
|
}
|
|
|
|
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: translate('::App.DeveloperKit.DynamicServices.Total'),
|
|
value: totalServices,
|
|
icon: FaCode,
|
|
color: 'text-purple-600',
|
|
bgColor: 'bg-purple-100',
|
|
},
|
|
{
|
|
name: translate('::App.DeveloperKit.DynamicServices.Successful'),
|
|
value: successServices,
|
|
icon: FaCheckCircle,
|
|
color: 'text-emerald-600',
|
|
bgColor: 'bg-emerald-100',
|
|
},
|
|
{
|
|
name: translate('::App.DeveloperKit.DynamicServices.Failed'),
|
|
value: failedServices,
|
|
icon: FaTimesCircle,
|
|
color: 'text-red-600',
|
|
bgColor: 'bg-red-100',
|
|
},
|
|
{
|
|
name: translate('::App.DeveloperKit.DynamicServices.Active'),
|
|
value: activeServices,
|
|
icon: FaArrowUp,
|
|
color: 'text-emerald-600',
|
|
bgColor: 'bg-blue-300',
|
|
},
|
|
{
|
|
name: translate('::App.DeveloperKit.DynamicServices.Passive'),
|
|
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 sm:grid-cols-2 xl:grid-cols-5 gap-4 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 lg:flex-row lg:items-center gap-3">
|
|
<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={translate('::App.DeveloperKit.DynamicServices.SearchPlaceholder')}
|
|
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 w-full lg:w-auto">
|
|
<FaFilter className="w-4 h-4 text-slate-500" />
|
|
<select
|
|
value={filterStatus}
|
|
onChange={(e) =>
|
|
setFilterStatus(e.target.value as 'all' | 'Success' | 'Failed' | 'Pending')
|
|
}
|
|
className="w-full lg:w-auto px-3 py-2 border border-slate-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
|
>
|
|
<option value="all">{translate('::App.DeveloperKit.DynamicServices.FilterAll')}</option>
|
|
<option value="Success">{translate('::App.DeveloperKit.DynamicServices.Successful')}</option>
|
|
<option value="Failed">{translate('::App.DeveloperKit.DynamicServices.Failed')}</option>
|
|
<option value="Pending">{translate('::App.DeveloperKit.DynamicServices.FilterPending')}</option>
|
|
</select>
|
|
</div>
|
|
<div className="w-full sm:w-auto">
|
|
<Link
|
|
to={ROUTES_ENUM.protected.saas.developerKit.dynamicServicesNew}
|
|
className="w-full sm:w-auto justify-center 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" />
|
|
{translate('::App.DeveloperKit.DynamicServices.NewService')}
|
|
</Link>
|
|
</div>
|
|
<div className="w-full sm:w-auto">
|
|
<button
|
|
onClick={openSwagger}
|
|
className="w-full sm:w-auto justify-center 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 flex-col gap-3 sm:flex-row sm:items-start sm: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 sm: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={translate('::App.DeveloperKit.DynamicServices.EditTooltip')}
|
|
>
|
|
<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={translate('::App.DeveloperKit.DynamicServices.DeleteTooltip')}
|
|
>
|
|
<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' ? translate('::App.DeveloperKit.DynamicServices.NoResults') : translate('::App.DeveloperKit.DynamicServices.NoServicesYet')}
|
|
</h3>
|
|
<p className="text-slate-500 mb-6">
|
|
{searchTerm || filterStatus !== 'all'
|
|
? translate('::App.DeveloperKit.DynamicServices.TryChangingFilter')
|
|
: translate('::App.DeveloperKit.DynamicServices.GetStarted')}
|
|
</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" />
|
|
{translate('::App.DeveloperKit.DynamicServices.CreateNewService')}
|
|
</Link>
|
|
)}
|
|
</div>
|
|
)}
|
|
</div>
|
|
)
|
|
}
|
|
|
|
export default DynamicServiceManager
|