Skip to content

Commit 8a8b3a1

Browse files
authored
Better user interface to edit reverse DNS servers (dns.revServers) (#3769)
2 parents 608563d + cec359a commit 8a8b3a1

5 files changed

Lines changed: 483 additions & 19 deletions

File tree

scripts/js/footer.js

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -175,13 +175,16 @@ function testCookies() {
175175
}
176176

177177
function applyCheckboxRadioStyle() {
178-
// Get all radio/checkboxes for theming, with the exception of the two radio buttons on the custom disable timer,
179-
// as well as every element with an id that starts with "status_"
178+
// Get all radio/checkboxes for theming, with the exception of:
179+
// - the two radio buttons on the custom disable timer,
180+
// - radio/checkboxes elements with class "no-icheck",
181+
// - every element with an id that starts with "status_"
180182
const sel = $("input[type='radio'],input[type='checkbox']")
181183
.not("#selSec")
182184
.not("#selMin")
183185
.not("#expert-settings")
184186
.not("#only-changed")
187+
.not(".no-icheck")
185188
.not("[id^=status_]");
186189
sel.parent().removeClass();
187190
sel.parent().addClass("icheck-primary");

scripts/js/settings-dns.js

Lines changed: 323 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,225 @@ function updateDNSserversTextfield(upstreams, customServers) {
136136
);
137137
}
138138

139+
function getRevServerLines() {
140+
// Return the lines from the textarea (array of lines)
141+
return $(".revServers")
142+
.val()
143+
.split(/\r?\n/u)
144+
.filter(line => line.trim() !== "");
145+
}
146+
147+
// Return an array of objects containing the current values from the textarea
148+
function getRevServerArray() {
149+
const items = [];
150+
151+
const lines = getRevServerLines();
152+
for (const line of lines) {
153+
const cols = line.split(",").map(s => s.trim());
154+
items.push({
155+
enabled: cols[0] ?? "",
156+
network: cols[1] ?? "",
157+
ip: cols[2] ?? "",
158+
domain: cols[3] ?? "",
159+
});
160+
}
161+
162+
return items;
163+
}
164+
165+
function createRevServerTable() {
166+
// Get the data
167+
const tableRows = getRevServerArray();
168+
169+
$("#revServers-table").DataTable({
170+
data: tableRows,
171+
autoWidth: false,
172+
columns: [
173+
{ data: "enabled", width: "54px", className: "revserver-chkbox text-center" },
174+
{ data: "network", className: "revserver-network" },
175+
{ data: "ip", className: "revserver-ip" },
176+
{ data: "domain", className: "revserver-domain" },
177+
{ data: null, width: "82px", className: "actions" },
178+
],
179+
ordering: false,
180+
columnDefs: [
181+
{
182+
targets: 0,
183+
// eslint-disable-next-line no-unused-vars
184+
createdCell(td, cellData, rowData, row, col) {
185+
$(td).attr("data-initial-value", cellData);
186+
},
187+
render(data, type, row, meta) {
188+
const name = "enabled_" + meta.row;
189+
const ckbox =
190+
`<input type="checkbox" name="${name}" id="${name}" class="no-icheck" ` +
191+
(data === "true" ? "checked " : "") +
192+
">";
193+
return ckbox;
194+
},
195+
},
196+
{
197+
targets: [1, 2, 3],
198+
// eslint-disable-next-line no-unused-vars
199+
createdCell(td, cellData, rowData, row, col) {
200+
$(td).attr("contenteditable", "true").attr("data-initial-value", cellData);
201+
},
202+
},
203+
],
204+
drawCallback() {
205+
$(".deleteRevServers").on("click", deleteRecord);
206+
$("tbody .saveRevServers").on("click", saveRecord);
207+
$(".cancelRevServers").on("click", restoreRecord);
208+
},
209+
rowCallback(row, data, displayNum, displayIndex, dataIndex) {
210+
$(row).attr("data-index", dataIndex);
211+
const bt = '<button type="button" class="btn btn-xs"></button>';
212+
const btDel = $(bt)
213+
.addClass("btn-danger deleteRevServers")
214+
.attr("title", "Delete")
215+
.append('<span class="fa fa-trash"></span>');
216+
const btSave = $(bt)
217+
.addClass("btn-success saveRevServers")
218+
.attr("title", "Confirm changes")
219+
.append('<span class="fa fa-check"></span>');
220+
const btCancel = $(bt)
221+
.addClass("btn-warning cancelRevServers")
222+
.attr("title", "Undo changes")
223+
.append('<span class="fa fa-undo"></span>');
224+
225+
$("td:eq(4)", row).html(btSave).append(" ", btCancel, " ", btDel);
226+
},
227+
dom:
228+
"<'row'<'col-sm-12 text-right'l>>" +
229+
"<'row'<'col-sm-12'<'table-responsive'tr>>><'row'<'col-sm-12'i>>",
230+
lengthMenu: [
231+
[10, 25, 50, 100, -1],
232+
[10, 25, 50, 100, "All"],
233+
],
234+
language: {
235+
emptyTable: "No revese DNS servers defined.",
236+
},
237+
stateSave: true,
238+
stateDuration: 0,
239+
processing: true,
240+
stateSaveCallback(settings, data) {
241+
utils.stateSaveCallback("revServers-records-table", data);
242+
},
243+
stateLoadCallback() {
244+
const data = utils.stateLoadCallback("revServers-records-table");
245+
// Return if not available
246+
if (data === null) return null;
247+
248+
// Apply loaded state to table
249+
return data;
250+
},
251+
});
252+
}
253+
254+
function addRevServer() {
255+
const values = [];
256+
values[0] = $("#enabled-revServers input").prop("checked") ? "true" : "false";
257+
values[1] = $("#network-revServers").text();
258+
values[2] = $("#ip-revServers").text();
259+
values[3] = $("#domain-revServers").text();
260+
261+
// Reject empty network range and server IP
262+
if (values[1] === "" || values[2] === "") {
263+
// Show error message
264+
utils.showAlert("error", "fa fa-ban", "Network Range and Server IP are required", "");
265+
return;
266+
}
267+
268+
// Domain is optional: if empty, remove it from the array
269+
if (values[3] === "") values.pop();
270+
271+
// Add the new values to the textarea
272+
$(".revServers").val($(".revServers").val() + "\n" + values.join(","));
273+
274+
// Clear the table footer fields
275+
$("#revServers-table tfoot [contenteditable]").text("");
276+
$("#revServers-table tfoot input[type=checkbox]").prop("checked", false);
277+
278+
// Save changes with message
279+
saveRevServerData("Added values: " + values.join(", "));
280+
}
281+
282+
// Button to add a new reverse server
283+
$("#btnAddRevServers").on("click", addRevServer);
284+
285+
function saveRecord() {
286+
// Find the row and its index number
287+
const row = $(this).closest("tr");
288+
const index = row.attr("data-index");
289+
290+
// Get the edited values from each field
291+
const values = [];
292+
values[0] = $(".revserver-chkbox input", row).prop("checked") ? "true" : "false";
293+
values[1] = $(".revserver-network", row).text();
294+
values[2] = $(".revserver-ip", row).text();
295+
values[3] = $(".revserver-domain", row).text();
296+
297+
// Remove "editing" class from the row. Buttons will be shown/hidden via CSS
298+
row.removeClass("editing");
299+
300+
// Reject empty network range and server IP
301+
if (values[1] === "" || values[2] === "") {
302+
// Show error message
303+
utils.showAlert("error", "fa fa-ban", "Network Range and Server IP are required", "");
304+
return;
305+
}
306+
307+
// Domain is optional: if empty, remove it from the array
308+
if (values[3] === "") values.pop();
309+
310+
// Get the values from the textarea
311+
const lines = getRevServerLines();
312+
313+
// Update the edited line on the textarea
314+
lines[index] = values.join(",");
315+
$(".revServers").val(lines.join("\n"));
316+
317+
// Save changes with message
318+
saveRevServerData("Updated values: " + values.join(", "));
319+
}
320+
321+
function restoreRecord() {
322+
// Find the row and its index number
323+
const row = $(this).closest("tr");
324+
325+
// Reset values using "data-initial-value"
326+
$(".revserver-chkbox input", row).prop(
327+
"checked",
328+
$(".revserver-chkbox input", row).attr("data-initial-value")
329+
);
330+
$('[contenteditable="true"]', row).text(function () {
331+
return $(this).attr("data-initial-value");
332+
});
333+
334+
// Show cancellation message
335+
utils.showAlert("info", "fas fa-undo", "Canceled", "Original values restored");
336+
337+
// Make sure all highlighted cells are restored
338+
// Remove "editing" class from the row. The buttons will be shown/hidden via CSS
339+
row.find(".table-danger").removeClass("table-danger");
340+
row.removeClass("editing");
341+
}
342+
343+
function deleteRecord() {
344+
// Find the row index (this is also the index of the deleted row)
345+
const index = $(this).closest("tr").attr("data-index");
346+
347+
// Get the current lines from the textarea
348+
const lines = getRevServerLines();
349+
350+
// Remove the deleted line and update the textearea
351+
lines.splice(index, 1);
352+
$(".revServers").val(lines.join("\n"));
353+
354+
// Save changes with message
355+
saveRevServerData("Line successfully deleted");
356+
}
357+
139358
function processDNSConfig() {
140359
$.ajax({
141360
url: document.body.dataset.apiurl + "/config/dns?detailed=true", // We need the detailed output to get the DNS server list
@@ -146,6 +365,19 @@ function processDNSConfig() {
146365
setInterfaceName(data.config.dns.interface.value);
147366
setConfigValues("dns", "dns", data.config.dns);
148367
})
368+
.done(() => {
369+
// This will be executed only after the done block above is executed
370+
371+
// If Conditional Forwarding option is set via ENV VAR, the textarea will be disabled and no values can be edited
372+
if ($(".revServers").prop("disabled")) {
373+
// In this case, we hide the table and show the textarea
374+
$("#revServers-table").hide();
375+
$(".revServers").show().attr("title", "Disabled: Set by environment variable");
376+
} else {
377+
// We only populate the table when the textarea is enabled
378+
createRevServerTable();
379+
}
380+
})
149381
.fail(data => {
150382
apiFailure(data);
151383
});
@@ -154,3 +386,94 @@ function processDNSConfig() {
154386
$(() => {
155387
processDNSConfig();
156388
});
389+
390+
// Save the Reverse Servers data via API and recreate the table with updated values
391+
function saveRevServerData(msg) {
392+
// Get the data from the textarea
393+
const data = getRevServerLines();
394+
395+
// Call the API to save only the dns.revServers option
396+
$.ajax({
397+
url: document.body.dataset.apiurl + "/config",
398+
method: "PATCH",
399+
dataType: "json",
400+
processData: false,
401+
data: JSON.stringify({ config: { dns: { revServers: data } } }),
402+
contentType: "application/json; charset=utf-8",
403+
})
404+
.done(() => {
405+
utils.enableAll();
406+
utils.showAlert(
407+
"success",
408+
"fa-solid fa-fw fa-floppy-disk",
409+
"Conditional Forwarding settings successfully saved",
410+
msg
411+
);
412+
// Show loading overlay (without reloading the page)
413+
utils.loadingOverlay(false);
414+
415+
// Reset the table to show the updated data
416+
// Remove all rows from the table, then create rows with the updated data and finally redraw the table
417+
const table = $("#revServers-table").DataTable();
418+
table.clear().rows.add(getRevServerArray()).draw();
419+
})
420+
.fail((data, exception) => {
421+
utils.enableAll();
422+
utils.showAlert("error", "", "Error while applying settings", data.responseText);
423+
console.log(exception); // eslint-disable-line no-console
424+
apiFailure(data);
425+
});
426+
}
427+
428+
// Mark the row with "editing" class when an editable cell or checkbox is focused/edited
429+
// This will use CSS rules to show/hide buttons
430+
$(document).on("focus input", "#revServers-table [contenteditable]", function () {
431+
$(this).closest("tr").addClass("editing");
432+
433+
// Make sure the placeholder text is shown when a contenteditable cell is empty (or only contains spaces)
434+
if ($(this).text().trim() === "") {
435+
$(this).empty();
436+
}
437+
});
438+
$(document).on("change", "#revServers-table .revserver-chkbox input", function () {
439+
$(this).closest("tr").addClass("editing");
440+
});
441+
442+
// Validate data entered on the table
443+
// If a cell contains an invalid value, it will be highlighted and the save button will be disabled
444+
$(document).on("input blur paste", ".revserver-network", function () {
445+
const val = $(this).text().trim();
446+
if (val && !(utils.validateIPv4(val) || utils.validateIPv6(val))) {
447+
$(this).addClass("table-danger");
448+
$(this).attr("title", "Invalid network range");
449+
$(this).siblings(".actions").find(".saveRevServers").prop("disabled", true);
450+
} else {
451+
$(this).removeClass("table-danger");
452+
$(this).attr("title", "");
453+
$(this).siblings(".actions").find(".saveRevServers").prop("disabled", false);
454+
}
455+
});
456+
$(document).on("input blur paste", ".revserver-ip", function () {
457+
const val = $(this).text().trim();
458+
if (val && !(utils.validateIPv4WithPort(val) || utils.validateIPv6WithPort(val))) {
459+
$(this).addClass("table-danger");
460+
$(this).attr("title", "Invalid server IP");
461+
$(this).siblings(".actions").find(".saveRevServers").prop("disabled", true);
462+
} else {
463+
$(this).removeClass("table-danger");
464+
$(this).attr("title", "");
465+
$(this).siblings(".actions").find(".saveRevServers").prop("disabled", false);
466+
}
467+
});
468+
$(document).on("input blur paste", ".revserver-domain", function () {
469+
const val = $(this).text().trim();
470+
if (val && !utils.validateHostnameStrict(val)) {
471+
$(this).addClass("table-danger");
472+
$(this).attr("title", "Invalid domain");
473+
$(this).siblings(".actions").find(".saveRevServers").prop("disabled", true);
474+
} else {
475+
$(this).removeClass("table-danger");
476+
$(this).attr("title", "");
477+
$(this).siblings(".actions").find(".saveRevServers").prop("disabled", false);
478+
}
479+
});

0 commit comments

Comments
 (0)