EverydayTech Platform - Developer Reference
Complete Source Code Documentation - All Applications
Loading...
Searching...
No Matches
ServerDetail.jsx
Go to the documentation of this file.
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';
6
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);
15
16 const [customers, setCustomers] = useState([]);
17 const [tenants, setTenants] = useState([]);
18
19 const [formData, setFormData] = useState({
20 customer_id: '',
21 assigned_tenant_id: ''
22 });
23
24 useEffect(() => {
25 fetchServerDetail();
26 fetchCustomers();
27 fetchTenants();
28 }, [id]);
29
30 async function fetchServerDetail() {
31 setLoading(true);
32 setError('');
33 try {
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);
38 setFormData({
39 customer_id: data.droplet.customer_id || '',
40 assigned_tenant_id: data.droplet.assigned_tenant_id || ''
41 });
42
43 // Metrics are already cached in the droplet record
44 if (data.droplet) {
45 setMetrics({
46 cpu_usage_percent: data.droplet.cpu_usage_percent
47 });
48 }
49 } catch (err) {
50 console.error(err);
51 setError(err.message || 'Failed to load server');
52 } finally {
53 setLoading(false);
54 }
55 }
56
57 async function fetchCustomers() {
58 try {
59 const res = await apiFetch('/customers');
60 if (res.ok) {
61 const data = await res.json();
62 setCustomers(data.customers || []);
63 }
64 } catch (err) {
65 console.error('Error fetching customers:', err);
66 }
67 }
68
69 async function fetchTenants() {
70 try {
71 const res = await apiFetch('/tenants');
72 if (res.ok) {
73 const data = await res.json();
74 setTenants(data.tenants || []);
75 }
76 } catch (err) {
77 console.error('Error fetching tenants:', err);
78 }
79 }
80
81 async function handleSubmit(e) {
82 e.preventDefault();
83 setSaving(true);
84 try {
85 const res = await apiFetch(`/hosting/droplets/${id}`, {
86 method: 'PUT',
87 headers: { 'Content-Type': 'application/json' },
88 body: JSON.stringify(formData)
89 });
90
91 if (!res.ok) throw new Error('Failed to update server');
92
93 notifySuccess('Server updated successfully');
94 fetchServerDetail();
95 } catch (err) {
96 console.error(err);
97 notifyError(err.message || 'Failed to update server');
98 } finally {
99 setSaving(false);
100 }
101 }
102
103 function handleInputChange(field, value) {
104 setFormData(prev => ({ ...prev, [field]: value }));
105 }
106
107 function getStatusColor(status) {
108 const statusMap = {
109 'active': 'success',
110 'new': 'info',
111 'off': 'secondary',
112 'archive': 'warning',
113 'deleted': 'error',
114 'terminated': 'error'
115 };
116 return statusMap[status?.toLowerCase()] || 'secondary';
117 }
118
119 function getStatusLabel(status) {
120 if (status?.toLowerCase() === 'deleted') return 'terminated';
121 return status;
122 }
123
124 if (loading) {
125 return (
126 <MainLayout>
127 <div style={{ padding: '40px', textAlign: 'center' }}>
128 <div className="spinner"></div>
129 </div>
130 </MainLayout>
131 );
132 }
133
134 if (error) {
135 return (
136 <MainLayout>
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>
141 </div>
142 </div>
143 </MainLayout>
144 );
145 }
146
147 if (!server) {
148 return (
149 <MainLayout>
150 <div className="card" style={{ padding: '20px', margin: '20px' }}>
151 <p>Server not found</p>
152 </div>
153 </MainLayout>
154 );
155 }
156
157 return (
158 <MainLayout>
159 <div style={{ padding: '20px', maxWidth: '1200px', margin: '0 auto' }}>
160 {/* Header */}
161 <div style={{ marginBottom: '24px' }}>
162 <button
163 onClick={() => navigate('/services?tab=servers')}
164 className="btn btn-secondary"
165 style={{ marginBottom: '16px', display: 'flex', alignItems: 'center', gap: '8px' }}
166 >
167 <span className="material-symbols-outlined">arrow_back</span>
168 Back to Servers
169 </button>
170
171 <div style={{ display: 'flex', alignItems: 'center', gap: '16px' }}>
172 <span className="material-symbols-outlined" style={{ fontSize: '48px', color: 'var(--primary)' }}>
173 dns
174 </span>
175 <div>
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)}
179 </span>
180 </div>
181 </div>
182 </div>
183
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>
189 Server Details
190 </h2>
191
192 <dl style={{ display: 'grid', gap: '16px' }}>
193 <div>
194 <dt style={{ color: 'var(--text-secondary)', fontSize: '14px', marginBottom: '4px' }}>Region</dt>
195 <dd style={{ fontWeight: '600', fontSize: '16px' }}>{server.region}</dd>
196 </div>
197
198 <div>
199 <dt style={{ color: 'var(--text-secondary)', fontSize: '14px', marginBottom: '4px' }}>Size</dt>
200 <dd style={{ fontWeight: '600', fontSize: '16px' }}>{server.size}</dd>
201 </div>
202
203 {server.vcpus && (
204 <div>
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
208 </dd>
209 </div>
210 )}
211
212 {server.ip_address && (
213 <div>
214 <dt style={{ color: 'var(--text-secondary)', fontSize: '14px', marginBottom: '4px' }}>IPv4 Address</dt>
215 <dd style={{ fontFamily: 'monospace', fontSize: '16px', color: 'var(--primary)' }}>
216 {server.ip_address}
217 </dd>
218 </div>
219 )}
220
221 {server.ipv6_address && (
222 <div>
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}
226 </dd>
227 </div>
228 )}
229
230 {server.image && (
231 <div>
232 <dt style={{ color: 'var(--text-secondary)', fontSize: '14px', marginBottom: '4px' }}>Image</dt>
233 <dd style={{ fontWeight: '500', fontSize: '14px' }}>{server.image}</dd>
234 </div>
235 )}
236
237 {server.tags && server.tags.length > 0 && (
238 <div>
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">
243 {tag}
244 </span>
245 ))}
246 </dd>
247 </div>
248 )}
249
250 <div>
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'}
254 </dd>
255 </div>
256
257 <div>
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'}
261 </dd>
262 </div>
263
264 <div>
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}
268 </dd>
269 </div>
270 </dl>
271
272 {/* Metrics */}
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>
277 Cached Metrics
278 </h3>
279 <div style={{ display: 'grid', gap: '12px' }}>
280 <div>
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>
284 </div>
285 <div style={{ width: '100%', height: '8px', background: 'var(--border)', borderRadius: '4px', overflow: 'hidden' }}>
286 <div style={{
287 width: `${Math.min(metrics.cpu_usage_percent, 100)}%`,
288 height: '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'
291 }}></div>
292 </div>
293 </div>
294 </div>
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()}
298 </p>
299 )}
300 </div>
301 )}
302 </div>
303
304 {/* Right Column - Assignment Form */}
305 <div>
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>
309 Assignment
310 </h2>
311
312 <form onSubmit={handleSubmit}>
313 <div style={{ marginBottom: '20px' }}>
314 <label style={{ display: 'block', marginBottom: '8px', fontWeight: '500' }}>
315 Customer
316 </label>
317 <select
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)' }}
322 >
323 <option value="">-- Select Customer --</option>
324 {customers.map(c => (
325 <option key={c.customer_id} value={c.customer_id}>
326 {c.name}
327 </option>
328 ))}
329 </select>
330 </div>
331
332 <div style={{ marginBottom: '24px' }}>
333 <label style={{ display: 'block', marginBottom: '8px', fontWeight: '500' }}>
334 Tenant
335 </label>
336 <select
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)' }}
341 >
342 <option value="">-- Select Tenant --</option>
343 {tenants.map(t => (
344 <option key={t.tenant_id} value={t.tenant_id}>
345 {t.company_name}
346 </option>
347 ))}
348 </select>
349 </div>
350
351 <button
352 type="submit"
353 className="btn btn-primary"
354 disabled={saving}
355 style={{ width: '100%' }}
356 >
357 {saving ? 'Saving...' : 'Save Assignment'}
358 </button>
359 </form>
360 </div>
361
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>
366 Quick Actions
367 </h2>
368
369 <div style={{ display: 'grid', gap: '12px' }}>
370 <button
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' }}
374 >
375 <span className="material-symbols-outlined">open_in_new</span>
376 View in DigitalOcean Console
377 </button>
378
379 {server.ip_address && (
380 <button
381 className="btn btn-secondary"
382 onClick={() => navigator.clipboard.writeText(server.ip_address)}
383 style={{ display: 'flex', alignItems: 'center', gap: '8px', justifyContent: 'center' }}
384 >
385 <span className="material-symbols-outlined">content_copy</span>
386 Copy IP Address
387 </button>
388 )}
389 </div>
390 </div>
391 </div>
392 </div>
393 </div>
394 </MainLayout>
395 );
396}
397
398export default ServerDetail;