1import { useState, useEffect } from 'react';
2import { Link } from 'react-router-dom';
3import { apiFetch } from '../../lib/api';
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);
17 const fetchDatabases = async () => {
21 const res = await apiFetch('/hosting/databases');
23 const data = await res.json();
24 setDatabases(data.databases || []);
25 setLastSync(data.lastSync);
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,
36 setMetrics(metricsData);
38 const errorData = await res.json();
39 setError(errorData.error || 'Failed to fetch databases');
42 console.error('Error fetching databases:', err);
43 setError(err.message || 'Failed to load databases');
49 const handleSync = async () => {
53 const res = await apiFetch('/hosting/sync', { method: 'POST' });
55 await fetchDatabases();
57 const errorData = await res.json();
58 setError(errorData.error || 'Failed to sync');
61 console.error('Error syncing databases:', err);
62 setError(err.message || 'Failed to sync databases');
68 const getStatusColor = (status) => {
72 'offline': 'secondary',
75 return statusMap[status?.toLowerCase()] || 'secondary';
78 const formatTime = (date) => {
80 const now = new Date();
81 const then = new Date(date);
82 const diff = Math.floor((now - then) / 1000);
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`;
92 <div style={{ padding: '40px', textAlign: 'center' }}>
93 <div className="spinner"></div>
99 <div style={{ padding: '20px' }}>
100 <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '20px' }}>
103 <p style={{ color: 'var(--text-secondary)', marginTop: '8px' }}>
104 Manage your DigitalOcean managed databases
110 className="btn btn-primary"
111 style={{ display: 'flex', alignItems: 'center', gap: '8px' }}
113 <span className="material-symbols-outlined" style={{ animation: syncing ? 'spin 1s linear infinite' : 'none' }}>
116 {syncing ? 'Syncing...' : 'Sync Now'}
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)' }}>
125 <div style={{ fontSize: '14px', color: 'var(--info-text)' }}>
126 Data refreshes every 5 minutes from DigitalOcean.
128 <span style={{ marginLeft: '8px', opacity: 0.8 }}>
129 Last synced {formatTime(lastSync)}
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)' }}>
142 <span style={{ fontSize: '14px', color: 'var(--error-text)' }}>{error}</span>
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)' }}>
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
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';
166 to={`/hosting/databases/${db.database_id}`}
170 textDecoration: 'none',
173 transition: 'transform 0.2s, box-shadow 0.2s',
176 onMouseEnter={(e) => {
177 e.currentTarget.style.transform = 'translateY(-2px)';
178 e.currentTarget.style.boxShadow = '0 4px 12px rgba(0,0,0,0.15)';
180 onMouseLeave={(e) => {
181 e.currentTarget.style.transform = 'translateY(0)';
182 e.currentTarget.style.boxShadow = '';
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'}
193 <span className={`badge badge-${getStatusColor(db.status)}`} style={{ textTransform: 'capitalize' }}>
194 {isOnline && <span style={{ marginRight: '4px' }}>●</span>}
199 <div style={{ fontSize: '14px', color: 'var(--text-secondary)' }}>
200 <strong>{db.engine}</strong> {db.version}
204 {/* Metrics Section */}
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'}
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'}
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'}
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>
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>
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>
246 {db.connection_host && (
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}
254 {db.tags && db.tags.length > 0 && (
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' }}>