init aegis
This commit is contained in:
@@ -0,0 +1,43 @@
|
||||
export default function BackupModale(){
|
||||
return (
|
||||
<div className="bg-white rounded-2xl shadow-xl w-full max-w-3xl overflow-hidden animate-in fade-in zoom-in-95 duration-200">
|
||||
<div className="px-6 py-4 border-b border-slate-100 flex justify-between items-center bg-slate-50">
|
||||
<h3 className="text-lg font-bold text-slate-900">Historique des Sauvegardes PRA</h3>
|
||||
<button onClick={closeModal} className="text-slate-400 hover:text-slate-900"><svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M6 18L18 6M6 6l12 12" /></svg></button>
|
||||
</div>
|
||||
<div className="p-0 overflow-x-auto">
|
||||
<table className="min-w-full divide-y divide-slate-200">
|
||||
<thead className="bg-slate-50">
|
||||
<tr>
|
||||
<th className="px-6 py-3 text-left text-xs font-semibold text-slate-500 uppercase">Date d'exécution</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-semibold text-slate-500 uppercase">Cible (Nœud)</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-semibold text-slate-500 uppercase">Type</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-semibold text-slate-500 uppercase">Volume</th>
|
||||
<th className="px-6 py-3 text-right text-xs font-semibold text-slate-500 uppercase">Statut</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="bg-white divide-y divide-slate-100">
|
||||
{[
|
||||
{ date: 'Aujourd\'hui, 03:00 AM', node: 'Cluster Proxmox (Compta)', type: 'Incrémentielle', size: '12 GB' },
|
||||
{ date: 'Hier, 03:00 AM', node: 'Cluster Proxmox (Compta)', type: 'Incrémentielle', size: '8.4 GB' },
|
||||
{ date: 'Dimanche, 01:00 AM', node: 'Cluster Proxmox (Compta)', type: 'Totale (Full)', size: '240 GB' },
|
||||
{ date: 'Samedi, 03:00 AM', node: 'Cluster Proxmox (Compta)', type: 'Incrémentielle', size: '4.1 GB' },
|
||||
].map((bkp, i) => (
|
||||
<tr key={i} className="hover:bg-slate-50">
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium text-slate-900">{bkp.date}</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm text-slate-500">{bkp.node}</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm text-slate-500">{bkp.type}</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm text-slate-500">{bkp.size}</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-right">
|
||||
<span className="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-emerald-50 text-emerald-700 border border-emerald-100">
|
||||
Succès (Chiffré)
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
import React from 'react';
|
||||
|
||||
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>
|
||||
|
||||
<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>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import React from 'react';
|
||||
|
||||
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>
|
||||
|
||||
<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>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,193 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { pb } from '../../config/pocketbase';
|
||||
|
||||
export default function ServicesWidget() {
|
||||
const [contracts, setContracts] = useState([]);
|
||||
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);
|
||||
}
|
||||
};
|
||||
|
||||
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>
|
||||
|
||||
< 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) => (
|
||||
<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>
|
||||
|
||||
< 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>
|
||||
</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
|
||||
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>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { pb } from '../../config/pocketbase';
|
||||
|
||||
interface InfrastructureNode {
|
||||
id: string;
|
||||
label: string;
|
||||
type: string;
|
||||
status: string;
|
||||
uptime_sla: string;
|
||||
}
|
||||
|
||||
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'
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user