This commit is contained in:
+73
-34
@@ -1,7 +1,28 @@
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import { getMyServices, getServiceDetails, getHostingServiceDetails, resetHostingPassword } from '../../services/api';
|
||||
import { useVPC } from '../../services/useVPC';
|
||||
import { Server, Database, Cloud, Globe, Folder, Trash2, ExternalLink, Loader, Plus, Play, Lock } from 'lucide-react';
|
||||
import { Server, Database, Cloud, Globe, Folder, Trash2, ExternalLink, Loader, Plus, Play, Lock, Maximize2 } from 'lucide-react';
|
||||
|
||||
// ============================================================================
|
||||
// COMPOSANT 0 : MODAL DE NOTIFICATION (Remplace les alert() natifs)
|
||||
// ============================================================================
|
||||
const NotificationModal = ({ notification, onClose }) => {
|
||||
if (!notification) return null;
|
||||
const isError = notification.type === 'error';
|
||||
return (
|
||||
<div className="fixed inset-0 bg-black/80 backdrop-blur-sm z-[100] flex items-center justify-center p-4">
|
||||
<div className={`bg-gray-900 border ${isError ? 'border-red-500/50 shadow-red-500/10' : 'border-cyan-500/50 shadow-cyan-500/10'} rounded-lg shadow-2xl max-w-sm w-full p-6 text-gray-200`}>
|
||||
<h4 className={`text-lg font-mono font-bold tracking-wider mb-2 ${isError ? 'text-red-400' : 'text-cyan-400'}`}>
|
||||
{notification.title.toUpperCase()}
|
||||
</h4>
|
||||
<p className="text-sm text-gray-400 mb-6">{notification.message}</p>
|
||||
<button onClick={onClose} className={`w-full font-mono py-2 rounded text-xs font-bold transition ${isError ? 'bg-red-600 hover:bg-red-500 text-white' : 'bg-cyan-500 hover:bg-cyan-400 text-gray-950'}`}>
|
||||
COMPRIS
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// COMPOSANT 1 : INSTANCE CARD (Mode PaaS Pur - IP Masquée)
|
||||
@@ -129,13 +150,13 @@ const InstanceCard = ({ service, vpcs, onAssignVpc, onRemoveVpc, onOpenConsole,
|
||||
// ============================================================================
|
||||
// COMPOSANT 2 : VPS DEPLOYER (Instanciation)
|
||||
// ============================================================================
|
||||
const VpsDeployer = ({ serviceId, onDeploySuccess }) => {
|
||||
const VpsDeployer = ({ serviceId, onDeploySuccess, onAlert }) => {
|
||||
const [domain, setDomain] = useState('');
|
||||
const [password, setPassword] = useState('');
|
||||
const [isDeploying, setIsDeploying] = useState(false);
|
||||
|
||||
const handleDeploy = async () => {
|
||||
if (password.length < 8) { alert("Le mot de passe doit faire au moins 8 caractères."); return; }
|
||||
if (password.length < 8) { onAlert("Sécurité", "Le mot de passe doit faire au moins 8 caractères.", "error"); return; }
|
||||
|
||||
setIsDeploying(true);
|
||||
try {
|
||||
@@ -149,10 +170,10 @@ const VpsDeployer = ({ serviceId, onDeploySuccess }) => {
|
||||
if (data.status === 'success') {
|
||||
onDeploySuccess({ ip: data.ip, domain: data.domain, password: password });
|
||||
} else {
|
||||
alert("Erreur de provisionnement : " + (data.message || 'Inconnue'));
|
||||
onAlert("Échec", "Erreur de provisionnement : " + (data.message || 'Inconnue'), "error");
|
||||
}
|
||||
} catch (err) {
|
||||
alert("Erreur de communication avec l'API Proxmox Gateway.");
|
||||
onAlert("Erreur Réseau", "Erreur de communication avec l'API Proxmox Gateway.", "error");
|
||||
} finally {
|
||||
setIsDeploying(false);
|
||||
}
|
||||
@@ -185,7 +206,7 @@ const VpsDeployer = ({ serviceId, onDeploySuccess }) => {
|
||||
// ============================================================================
|
||||
// COMPOSANT 3 : VPS MANAGER (Day-2 Operations)
|
||||
// ============================================================================
|
||||
const VpsManager = ({ details, orderId, onRefresh, onClose }) => {
|
||||
const VpsManager = ({ details, orderId, onRefresh, onAlert }) => {
|
||||
const [showTerminal, setShowTerminal] = useState(false);
|
||||
const [isEditingDomain, setIsEditingDomain] = useState(false);
|
||||
const [newDomain, setNewDomain] = useState(details.domain);
|
||||
@@ -211,10 +232,10 @@ const VpsManager = ({ details, orderId, onRefresh, onClose }) => {
|
||||
if (data.status === 'success') {
|
||||
setIsEditingDomain(false);
|
||||
if (onRefresh) onRefresh();
|
||||
alert("Domaine VPS mis à jour avec succès sur l'infrastructure !");
|
||||
} else alert("Erreur: " + data.error);
|
||||
onAlert("Succès", "Domaine VPS mis à jour avec succès sur l'infrastructure !", "success");
|
||||
} else onAlert("Erreur", data.error, "error");
|
||||
} catch (err) {
|
||||
alert("Erreur de connexion avec l'API.");
|
||||
onAlert("Erreur", "Erreur de connexion avec l'API.", "error");
|
||||
} finally { setIsUpdating(false); }
|
||||
};
|
||||
|
||||
@@ -229,10 +250,10 @@ const VpsManager = ({ details, orderId, onRefresh, onClose }) => {
|
||||
const data = await response.json();
|
||||
if (data.status === 'success') {
|
||||
setSslStatus('success');
|
||||
alert("Certificat SSL Let's Encrypt généré et activé avec succès !");
|
||||
onAlert("SSL Activé", "Certificat SSL Let's Encrypt généré et activé avec succès !", "success");
|
||||
} else {
|
||||
setSslStatus('error');
|
||||
alert("Échec de la validation DNS. Vérifiez que votre domaine pointe bien vers notre IP.");
|
||||
onAlert("Challenge DNS Échoué", "Vérifiez que votre domaine pointe bien vers notre IP publique.", "error");
|
||||
}
|
||||
} catch (e) { setSslStatus('error'); }
|
||||
};
|
||||
@@ -268,6 +289,13 @@ const VpsManager = ({ details, orderId, onRefresh, onClose }) => {
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* ACCÈS DE FAUT REMIS À JOUR */}
|
||||
<div className="mt-4 bg-black/60 border border-gray-900 rounded p-3 text-[11px] space-y-1">
|
||||
<span className="text-gray-500 font-bold uppercase tracking-wider block mb-1 text-[10px]">Identifiants d'usine :</span>
|
||||
<p className="text-gray-400 font-mono">Console SSH : <span className="text-cyan-400 font-bold">root</span> / <span className="text-gray-500 italic">Clé choisie à l'initialisation</span></p>
|
||||
<p className="text-gray-400 font-mono">Explorateur : <span className="text-cyan-400 font-bold">admin</span> / <span className="text-cyan-400 font-bold">admin</span></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6 mb-6">
|
||||
@@ -291,7 +319,13 @@ const VpsManager = ({ details, orderId, onRefresh, onClose }) => {
|
||||
<div>
|
||||
<div className="flex justify-between items-center mb-4 border-b border-gray-800 pb-2">
|
||||
<span className="text-cyan-400 font-mono text-sm tracking-widest">NEXUS TERMINAL ({details.domain})</span>
|
||||
<button onClick={() => setShowTerminal(false)} className="text-xs bg-red-900/30 text-white px-3 py-1 rounded">Fermer</button>
|
||||
<div className="flex items-center space-x-2">
|
||||
{/* OPTION AJOUTÉE : OUVRIR EN PLEIN ÉCRAN DANS UN NOUVEL ONGLET */}
|
||||
<button onClick={() => { window.open(terminalUrl, '_blank'); setShowTerminal(false); }} className="text-xs bg-cyan-600/30 text-cyan-400 hover:bg-cyan-600 hover:text-white border border-cyan-500/30 px-3 py-1 rounded transition flex items-center gap-1">
|
||||
<Maximize2 className="w-3 h-3" /> Plein Écran ↗
|
||||
</button>
|
||||
<button onClick={() => setShowTerminal(false)} className="text-xs bg-red-900/30 text-white px-3 py-1 rounded">Fermer</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="bg-black border border-gray-800 rounded h-[400px]">
|
||||
<iframe src={terminalUrl} className="w-full h-full border-none" title="Terminal" />
|
||||
@@ -305,7 +339,7 @@ const VpsManager = ({ details, orderId, onRefresh, onClose }) => {
|
||||
// ============================================================================
|
||||
// COMPOSANT 4 : WEB MANAGER (HestiaCP & Day-2 Operations)
|
||||
// ============================================================================
|
||||
const WebManager = ({ details, orderId, onRefresh, onOpenSso }) => {
|
||||
const WebManager = ({ details, orderId, onRefresh, onOpenSso, onAlert }) => {
|
||||
const [isEditingDomain, setIsEditingDomain] = useState(false);
|
||||
const [newDomain, setNewDomain] = useState(details.domain);
|
||||
const [isUpdating, setIsUpdating] = useState(false);
|
||||
@@ -328,10 +362,10 @@ const WebManager = ({ details, orderId, onRefresh, onOpenSso }) => {
|
||||
if (data.status === 'success') {
|
||||
setIsEditingDomain(false);
|
||||
if (onRefresh) onRefresh();
|
||||
alert("Domaine web mis à jour avec succès sur le serveur HestiaCP !");
|
||||
} else alert("Erreur: " + data.error);
|
||||
onAlert("Succès", "Domaine web mis à jour avec succès sur le serveur HestiaCP !", "success");
|
||||
} else onAlert("Erreur", data.error, "error");
|
||||
} catch (err) {
|
||||
alert("Erreur de connexion avec l'API.");
|
||||
onAlert("Erreur", "Erreur de connexion avec l'API.", "error");
|
||||
} finally { setIsUpdating(false); }
|
||||
};
|
||||
|
||||
@@ -346,10 +380,10 @@ const WebManager = ({ details, orderId, onRefresh, onOpenSso }) => {
|
||||
const data = await response.json();
|
||||
if (data.status === 'success') {
|
||||
setSslStatus('success');
|
||||
alert("Certificat SSL Let's Encrypt généré !");
|
||||
onAlert("SSL Validé", "Certificat SSL Let's Encrypt généré !", "success");
|
||||
} else {
|
||||
setSslStatus('error');
|
||||
alert("Échec Let's Encrypt. Vérifiez vos DNS.");
|
||||
onAlert("Erreur DNS", "Échec Let's Encrypt. Vérifiez vos DNS.", "error");
|
||||
}
|
||||
} catch (e) { setSslStatus('error'); }
|
||||
};
|
||||
@@ -360,11 +394,9 @@ const WebManager = ({ details, orderId, onRefresh, onOpenSso }) => {
|
||||
const secureHash = Math.random().toString(36).slice(-8) + Math.random().toString(36).slice(-4).toUpperCase();
|
||||
const rollingPassword = `Nx${secureHash}`;
|
||||
await resetHostingPassword(orderId, rollingPassword);
|
||||
|
||||
// On déclenche le modal SSO du composant parent
|
||||
onOpenSso({ username: details.username, password: rollingPassword, url: 'https://panel.gise.be/login/' });
|
||||
} catch (err) {
|
||||
alert("Erreur lors de la génération du SSO HestiaCP.");
|
||||
onAlert("Erreur Métal", "Erreur lors de la génération du SSO HestiaCP.", "error");
|
||||
} finally {
|
||||
setIsGeneratingSso(false);
|
||||
}
|
||||
@@ -438,11 +470,15 @@ export default function Services() {
|
||||
const [newVpcName, setNewVpcName] = useState("");
|
||||
const [isConnecting, setIsConnecting] = useState(null);
|
||||
const [ssoVault, setSsoVault] = useState(null);
|
||||
const [activeServiceModal, setActiveServiceModal] = useState(null); // Gère VPS et WEB
|
||||
const [activeServiceModal, setActiveServiceModal] = useState(null);
|
||||
const [customAlert, setCustomAlert] = useState(null); // Gère les pop-ups personnalisés
|
||||
|
||||
const { vpcs, createVPC, deleteVPC, assignToVPC, removeFromVPC } = useVPC();
|
||||
|
||||
// Fonction extraite pour pouvoir être rappelée par les Managers
|
||||
const triggerAlert = (title, message, type = "info") => {
|
||||
setCustomAlert({ title, message, type });
|
||||
};
|
||||
|
||||
const fetchServices = useCallback(async () => {
|
||||
try {
|
||||
const data = await getMyServices();
|
||||
@@ -495,12 +531,11 @@ export default function Services() {
|
||||
} else if (isDB) {
|
||||
window.open('https://pma.gise.be/', '_blank');
|
||||
} else if (isVPS || isWeb) {
|
||||
// Pour VPS et Web, on récupère les détails frais et on ouvre le Modal Générique
|
||||
const freshDetails = await getHostingServiceDetails(service.id);
|
||||
setActiveServiceModal({ type: isVPS ? 'vps' : 'web', data: service, details: freshDetails });
|
||||
}
|
||||
} catch (err) {
|
||||
alert("Échec du protocole : " + err.message);
|
||||
triggerAlert("Erreur d'Orchestration", "Échec du protocole : " + err.message, "error");
|
||||
} finally {
|
||||
setIsConnecting(null);
|
||||
}
|
||||
@@ -521,7 +556,7 @@ export default function Services() {
|
||||
|
||||
<div className="flex space-x-2 bg-gray-900 p-2 rounded-xl border border-gray-800">
|
||||
<input type="text" placeholder="Nom du nouveau VPC..." value={newVpcName} onChange={(e) => setNewVpcName(e.target.value)} className="bg-black border border-gray-800 rounded-lg px-4 py-2 text-sm text-white focus:outline-none focus:border-cyan-400 w-64" />
|
||||
<button onClick={() => { createVPC(newVpcName); setNewVpcName(""); }} disabled={!newVpcName.trim()} className="bg-cyan-400 text-gray-900 px-4 py-2 rounded-lg font-bold text-sm disabled:opacity-50 hover:bg-cyan-300 flex items-center">
|
||||
<button onClick={() => { createVPC(newVpcName); setNewVpcName(""); triggerAlert("VPC", "Nouveau groupe de routage VPC créé avec succès !", "success"); }} disabled={!newVpcName.trim()} className="bg-cyan-400 text-gray-900 px-4 py-2 rounded-lg font-bold text-sm disabled:opacity-50 hover:bg-cyan-300 flex items-center">
|
||||
<Plus className="w-4 h-4 mr-1" /> CRÉER GROUPE
|
||||
</button>
|
||||
</div>
|
||||
@@ -540,7 +575,7 @@ export default function Services() {
|
||||
<h2 className="text-xl font-bold tracking-wider">{vpc.name.toUpperCase()}</h2>
|
||||
<span className="bg-gray-800 text-gray-400 px-2 py-0.5 rounded text-xs font-mono">{vpcServices.length} INSTANCES</span>
|
||||
</div>
|
||||
<button onClick={() => deleteVPC(vpc.id)} className="text-gray-500 hover:text-red-400 transition-colors flex items-center text-sm">
|
||||
<button onClick={() => { deleteVPC(vpc.id); triggerAlert("VPC Démantelé", "Le groupe de routage a été dissous. Les instances ont été reversées dans le pool libre.", "info"); }} className="text-gray-500 hover:text-red-400 transition-colors flex items-center text-sm">
|
||||
<Trash2 className="w-4 h-4 mr-1" /> Démanteler VPC
|
||||
</button>
|
||||
</div>
|
||||
@@ -591,7 +626,8 @@ export default function Services() {
|
||||
<div className="bg-gray-900 border border-emerald-500/50 rounded-lg shadow-2xl shadow-emerald-500/20 max-w-md w-full p-6 text-gray-200">
|
||||
<div className="flex justify-between items-center mb-6">
|
||||
<h3 className="text-xl font-bold text-emerald-400 font-mono tracking-wider">ACCÈS AUTORISÉ</h3>
|
||||
<button onClick={() => setSsoVault(null)} className="text-gray-400 hover:text-white transition">✖</button>
|
||||
{/* La croix permet de fermer manuellement le modal après copie */}
|
||||
<button onClick={() => setSsoVault(null)} className="text-gray-400 hover:text-white transition text-lg">✖</button>
|
||||
</div>
|
||||
<p className="text-sm text-gray-400 mb-6">Le pare-feu HestiaCP bloque les injections directes. Un mot de passe <strong>jetable</strong> a été généré. Copiez-le et connectez-vous.</p>
|
||||
<div className="space-y-4 mb-8">
|
||||
@@ -610,8 +646,9 @@ export default function Services() {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<a href={ssoVault.url} target="_blank" rel="noopener noreferrer" onClick={() => setSsoVault(null)} className="block w-full bg-emerald-500 hover:bg-emerald-400 text-gray-950 font-bold py-3 text-center rounded transition font-mono tracking-widest uppercase">
|
||||
Ouvrir le Panel Web
|
||||
{/* REFIXÉ : Pas de setSsoVault(null) ici pour garder le modal ouvert au retour de l'onglet */}
|
||||
<a href={ssoVault.url} target="_blank" rel="noopener noreferrer" className="block w-full bg-emerald-500 hover:bg-emerald-400 text-gray-950 font-bold py-3 text-center rounded transition font-mono tracking-widest uppercase">
|
||||
Ouvrir le Panel Web ↗
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
@@ -632,19 +669,21 @@ export default function Services() {
|
||||
<button onClick={() => setActiveServiceModal(null)} className="text-gray-400 hover:text-white transition text-xl">✖</button>
|
||||
</div>
|
||||
|
||||
{/* ROUTAGE DU COMPOSANT INTERNE SELON L'ÉTAT DU SERVICE */}
|
||||
{activeServiceModal.type === 'vps' ? (
|
||||
activeServiceModal.details?.ip && activeServiceModal.details.ip !== '127.0.0.1' ? (
|
||||
<VpsManager details={activeServiceModal.details} orderId={activeServiceModal.data.id} onRefresh={fetchServices} />
|
||||
<VpsManager details={activeServiceModal.details} orderId={activeServiceModal.data.id} onRefresh={fetchServices} onAlert={triggerAlert} />
|
||||
) : (
|
||||
<VpsDeployer serviceId={activeServiceModal.data.id} onDeploySuccess={() => { setActiveServiceModal(null); fetchServices(); }} />
|
||||
<VpsDeployer serviceId={activeServiceModal.data.id} onDeploySuccess={() => { setActiveServiceModal(null); fetchServices(); }} onAlert={triggerAlert} />
|
||||
)
|
||||
) : (
|
||||
<WebManager details={activeServiceModal.details} orderId={activeServiceModal.data.id} onRefresh={fetchServices} onOpenSso={setSsoVault} />
|
||||
<WebManager details={activeServiceModal.details} orderId={activeServiceModal.data.id} onRefresh={fetchServices} onOpenSso={setSsoVault} onAlert={triggerAlert} />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* VRAI MODAL DE NOTIFICATION REACT SURCHARGE */}
|
||||
<NotificationModal notification={customAlert} onClose={() => setCustomAlert(null)} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user