529 lines
21 KiB
TypeScript
529 lines
21 KiB
TypeScript
import React from 'react'
|
|
import { Link } from 'react-router-dom'
|
|
import { useComponents } from '../../contexts/ComponentContext'
|
|
import { useEntities } from '../../contexts/EntityContext'
|
|
import { useSystemHealth } from '../../utils/hooks/useDeveloperKit'
|
|
import {
|
|
FaDatabase,
|
|
FaBolt,
|
|
FaServer,
|
|
FaPuzzlePiece,
|
|
FaCog,
|
|
FaChartLine,
|
|
FaCode,
|
|
FaCheckCircle,
|
|
FaArrowRight,
|
|
FaExclamationCircle,
|
|
FaWifi,
|
|
FaWindowClose,
|
|
} from 'react-icons/fa'
|
|
import { ROUTES_ENUM } from '@/routes/route.constant'
|
|
import { useLocalization } from '@/utils/hooks/useLocalization'
|
|
|
|
const Dashboard: React.FC = () => {
|
|
const { components } = useComponents()
|
|
const { entities, migrations, generatedEndpoints } = useEntities()
|
|
const { isOnline, lastCheck, recheckHealth } = useSystemHealth()
|
|
const { translate } = useLocalization()
|
|
|
|
const stats = [
|
|
{
|
|
name: translate('::App.DeveloperKit.Dashboard.Stats.Entities'),
|
|
value: entities.filter((e) => e.isActive).length,
|
|
total: entities.length,
|
|
icon: FaDatabase,
|
|
color: 'text-blue-600',
|
|
bgColor: 'bg-blue-100',
|
|
href: ROUTES_ENUM.protected.saas.developerKit.entities,
|
|
},
|
|
{
|
|
name: translate('::App.DeveloperKit.Dashboard.Stats.Migrations'),
|
|
value: migrations.filter((m) => m.status === 'pending').length,
|
|
total: migrations.length,
|
|
icon: FaBolt,
|
|
color: 'text-yellow-600',
|
|
bgColor: 'bg-yellow-100',
|
|
href: ROUTES_ENUM.protected.saas.developerKit.migrations,
|
|
},
|
|
{
|
|
name: translate('::App.DeveloperKit.Dashboard.Stats.APIs'),
|
|
value: generatedEndpoints.filter((e) => e.isActive).length,
|
|
total: generatedEndpoints.length,
|
|
icon: FaServer,
|
|
color: 'text-emerald-600',
|
|
bgColor: 'bg-emerald-100',
|
|
href: ROUTES_ENUM.protected.saas.developerKit.endpoints,
|
|
},
|
|
{
|
|
name: translate('::App.DeveloperKit.Components'),
|
|
value: components?.filter((c) => c.isActive).length,
|
|
total: components?.length,
|
|
icon: FaPuzzlePiece,
|
|
color: 'text-purple-600',
|
|
bgColor: 'bg-purple-100',
|
|
href: ROUTES_ENUM.protected.saas.developerKit.components,
|
|
},
|
|
]
|
|
|
|
const developmentFlow = [
|
|
{
|
|
step: 1,
|
|
title: translate('::App.DeveloperKit.Entity.CreateEntity'),
|
|
description: translate('::App.DeveloperKit.Dashboard.Flow.CreateEntity.Desc'),
|
|
icon: FaDatabase,
|
|
color: 'bg-blue-600',
|
|
href: ROUTES_ENUM.protected.saas.developerKit.entitiesNew,
|
|
status: 'ready',
|
|
},
|
|
{
|
|
step: 2,
|
|
title: translate('::App.DeveloperKit.Dashboard.Flow.GenerateMigration'),
|
|
description: translate('::App.DeveloperKit.Dashboard.Flow.GenerateMigration.Desc'),
|
|
icon: FaBolt,
|
|
color: 'bg-yellow-600',
|
|
href: ROUTES_ENUM.protected.saas.developerKit.migrations,
|
|
status: entities.some((e) => e.migrationStatus === 'pending') ? 'action-needed' : 'ready',
|
|
},
|
|
{
|
|
step: 3,
|
|
title: translate('::App.DeveloperKit.Dashboard.Flow.ApplyMigration'),
|
|
description: translate('::App.DeveloperKit.Dashboard.Flow.ApplyMigration.Desc'),
|
|
icon: FaCheckCircle,
|
|
color: 'bg-green-600',
|
|
href: ROUTES_ENUM.protected.saas.developerKit.migrations,
|
|
status: migrations.some((m) => m.status === 'pending') ? 'action-needed' : 'ready',
|
|
},
|
|
{
|
|
step: 4,
|
|
title: translate('::App.DeveloperKit.Dashboard.Flow.GenerateAPI'),
|
|
description: translate('::App.DeveloperKit.Dashboard.Flow.GenerateAPI.Desc'),
|
|
icon: FaServer,
|
|
color: 'bg-emerald-600',
|
|
href: ROUTES_ENUM.protected.saas.developerKit.endpoints,
|
|
status: 'ready',
|
|
},
|
|
{
|
|
step: 5,
|
|
title: translate('::App.DeveloperKit.Dashboard.Flow.BuildComponent'),
|
|
description: translate('::App.DeveloperKit.Dashboard.Flow.BuildComponent.Desc'),
|
|
icon: FaPuzzlePiece,
|
|
color: 'bg-purple-600',
|
|
href: ROUTES_ENUM.protected.saas.developerKit.componentsNew,
|
|
status: 'ready',
|
|
},
|
|
]
|
|
|
|
const recentEntities = entities.slice(0, 3)
|
|
const recentMigrations = migrations.slice(0, 3)
|
|
const recentEndpoints = [
|
|
...generatedEndpoints.map((e) => ({
|
|
id: e.id,
|
|
name: `${e.entityName} ${e.operationType}`,
|
|
method: e.method,
|
|
path: e.path,
|
|
description: `Generated ${e.operationType} for ${e.entityName}`,
|
|
isActive: e.isActive,
|
|
lastModificationTime: e.lastModificationTime,
|
|
creationTime: e.creationTime,
|
|
})),
|
|
].slice(0, 3)
|
|
|
|
const systemHealth = [
|
|
{
|
|
name: translate('::App.DeveloperKit.Dashboard.SystemHealth.Frontend'),
|
|
status: translate('::App.DeveloperKit.Dashboard.SystemHealth.Healthy'),
|
|
icon: FaCode,
|
|
},
|
|
{
|
|
name: translate('::App.DeveloperKit.Dashboard.SystemHealth.Backend'),
|
|
status: isOnline
|
|
? translate('::App.DeveloperKit.Dashboard.SystemHealth.Healthy')
|
|
: translate('::App.DeveloperKit.Dashboard.SystemHealth.Offline'),
|
|
icon: FaServer,
|
|
},
|
|
{
|
|
name: translate('::App.DeveloperKit.Dashboard.SystemHealth.Database'),
|
|
status: isOnline
|
|
? translate('::App.DeveloperKit.Dashboard.SystemHealth.Healthy')
|
|
: translate('::App.DeveloperKit.Dashboard.SystemHealth.Unknown'),
|
|
icon: FaDatabase,
|
|
},
|
|
{
|
|
name: translate('::App.DeveloperKit.Dashboard.SystemHealth.Migrations'),
|
|
status: migrations.some((m) => m.status === 'failed')
|
|
? translate('::App.DeveloperKit.Dashboard.SystemHealth.Warning')
|
|
: translate('::App.DeveloperKit.Dashboard.SystemHealth.Healthy'),
|
|
icon: FaBolt,
|
|
},
|
|
]
|
|
|
|
return (
|
|
<div className="space-y-4">
|
|
{/* Header */}
|
|
<div className="flex items-center justify-between">
|
|
<div>
|
|
<h1 className="text-2xl font-bold text-slate-900">
|
|
{translate('::App.DeveloperKit.Dashboard.Title')}
|
|
</h1>
|
|
<p className="text-slate-600">{translate('::App.DeveloperKit.Dashboard.Description')}</p>
|
|
</div>
|
|
<div className="flex items-center gap-3">
|
|
<button
|
|
onClick={recheckHealth}
|
|
className={`flex items-center gap-2 px-3 py-1 rounded-full text-sm font-medium transition-colors duration-200 ${
|
|
isOnline
|
|
? 'bg-green-100 text-green-700 hover:bg-green-200'
|
|
: 'bg-red-100 text-red-700 hover:bg-red-200'
|
|
}`}
|
|
title={`Son kontrol: ${lastCheck.toLocaleTimeString()}`}
|
|
>
|
|
<div
|
|
className={`w-2 h-2 rounded-full ${
|
|
isOnline ? 'bg-green-500 animate-pulse' : 'bg-red-500 animate-pulse'
|
|
}`}
|
|
/>
|
|
{isOnline ? (
|
|
<>
|
|
<FaWifi className="w-4 h-4" />
|
|
{translate('::App.DeveloperKit.Dashboard.SystemHealth.Online')}
|
|
</>
|
|
) : (
|
|
<>
|
|
<FaWindowClose className="w-4 h-4" />
|
|
{translate('::App.DeveloperKit.Dashboard.SystemHealth.OfflineStatus')}
|
|
</>
|
|
)}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Stats Grid */}
|
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
|
|
{stats.map((stat) => {
|
|
const Icon = stat.icon
|
|
return (
|
|
<Link
|
|
key={stat.name}
|
|
to={stat.href}
|
|
className="bg-white rounded-xl shadow-sm border border-slate-200 p-4 hover:shadow-md transition-all duration-200 group"
|
|
>
|
|
<div className="flex items-center justify-between">
|
|
<div
|
|
className={`${stat.bgColor} ${stat.color} p-3 rounded-lg group-hover:scale-110 transition-transform`}
|
|
>
|
|
<Icon className="w-6 h-6" />
|
|
</div>
|
|
<FaArrowRight className="w-4 h-4 text-slate-400 group-hover:text-slate-600 transition-colors" />
|
|
</div>
|
|
<div>
|
|
<p className="text-sm font-medium text-slate-600">{stat.name}</p>
|
|
<p className="text-2xl font-bold text-slate-900 mt-1">
|
|
{stat.value}
|
|
<span className="text-sm font-normal text-slate-500 ml-1">/ {stat.total}</span>
|
|
</p>
|
|
</div>
|
|
</Link>
|
|
)
|
|
})}
|
|
</div>
|
|
|
|
{/* Development Flow */}
|
|
<div className="bg-white rounded-xl shadow-sm border border-slate-200 p-4">
|
|
<div className="flex items-center gap-2 mb-4">
|
|
<FaCog className="w-6 h-6 text-blue-600" />
|
|
<h2 className="text-xl font-semibold text-slate-900">
|
|
{translate('::App.DeveloperKit.Dashboard.Flow.Title')}
|
|
</h2>
|
|
</div>
|
|
<div className="grid grid-cols-1 lg:grid-cols-5 gap-8">
|
|
{developmentFlow.map((flow, index) => {
|
|
const Icon = flow.icon
|
|
return (
|
|
<Link key={flow.step} to={flow.href} className="group relative">
|
|
<div
|
|
className={`${flow.color} text-white p-4 rounded-lg transition-all duration-200 transform group-hover:scale-105`}
|
|
>
|
|
<div className="flex items-center justify-between mb-4">
|
|
<div className="bg-white bg-opacity-20 rounded-lg p-2">
|
|
<Icon className="w-6 h-6" />
|
|
</div>
|
|
<div className="bg-white bg-opacity-20 rounded-full w-8 h-8 flex items-center justify-center">
|
|
<span className="text-sm font-bold">{flow.step}</span>
|
|
</div>
|
|
</div>
|
|
<h3 className="font-semibold text-lg mb-2 text-white">{flow.title}</h3>
|
|
<p className="text-xs opacity-90">{flow.description}</p>
|
|
|
|
{flow.status === 'action-needed' && (
|
|
<div className="absolute -top-2 -right-2">
|
|
<div className="bg-red-500 text-white rounded-full w-6 h-6 flex items-center justify-center">
|
|
<FaExclamationCircle className="w-4 h-4" />
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{index < developmentFlow.length - 1 && (
|
|
<div className="hidden lg:block absolute top-1/2 -right-3 transform -translate-y-1/2">
|
|
<FaArrowRight className="w-6 h-6 text-slate-300" />
|
|
</div>
|
|
)}
|
|
</Link>
|
|
)
|
|
})}
|
|
</div>
|
|
</div>
|
|
|
|
<div className="grid grid-cols-1 lg:grid-cols-4 gap-4">
|
|
{/* System Health */}
|
|
<div className="bg-white rounded-xl shadow-sm border border-slate-200 p-4">
|
|
<div className="flex items-center gap-2 mb-4">
|
|
<FaCog className="w-5 h-5 text-green-500" />
|
|
<h3 className="text-lg font-semibold text-slate-900">
|
|
{translate('::App.DeveloperKit.Dashboard.SystemHealth.Title')}
|
|
</h3>
|
|
</div>
|
|
<div className="space-y-3">
|
|
{systemHealth.map((system) => {
|
|
const Icon = system.icon
|
|
return (
|
|
<div
|
|
key={system.name}
|
|
className="flex items-center justify-between p-3 bg-slate-50 rounded-lg"
|
|
>
|
|
<div className="flex items-center gap-3">
|
|
<Icon className="w-4 h-4 text-slate-600" />
|
|
<span className="font-medium text-slate-900">{system.name}</span>
|
|
</div>
|
|
<div className="flex items-center gap-2">
|
|
<div
|
|
className={`w-2 h-2 rounded-full ${
|
|
system.status === 'healthy'
|
|
? 'bg-green-500'
|
|
: system.status === 'warning'
|
|
? 'bg-yellow-500'
|
|
: system.status === 'offline'
|
|
? 'bg-red-500'
|
|
: 'bg-gray-500'
|
|
}`}
|
|
/>
|
|
<span
|
|
className={`text-sm capitalize ${
|
|
system.status === 'healthy'
|
|
? 'text-green-600'
|
|
: system.status === 'warning'
|
|
? 'text-yellow-600'
|
|
: system.status === 'offline'
|
|
? 'text-red-600'
|
|
: 'text-gray-600'
|
|
}`}
|
|
>
|
|
{system.status === 'offline' ? 'Offline' : system.status}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
)
|
|
})}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Recent Entities */}
|
|
<div className="bg-white rounded-xl shadow-sm border border-slate-200 p-6">
|
|
<div className="flex items-center justify-between mb-4">
|
|
<h3 className="text-lg font-semibold text-slate-900">
|
|
{translate('::App.DeveloperKit.Dashboard.RecentEntities.Title')}
|
|
</h3>
|
|
<Link
|
|
to={ROUTES_ENUM.protected.saas.developerKit.entities}
|
|
className="text-blue-600 hover:text-blue-700 text-sm font-medium"
|
|
>
|
|
{translate('::App.DeveloperKit.Dashboard.ViewAll')}
|
|
</Link>
|
|
</div>
|
|
<div className="space-y-3">
|
|
{recentEntities.length > 0 ? (
|
|
recentEntities.map((entity) => (
|
|
<div
|
|
key={entity.id}
|
|
className="flex items-center justify-between p-3 bg-slate-50 rounded-lg hover:bg-slate-100 transition-colors"
|
|
>
|
|
<div>
|
|
<p className="font-medium text-slate-900">{entity.displayName}</p>
|
|
<p className="text-sm text-slate-500">
|
|
{entity.fields.length} fields • {entity.migrationStatus}
|
|
</p>
|
|
</div>
|
|
<div className="flex items-center gap-2">
|
|
<div
|
|
className={`w-2 h-2 rounded-full ${
|
|
entity.migrationStatus === 'applied'
|
|
? 'bg-green-500'
|
|
: entity.migrationStatus === 'pending'
|
|
? 'bg-yellow-500'
|
|
: 'bg-red-500'
|
|
}`}
|
|
/>
|
|
<Link
|
|
to={ROUTES_ENUM.protected.saas.developerKit.entitiesEdit.replace(
|
|
':id',
|
|
entity.id,
|
|
)}
|
|
className="text-blue-600 hover:text-blue-700"
|
|
>
|
|
<FaDatabase className="w-4 h-4" />
|
|
</Link>
|
|
</div>
|
|
</div>
|
|
))
|
|
) : (
|
|
<div className="text-center py-8">
|
|
<FaDatabase className="w-12 h-12 text-slate-300 mx-auto mb-2" />
|
|
<p className="text-slate-500 mb-2">
|
|
{translate('::App.DeveloperKit.Dashboard.Empty.Entity')}
|
|
</p>
|
|
<Link
|
|
to={ROUTES_ENUM.protected.saas.developerKit.entitiesNew}
|
|
className="text-blue-600 hover:text-blue-700 text-sm font-medium"
|
|
>
|
|
Create your first entity
|
|
</Link>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Recent Migrations */}
|
|
<div className="bg-white rounded-xl shadow-sm border border-slate-200 p-6">
|
|
<div className="flex items-center justify-between mb-4">
|
|
<h3 className="text-lg font-semibold text-slate-900">
|
|
{translate('::App.DeveloperKit.Dashboard.RecentMigrations.Title')}
|
|
</h3>
|
|
<Link
|
|
to={ROUTES_ENUM.protected.saas.developerKit.migrations}
|
|
className="text-yellow-600 hover:text-yellow-700 text-sm font-medium"
|
|
>
|
|
{translate('::App.DeveloperKit.Dashboard.ViewAll')}
|
|
</Link>
|
|
</div>
|
|
<div className="space-y-3">
|
|
{recentMigrations.length > 0 ? (
|
|
recentMigrations.map((migration) => (
|
|
<div
|
|
key={migration.id}
|
|
className="flex items-center justify-between p-3 bg-slate-50 rounded-lg hover:bg-slate-100 transition-colors"
|
|
>
|
|
<div>
|
|
<p className="font-medium text-slate-900">{migration.entityName}</p>
|
|
<p className="text-sm text-slate-500">
|
|
{migration.status} • {new Date(migration.creationTime).toLocaleDateString()}
|
|
</p>
|
|
</div>
|
|
<div className="flex items-center gap-2">
|
|
<div
|
|
className={`w-2 h-2 rounded-full ${
|
|
migration.status === 'applied'
|
|
? 'bg-green-500'
|
|
: migration.status === 'pending'
|
|
? 'bg-yellow-500'
|
|
: 'bg-red-500'
|
|
}`}
|
|
/>
|
|
<Link
|
|
to={ROUTES_ENUM.protected.saas.developerKit.migrations}
|
|
className="text-yellow-600 hover:text-yellow-700"
|
|
>
|
|
<FaBolt className="w-4 h-4" />
|
|
</Link>
|
|
</div>
|
|
</div>
|
|
))
|
|
) : (
|
|
<div className="text-center py-8">
|
|
<FaBolt className="w-12 h-12 text-slate-300 mx-auto mb-2" />
|
|
<p className="text-slate-500 mb-2">
|
|
{translate('::App.DeveloperKit.Dashboard.Empty.Migration')}
|
|
</p>
|
|
<Link
|
|
to={ROUTES_ENUM.protected.saas.developerKit.entitiesNew}
|
|
className="text-yellow-600 hover:text-yellow-700 text-sm font-medium"
|
|
>
|
|
{translate('::App.DeveloperKit.Dashboard.Action.GenerateMigrations')}
|
|
</Link>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Recent API Endpoints */}
|
|
<div className="bg-white rounded-xl shadow-sm border border-slate-200 p-6">
|
|
<div className="flex items-center justify-between mb-4">
|
|
<h3 className="text-lg font-semibold text-slate-900">
|
|
{translate('::App.DeveloperKit.Dashboard.RecentEndpoints.Title')}
|
|
</h3>
|
|
<Link
|
|
to={ROUTES_ENUM.protected.saas.developerKit.endpoints}
|
|
className="text-emerald-600 hover:text-emerald-700 text-sm font-medium"
|
|
>
|
|
{translate('::App.DeveloperKit.Dashboard.ViewAll')}
|
|
</Link>
|
|
</div>
|
|
<div className="space-y-3">
|
|
{recentEndpoints.length > 0 ? (
|
|
recentEndpoints.map((endpoint) => (
|
|
<div
|
|
key={endpoint.id}
|
|
className="flex items-center justify-between p-3 bg-slate-50 rounded-lg hover:bg-slate-100 transition-colors"
|
|
>
|
|
<div>
|
|
<div className="flex items-center gap-2 mb-1">
|
|
<span
|
|
className={`px-2 py-0.5 text-xs font-medium rounded ${
|
|
endpoint.method === 'GET'
|
|
? 'bg-blue-100 text-blue-800'
|
|
: endpoint.method === 'POST'
|
|
? 'bg-green-100 text-green-800'
|
|
: endpoint.method === 'PUT'
|
|
? 'bg-yellow-100 text-yellow-800'
|
|
: 'bg-red-100 text-red-800'
|
|
}`}
|
|
>
|
|
{endpoint.method}
|
|
</span>
|
|
<p className="font-medium text-slate-900 text-sm">{endpoint.name}</p>
|
|
</div>
|
|
<p className="text-xs text-slate-500">{endpoint.path}</p>
|
|
</div>
|
|
<div className="flex items-center gap-2">
|
|
<div
|
|
className={`w-2 h-2 rounded-full ${
|
|
endpoint.isActive ? 'bg-green-500' : 'bg-slate-300'
|
|
}`}
|
|
/>
|
|
<Link to="/docs" className="text-emerald-600 hover:text-emerald-700">
|
|
<FaServer className="w-4 h-4" />
|
|
</Link>
|
|
</div>
|
|
</div>
|
|
))
|
|
) : (
|
|
<div className="text-center py-8">
|
|
<FaServer className="w-12 h-12 text-slate-300 mx-auto mb-2" />
|
|
<p className="text-slate-500 mb-2">
|
|
{translate('::App.DeveloperKit.Dashboard.Empty.Endpoint')}
|
|
</p>
|
|
<Link
|
|
to={ROUTES_ENUM.protected.saas.developerKit.endpointsNew}
|
|
className="text-emerald-600 hover:text-emerald-700 text-sm font-medium"
|
|
>
|
|
{translate('::App.DeveloperKit.Dashboard.Action.CreateEntity')}
|
|
</Link>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
export default Dashboard
|