EverydayTech Platform - Developer Reference
Complete Source Code Documentation - All Applications
Loading...
Searching...
No Matches
DatabaseDetail.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 DatabaseDetail() {
8 const { id } = useParams();
9 const navigate = useNavigate();
10 const [database, setDatabase] = 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 [contracts, setContracts] = useState([]);
18 const [tenants, setTenants] = useState([]);
19
20 const [formData, setFormData] = useState({
21 customer_id: '',
22 contract_id: '',
23 assigned_tenant_id: ''
24 });
25
26 useEffect(() => {
27 fetchDatabaseDetail();
28 fetchCustomers();
29 fetchContracts();
30 fetchTenants();
31 }, [id]);
32
33 async function fetchDatabaseDetail() {
34 setLoading(true);
35 setError('');
36 try {
37 const res = await apiFetch(`/hosting/databases/${id}`);
38 if (!res.ok) throw new Error('Failed to load database');
39 const data = await res.json();
40 setDatabase(data.database);
41 setFormData({
42 customer_id: data.database.customer_id || '',
43 contract_id: data.database.contract_id || '',
44 assigned_tenant_id: data.database.assigned_tenant_id || ''
45 });
46
47 // Metrics are already cached in the database record
48 if (data.database) {
49 setMetrics({
50 cpu_count: data.database.cpu_count,
51 memory_mb: data.database.memory_mb,
52 disk_gb: data.database.disk_gb
53 });
54 }
55 } catch (err) {
56 console.error(err);
57 setError(err.message || 'Failed to load database');
58 } finally {
59 setLoading(false);
60 }
61 }
62
63 async function fetchCustomers() {
64 try {
65 const res = await apiFetch('/customers');
66 if (res.ok) {
67 const data = await res.json();
68 setCustomers(data.customers || []);
69 }
70 } catch (err) {
71 console.error('Error fetching customers:', err);
72 }
73 }
74
75 async function fetchContracts() {
76 try {
77 const res = await apiFetch('/contracts');
78 if (res.ok) {
79 const data = await res.json();
80 setContracts(data.contracts || []);
81 }
82 } catch (err) {
83 console.error('Error fetching contracts:', err);
84 }
85 }
86
87 async function fetchTenants() {
88 try {
89 const res = await apiFetch('/tenants');
90 if (res.ok) {
91 const data = await res.json();
92 setTenants(data.tenants || []);
93 }
94 } catch (err) {
95 console.error('Error fetching tenants:', err);
96 }
97 }
98
99 async function handleSubmit(e) {
100 e.preventDefault();
101 setSaving(true);
102 try {
103 const res = await apiFetch(`/hosting/databases/${id}`, {
104 method: 'PUT',
105 headers: { 'Content-Type': 'application/json' },
106 body: JSON.stringify(formData)
107 });
108
109 if (!res.ok) throw new Error('Failed to update database');
110
111 notifySuccess('Database updated successfully');
112 fetchDatabaseDetail();
113 } catch (err) {
114 console.error(err);
115 notifyError(err.message || 'Failed to update database');
116 } finally {
117 setSaving(false);
118 }
119 }
120
121 function handleInputChange(field, value) {
122 setFormData(prev => ({ ...prev, [field]: value }));
123 }
124
125 // Filter contracts based on selected customer and tenant
126 function getFilteredContracts() {
127 return contracts.filter(contract => {
128 // Filter by customer if one is selected
129 if (formData.customer_id && contract.customer_id !== parseInt(formData.customer_id)) {
130 return false;
131 }
132 // Filter by tenant if one is selected, otherwise show only root tenant contracts
133 if (formData.assigned_tenant_id) {
134 return contract.tenant_id === parseInt(formData.assigned_tenant_id);
135 } else {
136 // No tenant selected = show root tenant contracts (tenant_id is null)
137 return !contract.tenant_id;
138 }
139 });
140 }
141
142 function getStatusColor(status) {
143 const statusMap = {
144 'online': 'success',
145 'creating': 'info',
146 'offline': 'danger'
147 };
148 return statusMap[status?.toLowerCase()] || 'secondary';
149 }
150
151 if (loading) {
152 return (
153 <MainLayout>
154 <div style={{ padding: '40px', textAlign: 'center' }}>
155 <div className="spinner"></div>
156 </div>
157 </MainLayout>
158 );
159 }
160
161 if (error) {
162 return (
163 <MainLayout>
164 <div className="card" style={{ padding: '20px', margin: '20px', background: 'var(--error-bg)', border: '1px solid var(--error-border)' }}>
165 <div style={{ display: 'flex', alignItems: 'center', gap: '12px' }}>
166 <span className="material-symbols-outlined" style={{ color: 'var(--error-text)' }}>error</span>
167 <span style={{ color: 'var(--error-text)' }}>{error}</span>
168 </div>
169 </div>
170 </MainLayout>
171 );
172 }
173
174 if (!database) {
175 return (
176 <MainLayout>
177 <div className="card" style={{ padding: '20px', margin: '20px', textAlign: 'center' }}>
178 <p>Database not found</p>
179 </div>
180 </MainLayout>
181 );
182 }
183
184 return (
185 <MainLayout>
186 <div style={{ padding: '20px' }}>
187 {/* Header */}
188 <div style={{ display: 'flex', alignItems: 'center', gap: '12px', marginBottom: '20px' }}>
189 <button
190 onClick={() => navigate('/services?tab=databases')}
191 className="btn btn-secondary"
192 style={{ display: 'flex', alignItems: 'center', gap: '6px' }}
193 >
194 <span className="material-symbols-outlined">arrow_back</span>
195 Back
196 </button>
197 <h1 style={{ margin: 0, fontSize: '28px' }}>{database.database_name}</h1>
198 <span className={`badge badge-${getStatusColor(database.status)}`} style={{ textTransform: 'capitalize', fontSize: '14px' }}>
199 {database.status?.toLowerCase() === 'online' && <span style={{ marginRight: '4px' }}>●</span>}
200 {database.status}
201 </span>
202 </div>
203
204 <div style={{ display: 'grid', gap: '20px', gridTemplateColumns: '2fr 1fr' }}>
205 {/* Main Info Card */}
206 <div className="card" style={{ padding: '24px' }}>
207 <h2 style={{ marginBottom: '20px', fontSize: '20px' }}>Database Information</h2>
208
209 <dl style={{ display: 'grid', gap: '16px', fontSize: '15px' }}>
210 <div style={{ display: 'grid', gridTemplateColumns: '180px 1fr', gap: '12px' }}>
211 <dt style={{ color: 'var(--text-secondary)', fontWeight: '500' }}>Engine</dt>
212 <dd style={{ margin: 0, fontWeight: '600' }}>
213 {database.engine} {database.version}
214 </dd>
215 </div>
216
217 <div style={{ display: 'grid', gridTemplateColumns: '180px 1fr', gap: '12px' }}>
218 <dt style={{ color: 'var(--text-secondary)', fontWeight: '500' }}>Region</dt>
219 <dd style={{ margin: 0, fontWeight: '600' }}>{database.region}</dd>
220 </div>
221
222 <div style={{ display: 'grid', gridTemplateColumns: '180px 1fr', gap: '12px' }}>
223 <dt style={{ color: 'var(--text-secondary)', fontWeight: '500' }}>Size</dt>
224 <dd style={{ margin: 0, fontWeight: '600' }}>{database.size}</dd>
225 </div>
226
227 {database.num_nodes && (
228 <div style={{ display: 'grid', gridTemplateColumns: '180px 1fr', gap: '12px' }}>
229 <dt style={{ color: 'var(--text-secondary)', fontWeight: '500' }}>Nodes</dt>
230 <dd style={{ margin: 0, fontWeight: '600' }}>{database.num_nodes}</dd>
231 </div>
232 )}
233
234 {database.connection_host && (
235 <div style={{ display: 'grid', gridTemplateColumns: '180px 1fr', gap: '12px' }}>
236 <dt style={{ color: 'var(--text-secondary)', fontWeight: '500' }}>Connection Host</dt>
237 <dd style={{ margin: 0, fontFamily: 'monospace', fontSize: '13px', background: 'var(--surface-2, #f8f9fa)', padding: '8px 12px', borderRadius: '4px', border: '1px solid var(--border)' }}>
238 {database.connection_host}:{database.connection_port}
239 </dd>
240 </div>
241 )}
242
243 {database.connection_database && (
244 <div style={{ display: 'grid', gridTemplateColumns: '180px 1fr', gap: '12px' }}>
245 <dt style={{ color: 'var(--text-secondary)', fontWeight: '500' }}>Default Database</dt>
246 <dd style={{ margin: 0, fontFamily: 'monospace', fontWeight: '600' }}>
247 {database.connection_database}
248 </dd>
249 </div>
250 )}
251
252 {database.tags && database.tags.length > 0 && (
253 <div style={{ display: 'grid', gridTemplateColumns: '180px 1fr', gap: '12px' }}>
254 <dt style={{ color: 'var(--text-secondary)', fontWeight: '500' }}>Tags</dt>
255 <dd style={{ display: 'flex', flexWrap: 'wrap', gap: '6px', margin: 0 }}>
256 {database.tags.map((tag, i) => (
257 <span key={i} className="badge badge-secondary">
258 {tag}
259 </span>
260 ))}
261 </dd>
262 </div>
263 )}
264 </dl>
265 </div>
266
267 {/* Metrics Card */}
268 {metrics && (
269 <div className="card" style={{ padding: '24px' }}>
270 <h2 style={{ marginBottom: '20px', fontSize: '20px' }}>Resources</h2>
271
272 <div style={{ display: 'grid', gap: '20px' }}>
273 <div style={{ textAlign: 'center', padding: '20px', background: 'var(--surface-2, #f8f9fa)', borderRadius: '8px' }}>
274 <div style={{ fontSize: '13px', color: 'var(--text-secondary)', marginBottom: '8px', fontWeight: '500' }}>
275 CPU Cores
276 </div>
277 <div style={{ fontSize: '36px', fontWeight: '700', color: 'var(--text-primary)' }}>
278 {metrics.cpu_count || 'N/A'}
279 </div>
280 </div>
281
282 <div style={{ textAlign: 'center', padding: '20px', background: 'var(--surface-2, #f8f9fa)', borderRadius: '8px' }}>
283 <div style={{ fontSize: '13px', color: 'var(--text-secondary)', marginBottom: '8px', fontWeight: '500' }}>
284 Memory
285 </div>
286 <div style={{ fontSize: '36px', fontWeight: '700', color: 'var(--text-primary)' }}>
287 {metrics.memory_mb ? `${(metrics.memory_mb / 1024).toFixed(1)}` : 'N/A'}
288 </div>
289 <div style={{ fontSize: '14px', color: 'var(--text-secondary)', marginTop: '4px' }}>GB</div>
290 </div>
291
292 <div style={{ textAlign: 'center', padding: '20px', background: 'var(--surface-2, #f8f9fa)', borderRadius: '8px' }}>
293 <div style={{ fontSize: '13px', color: 'var(--text-secondary)', marginBottom: '8px', fontWeight: '500' }}>
294 Disk Storage
295 </div>
296 <div style={{ fontSize: '36px', fontWeight: '700', color: 'var(--text-primary)' }}>
297 {metrics.disk_gb || 'N/A'}
298 </div>
299 <div style={{ fontSize: '14px', color: 'var(--text-secondary)', marginTop: '4px' }}>GB</div>
300 </div>
301 </div>
302 </div>
303 )}
304 </div>
305
306 {/* Assignment Form */}
307 <div className="card" style={{ padding: '24px', marginTop: '20px' }}>
308 <h2 style={{ marginBottom: '20px', fontSize: '20px' }}>Assignment</h2>
309
310 <form onSubmit={handleSubmit}>
311 <div style={{ display: 'grid', gap: '20px', gridTemplateColumns: 'repeat(auto-fit, minmax(250px, 1fr))' }}>
312 <div>
313 <label style={{ display: 'block', marginBottom: '8px', fontWeight: '500' }}>
314 Customer
315 </label>
316 <select
317 value={formData.customer_id}
318 onChange={(e) => handleInputChange('customer_id', e.target.value)}
319 className="input"
320 style={{ width: '100%' }}
321 >
322 <option value="">-- Select Customer --</option>
323 {customers.map(c => (
324 <option key={c.customer_id} value={c.customer_id}>
325 {c.customer_name}
326 </option>
327 ))}
328 </select>
329 {database.customer_name && (
330 <div style={{ marginTop: '6px', fontSize: '13px', color: 'var(--text-secondary)' }}>
331 Current: {database.customer_name}
332 </div>
333 )}
334 </div>
335
336 <div>
337 <label style={{ display: 'block', marginBottom: '8px', fontWeight: '500' }}>
338 Contract
339 </label>
340 <select
341 value={formData.contract_id}
342 onChange={(e) => handleInputChange('contract_id', e.target.value)}
343 className="input"
344 style={{ width: '100%', color: 'var(--text)' }}
345 >
346 <option value="">-- Select Contract --</option>
347 {getFilteredContracts().map(c => (
348 <option key={c.contract_id} value={c.contract_id}>
349 {c.title || c.contract_name} {c.customer_name ? `- ${c.customer_name}` : ''}
350 </option>
351 ))}
352 </select>
353 </div>
354
355 <div>
356 <label style={{ display: 'block', marginBottom: '8px', fontWeight: '500' }}>
357 Tenant
358 </label>
359 <select
360 value={formData.assigned_tenant_id}
361 onChange={(e) => handleInputChange('assigned_tenant_id', e.target.value)}
362 className="input"
363 style={{ width: '100%', color: 'var(--text)' }}
364 >
365 <option value="">-- Select Tenant --</option>
366 {tenants.map(t => (
367 <option key={t.tenant_id} value={t.tenant_id}>
368 {t.tenant_name}
369 </option>
370 ))}
371 </select>
372 </div>
373 </div>
374
375 <div style={{ marginTop: '24px', display: 'flex', gap: '12px' }}>
376 <button type="submit" disabled={saving} className="btn btn-primary">
377 {saving ? 'Saving...' : 'Save Changes'}
378 </button>
379 <button
380 type="button"
381 onClick={() => navigate('/services?tab=databases')}
382 className="btn btn-secondary"
383 >
384 Cancel
385 </button>
386 </div>
387 </form>
388 </div>
389 </div>
390 </MainLayout>
391 );
392}
393
394export default DatabaseDetail;