remove alert
Deploy Nexus Portal to HestiaCP (FTP) / build-and-deploy (push) Successful in 31s

This commit is contained in:
maximus
2026-06-22 22:27:39 +02:00
parent bdcb6cb3f9
commit 421b97156a
3 changed files with 224 additions and 82 deletions
+73 -34
View File
@@ -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>
);
}