Recursive tree version
This commit is contained in:
parent
42c5d43610
commit
97058d036d
8 changed files with 599 additions and 134 deletions
29
dns.go
29
dns.go
|
|
@ -9,12 +9,13 @@ import (
|
|||
)
|
||||
|
||||
type DNSRecord struct {
|
||||
ID string `json:".id"`
|
||||
Disabled string
|
||||
Dynamic string
|
||||
Name string
|
||||
TTL string
|
||||
Type string
|
||||
ID string `json:".id"`
|
||||
Disabled string
|
||||
Dynamic string
|
||||
Name string
|
||||
TTL string
|
||||
Type string
|
||||
ParsedValue string // not from RouterOS, here to not having to have the value logics in frontend too.
|
||||
|
||||
Address string
|
||||
CNAME string
|
||||
|
|
@ -159,13 +160,13 @@ func (dp *DomainPart) ToHTMLElements(parts []string) []HTMLElement {
|
|||
<div class="type"></div>
|
||||
<div class="value"></div>
|
||||
`,
|
||||
topmost, // .top-most
|
||||
restPart, // data-top
|
||||
fqdn, // data-self
|
||||
topmost, // .top-most
|
||||
restPart, // data-top
|
||||
fqdn, // data-self
|
||||
(len(newParts)-1)*32, // margin-left
|
||||
VERSION, // images/
|
||||
VERSION, // images/
|
||||
mostSpecificPart, // innerText
|
||||
VERSION, // images/
|
||||
VERSION, // images/
|
||||
mostSpecificPart, // innerText
|
||||
restPart,
|
||||
)
|
||||
lines = append(lines, HTMLElement{Header: true, HTML: html})
|
||||
|
|
@ -195,8 +196,8 @@ func (dp *DomainPart) ToHTMLElements(parts []string) []HTMLElement {
|
|||
for _, rec := range subpart.Record {
|
||||
html := fmt.Sprintf(
|
||||
`
|
||||
<div class="record" style="padding-left: %dpx" data-top="%s"><span>%s</span><span>%s</span></div>
|
||||
<div class="type">%s</div>
|
||||
<div class="record" style="padding-left: %dpx" data-top="%s"><div><span>%s</span><span>%s</span></div></div>
|
||||
<div class="type"><div>%s</div></div>
|
||||
<div class="value">%s</div>
|
||||
`,
|
||||
(len(newParts)-1)*32,
|
||||
|
|
|
|||
|
|
@ -1,3 +1,19 @@
|
|||
:root {
|
||||
--line-color: #ccc;
|
||||
--line-color-record: #eee;
|
||||
|
||||
--type-background: #ddd;
|
||||
--type-foreground: #000;
|
||||
|
||||
--label-first: #800033;
|
||||
--label-rest: #666;
|
||||
|
||||
--label-background: #f8f8f8;
|
||||
--label-border: #ccc;
|
||||
|
||||
--copy-color: #d48700;
|
||||
}
|
||||
|
||||
html {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
|
@ -14,102 +30,151 @@ html {
|
|||
|
||||
body {
|
||||
font-family: sans-serif;
|
||||
font-size: 12pt;
|
||||
margin-left: 32px;
|
||||
}
|
||||
|
||||
.records-tree {
|
||||
display: grid;
|
||||
grid-template-columns: min-content min-content 1fr;
|
||||
#records-tree {
|
||||
white-space: nowrap;
|
||||
|
||||
.show {
|
||||
display: block !important;
|
||||
}
|
||||
.folder {
|
||||
padding-left: 32px;
|
||||
|
||||
.top,
|
||||
.record,
|
||||
.type,
|
||||
.value {
|
||||
display: none;
|
||||
border-bottom: 1px solid #ccc;
|
||||
padding: 4px 0px;
|
||||
}
|
||||
|
||||
.top {
|
||||
font-weight: bold;
|
||||
background-color: #f8f8f8;
|
||||
user-select: none;
|
||||
grid-template-columns: repeat(3, min-content);
|
||||
align-items: center;
|
||||
&.top-most {
|
||||
padding-left: 0px;
|
||||
}
|
||||
|
||||
&.open {
|
||||
.folder.open {
|
||||
display: inline-block;
|
||||
&>.label>img.open {
|
||||
display: block;
|
||||
}
|
||||
.folder.closed {
|
||||
}
|
||||
|
||||
&.closed {
|
||||
&>.label>img.closed {
|
||||
display: block;
|
||||
}
|
||||
|
||||
&>.subfolders {
|
||||
display: none;
|
||||
}
|
||||
|
||||
&>.records {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
&:not(.open) {
|
||||
.folder.open {
|
||||
&>.label {
|
||||
display: grid;
|
||||
grid-template-columns: min-content min-content 1fr;
|
||||
align-items: center;
|
||||
padding: 5px 0px;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
|
||||
/*
|
||||
background-color: var(--label-background);
|
||||
width: min-content;
|
||||
padding: 4px 8px;
|
||||
border-radius: 4px;
|
||||
border: 1px solid var(--label-border);
|
||||
margin-top: 8px;
|
||||
margin-bottom: 8px;
|
||||
*/
|
||||
|
||||
img {
|
||||
height: 20px;
|
||||
margin-right: 6px;
|
||||
display: none;
|
||||
}
|
||||
.folder.closed {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
&>.subfolders {
|
||||
border-left: 1px solid var(--line-color);
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
&>.records {
|
||||
padding-left: 30px;
|
||||
margin-left: 10px;
|
||||
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, min-content) 1fr;
|
||||
grid-gap: 4px 10px;
|
||||
align-items: center;
|
||||
border-left: 1px solid var(--line-color);
|
||||
|
||||
&>img {
|
||||
display: block;
|
||||
padding-left: 4px;
|
||||
}
|
||||
|
||||
.copy {
|
||||
color: var(--copy-color) !important;
|
||||
|
||||
span {
|
||||
color: var(--copy-color) !important;
|
||||
}
|
||||
}
|
||||
|
||||
.fqdn {
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
|
||||
.first-label {
|
||||
color: var(--label-first);
|
||||
}
|
||||
|
||||
.rest-label {
|
||||
color: var(--label-rest);
|
||||
}
|
||||
}
|
||||
|
||||
.type {
|
||||
background-color: var(--type-background);
|
||||
color: var(--type-foreground);
|
||||
padding: 4px 8px;
|
||||
border-radius: 4px;
|
||||
margin-top: 2px;
|
||||
margin-bottom: 2px;
|
||||
width: min-content;
|
||||
font-weight: bold;
|
||||
font-size: 0.85em;
|
||||
}
|
||||
|
||||
.value {
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.separator {
|
||||
grid-column: 1 / -1;
|
||||
|
||||
&:not(:last-child) {
|
||||
border-bottom: 1px solid var(--line-color-record);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
img {
|
||||
|
||||
height: 16px;
|
||||
display: none;
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
|
||||
span:first-child {
|
||||
color: #800033;
|
||||
}
|
||||
|
||||
span:last-child {
|
||||
color: #444;
|
||||
font-weight: normal;
|
||||
}
|
||||
}
|
||||
|
||||
.top+.type,
|
||||
.top+.type+.value {
|
||||
background-color: #f8f8f8;
|
||||
}
|
||||
|
||||
.top-most,
|
||||
.top-most + .type,
|
||||
.top-most + .type + .value
|
||||
{
|
||||
display:grid;
|
||||
}
|
||||
|
||||
.record {
|
||||
display: none;
|
||||
font-weight: normal;
|
||||
color: #444;
|
||||
|
||||
span:first-child {
|
||||
color: #800033;
|
||||
}
|
||||
|
||||
span:last-child {
|
||||
color: #888;
|
||||
}
|
||||
}
|
||||
|
||||
.record+.type,
|
||||
.record+.type+.value {
|
||||
display: none;
|
||||
padding-left: 16px;
|
||||
}
|
||||
|
||||
.record+.type+.value {
|
||||
color: #800033;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#record-dialog {
|
||||
display: grid;
|
||||
grid-template-columns: min-content 1fr;
|
||||
grid-gap: 8px;
|
||||
align-items: center;
|
||||
|
||||
.buttons {
|
||||
grid-column: 1 / -1;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
input, select, button {
|
||||
font-size: 1em;
|
||||
padding: 4px 8px;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,11 +3,11 @@
|
|||
|
||||
<svg
|
||||
width="21.200001"
|
||||
height="16.000025"
|
||||
viewBox="0 0 5.6091668 4.2333399"
|
||||
height="18"
|
||||
viewBox="0 0 5.6091668 4.7625"
|
||||
version="1.1"
|
||||
id="svg1"
|
||||
inkscape:version="1.4.2 (ebf0e94, 2025-05-08)"
|
||||
inkscape:version="1.4 (e7c3feb, 2024-10-09)"
|
||||
sodipodi:docname="icon_folder.svg"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
|
|
@ -24,12 +24,12 @@
|
|||
inkscape:deskcolor="#d1d1d1"
|
||||
inkscape:document-units="px"
|
||||
inkscape:zoom="22.627417"
|
||||
inkscape:cx="4.065864"
|
||||
inkscape:cy="7.6455921"
|
||||
inkscape:window-width="1916"
|
||||
inkscape:window-height="1041"
|
||||
inkscape:window-x="1920"
|
||||
inkscape:window-y="1098"
|
||||
inkscape:cx="16.175068"
|
||||
inkscape:cy="10.341437"
|
||||
inkscape:window-width="2190"
|
||||
inkscape:window-height="1404"
|
||||
inkscape:window-x="1463"
|
||||
inkscape:window-y="16"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="layer1" />
|
||||
<defs
|
||||
|
|
@ -42,7 +42,7 @@
|
|||
<title
|
||||
id="title1">folder-outline</title>
|
||||
<rect
|
||||
style="fill:#ffeeaa;stroke-width:0.352777;stroke-opacity:0.24447;stroke:none"
|
||||
style="fill:#ffeeaa;stroke:none;stroke-width:0.352777;stroke-opacity:0.24447"
|
||||
id="rect1"
|
||||
width="4.6421375"
|
||||
height="3.0401909"
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 1.9 KiB After Width: | Height: | Size: 1.9 KiB |
|
|
@ -3,11 +3,11 @@
|
|||
|
||||
<svg
|
||||
width="21.200001"
|
||||
height="16.000025"
|
||||
viewBox="0 0 5.6091669 4.2333399"
|
||||
height="18"
|
||||
viewBox="0 0 5.6091669 4.7625"
|
||||
version="1.1"
|
||||
id="svg1"
|
||||
inkscape:version="1.4.2 (ebf0e94, 2025-05-08)"
|
||||
inkscape:version="1.4 (e7c3feb, 2024-10-09)"
|
||||
sodipodi:docname="icon_folder_open.svg"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
|
|
@ -23,15 +23,16 @@
|
|||
inkscape:pagecheckerboard="0"
|
||||
inkscape:deskcolor="#d1d1d1"
|
||||
inkscape:document-units="px"
|
||||
inkscape:zoom="22.627417"
|
||||
inkscape:cx="16.440233"
|
||||
inkscape:cy="12.551145"
|
||||
inkscape:window-width="1916"
|
||||
inkscape:window-height="1041"
|
||||
inkscape:window-x="1920"
|
||||
inkscape:window-y="1098"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="layer1" />
|
||||
inkscape:zoom="17.076364"
|
||||
inkscape:cx="35.487649"
|
||||
inkscape:cy="7.9642249"
|
||||
inkscape:window-width="2190"
|
||||
inkscape:window-height="1404"
|
||||
inkscape:window-x="1463"
|
||||
inkscape:window-y="16"
|
||||
inkscape:window-maximized="0"
|
||||
inkscape:current-layer="layer1"
|
||||
showgrid="false" />
|
||||
<defs
|
||||
id="defs1" />
|
||||
<g
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 2 KiB After Width: | Height: | Size: 2 KiB |
67
static/images/icon_record.svg
Normal file
67
static/images/icon_record.svg
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||
|
||||
<svg
|
||||
width="18"
|
||||
height="18"
|
||||
viewBox="0 0 4.7624999 4.7625001"
|
||||
version="1.1"
|
||||
id="svg8"
|
||||
inkscape:version="1.4 (e7c3feb, 2024-10-09)"
|
||||
sodipodi:docname="icon_record.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"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/">
|
||||
<defs
|
||||
id="defs2" />
|
||||
<sodipodi:namedview
|
||||
id="base"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1.0"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:zoom="1"
|
||||
inkscape:cx="9.5"
|
||||
inkscape:cy="10"
|
||||
inkscape:document-units="px"
|
||||
inkscape:current-layer="layer1"
|
||||
showgrid="false"
|
||||
units="px"
|
||||
inkscape:window-width="2190"
|
||||
inkscape:window-height="1404"
|
||||
inkscape:window-x="1463"
|
||||
inkscape:window-y="16"
|
||||
inkscape:window-maximized="0"
|
||||
inkscape:showpageshadow="true"
|
||||
inkscape:pagecheckerboard="0"
|
||||
inkscape:deskcolor="#d6d6d6"
|
||||
showborder="true" />
|
||||
<metadata
|
||||
id="metadata5">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<g
|
||||
inkscape:label="Layer 1"
|
||||
inkscape:groupmode="layer"
|
||||
id="layer1"
|
||||
transform="translate(-90.4875,-148.69583)">
|
||||
<title
|
||||
id="title1">text-box-outline</title>
|
||||
<path
|
||||
d="m 91.016667,148.69583 c -0.293688,0 -0.529167,0.23548 -0.529167,0.52917 v 3.70417 c 0,0.29368 0.235479,0.52916 0.529167,0.52916 h 3.704167 c 0.293687,0 0.529166,-0.23548 0.529166,-0.52916 V 149.225 c 0,-0.29369 -0.235479,-0.52917 -0.529166,-0.52917 h -3.704167 m 0,0.52917 h 3.704167 v 3.70417 H 91.016667 V 149.225 m 0.529167,0.52917 v 0.52916 h 2.645833 v -0.52916 h -2.645833 m 0,1.05833 v 0.52917 h 2.645833 v -0.52917 h -2.645833 m 0,1.05833 V 152.4 h 1.852083 v -0.52917 z"
|
||||
id="path1"
|
||||
style="stroke-width:0.264583" />
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.3 KiB |
|
|
@ -1,16 +1,85 @@
|
|||
export class Application {
|
||||
constructor() {
|
||||
this.addTopHandlers()
|
||||
}
|
||||
constructor(records) {// {{{
|
||||
this.records = this.parseRecords(records)
|
||||
this.folders = this.createFolders()
|
||||
this.render()
|
||||
}// }}}
|
||||
parseRecords(recordsData) {// {{{
|
||||
const records = recordsData.map(d => new Record(d))
|
||||
return records
|
||||
}// }}}
|
||||
createFolders() {// {{{
|
||||
this.records.sort(this.sortRecords)
|
||||
const topFolder = new Folder(this, 'root')
|
||||
|
||||
addTopHandlers() {
|
||||
for (const top of document.querySelectorAll('.top'))
|
||||
top.addEventListener('click', event => this.handlerTop(event))
|
||||
}
|
||||
for (const rec of this.records) {
|
||||
const labels = rec.labels().reverse().slice(0, -1)
|
||||
|
||||
handlerTop(event) {
|
||||
let currFolder = topFolder
|
||||
let accFolderLabels = []
|
||||
for (const i in labels) {
|
||||
const label = labels[i]
|
||||
|
||||
accFolderLabels.push(label)
|
||||
const accFolderName = accFolderLabels.map(v => v).reverse().join('.')
|
||||
|
||||
let folder = currFolder.subfolders.get(label)
|
||||
if (folder === undefined) {
|
||||
folder = new Folder(this, accFolderName)
|
||||
currFolder.subfolders.set(label, folder)
|
||||
}
|
||||
currFolder = folder
|
||||
|
||||
// Add the record to the innermost folder
|
||||
}
|
||||
currFolder.addRecord(rec)
|
||||
}
|
||||
|
||||
return topFolder
|
||||
}// }}}
|
||||
sortFolders(a, b) {// {{{
|
||||
const aLabels = a.labels().reverse()
|
||||
const bLabels = b.labels().reverse()
|
||||
|
||||
for (let i = 0; i < aLabels.length && i < bLabels.length; i++) {
|
||||
if (aLabels[i] < bLabels[i])
|
||||
return -1
|
||||
if (aLabels[i] > bLabels[i])
|
||||
return 1
|
||||
}
|
||||
|
||||
if (a.length < b.length) return 1
|
||||
if (a.length > b.length) return -1
|
||||
|
||||
return 0
|
||||
}// }}}
|
||||
sortRecords(a, b) {// {{{
|
||||
const aLabels = a.labels().reverse()
|
||||
const bLabels = b.labels().reverse()
|
||||
|
||||
for (let i = 0; i < aLabels.length && i < bLabels.length; i++) {
|
||||
if (aLabels[i] < bLabels[i])
|
||||
return -1
|
||||
if (aLabels[i] > bLabels[i])
|
||||
return 1
|
||||
}
|
||||
|
||||
return 0
|
||||
}// }}}
|
||||
render() {// {{{
|
||||
const tree = document.getElementById('records-tree')
|
||||
tree.replaceChildren()
|
||||
|
||||
// Top root folder doesn't have to be shown.
|
||||
const folders = Array.from(this.folders.subfolders.values())
|
||||
folders.sort(this.sortFolders)
|
||||
|
||||
for (const folder of folders)
|
||||
tree.append(folder.toElement())
|
||||
|
||||
}// }}}
|
||||
handlerTop(event) {// {{{
|
||||
const topEl = event.target.closest('.top')
|
||||
console.log(topEl.dataset.self)
|
||||
|
||||
let records, types, values
|
||||
if (topEl.classList.contains('open')) {
|
||||
|
|
@ -30,12 +99,22 @@ export class Application {
|
|||
|
||||
topEl.classList.remove('open')
|
||||
} else {
|
||||
records = document.querySelectorAll(`[data-top="${topEl.dataset.self}"]`)
|
||||
types = document.querySelectorAll(`[data-top="${topEl.dataset.self}"] + .type`)
|
||||
values = document.querySelectorAll(`[data-top="${topEl.dataset.self}"] + .type + .value`)
|
||||
if (event.shiftKey) {
|
||||
records = document.querySelectorAll(`[data-top$="${topEl.dataset.self}"]`)
|
||||
types = document.querySelectorAll(`[data-top$="${topEl.dataset.self}"] + .type`)
|
||||
values = document.querySelectorAll(`[data-top$="${topEl.dataset.self}"] + .type + .value`)
|
||||
} else {
|
||||
records = document.querySelectorAll(`[data-top="${topEl.dataset.self}"]`)
|
||||
types = document.querySelectorAll(`[data-top="${topEl.dataset.self}"] + .type`)
|
||||
values = document.querySelectorAll(`[data-top="${topEl.dataset.self}"] + .type + .value`)
|
||||
console.log(records)
|
||||
}
|
||||
|
||||
for (const r of records)
|
||||
for (const r of records) {
|
||||
r.classList.add('show')
|
||||
if (event.shiftKey && r.classList.contains('top'))
|
||||
r.classList.add('open')
|
||||
}
|
||||
for (const r of types)
|
||||
r.classList.add('show')
|
||||
for (const r of values)
|
||||
|
|
@ -45,5 +124,255 @@ export class Application {
|
|||
}
|
||||
|
||||
|
||||
}// }}}
|
||||
}
|
||||
|
||||
class Folder {
|
||||
constructor(app, name) {// {{{
|
||||
this.application = app
|
||||
this.open = false
|
||||
this.folderName = name
|
||||
this.subfolders = new Map()
|
||||
this.records = []
|
||||
|
||||
this.div = null
|
||||
this.divSubfolders = null
|
||||
this.divRecords = null
|
||||
}// }}}
|
||||
name() {// {{{
|
||||
return this.folderName.toLowerCase()
|
||||
}// }}}
|
||||
labels() {// {{{
|
||||
return this.name().split('.')
|
||||
}// }}}
|
||||
addRecord(rec) {// {{{
|
||||
this.records.push(rec)
|
||||
}// }}}
|
||||
openFolder(recursive) {// {{{
|
||||
this.open = true
|
||||
this.div.classList.add('open')
|
||||
this.div.classList.remove('closed')
|
||||
|
||||
if (recursive)
|
||||
this.subfolders.forEach(folder => folder.openFolder(recursive))
|
||||
}// }}}
|
||||
closeFolder(recursive) {// {{{
|
||||
this.open = false
|
||||
this.div.classList.remove('open')
|
||||
this.div.classList.add('closed')
|
||||
|
||||
if (recursive)
|
||||
this.subfolders.forEach(folder => folder.closeFolder(recursive))
|
||||
}// }}}
|
||||
toggleFolder(event) {// {{{
|
||||
event.stopPropagation()
|
||||
|
||||
if (this.open)
|
||||
this.closeFolder(event.shiftKey)
|
||||
else
|
||||
this.openFolder(event.shiftKey)
|
||||
}// }}}
|
||||
toElement() {// {{{
|
||||
if (this.div === null) {
|
||||
this.div = document.createElement('div')
|
||||
this.div.classList.add('folder')
|
||||
this.div.classList.add(this.open ? 'open' : 'closed')
|
||||
if (this.labels().length == 1)
|
||||
this.div.classList.add('top-most')
|
||||
this.div.dataset.top = this.labels().slice(1).join('.')
|
||||
this.div.dataset.self = this.name()
|
||||
|
||||
const firstLabel = this.labels()[0]
|
||||
const restLabels = this.labels().slice(1).join('.')
|
||||
|
||||
this.div.innerHTML = `
|
||||
<div class="label">
|
||||
<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>
|
||||
</div>
|
||||
<div class="subfolders"></div>
|
||||
<div class="records"></div>
|
||||
`
|
||||
this.divSubfolders = this.div.querySelector('.subfolders')
|
||||
this.divRecords = this.div.querySelector('.records')
|
||||
|
||||
this.div.querySelector('.label').addEventListener('click', event => this.toggleFolder(event))
|
||||
}
|
||||
|
||||
|
||||
// Subfolders are refreshed.
|
||||
this.divSubfolders.replaceChildren()
|
||||
const subfolders = Array.from(this.subfolders.values())
|
||||
subfolders.sort(this.application.sortFolders)
|
||||
|
||||
for (const folder of subfolders)
|
||||
this.divSubfolders.append(folder.toElement())
|
||||
|
||||
// Records are refreshed.
|
||||
this.divRecords.replaceChildren()
|
||||
for (const rec of Array.from(this.records))
|
||||
this.divRecords.append(...rec.toElements())
|
||||
|
||||
return this.div
|
||||
}// }}}
|
||||
}
|
||||
|
||||
class Record {
|
||||
constructor(data) {// {{{
|
||||
this.data = data
|
||||
|
||||
this.imgIcon = null
|
||||
this.divFQDN = null
|
||||
this.divType = null
|
||||
this.divValue = null
|
||||
this.divSeparator = null
|
||||
}// }}}
|
||||
id() {// {{{
|
||||
return this.data['.id']
|
||||
}// }}}
|
||||
disabled() {// {{{
|
||||
return this.data.Disabled === 'true'
|
||||
}// }}}
|
||||
dynamic() {// {{{
|
||||
return this.data.Dynamic === 'true'
|
||||
}// }}}
|
||||
name() {// {{{
|
||||
return this.data.Name.toLowerCase()
|
||||
}// }}}
|
||||
ttl() {// {{{
|
||||
return this.data.TTL
|
||||
}// }}}
|
||||
type() {// {{{
|
||||
return this.data.Type.toUpperCase()
|
||||
}// }}}
|
||||
value() {// {{{
|
||||
switch (this.type()) {
|
||||
case 'A':
|
||||
case 'AAAA':
|
||||
return this.data.Address
|
||||
|
||||
case 'CNAME':
|
||||
return this.data.CNAME
|
||||
}
|
||||
}// }}}
|
||||
labels() {// {{{
|
||||
return this.name().split('.')
|
||||
}// }}}
|
||||
copy(el, text) {// {{{
|
||||
el.classList.add('copy')
|
||||
navigator.clipboard.writeText(text)
|
||||
setTimeout(() => el.classList.remove('copy'), 200)
|
||||
}// }}}
|
||||
edit() {// {{{
|
||||
new RecordDialog(this).show()
|
||||
}// }}}
|
||||
set(key, value) {
|
||||
if (key != 'Value') {
|
||||
this.data[key] = value
|
||||
return
|
||||
}
|
||||
|
||||
switch(this.data.type.toUpperCase()) {
|
||||
case 'A':
|
||||
case 'AAAA':
|
||||
this.data.Address = value
|
||||
break
|
||||
|
||||
case 'CNAME':
|
||||
this.data.CNAME = value
|
||||
break
|
||||
}
|
||||
}
|
||||
toElements() {// {{{
|
||||
if (this.divFQDN === null) {
|
||||
this.imgIcon = document.createElement('img')
|
||||
this.divFQDN = document.createElement('div')
|
||||
this.divType = document.createElement('div')
|
||||
this.divValue = document.createElement('div')
|
||||
this.divSeparator = document.createElement('div')
|
||||
|
||||
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.divFQDN.innerHTML = `
|
||||
<span class="first-label">${this.labels()[0]}</span>
|
||||
<span class="rest-label">${this.labels().slice(1).join('.')}</span>
|
||||
`
|
||||
|
||||
this.divFQDN.addEventListener('click', event => {
|
||||
if (event.shiftKey)
|
||||
this.copy(event.target.closest('.fqdn'), this.name())
|
||||
else
|
||||
this.edit()
|
||||
})
|
||||
|
||||
this.divValue.addEventListener('click', event => {
|
||||
if (event.shiftKey)
|
||||
this.copy(event.target.closest('.value'), this.value())
|
||||
else
|
||||
this.edit()
|
||||
})
|
||||
}
|
||||
|
||||
this.divType.innerText = this.type()
|
||||
this.divValue.innerText = this.value()
|
||||
|
||||
return [this.imgIcon, this.divFQDN, this.divType, this.divValue, this.divSeparator]
|
||||
}// }}}
|
||||
}
|
||||
|
||||
class RecordDialog {
|
||||
constructor(record) {
|
||||
this.record = record
|
||||
}
|
||||
|
||||
show() {
|
||||
const dlg = document.createElement('dialog')
|
||||
dlg.id = "record-dialog"
|
||||
dlg.innerHTML = `
|
||||
<div>Name</div>
|
||||
<input type="text" class="name">
|
||||
|
||||
<div>Type</div>
|
||||
<select class="type">
|
||||
<option>A</option>
|
||||
<option>AAAA</option>
|
||||
<option>CNAME</option>
|
||||
</select>
|
||||
|
||||
<div>Value</div>
|
||||
<input type="text" class="value">
|
||||
|
||||
<div>TTL</div>
|
||||
<input type="text" class="ttl">
|
||||
|
||||
<div class="buttons">
|
||||
<button class="save">Save</button>
|
||||
<button class="close">Close</button>
|
||||
</div>
|
||||
`
|
||||
|
||||
dlg.querySelector('.name').value = this.record.name()
|
||||
dlg.querySelector('.type').value = this.record.type()
|
||||
dlg.querySelector('.value').value = this.record.value()
|
||||
dlg.querySelector('.ttl').value = this.record.ttl()
|
||||
|
||||
dlg.querySelector('.save').addEventListener('click', ()=>this.save())
|
||||
dlg.querySelector('.close').addEventListener('click', ()=>dlg.close())
|
||||
|
||||
dlg.addEventListener('close', ()=>dlg.remove())
|
||||
document.body.appendChild(dlg)
|
||||
dlg.showModal()
|
||||
}
|
||||
|
||||
save() {
|
||||
this.record.set('Name', dlg.querySelector('.name').value)
|
||||
this.record.set('Type', dlg.querySelector('.type').value)
|
||||
this.record.set('Value', dlg.querySelector('.value').value)
|
||||
this.record.set('TTL', dlg.querySelector('.ttl').value)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,13 +1,12 @@
|
|||
{{ define "page" }}
|
||||
<script type="module">
|
||||
import { Application } from "/js/{{ .Data.VERSION }}/dns.mjs"
|
||||
const app = new Application()
|
||||
window._app = new Application({{ .Data.DNSRecords }})
|
||||
</script>
|
||||
|
||||
<h1>{{ .Data.Identity }}</h1>
|
||||
|
||||
<div class="records-tree">
|
||||
{{ .Data.Tree }}
|
||||
<div id="records-tree">
|
||||
</div>
|
||||
|
||||
{{ end }}
|
||||
|
|
|
|||
|
|
@ -6,12 +6,12 @@ import (
|
|||
|
||||
// Standard
|
||||
"embed"
|
||||
"encoding/json"
|
||||
_ "encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"slices"
|
||||
"html/template"
|
||||
_ "html/template"
|
||||
)
|
||||
|
||||
var (
|
||||
|
|
@ -66,8 +66,10 @@ func rootHandler(w http.ResponseWriter, r *http.Request) {
|
|||
slices.SortFunc(entries, SortDNSRecord)
|
||||
data["DNSRecords"] = entries
|
||||
|
||||
/*
|
||||
tree := BuildRecordsTree(entries)
|
||||
htmlElements := tree.ToHTMLElements([]string{})
|
||||
data["TreeData"] = tree
|
||||
|
||||
var html string
|
||||
for _, el := range htmlElements {
|
||||
|
|
@ -78,6 +80,7 @@ func rootHandler(w http.ResponseWriter, r *http.Request) {
|
|||
|
||||
j, _ := json.Marshal(tree)
|
||||
os.WriteFile("/tmp/tree.json", j, 0644)
|
||||
*/
|
||||
|
||||
data["VERSION"] = VERSION
|
||||
page.Data = data
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue