@@ -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+
139358function 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