535 lines
21 KiB
TypeScript
535 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 {
|
|
Database,
|
|
Zap,
|
|
Server,
|
|
Puzzle,
|
|
Activity,
|
|
Code,
|
|
CheckCircle,
|
|
ArrowRight,
|
|
AlertCircle,
|
|
Wifi,
|
|
WifiOff,
|
|
} from "lucide-react";
|
|
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: Database,
|
|
color: "text-blue-600",
|
|
bgColor: "bg-blue-100",
|
|
href: ROUTES_ENUM.protected.saas.developerKitEntities,
|
|
},
|
|
{
|
|
name: translate('::App.DeveloperKit.Dashboard.Stats.Migrations'),
|
|
value: migrations.filter((m) => m.status === "pending").length,
|
|
total: migrations.length,
|
|
icon: Zap,
|
|
color: "text-yellow-600",
|
|
bgColor: "bg-yellow-100",
|
|
href: ROUTES_ENUM.protected.saas.developerKitMigrations,
|
|
},
|
|
{
|
|
name: translate('::App.DeveloperKit.Dashboard.Stats.APIs'),
|
|
value: generatedEndpoints.filter((e) => e.isActive).length,
|
|
total: generatedEndpoints.length,
|
|
icon: Server,
|
|
color: "text-emerald-600",
|
|
bgColor: "bg-emerald-100",
|
|
href: ROUTES_ENUM.protected.saas.developerKitEndpoints,
|
|
},
|
|
{
|
|
name: translate('::App.DeveloperKit.Dashboard.Stats.Components'),
|
|
value: components?.filter((c) => c.isActive).length,
|
|
total: components?.length,
|
|
icon: Puzzle,
|
|
color: "text-purple-600",
|
|
bgColor: "bg-purple-100",
|
|
href: ROUTES_ENUM.protected.saas.developerKitComponents,
|
|
},
|
|
];
|
|
|
|
const developmentFlow = [
|
|
{
|
|
step: 1,
|
|
title: translate('::App.DeveloperKit.Dashboard.Flow.CreateEntity'),
|
|
description: translate('::App.DeveloperKit.Dashboard.Flow.CreateEntity.Desc'),
|
|
icon: Database,
|
|
color: "bg-blue-600",
|
|
href: ROUTES_ENUM.protected.saas.developerKitEntitiesNew,
|
|
status: "ready",
|
|
},
|
|
{
|
|
step: 2,
|
|
title: translate('::App.DeveloperKit.Dashboard.Flow.GenerateMigration'),
|
|
description: translate('::App.DeveloperKit.Dashboard.Flow.GenerateMigration.Desc'),
|
|
icon: Zap,
|
|
color: "bg-yellow-600",
|
|
href: ROUTES_ENUM.protected.saas.developerKitMigrations,
|
|
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: CheckCircle,
|
|
color: "bg-green-600",
|
|
href: ROUTES_ENUM.protected.saas.developerKitMigrations,
|
|
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: Server,
|
|
color: "bg-emerald-600",
|
|
href: ROUTES_ENUM.protected.saas.developerKitEndpoints,
|
|
status: "ready",
|
|
},
|
|
{
|
|
step: 5,
|
|
title: translate('::App.DeveloperKit.Dashboard.Flow.BuildComponent'),
|
|
description: translate('::App.DeveloperKit.Dashboard.Flow.BuildComponent.Desc'),
|
|
icon: Puzzle,
|
|
color: "bg-purple-600",
|
|
href: ROUTES_ENUM.protected.saas.developerKitComponentsNew,
|
|
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: Code },
|
|
{
|
|
name: translate('::App.DeveloperKit.Dashboard.SystemHealth.Backend'),
|
|
status: isOnline ? translate('::App.DeveloperKit.Dashboard.SystemHealth.Healthy') : translate('::App.DeveloperKit.Dashboard.SystemHealth.Offline'),
|
|
icon: Server,
|
|
},
|
|
{
|
|
name: translate('::App.DeveloperKit.Dashboard.SystemHealth.Database'),
|
|
status: isOnline ? translate('::App.DeveloperKit.Dashboard.SystemHealth.Healthy') : translate('::App.DeveloperKit.Dashboard.SystemHealth.Unknown'),
|
|
icon: Database,
|
|
},
|
|
{
|
|
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: Zap,
|
|
},
|
|
];
|
|
|
|
return (
|
|
<div className="space-y-8">
|
|
{/* Header */}
|
|
<div className="flex items-center justify-between">
|
|
<div>
|
|
<h1 className="text-3xl font-bold text-slate-900 mb-2">
|
|
{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 ? (
|
|
<>
|
|
<Wifi className="w-4 h-4" />
|
|
{translate('::App.DeveloperKit.Dashboard.SystemHealth.Online')}
|
|
</>
|
|
) : (
|
|
<>
|
|
<WifiOff 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-6">
|
|
{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-6 hover:shadow-md transition-all duration-200 group"
|
|
>
|
|
<div className="flex items-center justify-between mb-4">
|
|
<div
|
|
className={`${stat.bgColor} ${stat.color} p-3 rounded-lg group-hover:scale-110 transition-transform`}
|
|
>
|
|
<Icon className="w-6 h-6" />
|
|
</div>
|
|
<ArrowRight 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-8">
|
|
<div className="flex items-center gap-2 mb-6">
|
|
<Activity 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-6">
|
|
{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-6 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-sm 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">
|
|
<AlertCircle 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">
|
|
<ArrowRight className="w-6 h-6 text-slate-300" />
|
|
</div>
|
|
)}
|
|
</Link>
|
|
);
|
|
})}
|
|
</div>
|
|
</div>
|
|
|
|
<div className="grid grid-cols-1 lg:grid-cols-4 gap-8">
|
|
{/* System Health */}
|
|
<div className="bg-white rounded-xl shadow-sm border border-slate-200 p-6">
|
|
<div className="flex items-center gap-2 mb-4">
|
|
<Activity 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.developerKitEntities}
|
|
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.developerKitEntitiesEdit.replace(':id', entity.id)}
|
|
className="text-blue-600 hover:text-blue-700"
|
|
>
|
|
<Database className="w-4 h-4" />
|
|
</Link>
|
|
</div>
|
|
</div>
|
|
))
|
|
) : (
|
|
<div className="text-center py-8">
|
|
<Database 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.developerKitEntitiesNew}
|
|
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.developerKitMigrations}
|
|
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.developerKitMigrations}
|
|
className="text-yellow-600 hover:text-yellow-700"
|
|
>
|
|
<Zap className="w-4 h-4" />
|
|
</Link>
|
|
</div>
|
|
</div>
|
|
))
|
|
) : (
|
|
<div className="text-center py-8">
|
|
<Zap 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.developerKitEntitiesNew}
|
|
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.developerKitEndpoints}
|
|
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"
|
|
>
|
|
<Server className="w-4 h-4" />
|
|
</Link>
|
|
</div>
|
|
</div>
|
|
))
|
|
) : (
|
|
<div className="text-center py-8">
|
|
<Server 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.developerKitEndpointsNew}
|
|
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;
|