diff --git a/dns.go b/dns.go
index a88ded2..9b86886 100644
--- a/dns.go
+++ b/dns.go
@@ -10,15 +10,18 @@ import (
type DNSRecord struct {
ID string `json:".id"`
- Address string
Disabled string
Dynamic string
Name string
TTL string
Type string
+
+ Address string
+ CNAME string
}
type DomainPart struct {
+ Record DNSRecord
Subparts map[string]*DomainPart `json:",omitempty"`
}
@@ -53,6 +56,17 @@ func SortDNSRecord(a, b DNSRecord) int {
return 0
}
+func (r DNSRecord) String() string {
+ switch r.Type {
+ case "A", "AAAA":
+ return r.Address
+ case "CNAME":
+ return r.CNAME
+ }
+
+ return fmt.Sprintf("Implement type '%s'", r.Type)
+}
+
func (r DNSRecord) Parts() int {
return len(strings.Split(r.Name, "."))
}
@@ -92,6 +106,7 @@ func BuildRecordsTree(records []DNSRecord) *DomainPart {
for _, part := range currentDomainNameSplit {
if nextDomainPart, found := curPart.Subparts[part]; !found {
newPart := new(DomainPart)
+ newPart.Record = record
newPart.Subparts = make(map[string]*DomainPart)
curPart.Subparts[strings.ToLower(part)] = newPart
curPart = newPart
@@ -109,12 +124,12 @@ type HTMLElement struct {
HTML string
}
-func (dp *DomainPart) ToHTMLElements(parts []string, idFrom int) []HTMLElement {
+func (dp *DomainPart) ToHTMLElements(parts []string) []HTMLElement {
var lines []HTMLElement
sortedParts := slices.Sorted(maps.Keys(dp.Subparts))
- for i, part := range sortedParts {
+ for _, part := range sortedParts {
subpart := dp.Subparts[part]
newParts := append(parts, part)
@@ -130,38 +145,67 @@ func (dp *DomainPart) ToHTMLElements(parts []string, idFrom int) []HTMLElement {
mostSpecificPart += "."
}
+ if len(subpart.Subparts) != 0 {
+ topmost := ""
+ if len(newParts) == 1 {
+ topmost = "top-most"
+ }
+ html := fmt.Sprintf(`
+
+

+

+
%s%s
+
+
+
+ `,
+ topmost, // .top-most
+ restPart, // data-top
+ fqdn, // data-self
+ (len(newParts)-1)*32, // margin-left
+ VERSION, // images/
+ VERSION, // images/
+ mostSpecificPart, // innerText
+ restPart,
+ )
+ lines = append(lines, HTMLElement{Header: true, HTML: html})
+ subLines := subpart.ToHTMLElements(newParts)
+ lines = append(lines, subLines...)
+ }
+
+ }
+
+ for _, part := range sortedParts {
+ subpart := dp.Subparts[part]
+ newParts := append(parts, part)
+
+ reversedParts := make([]string, len(newParts))
+ copy(reversedParts, newParts)
+ slices.Reverse(reversedParts)
+ //fqdn := strings.Join(reversedParts, ".")
+
+ mostSpecificPart := reversedParts[0]
+ restPart := ""
+ if len(reversedParts) > 1 {
+ restPart = strings.Join(reversedParts[1:], ".")
+ mostSpecificPart += "."
+ }
+
if len(subpart.Subparts) == 0 {
html := fmt.Sprintf(
`
- %s%s
- hum
+ %s%s
+ %s
+ %s
`,
- len(newParts)*32,
- idFrom + i, // data-i
+ (len(newParts)-1)*32,
restPart, // data-top
mostSpecificPart,
restPart,
- idFrom + i,
+ subpart.Record.Type,
+ subpart.Record.String(),
)
lines = append(lines, HTMLElement{Header: false, HTML: html})
- } else {
- html := fmt.Sprintf(`
-
- %s%s
-
-
- `,
- idFrom + i, // data-i
- restPart, // data-top
- fqdn, // data-self
- len(newParts)*32, // margin-left
- mostSpecificPart, // innerText
- restPart,
- idFrom + i,
- )
- lines = append(lines, HTMLElement{Header: true, HTML: html})
- subLines := subpart.ToHTMLElements(newParts, idFrom + i)
- lines = append(lines, subLines...)
}
}
diff --git a/static/css/index.css b/static/css/index.css
index f51925b..7e921e4 100644
--- a/static/css/index.css
+++ b/static/css/index.css
@@ -18,10 +18,18 @@ body {
.records-tree {
display: grid;
- grid-template-columns: min-content 1fr;
+ grid-template-columns: min-content min-content 1fr;
white-space: nowrap;
- .top, .record, .type {
+ .show {
+ display: block !important;
+ }
+
+ .top,
+ .record,
+ .type,
+ .value {
+ display: none;
border-bottom: 1px solid #ccc;
padding: 4px 0px;
}
@@ -29,9 +37,38 @@ body {
.top {
font-weight: bold;
background-color: #f8f8f8;
+ user-select: none;
+ grid-template-columns: repeat(3, min-content);
+ align-items: center;
+
+ &.open {
+ .folder.open {
+ display: inline-block;
+ }
+ .folder.closed {
+ display: none;
+ }
+ }
+
+ &:not(.open) {
+ .folder.open {
+ display: none;
+ }
+ .folder.closed {
+ display: inline-block;
+ }
+ }
+
+ img {
+
+ height: 16px;
+ display: none;
+ margin-right: 4px;
+ }
+
span:first-child {
- /*color: #004680;*/
+ color: #800033;
}
span:last-child {
@@ -39,19 +76,24 @@ body {
font-weight: normal;
}
}
- .top + .type {
- background-color: #f8f;
+
+ .top+.type,
+ .top+.type+.value {
+ background-color: #f8f8f8;
+ }
+
+ .top-most,
+ .top-most + .type,
+ .top-most + .type + .value
+ {
+ display:grid;
}
.record {
- /*display: none;*/
+ display: none;
font-weight: normal;
color: #444;
- &.show {
- display: block;
- }
-
span:first-child {
color: #800033;
}
@@ -61,7 +103,13 @@ body {
}
}
- .record + .type {
- color: #f0f;
+ .record+.type,
+ .record+.type+.value {
+ display: none;
+ padding-left: 16px;
+ }
+
+ .record+.type+.value {
+ color: #800033;
}
}
diff --git a/static/images/icon_folder.svg b/static/images/icon_folder.svg
new file mode 100644
index 0000000..3ab4b80
--- /dev/null
+++ b/static/images/icon_folder.svg
@@ -0,0 +1,56 @@
+
+
+
+
diff --git a/static/images/icon_folder_open.svg b/static/images/icon_folder_open.svg
new file mode 100644
index 0000000..be1e573
--- /dev/null
+++ b/static/images/icon_folder_open.svg
@@ -0,0 +1,58 @@
+
+
+
+
diff --git a/static/js/dns.mjs b/static/js/dns.mjs
index b3dc270..91187d5 100644
--- a/static/js/dns.mjs
+++ b/static/js/dns.mjs
@@ -1,35 +1,6 @@
export class Application {
constructor() {
this.addTopHandlers()
- this.moveRecords()
- }
-
- moveRecords() {
- const records = Array.from(document.querySelectorAll('.record'))
-
- records.sort((a, b) => {
- if (a.innerText < b.innerText) return 1
- if (a.innerText > b.innerText) return -1
- return 0
- })
-
- for (const r of records) {
- const subTops = document.querySelectorAll(`.top[data-top="${r.dataset.top}"] + .type`)
- if (subTops === null) {
- continue
- }
-
- let lastSubTop = subTops.item(subTops.length - 1)
- if (lastSubTop === null) {
- lastSubTop = document.querySelector(`.top[data-self="${r.dataset.top}"] + .type`)
- }
-
- if (lastSubTop !== null) {
- const other = Array.from(document.querySelectorAll(`[data-i="${r.dataset.i}"]`))
- console.log(other)
- lastSubTop.after(...other)
- }
- }
}
addTopHandlers() {
@@ -39,10 +10,40 @@ export class Application {
handlerTop(event) {
const topEl = event.target.closest('.top')
+ console.log(topEl.dataset.self)
- const records = document.querySelectorAll(`.record[data-top="${topEl.dataset.self}"]`)
- for (const r of records) {
- r.classList.toggle('show')
+ let records, types, values
+ if (topEl.classList.contains('open')) {
+ 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`)
+
+ for (const r of records) {
+ r.classList.remove('show')
+ r.classList.remove('open')
+ }
+
+ for (const r of types)
+ r.classList.remove('show')
+ for (const r of values)
+ r.classList.remove('show')
+
+ 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`)
+
+ for (const r of records)
+ r.classList.add('show')
+ for (const r of types)
+ r.classList.add('show')
+ for (const r of values)
+ r.classList.add('show')
+
+ topEl.classList.add('open')
}
+
+
}
}
diff --git a/webserver.go b/webserver.go
index e555f39..d4fb454 100644
--- a/webserver.go
+++ b/webserver.go
@@ -67,18 +67,11 @@ func rootHandler(w http.ResponseWriter, r *http.Request) {
data["DNSRecords"] = entries
tree := BuildRecordsTree(entries)
- htmlElements := tree.ToHTMLElements([]string{}, 0)
+ htmlElements := tree.ToHTMLElements([]string{})
var html string
for _, el := range htmlElements {
- if el.Header {
- html += el.HTML
- }
- }
- for _, el := range htmlElements {
- if !el.Header {
- html += el.HTML
- }
+ html += el.HTML
}
data["Tree"] = template.HTML(html)