Delete records
This commit is contained in:
parent
52bd8b34b3
commit
df936baa8f
5 changed files with 167 additions and 23 deletions
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,7 @@
|
||||||
--record-NXDOMAIN: #aa0000;
|
--record-NXDOMAIN: #aa0000;
|
||||||
--record-other: #888;
|
--record-other: #888;
|
||||||
|
|
||||||
--record-hover: #fffff8;
|
--record-hover: #fffff4;
|
||||||
}
|
}
|
||||||
|
|
||||||
html {
|
html {
|
||||||
|
|
@ -164,9 +164,8 @@ button {
|
||||||
margin-left: 10px;
|
margin-left: 10px;
|
||||||
|
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(5, min-content);
|
grid-template-columns: repeat(6, min-content);
|
||||||
width: min-content;
|
width: min-content;
|
||||||
/*grid-gap: 4px 10px;*/
|
|
||||||
align-items: center;
|
align-items: center;
|
||||||
border-left: 1px solid var(--line-color);
|
border-left: 1px solid var(--line-color);
|
||||||
|
|
||||||
|
|
@ -174,7 +173,7 @@ button {
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
font-size: 0.75em;
|
font-size: 0.75em;
|
||||||
background: #eee;
|
background: #eee;
|
||||||
padding: 2px 8px;
|
padding: 4px 8px;
|
||||||
border-left: 1px solid var(--header-line);
|
border-left: 1px solid var(--header-line);
|
||||||
border-top: 1px solid var(--header-line);
|
border-top: 1px solid var(--header-line);
|
||||||
border-bottom: 1px solid var(--header-line);
|
border-bottom: 1px solid var(--header-line);
|
||||||
|
|
@ -319,6 +318,18 @@ button {
|
||||||
|
|
||||||
.ttl {
|
.ttl {
|
||||||
cursor: pointer;
|
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-left: 1px solid var(--header-line);
|
||||||
border-right: 1px solid var(--header-line);
|
border-right: 1px solid var(--header-line);
|
||||||
border-bottom: 1px solid var(--header-line);
|
border-bottom: 1px solid var(--header-line);
|
||||||
|
|
@ -329,6 +340,11 @@ button {
|
||||||
&.mouse-over {
|
&.mouse-over {
|
||||||
background-color: var(--record-hover);
|
background-color: var(--record-hover);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
img {
|
||||||
|
display: block;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
49
static/images/icon_delete.svg
Normal file
49
static/images/icon_delete.svg
Normal 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 |
|
|
@ -29,6 +29,10 @@ 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)
|
this.records.sort(this.sortRecords)
|
||||||
|
|
||||||
|
|
@ -236,7 +240,8 @@ class Folder {
|
||||||
<div class="header">FQDN</div>
|
<div class="header">FQDN</div>
|
||||||
<div class="header">Type</div>
|
<div class="header">Type</div>
|
||||||
<div class="header">Value</div>
|
<div class="header">Value</div>
|
||||||
<div class="header last">TTL</div>
|
<div class="header">TTL</div>
|
||||||
|
<div class="header last">Actions</div>
|
||||||
</div>
|
</div>
|
||||||
`
|
`
|
||||||
this.divSubfolders = this.div.querySelector('.subfolders')
|
this.divSubfolders = this.div.querySelector('.subfolders')
|
||||||
|
|
@ -259,6 +264,17 @@ class Folder {
|
||||||
else
|
else
|
||||||
this.divRecords.querySelectorAll('.header').forEach(h => h.style.display = 'block')
|
this.divRecords.querySelectorAll('.header').forEach(h => h.style.display = 'block')
|
||||||
|
|
||||||
|
// Remove old ones
|
||||||
|
console.log(`removing records from ${this.name()}`)
|
||||||
|
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())
|
||||||
|
|
||||||
|
|
@ -285,6 +301,7 @@ class Record {
|
||||||
this.divType = null
|
this.divType = null
|
||||||
this.divValue = null
|
this.divValue = null
|
||||||
this.divTTL = null
|
this.divTTL = null
|
||||||
|
this.divActions = null
|
||||||
}// }}}
|
}// }}}
|
||||||
|
|
||||||
id() {// {{{
|
id() {// {{{
|
||||||
|
|
@ -347,24 +364,24 @@ class Record {
|
||||||
this.divType = document.createElement('div')
|
this.divType = document.createElement('div')
|
||||||
this.divValue = document.createElement('div')
|
this.divValue = document.createElement('div')
|
||||||
this.divTTL = 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.innerHTML = `<img src="/images/${_VERSION}/icon_record.svg">`
|
||||||
this.imgIcon.classList.add("record-icon")
|
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.divFQDN.classList.add('fqdn')
|
this.divFQDN.classList.add('fqdn')
|
||||||
this.divType.classList.add('type')
|
this.divType.classList.add('type')
|
||||||
this.divType.classList.add(this.type())
|
|
||||||
this.divValue.classList.add('value')
|
this.divValue.classList.add('value')
|
||||||
this.divTTL.classList.add('ttl')
|
this.divTTL.classList.add('ttl')
|
||||||
|
this.divActions.classList.add('actions')
|
||||||
this.divFQDN.addEventListener('mouseenter', ()=>this.mouseEnter())
|
|
||||||
this.divFQDN.addEventListener('mouseleave', ()=>this.mouseLeave())
|
|
||||||
this.divType.addEventListener('mouseenter', ()=>this.mouseEnter())
|
|
||||||
this.divType.addEventListener('mouseleave', ()=>this.mouseLeave())
|
|
||||||
this.divValue.addEventListener('mouseenter', ()=>this.mouseEnter())
|
|
||||||
this.divValue.addEventListener('mouseleave', ()=>this.mouseLeave())
|
|
||||||
this.divTTL.addEventListener('mouseenter', ()=>this.mouseEnter())
|
|
||||||
this.divTTL.addEventListener('mouseleave', ()=>this.mouseLeave())
|
|
||||||
|
|
||||||
this.divType.innerHTML = `<div></div>`
|
this.divType.innerHTML = `<div></div>`
|
||||||
this.divFQDN.innerHTML = `
|
this.divFQDN.innerHTML = `
|
||||||
|
|
@ -372,6 +389,9 @@ class Record {
|
||||||
<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)
|
||||||
|
|
@ -387,6 +407,7 @@ class Record {
|
||||||
})
|
})
|
||||||
this.divType.addEventListener('click', () => this.edit())
|
this.divType.addEventListener('click', () => this.edit())
|
||||||
this.divTTL.addEventListener('click', () => this.edit())
|
this.divTTL.addEventListener('click', () => this.edit())
|
||||||
|
this.divActions.querySelector('.delete').addEventListener('click', () => this.delete())
|
||||||
}
|
}
|
||||||
|
|
||||||
// FQDN is updated.
|
// FQDN is updated.
|
||||||
|
|
@ -404,20 +425,22 @@ class Record {
|
||||||
this.divValue.innerText = this.value()
|
this.divValue.innerText = this.value()
|
||||||
this.divTTL.innerText = this.ttl()
|
this.divTTL.innerText = this.ttl()
|
||||||
|
|
||||||
return [this.imgIcon, this.divFQDN, this.divType, this.divValue, this.divTTL]
|
return [this.imgIcon, this.divFQDN, this.divType, this.divValue, this.divTTL, this.divActions]
|
||||||
}// }}}
|
}// }}}
|
||||||
mouseEnter() {
|
mouseEnter() {// {{{
|
||||||
this.divFQDN.classList.add('mouse-over')
|
this.divFQDN.classList.add('mouse-over')
|
||||||
this.divType.classList.add('mouse-over')
|
this.divType.classList.add('mouse-over')
|
||||||
this.divValue.classList.add('mouse-over')
|
this.divValue.classList.add('mouse-over')
|
||||||
this.divTTL.classList.add('mouse-over')
|
this.divTTL.classList.add('mouse-over')
|
||||||
}
|
this.divActions.classList.add('mouse-over')
|
||||||
mouseLeave() {
|
}// }}}
|
||||||
|
mouseLeave() {// {{{
|
||||||
this.divFQDN.classList.remove('mouse-over')
|
this.divFQDN.classList.remove('mouse-over')
|
||||||
this.divType.classList.remove('mouse-over')
|
this.divType.classList.remove('mouse-over')
|
||||||
this.divValue.classList.remove('mouse-over')
|
this.divValue.classList.remove('mouse-over')
|
||||||
this.divTTL.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() == '')
|
||||||
|
|
@ -456,6 +479,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)
|
||||||
|
|
|
||||||
28
webserver.go
28
webserver.go
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue