1import { useEffect, useState } from 'react';
2import { useParams, useNavigate } from 'react-router-dom';
3import MainLayout from '../components/Layout/MainLayout';
4import { apiFetch } from '../lib/api';
5import { notifySuccess, notifyError } from '../utils/notifications';
7function ServerDetail() {
8 const { id } = useParams();
9 const navigate = useNavigate();
10 const [server, setServer] = useState(null);
11 const [metrics, setMetrics] = useState(null);
12 const [loading, setLoading] = useState(true);
13 const [error, setError] = useState('');
14 const [saving, setSaving] = useState(false);
16 const [customers, setCustomers] = useState([]);
17 const [tenants, setTenants] = useState([]);
19 const [formData, setFormData] = useState({
21 assigned_tenant_id: ''
30 async function fetchServerDetail() {
34 const res = await apiFetch(`/hosting/droplets/${id}`);
35 if (!res.ok) throw new Error('Failed to load server');
36 const data = await res.json();
37 setServer(data.droplet);
39 customer_id: data.droplet.customer_id || '',
40 assigned_tenant_id: data.droplet.assigned_tenant_id || ''
43 // Metrics are already cached in the droplet record
46 cpu_usage_percent: data.droplet.cpu_usage_percent
51 setError(err.message || 'Failed to load server');
57 async function fetchCustomers() {
59 const res = await apiFetch('/customers');
61 const data = await res.json();
62 setCustomers(data.customers || []);
65 console.error('Error fetching customers:', err);
69 async function fetchTenants() {
71 const res = await apiFetch('/tenants');
73 const data = await res.json();
74 setTenants(data.tenants || []);
77 console.error('Error fetching tenants:', err);
81 async function handleSubmit(e) {
85 const res = await apiFetch(`/hosting/droplets/${id}`, {
87 headers: { 'Content-Type': 'application/json' },
88 body: JSON.stringify(formData)
91 if (!res.ok) throw new Error('Failed to update server');
93 notifySuccess('Server updated successfully');
97 notifyError(err.message || 'Failed to update server');
103 function handleInputChange(field, value) {
104 setFormData(prev => ({ ...prev, [field]: value }));
107 function getStatusColor(status) {
112 'archive': 'warning',
114 'terminated': 'error'
116 return statusMap[status?.toLowerCase()] || 'secondary';
119 function getStatusLabel(status) {
120 if (status?.toLowerCase() === 'deleted') return 'terminated';
127 <div style={{ padding: '40px', textAlign: 'center' }}>
128 <div className="spinner"></div>
137 <div className="card" style={{ padding: '20px', margin: '20px', background: 'var(--error-bg)', border: '1px solid var(--error-border)' }}>
138 <div style={{ display: 'flex', alignItems: 'center', gap: '12px' }}>
139 <span className="material-symbols-outlined" style={{ color: 'var(--error-text)' }}>error</span>
140 <span style={{ color: 'var(--error-text)' }}>{error}</span>
150 <div className="card" style={{ padding: '20px', margin: '20px' }}>
151 <p>Server not found</p>
159 <div style={{ padding: '20px', maxWidth: '1200px', margin: '0 auto' }}>
161 <div style={{ marginBottom: '24px' }}>
163 onClick={() => navigate('/services?tab=servers')}
164 className="btn btn-secondary"
165 style={{ marginBottom: '16px', display: 'flex', alignItems: 'center', gap: '8px' }}
167 <span className="material-symbols-outlined">arrow_back</span>
171 <div style={{ display: 'flex', alignItems: 'center', gap: '16px' }}>
172 <span className="material-symbols-outlined" style={{ fontSize: '48px', color: 'var(--primary)' }}>
176 <h1 style={{ fontSize: '28px', marginBottom: '8px' }}>{server.droplet_name}</h1>
177 <span className={`badge badge-${getStatusColor(server.status)}`} style={{ fontSize: '14px' }}>
178 {getStatusLabel(server.status)}
184 <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '24px' }}>
185 {/* Left Column - Details */}
186 <div className="card" style={{ padding: '24px' }}>
187 <h2 style={{ fontSize: '20px', marginBottom: '20px', display: 'flex', alignItems: 'center', gap: '8px' }}>
188 <span className="material-symbols-outlined">info</span>
192 <dl style={{ display: 'grid', gap: '16px' }}>
194 <dt style={{ color: 'var(--text-secondary)', fontSize: '14px', marginBottom: '4px' }}>Region</dt>
195 <dd style={{ fontWeight: '600', fontSize: '16px' }}>{server.region}</dd>
199 <dt style={{ color: 'var(--text-secondary)', fontSize: '14px', marginBottom: '4px' }}>Size</dt>
200 <dd style={{ fontWeight: '600', fontSize: '16px' }}>{server.size}</dd>
205 <dt style={{ color: 'var(--text-secondary)', fontSize: '14px', marginBottom: '4px' }}>Resources</dt>
206 <dd style={{ fontWeight: '600', fontSize: '16px' }}>
207 {server.vcpus} vCPU • {(server.memory / 1024).toFixed(0)} GB RAM • {server.disk} GB Disk
212 {server.ip_address && (
214 <dt style={{ color: 'var(--text-secondary)', fontSize: '14px', marginBottom: '4px' }}>IPv4 Address</dt>
215 <dd style={{ fontFamily: 'monospace', fontSize: '16px', color: 'var(--primary)' }}>
221 {server.ipv6_address && (
223 <dt style={{ color: 'var(--text-secondary)', fontSize: '14px', marginBottom: '4px' }}>IPv6 Address</dt>
224 <dd style={{ fontFamily: 'monospace', fontSize: '14px', color: 'var(--primary)', wordBreak: 'break-all' }}>
225 {server.ipv6_address}
232 <dt style={{ color: 'var(--text-secondary)', fontSize: '14px', marginBottom: '4px' }}>Image</dt>
233 <dd style={{ fontWeight: '500', fontSize: '14px' }}>{server.image}</dd>
237 {server.tags && server.tags.length > 0 && (
239 <dt style={{ color: 'var(--text-secondary)', fontSize: '14px', marginBottom: '8px' }}>Tags</dt>
240 <dd style={{ display: 'flex', flexWrap: 'wrap', gap: '8px' }}>
241 {server.tags.map((tag, i) => (
242 <span key={i} className="badge badge-secondary">
251 <dt style={{ color: 'var(--text-secondary)', fontSize: '14px', marginBottom: '4px' }}>Created</dt>
252 <dd style={{ fontSize: '14px' }}>
253 {server.created_at ? new Date(server.created_at).toLocaleString() : 'N/A'}
258 <dt style={{ color: 'var(--text-secondary)', fontSize: '14px', marginBottom: '4px' }}>Last Updated</dt>
259 <dd style={{ fontSize: '14px' }}>
260 {server.updated_at ? new Date(server.updated_at).toLocaleString() : 'N/A'}
265 <dt style={{ color: 'var(--text-secondary)', fontSize: '14px', marginBottom: '4px' }}>DO Droplet ID</dt>
266 <dd style={{ fontFamily: 'monospace', fontSize: '12px', color: 'var(--text-secondary)' }}>
267 {server.do_droplet_id}
273 {metrics && metrics.cpu_usage_percent !== null && (
274 <div style={{ marginTop: '24px', padding: '16px', background: 'var(--surface-2, #f8f9fa)', borderRadius: '8px' }}>
275 <h3 style={{ fontSize: '16px', marginBottom: '12px', display: 'flex', alignItems: 'center', gap: '8px' }}>
276 <span className="material-symbols-outlined">monitoring</span>
279 <div style={{ display: 'grid', gap: '12px' }}>
281 <div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: '8px' }}>
282 <span style={{ fontSize: '14px', color: 'var(--text-secondary)' }}>CPU Usage (avg)</span>
283 <span style={{ fontSize: '16px', fontWeight: '600' }}>{metrics.cpu_usage_percent}%</span>
285 <div style={{ width: '100%', height: '8px', background: 'var(--border)', borderRadius: '4px', overflow: 'hidden' }}>
287 width: `${Math.min(metrics.cpu_usage_percent, 100)}%`,
289 background: metrics.cpu_usage_percent > 80 ? 'var(--error)' : metrics.cpu_usage_percent > 50 ? 'var(--warning)' : 'var(--success)',
290 transition: 'width 0.3s ease'
295 {server.metrics_last_updated && (
296 <p style={{ fontSize: '12px', color: 'var(--text-secondary)', marginTop: '12px' }}>
297 Last updated: {new Date(server.metrics_last_updated).toLocaleString()}
304 {/* Right Column - Assignment Form */}
306 <div className="card" style={{ padding: '24px', marginBottom: '20px' }}>
307 <h2 style={{ fontSize: '20px', marginBottom: '20px', display: 'flex', alignItems: 'center', gap: '8px' }}>
308 <span className="material-symbols-outlined">business</span>
312 <form onSubmit={handleSubmit}>
313 <div style={{ marginBottom: '20px' }}>
314 <label style={{ display: 'block', marginBottom: '8px', fontWeight: '500' }}>
318 className="form-control"
319 value={formData.customer_id}
320 onChange={(e) => handleInputChange('customer_id', e.target.value)}
321 style={{ width: '100%', padding: '10px', borderRadius: '4px', border: '1px solid var(--border)' }}
323 <option value="">-- Select Customer --</option>
324 {customers.map(c => (
325 <option key={c.customer_id} value={c.customer_id}>
332 <div style={{ marginBottom: '24px' }}>
333 <label style={{ display: 'block', marginBottom: '8px', fontWeight: '500' }}>
337 className="form-control"
338 value={formData.assigned_tenant_id}
339 onChange={(e) => handleInputChange('assigned_tenant_id', e.target.value)}
340 style={{ width: '100%', padding: '10px', borderRadius: '4px', border: '1px solid var(--border)', color: 'var(--text)' }}
342 <option value="">-- Select Tenant --</option>
344 <option key={t.tenant_id} value={t.tenant_id}>
353 className="btn btn-primary"
355 style={{ width: '100%' }}
357 {saving ? 'Saving...' : 'Save Assignment'}
362 {/* Quick Actions */}
363 <div className="card" style={{ padding: '24px' }}>
364 <h2 style={{ fontSize: '20px', marginBottom: '20px', display: 'flex', alignItems: 'center', gap: '8px' }}>
365 <span className="material-symbols-outlined">bolt</span>
369 <div style={{ display: 'grid', gap: '12px' }}>
371 className="btn btn-secondary"
372 onClick={() => window.open(`https://cloud.digitalocean.com/droplets/${server.do_droplet_id}`, '_blank')}
373 style={{ display: 'flex', alignItems: 'center', gap: '8px', justifyContent: 'center' }}
375 <span className="material-symbols-outlined">open_in_new</span>
376 View in DigitalOcean Console
379 {server.ip_address && (
381 className="btn btn-secondary"
382 onClick={() => navigator.clipboard.writeText(server.ip_address)}
383 style={{ display: 'flex', alignItems: 'center', gap: '8px', justifyContent: 'center' }}
385 <span className="material-symbols-outlined">content_copy</span>
398export default ServerDetail;