Compare commits

...

2 commits

Author SHA1 Message Date
Magnus Åhall
7213ee7a28 Create new devices 2026-02-26 22:59:42 +01:00
Magnus Åhall
06054c8d39 Delete device, better device edit dialog 2026-02-26 22:34:13 +01:00
4 changed files with 187 additions and 32 deletions

View file

@ -48,7 +48,7 @@ func readConfig() (config Config, err error) {
return
}
func (cfg *Config) UpdateDevice(currentName string, deviceToUpdate Device) (dev Device, err error) {
func (cfg *Config) UpdateDevice(currentName string, deviceToUpdate Device) (dev Device, err error) {// {{{
i := slices.IndexFunc(cfg.Devices, func(d Device) bool {
return strings.TrimSpace(strings.ToLower(d.Name)) == strings.TrimSpace(strings.ToLower(currentName))
@ -104,4 +104,14 @@ func (cfg *Config) UpdateDevice(currentName string, deviceToUpdate Device) (dev
err = os.WriteFile(cfg.filename, j, 0600)
return
}
}// }}}
func (cfg *Config) DeleteDevice(devname string) (err error) {// {{{
cfg.Devices = slices.DeleteFunc(cfg.Devices, func(d Device) bool {
return strings.TrimSpace(strings.ToLower(d.Name)) == strings.TrimSpace(strings.ToLower(devname))
})
j, _ := json.Marshal(cfg)
err = os.WriteFile(cfg.filename, j, 0600)
return
}// }}}

View file

@ -35,6 +35,7 @@ html {
*:after {
box-sizing: inherit;
}
*:focus {
outline: none;
}
@ -150,6 +151,7 @@ button {
&.show {
border-right: var(--header-border);
&>* {
display: initial;
}
@ -478,7 +480,7 @@ button {
#device-dialog {
display: grid;
grid-template-columns: min-content min-content;
align-items: top;
align-items: start;
grid-gap: 16px;
.devices {
@ -486,10 +488,28 @@ button {
flex-direction: column;
gap: 8px;
align-items: flex-start;
min-height: 256px;
height: 75vh;
.header {
width: 100%;
display: grid;
grid-template-columns: 1fr min-content;
margin-bottom: 8px;
font-weight: bold;
.filter {
grid-column: 1 / -1;
margin-top: 8px;
margin-bottom: 8px;
}
}
.device-list {
overflow-y: auto;
min-width: 256px;
justify-items: start;
.device {
border: 1px solid #aaa;
@ -498,12 +518,21 @@ button {
border-radius: 8px;
cursor: pointer;
user-select: none;
white-space: nowrap;
margin-bottom: 16px;
margin-right: 16px;
&.selected {
background-color: var(--header-background);
}
&.filtered {
display: none;
}
}
}
}
.fields {
display: grid;

View file

@ -26,6 +26,7 @@ export class Application {
})
_mbus.subscribe('device_selected', event => this.connectDevice(event.detail.devName))
_mbus.subscribe('device_deleted', event => this.deleteDevice(event.detail.devName))
_mbus.subscribe('search', () => this.search())
}// }}}
@ -96,6 +97,15 @@ export class Application {
}
}// }}}
// deleteDevice removes it from the list and resets app state if it was connected.
deleteDevice(devName) {// {{{
this.devices.delete(devName)
if (this.currentDevice?.name() == devName) {
this.resetDeviceState()
this.settings.set('last_device', '')
}
}// }}}
// filteredRecords takes the current search value and only returns records matching it.
filteredRecords() {// {{{
const searchFor = this.searchWidget.value()
@ -420,6 +430,9 @@ class DeviceSelectWidget {
this.div = null
this.elDeviceSelect = null
this.currentlySelected = null
_mbus.subscribe('device_deleted', event => this.deleteDevice(event.detail.devName))
_mbus.subscribe('device_updated', event => this.updateDevice(event.detail.device))
}// }}}
setDevices(devices) {// {{{
this.devices = devices
@ -448,7 +461,9 @@ class DeviceSelectWidget {
emptyOption.dataset.devicename = "-"
this.elDeviceSelect.appendChild(emptyOption)
this.devices.forEach(dev => {
const devNames = Array.from(this.devices.keys()).sort()
devNames.forEach(devName => {
const dev = this.devices.get(devName)
const option = document.createElement('option')
option.innerText = dev.name()
option.dataset.devicename = dev.name()
@ -467,6 +482,15 @@ class DeviceSelectWidget {
this.elDeviceSelect.value = devname
this.notifyDeviceSelect()
}// }}}
deleteDevice(devName) {// {{{
this.devices.delete(devName)
this.restockDeviceSelect()
}// }}}
updateDevice(dev) {// {{{
console.log(dev)
this.devices.set(dev.name(), dev)
this.restockDeviceSelect()
}// }}}
}
class Folder {
@ -1012,7 +1036,14 @@ class DeviceDialog {
this.dlg.id = 'device-dialog'
this.dlg.innerHTML = `
<div class="devices">
<div class="header">Devices</div>
<div class="header">
<div>Devices</div>
<img class="create" src="/images/${_VERSION}/icon_create.svg">
<input type="text" class="filter" placeholder="Filter">
</div>
<div class="device-list">
</div>
</div>
<div class="fields">
<div>Name</div>
@ -1032,12 +1063,13 @@ class DeviceDialog {
<div class="actions">
<button class="delete">Delete</button>
<button class="update">Update</button>
<button class="update">Save</button>
</div>
</div>
`
this.elDevices = this.dlg.querySelector('.devices')
this.elDeviceList = this.dlg.querySelector('.device-list')
this.elFields = this.dlg.querySelector('.fields')
this.elName = this.dlg.querySelector('.fields .name')
this.elAddress = this.dlg.querySelector('.fields .address')
@ -1045,6 +1077,30 @@ class DeviceDialog {
this.elUsername = this.dlg.querySelector('.fields .username')
this.elPassword = this.dlg.querySelector('.fields .password')
this.dlg.querySelector('.create').addEventListener('click', () => this.createDevice())
this.dlg.querySelector('.filter').addEventListener('input', () => this.filterDevices())
this.dlg.querySelector('.update').addEventListener('click', () => this.updateDevice())
this.dlg.querySelector('.delete').addEventListener('click', () => this.deleteDevice())
this.dlg.addEventListener('close', () => this.dlg.remove())
this.updateDeviceList()
document.body.appendChild(this.dlg)
this.dlg.showModal()
}// }}}
filterDevices() {// {{{
const filter = this.dlg.querySelector('.filter').value.toLowerCase()
this.elDeviceList.querySelectorAll('.device').forEach(dev=>{
if (dev.innerText.toLowerCase().includes(filter))
dev.classList.remove('filtered')
else
dev.classList.add('filtered')
})
}// }}}
updateDeviceList() {// {{{
this.elDeviceList.replaceChildren()
const devices = Array.from(this.devices.values())
devices.sort((a, b) => {
if (a.name() < b.name()) return -1
@ -1054,16 +1110,11 @@ class DeviceDialog {
devices.forEach(dev => {
const devEl = document.createElement('div')
devEl.classList.add('device')
devEl.dataset.name = dev.name()
devEl.innerText = dev.name()
devEl.addEventListener('click', () => this.editDevice(dev, devEl))
this.elDevices.appendChild(devEl)
this.elDeviceList.appendChild(devEl)
})
this.dlg.querySelector('.update').addEventListener('click', ()=>this.updateDevice())
this.dlg.addEventListener('close', ()=>this.dlg.remove())
document.body.appendChild(this.dlg)
this.dlg.showModal()
}// }}}
editDevice(dev, devEl) {// {{{
@ -1078,6 +1129,26 @@ class DeviceDialog {
this.elUsername.value = dev.username()
this.elPassword.value = dev.password()
}// }}}
async createDevice() {// {{{
let name = prompt('Name of new device')
if (name === null || name.trim() === '')
return
name = name.trim().toLowerCase()
// Make sure it doesn't already exist.
if (this.devices.has(name)) {
alert('The device already exist.')
return
}
const dev = new Device({Name: name, Port: 443}, true)
this.devices.set(name, dev)
this.updateDeviceList()
const devEl = this.elDeviceList.querySelector(`[data-name="${dev.name()}"]`)
this.editDevice(dev, devEl)
}// }}}
async updateDevice() {// {{{
const req = {
CurrentName: this.device.name(),
@ -1096,6 +1167,10 @@ class DeviceDialog {
body: JSON.stringify(req),
})
const json = await data.json()
if (!json.OK) {
alert(json.Error)
return
}
this.device.data.Name = json.Device.Name
this.device.data.Address = json.Device.Address
@ -1110,4 +1185,32 @@ class DeviceDialog {
alert(err)
}
}// }}}
async deleteDevice() {// {{{
const devname = this.device.name()
if (!confirm(`Do you want to delete '${devname}'?`))
return
try {
const data = await fetch(`/device/${devname}`, { method: 'DELETE' })
const json = await data.json()
if (!json.OK) {
alert(json.Error)
return
}
this.elName.value = ''
this.elAddress.value = ''
this.elPort.value = ''
this.elUsername.value = ''
this.elPassword.value = ''
this.devices.delete(devname)
this.device = null
this.updateDeviceList()
_mbus.dispatch('device_deleted', { devName: devname })
} catch (err) {
console.error(err)
alert(err)
}
}// }}}
}

View file

@ -35,6 +35,7 @@ func registerWebserverHandlers() { // {{{
http.HandleFunc("/", rootHandler)
http.HandleFunc("GET /devices", actionDevices)
http.HandleFunc("POST /device", actionDeviceUpdate)
http.HandleFunc("DELETE /device/{dev}", actionDeviceDelete)
http.HandleFunc("GET /device/{dev}/dns_records", actionDNSRecords)
http.HandleFunc("POST /device/{dev}/record", actionRecordSave)
http.HandleFunc("DELETE /device/{dev}/record/{id}", actionRecordDelete)
@ -126,6 +127,18 @@ func actionDeviceUpdate(w http.ResponseWriter, r *http.Request) { // {{{
})
w.Write(j)
} // }}}
func actionDeviceDelete(w http.ResponseWriter, r *http.Request) { // {{{
devname := r.PathValue("dev")
err := config.DeleteDevice(devname)
if err != nil {
httpError(w, err)
return
}
j, _ := json.Marshal(struct{ OK bool }{true})
w.Write(j)
} // }}}
func actionDNSRecords(w http.ResponseWriter, r *http.Request) { // {{{
devname := r.PathValue("dev")