EverydayTech Platform - Developer Reference
Complete Source Code Documentation - All Applications
Loading...
Searching...
No Matches
HostingAppDetail.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 HostingAppDetail() {
8 const { id } = useParams();
9 const navigate = useNavigate();
10 const [app, setApp] = useState(null);
11 const [loading, setLoading] = useState(true);
12 const [error, setError] = useState('');
13 const [saving, setSaving] = useState(false);
14
15 const [customers, setCustomers] = useState([]);
16 const [contracts, setContracts] = useState([]);
17 const [tenants, setTenants] = useState([]);
18
19 const [formData, setFormData] = useState({
20 customer_id: '',
21 contract_id: '',
22 assigned_tenant_id: ''
23 });
24
25 useEffect(() => {
26 fetchAppDetail();
27 fetchCustomers();
28 fetchContracts();
29 fetchTenants();
30 }, [id]);
31
32 async function fetchAppDetail() {
33 setLoading(true);
34 setError('');
35 try {
36 const res = await apiFetch(`/hosting/apps/${id}`);
37 if (!res.ok) throw new Error('Failed to load app');
38 const data = await res.json();
39 setApp(data.app);
40 setFormData({
41 customer_id: data.app.customer_id || '',
42 contract_id: data.app.contract_id || '',
43 assigned_tenant_id: data.app.assigned_tenant_id || ''
44 });
45 } catch (err) {
46 console.error(err);
47 setError(err.message || 'Failed to load app');
48 } finally {
49 setLoading(false);
50 }
51 }
52
53 async function fetchCustomers() {
54 try {
55 const res = await apiFetch('/customers');
56 if (res.ok) {
57 const data = await res.json();
58 setCustomers(data.customers || []);
59 }
60 } catch (err) {
61 console.error('Error fetching customers:', err);
62 }
63 }
64
65 async function fetchContracts() {
66 try {
67 const res = await apiFetch('/contracts');
68 if (res.ok) {
69 const data = await res.json();
70 setContracts(data.contracts || []);
71 }
72 } catch (err) {
73 console.error('Error fetching contracts:', err);
74 }
75 }
76
77 async function fetchTenants() {
78 try {
79 const res = await apiFetch('/tenants');
80 if (res.ok) {
81 const data = await res.json();
82 setTenants(data.tenants || []);
83 }
84 } catch (err) {
85 console.error('Error fetching tenants:', err);
86 // Not root user, ignore
87 }
88 }
89
90 async function handleSubmit(e) {
91 e.preventDefault();
92 setSaving(true);
93 try {
94 const res = await apiFetch(`/hosting/apps/${id}`, {
95 method: 'PUT',
96 headers: { 'Content-Type': 'application/json' },
97 body: JSON.stringify(formData)
98 });
99
100 if (!res.ok) throw new Error('Failed to update app');
101
102 await notifySuccess('App Updated', 'Hosting app updated successfully');
103 fetchAppDetail();
104 } catch (err) {
105 console.error(err);
106 await notifyError('Update Failed', err.message || 'Failed to update app');
107 } finally {
108 setSaving(false);
109 }
110 }
111
112 // Filter contracts based on selected customer and tenant
113 function getFilteredContracts() {
114 return contracts.filter(contract => {
115 // Filter by customer if one is selected
116 if (formData.customer_id && contract.customer_id !== parseInt(formData.customer_id)) {
117 return false;
118 }
119 // Filter by tenant if one is selected, otherwise show only root tenant contracts
120 if (formData.assigned_tenant_id) {
121 return contract.tenant_id === parseInt(formData.assigned_tenant_id);
122 } else {
123 // No tenant selected = show root tenant contracts (tenant_id is null)
124 return !contract.tenant_id;
125 }
126 });
127 }
128
129 if (loading) {
130 return (
131 <MainLayout>
132 <div className="page-content">
133 <div className="loading">Loading app...</div>
134 </div>
135 </MainLayout>
136 );
137 }
138
139 if (error) {
140 return (
141 <MainLayout>
142 <div className="page-content">
143 <div className="error-message">{error}</div>
144 </div>
145 </MainLayout>
146 );
147 }
148
149 if (!app) return null;
150
151 return (
152 <MainLayout>
153 <div className="page-content">
154 <div style={{ marginBottom: '24px', display: 'flex', alignItems: 'center', gap: '16px' }}>
155 <button
156 onClick={() => navigate('/services')}
157 className="btn"
158 style={{ display: 'flex', alignItems: 'center', gap: '8px' }}
159 >
160 <span className="material-symbols-outlined">arrow_back</span>
161 Back to Services
162 </button>
163 <h1 style={{ margin: 0, fontSize: '28px' }}>{app.app_name}</h1>
164 <span className={`badge badge-${getStatusColor(app.status)}`} style={{ marginLeft: 'auto' }}>
165 {getStatusLabel(app.status)}
166 </span>
167 </div>
168
169 <div style={{ display: 'grid', gap: '24px', gridTemplateColumns: '1fr 1fr' }}>
170 {/* App Information Card */}
171 <div className="card">
172 <div style={{ padding: '20px', borderBottom: '1px solid var(--border)' }}>
173 <h2 style={{ margin: 0, fontSize: '20px', fontWeight: '600' }}>App Information</h2>
174 </div>
175 <div style={{ padding: '20px' }}>
176 <dl style={{ display: 'grid', gap: '16px', fontSize: '14px' }}>
177 <div>
178 <dt style={{ color: 'var(--text-secondary)', marginBottom: '4px' }}>App ID</dt>
179 <dd style={{ fontWeight: '500', fontFamily: 'monospace' }}>{app.app_id}</dd>
180 </div>
181 <div>
182 <dt style={{ color: 'var(--text-secondary)', marginBottom: '4px' }}>DO App ID</dt>
183 <dd style={{ fontWeight: '500', fontFamily: 'monospace' }}>{app.do_app_id}</dd>
184 </div>
185 <div>
186 <dt style={{ color: 'var(--text-secondary)', marginBottom: '4px' }}>Region</dt>
187 <dd style={{ fontWeight: '500', textTransform: 'uppercase' }}>{app.region}</dd>
188 </div>
189 <div>
190 <dt style={{ color: 'var(--text-secondary)', marginBottom: '4px' }}>Type</dt>
191 <dd style={{ fontWeight: '500', textTransform: 'capitalize' }}>{app.app_type}</dd>
192 </div>
193 {app.default_domain && (
194 <div>
195 <dt style={{ color: 'var(--text-secondary)', marginBottom: '4px' }}>Default Domain</dt>
196 <dd style={{ fontWeight: '500' }}>
197 <a
198 href={`https://${app.default_domain}`}
199 target="_blank"
200 rel="noopener noreferrer"
201 style={{ color: 'var(--primary)', textDecoration: 'none' }}
202 >
203 {app.default_domain}
204 </a>
205 </dd>
206 </div>
207 )}
208 {app.live_url && (
209 <div>
210 <dt style={{ color: 'var(--text-secondary)', marginBottom: '4px' }}>Live URL</dt>
211 <dd style={{ fontWeight: '500' }}>
212 <a
213 href={app.live_url}
214 target="_blank"
215 rel="noopener noreferrer"
216 style={{ color: 'var(--primary)', textDecoration: 'none', wordBreak: 'break-all' }}
217 >
218 {app.live_url}
219 </a>
220 </dd>
221 </div>
222 )}
223 <div>
224 <dt style={{ color: 'var(--text-secondary)', marginBottom: '4px' }}>Created</dt>
225 <dd style={{ fontWeight: '500' }}>{new Date(app.created_at).toLocaleString()}</dd>
226 </div>
227 <div>
228 <dt style={{ color: 'var(--text-secondary)', marginBottom: '4px' }}>Last Updated</dt>
229 <dd style={{ fontWeight: '500' }}>{new Date(app.updated_at).toLocaleString()}</dd>
230 </div>
231 </dl>
232 </div>
233 </div>
234
235 {/* WordPress Credentials Card - Only show for WordPress apps */}
236 {app.wordpress && (
237 <div className="card">
238 <div style={{ padding: '20px', borderBottom: '1px solid var(--border)' }}>
239 <h2 style={{ margin: 0, fontSize: '20px', fontWeight: '600', display: 'flex', alignItems: 'center', gap: '8px' }}>
240 <span className="material-symbols-outlined" style={{ color: 'var(--primary)' }}>lock</span>
241 WordPress Credentials
242 </h2>
243 </div>
244 <div style={{ padding: '20px' }}>
245 <div style={{
246 padding: '12px 16px',
247 background: 'var(--warning-bg)',
248 border: '1px solid var(--warning-border)',
249 borderRadius: '4px',
250 marginBottom: '20px',
251 fontSize: '13px',
252 color: 'var(--warning-text)',
253 display: 'flex',
254 alignItems: 'center',
255 gap: '8px'
256 }}>
257 <span className="material-symbols-outlined" style={{ fontSize: '18px' }}>info</span>
258 <span>Keep these credentials secure. They provide admin access to your WordPress site.</span>
259 </div>
260
261 <dl style={{ display: 'grid', gap: '16px', fontSize: '14px' }}>
262 <div>
263 <dt style={{ color: 'var(--text-secondary)', marginBottom: '4px' }}>Admin Username</dt>
264 <dd style={{
265 fontWeight: '500',
266 fontFamily: 'monospace',
267 background: 'var(--input-bg)',
268 padding: '8px 12px',
269 borderRadius: '4px',
270 display: 'flex',
271 alignItems: 'center',
272 justifyContent: 'space-between'
273 }}>
274 <span>{app.wordpress.wp_admin_user}</span>
275 <button
276 onClick={() => {
277 navigator.clipboard.writeText(app.wordpress.wp_admin_user);
278 notifySuccess('Copied', 'Username copied to clipboard');
279 }}
280 className="btn btn-sm"
281 style={{ padding: '4px 8px', fontSize: '12px' }}
282 >
283 <span className="material-symbols-outlined" style={{ fontSize: '16px' }}>content_copy</span>
284 </button>
285 </dd>
286 </div>
287
288 <div>
289 <dt style={{ color: 'var(--text-secondary)', marginBottom: '4px' }}>Admin Password</dt>
290 <dd style={{
291 fontWeight: '500',
292 fontFamily: 'monospace',
293 background: 'var(--input-bg)',
294 padding: '8px 12px',
295 borderRadius: '4px',
296 display: 'flex',
297 alignItems: 'center',
298 justifyContent: 'space-between'
299 }}>
300 <span style={{ letterSpacing: '0.1em' }}>{app.wordpress.wp_admin_password}</span>
301 <button
302 onClick={() => {
303 navigator.clipboard.writeText(app.wordpress.wp_admin_password);
304 notifySuccess('Copied', 'Password copied to clipboard');
305 }}
306 className="btn btn-sm"
307 style={{ padding: '4px 8px', fontSize: '12px' }}
308 >
309 <span className="material-symbols-outlined" style={{ fontSize: '16px' }}>content_copy</span>
310 </button>
311 </dd>
312 </div>
313
314 <div>
315 <dt style={{ color: 'var(--text-secondary)', marginBottom: '4px' }}>Admin Email</dt>
316 <dd style={{
317 fontWeight: '500',
318 fontFamily: 'monospace',
319 background: 'var(--input-bg)',
320 padding: '8px 12px',
321 borderRadius: '4px',
322 display: 'flex',
323 alignItems: 'center',
324 justifyContent: 'space-between'
325 }}>
326 <span>{app.wordpress.wp_admin_email}</span>
327 <button
328 onClick={() => {
329 navigator.clipboard.writeText(app.wordpress.wp_admin_email);
330 notifySuccess('Copied', 'Email copied to clipboard');
331 }}
332 className="btn btn-sm"
333 style={{ padding: '4px 8px', fontSize: '12px' }}
334 >
335 <span className="material-symbols-outlined" style={{ fontSize: '16px' }}>content_copy</span>
336 </button>
337 </dd>
338 </div>
339
340 <div style={{ borderTop: '1px solid var(--border)', paddingTop: '16px' }}>
341 <dt style={{ color: 'var(--text-secondary)', marginBottom: '4px' }}>Database Name</dt>
342 <dd style={{ fontWeight: '500', fontFamily: 'monospace' }}>{app.wordpress.db_name}</dd>
343 </div>
344
345 <div>
346 <dt style={{ color: 'var(--text-secondary)', marginBottom: '4px' }}>Spaces Path</dt>
347 <dd style={{ fontWeight: '500', fontFamily: 'monospace' }}>
348 everydaytech-wordpress/{app.wordpress.spaces_path}/
349 </dd>
350 </div>
351 </dl>
352 </div>
353 </div>
354 )}
355
356 {/* Assignment Form Card */}
357 <div className="card">
358 <div style={{ padding: '20px', borderBottom: '1px solid var(--border)' }}>
359 <h2 style={{ margin: 0, fontSize: '20px', fontWeight: '600' }}>Assignments</h2>
360 </div>
361 <form onSubmit={handleSubmit} style={{ padding: '20px' }}>
362 <div style={{ display: 'grid', gap: '20px' }}>
363 <div>
364 <label htmlFor="customer" style={{ display: 'block', marginBottom: '8px', fontWeight: '500' }}>
365 Customer
366 </label>
367 <select
368 id="customer"
369 value={formData.customer_id}
370 onChange={(e) => setFormData(prev => ({ ...prev, customer_id: e.target.value }))}
371 style={{ width: '100%', padding: '8px 12px', border: '1px solid var(--border)', borderRadius: '4px' }}
372 >
373 <option value="">Unassigned</option>
374 {customers.map(c => (
375 <option key={c.customer_id} value={c.customer_id}>
376 {c.name}
377 </option>
378 ))}
379 </select>
380 </div>
381
382 <div>
383 <label htmlFor="contract" style={{ display: 'block', marginBottom: '8px', fontWeight: '500' }}>
384 Contract
385 </label>
386 <select
387 id="contract"
388 value={formData.contract_id}
389 onChange={(e) => setFormData(prev => ({ ...prev, contract_id: e.target.value }))}
390 style={{ width: '100%', padding: '8px 12px', border: '1px solid var(--border)', borderRadius: '4px', color: 'var(--text)' }}
391 >
392 <option value="">Unassigned</option>
393 {getFilteredContracts().map(c => (
394 <option key={c.contract_id} value={c.contract_id}>
395 {c.title} {c.customer_name ? `- ${c.customer_name}` : ''}
396 </option>
397 ))}
398 </select>
399 </div>
400
401 {tenants.length > 0 && (
402 <div>
403 <label htmlFor="tenant" style={{ display: 'block', marginBottom: '8px', fontWeight: '500' }}>
404 Tenant
405 </label>
406 <select
407 id="tenant"
408 value={formData.assigned_tenant_id}
409 onChange={(e) => setFormData(prev => ({ ...prev, assigned_tenant_id: e.target.value }))}
410 style={{ width: '100%', padding: '8px 12px', border: '1px solid var(--border)', borderRadius: '4px', color: 'var(--text)' }}
411 >
412 <option value="">Unassigned</option>
413 {tenants.map(t => (
414 <option key={t.tenant_id} value={t.tenant_id}>
415 {t.name} ({t.subdomain})
416 </option>
417 ))}
418 </select>
419 </div>
420 )}
421
422 <button
423 type="submit"
424 disabled={saving}
425 className="btn btn-primary"
426 style={{ marginTop: '8px', display: 'flex', alignItems: 'center', gap: '8px', justifyContent: 'center' }}
427 >
428 {saving ? (
429 <>
430 <span className="spinner" style={{ width: '16px', height: '16px' }}></span>
431 Saving...
432 </>
433 ) : (
434 <>
435 <span className="material-symbols-outlined">save</span>
436 Save Changes
437 </>
438 )}
439 </button>
440 </div>
441 </form>
442 </div>
443 </div>
444 </div>
445 </MainLayout>
446 );
447}
448
449function getStatusColor(status) {
450 switch (status?.toLowerCase()) {
451 case 'active': return 'success';
452 case 'running': return 'success';
453 case 'building': return 'info';
454 case 'deploying': return 'info';
455 case 'pending': return 'warning';
456 case 'error': return 'danger';
457 case 'deleted': return 'error';
458 case 'terminated': return 'error';
459 case 'canceled': return 'warning';
460 default: return 'secondary';
461 }
462}
463
464function getStatusLabel(status) {
465 if (status?.toLowerCase() === 'deleted') return 'terminated';
466 return status;
467}
468
469export default HostingAppDetail;