EverydayTech Platform - Developer Reference
Complete Source Code Documentation - All Applications
Loading...
Searching...
No Matches
TabServers.jsx
Go to the documentation of this file.
1import { useState, useEffect } from 'react';
2import { useNavigate } from 'react-router-dom';
3import { apiFetch } from '../../lib/api';
4
5export default function TabServers() {
6 const navigate = useNavigate();
7 const [droplets, setDroplets] = useState([]);
8 const [metrics, setMetrics] = useState({});
9 const [loading, setLoading] = useState(true);
10 const [error, setError] = useState('');
11 const [syncing, setSyncing] = useState(false);
12 const [lastSync, setLastSync] = useState(null);
13
14 useEffect(() => {
15 fetchDroplets();
16 }, []);
17
18 const fetchDroplets = async () => {
19 setLoading(true);
20 setError('');
21 try {
22 const res = await apiFetch('/hosting/droplets');
23 if (res.ok) {
24 const data = await res.json();
25
26 // Filter out WordPress and Node.js droplets
27 const filteredDroplets = (data.droplets || []).filter(droplet => {
28 const name = droplet.droplet_name?.toLowerCase() || '';
29 const tags = droplet.tags || [];
30 const tagString = tags.join(' ').toLowerCase();
31
32 // Exclude if name or tags contain wordpress, node, or nodejs
33 return !name.includes('wordpress') &&
34 !name.includes('node') &&
35 !name.includes('nodejs') &&
36 !tagString.includes('wordpress') &&
37 !tagString.includes('node');
38 });
39
40 setDroplets(filteredDroplets);
41 setLastSync(data.lastSync);
42
43 // Metrics are already cached in the droplet records
44 const metricsData = {};
45 for (const droplet of filteredDroplets) {
46 metricsData[droplet.do_droplet_id] = {
47 cpu_usage_percent: droplet.cpu_usage_percent,
48 memory_usage_percent: droplet.memory_usage_percent,
49 disk_usage_percent: droplet.disk_usage_percent
50 };
51 }
52 setMetrics(metricsData);
53 } else {
54 const errorData = await res.json();
55 setError(errorData.error || 'Failed to fetch droplets');
56 }
57 } catch (err) {
58 console.error('Error fetching droplets:', err);
59 setError(err.message || 'Failed to load droplets');
60 } finally {
61 setLoading(false);
62 }
63 };
64
65 const handleSync = async () => {
66 setSyncing(true);
67 setError('');
68 try {
69 const res = await apiFetch('/hosting/sync', { method: 'POST' });
70 if (res.ok) {
71 await fetchDroplets();
72 } else {
73 const errorData = await res.json();
74 setError(errorData.error || 'Failed to sync');
75 }
76 } catch (err) {
77 console.error('Error syncing droplets:', err);
78 setError(err.message || 'Failed to sync droplets');
79 } finally {
80 setSyncing(false);
81 }
82 };
83
84 const getStatusColor = (status) => {
85 const statusMap = {
86 'active': 'success',
87 'new': 'info',
88 'off': 'secondary',
89 'archive': 'warning',
90 'deleted': 'error',
91 'terminated': 'error'
92 };
93 return statusMap[status?.toLowerCase()] || 'secondary';
94 };
95
96 const getStatusLabel = (status) => {
97 if (status?.toLowerCase() === 'deleted') return 'terminated';
98 return status;
99 };
100
101 const formatTime = (date) => {
102 if (!date) return '';
103 const now = new Date();
104 const then = new Date(date);
105 const diff = Math.floor((now - then) / 1000);
106
107 if (diff < 60) return `${diff} seconds ago`;
108 if (diff < 3600) return `${Math.floor(diff / 60)} minutes ago`;
109 if (diff < 86400) return `${Math.floor(diff / 3600)} hours ago`;
110 return `${Math.floor(diff / 86400)} days ago`;
111 };
112
113 if (loading) {
114 return (
115 <div style={{ padding: '40px', textAlign: 'center' }}>
116 <div className="spinner"></div>
117 </div>
118 );
119 }
120
121 return (
122 <div style={{ padding: '20px' }}>
123 <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '20px' }}>
124 <div>
125 <h2>Servers (Droplets)</h2>
126 <p style={{ color: 'var(--text-secondary)', marginTop: '8px' }}>
127 Manage your DigitalOcean droplets
128 </p>
129 </div>
130 <button
131 onClick={handleSync}
132 disabled={syncing}
133 className="btn btn-primary"
134 style={{ display: 'flex', alignItems: 'center', gap: '8px' }}
135 >
136 <span className="material-symbols-outlined" style={{ animation: syncing ? 'spin 1s linear infinite' : 'none' }}>
137 refresh
138 </span>
139 {syncing ? 'Syncing...' : 'Sync Now'}
140 </button>
141 </div>
142
143 <div className="card" style={{ padding: '16px', marginBottom: '20px', background: 'var(--info-bg)', border: '1px solid var(--info-border)' }}>
144 <div style={{ display: 'flex', alignItems: 'center', gap: '12px' }}>
145 <span className="material-symbols-outlined" style={{ color: 'var(--info-text)' }}>
146 schedule
147 </span>
148 <div style={{ fontSize: '14px', color: 'var(--info-text)' }}>
149 Data refreshes every 5 minutes from DigitalOcean.
150 {lastSync && (
151 <span style={{ marginLeft: '8px', opacity: 0.8 }}>
152 Last synced {formatTime(lastSync)}
153 </span>
154 )}
155 </div>
156 </div>
157 </div>
158
159 {error && (
160 <div className="card" style={{ padding: '16px', marginBottom: '20px', background: 'var(--error-bg)', border: '1px solid var(--error-border)' }}>
161 <div style={{ display: 'flex', alignItems: 'center', gap: '12px' }}>
162 <span className="material-symbols-outlined" style={{ color: 'var(--error-text)' }}>
163 error
164 </span>
165 <span style={{ fontSize: '14px', color: 'var(--error-text)' }}>{error}</span>
166 </div>
167 </div>
168 )}
169
170 {droplets.length === 0 ? (
171 <div className="card" style={{ padding: '60px 20px', textAlign: 'center' }}>
172 <span className="material-symbols-outlined" style={{ fontSize: '64px', marginBottom: '16px', display: 'block', opacity: 0.5, color: 'var(--text-secondary)' }}>
173 dns
174 </span>
175 <p style={{ color: 'var(--text-secondary)' }}>No droplets found</p>
176 <p style={{ fontSize: '14px', color: 'var(--text-tertiary)', marginTop: '8px' }}>
177 Create a droplet in your DigitalOcean account
178 </p>
179 </div>
180 ) : (
181 <div style={{ display: 'grid', gap: '20px', gridTemplateColumns: 'repeat(auto-fill, minmax(350px, 1fr))' }}>
182 {droplets.map((droplet) => {
183 const dropletMetrics = metrics[droplet.do_droplet_id];
184 const isActive = droplet.status?.toLowerCase() === 'active';
185
186 // Calculate average CPU if metrics available
187 let avgCpu = null;
188 if (dropletMetrics && dropletMetrics.data && dropletMetrics.data.result && dropletMetrics.data.result.length > 0) {
189 const values = dropletMetrics.data.result[0].values || [];
190 if (values.length > 0) {
191 const sum = values.reduce((acc, [, value]) => acc + parseFloat(value), 0);
192 avgCpu = (sum / values.length).toFixed(1);
193 }
194 }
195
196 return (
197 <div
198 key={droplet.droplet_id}
199 className="card"
200 style={{ overflow: 'hidden', cursor: 'pointer' }}
201 onClick={() => navigate(`/hosting/servers/${droplet.droplet_id}`)}
202 >
203 <div style={{ padding: '20px', borderBottom: '1px solid var(--border)' }}>
204 <div style={{ display: 'flex', alignItems: 'flex-start', justifyContent: 'space-between', marginBottom: '12px' }}>
205 <div style={{ flex: 1 }}>
206 <h3 style={{ fontSize: '18px', marginBottom: '6px', fontWeight: '600' }}>{droplet.droplet_name}</h3>
207 <p style={{ fontSize: '13px', color: 'var(--text-secondary)' }}>
208 {droplet.customer_name || 'Unassigned'}
209 </p>
210 </div>
211 <span className={`badge badge-${getStatusColor(droplet.status)}`} style={{ textTransform: 'capitalize' }}>
212 {isActive && <span style={{ marginRight: '4px' }}>●</span>}
213 {getStatusLabel(droplet.status)}
214 </span>
215 </div>
216
217 {droplet.ip_address && (
218 <div style={{ fontSize: '13px', fontFamily: 'monospace', color: 'var(--text-secondary)' }}>
219 {droplet.ip_address}
220 </div>
221 )}
222 </div>
223
224 {/* Metrics Section */}
225 {avgCpu && (
226 <div style={{ padding: '16px', background: 'var(--surface-2, #f8f9fa)', borderBottom: '1px solid var(--border)' }}>
227 <div style={{ display: 'flex', alignItems: 'center', gap: '12px', marginBottom: '8px' }}>
228 <span style={{ fontSize: '13px', color: 'var(--text-secondary)', fontWeight: '500' }}>CPU Usage (avg)</span>
229 <span style={{ fontSize: '16px', fontWeight: '600', color: 'var(--text-primary)', marginLeft: 'auto' }}>
230 {avgCpu}%
231 </span>
232 </div>
233 <div style={{ width: '100%', height: '6px', background: 'var(--border)', borderRadius: '3px', overflow: 'hidden' }}>
234 <div style={{
235 width: `${Math.min(avgCpu, 100)}%`,
236 height: '100%',
237 background: avgCpu > 80 ? 'var(--error)' : avgCpu > 50 ? 'var(--warning)' : 'var(--success)',
238 transition: 'width 0.3s ease'
239 }}></div>
240 </div>
241 </div>
242 )}
243
244 <div style={{ padding: '20px' }}>
245 <dl style={{ display: 'grid', gap: '12px', fontSize: '14px', margin: 0 }}>
246 <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
247 <dt style={{ color: 'var(--text-secondary)', fontWeight: '500' }}>Region</dt>
248 <dd style={{ margin: 0, fontWeight: '600', color: 'var(--text-primary)' }}>{droplet.region}</dd>
249 </div>
250 <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
251 <dt style={{ color: 'var(--text-secondary)', fontWeight: '500' }}>Size</dt>
252 <dd style={{ margin: 0, fontWeight: '600', color: 'var(--text-primary)' }}>{droplet.size}</dd>
253 </div>
254 {droplet.vcpus && (
255 <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
256 <dt style={{ color: 'var(--text-secondary)', fontWeight: '500' }}>Resources</dt>
257 <dd style={{ margin: 0, fontWeight: '600', color: 'var(--text-primary)' }}>
258 {droplet.vcpus} vCPU • {(droplet.memory / 1024).toFixed(0)} GB • {droplet.disk} GB
259 </dd>
260 </div>
261 )}
262 {droplet.image && (
263 <div>
264 <dt style={{ color: 'var(--text-secondary)', marginBottom: '4px' }}>Image</dt>
265 <dd style={{ margin: 0, fontWeight: '500', fontSize: '12px', background: 'var(--surface-1, #fff)', padding: '8px', borderRadius: '4px', border: '1px solid var(--border)' }}>
266 {droplet.image}
267 </dd>
268 </div>
269 )}
270 {droplet.tags && droplet.tags.length > 0 && (
271 <div>
272 <dt style={{ color: 'var(--text-secondary)', marginBottom: '4px' }}>Tags</dt>
273 <dd style={{ display: 'flex', flexWrap: 'wrap', gap: '4px', marginTop: '4px', margin: 0 }}>
274 {droplet.tags.map((tag, i) => (
275 <span key={i} className="badge badge-secondary" style={{ fontSize: '11px' }}>
276 {tag}
277 </span>
278 ))}
279 </dd>
280 </div>
281 )}
282 </dl>
283 </div>
284 </div>
285 );
286 })}
287 </div>
288 )}
289 </div>
290 );
291}