EverydayTech Platform - Developer Reference
Complete Source Code Documentation - All Applications
Loading...
Searching...
No Matches
DNSManagement.jsx
Go to the documentation of this file.
1import { useState, useEffect } from 'react';
2import { useParams, useNavigate } from 'react-router-dom';
3import MainLayout from '../components/Layout/MainLayout';
4import '../styles/DNSManagement.css';
5
6const API_URL = import.meta.env.VITE_API_URL || 'http://localhost:1338';
7
8// DNS Record Types
9const DNS_TYPES = ['A', 'AAAA', 'CNAME', 'MX', 'TXT', 'SRV', 'NS', 'CAA'];
10
11// TTL Presets (in seconds)
12const TTL_PRESETS = [
13 { label: 'Auto', value: 1 },
14 { label: '2 minutes', value: 120 },
15 { label: '5 minutes', value: 300 },
16 { label: '15 minutes', value: 900 },
17 { label: '30 minutes', value: 1800 },
18 { label: '1 hour', value: 3600 },
19 { label: '2 hours', value: 7200 },
20 { label: '5 hours', value: 18000 },
21 { label: '12 hours', value: 43200 },
22 { label: '1 day', value: 86400 }
23];
24
25export default function DNSManagement() {
26 const { domain } = useParams();
27 const navigate = useNavigate();
28
29 const [loading, setLoading] = useState(true);
30 const [records, setRecords] = useState([]);
31 const [zoneId, setZoneId] = useState(null);
32 const [error, setError] = useState('');
33 const [showAddModal, setShowAddModal] = useState(false);
34 const [editingRecord, setEditingRecord] = useState(null);
35 const [filter, setFilter] = useState('');
36 const [typeFilter, setTypeFilter] = useState('ALL');
37 const [lastSynced, setLastSynced] = useState(null);
38 const [fromCache, setFromCache] = useState(false);
39
40 // Form state
41 const [formData, setFormData] = useState({
42 type: 'A',
43 name: '',
44 content: '',
45 ttl: 3600,
46 proxied: false,
47 priority: 10
48 });
49
50 useEffect(() => {
51 if (domain) {
52 fetchDNSRecords();
53 }
54 }, [domain]);
55
56 const fetchDNSRecords = async (forceRefresh = false) => {
57 try {
58 setLoading(true);
59 // Only clear error on successful start, preserve it if we have cached data
60 if (records.length === 0) {
61 setError('');
62 }
63
64 const token = localStorage.getItem('token');
65 const url = forceRefresh
66 ? `${API_URL}/cloudflare-dns/${domain}/records?refresh=true`
67 : `${API_URL}/cloudflare-dns/${domain}/records`;
68
69 const response = await fetch(url, {
70 headers: {
71 'Authorization': `Bearer ${token}`,
72 'Content-Type': 'application/json'
73 }
74 });
75
76 const data = await response.json();
77
78 if (data.success) {
79 setRecords(data.records || []);
80 setZoneId(data.zone_id);
81 setLastSynced(data.last_synced);
82 setFromCache(data.from_cache || false);
83 setError(''); // Clear error on success
84 } else {
85 // If we have existing records, show warning but keep data
86 if (records.length > 0) {
87 setError('⚠️ Unable to fetch live data - showing cached records');
88 } else {
89 setError(data.error || 'Failed to fetch DNS records');
90 }
91 }
92 } catch (err) {
93 console.error('Error fetching DNS records:', err);
94 // If we have existing records, show warning but keep data
95 if (records.length > 0) {
96 setError('⚠️ Unable to connect to API - showing cached records');
97 } else {
98 setError('Failed to connect to API');
99 }
100 } finally {
101 setLoading(false);
102 }
103 };
104
105 const handleRefresh = () => {
106 fetchDNSRecords(true);
107 };
108
109 const handleAddRecord = () => {
110 setEditingRecord(null);
111 setFormData({
112 type: 'A',
113 name: '',
114 content: '',
115 ttl: 3600,
116 proxied: false,
117 priority: 10
118 });
119 setShowAddModal(true);
120 };
121
122 const handleEditRecord = (record) => {
123 setEditingRecord(record);
124 setFormData({
125 type: record.type,
126 name: record.name,
127 content: record.content,
128 ttl: record.ttl,
129 proxied: record.proxied || false,
130 priority: record.priority || 10
131 });
132 setShowAddModal(true);
133 };
134
135 const handleSubmit = async (e) => {
136 e.preventDefault();
137
138 try {
139 const token = localStorage.getItem('token');
140 const url = editingRecord
141 ? `${API_URL}/cloudflare-dns/${domain}/records/${editingRecord.id}`
142 : `${API_URL}/cloudflare-dns/${domain}/records`;
143
144 const method = editingRecord ? 'PUT' : 'POST';
145
146 const response = await fetch(url, {
147 method,
148 headers: {
149 'Authorization': `Bearer ${token}`,
150 'Content-Type': 'application/json'
151 },
152 body: JSON.stringify(formData)
153 });
154
155 const data = await response.json();
156
157 if (data.success) {
158 setShowAddModal(false);
159 fetchDNSRecords();
160 } else {
161 alert(data.error || 'Failed to save DNS record');
162 }
163 } catch (err) {
164 console.error('Error saving DNS record:', err);
165 alert('Failed to save DNS record');
166 }
167 };
168
169 const handleDeleteRecord = async (recordId) => {
170 if (!confirm('Are you sure you want to delete this DNS record?')) {
171 return;
172 }
173
174 try {
175 const token = localStorage.getItem('token');
176 const response = await fetch(`${API_URL}/cloudflare-dns/${domain}/records/${recordId}`, {
177 method: 'DELETE',
178 headers: {
179 'Authorization': `Bearer ${token}`,
180 'Content-Type': 'application/json'
181 }
182 });
183
184 const data = await response.json();
185
186 if (data.success) {
187 fetchDNSRecords();
188 } else {
189 alert(data.error || 'Failed to delete DNS record');
190 }
191 } catch (err) {
192 console.error('Error deleting DNS record:', err);
193 alert('Failed to delete DNS record');
194 }
195 };
196
197 const filteredRecords = records.filter(record => {
198 const matchesSearch = !filter ||
199 record.name.toLowerCase().includes(filter.toLowerCase()) ||
200 record.content.toLowerCase().includes(filter.toLowerCase());
201
202 const matchesType = typeFilter === 'ALL' || record.type === typeFilter;
203
204 return matchesSearch && matchesType;
205 });
206
207 return (
208 <MainLayout>
209 <div className="dns-management">
210 <div className="dns-header">
211 <div className="dns-header-left">
212 <button className="back-button" onClick={() => navigate(-1)}>
213 ← Back
214 </button>
215 <div>
216 <h1>DNS Management</h1>
217 <p className="domain-name">{domain}</p>
218 {zoneId && <code className="zone-id">Zone ID: {zoneId}</code>}
219 {lastSynced && (
220 <div className="sync-status">
221 {fromCache ? '🔄' : '🌐'} Last synced: {new Date(lastSynced).toLocaleString()}
222 {fromCache && <span style={{color: '#666', fontSize: '0.85em'}}> (cached)</span>}
223 </div>
224 )}
225 </div>
226 </div>
227 <div style={{ display: 'flex', gap: '10px' }}>
228 <button
229 className="btn-secondary"
230 onClick={handleRefresh}
231 disabled={loading}
232 title="Refresh from Cloudflare"
233 >
234 🔄 Refresh
235 </button>
236 <button className="btn-primary" onClick={handleAddRecord}>
237 + Add Record
238 </button>
239 </div>
240 </div>
241
242 {error && (
243 <div className={`error-banner ${records.length > 0 ? 'warning-banner' : ''}`}>
244 <span>{error}</span>
245 {records.length > 0 && (
246 <button
247 className="btn-link"
248 onClick={handleRefresh}
249 style={{ marginLeft: '10px', textDecoration: 'underline' }}
250 >
251 Try refreshing
252 </button>
253 )}
254 </div>
255 )}
256
257 <div className="dns-controls">
258 <input
259 type="text"
260 placeholder="Search records..."
261 value={filter}
262 onChange={(e) => setFilter(e.target.value)}
263 className="search-input"
264 />
265 <select
266 value={typeFilter}
267 onChange={(e) => setTypeFilter(e.target.value)}
268 className="type-filter"
269 >
270 <option value="ALL">All Types</option>
271 {DNS_TYPES.map(type => (
272 <option key={type} value={type}>{type}</option>
273 ))}
274 </select>
275 </div>
276
277 {loading ? (
278 <div className="loading-spinner">Loading DNS records...</div>
279 ) : (
280 <div className="dns-records-table-container">
281 <table className="dns-records-table">
282 <thead>
283 <tr>
284 <th>Type</th>
285 <th>Name</th>
286 <th>Content</th>
287 <th>TTL</th>
288 <th>Proxy</th>
289 <th>Actions</th>
290 </tr>
291 </thead>
292 <tbody>
293 {filteredRecords.length === 0 ? (
294 <tr>
295 <td colSpan="6" className="no-records">
296 No DNS records found
297 </td>
298 </tr>
299 ) : (
300 filteredRecords.map((record) => (
301 <tr key={record.id}>
302 <td>
303 <span className={`record-type type-${record.type}`}>
304 {record.type}
305 </span>
306 </td>
307 <td className="record-name">{record.name}</td>
308 <td className="record-content">{record.content}</td>
309 <td>{record.ttl === 1 ? 'Auto' : `${record.ttl}s`}</td>
310 <td>
311 {(record.type === 'A' || record.type === 'AAAA' || record.type === 'CNAME') && (
312 <span className={record.proxied ? 'proxied-yes' : 'proxied-no'}>
313 {record.proxied ? '✓ Proxied' : '○ DNS Only'}
314 </span>
315 )}
316 </td>
317 <td className="record-actions">
318 <button
319 className="btn-edit"
320 onClick={() => handleEditRecord(record)}
321 title="Edit record"
322 >
323 Edit
324 </button>
325 <button
326 className="btn-delete"
327 onClick={() => handleDeleteRecord(record.id)}
328 title="Delete record"
329 >
330 Delete
331 </button>
332 </td>
333 </tr>
334 ))
335 )}
336 </tbody>
337 </table>
338 </div>
339 )}
340
341 {showAddModal && (
342 <div className="modal-overlay" onClick={() => setShowAddModal(false)}>
343 <div className="modal-content" onClick={(e) => e.stopPropagation()}>
344 <div className="modal-header">
345 <h2>{editingRecord ? 'Edit DNS Record' : 'Add DNS Record'}</h2>
346 <button className="modal-close" onClick={() => setShowAddModal(false)}>×</button>
347 </div>
348
349 <form onSubmit={handleSubmit} className="dns-form">
350 <div className="form-group">
351 <label>Type *</label>
352 <select
353 value={formData.type}
354 onChange={(e) => setFormData({ ...formData, type: e.target.value })}
355 required
356 >
357 {DNS_TYPES.map(type => (
358 <option key={type} value={type}>{type}</option>
359 ))}
360 </select>
361 </div>
362
363 <div className="form-group">
364 <label>Name *</label>
365 <input
366 type="text"
367 value={formData.name}
368 onChange={(e) => setFormData({ ...formData, name: e.target.value })}
369 placeholder={`subdomain.${domain} or @ for root`}
370 required
371 />
372 <small>Use @ for the root domain</small>
373 </div>
374
375 <div className="form-group">
376 <label>Content *</label>
377 <input
378 type="text"
379 value={formData.content}
380 onChange={(e) => setFormData({ ...formData, content: e.target.value })}
381 placeholder={
382 formData.type === 'A' ? '192.0.2.1' :
383 formData.type === 'AAAA' ? '2001:db8::1' :
384 formData.type === 'CNAME' ? 'example.com' :
385 formData.type === 'MX' ? 'mail.example.com' :
386 formData.type === 'TXT' ? 'verification string' :
387 'Record value'
388 }
389 required
390 />
391 </div>
392
393 {formData.type === 'MX' && (
394 <div className="form-group">
395 <label>Priority *</label>
396 <input
397 type="number"
398 value={formData.priority}
399 onChange={(e) => setFormData({ ...formData, priority: parseInt(e.target.value) })}
400 min="0"
401 max="65535"
402 required
403 />
404 </div>
405 )}
406
407 <div className="form-group">
408 <label>TTL</label>
409 <select
410 value={formData.ttl}
411 onChange={(e) => setFormData({ ...formData, ttl: parseInt(e.target.value) })}
412 >
413 {TTL_PRESETS.map(preset => (
414 <option key={preset.value} value={preset.value}>
415 {preset.label}
416 </option>
417 ))}
418 </select>
419 </div>
420
421 {(formData.type === 'A' || formData.type === 'AAAA' || formData.type === 'CNAME') && (
422 <div className="form-group checkbox-group">
423 <label>
424 <input
425 type="checkbox"
426 checked={formData.proxied}
427 onChange={(e) => setFormData({ ...formData, proxied: e.target.checked })}
428 />
429 Proxy through Cloudflare (orange cloud)
430 </label>
431 <small>Enable Cloudflare's performance and security features</small>
432 </div>
433 )}
434
435 <div className="modal-actions">
436 <button type="button" className="btn-secondary" onClick={() => setShowAddModal(false)}>
437 Cancel
438 </button>
439 <button type="submit" className="btn-primary">
440 {editingRecord ? 'Update Record' : 'Create Record'}
441 </button>
442 </div>
443 </form>
444 </div>
445 </div>
446 )}
447 </div>
448 </MainLayout>
449 );
450}