Added basic multidevice support
This commit is contained in:
parent
d0b0aba30d
commit
7feeacea42
7 changed files with 505 additions and 186 deletions
|
|
@ -19,13 +19,16 @@ type Config struct {
|
|||
LogDir string
|
||||
}
|
||||
|
||||
Device struct {
|
||||
Devices []Device
|
||||
}
|
||||
|
||||
type Device struct {
|
||||
Name string
|
||||
Address string
|
||||
Port int
|
||||
Username string
|
||||
Password string
|
||||
Timeout int
|
||||
}
|
||||
}
|
||||
|
||||
func readConfig() (config Config, err error) {
|
||||
|
|
|
|||
36
main.go
36
main.go
|
|
@ -9,6 +9,8 @@ import (
|
|||
"log/slog"
|
||||
"os"
|
||||
"path"
|
||||
"slices"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
const VERSION = "v1"
|
||||
|
|
@ -23,7 +25,7 @@ var (
|
|||
initLogger *slog.Logger
|
||||
logger *slog.Logger
|
||||
|
||||
device RouterosDevice
|
||||
devices map[string]RouterosDevice
|
||||
)
|
||||
|
||||
func init() { // {{{
|
||||
|
|
@ -73,13 +75,45 @@ func initLogging(config Config) *slog.Logger { // {{{
|
|||
func main() {
|
||||
initLogger.Info("application", "version", VERSION)
|
||||
|
||||
devices = make(map[string]RouterosDevice)
|
||||
/*
|
||||
device.Host = config.Device.Address
|
||||
device.Port = config.Device.Port
|
||||
device.Username = config.Device.Username
|
||||
device.Password = config.Device.Password
|
||||
device.Timeout = config.Device.Timeout
|
||||
device.Init()
|
||||
*/
|
||||
|
||||
registerWebserverHandlers()
|
||||
startWebserver()
|
||||
}
|
||||
|
||||
func routerosDevice(name string) (dev RouterosDevice, err error) {
|
||||
var found bool
|
||||
if dev, found = devices[name]; found {
|
||||
logger.Debug("routeros", "op", "connection", "name", name, "cached", true)
|
||||
return
|
||||
}
|
||||
|
||||
i := slices.IndexFunc(config.Devices, func(d Device) bool {
|
||||
return d.Name == name
|
||||
})
|
||||
if i == -1 {
|
||||
err = fmt.Errorf("Unknown device '%s'", name)
|
||||
logger.Error("routeros", "op", "connection", "error", err)
|
||||
return
|
||||
}
|
||||
|
||||
logger.Debug("routeros", "name", name, "cached", false)
|
||||
confDev := config.Devices[i]
|
||||
dev.Host = confDev.Address
|
||||
dev.Port = confDev.Port
|
||||
dev.Username = confDev.Username
|
||||
dev.Password = confDev.Password
|
||||
dev.Timeout = confDev.Timeout
|
||||
dev.Init()
|
||||
devices[name] = dev
|
||||
|
||||
return
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,6 @@
|
|||
:root {
|
||||
--header-background: #acbb78;
|
||||
|
||||
--line-color: #ccc;
|
||||
--line-color-record: #eee;
|
||||
|
||||
|
|
@ -37,14 +39,27 @@ html {
|
|||
cursor: pointer;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 1.5em;
|
||||
|
||||
&:first-child {
|
||||
margin-top: 0px;
|
||||
}
|
||||
}
|
||||
|
||||
label {
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
html,
|
||||
body {
|
||||
margin: 0px;
|
||||
padding: 0px;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: sans-serif;
|
||||
font-size: 12pt;
|
||||
margin-left: 32px;
|
||||
|
||||
/* Boxed folders are a settings for the user. */
|
||||
&.boxed-folders {
|
||||
|
|
@ -90,28 +105,63 @@ button {
|
|||
padding: 4px 8px;
|
||||
}
|
||||
|
||||
#create-icon {
|
||||
position: absolute;
|
||||
top: 18px;
|
||||
right: 64px;
|
||||
#application-header {
|
||||
display: grid;
|
||||
grid-template-columns: min-content 1fr min-content min-content;
|
||||
align-items: center;
|
||||
border-bottom: 1px solid #a4bc52;
|
||||
background-color: var(--header-background);
|
||||
|
||||
|
||||
.device-select {
|
||||
padding: 16px 16px 16px 32px;
|
||||
|
||||
.device-name {
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
|
||||
#search {
|
||||
padding: 16px;
|
||||
display: grid;
|
||||
grid-template-columns: min-content min-content;
|
||||
grid-gap: 0px 8px;
|
||||
|
||||
&>* {
|
||||
display: none;
|
||||
}
|
||||
|
||||
&.show {
|
||||
&>* {
|
||||
display: initial;
|
||||
}
|
||||
}
|
||||
|
||||
.search-label {
|
||||
grid-column: 1 / -1;
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
|
||||
#create-icon {
|
||||
width: 24px;
|
||||
cursor: pointer;
|
||||
}
|
||||
margin-right: 16px;
|
||||
}
|
||||
|
||||
#settings-icon {
|
||||
position: absolute;
|
||||
top: 16px;
|
||||
right: 16px;
|
||||
#settings-icon {
|
||||
width: 32px;
|
||||
cursor: pointer;
|
||||
}
|
||||
margin-right: 16px;
|
||||
}
|
||||
|
||||
#search {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
#records-tree {
|
||||
white-space: nowrap;
|
||||
margin-top: calc(32px - 5px);
|
||||
/* padding from the topmost folder */
|
||||
margin-left: 32px;
|
||||
|
||||
.folder {
|
||||
padding-left: 32px;
|
||||
|
|
@ -120,7 +170,7 @@ button {
|
|||
padding-left: 0px;
|
||||
}
|
||||
|
||||
&.no-domain > .label {
|
||||
&.no-domain>.label {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -25,9 +25,9 @@
|
|||
borderopacity="1.0"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:zoom="1"
|
||||
inkscape:cx="-71"
|
||||
inkscape:cy="-52"
|
||||
inkscape:zoom="1.1646825"
|
||||
inkscape:cx="-71.693356"
|
||||
inkscape:cy="-55.809199"
|
||||
inkscape:document-units="px"
|
||||
inkscape:current-layer="layer1"
|
||||
showgrid="false"
|
||||
|
|
@ -62,6 +62,6 @@
|
|||
<path
|
||||
d="m -80.697917,15.345833 h -1.058333 v 1.058334 h -0.529167 v -1.058334 h -1.058333 v -0.529166 h 1.058333 v -1.058334 h 0.529167 v 1.058334 h 1.058333 M -80.16875,12.7 h -3.704167 c -0.293687,0 -0.529167,0.235479 -0.529167,0.529167 v 3.704166 a 0.52916667,0.52916667 0 0 0 0.529167,0.529167 h 3.704167 a 0.52916667,0.52916667 0 0 0 0.529166,-0.529167 v -3.704166 c 0,-0.293688 -0.238125,-0.529167 -0.529166,-0.529167 z"
|
||||
id="path1"
|
||||
style="stroke-width:0.264583;fill:#89a02c" />
|
||||
style="stroke-width:0.264583;fill:#000000" />
|
||||
</g>
|
||||
</svg>
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 2.3 KiB After Width: | Height: | Size: 2.3 KiB |
|
|
@ -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,11 +530,9 @@ 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')
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Subfolders are refreshed.
|
||||
|
|
@ -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()
|
||||
})
|
||||
}// }}}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,9 +1,6 @@
|
|||
{{ define "page" }}
|
||||
<script type="module">
|
||||
import { Application } from "/js/{{ .Data.VERSION }}/dns.mjs"
|
||||
window._app = new Application({{ .Data.DNSRecords }})
|
||||
window._app = new Application()
|
||||
</script>
|
||||
|
||||
<h1>{{ .Data.Identity }}</h1>
|
||||
|
||||
{{ end }}
|
||||
|
|
|
|||
125
webserver.go
125
webserver.go
|
|
@ -24,7 +24,7 @@ var (
|
|||
viewFS embed.FS
|
||||
)
|
||||
|
||||
func registerWebserverHandlers() {
|
||||
func registerWebserverHandlers() { // {{{
|
||||
var err error
|
||||
htmlEngine, err = HTMLTemplate.NewEngine(viewFS, staticFS, flagDev)
|
||||
if err != nil {
|
||||
|
|
@ -33,17 +33,31 @@ func registerWebserverHandlers() {
|
|||
}
|
||||
|
||||
http.HandleFunc("/", rootHandler)
|
||||
http.HandleFunc("/record/save", actionRecordSave)
|
||||
http.HandleFunc("/record/delete/{id}", actionRecordDelete)
|
||||
}
|
||||
|
||||
func startWebserver() {
|
||||
http.HandleFunc("/devices", actionDevices)
|
||||
http.HandleFunc("GET /device/{dev}/dns_records", actionDNSRecords)
|
||||
http.HandleFunc("POST /device/{dev}/record", actionRecordSave)
|
||||
http.HandleFunc("DELETE /device/{dev}/record/{id}", actionRecordDelete)
|
||||
} // }}}
|
||||
func startWebserver() { // {{{
|
||||
listen := fmt.Sprintf("%s:%d", config.Network.Address, config.Network.Port)
|
||||
logger.Info("webserver", "listen", listen)
|
||||
http.ListenAndServe(listen, nil)
|
||||
}
|
||||
} // }}}
|
||||
|
||||
func rootHandler(w http.ResponseWriter, r *http.Request) {
|
||||
func httpError(w http.ResponseWriter, err error) { // {{{
|
||||
resp := struct {
|
||||
OK bool
|
||||
Error string
|
||||
}{
|
||||
false,
|
||||
err.Error(),
|
||||
}
|
||||
|
||||
j, _ := json.Marshal(resp)
|
||||
w.Write(j)
|
||||
} // }}}
|
||||
|
||||
func rootHandler(w http.ResponseWriter, r *http.Request) { // {{{
|
||||
if r.URL.Path == "/" {
|
||||
page := HTMLTemplate.SimplePage{}
|
||||
page.Layout = "main"
|
||||
|
|
@ -52,21 +66,6 @@ func rootHandler(w http.ResponseWriter, r *http.Request) {
|
|||
var err error
|
||||
data := make(map[string]any)
|
||||
|
||||
data["Identity"], err = device.GetIdentity()
|
||||
if err != nil {
|
||||
w.Write([]byte(err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
var entries []DNSRecord
|
||||
entries, err = device.StaticDNSEntries()
|
||||
if err != nil {
|
||||
w.Write([]byte(err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
data["DNSRecords"] = entries
|
||||
|
||||
data["VERSION"] = VERSION
|
||||
page.Data = data
|
||||
|
||||
|
|
@ -79,25 +78,62 @@ func rootHandler(w http.ResponseWriter, r *http.Request) {
|
|||
}
|
||||
|
||||
htmlEngine.StaticResource(w, r)
|
||||
}
|
||||
|
||||
func httpError(w http.ResponseWriter, err error) {
|
||||
resp := struct {
|
||||
OK bool
|
||||
Error string
|
||||
}{
|
||||
false,
|
||||
err.Error(),
|
||||
} // }}}
|
||||
func actionDevices(w http.ResponseWriter, r *http.Request) { // {{{
|
||||
var devs []Device
|
||||
for _, dev := range config.Devices {
|
||||
dev.Password = ""
|
||||
devs = append(devs, dev)
|
||||
}
|
||||
|
||||
j, _ := json.Marshal(resp)
|
||||
w.Write(j)
|
||||
}
|
||||
j, _ := json.Marshal(struct {
|
||||
OK bool
|
||||
Devices []Device
|
||||
}{
|
||||
true,
|
||||
devs,
|
||||
})
|
||||
|
||||
w.Write(j)
|
||||
} // }}}
|
||||
|
||||
func actionDNSRecords(w http.ResponseWriter, r *http.Request) { // {{{
|
||||
devname := r.PathValue("dev")
|
||||
|
||||
device, err := routerosDevice(devname)
|
||||
if err != nil {
|
||||
httpError(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
var records []DNSRecord
|
||||
records, err = device.StaticDNSEntries()
|
||||
if err != nil {
|
||||
httpError(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
j, _ := json.Marshal(struct {
|
||||
OK bool
|
||||
Records []DNSRecord
|
||||
}{
|
||||
true,
|
||||
records,
|
||||
})
|
||||
w.Write(j)
|
||||
|
||||
} // }}}
|
||||
func actionRecordSave(w http.ResponseWriter, r *http.Request) { // {{{
|
||||
devName := r.PathValue("dev")
|
||||
device, err := routerosDevice(devName)
|
||||
if err != nil {
|
||||
httpError(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
func actionRecordSave(w http.ResponseWriter, r *http.Request) {
|
||||
body, _ := io.ReadAll(r.Body)
|
||||
var record DNSRecord
|
||||
err := json.Unmarshal(body, &record)
|
||||
err = json.Unmarshal(body, &record)
|
||||
if err != nil {
|
||||
httpError(w, err)
|
||||
return
|
||||
|
|
@ -117,17 +153,22 @@ func actionRecordSave(w http.ResponseWriter, r *http.Request) {
|
|||
Record DNSRecord
|
||||
}{true, record})
|
||||
w.Write(j)
|
||||
}
|
||||
} // }}}
|
||||
func actionRecordDelete(w http.ResponseWriter, r *http.Request) { // {{{
|
||||
devName := r.PathValue("dev")
|
||||
device, err := routerosDevice(devName)
|
||||
if err != nil {
|
||||
httpError(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
func actionRecordDelete(w http.ResponseWriter, r *http.Request) {
|
||||
id := r.PathValue("id")
|
||||
|
||||
if id == "" {
|
||||
httpError(w, errors.New("No ID provided"))
|
||||
return
|
||||
}
|
||||
|
||||
err := device.DeleteDNSEntry(id)
|
||||
err = device.DeleteDNSEntry(id)
|
||||
if err != nil {
|
||||
httpError(w, err)
|
||||
logger.Error("webserver", "op", "record_delete", "error", err)
|
||||
|
|
@ -138,4 +179,4 @@ func actionRecordDelete(w http.ResponseWriter, r *http.Request) {
|
|||
OK bool
|
||||
}{true})
|
||||
w.Write(j)
|
||||
}
|
||||
} // }}}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue