Recursive tree version
This commit is contained in:
parent
42c5d43610
commit
97058d036d
8 changed files with 599 additions and 134 deletions
5
dns.go
5
dns.go
|
|
@ -15,6 +15,7 @@ type DNSRecord struct {
|
||||||
Name string
|
Name string
|
||||||
TTL string
|
TTL string
|
||||||
Type string
|
Type string
|
||||||
|
ParsedValue string // not from RouterOS, here to not having to have the value logics in frontend too.
|
||||||
|
|
||||||
Address string
|
Address string
|
||||||
CNAME string
|
CNAME string
|
||||||
|
|
@ -195,8 +196,8 @@ func (dp *DomainPart) ToHTMLElements(parts []string) []HTMLElement {
|
||||||
for _, rec := range subpart.Record {
|
for _, rec := range subpart.Record {
|
||||||
html := fmt.Sprintf(
|
html := fmt.Sprintf(
|
||||||
`
|
`
|
||||||
<div class="record" style="padding-left: %dpx" data-top="%s"><span>%s</span><span>%s</span></div>
|
<div class="record" style="padding-left: %dpx" data-top="%s"><div><span>%s</span><span>%s</span></div></div>
|
||||||
<div class="type">%s</div>
|
<div class="type"><div>%s</div></div>
|
||||||
<div class="value">%s</div>
|
<div class="value">%s</div>
|
||||||
`,
|
`,
|
||||||
(len(newParts)-1)*32,
|
(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 {
|
html {
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
|
|
@ -14,102 +30,151 @@ html {
|
||||||
|
|
||||||
body {
|
body {
|
||||||
font-family: sans-serif;
|
font-family: sans-serif;
|
||||||
|
font-size: 12pt;
|
||||||
|
margin-left: 32px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.records-tree {
|
#records-tree {
|
||||||
display: grid;
|
|
||||||
grid-template-columns: min-content min-content 1fr;
|
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
|
|
||||||
.show {
|
.folder {
|
||||||
display: block !important;
|
padding-left: 32px;
|
||||||
}
|
|
||||||
|
|
||||||
.top,
|
&.top-most {
|
||||||
.record,
|
padding-left: 0px;
|
||||||
.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;
|
|
||||||
|
|
||||||
&.open {
|
&.open {
|
||||||
.folder.open {
|
&>.label>img.open {
|
||||||
display: inline-block;
|
display: block;
|
||||||
}
|
}
|
||||||
.folder.closed {
|
}
|
||||||
|
|
||||||
|
&.closed {
|
||||||
|
&>.label>img.closed {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
&>.subfolders {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
&>.records {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&:not(.open) {
|
&>.label {
|
||||||
.folder.open {
|
display: grid;
|
||||||
display: none;
|
grid-template-columns: min-content min-content 1fr;
|
||||||
}
|
align-items: center;
|
||||||
.folder.closed {
|
padding: 5px 0px;
|
||||||
display: inline-block;
|
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 {
|
img {
|
||||||
|
height: 20px;
|
||||||
height: 16px;
|
margin-right: 6px;
|
||||||
display: none;
|
display: none;
|
||||||
margin-right: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
span:first-child {
|
|
||||||
color: #800033;
|
|
||||||
}
|
|
||||||
|
|
||||||
span:last-child {
|
|
||||||
color: #444;
|
|
||||||
font-weight: normal;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.top+.type,
|
&>.subfolders {
|
||||||
.top+.type+.value {
|
border-left: 1px solid var(--line-color);
|
||||||
background-color: #f8f8f8;
|
margin-left: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.top-most,
|
&>.records {
|
||||||
.top-most + .type,
|
padding-left: 30px;
|
||||||
.top-most + .type + .value
|
margin-left: 10px;
|
||||||
{
|
|
||||||
display:grid;
|
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
.record {
|
.copy {
|
||||||
display: none;
|
color: var(--copy-color) !important;
|
||||||
font-weight: normal;
|
|
||||||
color: #444;
|
|
||||||
|
|
||||||
span:first-child {
|
span {
|
||||||
color: #800033;
|
color: var(--copy-color) !important;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
span:last-child {
|
|
||||||
color: #888;
|
.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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.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
|
<svg
|
||||||
width="21.200001"
|
width="21.200001"
|
||||||
height="16.000025"
|
height="18"
|
||||||
viewBox="0 0 5.6091668 4.2333399"
|
viewBox="0 0 5.6091668 4.7625"
|
||||||
version="1.1"
|
version="1.1"
|
||||||
id="svg1"
|
id="svg1"
|
||||||
inkscape:version="1.4.2 (ebf0e94, 2025-05-08)"
|
inkscape:version="1.4 (e7c3feb, 2024-10-09)"
|
||||||
sodipodi:docname="icon_folder.svg"
|
sodipodi:docname="icon_folder.svg"
|
||||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||||
|
|
@ -24,12 +24,12 @@
|
||||||
inkscape:deskcolor="#d1d1d1"
|
inkscape:deskcolor="#d1d1d1"
|
||||||
inkscape:document-units="px"
|
inkscape:document-units="px"
|
||||||
inkscape:zoom="22.627417"
|
inkscape:zoom="22.627417"
|
||||||
inkscape:cx="4.065864"
|
inkscape:cx="16.175068"
|
||||||
inkscape:cy="7.6455921"
|
inkscape:cy="10.341437"
|
||||||
inkscape:window-width="1916"
|
inkscape:window-width="2190"
|
||||||
inkscape:window-height="1041"
|
inkscape:window-height="1404"
|
||||||
inkscape:window-x="1920"
|
inkscape:window-x="1463"
|
||||||
inkscape:window-y="1098"
|
inkscape:window-y="16"
|
||||||
inkscape:window-maximized="1"
|
inkscape:window-maximized="1"
|
||||||
inkscape:current-layer="layer1" />
|
inkscape:current-layer="layer1" />
|
||||||
<defs
|
<defs
|
||||||
|
|
@ -42,7 +42,7 @@
|
||||||
<title
|
<title
|
||||||
id="title1">folder-outline</title>
|
id="title1">folder-outline</title>
|
||||||
<rect
|
<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"
|
id="rect1"
|
||||||
width="4.6421375"
|
width="4.6421375"
|
||||||
height="3.0401909"
|
height="3.0401909"
|
||||||
|
|
|
||||||
|
Before Width: | Height: | Size: 1.9 KiB After Width: | Height: | Size: 1.9 KiB |
|
|
@ -3,11 +3,11 @@
|
||||||
|
|
||||||
<svg
|
<svg
|
||||||
width="21.200001"
|
width="21.200001"
|
||||||
height="16.000025"
|
height="18"
|
||||||
viewBox="0 0 5.6091669 4.2333399"
|
viewBox="0 0 5.6091669 4.7625"
|
||||||
version="1.1"
|
version="1.1"
|
||||||
id="svg1"
|
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"
|
sodipodi:docname="icon_folder_open.svg"
|
||||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||||
|
|
@ -23,15 +23,16 @@
|
||||||
inkscape:pagecheckerboard="0"
|
inkscape:pagecheckerboard="0"
|
||||||
inkscape:deskcolor="#d1d1d1"
|
inkscape:deskcolor="#d1d1d1"
|
||||||
inkscape:document-units="px"
|
inkscape:document-units="px"
|
||||||
inkscape:zoom="22.627417"
|
inkscape:zoom="17.076364"
|
||||||
inkscape:cx="16.440233"
|
inkscape:cx="35.487649"
|
||||||
inkscape:cy="12.551145"
|
inkscape:cy="7.9642249"
|
||||||
inkscape:window-width="1916"
|
inkscape:window-width="2190"
|
||||||
inkscape:window-height="1041"
|
inkscape:window-height="1404"
|
||||||
inkscape:window-x="1920"
|
inkscape:window-x="1463"
|
||||||
inkscape:window-y="1098"
|
inkscape:window-y="16"
|
||||||
inkscape:window-maximized="1"
|
inkscape:window-maximized="0"
|
||||||
inkscape:current-layer="layer1" />
|
inkscape:current-layer="layer1"
|
||||||
|
showgrid="false" />
|
||||||
<defs
|
<defs
|
||||||
id="defs1" />
|
id="defs1" />
|
||||||
<g
|
<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 {
|
export class Application {
|
||||||
constructor() {
|
constructor(records) {// {{{
|
||||||
this.addTopHandlers()
|
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')
|
||||||
|
|
||||||
|
for (const rec of this.records) {
|
||||||
|
const labels = rec.labels().reverse().slice(0, -1)
|
||||||
|
|
||||||
|
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)
|
||||||
}
|
}
|
||||||
|
|
||||||
addTopHandlers() {
|
return topFolder
|
||||||
for (const top of document.querySelectorAll('.top'))
|
}// }}}
|
||||||
top.addEventListener('click', event => this.handlerTop(event))
|
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
|
||||||
}
|
}
|
||||||
|
|
||||||
handlerTop(event) {
|
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')
|
const topEl = event.target.closest('.top')
|
||||||
console.log(topEl.dataset.self)
|
|
||||||
|
|
||||||
let records, types, values
|
let records, types, values
|
||||||
if (topEl.classList.contains('open')) {
|
if (topEl.classList.contains('open')) {
|
||||||
|
|
@ -29,13 +98,23 @@ export class Application {
|
||||||
r.classList.remove('show')
|
r.classList.remove('show')
|
||||||
|
|
||||||
topEl.classList.remove('open')
|
topEl.classList.remove('open')
|
||||||
|
} else {
|
||||||
|
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 {
|
} else {
|
||||||
records = document.querySelectorAll(`[data-top="${topEl.dataset.self}"]`)
|
records = document.querySelectorAll(`[data-top="${topEl.dataset.self}"]`)
|
||||||
types = document.querySelectorAll(`[data-top="${topEl.dataset.self}"] + .type`)
|
types = document.querySelectorAll(`[data-top="${topEl.dataset.self}"] + .type`)
|
||||||
values = document.querySelectorAll(`[data-top="${topEl.dataset.self}"] + .type + .value`)
|
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')
|
r.classList.add('show')
|
||||||
|
if (event.shiftKey && r.classList.contains('top'))
|
||||||
|
r.classList.add('open')
|
||||||
|
}
|
||||||
for (const r of types)
|
for (const r of types)
|
||||||
r.classList.add('show')
|
r.classList.add('show')
|
||||||
for (const r of values)
|
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" }}
|
{{ define "page" }}
|
||||||
<script type="module">
|
<script type="module">
|
||||||
import { Application } from "/js/{{ .Data.VERSION }}/dns.mjs"
|
import { Application } from "/js/{{ .Data.VERSION }}/dns.mjs"
|
||||||
const app = new Application()
|
window._app = new Application({{ .Data.DNSRecords }})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<h1>{{ .Data.Identity }}</h1>
|
<h1>{{ .Data.Identity }}</h1>
|
||||||
|
|
||||||
<div class="records-tree">
|
<div id="records-tree">
|
||||||
{{ .Data.Tree }}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{{ end }}
|
{{ end }}
|
||||||
|
|
|
||||||
|
|
@ -6,12 +6,12 @@ import (
|
||||||
|
|
||||||
// Standard
|
// Standard
|
||||||
"embed"
|
"embed"
|
||||||
"encoding/json"
|
_ "encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"slices"
|
"slices"
|
||||||
"html/template"
|
_ "html/template"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
|
@ -66,8 +66,10 @@ func rootHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
slices.SortFunc(entries, SortDNSRecord)
|
slices.SortFunc(entries, SortDNSRecord)
|
||||||
data["DNSRecords"] = entries
|
data["DNSRecords"] = entries
|
||||||
|
|
||||||
|
/*
|
||||||
tree := BuildRecordsTree(entries)
|
tree := BuildRecordsTree(entries)
|
||||||
htmlElements := tree.ToHTMLElements([]string{})
|
htmlElements := tree.ToHTMLElements([]string{})
|
||||||
|
data["TreeData"] = tree
|
||||||
|
|
||||||
var html string
|
var html string
|
||||||
for _, el := range htmlElements {
|
for _, el := range htmlElements {
|
||||||
|
|
@ -78,6 +80,7 @@ func rootHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
|
|
||||||
j, _ := json.Marshal(tree)
|
j, _ := json.Marshal(tree)
|
||||||
os.WriteFile("/tmp/tree.json", j, 0644)
|
os.WriteFile("/tmp/tree.json", j, 0644)
|
||||||
|
*/
|
||||||
|
|
||||||
data["VERSION"] = VERSION
|
data["VERSION"] = VERSION
|
||||||
page.Data = data
|
page.Data = data
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue