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';
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);
16 const [customers, setCustomers] = useState([]);
17 const [contracts, setContracts] = useState([]);
18 const [tenants, setTenants] = useState([]);
20 const [formData, setFormData] = useState({
23 assigned_tenant_id: ''
27 fetchDatabaseDetail();
33 async function fetchDatabaseDetail() {
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);
42 customer_id: data.database.customer_id || '',
43 contract_id: data.database.contract_id || '',
44 assigned_tenant_id: data.database.assigned_tenant_id || ''
47 // Metrics are already cached in the database record
50 cpu_count: data.database.cpu_count,
51 memory_mb: data.database.memory_mb,
52 disk_gb: data.database.disk_gb
57 setError(err.message || 'Failed to load database');
63 async function fetchCustomers() {
65 const res = await apiFetch('/customers');
67 const data = await res.json();
68 setCustomers(data.customers || []);
71 console.error('Error fetching customers:', err);
75 async function fetchContracts() {
77 const res = await apiFetch('/contracts');
79 const data = await res.json();
80 setContracts(data.contracts || []);
83 console.error('Error fetching contracts:', err);
87 async function fetchTenants() {
89 const res = await apiFetch('/tenants');
91 const data = await res.json();
92 setTenants(data.tenants || []);
95 console.error('Error fetching tenants:', err);
99 async function handleSubmit(e) {
103 const res = await apiFetch(`/hosting/databases/${id}`, {
105 headers: { 'Content-Type': 'application/json' },
106 body: JSON.stringify(formData)
109 if (!res.ok) throw new Error('Failed to update database');
111 notifySuccess('Database updated successfully');
112 fetchDatabaseDetail();
115 notifyError(err.message || 'Failed to update database');
121 function handleInputChange(field, value) {
122 setFormData(prev => ({ ...prev, [field]: value }));
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)) {
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);
136 // No tenant selected = show root tenant contracts (tenant_id is null)
137 return !contract.tenant_id;
142 function getStatusColor(status) {
148 return statusMap[status?.toLowerCase()] || 'secondary';
154 <div style={{ padding: '40px', textAlign: 'center' }}>
155 <div className="spinner"></div>
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>
177 <div className="card" style={{ padding: '20px', margin: '20px', textAlign: 'center' }}>
178 <p>Database not found</p>
186 <div style={{ padding: '20px' }}>
188 <div style={{ display: 'flex', alignItems: 'center', gap: '12px', marginBottom: '20px' }}>
190 onClick={() => navigate('/services?tab=databases')}
191 className="btn btn-secondary"
192 style={{ display: 'flex', alignItems: 'center', gap: '6px' }}
194 <span className="material-symbols-outlined">arrow_back</span>
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>}
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>
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}
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>
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>
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>
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}
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}
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">
269 <div className="card" style={{ padding: '24px' }}>
270 <h2 style={{ marginBottom: '20px', fontSize: '20px' }}>Resources</h2>
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' }}>
277 <div style={{ fontSize: '36px', fontWeight: '700', color: 'var(--text-primary)' }}>
278 {metrics.cpu_count || 'N/A'}
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' }}>
286 <div style={{ fontSize: '36px', fontWeight: '700', color: 'var(--text-primary)' }}>
287 {metrics.memory_mb ? `${(metrics.memory_mb / 1024).toFixed(1)}` : 'N/A'}
289 <div style={{ fontSize: '14px', color: 'var(--text-secondary)', marginTop: '4px' }}>GB</div>
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' }}>
296 <div style={{ fontSize: '36px', fontWeight: '700', color: 'var(--text-primary)' }}>
297 {metrics.disk_gb || 'N/A'}
299 <div style={{ fontSize: '14px', color: 'var(--text-secondary)', marginTop: '4px' }}>GB</div>
306 {/* Assignment Form */}
307 <div className="card" style={{ padding: '24px', marginTop: '20px' }}>
308 <h2 style={{ marginBottom: '20px', fontSize: '20px' }}>Assignment</h2>
310 <form onSubmit={handleSubmit}>
311 <div style={{ display: 'grid', gap: '20px', gridTemplateColumns: 'repeat(auto-fit, minmax(250px, 1fr))' }}>
313 <label style={{ display: 'block', marginBottom: '8px', fontWeight: '500' }}>
317 value={formData.customer_id}
318 onChange={(e) => handleInputChange('customer_id', e.target.value)}
320 style={{ width: '100%' }}
322 <option value="">-- Select Customer --</option>
323 {customers.map(c => (
324 <option key={c.customer_id} value={c.customer_id}>
329 {database.customer_name && (
330 <div style={{ marginTop: '6px', fontSize: '13px', color: 'var(--text-secondary)' }}>
331 Current: {database.customer_name}
337 <label style={{ display: 'block', marginBottom: '8px', fontWeight: '500' }}>
341 value={formData.contract_id}
342 onChange={(e) => handleInputChange('contract_id', e.target.value)}
344 style={{ width: '100%', color: 'var(--text)' }}
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}` : ''}
356 <label style={{ display: 'block', marginBottom: '8px', fontWeight: '500' }}>
360 value={formData.assigned_tenant_id}
361 onChange={(e) => handleInputChange('assigned_tenant_id', e.target.value)}
363 style={{ width: '100%', color: 'var(--text)' }}
365 <option value="">-- Select Tenant --</option>
367 <option key={t.tenant_id} value={t.tenant_id}>
375 <div style={{ marginTop: '24px', display: 'flex', gap: '12px' }}>
376 <button type="submit" disabled={saving} className="btn btn-primary">
377 {saving ? 'Saving...' : 'Save Changes'}
381 onClick={() => navigate('/services?tab=databases')}
382 className="btn btn-secondary"
394export default DatabaseDetail;