Compare commits

...

10 commits

Author SHA1 Message Date
Magnus Åhall
d0b0aba30d Show _no.domain in italic 2026-02-26 09:28:14 +01:00
Magnus Åhall
96f3a90ab9 Fixed creating records beneath _no.domain 2026-02-26 09:17:48 +01:00
Magnus Åhall
fc9583ecd2 Concatenate the two most significant labels 2026-02-26 09:12:05 +01:00
Magnus Åhall
cbd1b137cf Recreate the records tree when searching 2026-02-26 08:31:27 +01:00
Magnus Åhall
b20943cfd2 WIP search 2026-02-25 23:50:31 +01:00
Magnus Åhall
39b9515f76 Basic search 2026-02-25 21:46:28 +01:00
Magnus Åhall
8aef6d8a2e Creating records in folders 2026-02-25 21:18:00 +01:00
Magnus Åhall
df936baa8f Delete records 2026-02-25 21:05:02 +01:00
Magnus Åhall
52bd8b34b3 Hover over row 2026-02-25 16:54:39 +01:00
Magnus Åhall
ad7bc0345a Added headers and table for records 2026-02-25 16:43:41 +01:00
5 changed files with 483 additions and 53 deletions

View file

@ -58,7 +58,7 @@ func (dev *RouterosDevice) Init() { // {{{
// query sends a RouterOS REST API query and returns the unparsed body. // query sends a RouterOS REST API query and returns the unparsed body.
func (dev RouterosDevice) query(method, path string, reqBody []byte) (body []byte, err error) { // {{{ func (dev RouterosDevice) query(method, path string, reqBody []byte) (body []byte, err error) { // {{{
url := fmt.Sprintf("https://%s/rest%s", dev.Host, path) url := fmt.Sprintf("https://%s:%d/rest%s", dev.Host, dev.Port, path)
logger.Info("URL", "method", method, "url", url) logger.Info("URL", "method", method, "url", url)
var request *http.Request var request *http.Request
@ -150,6 +150,19 @@ func (dev *RouterosDevice) UpdateDNSEntry(record DNSEntry) (entry DNSEntry, err
err = json.Unmarshal(body, &entry) err = json.Unmarshal(body, &entry)
return return
}// }}} }// }}}
func (dev *RouterosDevice) DeleteDNSEntry(id string) (err error) {
_, err = dev.query("DELETE", "/ip/dns/static/"+id, []byte{})
if err != nil {
rosError := struct{ Detail string }{}
if jsonError := json.Unmarshal([]byte(err.Error()), &rosError); jsonError == nil {
logger.Error("routeros", "error", jsonError)
err = errors.New(rosError.Detail)
return
}
return
}
return
}
/* /*
// FillPeerDetails retrieves RouterOS resource ID, allowed-address and comment from the router // FillPeerDetails retrieves RouterOS resource ID, allowed-address and comment from the router

View file

@ -2,9 +2,6 @@
--line-color: #ccc; --line-color: #ccc;
--line-color-record: #eee; --line-color-record: #eee;
--type-background: #ddd;
--type-foreground: #000;
--label-first: #800033; --label-first: #800033;
--label-rest: #666; --label-rest: #666;
@ -12,6 +9,18 @@
--label-border: #ccc; --label-border: #ccc;
--copy-color: #d48700; --copy-color: #d48700;
--header-line: #d0d0d0;
--record-A: #89a02c;
--record-AAAA: #2f4858;
--record-CNAME: #f6ae2d;
--record-FWD: #f26419;
--record-TXT: #86bbd8;
--record-NXDOMAIN: #aa0000;
--record-other: #888;
--record-hover: #fffff4;
} }
html { html {
@ -97,6 +106,10 @@ button {
cursor: pointer; cursor: pointer;
} }
#search {
margin-bottom: 16px;
}
#records-tree { #records-tree {
white-space: nowrap; white-space: nowrap;
@ -107,6 +120,10 @@ button {
padding-left: 0px; padding-left: 0px;
} }
&.no-domain > .label {
font-style: italic;
}
&.open { &.open {
&>.label>img.open { &>.label>img.open {
display: block; display: block;
@ -129,7 +146,7 @@ button {
&>.label { &>.label {
display: grid; display: grid;
grid-template-columns: min-content min-content 1fr; grid-template-columns: repeat(4, min-content);
align-items: center; align-items: center;
padding: 5px 0px; padding: 5px 0px;
cursor: pointer; cursor: pointer;
@ -140,6 +157,18 @@ button {
margin-right: 6px; margin-right: 6px;
display: none; display: none;
} }
img.create {
display: none;
height: 16px;
margin-left: 8px;
}
&:hover {
img.create {
display: block;
}
}
} }
&>.subfolders { &>.subfolders {
@ -149,17 +178,51 @@ button {
&>.records { &>.records {
padding-left: 30px; padding-left: 30px;
padding-top: 8px;
padding-bottom: 8px;
margin-left: 10px; margin-left: 10px;
display: grid; display: grid;
grid-template-columns: repeat(3, min-content) 1fr; grid-template-columns: repeat(6, min-content);
grid-gap: 4px 10px; width: min-content;
align-items: center; align-items: center;
border-left: 1px solid var(--line-color); border-left: 1px solid var(--line-color);
&>img { .header {
display: block; font-weight: bold;
padding-left: 4px; font-size: 0.75em;
background: #eee;
padding: 4px 8px;
border-left: 1px solid var(--header-line);
border-top: 1px solid var(--header-line);
border-bottom: 1px solid var(--header-line);
&:first-child {
grid-column: 1 / 3;
border-top-left-radius: 4px;
}
&.last {
border-right: 1px solid var(--header-line);
border-top-right-radius: 4px;
}
}
&>:not(.header) {
height: 40px;
}
&>.record-icon {
border-left: 1px solid var(--header-line);
align-content: center;
padding-left: 8px;
border-bottom: 1px solid var(--header-line);
img {
display: block;
padding-left: 4px;
}
} }
.copy { .copy {
@ -174,6 +237,14 @@ button {
cursor: pointer; cursor: pointer;
user-select: none; user-select: none;
display: flex; display: flex;
padding: 0 16px 0 8px;
border-bottom: 1px solid var(--header-line);
height: 100%;
align-items: center;
&.mouse-over {
background-color: var(--record-hover);
}
&.created { &.created {
* { * {
@ -198,34 +269,107 @@ button {
.rest-label { .rest-label {
color: var(--label-rest); color: var(--label-rest);
&.no-domain {
font-style: italic;
}
} }
} }
.type { .type {
background-color: var(--type-background); padding: 2px 8px;
color: var(--type-foreground); border-bottom: 1px solid var(--header-line);
padding: 4px 8px; border-left: 1px solid var(--header-line);
border-radius: 4px; align-content: center;
margin-top: 2px; cursor: pointer;
margin-bottom: 2px;
width: min-content; &.mouse-over {
font-weight: bold; background-color: var(--record-hover);
font-size: 0.85em; }
div {
background-color: var(--record-other);
color: #fff;
padding: 4px 8px;
border-radius: 4px;
width: min-content;
height: min-content;
font-weight: bold;
font-size: 0.85em;
}
&.A div {
background-color: var(--record-A);
}
&.AAAA div {
background-color: var(--record-AAAA);
}
&.CNAME div {
background-color: var(--record-CNAME);
color: #000;
}
&.FWD div {
background-color: var(--record-FWD);
}
&.TXT div {
background-color: var(--record-TXT);
color: #000;
}
&.NXDOMAIN div {
background-color: var(--record-NXDOMAIN);
}
} }
.value { .value {
cursor: pointer; cursor: pointer;
user-select: none; user-select: none;
} border-left: 1px solid var(--header-line);
border-bottom: 1px solid var(--header-line);
padding: 0px 8px;
height: 100%;
align-content: center;
.separator { &.mouse-over {
grid-column: 1 / -1; background-color: var(--record-hover);
&:not(:last-child) {
border-bottom: 1px solid var(--line-color-record);
} }
} }
.ttl {
cursor: pointer;
border-left: 1px solid var(--header-line);
border-bottom: 1px solid var(--header-line);
padding: 0px 8px;
height: 100%;
align-content: center;
&.mouse-over {
background-color: var(--record-hover);
}
}
.actions {
border-left: 1px solid var(--header-line);
border-right: 1px solid var(--header-line);
border-bottom: 1px solid var(--header-line);
padding: 0px 8px;
height: 100%;
align-content: center;
&.mouse-over {
background-color: var(--record-hover);
}
img {
display: block;
cursor: pointer;
}
}
} }
} }

View file

@ -0,0 +1,49 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="4.2333398mm"
height="4.7624998mm"
viewBox="0 0 4.2333398 4.7624998"
version="1.1"
id="svg1"
inkscape:version="1.4.2 (ebf0e94, 2025-05-08)"
sodipodi:docname="icon_delete.svg"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<sodipodi:namedview
id="namedview1"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:document-units="mm"
inkscape:zoom="1"
inkscape:cx="8"
inkscape:cy="9"
inkscape:window-width="1916"
inkscape:window-height="1161"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:current-layer="layer1" />
<defs
id="defs1" />
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(-102.65833,-146.05)">
<title
id="title1">trash-can-outline</title>
<path
d="m 103.98125,146.05 v 0.26458 h -1.32292 v 0.52917 h 0.26459 v 3.43958 a 0.52916667,0.52916667 0 0 0 0.52916,0.52917 h 2.64584 a 0.52916667,0.52916667 0 0 0 0.52916,-0.52917 v -3.43958 h 0.26459 v -0.52917 h -1.32292 V 146.05 h -1.5875 m -0.52917,0.79375 h 2.64584 v 3.43958 h -2.64584 v -3.43958 m 0.52917,0.52917 v 2.38125 h 0.52917 v -2.38125 h -0.52917 m 1.05833,0 v 2.38125 h 0.52917 v -2.38125 z"
id="path1"
style="stroke-width:0.264583;fill:#800000" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

View file

@ -8,10 +8,22 @@ export class Application {
this.topFolder = new Folder(this, null, 'root') this.topFolder = new Folder(this, null, 'root')
this.recordsTree = null this.recordsTree = null
this.settingsIcon = null this.settingsIcon = null
this.createIcon = null
this.searchFor = ''
this.renderFolders() this.renderFolders()
this.render() this.render()
}// }}} }// }}}
filteredRecords() {// {{{
if (this.searchFor === '')
return this.records
const records = this.records.filter(r => {
return (r.name().includes(this.searchFor))
})
return records
}// }}}
parseRecords(recordsData) {// {{{ parseRecords(recordsData) {// {{{
const records = recordsData.map(d => new Record(d)) const records = recordsData.map(d => new Record(d))
return records return records
@ -29,14 +41,24 @@ export class Application {
this.cleanFolders(folder) this.cleanFolders(folder)
}) })
}// }}} }// }}}
deleteRecord(id) {// {{{
const i = this.records.findIndex(rec => rec.id() == id)
this.records.splice(i, 1)
}// }}}
renderFolders() {// {{{ renderFolders() {// {{{
this.records.sort(this.sortRecords) const records = this.filteredRecords()
records.sort(this.sortRecords)
for (const rec of records) {
// rec: for example www.google.com // It felt wrong when records for the base domain (e.g. google.com) was put in the top level domain (.com).
for (const rec of this.records) { // While technically correct, the first label (com) is grouped together with the second label (google).
// com.google (reverse and remove wwww) // Labels are counted reversely since the top domain is most significant.
const labels = rec.labels().reverse().slice(0, -1) //
// The `if` here is the exception for records that only has the two first labels. It would otherwise just
// be an empty array of labels and thus discarded.
let labels = rec.labels().reverse().slice(0, -1)
if (rec.labels().length == 1)
labels = rec.labels()
// Start each record from the top and iterate through all its labels // Start each record from the top and iterate through all its labels
// except the first one since that would be the actual record. // except the first one since that would be the actual record.
@ -93,10 +115,7 @@ export class Application {
return 0 return 0
}// }}} }// }}}
render() {// {{{ render() {// {{{
if (this.recordsTree == null) { if (this.createIcon === null) {
this.recordsTree = document.createElement('div')
this.recordsTree.id = 'records-tree'
this.createIcon = document.createElement('img') this.createIcon = document.createElement('img')
this.createIcon.id = 'create-icon' this.createIcon.id = 'create-icon'
this.createIcon.src = `/images/${_VERSION}/icon_create.svg` this.createIcon.src = `/images/${_VERSION}/icon_create.svg`
@ -107,13 +126,36 @@ export class Application {
this.settingsIcon.src = `/images/${_VERSION}/icon_settings.svg` this.settingsIcon.src = `/images/${_VERSION}/icon_settings.svg`
this.settingsIcon.addEventListener('click', () => new SettingsDialog(this).show()) this.settingsIcon.addEventListener('click', () => new SettingsDialog(this).show())
document.body.appendChild(this.recordsTree) const searchEl = document.createElement('div')
searchEl.id = 'search'
searchEl.innerHTML = `
<input type="text" class="search-for">
<button class="search">Search</button>
`
this.searchField = searchEl.querySelector('.search-for')
this.searchField.addEventListener('keydown', event => {
if (event.key == 'Enter')
this.search()
})
const search = searchEl.querySelector('button.search')
search.addEventListener('click', () => this.search())
document.body.appendChild(this.settingsIcon) document.body.appendChild(this.settingsIcon)
document.body.appendChild(this.createIcon) document.body.appendChild(this.createIcon)
document.body.appendChild(searchEl)
document.body.addEventListener('keydown', event => this.handlerKeys(event)) document.body.addEventListener('keydown', event => this.handlerKeys(event))
} }
// The recordstree is deleted when making a search and is therefore
// not created along with icons and search above.
if (this.recordsTree === null) {
this.recordsTree = document.createElement('div')
this.recordsTree.id = 'records-tree'
document.body.appendChild(this.recordsTree)
}
// Top root folder doesn't have to be shown. // Top root folder doesn't have to be shown.
const folders = Array.from(this.topFolder.subfolders.values()) const folders = Array.from(this.topFolder.subfolders.values())
folders.sort(this.sortFolders) folders.sort(this.sortFolders)
@ -121,11 +163,31 @@ export class Application {
for (const folder of folders) for (const folder of folders)
this.recordsTree.append(folder.render()) this.recordsTree.append(folder.render())
this.removeEmptyFolders()
// Subscribe to settings update since the elements they will change // Subscribe to settings update since the elements they will change
// exists now. // exists now.
_mbus.subscribe('settings_updated', event => this.handlerSettingsUpdated(event.detail)) _mbus.subscribe('settings_updated', event => this.handlerSettingsUpdated(event.detail))
this.setBoxedFolders(this.settings.get('boxed_folders')) this.setBoxedFolders(this.settings.get('boxed_folders'))
}// }}}
removeEmptyFolders(folder) {// {{{
if (folder === undefined)
folder = this.topFolder
folder.subfolders.forEach((folder, _label) => {
this.removeEmptyFolders(folder)
})
// This is a leaf folder in the tree.
// It has to be removed from the parent as well, since that could be up for
// removal as well, all the way up the chain.
if (folder.subfolders.size === 0 && folder.records.length === 0) {
if (folder.parentFolder) {
folder.parentFolder.subfolders.delete(folder.labels()[0])
}
folder.div?.remove()
}
}// }}} }// }}}
handlerKeys(event) {// {{{ handlerKeys(event) {// {{{
let handled = true let handled = true
@ -143,6 +205,10 @@ export class Application {
new RecordDialog(new Record()).show() new RecordDialog(new Record()).show()
break break
case 'f':
this.searchField.focus()
break
default: default:
handled = false handled = false
} }
@ -163,6 +229,16 @@ export class Application {
else else
document.body.classList.remove('boxed-folders') document.body.classList.remove('boxed-folders')
}// }}} }// }}}
search() {// {{{
this.searchFor = this.searchField.value.trim().toLowerCase()
this.recordsTree.remove()
this.topFolder = new Folder(this, null, 'root')
this.recordsTree = null
this.cleanFolders()
this.renderFolders()
this.render()
}// }}}
} }
class Folder { class Folder {
@ -184,7 +260,18 @@ class Folder {
return this.folderName.toLowerCase() return this.folderName.toLowerCase()
}// }}} }// }}}
labels() {// {{{ labels() {// {{{
return this.name().split('.') let labels = this.name().split('.')
// It is very uncommon to see just the top level domain.
// We're much more used to google.com than com and then google.
// First level is therefore the two most significant labels concatenated.
if (labels.length > 1) {
labels.reverse()
labels = [`${labels[1]}.${labels[0]}`].concat(labels.slice(2))
labels.reverse()
}
return labels
}// }}} }// }}}
addRecord(rec) {// {{{ addRecord(rec) {// {{{
this.records.push(rec) this.records.push(rec)
@ -230,14 +317,27 @@ class Folder {
<img class="closed" src="/images/${_VERSION}/icon_folder.svg"> <img class="closed" src="/images/${_VERSION}/icon_folder.svg">
<img class="open" src="/images/${_VERSION}/icon_folder_open.svg"> <img class="open" src="/images/${_VERSION}/icon_folder_open.svg">
<span>${firstLabel}</span><span>${restLabels != '' ? '.' + restLabels : ''}</span> <span>${firstLabel}</span><span>${restLabels != '' ? '.' + restLabels : ''}</span>
<img class="create" src="/images/${_VERSION}/icon_create.svg">
</div> </div>
<div class="subfolders"></div> <div class="subfolders"></div>
<div class="records"></div> <div class="records">
<div class="header">FQDN</div>
<div class="header">Type</div>
<div class="header">Value</div>
<div class="header">TTL</div>
<div class="header last">Actions</div>
</div>
` `
this.divSubfolders = this.div.querySelector('.subfolders') this.divSubfolders = this.div.querySelector('.subfolders')
this.divRecords = this.div.querySelector('.records') this.divRecords = this.div.querySelector('.records')
this.div.querySelector('.label').addEventListener('click', event => this.toggleFolder(event)) this.div.querySelector('.label').addEventListener('click', event => this.toggleFolder(event))
this.div.querySelector('.label .create').addEventListener('click', event => this.createRecord(event))
if (this.name() == '_no.domain') {
console.log('wut')
this.div.classList.add('no-domain')
}
} }
@ -249,11 +349,26 @@ class Folder {
this.divSubfolders.append(folder.render()) this.divSubfolders.append(folder.render())
// Records are refreshed. // Records are refreshed.
if (this.records.length == 0)
this.divRecords.style.display = 'none'
else
this.divRecords.style.display = ''
// Remove old ones
for (const recdiv of this.divRecords.children) {
if (recdiv?.classList?.contains('fqdn')) {
const rec = this.records.find(r => r.id() == recdiv.dataset.record_id)
if (!rec)
this.divRecords.querySelectorAll(`[data-record_id="${recdiv.dataset.record_id}"]`)
.forEach(el => el.remove())
}
}
for (const rec of Array.from(this.records)) for (const rec of Array.from(this.records))
this.divRecords.append(...rec.render()) this.divRecords.append(...rec.render())
// Open this folder automatically if it is a toplevel folder and the settings change to open. // Open this folder automatically if it is a toplevel folder and the settings change to open.
_mbus.subscribe('settings_updated', ({ detail: { key, value }}) => { _mbus.subscribe('settings_updated', ({ detail: { key, value } }) => {
if (key !== 'toplevel_open') if (key !== 'toplevel_open')
return return
@ -263,6 +378,11 @@ class Folder {
return this.div return this.div
}// }}} }// }}}
createRecord(event) {// {{{
event.stopPropagation()
const record = new Record({ Name: this.name() })
new RecordDialog(record).show()
}// }}}
} }
class Record { class Record {
@ -274,7 +394,8 @@ class Record {
this.divFQDN = null this.divFQDN = null
this.divType = null this.divType = null
this.divValue = null this.divValue = null
this.divSeparator = null this.divTTL = null
this.divActions = null
}// }}} }// }}}
id() {// {{{ id() {// {{{
@ -302,7 +423,24 @@ class Record {
return this.data.MatchSubdomain === 'true' return this.data.MatchSubdomain === 'true'
}// }}} }// }}}
labels() {// {{{ labels() {// {{{
return this.name().split('.') let labels = this.name().split('.')
if (labels.length === 1) {
labels = [labels[0], '_no', 'domain']
}
// It is very uncommon to see just the top level domain.
// We're much more used to google.com than com and then google.
// First level is therefore the two most significant labels concatenated.
if (labels.length > 1) {
labels.reverse()
labels = [`${labels[1]}.${labels[0]}`].concat(labels.slice(2))
labels.reverse()
} else {
console.log(this, labels)
}
return labels
}// }}} }// }}}
copy(el, text) {// {{{ copy(el, text) {// {{{
@ -318,6 +456,8 @@ class Record {
if (value.slice(0, 2) == '*.') { if (value.slice(0, 2) == '*.') {
this.data['Name'] = value.slice(2) this.data['Name'] = value.slice(2)
this.data['MatchSubdomain'] = 'true' this.data['MatchSubdomain'] = 'true'
} else if (value.slice(-11) == '._no.domain') {
this.data['Name'] = value.slice(0, -11)
} else { } else {
this.data['Name'] = value this.data['Name'] = value
this.data['MatchSubdomain'] = 'false' this.data['MatchSubdomain'] = 'false'
@ -332,23 +472,39 @@ class Record {
}// }}} }// }}}
render() {// {{{ render() {// {{{
if (this.divFQDN === null) { if (this.divFQDN === null) {
this.imgIcon = document.createElement('img') this.imgIcon = document.createElement('div')
this.divFQDN = document.createElement('div') this.divFQDN = document.createElement('div')
this.divType = document.createElement('div') this.divType = document.createElement('div')
this.divValue = document.createElement('div') this.divValue = document.createElement('div')
this.divSeparator = document.createElement('div') this.divTTL = document.createElement('div')
this.divActions = document.createElement('div')
this.imgIcon.innerHTML = `<img src="/images/${_VERSION}/icon_record.svg">`
this.imgIcon.classList.add("record-icon");
[this.imgIcon, this.divFQDN, this.divType, this.divValue, this.divTTL, this.divActions]
.forEach(div => {
div.addEventListener('mouseenter', () => this.mouseEnter())
div.addEventListener('mouseleave', () => this.mouseLeave())
div.dataset.record_id = this.id()
})
this.divType.classList.add(this.type())
this.imgIcon.src = `/images/${_VERSION}/icon_record.svg`
this.divFQDN.classList.add('fqdn') this.divFQDN.classList.add('fqdn')
this.divType.classList.add('type') this.divType.classList.add('type')
this.divValue.classList.add('value') this.divValue.classList.add('value')
this.divSeparator.classList.add('separator') this.divTTL.classList.add('ttl')
this.divActions.classList.add('actions')
this.divType.innerHTML = `<div></div>`
this.divFQDN.innerHTML = ` this.divFQDN.innerHTML = `
<span class="subdomains">*.</span> <span class="subdomains">*.</span>
<span class="first-label"></span> <span class="first-label"></span>
<span class="rest-label"></span> <span class="rest-label"></span>
` `
this.divActions.innerHTML = `
<img class="delete" src="/images/${_VERSION}/icon_delete.svg">
`
this.divFQDN.addEventListener('click', event => { this.divFQDN.addEventListener('click', event => {
if (event.shiftKey) if (event.shiftKey)
@ -356,13 +512,15 @@ class Record {
else else
this.edit() this.edit()
}) })
this.divValue.addEventListener('click', event => { this.divValue.addEventListener('click', event => {
if (event.shiftKey) if (event.shiftKey)
this.copy(event.target.closest('.value'), this.value()) this.copy(event.target.closest('.value'), this.value())
else else
this.edit() this.edit()
}) })
this.divType.addEventListener('click', () => this.edit())
this.divTTL.addEventListener('click', () => this.edit())
this.divActions.querySelector('.delete').addEventListener('click', () => this.delete())
} }
// FQDN is updated. // FQDN is updated.
@ -373,14 +531,37 @@ class Record {
const fl = this.labels()[0] const fl = this.labels()[0]
const rl = this.labels().slice(1).join('.') const rl = this.labels().slice(1).join('.')
this.divFQDN.querySelector('.first-label').innerText = fl const flEl = this.divFQDN.querySelector('.first-label')
this.divFQDN.querySelector('.rest-label').innerText = rl != '' ? `.${rl}` : '' const rlEl = this.divFQDN.querySelector('.rest-label')
flEl.innerText = fl
rlEl.innerText = rl != '' ? `.${rl}` : ''
this.divType.innerText = this.type() if (rl == '_no.domain')
rlEl.classList.add('no-domain')
else
rlEl.classList.remove('no-domain')
this.divType.querySelector('div').innerText = this.type()
this.divValue.innerText = this.value() this.divValue.innerText = this.value()
this.divTTL.innerText = this.ttl()
return [this.imgIcon, this.divFQDN, this.divType, this.divValue, this.divSeparator] return [this.imgIcon, this.divFQDN, this.divType, this.divValue, this.divTTL, this.divActions]
}// }}} }// }}}
mouseEnter() {// {{{
this.divFQDN.classList.add('mouse-over')
this.divType.classList.add('mouse-over')
this.divValue.classList.add('mouse-over')
this.divTTL.classList.add('mouse-over')
this.divActions.classList.add('mouse-over')
}// }}}
mouseLeave() {// {{{
this.divFQDN.classList.remove('mouse-over')
this.divType.classList.remove('mouse-over')
this.divValue.classList.remove('mouse-over')
this.divTTL.classList.remove('mouse-over')
this.divActions.classList.remove('mouse-over')
}// }}}
save() {// {{{ save() {// {{{
const created = (this.id() == '') const created = (this.id() == '')
@ -418,6 +599,23 @@ class Record {
} }
}) })
}// }}} }// }}}
delete() {// {{{
if (!confirm(`Are you sure you want to delete ${this.name()}?`))
return
fetch(`/record/delete/${this.id()}`)
.then(data => data.json())
.then(json => {
if (!json.OK) {
alert(json.Error)
return
}
_app.deleteRecord(this.id())
_app.cleanFolders()
_app.renderFolders()
_app.render()
})
}// }}}
openParentFolders(folder) {// {{{ openParentFolders(folder) {// {{{
if (folder === undefined) if (folder === undefined)
@ -505,8 +703,8 @@ class RecordDialog {
class Settings { class Settings {
constructor() {// {{{ constructor() {// {{{
this.settings = new Map([ this.settings = new Map([
['boxed_folders', true], ['boxed_folders', false],
['toplevel_open', true], ['toplevel_open', false],
]) ])
// Read any configured settings from local storage, but keeping default value // Read any configured settings from local storage, but keeping default value

View file

@ -7,6 +7,7 @@ import (
// Standard // Standard
"embed" "embed"
"encoding/json" "encoding/json"
"errors"
"fmt" "fmt"
"io" "io"
"net/http" "net/http"
@ -33,6 +34,7 @@ func registerWebserverHandlers() {
http.HandleFunc("/", rootHandler) http.HandleFunc("/", rootHandler)
http.HandleFunc("/record/save", actionRecordSave) http.HandleFunc("/record/save", actionRecordSave)
http.HandleFunc("/record/delete/{id}", actionRecordDelete)
} }
func startWebserver() { func startWebserver() {
@ -110,6 +112,30 @@ func actionRecordSave(w http.ResponseWriter, r *http.Request) {
record = NewDNSRecord(entry) record = NewDNSRecord(entry)
j, _ := json.Marshal(struct{ OK bool; Record DNSRecord }{true, record}) j, _ := json.Marshal(struct {
OK bool
Record DNSRecord
}{true, record})
w.Write(j)
}
func actionRecordDelete(w http.ResponseWriter, r *http.Request) {
id := r.PathValue("id")
if id == "" {
httpError(w, errors.New("No ID provided"))
return
}
err := device.DeleteDNSEntry(id)
if err != nil {
httpError(w, err)
logger.Error("webserver", "op", "record_delete", "error", err)
return
}
j, _ := json.Marshal(struct {
OK bool
}{true})
w.Write(j) w.Write(j)
} }