EverydayTech Platform - Developer Reference
Complete Source Code Documentation - All Applications
Loading...
Searching...
No Matches
TabDatabases.jsx
Go to the documentation of this file.
1import { useState, useEffect } from 'react';
2import { Link } from 'react-router-dom';
3import { apiFetch } from '../../lib/api';
4
5export default function TabDatabases() {
6 const [databases, setDatabases] = useState([]);
7 const [metrics, setMetrics] = useState({});
8 const [loading, setLoading] = useState(true);
9 const [error, setError] = useState('');
10 const [syncing, setSyncing] = useState(false);
11 const [lastSync, setLastSync] = useState(null);
12
13 useEffect(() => {
14 fetchDatabases();
15 }, []);
16
17 const fetchDatabases = async () => {
18 setLoading(true);
19 setError('');
20 try {
21 const res = await apiFetch('/hosting/databases');
22 if (res.ok) {
23 const data = await res.json();
24 setDatabases(data.databases || []);
25 setLastSync(data.lastSync);
26
27 // Metrics are already cached in the database records
28 const metricsData = {};
29 for (const db of (data.databases || [])) {
30 metricsData[db.do_database_id] = {
31 cpu_count: db.cpu_count,
32 memory_mb: db.memory_mb,
33 disk_gb: db.disk_gb
34 };
35 }
36 setMetrics(metricsData);
37 } else {
38 const errorData = await res.json();
39 setError(errorData.error || 'Failed to fetch databases');
40 }
41 } catch (err) {
42 console.error('Error fetching databases:', err);
43 setError(err.message || 'Failed to load databases');
44 } finally {
45 setLoading(false);
46 }
47 };
48
49 const handleSync = async () => {
50 setSyncing(true);
51 setError('');
52 try {
53 const res = await apiFetch('/hosting/sync', { method: 'POST' });
54 if (res.ok) {
55 await fetchDatabases();
56 } else {
57 const errorData = await res.json();
58 setError(errorData.error || 'Failed to sync');
59 }
60 } catch (err) {
61 console.error('Error syncing databases:', err);
62 setError(err.message || 'Failed to sync databases');
63 } finally {
64 setSyncing(false);
65 }
66 };
67
68 const getStatusColor = (status) => {
69 const statusMap = {
70 'online': 'success',
71 'creating': 'info',
72 'offline': 'secondary',
73 'resizing': 'warning'
74 };
75 return statusMap[status?.toLowerCase()] || 'secondary';
76 };
77
78 const formatTime = (date) => {
79 if (!date) return '';
80 const now = new Date();
81 const then = new Date(date);
82 const diff = Math.floor((now - then) / 1000);
83
84 if (diff < 60) return `${diff} seconds ago`;
85 if (diff < 3600) return `${Math.floor(diff / 60)} minutes ago`;
86 if (diff < 86400) return `${Math.floor(diff / 3600)} hours ago`;
87 return `${Math.floor(diff / 86400)} days ago`;
88 };
89
90 if (loading) {
91 return (
92 <div style={{ padding: '40px', textAlign: 'center' }}>
93 <div className="spinner"></div>
94 </div>
95 );
96 }
97
98 return (
99 <div style={{ padding: '20px' }}>
100 <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '20px' }}>
101 <div>
102 <h2>Databases</h2>
103 <p style={{ color: 'var(--text-secondary)', marginTop: '8px' }}>
104 Manage your DigitalOcean managed databases
105 </p>
106 </div>
107 <button
108 onClick={handleSync}
109 disabled={syncing}
110 className="btn btn-primary"
111 style={{ display: 'flex', alignItems: 'center', gap: '8px' }}
112 >
113 <span className="material-symbols-outlined" style={{ animation: syncing ? 'spin 1s linear infinite' : 'none' }}>
114 refresh
115 </span>
116 {syncing ? 'Syncing...' : 'Sync Now'}
117 </button>
118 </div>
119
120 <div className="card" style={{ padding: '16px', marginBottom: '20px', background: 'var(--info-bg)', border: '1px solid var(--info-border)' }}>
121 <div style={{ display: 'flex', alignItems: 'center', gap: '12px' }}>
122 <span className="material-symbols-outlined" style={{ color: 'var(--info-text)' }}>
123 schedule
124 </span>
125 <div style={{ fontSize: '14px', color: 'var(--info-text)' }}>
126 Data refreshes every 5 minutes from DigitalOcean.
127 {lastSync && (
128 <span style={{ marginLeft: '8px', opacity: 0.8 }}>
129 Last synced {formatTime(lastSync)}
130 </span>
131 )}
132 </div>
133 </div>
134 </div>
135
136 {error && (
137 <div className="card" style={{ padding: '16px', marginBottom: '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)' }}>
140 error
141 </span>
142 <span style={{ fontSize: '14px', color: 'var(--error-text)' }}>{error}</span>
143 </div>
144 </div>
145 )}
146
147 {databases.length === 0 ? (
148 <div className="card" style={{ padding: '60px 20px', textAlign: 'center' }}>
149 <span className="material-symbols-outlined" style={{ fontSize: '64px', marginBottom: '16px', display: 'block', opacity: 0.5, color: 'var(--text-secondary)' }}>
150 storage
151 </span>
152 <p style={{ color: 'var(--text-secondary)' }}>No databases found</p>
153 <p style={{ fontSize: '14px', color: 'var(--text-tertiary)', marginTop: '8px' }}>
154 Create a managed database in your DigitalOcean account
155 </p>
156 </div>
157 ) : (
158 <div style={{ display: 'grid', gap: '20px', gridTemplateColumns: 'repeat(auto-fill, minmax(350px, 1fr))' }}>
159 {databases.map((db) => {
160 const dbMetrics = metrics[db.do_database_id];
161 const isOnline = db.status?.toLowerCase() === 'online';
162
163 return (
164 <Link
165 key={db.database_id}
166 to={`/hosting/databases/${db.database_id}`}
167 className="card"
168 style={{
169 overflow: 'hidden',
170 textDecoration: 'none',
171 color: 'inherit',
172 display: 'block',
173 transition: 'transform 0.2s, box-shadow 0.2s',
174 cursor: 'pointer'
175 }}
176 onMouseEnter={(e) => {
177 e.currentTarget.style.transform = 'translateY(-2px)';
178 e.currentTarget.style.boxShadow = '0 4px 12px rgba(0,0,0,0.15)';
179 }}
180 onMouseLeave={(e) => {
181 e.currentTarget.style.transform = 'translateY(0)';
182 e.currentTarget.style.boxShadow = '';
183 }}
184 >
185 <div style={{ padding: '20px', borderBottom: '1px solid var(--border)' }}>
186 <div style={{ display: 'flex', alignItems: 'flex-start', justifyContent: 'space-between', marginBottom: '12px' }}>
187 <div style={{ flex: 1 }}>
188 <h3 style={{ fontSize: '18px', marginBottom: '6px', fontWeight: '600' }}>{db.database_name}</h3>
189 <p style={{ fontSize: '13px', color: 'var(--text-secondary)' }}>
190 {db.customer_name || 'Unassigned'}
191 </p>
192 </div>
193 <span className={`badge badge-${getStatusColor(db.status)}`} style={{ textTransform: 'capitalize' }}>
194 {isOnline && <span style={{ marginRight: '4px' }}>●</span>}
195 {db.status}
196 </span>
197 </div>
198
199 <div style={{ fontSize: '14px', color: 'var(--text-secondary)' }}>
200 <strong>{db.engine}</strong> {db.version}
201 </div>
202 </div>
203
204 {/* Metrics Section */}
205 {dbMetrics && (
206 <div style={{ padding: '16px', background: 'var(--surface-2, #f8f9fa)', borderBottom: '1px solid var(--border)' }}>
207 <div style={{ display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: '12px', fontSize: '13px' }}>
208 <div style={{ textAlign: 'center' }}>
209 <div style={{ color: 'var(--text-secondary)', marginBottom: '4px' }}>CPU Cores</div>
210 <div style={{ fontSize: '18px', fontWeight: '600', color: 'var(--text-primary)' }}>
211 {dbMetrics.cpu_count || 'N/A'}
212 </div>
213 </div>
214 <div style={{ textAlign: 'center' }}>
215 <div style={{ color: 'var(--text-secondary)', marginBottom: '4px' }}>Memory</div>
216 <div style={{ fontSize: '18px', fontWeight: '600', color: 'var(--text-primary)' }}>
217 {dbMetrics.memory_mb ? `${(dbMetrics.memory_mb / 1024).toFixed(1)} GB` : 'N/A'}
218 </div>
219 </div>
220 <div style={{ textAlign: 'center' }}>
221 <div style={{ color: 'var(--text-secondary)', marginBottom: '4px' }}>Disk</div>
222 <div style={{ fontSize: '18px', fontWeight: '600', color: 'var(--text-primary)' }}>
223 {dbMetrics.disk_gb ? `${dbMetrics.disk_gb} GB` : 'N/A'}
224 </div>
225 </div>
226 </div>
227 </div>
228 )}
229
230 <div style={{ padding: '20px' }}>
231 <dl style={{ display: 'grid', gap: '12px', fontSize: '14px', margin: 0 }}>
232 <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
233 <dt style={{ color: 'var(--text-secondary)', fontWeight: '500' }}>Region</dt>
234 <dd style={{ margin: 0, fontWeight: '600', color: 'var(--text-primary)' }}>{db.region}</dd>
235 </div>
236 <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
237 <dt style={{ color: 'var(--text-secondary)', fontWeight: '500' }}>Size</dt>
238 <dd style={{ margin: 0, fontWeight: '600', color: 'var(--text-primary)' }}>{db.size}</dd>
239 </div>
240 {db.num_nodes && (
241 <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
242 <dt style={{ color: 'var(--text-secondary)', fontWeight: '500' }}>Nodes</dt>
243 <dd style={{ margin: 0, fontWeight: '600', color: 'var(--text-primary)' }}>{db.num_nodes}</dd>
244 </div>
245 )}
246 {db.connection_host && (
247 <div>
248 <dt style={{ color: 'var(--text-secondary)', marginBottom: '4px' }}>Connection</dt>
249 <dd style={{ margin: 0, fontWeight: '500', fontFamily: 'monospace', fontSize: '12px', wordBreak: 'break-all', background: 'var(--surface-1, #fff)', padding: '8px', borderRadius: '4px', border: '1px solid var(--border)' }}>
250 {db.connection_host}:{db.connection_port}
251 </dd>
252 </div>
253 )}
254 {db.tags && db.tags.length > 0 && (
255 <div>
256 <dt style={{ color: 'var(--text-secondary)', marginBottom: '4px' }}>Tags</dt>
257 <dd style={{ display: 'flex', flexWrap: 'wrap', gap: '4px', marginTop: '4px', margin: 0 }}>
258 {db.tags.map((tag, i) => (
259 <span key={i} className="badge badge-secondary" style={{ fontSize: '11px' }}>
260 {tag}
261 </span>
262 ))}
263 </dd>
264 </div>
265 )}
266 </dl>
267 </div>
268 </Link>
269 );
270 })}
271 </div>
272 )}
273 </div>
274 );
275}