refact & cleanup : Sidebar, Button, card, EmptyState, Loader, ServiceObject, StatusBadge, UptameObject, BackupWidget, SecurityWidget, ServicesWidget, SupportCtaWidget, UptimeWidget, DashboardLayout

This commit is contained in:
LathanDevers
2026-07-29 22:50:04 +02:00
parent 438d739b1a
commit ec900efe3a
42 changed files with 3357 additions and 3966 deletions
+57
View File
@@ -0,0 +1,57 @@
import React from 'react';
import { NavLink } from 'react-router-dom';
import { cn } from '@/utils/utils';
interface SidebarProps {
onLogout: () => void;
}
const NAVIGATION_ITEMS = [
{ name: 'Trust Center', path: '/dashboard' },
{ name: 'Service Desk', path: '/support' },
{ name: 'Coffre-fort', path: '/vault' },
{ name: 'Paramètres', path: '/account' },
];
export const Sidebar: React.FC<SidebarProps> = ({ onLogout }) => {
return (
<aside className="w-64 bg-slate-900 flex flex-col shrink-0">
{/* En-tête / Logo */}
<div className="h-16 flex items-center px-6 border-b border-slate-800">
<span className="text-xl font-bold text-white tracking-widest">Aegis<span className="text-blue-500">.</span></span>
</div>
{/* Menu de navigation URL-based */}
<nav className="flex-1 px-4 py-6 space-y-2">
{NAVIGATION_ITEMS.map((item) => (
<NavLink
key={item.path}
to={item.path}
className={({ isActive }) =>
cn(
"flex items-center px-4 py-3 text-sm font-medium rounded-lg transition-colors duration-200",
isActive
? "bg-blue-900/50 text-white border border-blue-800/50"
: "text-slate-400 hover:bg-slate-800 hover:text-slate-200 border border-transparent"
)
}
>
{item.name}
</NavLink>
))}
</nav>
{/* Zone de déconnexion */}
<div className="p-4 border-t border-slate-800">
<button
onClick={onLogout}
className="w-full flex items-center justify-center px-4 py-2 text-sm font-medium text-slate-400 hover:text-red-400 hover:bg-slate-800 rounded-lg transition-all cursor-pointer"
>
Déconnexion
</button>
</div>
</aside>
);
};
export default Sidebar;
+37
View File
@@ -0,0 +1,37 @@
import React from 'react';
import { cn } from '@/utils/utils';
interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
variant?: 'primary' | 'outline' | 'ghost';
size?: 'sm' | 'md' | 'lg';
isLoading?: boolean;
}
export const Button: React.FC<ButtonProps> = ({
children, variant = 'primary', size = 'md', isLoading, className, disabled, ...props
}) => {
const baseStyles = "inline-flex items-center justify-center font-medium rounded-lg transition-colors focus:outline-none focus:ring-2 focus:ring-offset-2";
const variants = {
primary: "bg-blue-900 text-white hover:bg-blue-800 focus:ring-blue-900 shadow-sm",
outline: "border border-slate-200 text-slate-900 hover:border-blue-900 hover:bg-blue-50 focus:ring-blue-900",
ghost: "text-slate-600 hover:text-slate-900 hover:bg-slate-100 focus:ring-slate-500",
};
const sizes = {
sm: "px-3 py-1.5 text-sm",
md: "px-4 py-2 text-base",
lg: "w-full py-3 text-base",
};
return (
<button
disabled={disabled || isLoading}
className={cn(baseStyles, variants[variant], sizes[size], (disabled || isLoading) && "opacity-50 cursor-not-allowed", className)}
{...props}
>
{isLoading && <span className="mr-2 w-4 h-4 rounded-full border-2 border-current border-b-transparent animate-spin" />}
{children}
</button>
);
};
+31
View File
@@ -0,0 +1,31 @@
import React from 'react';
import { cn } from '@/utils/utils';
export const Card = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("bg-white rounded-xl shadow-sm border border-slate-200 flex flex-col overflow-hidden", className)}
{...props}
/>
)
);
Card.displayName = "Card";
export const CardHeader = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("px-6 py-5 border-b border-slate-100 flex flex-col space-y-1.5", className)}
{...props}
/>
)
);
CardHeader.displayName = "CardHeader";
export const CardContent = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
({ className, ...props }, ref) => (
<div ref={ref} className={cn("p-6 flex-1", className)} {...props} />
)
);
CardContent.displayName = "CardContent";
+18
View File
@@ -0,0 +1,18 @@
import React from 'react';
import { cn } from '@utils/utils';
interface EmptyStateProps {
message: string;
className?: string;
}
export const EmptyState: React.FC<EmptyStateProps> = ({ message, className }) => {
return (
<div className={cn(
"flex-1 flex justify-center items-center py-8 text-sm text-slate-500 italic",
className
)}>
{message}
</div>
);
};
+14
View File
@@ -0,0 +1,14 @@
import React from 'react';
import { cn } from '@utils/utils';
interface LoaderProps {
className?: string;
}
export const Loader: React.FC<LoaderProps> = ({ className }) => {
return (
<div className={cn("flex-1 flex justify-center items-center py-8", className)}>
<div className="animate-spin rounded-full h-6 w-6 border-b-2 border-slate-900"></div>
</div>
);
};
+23
View File
@@ -0,0 +1,23 @@
import React from 'react';
import type { ManagedContract } from '@/types/ManagedContract';
import { StatusBadge } from '@/components/ui/StatusBadge';
interface ServiceObjectProps {
contract: ManagedContract;
}
export const ServiceObject: React.FC<ServiceObjectProps> = ({ contract }) => {
return (
<div className="p-4 bg-slate-50 rounded-lg border border-slate-100 flex justify-between items-center">
<div>
<h3 className="text-sm font-medium text-slate-900">{contract.service_name}</h3>
{contract.description && (
<p className="text-xs text-slate-500 mt-1">{contract.description}</p>
)}
</div>
{/* Fonctionne instantanément avec 'Actif', 'En déploiement', 'Suspendu' */}
<StatusBadge status={contract.status} />
</div>
);
};
+66
View File
@@ -0,0 +1,66 @@
// Fichier : src/components/ui/StatusBadge.tsx
import React from 'react';
import { cn } from '@/utils/utils';
import type { AegisStatus } from '@/types/Status';
interface StatusBadgeProps {
status: AegisStatus;
className?: string;
}
// Configuration visuelle unique pour chaque statut
const STATUS_CONFIG: Record<AegisStatus, { bg: string; dot: string }> = {
// Statuts Infrastructure & Contrats Positifs
'Opérationnel': {
bg: 'bg-emerald-50 text-emerald-700 border-emerald-200/60',
dot: 'bg-emerald-500',
},
'Actif': {
bg: 'bg-emerald-50 text-emerald-700 border-emerald-200/60',
dot: 'bg-emerald-500',
},
// Statuts Intermédiaires / En cours
'Dégradé': {
bg: 'bg-amber-50 text-amber-700 border-amber-200/60',
dot: 'bg-amber-500',
},
'En déploiement': {
bg: 'bg-blue-50 text-blue-700 border-blue-200/60',
dot: 'bg-blue-500',
},
// Statuts Alertes / Inactifs
'Hors Ligne': {
bg: 'bg-red-50 text-red-700 border-red-200/60',
dot: 'bg-red-500',
},
'Suspendu': {
bg: 'bg-slate-100 text-slate-700 border-slate-200',
dot: 'bg-slate-400',
},
};
export const StatusBadge: React.FC<StatusBadgeProps> = ({ status, className }) => {
// Récupération de la configuration ou fallback de sécurité
const config = STATUS_CONFIG[status] || {
bg: 'bg-slate-50 text-slate-600 border-slate-200',
dot: 'bg-slate-400',
};
return (
<div
className={cn(
"px-2.5 py-1 rounded-full text-xs font-semibold inline-flex items-center border transition-colors",
config.bg,
className
)}
>
<span className={cn("w-1.5 h-1.5 rounded-full mr-2 shrink-0", config.dot)} />
{status}
</div>
);
};
export default StatusBadge;
+32
View File
@@ -0,0 +1,32 @@
import React from 'react';
import type { InfrastructureNode } from '@/types/InfrastructureNode';
import { StatusBadge } from '@ui/StatusBadge';
interface UptimeObjectProps {
node: InfrastructureNode;
}
export const UptimeObject: React.FC<UptimeObjectProps> = ({ node }) => {
return (
// La "key" a été retirée d'ici, elle sera gérée par le parent
<div className="py-4 flex items-center justify-between first:pt-0 last:pb-0">
{/* Infos Serveur */}
<div>
<p className="font-medium text-slate-900">{node.label}</p>
<p className="text-xs text-slate-500 mt-0.5">{node.type}</p>
</div>
{/* Métriques & Badge */}
<div className="flex items-center space-x-6">
<div className="text-right">
<p className="text-xs text-slate-500">SLA</p>
<p className="text-sm font-semibold text-slate-900">{node.uptime_sla}</p>
</div>
<StatusBadge status={node.status} />
</div>
</div>
);
};
+32 -22
View File
@@ -1,25 +1,35 @@
export default function BackupWidget({ onClick }: { onClick?: () => void }) {
return (
<div
onClick={onClick}
className="bg-white rounded-xl shadow-sm border border-slate-200 p-6 cursor-pointer hover:border-blue-300 hover:shadow-md transition-all group"
>
<div className="flex items-center justify-between mb-4">
<h2 className="text-lg font-semibold text-slate-900 group-hover:text-blue-900 transition-colors">Sauvegardes</h2>
<span className="bg-slate-100 text-slate-600 text-xs px-2 py-1 rounded font-medium border border-slate-200">
PRA Immuable
</span>
</div>
<div className="bg-slate-50 rounded-lg p-4 border border-slate-100 mb-4 group-hover:bg-blue-50/50 transition-colors">
<p className="text-sm text-slate-500 mb-1">Dernier snapshot système</p>
<p className="text-slate-900 font-medium">Aujourd'hui à 03:00 AM</p>
</div>
import { Card, CardContent } from '@/components/ui/Card';
<div className="flex items-center text-emerald-600 text-sm font-medium">
<svg className="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M5 13l4 4L19 7" /></svg>
Intégrité validée avec succès
</div>
</div>
interface BackupWidgetProps {
onClick?: () => void;
}
export default function BackupWidget({ onClick }: BackupWidgetProps) {
return (
<Card
onClick={onClick}
className="cursor-pointer hover:border-blue-300 hover:shadow-md transition-all group"
>
<CardContent className="p-6">
<div className="flex items-center justify-between mb-4">
<h2 className="text-lg font-semibold text-slate-900 group-hover:text-blue-900 transition-colors">Sauvegardes</h2>
<span className="bg-slate-100 text-slate-600 text-xs px-2 py-1 rounded font-medium border border-slate-200">
PRA Immuable
</span>
</div>
<div className="bg-slate-50 rounded-lg p-4 border border-slate-100 mb-4 group-hover:bg-blue-50/50 transition-colors">
<p className="text-sm text-slate-500 mb-1">Dernier snapshot système</p>
<p className="text-slate-900 font-medium">Aujourd'hui à 03:00 AM</p>
</div>
<div className="flex items-center text-emerald-600 text-sm font-medium">
<svg className="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M5 13l4 4L19 7" />
</svg>
Intégrité validée avec succès
</div>
</CardContent>
</Card>
);
}
+29 -18
View File
@@ -1,23 +1,34 @@
export default function SecurityWidget() {
return (
<div className="bg-white rounded-xl shadow-sm border border-slate-200 p-6">
<h2 className="text-lg font-semibold text-slate-900 mb-4">Posture de Sécurité</h2>
<div className="flex items-baseline space-x-2 mb-4">
<span className="text-3xl font-bold text-slate-900">1,432</span>
<span className="text-sm text-slate-500">menaces bloquées (30j)</span>
</div>
import { Card, CardContent } from '@/components/ui/Card';
<div className="space-y-3">
<div className="flex justify-between items-center text-sm">
<span className="text-slate-600">Bouclier Cloudflare WAF</span>
<span className="text-emerald-500 font-medium">Actif</span>
interface SecurityWidgetProps {
onClick?: () => void;
}
export default function SecurityWidget({ onClick }: SecurityWidgetProps) {
return (
<Card
onClick={onClick}
className="cursor-pointer hover:border-blue-300 hover:shadow-md transition-all group"
>
<CardContent className="p-6">
<h2 className="text-lg font-semibold text-slate-900 mb-4 group-hover:text-blue-900 transition-colors">Posture de Sécurité</h2>
<div className="flex items-baseline space-x-2 mb-4">
<span className="text-3xl font-bold text-slate-900">1,432</span>
<span className="text-sm text-slate-500">menaces bloquées (30j)</span>
</div>
<div className="flex justify-between items-center text-sm">
<span className="text-slate-600">Dernier audit de vulnérabilité</span>
<span className="text-slate-900 font-medium">Il y a 12 jours</span>
<div className="space-y-3">
<div className="flex justify-between items-center text-sm">
<span className="text-slate-600">Bouclier Cloudflare WAF</span>
<span className="text-emerald-500 font-medium">Actif</span>
</div>
<div className="flex justify-between items-center text-sm">
<span className="text-slate-600">Dernier audit de vulnérabilité</span>
<span className="text-slate-900 font-medium">Il y a 12 jours</span>
</div>
</div>
</div>
</div>
</CardContent>
</Card>
);
}
+29 -182
View File
@@ -1,193 +1,40 @@
import { useState, useEffect } from 'react';
import { pb } from '../../config/pocketbase';
import { useNavigate } from 'react-router-dom';
import { useContracts } from '@/hooks/useContracts';
import { ServiceObject } from '@/components/ui/ServiceObject';
import { Card, CardContent } from '@/components/ui/Card';
import { Button } from '@/components/ui/Button';
import { Loader } from '@/components/ui/Loader';
import { EmptyState } from '@/components/ui/EmptyState';
export default function ServicesWidget() {
const [contracts, setContracts] = useState<any[]>([]);
const [isModalOpen, setIsModalOpen] = useState(false);
const [isLoading, setIsLoading] = useState(true);
const [isSubmitting, setIsSubmitting] = useState(false);
const [submitSuccess, setSubmitSuccess] = useState(false);
useEffect(() => {
const fetchContracts = async () => {
try {
const records = await pb.collection('aegis_managed_contracts').getFullList({
sort: '-created',
});
setContracts(records);
} catch (error) {
console.error("Erreur lors de la récupération des contrats :", error);
} finally {
setIsLoading(false);
}
};
fetchContracts();
}, []);
// Fonction pour envoyer la demande d'évolution vers PocketBase
const handleEvolutionRequest = async (subject: string, description: string) => {
setIsSubmitting(true);
try {
await pb.collection('aegis_tickets').create({
author: pb.authStore.model?.id,
company: pb.authStore.model?.company,
category: "Évolution",
subject: subject,
description: description,
status: "Ouvert"
});
setSubmitSuccess(true);
// Ferme la modale après 2.5 secondes de message de succès
setTimeout(() => {
setSubmitSuccess(false);
setIsModalOpen(false);
}, 2500);
} catch (error) {
console.error("Erreur lors de la création du ticket d'évolution :", error);
alert("Une erreur est survenue. Veuillez contacter le support par téléphone.");
} finally {
setIsSubmitting(false);
}
};
const { contracts, isLoading, error } = useContracts();
const navigate = useNavigate();
return (
<>
<div className= "bg-white rounded-xl shadow-sm border border-slate-200 p-6 flex flex-col" >
<div className="mb-4" >
<h2 className="text-lg font-semibold text-slate-900" > Vos services gérés </h2>
< p className = "text-sm text-slate-500" > Catalogue des contrats d'infogérance actifs</p>
</div>
<Card>
<CardContent className="p-6 flex flex-col h-full">
<div className="mb-4">
<h2 className="text-lg font-semibold text-slate-900">Vos services gérés</h2>
<p className="text-sm text-slate-500">Catalogue des contrats d'infogérance actifs</p>
</div>
< div className = "flex-1 overflow-y-auto mb-6 space-y-3" >
{
isLoading?(
<p className = "text-sm text-slate-500 animate-pulse" > Chargement de vos contrats sécurisés...</ p >
) : contracts.length === 0 ? (
<p className= "text-sm text-slate-500" > Aucun contrat actif détecté.</p>
) : (
contracts.map((contract?: any) => (
<div key= { contract.id } className = "p-4 bg-slate-50 rounded-lg border border-slate-100 flex justify-between items-center" >
<div>
<h3 className="text-sm font-medium text-slate-900" > { contract.service_name } </h3>
{
contract.description && (
<p className="text-xs text-slate-500 mt-1"> { contract.description } </p>
)}
</div>
< span className = {`text-xs px-2 py-1 rounded-full font-medium ${contract.status === 'Actif' ? 'bg-emerald-50 text-emerald-600' :
contract.status === 'En déploiement' ? 'bg-blue-50 text-blue-600' :
'bg-slate-100 text-slate-600'
}`}>
{ contract.status }
</span>
</div>
))
<div className="flex-1 overflow-y-auto mb-6 space-y-3">
{error && <EmptyState message={error} className="text-red-500" />}
{isLoading && !error && <Loader />}
{!isLoading && !error && contracts.length === 0 && (
<EmptyState message="Aucun contrat actif détecté." />
)}
</div>
< button
onClick = {() => setIsModalOpen(true)}
className = "w-full bg-blue-900 text-white font-medium py-3 rounded-lg hover:bg-blue-800 transition-colors shadow-sm"
>
Demander une évolution du parc
</button>
</div>
{/* MODALE STRATÉGIQUE */ }
{
isModalOpen && (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-slate-900/50 backdrop-blur-sm p-4" >
<div className="bg-white rounded-xl shadow-xl w-full max-w-lg overflow-hidden animate-in fade-in zoom-in-95 duration-200" >
<div className="p-6" >
<div className="flex justify-between items-center mb-5" >
<h3 className="text-xl font-bold text-slate-900" > Évolution de l'infrastructure</h3>
{
!isSubmitting && !submitSuccess && (
<button onClick={ () => setIsModalOpen(false) } className = "text-slate-400 hover:text-slate-600" >
<svg className="w-6 h-6" fill = "none" viewBox = "0 0 24 24" stroke = "currentColor" >
<path strokeLinecap="round" strokeLinejoin = "round" strokeWidth = { 2} d = "M6 18L18 6M6 6l12 12" />
</svg>
</button>
)
}
</div>
{
submitSuccess ? (
<div className= "text-center py-8" >
<div className="w-16 h-16 bg-emerald-100 text-emerald-600 rounded-full flex items-center justify-center mx-auto mb-4" >
<svg className="w-8 h-8" fill = "none" viewBox = "0 0 24 24" stroke = "currentColor" >
<path strokeLinecap="round" strokeLinejoin = "round" strokeWidth = { 2} d = "M5 13l4 4L19 7" />
</svg>
</div>
< h4 className = "text-lg font-semibold text-slate-900 mb-2" > Demande transmise avec succès </h4>
< p className = "text-sm text-slate-500" > Un expert GISE analyse votre besoin et reviendra vers vous sous 24h.</p>
</div>
) : isSubmitting ? (
<div className= "text-center py-12" >
<p className="text-sm font-medium text-slate-500 animate-pulse" > Création de votre ticket projet sécurisé...</p>
{!isLoading && !error && contracts.map((contract) => (
<ServiceObject key={contract.id} contract={contract} />
))}
</div>
) : (
<>
<p className= "text-sm text-slate-600 mb-6" >
Quel est votre prochain objectif d'infrastructure ? Sélectionnez une initiative stratégique pour ouvrir un ticket projet avec votre expert.
</p>
< div className = "space-y-3" >
<button
onClick={ () => handleEvolutionRequest("Projet d'Évolution : Cybersécurité", "Le client souhaite renforcer sa sécurité (Audits, Tests d'intrusion, Plan de Reprise d'Activité).") }
className = "w-full text-left p-4 border border-slate-200 rounded-lg hover:border-blue-900 hover:bg-blue-50 transition-colors group"
>
<h4 className="font-medium text-slate-900 group-hover:text-blue-900" > Renforcer la sécurité face aux cybermenaces </h4>
< p className = "text-xs text-slate-500 mt-1" > Audits de sécurité, tests d'intrusion et Plan de Reprise d'Activité(PRA).</p>
</button>
<Button size="lg" onClick={() => navigate('/evolution-infrastructure')}>
Demander une évolution du parc
</Button>
< button
onClick = {() => handleEvolutionRequest("Projet d'Évolution : Architecture Réseau", "Le client souhaite étendre ses capacités (Migration Cloud Privé, nouvelles succursales).")
}
className = "w-full text-left p-4 border border-slate-200 rounded-lg hover:border-blue-900 hover:bg-blue-50 transition-colors group"
>
<h4 className="font-medium text-slate-900 group-hover:text-blue-900" > Étendre les capacités du réseau actuel </h4>
< p className = "text-xs text-slate-500 mt-1" > Migration vers Cloud Privé Sécurisé, ajout de succursales sécurisées.</p>
</button>
< button
onClick = {() => handleEvolutionRequest("Projet d'Évolution : Conformité Légal", "Le client demande un accompagnement vCISO pour la mise en conformité (RGPD, NIS2).")
}
className = "w-full text-left p-4 border border-slate-200 rounded-lg hover:border-blue-900 hover:bg-blue-50 transition-colors group"
>
<h4 className="font-medium text-slate-900 group-hover:text-blue-900" > Mise en conformité légale(RGPD, NIS2) </h4>
< p className = "text-xs text-slate-500 mt-1" > Accompagnement vCISO et mise aux normes de votre système d'information.</p>
</button>
< button
onClick = {() => handleEvolutionRequest("Projet d'Évolution : Outils Souverains", "Le client souhaite déployer de nouveaux outils collaboratifs souverains.")}
className = "w-full text-left p-4 border border-slate-200 rounded-lg hover:border-blue-900 hover:bg-blue-50 transition-colors group"
>
<h4 className="font-medium text-slate-900 group-hover:text-blue-900" > Autre demande stratégique </h4>
< p className = "text-xs text-slate-500 mt-1" > Déploiement d'outils collaboratifs souverains ou besoins spécifiques.</p>
</button>
</div>
</>
)}
</div>
{
!isSubmitting && !submitSuccess && (
<div className="bg-slate-50 px-6 py-4 border-t border-slate-200 flex justify-end" >
<button onClick={ () => setIsModalOpen(false) } className = "text-sm font-medium text-slate-600 hover:text-slate-900" >
Annuler
</button>
</div>
)
}
</div>
</div>
)}
</>
</CardContent>
</Card>
);
}
@@ -0,0 +1,27 @@
import React from 'react';
import { useNavigate } from 'react-router-dom';
import { Card, CardContent } from '@/components/ui/Card';
import { Button } from '@/components/ui/Button';
export const SupportCtaWidget: React.FC = () => {
const navigate = useNavigate();
return (
<Card className="bg-blue-900 border-blue-800 text-white shadow-sm">
<CardContent className="p-6">
<h3 className="text-lg font-semibold mb-2">Centre de Support</h3>
<p className="text-blue-200 text-sm mb-4 leading-relaxed">
Déclarez un incident critique ou demandez une évolution de votre infrastructure.
</p>
<Button
onClick={() => navigate('/support')}
className="w-full bg-white text-blue-900 hover:bg-slate-50 font-medium py-2.5 px-4 rounded-lg transition-colors shadow-sm"
>
Ouvrir un ticket sécurisé
</Button>
</CardContent>
</Card>
);
};
export default SupportCtaWidget;
+33 -93
View File
@@ -1,99 +1,39 @@
import { useState, useEffect } from 'react';
import { pb } from '../../config/pocketbase';
interface InfrastructureNode {
id: string;
label: string;
type: string;
status: string;
uptime_sla: string;
}
import { useInfrastructureNodes } from '@/hooks/useInfrastructureNodes';
import { UptimeObject } from '@/components/ui/UptimeObject';
import { Card, CardContent } from '@/components/ui/Card';
import { Loader } from '@/components/ui/Loader';
import { EmptyState } from '@/components/ui/EmptyState';
export default function UptimeWidget() {
const [nodes, setNodes] = useState<InfrastructureNode[]>([]);
const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
const fetchNodes = async () => {
try {
// Récupération sécurisée filtrée automatiquement par le Token JWT de la compagnie
const records = await pb.collection('aegis_infrastructure_nodes').getFullList<InfrastructureNode>({
sort: 'created',
});
setNodes(records);
} catch (error) {
console.error("Erreur lors de la récupération des nœuds :", error);
} finally {
setIsLoading(false);
}
};
fetchNodes();
}, []);
// Fonction pour adapter dynamiquement la couleur du badge selon le statut renvoyé par la base
const getStatusStyle = (status: string) => {
switch (status) {
case 'Opérationnel':
return {
bg: 'bg-emerald-50 text-emerald-600 border-emerald-100',
dot: 'bg-emerald-500'
};
case 'Dégradé':
return {
bg: 'bg-amber-50 text-amber-600 border-amber-100',
dot: 'bg-amber-500'
};
case 'Hors Ligne':
return {
bg: 'bg-red-50 text-red-600 border-red-100',
dot: 'bg-red-500'
};
default:
return {
bg: 'bg-slate-50 text-slate-600 border-slate-100',
dot: 'bg-slate-400'
};
}
};
const { nodes, isLoading, error } = useInfrastructureNodes();
return (
<div className="bg-white rounded-xl shadow-sm border border-slate-200 p-6 flex flex-col">
<h2 className="text-lg font-semibold text-slate-900 mb-4">État des Services & SLA</h2>
{isLoading ? (
<div className="flex-1 flex justify-center items-center py-8">
<div className="animate-spin rounded-full h-6 w-6 border-b-2 border-slate-900"></div>
</div>
) : nodes.length === 0 ? (
<div className="flex-1 flex justify-center items-center py-8 text-sm text-slate-500 italic">
Aucun équipement enregistré pour ce contrat.
</div>
) : (
<div className="divide-y divide-slate-100 flex-1">
{nodes.map((node) => {
const style = getStatusStyle(node.status);
return (
<div key={node.id} className="py-4 flex items-center justify-between first:pt-0 last:pb-0">
<div>
<p className="font-medium text-slate-900">{node.label}</p>
<p className="text-xs text-slate-500 mt-0.5">{node.type}</p>
</div>
<div className="flex items-center space-x-6">
<div className="text-right">
<p className="text-xs text-slate-500">SLA</p>
<p className="text-sm font-semibold text-slate-900">{node.uptime_sla}%</p>
</div>
<div className={`${style.bg} px-3 py-1 rounded-full text-xs font-medium flex items-center border`}>
<div className={`w-1.5 h-1.5 rounded-full ${style.dot} mr-2`}></div>
{node.status}
</div>
</div>
</div>
);
})}
</div>
)}
</div>
<Card>
<CardContent className="p-6 flex flex-col h-full">
<h2 className="text-lg font-semibold text-slate-900 mb-4">État des Services & SLA</h2>
{/* 1. Gestion de l'erreur */}
{error && (
<EmptyState message={error} className="text-red-500 not-italic font-medium" />
)}
{/* 2. Gestion du chargement */}
{isLoading && !error && <Loader />}
{/* 3. Gestion de l'état vide */}
{!isLoading && !error && nodes.length === 0 && (
<EmptyState message="Aucun équipement enregistré pour ce contrat." />
)}
{/* 4. Affichage des données */}
{!isLoading && !error && nodes.length > 0 && (
<div className="divide-y divide-slate-100 flex-1">
{nodes.map((node) => (
<UptimeObject key={node.id} node={node} />
))}
</div>
)}
</CardContent>
</Card>
);
}