Added basic multidevice support
This commit is contained in:
parent
d0b0aba30d
commit
7feeacea42
7 changed files with 505 additions and 186 deletions
|
|
@ -1,26 +1,105 @@
|
|||
import { MessageBus } from '@mbus'
|
||||
|
||||
export class Application {
|
||||
constructor(records) {// {{{
|
||||
constructor() {// {{{
|
||||
window._mbus = new MessageBus()
|
||||
this.settings = new Settings()
|
||||
this.records = this.parseRecords(records)
|
||||
this.topFolder = new Folder(this, null, 'root')
|
||||
this.recordsTree = null
|
||||
this.devices = new Map()
|
||||
this.currentDevice = null
|
||||
|
||||
this.settingsIcon = null
|
||||
this.createIcon = null
|
||||
this.elHeader = null
|
||||
this.deviceSelector = null
|
||||
this.searchWidget = null
|
||||
|
||||
this.searchFor = ''
|
||||
this.resetDeviceState()
|
||||
this.renderApplication()
|
||||
this.retrieveDevices()
|
||||
.then(() => {
|
||||
this.deviceSelector.setDevices(this.devices)
|
||||
this.renderApplication()
|
||||
})
|
||||
.catch(err => {
|
||||
console.error(err)
|
||||
alert(err)
|
||||
})
|
||||
|
||||
this.renderFolders()
|
||||
this.render()
|
||||
_mbus.subscribe('device_selected', event => this.connectDevice(event.detail.devName))
|
||||
_mbus.subscribe('search', ()=>this.search())
|
||||
}// }}}
|
||||
|
||||
// resetDeviceState removes the device state such as the records, records tree and search field.
|
||||
resetDeviceState() {// {{{
|
||||
if (this.recordsTree) {
|
||||
this.recordsTree.remove()
|
||||
this.recordsTree = null
|
||||
return
|
||||
}
|
||||
|
||||
this.topFolder = new Folder(this, null, 'root')
|
||||
this.records = null
|
||||
this.recordsTree = null
|
||||
|
||||
if (this.searchWidget)
|
||||
this.searchWidget.reset()
|
||||
}// }}}
|
||||
|
||||
// retrieveDevices fetches the preconfigured devices from server and populates this.devices.
|
||||
retrieveDevices() {// {{{
|
||||
return new Promise((resolve, reject) => {
|
||||
fetch('/devices')
|
||||
.then(data => data.json())
|
||||
.then(json => {
|
||||
if (!json.OK) {
|
||||
reject(json.Error)
|
||||
return
|
||||
}
|
||||
|
||||
this.devices = new Map()
|
||||
for (const devData of json.Devices) {
|
||||
const dev = new Device(devData)
|
||||
this.devices.set(dev.name(), dev)
|
||||
}
|
||||
|
||||
resolve()
|
||||
})
|
||||
})
|
||||
}// }}}
|
||||
|
||||
// connectDevice resets the device state, retrieves the records and renders everything necessary.
|
||||
async connectDevice(devName) {// {{{
|
||||
this.resetDeviceState()
|
||||
if (devName == '-') {
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const dev = this.devices.get(devName)
|
||||
if (dev === undefined) {
|
||||
alert(`Unknown device '${devName}'`)
|
||||
return
|
||||
}
|
||||
this.currentDevice = dev
|
||||
|
||||
const records = await this.currentDevice.retrieveRecords()
|
||||
this.records = this.parseRecords(records)
|
||||
this.createFolders()
|
||||
this.renderDevice()
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
alert(err)
|
||||
}
|
||||
}// }}}
|
||||
|
||||
// filteredRecords takes the current search value and only returns records matching it.
|
||||
filteredRecords() {// {{{
|
||||
if (this.searchFor === '')
|
||||
const searchFor = this.searchWidget.value()
|
||||
if (searchFor === '')
|
||||
return this.records
|
||||
|
||||
const records = this.records.filter(r => {
|
||||
return (r.name().includes(this.searchFor))
|
||||
return (r.name().includes(searchFor))
|
||||
})
|
||||
return records
|
||||
}// }}}
|
||||
|
|
@ -30,7 +109,7 @@ export class Application {
|
|||
}// }}}
|
||||
|
||||
// cleanFolders removes all records from all folders.
|
||||
// renderFolders can then put moved records in the correct
|
||||
// createFolders can then put moved records in the correct
|
||||
// (or newly created) folders again when record names are updated.
|
||||
cleanFolders(folder) {// {{{
|
||||
if (folder === undefined)
|
||||
|
|
@ -45,7 +124,96 @@ export class Application {
|
|||
const i = this.records.findIndex(rec => rec.id() == id)
|
||||
this.records.splice(i, 1)
|
||||
}// }}}
|
||||
renderFolders() {// {{{
|
||||
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
|
||||
}// }}}
|
||||
|
||||
// renderApplication creates and updates application-level elements (not device level elements).
|
||||
renderApplication() {// {{{
|
||||
if (this.elHeader === null) {
|
||||
this.elHeader = document.createElement('div')
|
||||
this.elHeader.id = 'application-header'
|
||||
|
||||
this.deviceSelector = new DeviceSelectWidget(this.devices)
|
||||
this.searchWidget = new SearchWidget()
|
||||
|
||||
this.createIcon = document.createElement('img')
|
||||
this.createIcon.id = 'create-icon'
|
||||
this.createIcon.src = `/images/${_VERSION}/icon_create.svg`
|
||||
this.createIcon.addEventListener('click', () => new RecordDialog(new Record()).show())
|
||||
|
||||
this.settingsIcon = document.createElement('img')
|
||||
this.settingsIcon.id = 'settings-icon'
|
||||
this.settingsIcon.src = `/images/${_VERSION}/icon_settings.svg`
|
||||
this.settingsIcon.addEventListener('click', () => new SettingsDialog(this).show())
|
||||
|
||||
this.elHeader.appendChild(this.deviceSelector.render())
|
||||
this.elHeader.appendChild(this.searchWidget.render())
|
||||
this.elHeader.appendChild(this.createIcon)
|
||||
this.elHeader.appendChild(this.settingsIcon)
|
||||
|
||||
document.getElementById('app').appendChild(this.elHeader)
|
||||
document.body.addEventListener('keydown', event => this.handlerKeys(event))
|
||||
return
|
||||
}
|
||||
|
||||
this.deviceSelector.render()
|
||||
}// }}}
|
||||
|
||||
// renderDevice creates the device specific elements and also updates them.
|
||||
renderDevice() {// {{{
|
||||
// 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.getElementById('app').appendChild(this.recordsTree)
|
||||
}
|
||||
|
||||
// Top root folder doesn't have to be shown.
|
||||
const folders = Array.from(this.topFolder.subfolders.values())
|
||||
folders.sort(this.sortFolders)
|
||||
|
||||
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'))
|
||||
}// }}}
|
||||
|
||||
// createFolders goes through the filtered records and create new folders based on the record labels.
|
||||
// It also populates the child records.
|
||||
createFolders() {// {{{
|
||||
const records = this.filteredRecords()
|
||||
records.sort(this.sortRecords)
|
||||
|
||||
|
|
@ -85,91 +253,9 @@ export class Application {
|
|||
currFolder.addRecord(rec)
|
||||
}
|
||||
}// }}}
|
||||
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() {// {{{
|
||||
if (this.createIcon === null) {
|
||||
this.createIcon = document.createElement('img')
|
||||
this.createIcon.id = 'create-icon'
|
||||
this.createIcon.src = `/images/${_VERSION}/icon_create.svg`
|
||||
this.createIcon.addEventListener('click', () => new RecordDialog(new Record()).show())
|
||||
|
||||
this.settingsIcon = document.createElement('img')
|
||||
this.settingsIcon.id = 'settings-icon'
|
||||
this.settingsIcon.src = `/images/${_VERSION}/icon_settings.svg`
|
||||
this.settingsIcon.addEventListener('click', () => new SettingsDialog(this).show())
|
||||
|
||||
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)
|
||||
|
||||
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 finds the leaf folders and recursively removes empty ones.
|
||||
// These are usually left from moving records away from them.
|
||||
removeEmptyFolders(folder) {// {{{
|
||||
if (folder === undefined)
|
||||
folder = this.topFolder
|
||||
|
|
@ -189,6 +275,7 @@ export class Application {
|
|||
folder.div?.remove()
|
||||
}
|
||||
}// }}}
|
||||
|
||||
handlerKeys(event) {// {{{
|
||||
let handled = true
|
||||
|
||||
|
|
@ -229,15 +316,124 @@ export class Application {
|
|||
else
|
||||
document.body.classList.remove('boxed-folders')
|
||||
}// }}}
|
||||
|
||||
|
||||
// search sets the search filter and re-renders the records tree.
|
||||
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()
|
||||
this.createFolders()
|
||||
this.renderDevice()
|
||||
}// }}}
|
||||
}
|
||||
|
||||
class SearchWidget {
|
||||
constructor() {// {{{
|
||||
_mbus.subscribe('device_selected', event => {
|
||||
if (event.detail.devName == '-')
|
||||
this.searchEl?.classList.remove('show')
|
||||
else
|
||||
this.searchEl?.classList.add('show')
|
||||
})
|
||||
}// }}}
|
||||
render() {// {{{
|
||||
if (this.searchEl)
|
||||
return
|
||||
|
||||
this.searchEl = document.createElement('div')
|
||||
this.searchEl.id = 'search'
|
||||
this.searchEl.innerHTML = `
|
||||
<div class="search-label">Search</div>
|
||||
<input type="text" class="search-for">
|
||||
<button class="search">Search</button>
|
||||
`
|
||||
this.searchField = this.searchEl.querySelector('.search-for')
|
||||
this.searchField.addEventListener('keydown', event => {
|
||||
if (event.key == 'Enter')
|
||||
this.search()
|
||||
})
|
||||
|
||||
const searchButton = this.searchEl.querySelector('button.search')
|
||||
searchButton.addEventListener('click', () => this.search())
|
||||
|
||||
return this.searchEl
|
||||
}// }}}
|
||||
search() {// {{{
|
||||
_mbus.dispatch('search', { search: this.searchField.value.trim().toLowerCase() })
|
||||
}// }}}
|
||||
value() {// {{{
|
||||
return this.searchField.value.trim().toLowerCase()
|
||||
}// }}}
|
||||
reset() {// {{{
|
||||
this.searchField.value = ''
|
||||
}// }}}
|
||||
}
|
||||
|
||||
class Device {
|
||||
constructor(data) {// {{{
|
||||
this.data = data
|
||||
}// }}}
|
||||
name() {// {{{
|
||||
return this.data.Name?.toLowerCase() || ''
|
||||
}// }}}
|
||||
async retrieveRecords() {// {{{
|
||||
const data = await fetch(`/device/${this.name()}/dns_records`)
|
||||
const json = await data.json()
|
||||
if (!json.OK)
|
||||
throw new Error(json.Error)
|
||||
return json.Records
|
||||
}// }}}
|
||||
}
|
||||
|
||||
class DeviceSelectWidget {
|
||||
constructor(devices) {// {{{
|
||||
this.devices = devices
|
||||
|
||||
this.div = null
|
||||
this.elDeviceSelect = null
|
||||
this.currentlySelected = null
|
||||
}// }}}
|
||||
setDevices(devices) {// {{{
|
||||
this.devices = devices
|
||||
}// }}}
|
||||
render() {// {{{
|
||||
if (this.div === null) {
|
||||
this.div = document.createElement('div')
|
||||
this.div.classList.add('device-select')
|
||||
this.div.innerHTML = `
|
||||
<div class="device-name">Select device</div>
|
||||
<select></select>
|
||||
`
|
||||
|
||||
this.elDeviceSelect = this.div.querySelector('select')
|
||||
this.elDeviceSelect.addEventListener('change', () => this.notifyDeviceSelect())
|
||||
}
|
||||
this.restockDeviceSelect()
|
||||
return this.div
|
||||
}// }}}
|
||||
restockDeviceSelect() {// {{{
|
||||
this.elDeviceSelect.replaceChildren()
|
||||
const emptyOption = document.createElement('option')
|
||||
emptyOption.innerText = "-"
|
||||
emptyOption.dataset.devicename = "-"
|
||||
this.elDeviceSelect.appendChild(emptyOption)
|
||||
|
||||
this.devices.forEach(dev => {
|
||||
const option = document.createElement('option')
|
||||
option.innerText = dev.name()
|
||||
option.dataset.devicename = dev.name()
|
||||
this.elDeviceSelect.appendChild(option)
|
||||
})
|
||||
|
||||
if (this.currentlySelected !== null)
|
||||
this.elDeviceSelect.value = this.currentlySelected
|
||||
}// }}}
|
||||
notifyDeviceSelect() {// {{{
|
||||
const devName = this.elDeviceSelect.value
|
||||
this.currentlySelected = devName
|
||||
_mbus.dispatch('device_selected', { devName })
|
||||
}// }}}
|
||||
}
|
||||
|
||||
|
|
@ -334,10 +530,8 @@ class Folder {
|
|||
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')
|
||||
if (this.name() == '_no.domain')
|
||||
this.div.classList.add('no-domain')
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -565,7 +759,7 @@ class Record {
|
|||
save() {// {{{
|
||||
const created = (this.id() == '')
|
||||
|
||||
fetch('/record/save', {
|
||||
fetch(`/device/${_app.currentDevice.name()}/record`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(this.data),
|
||||
})
|
||||
|
|
@ -584,10 +778,10 @@ class Record {
|
|||
}
|
||||
|
||||
_app.cleanFolders()
|
||||
_app.renderFolders()
|
||||
_app.render()
|
||||
_app.createFolders()
|
||||
_app.renderDevice()
|
||||
|
||||
// renderFolders is setting the folder the record resides in.
|
||||
// createFolders is setting the folder the record resides in.
|
||||
// It can now be expanded to the parent folder.
|
||||
if (created) {
|
||||
this.openParentFolders()
|
||||
|
|
@ -603,7 +797,7 @@ class Record {
|
|||
if (!confirm(`Are you sure you want to delete ${this.name()}?`))
|
||||
return
|
||||
|
||||
fetch(`/record/delete/${this.id()}`)
|
||||
fetch(`/device/${_app.currentDevice.name()}/record/${this.id()}`, { method: 'DELETE' })
|
||||
.then(data => data.json())
|
||||
.then(json => {
|
||||
if (!json.OK) {
|
||||
|
|
@ -612,8 +806,8 @@ class Record {
|
|||
}
|
||||
_app.deleteRecord(this.id())
|
||||
_app.cleanFolders()
|
||||
_app.renderFolders()
|
||||
_app.render()
|
||||
_app.createFolders()
|
||||
_app.renderDevice()
|
||||
})
|
||||
}// }}}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue