382 lines
9.9 KiB
JavaScript
382 lines
9.9 KiB
JavaScript
export class Application {
|
|
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')
|
|
|
|
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)
|
|
}
|
|
|
|
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')
|
|
|
|
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 {
|
|
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) {
|
|
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)
|
|
r.classList.add('show')
|
|
|
|
topEl.classList.add('open')
|
|
}
|
|
|
|
|
|
}// }}}
|
|
}
|
|
|
|
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.render())
|
|
|
|
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() {// {{{
|
|
return this.data.ParsedValue
|
|
}// }}}
|
|
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) {// {{{
|
|
this.data[key] = value
|
|
}// }}}
|
|
render() {// {{{
|
|
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]
|
|
}// }}}
|
|
save() {// {{{
|
|
fetch('/record/save', {
|
|
method: 'POST',
|
|
body: JSON.stringify(this.data),
|
|
})
|
|
.then(data => data.json())
|
|
.then(json => {
|
|
if (!json.OK) {
|
|
alert(json.Error)
|
|
return
|
|
}
|
|
})
|
|
}// }}}
|
|
}
|
|
|
|
class RecordDialog {
|
|
constructor(record) {// {{{
|
|
this.record = record
|
|
}// }}}
|
|
|
|
show() {// {{{
|
|
this.dlg = document.createElement('dialog')
|
|
this.dlg.id = "record-dialog"
|
|
this.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>
|
|
`
|
|
|
|
this.dlg.querySelector('.name').value = this.record.name()
|
|
this.dlg.querySelector('.type').value = this.record.type()
|
|
this.dlg.querySelector('.value').value = this.record.value()
|
|
this.dlg.querySelector('.ttl').value = this.record.ttl();
|
|
|
|
['.name', '.type', '.value', '.ttl'].forEach(v =>
|
|
this.dlg.querySelector(v).addEventListener('keydown', event => this.enterKeyHandler(event))
|
|
)
|
|
|
|
this.dlg.querySelector('.save').addEventListener('click', () => this.save())
|
|
this.dlg.querySelector('.close').addEventListener('click', () => this.dlg.close())
|
|
|
|
this.dlg.addEventListener('close', () => this.dlg.remove())
|
|
document.body.appendChild(this.dlg)
|
|
this.dlg.showModal()
|
|
}// }}}
|
|
enterKeyHandler(event) {// {{{
|
|
if (event.key == "Enter")
|
|
this.save()
|
|
}// }}}
|
|
save() {// {{{
|
|
this.record.set('Name', this.dlg.querySelector('.name').value)
|
|
this.record.set('Type', this.dlg.querySelector('.type').value)
|
|
this.record.set('ParsedValue', this.dlg.querySelector('.value').value)
|
|
this.record.set('TTL', this.dlg.querySelector('.ttl').value)
|
|
this.record.render()
|
|
this.record.save()
|
|
this.dlg.close()
|
|
}// }}}
|
|
}
|