separate components
This commit is contained in:
@@ -0,0 +1,68 @@
|
||||
import { Server, Database, Cloud, Globe, ExternalLink, Play, Loader } from 'lucide-react';
|
||||
|
||||
export default function InstanceCard({ service, vpcs, onAssignVpc, onRemoveVpc, onOpenConsole, isConnecting }) {
|
||||
const titleLower = (service.title || '').toLowerCase();
|
||||
const isVPS = titleLower.includes('vps') || titleLower.includes('compute');
|
||||
const isCloud = titleLower.includes('cloud') || titleLower.includes('nextcloud');
|
||||
const isDB = titleLower.includes('db') || titleLower.includes('base') || titleLower.includes('sql');
|
||||
const isWeb = !isVPS && !isCloud && !isDB;
|
||||
const hasIP = service.hostingDetails?.ip && service.hostingDetails.ip !== '127.0.0.1' && service.hostingDetails.ip !== '';
|
||||
|
||||
let buttonText = "CONSOLE D'ADMINISTRATION";
|
||||
let ButtonIcon = ExternalLink;
|
||||
let buttonStyle = "bg-cyan-400/10 hover:bg-cyan-400 text-cyan-400 hover:text-gray-900 border-cyan-400";
|
||||
|
||||
if (isVPS) {
|
||||
if (hasIP) { buttonText = "GÉRER L'INSTANCE"; ButtonIcon = ExternalLink; }
|
||||
else { buttonText = "INITIALISATION"; ButtonIcon = Play; buttonStyle = "bg-yellow-500/10 text-yellow-500 hover:bg-yellow-500 hover:text-gray-900 border-yellow-500/50"; }
|
||||
} else if (isWeb) { buttonText = "GÉRER L'HÉBERGEMENT"; ButtonIcon = Globe; buttonStyle = "bg-emerald-400/10 hover:bg-emerald-400 text-emerald-400 hover:text-gray-900 border-emerald-400"; }
|
||||
else if (isCloud) { buttonText = "ACCÉDER AU CLOUD"; buttonStyle = "bg-blue-400/10 hover:bg-blue-400 text-blue-400 hover:text-gray-900 border-blue-400"; }
|
||||
else if (isDB) { buttonText = "PHPMYADMIN / CLUSTER"; ButtonIcon = Database; buttonStyle = "bg-purple-400/10 hover:bg-purple-400 text-purple-400 hover:text-gray-900 border-purple-400"; }
|
||||
|
||||
const getServiceIcon = () => {
|
||||
if (isVPS) return <Server className="w-8 h-8 text-cyan-400" />;
|
||||
if (isCloud) return <Cloud className="w-8 h-8 text-blue-400" />;
|
||||
if (isDB) return <Database className="w-8 h-8 text-purple-400" />;
|
||||
return <Globe className="w-8 h-8 text-emerald-400" />;
|
||||
};
|
||||
|
||||
const baseTitle = (service.title || '').split(/(?: for | pour )/i)[0].trim();
|
||||
const shortTitle = baseTitle.split(' ').slice(0, 2).join(' ');
|
||||
const titleMatch = (service.title || '').match(/(?: for | pour )(.+)$/i);
|
||||
const domainFromTitle = titleMatch ? titleMatch[1].trim() : null;
|
||||
let displayDomain = service.hostingDetails?.domain && service.hostingDetails.domain !== '127.0.0.1' ? service.hostingDetails.domain : domainFromTitle || service.domain;
|
||||
if (!displayDomain || displayDomain === '127.0.0.1') displayDomain = null;
|
||||
|
||||
return (
|
||||
<div className="bg-gray-900 border border-gray-800 rounded-xl p-5 flex flex-col justify-between hover:border-gray-700 transition-all shadow-lg">
|
||||
<div>
|
||||
<div className="flex justify-between items-start mb-4">
|
||||
<div className="p-2 bg-black/40 rounded-lg">{getServiceIcon()}</div>
|
||||
{service.status === 'active' ? (
|
||||
<span className="bg-emerald-500/10 text-emerald-400 px-2 py-1 rounded text-xs border border-emerald-500/20 font-bold tracking-widest">
|
||||
{isVPS && !hasIP ? 'AWAITING INIT' : 'ONLINE'}
|
||||
</span>
|
||||
) : (
|
||||
<span className="bg-orange-500/10 text-orange-400 px-2 py-1 rounded text-xs border border-orange-500/20 animate-pulse">DEPLOYING</span>
|
||||
)}
|
||||
</div>
|
||||
<h3 className="font-bold text-white text-lg truncate" title={baseTitle}>{shortTitle}</h3>
|
||||
<div className="flex flex-col gap-1 mt-2 mb-4 h-8 justify-center">
|
||||
{displayDomain ? ( <p className={`${isVPS ? 'text-cyan-400' : 'text-emerald-400'} text-xs font-mono truncate`} title={displayDomain}>{displayDomain}</p> ) : ( <p className="text-gray-600 text-xs font-mono italic">En attente de déploiement</p> )}
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-auto space-y-3">
|
||||
<div className="flex items-center justify-between text-sm border-t border-gray-800 pt-3">
|
||||
<span className="text-gray-500 text-xs">Projet VPC:</span>
|
||||
<select onChange={(e) => { const val = e.target.value; if (val === "free") onRemoveVpc(service.id); else if (val) onAssignVpc(service.id, val); }} className="bg-black border border-gray-700 text-gray-300 rounded px-2 py-1 outline-none focus:border-cyan-400 text-xs w-[130px]" defaultValue={vpcs.find(v => v.services.includes(service.id))?.id || "free"}>
|
||||
<option value="free">-- Libre --</option>
|
||||
{vpcs.map(vpc => ( <option key={vpc.id} value={vpc.id}>{vpc.name}</option> ))}
|
||||
</select>
|
||||
</div>
|
||||
<button onClick={() => onOpenConsole(service)} disabled={isConnecting === service.id || service.status !== 'active'} className={`w-full flex items-center justify-center space-x-2 py-2.5 rounded-lg font-bold text-sm tracking-wide transition-all disabled:opacity-50 border ${buttonStyle}`}>
|
||||
{isConnecting === service.id ? ( <><Loader className="w-4 h-4 animate-spin" /><span>CONNEXION...</span></> ) : ( <><ButtonIcon className="w-4 h-4" /><span>{buttonText}</span></> )}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
import { useState } from 'react';
|
||||
|
||||
export default function VpsDeployer({ serviceId, onDeploySuccess, onAlert }) {
|
||||
const [domain, setDomain] = useState('');
|
||||
const [password, setPassword] = useState('');
|
||||
const [isDeploying, setIsDeploying] = useState(false);
|
||||
|
||||
const handleDeploy = async () => {
|
||||
if (password.length < 8) { onAlert("Sécurité", "Le mot de passe doit faire au moins 8 caractères.", "error"); return; }
|
||||
setIsDeploying(true);
|
||||
try {
|
||||
const response = await fetch('https://web.gise.be/custom_api/proxmox_create_vps.php', { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: new URLSearchParams({ service_id: serviceId, domain: domain, password: password }) });
|
||||
const data = await response.json();
|
||||
if (data.status === 'success') { onDeploySuccess({ ip: data.ip, domain: data.domain, password: password }); }
|
||||
else { onAlert("Échec", "Erreur de provisionnement : " + (data.message || 'Inconnue'), "error"); }
|
||||
} catch (err) { onAlert("Erreur Réseau", "Erreur de communication avec l'API Proxmox Gateway.", "error"); }
|
||||
finally { setIsDeploying(false); }
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="bg-yellow-900/20 border border-yellow-600/30 p-4 rounded mb-6 text-yellow-500/80 text-sm">
|
||||
<span className="font-bold text-yellow-500">PHASE D'INITIALISATION REQUISE</span><br />
|
||||
La facturation est activée. Veuillez définir un mot de passe Root pour lancer la création de l'instance.
|
||||
</div>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="block text-xs uppercase tracking-widest text-cyan-500 mb-1">Hostname personnalisé</label>
|
||||
<input type="text" value={domain} onChange={(e) => setDomain(e.target.value.toLowerCase())} placeholder="Optionnel" className="w-full bg-gray-950 border border-gray-800 text-white p-3 rounded font-mono outline-none focus:border-cyan-500 transition-colors" />
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-xs uppercase tracking-widest text-cyan-500 mb-1">Mot de passe Root</label>
|
||||
<input type="password" value={password} onChange={(e) => setPassword(e.target.value)} placeholder="••••••••" className="w-full bg-gray-950 border border-gray-800 text-white p-3 rounded font-mono outline-none focus:border-cyan-500 transition-colors" />
|
||||
</div>
|
||||
</div>
|
||||
<button onClick={handleDeploy} disabled={isDeploying} className={`mt-6 w-full px-6 py-3 rounded font-bold font-mono tracking-widest uppercase transition ${isDeploying ? 'bg-gray-800 text-gray-500 cursor-not-allowed' : 'bg-cyan-600 hover:bg-cyan-500 text-white shadow-lg shadow-cyan-500/20'}`}>
|
||||
{isDeploying ? 'Fabrication sur le Métal...' : 'Lancer l\'Instanciation'}
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
import { useState } from 'react';
|
||||
import { Folder, ExternalLink, Play, Maximize2, Lock } from 'lucide-react';
|
||||
|
||||
export default function VpsManager({ details, orderId, onRefresh, onAlert }) {
|
||||
const [showTerminal, setShowTerminal] = useState(false);
|
||||
const [isEditingDomain, setIsEditingDomain] = useState(false);
|
||||
const [newDomain, setNewDomain] = useState(details.domain);
|
||||
const [isUpdating, setIsUpdating] = useState(false);
|
||||
const [sslStatus, setSslStatus] = useState(null);
|
||||
|
||||
const isInternal = details.domain && details.domain.endsWith('.gise.be');
|
||||
const fileManagerUrl = isInternal ? `https://file-${details.domain}` : `http://${details.domain}:8080`;
|
||||
const terminalUrl = isInternal ? `https://terminal-${details.domain}` : `http://${details.domain}:8081`;
|
||||
|
||||
const handleUpdateDomain = async () => {
|
||||
if (!newDomain || newDomain === details.domain) return;
|
||||
setIsUpdating(true);
|
||||
try {
|
||||
const response = await fetch('https://web.gise.be/custom_api/update_service_domain.php', { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: new URLSearchParams({ order_id: orderId, new_domain: newDomain }) });
|
||||
const data = await response.json();
|
||||
if (data.status === 'success') { setIsEditingDomain(false); if (onRefresh) onRefresh(); onAlert("Succès", "Domaine VPS mis à jour !", "success"); }
|
||||
else onAlert("Erreur", data.error, "error");
|
||||
} catch (err) { onAlert("Erreur", "Erreur de connexion avec l'API.", "error"); } finally { setIsUpdating(false); }
|
||||
};
|
||||
|
||||
const handleGenerateSSL = async () => {
|
||||
setSslStatus('loading');
|
||||
try {
|
||||
const response = await fetch('https://web.gise.be/custom_api/generate_custom_ssl.php', { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: new URLSearchParams({ domain: details.domain }) });
|
||||
const data = await response.json();
|
||||
if (data.status === 'success') { setSslStatus('success'); onAlert("SSL Activé", "Certificat SSL généré !", "success"); }
|
||||
else { setSslStatus('error'); onAlert("Erreur DNS", "Vérifiez votre pointage.", "error"); }
|
||||
} catch (e) { setSslStatus('error'); }
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
{!showTerminal ? (
|
||||
<>
|
||||
<div className="mb-6 bg-gray-950 border border-gray-800 rounded p-4">
|
||||
<label className="block text-xs uppercase tracking-widest text-cyan-500 mb-2">Domaine de l'instance VPS</label>
|
||||
{!isEditingDomain ? (
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="font-mono text-xl text-green-400 font-bold flex items-center gap-2">
|
||||
<span className="h-3 w-3 rounded-full bg-green-500 shadow-[0_0_10px_#22c55e]"></span>{details.domain}
|
||||
</span>
|
||||
<button onClick={() => setIsEditingDomain(true)} className="text-xs border border-gray-700 hover:border-cyan-500 text-gray-400 hover:text-cyan-400 px-3 py-1.5 rounded transition">MODIFIER</button>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex gap-2">
|
||||
<input type="text" value={newDomain} onChange={(e) => setNewDomain(e.target.value.toLowerCase())} className="flex-1 bg-black border border-gray-700 rounded px-3 py-1.5 text-white font-mono text-sm focus:border-cyan-500 outline-none" disabled={isUpdating} />
|
||||
<button onClick={handleUpdateDomain} disabled={isUpdating} className="bg-cyan-500 text-gray-900 px-4 py-1.5 rounded text-xs font-bold hover:bg-cyan-400">{isUpdating ? 'SYNC...' : 'SAUVEGARDER'}</button>
|
||||
<button onClick={() => setIsEditingDomain(false)} className="bg-gray-800 text-white px-3 py-1.5 rounded text-xs hover:bg-gray-700">ANNULER</button>
|
||||
</div>
|
||||
)}
|
||||
{!isInternal && (
|
||||
<div className="mt-4 border-t border-gray-900 pt-3 flex justify-between items-center">
|
||||
<p className="text-[11px] text-gray-500">Domaine externe détecté.</p>
|
||||
<button onClick={handleGenerateSSL} disabled={sslStatus === 'loading'} className="text-xs bg-purple-600/20 hover:bg-purple-600 text-purple-400 hover:text-white border border-purple-500/30 px-3 py-1 rounded transition flex items-center gap-1">
|
||||
{sslStatus === 'loading' ? 'GÉNÉRATION...' : <><Lock className="w-3 h-3" /> GÉNÉRER SSL</>}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6 mb-6">
|
||||
<button onClick={() => window.open(fileManagerUrl, '_blank')} className="bg-gray-950 hover:bg-gray-900 border border-gray-800 hover:border-cyan-500/50 rounded p-3 flex justify-between items-center transition group">
|
||||
<span className="font-mono text-gray-300 text-sm flex items-center gap-2"><Folder className="w-4 h-4 text-cyan-500" /> Explorateur</span><ExternalLink className="w-4 h-4 text-gray-600 group-hover:text-cyan-400" />
|
||||
</button>
|
||||
<button onClick={() => setShowTerminal(true)} className="bg-cyan-900/20 hover:bg-cyan-900/40 border border-cyan-800/50 hover:border-cyan-500 rounded p-3 flex justify-between items-center transition group">
|
||||
<span className="font-mono text-cyan-400 text-sm flex items-center gap-2"><span>{">_"}</span> Terminal</span><Play className="w-4 h-4 text-cyan-600 group-hover:text-cyan-400" />
|
||||
</button>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<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">TERMINAL ({details.domain})</span>
|
||||
<div className="flex items-center space-x-2">
|
||||
<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" />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
import { useState } from 'react';
|
||||
import { Globe, ExternalLink, Lock, Loader } from 'lucide-react';
|
||||
import { resetHostingPassword } from '../../services/api';
|
||||
|
||||
export default function WebManager({ details, orderId, onRefresh, onOpenSso, onAlert }) {
|
||||
const [isEditingDomain, setIsEditingDomain] = useState(false);
|
||||
const [newDomain, setNewDomain] = useState(details.domain);
|
||||
const [isUpdating, setIsUpdating] = useState(false);
|
||||
const [sslStatus, setSslStatus] = useState(null);
|
||||
const [isGeneratingSso, setIsGeneratingSso] = useState(false);
|
||||
|
||||
const isCustomDomain = details.domain && !details.domain.endsWith('.gise.be');
|
||||
const siteUrl = (isCustomDomain && sslStatus !== 'success') ? `http://${details.domain}` : `https://${details.domain}`;
|
||||
|
||||
const handleUpdateDomain = async () => {
|
||||
if (!newDomain || newDomain === details.domain) return;
|
||||
setIsUpdating(true);
|
||||
try {
|
||||
const response = await fetch('https://web.gise.be/custom_api/update_service_domain.php', { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: new URLSearchParams({ order_id: orderId, new_domain: newDomain }) });
|
||||
const data = await response.json();
|
||||
if (data.status === 'success') { setIsEditingDomain(false); if (onRefresh) onRefresh(); onAlert("Succès", "Domaine mis à jour !", "success"); }
|
||||
else onAlert("Erreur", data.error, "error");
|
||||
} catch (err) { onAlert("Erreur", "Erreur API.", "error"); } finally { setIsUpdating(false); }
|
||||
};
|
||||
|
||||
const handleGenerateSSL = async () => {
|
||||
setSslStatus('loading');
|
||||
try {
|
||||
const response = await fetch('https://web.gise.be/custom_api/generate_custom_ssl.php', { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: new URLSearchParams({ domain: details.domain }) });
|
||||
const data = await response.json();
|
||||
if (data.status === 'success') { setSslStatus('success'); onAlert("SSL Validé", "Certificat SSL généré !", "success"); }
|
||||
else { setSslStatus('error'); onAlert("Erreur DNS", "Vérifiez vos DNS.", "error"); }
|
||||
} catch (e) { setSslStatus('error'); }
|
||||
};
|
||||
|
||||
const handleOpenPanel = async () => {
|
||||
setIsGeneratingSso(true);
|
||||
try {
|
||||
const secureHash = Math.random().toString(36).slice(-8) + Math.random().toString(36).slice(-4).toUpperCase();
|
||||
const rollingPassword = `Nx${secureHash}`;
|
||||
await resetHostingPassword(orderId, rollingPassword);
|
||||
onOpenSso({ username: details.username, password: rollingPassword, url: 'https://panel.gise.be/login/' });
|
||||
} catch (err) { onAlert("Erreur Métal", "Erreur lors de la génération du SSO HestiaCP.", "error"); }
|
||||
finally { setIsGeneratingSso(false); }
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="mb-6 bg-gray-950 border border-gray-800 rounded p-4">
|
||||
<label className="block text-xs uppercase tracking-widest text-emerald-500 mb-2">Domaine du Site Web</label>
|
||||
{!isEditingDomain ? (
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="font-mono text-xl text-emerald-400 font-bold flex items-center gap-2"><span className="h-3 w-3 rounded-full bg-emerald-500 shadow-[0_0_10px_#10b981]"></span>{details.domain}</span>
|
||||
<button onClick={() => setIsEditingDomain(true)} className="text-xs border border-gray-700 hover:border-emerald-500 text-gray-400 hover:text-emerald-400 px-3 py-1.5 rounded transition">MODIFIER</button>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex gap-2">
|
||||
<input type="text" value={newDomain} onChange={(e) => setNewDomain(e.target.value.toLowerCase())} className="flex-1 bg-black border border-gray-700 rounded px-3 py-1.5 text-white font-mono text-sm focus:border-emerald-500 outline-none" disabled={isUpdating} />
|
||||
<button onClick={handleUpdateDomain} disabled={isUpdating} className="bg-emerald-500 text-gray-900 px-4 py-1.5 rounded text-xs font-bold hover:bg-emerald-400">{isUpdating ? 'SYNC...' : 'SAUVEGARDER'}</button>
|
||||
<button onClick={() => setIsEditingDomain(false)} className="bg-gray-800 text-white px-3 py-1.5 rounded text-xs hover:bg-gray-700">ANNULER</button>
|
||||
</div>
|
||||
)}
|
||||
{isCustomDomain && (
|
||||
<div className="mt-4 border-t border-gray-900 pt-3 flex justify-between items-center">
|
||||
<p className="text-[11px] text-gray-500">Domaine personnalisé détecté.</p>
|
||||
<button onClick={handleGenerateSSL} disabled={sslStatus === 'loading'} className="text-xs bg-purple-600/20 hover:bg-purple-600 text-purple-400 hover:text-white border border-purple-500/30 px-3 py-1 rounded transition flex items-center gap-1">
|
||||
{sslStatus === 'loading' ? 'GÉNÉRATION...' : <><Lock className="w-3 h-3" /> GÉNÉRER SSL</>}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6 mb-6">
|
||||
<button onClick={() => window.open(siteUrl, '_blank')} className="bg-gray-950 hover:bg-gray-900 border border-gray-800 hover:border-emerald-500/50 rounded p-3 flex justify-between items-center transition group">
|
||||
<span className="font-mono text-gray-300 text-sm flex items-center gap-2"><Globe className="w-4 h-4 text-emerald-500" /> Ouvrir Site</span><ExternalLink className="w-4 h-4 text-gray-600 group-hover:text-emerald-400" />
|
||||
</button>
|
||||
<button onClick={handleOpenPanel} disabled={isGeneratingSso} className="bg-emerald-900/20 hover:bg-emerald-900/40 border border-emerald-800/50 hover:border-emerald-500 rounded p-3 flex justify-between items-center transition group disabled:opacity-50">
|
||||
{isGeneratingSso ? <span className="font-mono text-emerald-400 text-sm flex items-center gap-2"><Loader className="w-4 h-4 animate-spin" /> SSO...</span> : <><span className="font-mono text-emerald-400 text-sm flex items-center gap-2"><Lock className="w-4 h-4" /> Panel</span><ExternalLink className="w-4 h-4 text-emerald-600 group-hover:text-emerald-400" /></>}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user