diff options
| author | Bobby <[email protected]> | 2026-03-08 18:17:23 +0530 |
|---|---|---|
| committer | Bobby <[email protected]> | 2026-03-08 18:17:23 +0530 |
| commit | 1136af49815be77a0aca151f3b8ec7348bf4b4c8 (patch) | |
| tree | ca4d94f981be59c51fa7d160e32be978a8d4b4fb /templates | |
| parent | f48054e9bc5e4fb36b9aba9126c6ace9c5b1f470 (diff) | |
| download | dove-1136af49815be77a0aca151f3b8ec7348bf4b4c8.tar.xz dove-1136af49815be77a0aca151f3b8ec7348bf4b4c8.zip | |
feat(dns): add update functionality for DNS records (MX, SRV, TXT)
- Implemented UpdateMXRecord, UpdateSRVRecord, and UpdateTXTRecord functions in their respective repositories.
- Added UpdateRecord method in dns service to handle updates for various DNS record types.
- Updated router to include a new route for updating DNS records.
- Enhanced error messages for record updates in messages.go.
- Modified the frontend forms to support editing DNS records with improved UI components.
- Refactored existing domain management code to remove unused update functionality.
- Improved email handling by adding MX record validation during email delivery.
Diffstat (limited to 'templates')
| -rw-r--r-- | templates/domains/htmx/detail.htmx.django | 228 | ||||
| -rw-r--r-- | templates/domains/htmx/domains.htmx.django | 2 | ||||
| -rw-r--r-- | templates/partials/sidebar.django | 6 |
3 files changed, 183 insertions, 53 deletions
diff --git a/templates/domains/htmx/detail.htmx.django b/templates/domains/htmx/detail.htmx.django index 77a8615..eaa3806 100644 --- a/templates/domains/htmx/detail.htmx.django +++ b/templates/domains/htmx/detail.htmx.django @@ -23,36 +23,61 @@ {% url "domains.records.create" as create_record_path %} <form hx-post="{{ create_record_path }}" hx-swap="none" class="px-5 py-4 space-y-3"> <input type="hidden" name="domain_id" value="{{ domain.ID }}"> - <div class="grid grid-cols-6 gap-3"> + <div class="grid grid-cols-5 gap-3"> <div> <label class="block text-[10px] font-medium text-zinc-500 mb-1 ml-0.5">Type</label> - <select name="record_type" class="input-field text-xs" data-record-type-select> - <option value="A">A</option> - <option value="AAAA">AAAA</option> - <option value="CNAME">CNAME</option> - <option value="MX">MX</option> - <option value="TXT">TXT</option> - <option value="SRV">SRV</option> - </select> + <div class="dropdown" data-dropdown data-record-type-dropdown> + <input type="hidden" name="record_type" value="A" data-dropdown-value> + <button type="button" data-dropdown-trigger class="input-field text-xs text-left flex items-center justify-between"> + <span class="truncate" data-dropdown-label>A</span> + <svg class="w-3.5 h-3.5 text-zinc-500 shrink-0 ml-1 transition-transform duration-150" data-dropdown-chevron fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5"> + <path stroke-linecap="round" stroke-linejoin="round" d="m19.5 8.25-7.5 7.5-7.5-7.5" /> + </svg> + </button> + <div class="dropdown-menu" data-dropdown-menu> + <div class="dropdown-options" data-dropdown-options> + <button type="button" class="dropdown-option" data-dropdown-option data-value="A" data-label="A"><p class="text-sm text-zinc-200">A</p><p class="text-xs text-zinc-500">IPv4 address</p></button> + <button type="button" class="dropdown-option" data-dropdown-option data-value="AAAA" data-label="AAAA"><p class="text-sm text-zinc-200">AAAA</p><p class="text-xs text-zinc-500">IPv6 address</p></button> + <button type="button" class="dropdown-option" data-dropdown-option data-value="CNAME" data-label="CNAME"><p class="text-sm text-zinc-200">CNAME</p><p class="text-xs text-zinc-500">Canonical name</p></button> + <button type="button" class="dropdown-option" data-dropdown-option data-value="MX" data-label="MX"><p class="text-sm text-zinc-200">MX</p><p class="text-xs text-zinc-500">Mail exchange</p></button> + <button type="button" class="dropdown-option" data-dropdown-option data-value="TXT" data-label="TXT"><p class="text-sm text-zinc-200">TXT</p><p class="text-xs text-zinc-500">Text record</p></button> + <button type="button" class="dropdown-option" data-dropdown-option data-value="SRV" data-label="SRV"><p class="text-sm text-zinc-200">SRV</p><p class="text-xs text-zinc-500">Service locator</p></button> + </div> + </div> + </div> </div> <div> <label class="block text-[10px] font-medium text-zinc-500 mb-1 ml-0.5">Name</label> <input type="text" name="name" autocomplete="off" placeholder="@" class="input-field text-xs"> </div> <div class="col-span-2"> - <label class="block text-[10px] font-medium text-zinc-500 mb-1 ml-0.5" data-value-label>Address</label> + <label class="block text-[10px] font-medium text-zinc-500 mb-1 ml-0.5" data-value-label>Address (IPv4)</label> <input type="text" name="value" required autocomplete="off" placeholder="127.0.0.1" class="input-field text-xs" data-value-input> </div> <div> <label class="block text-[10px] font-medium text-zinc-500 mb-1 ml-0.5">TTL</label> - <input type="number" name="ttl" value="300" min="1" class="input-field text-xs"> - </div> - <div class="flex items-end gap-2"> - <button type="submit" class="btn-small bg-accent-500/20 text-accent-400 border-accent-500/20 hover:bg-accent-500/30">Save</button> - <button type="button" class="btn-small" data-cancel-new-record>Cancel</button> + <div class="dropdown" data-dropdown> + <input type="hidden" name="ttl" value="1" data-dropdown-value> + <button type="button" data-dropdown-trigger class="input-field text-xs text-left flex items-center justify-between"> + <span class="truncate" data-dropdown-label>Immediate</span> + <svg class="w-3.5 h-3.5 text-zinc-500 shrink-0 ml-1 transition-transform duration-150" data-dropdown-chevron fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5"> + <path stroke-linecap="round" stroke-linejoin="round" d="m19.5 8.25-7.5 7.5-7.5-7.5" /> + </svg> + </button> + <div class="dropdown-menu" data-dropdown-menu> + <div class="dropdown-options" data-dropdown-options> + <button type="button" class="dropdown-option" data-dropdown-option data-value="1" data-label="Immediate"><p class="text-sm text-zinc-200">Immediate</p><p class="text-xs text-zinc-500">1 second</p></button> + <button type="button" class="dropdown-option" data-dropdown-option data-value="300" data-label="5 minutes"><p class="text-sm text-zinc-200">5 minutes</p><p class="text-xs text-zinc-500">300 seconds</p></button> + <button type="button" class="dropdown-option" data-dropdown-option data-value="900" data-label="15 minutes"><p class="text-sm text-zinc-200">15 minutes</p><p class="text-xs text-zinc-500">900 seconds</p></button> + <button type="button" class="dropdown-option" data-dropdown-option data-value="3600" data-label="1 hour"><p class="text-sm text-zinc-200">1 hour</p><p class="text-xs text-zinc-500">3600 seconds</p></button> + <button type="button" class="dropdown-option" data-dropdown-option data-value="14400" data-label="4 hours"><p class="text-sm text-zinc-200">4 hours</p><p class="text-xs text-zinc-500">14400 seconds</p></button> + <button type="button" class="dropdown-option" data-dropdown-option data-value="86400" data-label="1 day"><p class="text-sm text-zinc-200">1 day</p><p class="text-xs text-zinc-500">86400 seconds</p></button> + </div> + </div> + </div> </div> </div> - <div class="grid grid-cols-6 gap-3" data-extra-fields> + <div class="grid grid-cols-5 gap-3" data-extra-fields> <div data-priority-field style="display: none;"> <label class="block text-[10px] font-medium text-zinc-500 mb-1 ml-0.5">Priority</label> <input type="number" name="priority" value="10" min="0" class="input-field text-xs"> @@ -71,42 +96,41 @@ </div> <div data-srv-protocol style="display: none;"> <label class="block text-[10px] font-medium text-zinc-500 mb-1 ml-0.5">Protocol</label> - <select name="protocol" class="input-field text-xs"> - <option value="tcp">TCP</option> - <option value="udp">UDP</option> - </select> + <div class="dropdown" data-dropdown> + <input type="hidden" name="protocol" value="tcp" data-dropdown-value> + <button type="button" data-dropdown-trigger class="input-field text-xs text-left flex items-center justify-between"> + <span class="truncate" data-dropdown-label>TCP</span> + <svg class="w-3.5 h-3.5 text-zinc-500 shrink-0 ml-1 transition-transform duration-150" data-dropdown-chevron fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5"> + <path stroke-linecap="round" stroke-linejoin="round" d="m19.5 8.25-7.5 7.5-7.5-7.5" /> + </svg> + </button> + <div class="dropdown-menu" data-dropdown-menu> + <div class="dropdown-options" data-dropdown-options> + <button type="button" class="dropdown-option" data-dropdown-option data-value="tcp" data-label="TCP"><p class="text-sm text-zinc-200">TCP</p></button> + <button type="button" class="dropdown-option" data-dropdown-option data-value="udp" data-label="UDP"><p class="text-sm text-zinc-200">UDP</p></button> + </div> + </div> + </div> </div> </div> + <div class="flex items-center gap-3 pt-1"> + <button type="submit" class="btn-small">Save Record</button> + <a href="javascript:void(0)" class="text-xs text-zinc-500 hover:text-zinc-300 transition-colors duration-150" data-cancel-new-record>Cancel</a> + </div> </form> </div> {% if records %} <div class="divide-y divide-white/[0.04]"> {% for record in records %} - <div class="flex items-center justify-between px-5 py-3"> - <div class="flex items-center gap-3 min-w-0"> + <div class="flex items-center justify-between px-5 py-3 group" data-record-row="{{ record.Type }}-{{ record.ID }}"> + <div class="flex items-center gap-3 min-w-0" data-record-display> {% if record.IsManaged %} - <div class="flex items-center justify-center w-8 h-8 rounded-lg bg-amber-500/10 shrink-0"> - <svg class="w-4 h-4 text-amber-400" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5"> + <div class="flex items-center justify-center w-6 h-6 rounded bg-amber-500/10 shrink-0"> + <svg class="w-3.5 h-3.5 text-amber-400" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5"> <path stroke-linecap="round" stroke-linejoin="round" d="M16.5 10.5V6.75a4.5 4.5 0 1 0-9 0v3.75m-.75 11.25h10.5a2.25 2.25 0 0 0 2.25-2.25v-6.75a2.25 2.25 0 0 0-2.25-2.25H6.75a2.25 2.25 0 0 0-2.25 2.25v6.75a2.25 2.25 0 0 0 2.25 2.25Z" /> </svg> </div> - {% else %} - <div class="flex items-center justify-center w-8 h-8 rounded-lg bg-surface-800 shrink-0"> - {% if record.Type == "A" %} - <span class="text-xs font-bold text-blue-400">A</span> - {% elif record.Type == "AAAA" %} - <span class="text-[10px] font-bold text-blue-300">AAAA</span> - {% elif record.Type == "CNAME" %} - <span class="text-[10px] font-bold text-purple-400">CN</span> - {% elif record.Type == "MX" %} - <span class="text-xs font-bold text-orange-400">MX</span> - {% elif record.Type == "TXT" %} - <span class="text-[10px] font-bold text-green-400">TXT</span> - {% elif record.Type == "SRV" %} - <span class="text-[10px] font-bold text-pink-400">SRV</span> - {% endif %} - </div> {% endif %} <div class="flex items-center gap-2 min-w-0"> <span class="inline-flex items-center px-1.5 py-0.5 rounded text-[10px] font-medium shrink-0 @@ -128,11 +152,12 @@ <span class="text-xs text-zinc-700 shrink-0">{{ record.TTL }}s</span> </div> </div> - <div class="flex items-center gap-3 shrink-0 ml-4"> + <div class="flex items-center gap-3 shrink-0 ml-4" data-record-actions> {% if record.IsManaged %} <span class="text-xs text-amber-500/60">System managed</span> {% else %} - <button data-confirm-trigger data-confirm-title="Delete {{ record.Type }} record" data-confirm-message="This DNS record will be permanently removed. This action cannot be undone." data-confirm-action="/domains/records/{{ record.Type }}/{{ record.ID }}?domain_id={{ domain.ID }}" class="text-xs text-zinc-600 hover:text-red-400 transition-colors duration-150">Delete</button> + <button type="button" class="text-xs text-zinc-600 hover:text-zinc-300 transition-colors duration-150 opacity-0 group-hover:opacity-100" data-edit-record data-record-type="{{ record.Type }}" data-record-id="{{ record.ID }}" data-record-name="{{ record.Name }}" data-record-value="{{ record.Value }}" data-record-ttl="{{ record.TTL }}" data-record-priority="{{ record.Priority }}">Edit</button> + <button data-confirm-trigger data-confirm-title="Delete {{ record.Type }} record" data-confirm-message="This DNS record will be permanently removed. This action cannot be undone." data-confirm-action="/domains/records/{{ record.Type }}/{{ record.ID }}?domain_id={{ domain.ID }}" class="text-xs text-zinc-600 hover:text-red-400 transition-colors duration-150 opacity-0 group-hover:opacity-100">Delete</button> {% endif %} </div> </div> @@ -178,7 +203,8 @@ var toggleButton = document.querySelector('[data-toggle-new-record]'); var cancelButton = document.querySelector('[data-cancel-new-record]'); var formContainer = document.querySelector('[data-new-record-form]'); - var typeSelect = document.querySelector('[data-record-type-select]'); + var typeDropdown = document.querySelector('[data-record-type-dropdown]'); + var typeHiddenInput = typeDropdown.querySelector('[data-dropdown-value]'); var priorityField = document.querySelector('[data-priority-field]'); var portMapField = document.querySelector('[data-port-map-field]'); var srvWeight = document.querySelector('[data-srv-weight]'); @@ -205,7 +231,8 @@ }; function updateFields() { - var config = fieldConfig[typeSelect.value]; + var selectedType = typeHiddenInput.value; + var config = fieldConfig[selectedType]; if (!config) return; valueLabel.textContent = config.label; @@ -217,7 +244,118 @@ srvProtocol.style.display = config.srv ? '' : 'none'; } - typeSelect.addEventListener('change', updateFields); + var typeOptions = typeDropdown.querySelectorAll('[data-dropdown-option]'); + typeOptions.forEach(function(option) { + option.addEventListener('click', function() { + setTimeout(updateFields, 10); + }); + }); + updateFields(); + + var ttlOptions = [['1', 'Immediate'], ['300', '5 min'], ['900', '15 min'], ['3600', '1 hour'], ['14400', '4 hours'], ['86400', '1 day']]; + + function createEditForm(recordType, recordId, recordName, recordValue, recordTtl, recordPriority) { + var form = document.createElement('form'); + form.setAttribute('hx-put', '/domains/records/' + recordType + '/' + recordId + '?domain_id={{ domain.ID }}'); + form.setAttribute('hx-swap', 'none'); + form.setAttribute('data-inline-edit', ''); + form.className = 'flex items-center gap-3 w-full'; + + var typeBadge = document.createElement('span'); + typeBadge.className = 'inline-flex items-center px-1.5 py-0.5 rounded text-[10px] font-medium shrink-0 bg-surface-800 text-zinc-400'; + typeBadge.textContent = recordType; + form.appendChild(typeBadge); + + var nameInput = document.createElement('input'); + nameInput.type = 'text'; + nameInput.name = 'name'; + nameInput.value = recordName; + nameInput.className = 'input-field text-xs w-20'; + nameInput.placeholder = '@'; + form.appendChild(nameInput); + + var valueInput = document.createElement('input'); + valueInput.type = 'text'; + valueInput.name = 'value'; + valueInput.value = recordValue; + valueInput.required = true; + valueInput.className = 'input-field text-xs flex-1'; + valueInput.placeholder = 'Value'; + form.appendChild(valueInput); + + var hasPriority = parseInt(recordPriority, 10) > 0; + if (hasPriority) { + var priorityInput = document.createElement('input'); + priorityInput.type = 'number'; + priorityInput.name = 'priority'; + priorityInput.value = recordPriority; + priorityInput.min = '0'; + priorityInput.className = 'input-field text-xs w-16'; + priorityInput.placeholder = 'Pri'; + form.appendChild(priorityInput); + } + + var ttlSelect = document.createElement('select'); + ttlSelect.name = 'ttl'; + ttlSelect.className = 'input-field text-xs w-24'; + for (var i = 0; i < ttlOptions.length; i++) { + var opt = document.createElement('option'); + opt.value = ttlOptions[i][0]; + opt.textContent = ttlOptions[i][1]; + if (recordTtl === ttlOptions[i][0]) opt.selected = true; + ttlSelect.appendChild(opt); + } + form.appendChild(ttlSelect); + + var saveButton = document.createElement('button'); + saveButton.type = 'submit'; + saveButton.className = 'btn-small'; + saveButton.textContent = 'Save'; + form.appendChild(saveButton); + + var cancelLink = document.createElement('a'); + cancelLink.href = 'javascript:void(0)'; + cancelLink.className = 'text-xs text-zinc-500 hover:text-zinc-300 transition-colors duration-150 shrink-0'; + cancelLink.textContent = 'Cancel'; + cancelLink.setAttribute('data-cancel-edit', ''); + form.appendChild(cancelLink); + + return form; + } + + function handleEditClick(event) { + var button = event.target.closest('[data-edit-record]'); + if (!button) return; + + var recordType = button.dataset.recordType; + var recordId = button.dataset.recordId; + var recordName = button.dataset.recordName; + var recordValue = button.dataset.recordValue; + var recordTtl = button.dataset.recordTtl; + var recordPriority = button.dataset.recordPriority; + + var row = document.querySelector('[data-record-row="' + recordType + '-' + recordId + '"]'); + if (!row || row.querySelector('[data-inline-edit]')) return; + + var originalContent = row.innerHTML; + + while (row.firstChild) row.removeChild(row.firstChild); + + var form = createEditForm(recordType, recordId, recordName, recordValue, recordTtl, recordPriority); + row.appendChild(form); + + form.querySelector('[data-cancel-edit]').addEventListener('click', function() { + row.innerHTML = originalContent; + htmx.process(row); + }); + + htmx.process(row); + } + + var recordsList = document.querySelector('.divide-y'); + if (recordsList) { + recordsList.addEventListener('click', handleEditClick); + } })(); </script>
\ No newline at end of file diff --git a/templates/domains/htmx/domains.htmx.django b/templates/domains/htmx/domains.htmx.django index 4c3e37f..86128f6 100644 --- a/templates/domains/htmx/domains.htmx.django +++ b/templates/domains/htmx/domains.htmx.django @@ -24,8 +24,6 @@ <div class="flex items-center gap-4"> {% url "domains.manage.detail" id=domain.ID as detail_domain_path %} <a href="{{ detail_domain_path }}" hx-get="{{ detail_domain_path }}" hx-target="#content" hx-swap="innerHTML" hx-push-url="true" class="text-xs text-zinc-600 hover:text-zinc-300 transition-colors duration-150">DNS Records</a> - {% url "domains.manage.edit" id=domain.ID as edit_domain_path %} - <a href="{{ edit_domain_path }}" hx-get="{{ edit_domain_path }}" hx-target="#content" hx-swap="innerHTML" hx-push-url="true" class="text-xs text-zinc-600 hover:text-zinc-300 transition-colors duration-150">Edit</a> {% url "domains.manage.delete" id=domain.ID as delete_domain_path %} <button data-confirm-trigger data-confirm-title="Delete {{ domain.Name }}.{{ domain.TLD.Name }}" data-confirm-message="This domain and all associated data will be permanently removed. This action cannot be undone." data-confirm-action="{{ delete_domain_path }}" class="text-xs text-zinc-600 hover:text-red-400 transition-colors duration-150">Delete</button> </div> diff --git a/templates/partials/sidebar.django b/templates/partials/sidebar.django index cb33ae5..01dd602 100644 --- a/templates/partials/sidebar.django +++ b/templates/partials/sidebar.django @@ -50,12 +50,6 @@ </svg> Domains </a> - <a href="{{ manage_path }}" hx-get="{{ manage_path }}" class="nav-link flex items-center gap-3 px-3 py-2 rounded-lg text-sm transition-colors duration-150"> - <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5"> - <path stroke-linecap="round" stroke-linejoin="round" d="M19.5 14.25v-2.625a3.375 3.375 0 0 0-3.375-3.375h-1.5A1.125 1.125 0 0 1 13.5 7.125v-1.5a3.375 3.375 0 0 0-3.375-3.375H8.25m0 12.75h7.5m-7.5 3H12M10.5 2.25H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 0 0-9-9Z" /> - </svg> - DNS Records - </a> </div> </div> |
