149 lines
10 KiB
React
149 lines
10 KiB
React
import { useState, useEffect, useCallback } from 'react';
|
|
import { getOrdersList, getOrderDetails } from '../../services/billing_api';
|
|
import { useVPC } from '../../services/useVPC';
|
|
import { Server, Globe, Folder, Trash2, Loader, Plus, AlertCircle } from 'lucide-react';
|
|
|
|
// Importation de tes nouveaux composants modulaires !
|
|
import NotificationModal from '../../components/ui/NotificationModal';
|
|
import InstanceCard from '../../components/services/InstanceCard';
|
|
import VpsDeployer from '../../components/services/VpsDeployer';
|
|
import VpsManager from '../../components/services/VpsManager';
|
|
import WebManager from '../../components/services/WebManager';
|
|
|
|
export default function Services() {
|
|
const [services, setServices] = useState([]);
|
|
const [isLoading, setIsLoading] = useState(true);
|
|
const [error, setError] = useState(null);
|
|
const [newVpcName, setNewVpcName] = useState("");
|
|
const [isConnecting, setIsConnecting] = useState(null);
|
|
const [activeServiceModal, setActiveServiceModal] = useState(null);
|
|
const [customAlert, setCustomAlert] = useState(null);
|
|
|
|
const { vpcs, createVPC, deleteVPC, assignToVPC, removeFromVPC } = useVPC();
|
|
|
|
const triggerAlert = (title, message, type = "info") => setCustomAlert({ title, message, type });
|
|
|
|
const fetchServices = useCallback(async () => {
|
|
try {
|
|
const data = await getOrdersList();
|
|
if (data.list && data.list.length > 0) {
|
|
const detailedServices = await Promise.all(data.list.map(async (order) => {
|
|
let hDetails = null;
|
|
if (order.status === 'active' && (order.plugin === 'hosting' || (order.title || '').toLowerCase().includes('vps'))) {
|
|
try { hDetails = await getOrderDetails(order.id); } catch (e) {}
|
|
}
|
|
return { ...order, hostingDetails: hDetails };
|
|
}));
|
|
|
|
const filteredServices = detailedServices.filter(s => {
|
|
const type = (s.type || '').toLowerCase();
|
|
const title = (s.title || '').toLowerCase();
|
|
const isGhostProduct = type === 'domain' || title.includes('domain') || title.includes('domaine') || title.includes('enregistrement');
|
|
return (s.status === 'active' || s.status === 'pending_setup') && !isGhostProduct;
|
|
});
|
|
setServices(filteredServices);
|
|
}
|
|
} catch (err) { setError(err.message || "Impossible de charger la télémétrie."); }
|
|
finally { setIsLoading(false); }
|
|
}, []);
|
|
|
|
useEffect(() => { fetchServices(); }, [fetchServices]);
|
|
|
|
const handleOpenConsole = async (service) => {
|
|
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;
|
|
|
|
setIsConnecting(service.id);
|
|
try {
|
|
if (isCloud) {
|
|
const titleMatch = (service.title || '').match(/(?: for | pour )(.+)$/i);
|
|
const domainUrl = titleMatch ? titleMatch[1].trim() : service.domain;
|
|
if (domainUrl) window.open(`https://${domainUrl}`, '_blank');
|
|
} else if (isDB) {
|
|
window.open('https://pma.gise.be/', '_blank');
|
|
} else if (isVPS || isWeb) {
|
|
const freshDetails = await getOrderDetails(service.id);
|
|
setActiveServiceModal({ type: isVPS ? 'vps' : 'web', data: service, details: freshDetails });
|
|
}
|
|
} catch (err) { triggerAlert("Erreur", err.message, "error"); }
|
|
finally { setIsConnecting(null); }
|
|
};
|
|
|
|
const assignedServiceIds = vpcs.flatMap(vpc => vpc.services);
|
|
const freeServices = services.filter(s => !assignedServiceIds.includes(s.id));
|
|
|
|
return (
|
|
<div className="w-full max-w-6xl p-6 mx-auto">
|
|
<div className="flex flex-col md:flex-row justify-between items-start md:items-end mb-8 gap-4">
|
|
<div>
|
|
<header>
|
|
<h1 className="text-3xl font-black text-white tracking-wider">TERMINAL <span className="text-cyan-400">NEXUS</span></h1>
|
|
<p className="text-gray-400 mt-2">Orchestration des environnements et des Virtual Private Clouds.</p>
|
|
</header>
|
|
</div>
|
|
<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(""); triggerAlert("VPC", "Nouveau groupe créé !", "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>
|
|
</div>
|
|
|
|
{isLoading && <div className="flex items-center space-x-3 text-cyan-400 mb-8"><Loader className="w-6 h-6 animate-spin" /><span>Analyse en cours...</span></div>}
|
|
{error && <div className="flex items-center space-x-3 text-red-400 bg-red-400/10 border border-red-400 p-4 rounded-lg mb-8"><AlertCircle className="w-6 h-6" /><span>{error}</span></div>}
|
|
|
|
{!isLoading && !error && (
|
|
<>
|
|
<div className="space-y-8 mb-12">
|
|
{vpcs.map(vpc => {
|
|
const vpcServices = services.filter(s => vpc.services.includes(s.id));
|
|
return (
|
|
<div key={vpc.id} className="bg-gray-900/40 border border-gray-800 rounded-2xl p-6">
|
|
<div className="flex justify-between items-center mb-6 border-b border-gray-800 pb-4">
|
|
<div className="flex items-center space-x-3 text-white">
|
|
<Folder className="w-6 h-6 text-cyan-400" /><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); triggerAlert("VPC Démantelé", "Instances reversées.", "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</button>
|
|
</div>
|
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
|
|
{vpcServices.length === 0 ? <div className="col-span-full text-gray-600 text-sm border border-dashed border-gray-800 rounded-xl p-6 text-center font-mono">Vide.</div> : vpcServices.map(service => <InstanceCard key={service.id} service={service} vpcs={vpcs} onAssignVpc={assignToVPC} onRemoveVpc={removeFromVPC} onOpenConsole={handleOpenConsole} isConnecting={isConnecting} />)}
|
|
</div>
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
<div>
|
|
<h2 className="text-lg font-bold text-gray-500 tracking-wider mb-6 flex items-center"><Server className="w-5 h-5 mr-2" /> POOL LIBRE</h2>
|
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
|
|
{freeServices.length === 0 ? <div className="col-span-full text-gray-600 text-sm border border-gray-900 bg-gray-900/20 rounded-xl p-6 text-center font-mono">Aucune instance libre.</div> : freeServices.map(service => <InstanceCard key={service.id} service={service} vpcs={vpcs} onAssignVpc={assignToVPC} onRemoveVpc={removeFromVPC} onOpenConsole={handleOpenConsole} isConnecting={isConnecting} />)}
|
|
</div>
|
|
</div>
|
|
</>
|
|
)}
|
|
|
|
|
|
{activeServiceModal && (
|
|
<div className="fixed inset-0 bg-black/80 backdrop-blur-sm z-50 flex items-center justify-center p-4">
|
|
<div className={`bg-gray-900 border rounded-lg shadow-2xl max-w-2xl w-full p-6 text-gray-200 ${activeServiceModal.type === 'vps' ? 'border-cyan-500/50 shadow-cyan-500/20' : 'border-emerald-500/50 shadow-emerald-500/20'}`}>
|
|
<div className="flex justify-between items-center mb-6 border-b border-gray-800 pb-4">
|
|
<h3 className="text-xl font-bold text-white tracking-wider flex items-center gap-3">
|
|
{activeServiceModal.type === 'vps' ? <><Server className="w-6 h-6 text-cyan-400" /> GESTION VPS</> : <><Globe className="w-6 h-6 text-emerald-400" /> GESTION WEB</>}
|
|
</h3>
|
|
<button onClick={() => setActiveServiceModal(null)} className="text-gray-400 hover:text-white transition text-xl">✖</button>
|
|
</div>
|
|
{activeServiceModal.type === 'vps' ? (
|
|
activeServiceModal.details?.ip && activeServiceModal.details.ip !== '127.0.0.1' ? <VpsManager details={activeServiceModal.details} orderId={activeServiceModal.data.id} onRefresh={fetchServices} onAlert={triggerAlert} /> : <VpsDeployer serviceId={activeServiceModal.data.id} onDeploySuccess={() => { setActiveServiceModal(null); fetchServices(); }} onAlert={triggerAlert} />
|
|
) : (
|
|
<WebManager details={activeServiceModal.details} orderId={activeServiceModal.data.id} onRefresh={fetchServices} onAlert={triggerAlert} />
|
|
)}
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
<NotificationModal notification={customAlert} onClose={() => setCustomAlert(null)} />
|
|
</div>
|
|
);
|
|
} |