Compare commits
10 commits
567133df67
...
d0b0aba30d
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d0b0aba30d | ||
|
|
96f3a90ab9 | ||
|
|
fc9583ecd2 | ||
|
|
cbd1b137cf | ||
|
|
b20943cfd2 | ||
|
|
39b9515f76 | ||
|
|
8aef6d8a2e | ||
|
|
df936baa8f | ||
|
|
52bd8b34b3 | ||
|
|
ad7bc0345a |
5 changed files with 483 additions and 53 deletions
|
|
@ -58,7 +58,7 @@ func (dev *RouterosDevice) Init() { // {{{
|
|||
|
||||
// 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) { // {{{
|
||||
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)
|
||||
|
||||
var request *http.Request
|
||||
|
|
@ -150,6 +150,19 @@ func (dev *RouterosDevice) UpdateDNSEntry(record DNSEntry) (entry DNSEntry, err
|
|||
err = json.Unmarshal(body, &entry)
|
||||
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
|
||||
|
|
|
|||
|
|
@ -2,9 +2,6 @@
|
|||
--line-color: #ccc;
|
||||
--line-color-record: #eee;
|
||||
|
||||
--type-background: #ddd;
|
||||
--type-foreground: #000;
|
||||
|
||||
--label-first: #800033;
|
||||
--label-rest: #666;
|
||||
|
||||
|
|
@ -12,6 +9,18 @@
|
|||
--label-border: #ccc;
|
||||
|
||||
--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 {
|
||||
|
|
@ -97,6 +106,10 @@ button {
|
|||
cursor: pointer;
|
||||
}
|
||||
|
||||
#search {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
#records-tree {
|
||||
white-space: nowrap;
|
||||
|
||||
|
|
@ -107,6 +120,10 @@ button {
|
|||
padding-left: 0px;
|
||||
}
|
||||
|
||||
&.no-domain > .label {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
&.open {
|
||||
&>.label>img.open {
|
||||
display: block;
|
||||
|
|
@ -129,7 +146,7 @@ button {
|
|||
|
||||
&>.label {
|
||||
display: grid;
|
||||
grid-template-columns: min-content min-content 1fr;
|
||||
grid-template-columns: repeat(4, min-content);
|
||||
align-items: center;
|
||||
padding: 5px 0px;
|
||||
cursor: pointer;
|
||||
|
|
@ -140,6 +157,18 @@ button {
|
|||
margin-right: 6px;
|
||||
display: none;
|
||||
}
|
||||
|
||||
img.create {
|
||||
display: none;
|
||||
height: 16px;
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
img.create {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&>.subfolders {
|
||||
|
|
@ -149,18 +178,52 @@ button {
|
|||
|
||||
&>.records {
|
||||
padding-left: 30px;
|
||||
padding-top: 8px;
|
||||
padding-bottom: 8px;
|
||||
|
||||
margin-left: 10px;
|
||||
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, min-content) 1fr;
|
||||
grid-gap: 4px 10px;
|
||||
grid-template-columns: repeat(6, min-content);
|
||||
width: min-content;
|
||||
align-items: center;
|
||||
border-left: 1px solid var(--line-color);
|
||||
|
||||
&>img {
|
||||
.header {
|
||||
font-weight: bold;
|
||||
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 {
|
||||
color: var(--copy-color) !important;
|
||||
|
|
@ -174,6 +237,14 @@ button {
|
|||
cursor: pointer;
|
||||
user-select: none;
|
||||
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 {
|
||||
* {
|
||||
|
|
@ -198,34 +269,107 @@ button {
|
|||
|
||||
.rest-label {
|
||||
color: var(--label-rest);
|
||||
|
||||
&.no-domain {
|
||||
font-style: italic;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.type {
|
||||
background-color: var(--type-background);
|
||||
color: var(--type-foreground);
|
||||
padding: 2px 8px;
|
||||
border-bottom: 1px solid var(--header-line);
|
||||
border-left: 1px solid var(--header-line);
|
||||
align-content: center;
|
||||
cursor: pointer;
|
||||
|
||||
&.mouse-over {
|
||||
background-color: var(--record-hover);
|
||||
}
|
||||
|
||||
div {
|
||||
background-color: var(--record-other);
|
||||
color: #fff;
|
||||
|
||||
padding: 4px 8px;
|
||||
border-radius: 4px;
|
||||
margin-top: 2px;
|
||||
margin-bottom: 2px;
|
||||
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 {
|
||||
cursor: pointer;
|
||||
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 {
|
||||
grid-column: 1 / -1;
|
||||
|
||||
&:not(:last-child) {
|
||||
border-bottom: 1px solid var(--line-color-record);
|
||||
&.mouse-over {
|
||||
background-color: var(--record-hover);
|
||||
}
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
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 |
|
|
@ -8,10 +8,22 @@ export class Application {
|
|||
this.topFolder = new Folder(this, null, 'root')
|
||||
this.recordsTree = null
|
||||
this.settingsIcon = null
|
||||
this.createIcon = null
|
||||
|
||||
this.searchFor = ''
|
||||
|
||||
this.renderFolders()
|
||||
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) {// {{{
|
||||
const records = recordsData.map(d => new Record(d))
|
||||
return records
|
||||
|
|
@ -29,14 +41,24 @@ export class Application {
|
|||
this.cleanFolders(folder)
|
||||
})
|
||||
}// }}}
|
||||
deleteRecord(id) {// {{{
|
||||
const i = this.records.findIndex(rec => rec.id() == id)
|
||||
this.records.splice(i, 1)
|
||||
}// }}}
|
||||
renderFolders() {// {{{
|
||||
this.records.sort(this.sortRecords)
|
||||
const records = this.filteredRecords()
|
||||
records.sort(this.sortRecords)
|
||||
|
||||
|
||||
// rec: for example www.google.com
|
||||
for (const rec of this.records) {
|
||||
// com.google (reverse and remove wwww)
|
||||
const labels = rec.labels().reverse().slice(0, -1)
|
||||
for (const rec of records) {
|
||||
// It felt wrong when records for the base domain (e.g. google.com) was put in the top level domain (.com).
|
||||
// While technically correct, the first label (com) is grouped together with the second label (google).
|
||||
// Labels are counted reversely since the top domain is most significant.
|
||||
//
|
||||
// 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
|
||||
// except the first one since that would be the actual record.
|
||||
|
|
@ -93,10 +115,7 @@ export class Application {
|
|||
return 0
|
||||
}// }}}
|
||||
render() {// {{{
|
||||
if (this.recordsTree == null) {
|
||||
this.recordsTree = document.createElement('div')
|
||||
this.recordsTree.id = 'records-tree'
|
||||
|
||||
if (this.createIcon === null) {
|
||||
this.createIcon = document.createElement('img')
|
||||
this.createIcon.id = 'create-icon'
|
||||
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.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.createIcon)
|
||||
document.body.appendChild(searchEl)
|
||||
|
||||
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.
|
||||
const folders = Array.from(this.topFolder.subfolders.values())
|
||||
folders.sort(this.sortFolders)
|
||||
|
|
@ -121,11 +163,31 @@ export class Application {
|
|||
for (const folder of folders)
|
||||
this.recordsTree.append(folder.render())
|
||||
|
||||
this.removeEmptyFolders()
|
||||
|
||||
// Subscribe to settings update since the elements they will change
|
||||
// exists now.
|
||||
_mbus.subscribe('settings_updated', event => this.handlerSettingsUpdated(event.detail))
|
||||
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) {// {{{
|
||||
let handled = true
|
||||
|
|
@ -143,6 +205,10 @@ export class Application {
|
|||
new RecordDialog(new Record()).show()
|
||||
break
|
||||
|
||||
case 'f':
|
||||
this.searchField.focus()
|
||||
break
|
||||
|
||||
default:
|
||||
handled = false
|
||||
}
|
||||
|
|
@ -163,6 +229,16 @@ export class Application {
|
|||
else
|
||||
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 {
|
||||
|
|
@ -184,7 +260,18 @@ class Folder {
|
|||
return this.folderName.toLowerCase()
|
||||
}// }}}
|
||||
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) {// {{{
|
||||
this.records.push(rec)
|
||||
|
|
@ -230,14 +317,27 @@ class Folder {
|
|||
<img class="closed" src="/images/${_VERSION}/icon_folder.svg">
|
||||
<img class="open" src="/images/${_VERSION}/icon_folder_open.svg">
|
||||
<span>${firstLabel}</span><span>${restLabels != '' ? '.' + restLabels : ''}</span>
|
||||
<img class="create" src="/images/${_VERSION}/icon_create.svg">
|
||||
</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.divRecords = this.div.querySelector('.records')
|
||||
|
||||
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())
|
||||
|
||||
// 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))
|
||||
this.divRecords.append(...rec.render())
|
||||
|
||||
// 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')
|
||||
return
|
||||
|
||||
|
|
@ -263,6 +378,11 @@ class Folder {
|
|||
|
||||
return this.div
|
||||
}// }}}
|
||||
createRecord(event) {// {{{
|
||||
event.stopPropagation()
|
||||
const record = new Record({ Name: this.name() })
|
||||
new RecordDialog(record).show()
|
||||
}// }}}
|
||||
}
|
||||
|
||||
class Record {
|
||||
|
|
@ -274,7 +394,8 @@ class Record {
|
|||
this.divFQDN = null
|
||||
this.divType = null
|
||||
this.divValue = null
|
||||
this.divSeparator = null
|
||||
this.divTTL = null
|
||||
this.divActions = null
|
||||
}// }}}
|
||||
|
||||
id() {// {{{
|
||||
|
|
@ -302,7 +423,24 @@ class Record {
|
|||
return this.data.MatchSubdomain === 'true'
|
||||
}// }}}
|
||||
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) {// {{{
|
||||
|
|
@ -318,6 +456,8 @@ class Record {
|
|||
if (value.slice(0, 2) == '*.') {
|
||||
this.data['Name'] = value.slice(2)
|
||||
this.data['MatchSubdomain'] = 'true'
|
||||
} else if (value.slice(-11) == '._no.domain') {
|
||||
this.data['Name'] = value.slice(0, -11)
|
||||
} else {
|
||||
this.data['Name'] = value
|
||||
this.data['MatchSubdomain'] = 'false'
|
||||
|
|
@ -332,23 +472,39 @@ class Record {
|
|||
}// }}}
|
||||
render() {// {{{
|
||||
if (this.divFQDN === null) {
|
||||
this.imgIcon = document.createElement('img')
|
||||
this.imgIcon = document.createElement('div')
|
||||
this.divFQDN = document.createElement('div')
|
||||
this.divType = 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.divType.classList.add('type')
|
||||
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 = `
|
||||
<span class="subdomains">*.</span>
|
||||
<span class="first-label"></span>
|
||||
<span class="rest-label"></span>
|
||||
`
|
||||
this.divActions.innerHTML = `
|
||||
<img class="delete" src="/images/${_VERSION}/icon_delete.svg">
|
||||
`
|
||||
|
||||
this.divFQDN.addEventListener('click', event => {
|
||||
if (event.shiftKey)
|
||||
|
|
@ -356,13 +512,15 @@ class Record {
|
|||
else
|
||||
this.edit()
|
||||
})
|
||||
|
||||
this.divValue.addEventListener('click', event => {
|
||||
if (event.shiftKey)
|
||||
this.copy(event.target.closest('.value'), this.value())
|
||||
else
|
||||
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.
|
||||
|
|
@ -373,14 +531,37 @@ class Record {
|
|||
|
||||
const fl = this.labels()[0]
|
||||
const rl = this.labels().slice(1).join('.')
|
||||
this.divFQDN.querySelector('.first-label').innerText = fl
|
||||
this.divFQDN.querySelector('.rest-label').innerText = rl != '' ? `.${rl}` : ''
|
||||
const flEl = this.divFQDN.querySelector('.first-label')
|
||||
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.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() {// {{{
|
||||
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) {// {{{
|
||||
if (folder === undefined)
|
||||
|
|
@ -505,8 +703,8 @@ class RecordDialog {
|
|||
class Settings {
|
||||
constructor() {// {{{
|
||||
this.settings = new Map([
|
||||
['boxed_folders', true],
|
||||
['toplevel_open', true],
|
||||
['boxed_folders', false],
|
||||
['toplevel_open', false],
|
||||
])
|
||||
|
||||
// Read any configured settings from local storage, but keeping default value
|
||||
|
|
|
|||
28
webserver.go
28
webserver.go
|
|
@ -7,6 +7,7 @@ import (
|
|||
// Standard
|
||||
"embed"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
|
|
@ -33,6 +34,7 @@ func registerWebserverHandlers() {
|
|||
|
||||
http.HandleFunc("/", rootHandler)
|
||||
http.HandleFunc("/record/save", actionRecordSave)
|
||||
http.HandleFunc("/record/delete/{id}", actionRecordDelete)
|
||||
}
|
||||
|
||||
func startWebserver() {
|
||||
|
|
@ -110,6 +112,30 @@ func actionRecordSave(w http.ResponseWriter, r *http.Request) {
|
|||
|
||||
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)
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue