feature: popupOnStart allows for many feedback options when actions are started (#227)

This commit is contained in:
James Read 2024-02-08 22:54:08 +01:00 committed by GitHub
parent dc0cf33d37
commit c12431d8a3
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 401 additions and 187 deletions

View File

@ -8,36 +8,38 @@ listenAddressSingleHTTPFrontend: 0.0.0.0:1337
# Choose from INFO (default), WARN and DEBUG # Choose from INFO (default), WARN and DEBUG
logLevel: "INFO" logLevel: "INFO"
entities:
- name: container
file: /etc/OliveTin/entities/containers.yaml
icon: container
helpers:
- title: refresh container file
shell: "docker ps --format '{{ .state }}' > /etc/OliveTin/entities/containers.yaml"
execOnStartup: true
execOnCron:
- "*/1 * * * * *"
# Actions (buttons) to show up on the WebUI: # Actions (buttons) to show up on the WebUI:
actions: actions:
- title: date - title: Restart media container
shell: date shell: docker restart mediacontainer
icon: clock icon: box
popupOnStart: true
- title: start
shell: docker start {{ container.name }}
icon: start
entity: container entity: container
- title: Check disk space
icon: disk
shell: df -h /media
popupOnStart: execution-dialog-stdout-only
- title: check dmesg logs
shell: dmesg | tail
icon: logs
popupOnStart: execution-dialog
# This will run a simple script that you create. # This will run a simple script that you create.
- title: Run backup script - title: Run backup script
shell: /opt/backupScript.sh shell: /opt/backupScript.sh
shellAfterCompleted: "apprise -t 'Notification: Backup script completed' -b 'The backup script completed with code {{ exitCode}}. The log is: \n {{ stdout }} '" shellAfterCompleted: "apprise -t 'Notification: Backup script completed' -b 'The backup script completed with code {{ exitCode}}. The log is: \n {{ stdout }} '"
maxConcurrent: 1 maxConcurrent: 1
timeout: 10
icon: backup icon: backup
popupOnStart: execution-dialog
- title: date
shell: date
timeout: 6
icon: clock
popupOnStart: execution-button
# This will send 1 ping (-c 1) # This will send 1 ping (-c 1)
# Docs: https://docs.olivetin.app/action-ping.html # Docs: https://docs.olivetin.app/action-ping.html
@ -58,13 +60,6 @@ actions:
default: 1 default: 1
description: How many times to do you want to ping? description: How many times to do you want to ping?
# Restart lightdm on host "server1"
# Docs: https://docs.olivetin.app/action-ping.html
- title: restart httpd
titleAlias: restart_httpd
icon: restart
shell: ssh root@server1 'service httpd restart'
# OliveTin can run long-running jobs like Ansible playbooks. # OliveTin can run long-running jobs like Ansible playbooks.
# #
# For such jobs, you will need to install ansible-playbook on the host where # For such jobs, you will need to install ansible-playbook on the host where
@ -76,6 +71,14 @@ actions:
shell: ansible-playbook -i /etc/hosts /root/myRepo/myPlaybook.yaml shell: ansible-playbook -i /etc/hosts /root/myRepo/myPlaybook.yaml
timeout: 120 timeout: 120
# Restart httpd on host "server1"
# Docs: https://docs.olivetin.app/action-ping.html
- title: restart httpd on server1
titleAlias: restart_httpd
icon: restart
timeout: 1
shell: ssh root@server1 'service httpd restart'
# OliveTin can control containers - docker is just a command line app. # OliveTin can control containers - docker is just a command line app.
# #
# However, if you are running in a container you will need to do some setup, # However, if you are running in a container you will need to do some setup,
@ -93,19 +96,42 @@ actions:
- value: traefik - value: traefik
- value: grafana - value: grafana
- title: Slow Script
shell: sleep 3
timeout: 5
icon: "&#x1F971"
- title: Broken Script (timeout)
shell: sleep 5
timeout: 5
icon: "&#x1F62A"
- title: Delete old backups - title: Delete old backups
icon: ashtonished icon: ashtonished
shell: rm -rf /opt/oldBackups/ shell: rm -rf /opt/oldBackups/
arguments: arguments:
- type: confirmation - type: confirmation
title: Are you sure?! title: Are you sure?!
- title: Server1 Power Off
shell: echo "Power Off Server 1"
- title: Server2 Wake On LAN
shell: echo "Sending Wake on LAN to Server 2"
- title: Server2 Power Off
shell: echo "Power Off Server 2"
- title: Ping All Servers
shell: echo "Ping all servers"
icon: ping
dashboards:
- title: My Servers
contents:
- title: Server Power Controls
type: fieldset
contents:
- title: Server 1
contents:
- link: Server1 Power Off
- title: Server 2
contents:
- link: Server2 Wake On LAN
- link: Server2 Power Off
- title: Server Utilities
type: fieldset
contents:
- link: Ping All Servers

View File

@ -103,6 +103,7 @@ type Config struct {
HelperActions []HelperAction `mapstructure:"helperActions"` HelperActions []HelperAction `mapstructure:"helperActions"`
CronSupportForSeconds bool CronSupportForSeconds bool
SectionNavigationStyle string SectionNavigationStyle string
DefaultPopupOnStart string
} }
type DashboardItem struct { type DashboardItem struct {
@ -134,6 +135,7 @@ func DefaultConfig() *Config {
config.WebUIDir = "./webui" config.WebUIDir = "./webui"
config.CronSupportForSeconds = false config.CronSupportForSeconds = false
config.SectionNavigationStyle = "sidebar" config.SectionNavigationStyle = "sidebar"
config.DefaultPopupOnStart = "nothing"
return &config return &config
} }

View File

@ -11,6 +11,8 @@ var emojis = map[string]string{
"box": "📦", "box": "📦",
"ashtonished": "😲", "ashtonished": "😲",
"clock": "🕒", "clock": "🕒",
"disk": "💽",
"logs": "🔍",
} }
func lookupHTMLIcon(keyToLookup string) string { func lookupHTMLIcon(keyToLookup string) string {

View File

@ -12,7 +12,7 @@ func (cfg *Config) Sanitize() {
// log.Infof("cfg %p", cfg) // log.Infof("cfg %p", cfg)
for idx := range cfg.Actions { for idx := range cfg.Actions {
cfg.Actions[idx].sanitize() cfg.Actions[idx].sanitize(cfg)
} }
} }
@ -23,12 +23,13 @@ func (cfg *Config) sanitizeLogLevel() {
} }
} }
func (action *Action) sanitize() { func (action *Action) sanitize(cfg *Config) {
if action.Timeout < 3 { if action.Timeout < 3 {
action.Timeout = 3 action.Timeout = 3
} }
action.Icon = lookupHTMLIcon(action.Icon) action.Icon = lookupHTMLIcon(action.Icon)
action.PopupOnStart = sanitizePopupOnStart(action.PopupOnStart, cfg)
if action.MaxConcurrent < 1 { if action.MaxConcurrent < 1 {
action.MaxConcurrent = 1 action.MaxConcurrent = 1
@ -39,6 +40,19 @@ func (action *Action) sanitize() {
} }
} }
func sanitizePopupOnStart(raw string, cfg *Config) string {
switch raw {
case "execution-dialog":
return raw
case "execution-dialog-stdout-only":
return raw
case "execution-button":
return raw
default:
return cfg.DefaultPopupOnStart
}
}
func (arg *ActionArgument) sanitize() { func (arg *ActionArgument) sanitize() {
if arg.Title == "" { if arg.Title == "" {
arg.Title = arg.Name arg.Title = arg.Name

View File

@ -83,12 +83,17 @@
<div class = "action-header"> <div class = "action-header">
<span id = "execution-dialog-icon" class = "icon" role = "img"></span> <span id = "execution-dialog-icon" class = "icon" role = "img"></span>
<h2>Log: <h2>
<span id = "execution-dialog-title">?</span> <span id = "execution-dialog-title">?</span>
</h2> </h2>
<button id = "execution-dialog-toggle-size">&#128470;</button>
</div> </div>
<div id = "execution-dialog-basics">
<strong>Started: </strong><span id = "execution-dialog-datetime-started">unknown</span>
</div>
<div id = "execution-dialog-details">
<p> <p>
<strong>Started: </strong><span id = "execution-dialog-datetime-started">unknown</span>.
<strong>Finished: </strong><span id = "execution-dialog-datetime-finished">unknown</span> <strong>Finished: </strong><span id = "execution-dialog-datetime-finished">unknown</span>
</p> </p>
<p> <p>
@ -97,24 +102,28 @@
<p> <p>
<strong>Status: </strong><span id = "execution-dialog-status">unknown</span> <strong>Status: </strong><span id = "execution-dialog-status">unknown</span>
</p> </p>
</div>
<div id = "execution-dialog-output">
<details> <details>
<summary>Output</summary> <summary>Standard Output</summary>
<pre id = "execution-dialog-stdout"> <pre id = "execution-dialog-stdout">
? ?
</pre> </pre>
</details> </details>
<details> <details>
<summary>Errors</summary> <summary>Standard Error</summary>
<pre id = "execution-dialog-stderr"> <pre id = "execution-dialog-stderr">
? ?
</pre> </pre>
</details> </details>
</div>
<form method = "dialog"> <form method = "dialog">
<div class = "buttons">
<button name = "Cancel">Close</button> <button name = "Cancel">Close</button>
</div>
</form> </form>
</dialog> </dialog>

View File

@ -1,7 +1,24 @@
import './ArgumentForm.js'
import './ExecutionButton.js' import './ExecutionButton.js'
import './ArgumentForm.js'
import { ExecutionFeedbackButton } from './ExecutionFeedbackButton.js'
class ActionButton extends ExecutionFeedbackButton {
constructDomFromTemplate () {
const tpl = document.getElementById('tplActionButton')
const content = tpl.content.cloneNode(true)
/*
* FIXME: Should probably be using a shadowdom here, but seem to
* get an error when combined with custom elements.
*/
this.appendChild(content)
this.btn = this.querySelector('button')
this.domTitle = this.btn.querySelector('.title')
this.domIcon = this.btn.querySelector('.icon')
}
class ActionButton extends window.HTMLElement {
constructFromJson (json) { constructFromJson (json) {
this.updateIterationTimestamp = 0 this.updateIterationTimestamp = 0
@ -35,6 +52,8 @@ class ActionButton extends window.HTMLElement {
} }
} }
this.popupOnStart = json.popupOnStart
this.updateFromJson(json) this.updateFromJson(json)
this.domTitle.innerText = this.btn.title this.domTitle.innerText = this.btn.title
@ -57,6 +76,14 @@ class ActionButton extends window.HTMLElement {
} }
} }
onExecStatusChanged () {
this.btn.disabled = false
setTimeout(() => {
this.updateDom(null, this.btn.title)
}, 2000)
}
getUniqueId () { getUniqueId () {
if (window.isSecureContext) { if (window.isSecureContext) {
return window.crypto.randomUUID() return window.crypto.randomUUID()
@ -82,9 +109,7 @@ class ActionButton extends window.HTMLElement {
uuid: this.getUniqueId() uuid: this.getUniqueId()
} }
const btnExecution = document.createElement('execution-button') this.onActionStarted(startActionArgs.uuid)
btnExecution.constructFromJson(startActionArgs.uuid)
this.querySelector('.action-button-footer').prepend(btnExecution)
window.fetch(this.actionCallUrl, { window.fetch(this.actionCallUrl, {
method: 'POST', method: 'POST',
@ -102,24 +127,30 @@ class ActionButton extends window.HTMLElement {
).then((json) => { ).then((json) => {
// The button used to wait for the action to finish, but now it is fire & forget // The button used to wait for the action to finish, but now it is fire & forget
}).catch(err => { }).catch(err => {
btnExecution.onActionError(err) throw err // We used to flash buttons red, but now hand to the global error handler
}) })
} }
constructDomFromTemplate () { onActionStarted (executionUuid) {
const tpl = document.getElementById('tplActionButton') if (this.popupOnStart === 'execution-button') {
const content = tpl.content.cloneNode(true) const btnExecution = document.createElement('execution-button')
btnExecution.constructFromJson(executionUuid)
this.querySelector('.action-button-footer').prepend(btnExecution)
/* return
* FIXME: Should probably be using a shadowdom here, but seem to }
* get an error when combined with custom elements.
*/
this.appendChild(content) if (this.popupOnStart.includes('execution-dialog')) {
window.executionDialog.reset()
this.btn = this.querySelector('button') if (this.popupOnStart === 'execution-dialog-stdout-only') {
this.domTitle = this.btn.querySelector('.title') window.executionDialog.hideEverythingApartFromOutput()
this.domIcon = this.btn.querySelector('.icon') }
window.executionDialog.show(this)
}
this.btn.disabled = true
} }
} }

View File

@ -1,6 +1,6 @@
import { ExecutionDialog } from './ExecutionDialog.js' import { ExecutionFeedbackButton } from './ExecutionFeedbackButton.js'
class ExecutionButton extends window.HTMLElement { class ExecutionButton extends ExecutionFeedbackButton {
constructFromJson (json) { constructFromJson (json) {
this.executionUuid = json this.executionUuid = json
this.ellapsed = 0 this.ellapsed = 0
@ -15,61 +15,20 @@ class ExecutionButton extends window.HTMLElement {
this.btn.onclick = () => { this.btn.onclick = () => {
this.show() this.show()
} }
this.domTitle = this.btn
} }
show () { show () {
if (typeof (window.executionDialog) === 'undefined') { window.executionDialog.reset()
window.executionDialog = new ExecutionDialog() window.executionDialog.show()
window.executionDialog.fetchExecutionResult(this.executionUuid)
} }
window.executionDialog.show(this.executionUuid) onExecStatusChanged () {
} this.domTitle.innerText = this.ellapsed + 's'
onFinished (LogEntry) {
if (LogEntry.timedOut) {
this.onActionResult('action-timeout', 'Timed out')
} else if (LogEntry.blocked) {
this.onActionResult('action-blocked', 'Blocked!')
} else if (LogEntry.exitCode !== 0) {
this.onActionResult('action-nonzero-exit', 'Exit code ' + LogEntry.exitCode)
} else {
console.log(LogEntry)
this.ellapsed = Math.ceil(new Date(LogEntry.datetimeFinished) - new Date(LogEntry.datetimeStarted)) / 1000
this.onActionResult('action-success', 'Success!')
}
}
onActionResult (cssClass, temporaryStatusMessage) {
this.temporaryStatusMessage = '[' + temporaryStatusMessage + ']'
this.updateDom()
this.btn.classList.add(cssClass)
}
onActionError (err) {
console.error('callback error', err)
this.isWaiting = false
this.updateDom()
this.btn.classList.add('action-failed')
}
updateDom () {
if (this.temporaryStatusMessage != null) {
this.btn.innerText = this.temporaryStatusMessage
this.btn.classList.add('temporary-status-message')
this.isWaiting = false
setTimeout(() => {
this.temporaryStatusMessage = null
this.btn.classList.remove('temporary-status-message')
this.updateDom()
}, 2000)
} else if (this.isWaiting) {
this.btn.innerText = 'Waiting...'
} else {
this.btn.innerText = this.ellapsed + 's'
this.btn.title = this.ellapsed + ' seconds' this.btn.title = this.ellapsed + ' seconds'
} }
}
} }
window.customElements.define('execution-button', ExecutionButton) window.customElements.define('execution-button', ExecutionButton)

View File

@ -2,21 +2,51 @@
// the <dialog /> element out of index.html and just re-uses that - as only // the <dialog /> element out of index.html and just re-uses that - as only
// one dialog can be shown at a time. // one dialog can be shown at a time.
export class ExecutionDialog { export class ExecutionDialog {
show (json) { constructor () {
this.executionUuid = json
this.dlg = document.querySelector('dialog#execution-results-popup') this.dlg = document.querySelector('dialog#execution-results-popup')
this.domIcon = document.getElementById('execution-dialog-icon') this.domIcon = document.getElementById('execution-dialog-icon')
this.domTitle = document.getElementById('execution-dialog-title') this.domTitle = document.getElementById('execution-dialog-title')
this.domStdout = document.getElementById('execution-dialog-stdout') this.domStdout = document.getElementById('execution-dialog-stdout')
this.domStderr = document.getElementById('execution-dialog-stderr') this.domStderr = document.getElementById('execution-dialog-stderr')
this.domStdoutToggleBig = document.getElementById('execution-dialog-toggle-size')
this.domStdoutToggleBig.onclick = () => {
this.toggleSize()
}
this.domDatetimeStarted = document.getElementById('execution-dialog-datetime-started') this.domDatetimeStarted = document.getElementById('execution-dialog-datetime-started')
this.domDatetimeFinished = document.getElementById('execution-dialog-datetime-finished') this.domDatetimeFinished = document.getElementById('execution-dialog-datetime-finished')
this.domExitCode = document.getElementById('execution-dialog-exit-code') this.domExitCode = document.getElementById('execution-dialog-exit-code')
this.domStatus = document.getElementById('execution-dialog-status') this.domStatus = document.getElementById('execution-dialog-status')
this.domTitle.innerText = 'Loading...' this.domExecutionBasics = document.getElementById('execution-dialog-basics')
this.domExecutionDetails = document.getElementById('execution-dialog-details')
this.domExecutionOutput = document.getElementById('execution-dialog-output')
}
toggleSize () {
if (this.dlg.classList.contains('big')) {
this.dlg.classList.remove('big')
this.domStdout.parentElement.open = false
} else {
this.dlg.classList.add('big')
this.domStdout.parentElement.open = true
}
}
reset () {
this.executionSeconds = 0
this.dlg.classList.remove('big')
this.dlg.style.maxWidth = 'calc(100vw - 2em)'
this.dlg.style.width = ''
this.dlg.style.height = ''
this.dlg.style.border = ''
this.domStdoutToggleBig.hidden = false
this.domIcon.innerText = ''
this.domTitle.innerText = 'Waiting for result... '
this.domExitCode.innerText = '?' this.domExitCode.innerText = '?'
this.domStatus.className = '' this.domStatus.className = ''
this.domDatetimeStarted.innerText = '' this.domDatetimeStarted.innerText = ''
@ -24,12 +54,44 @@ export class ExecutionDialog {
this.domStdout.innerText = '' this.domStdout.innerText = ''
this.domStderr.innerText = '' this.domStderr.innerText = ''
this.dlg.showModal() this.hideDetailsOnResult = false
this.domExecutionBasics.hidden = false
this.fetchExecutionResult() this.domExecutionDetails.hidden = true
this.domStdout.parentElement.open = false
this.domExecutionOutput.hidden = true
} }
fetchExecutionResult () { show (actionButton) {
if (typeof actionButton !== 'undefined' && actionButton != null) {
this.domIcon.innerText = actionButton.domIcon.innerText
}
clearInterval(window.executionDialogTicker)
this.executionSeconds = 0
this.executionTick()
window.executionDialogTicker = setInterval(() => {
this.executionTick()
}, 1000)
this.dlg.showModal()
}
executionTick () {
this.executionSeconds++
this.domDatetimeStarted.innerText = this.executionSeconds + ' seconds ago'
}
hideEverythingApartFromOutput () {
this.hideDetailsOnResult = true
this.domExecutionBasics.hidden = true
}
fetchExecutionResult (uuid) {
this.executionUuid = uuid
const executionStatusArgs = { const executionStatusArgs = {
executionUuid: this.executionUuid executionUuid: this.executionUuid
} }
@ -48,13 +110,23 @@ export class ExecutionDialog {
} }
} }
).then((json) => { ).then((json) => {
this.renderResult(json) this.renderExecutionResult(json)
}).catch(err => { }).catch(err => {
this.renderError(err) this.renderError(err)
}) })
} }
renderResult (res) { renderExecutionResult (res) {
clearInterval(window.executionDialogTicker)
this.domExecutionOutput.hidden = false
if (!this.hideDetailsOnResult) {
this.domExecutionDetails.hidden = false
} else {
this.domStdout.parentElement.open = true
}
this.executionUuid = res.logEntry.executionUuid this.executionUuid = res.logEntry.executionUuid
if (res.logEntry.executionFinished) { if (res.logEntry.executionFinished) {
@ -86,7 +158,7 @@ export class ExecutionDialog {
this.domStdout.innerText = res.logEntry.stdout this.domStdout.innerText = res.logEntry.stdout
this.domStdout.innerText = res.logEntry.stdout this.domStdout.innerText = res.logEntry.stdout
if (res.logEntry.stderr === '') { if (res.logEntry.stderr === '(empty)') {
this.domStderr.parentElement.style.display = 'none' this.domStderr.parentElement.style.display = 'none'
this.domStderr.innerText = res.logEntry.stderr this.domStderr.innerText = res.logEntry.stderr
} else { } else {

View File

@ -0,0 +1,29 @@
export class ExecutionFeedbackButton extends window.HTMLElement {
onExecutionFinished (LogEntry) {
if (LogEntry.timedOut) {
this.renderExecutionResult('action-timeout', 'Timed out')
} else if (LogEntry.blocked) {
this.renderExecutionResult('action-blocked', 'Blocked!')
} else if (LogEntry.exitCode !== 0) {
this.renderExecutionResult('action-nonzero-exit', 'Exit code ' + LogEntry.exitCode)
} else {
this.ellapsed = Math.ceil(new Date(LogEntry.datetimeFinished) - new Date(LogEntry.datetimeStarted)) / 1000
this.renderExecutionResult('action-success', 'Success!')
}
}
renderExecutionResult (resultCssClass, temporaryStatusMessage) {
this.updateDom(resultCssClass, '[' + temporaryStatusMessage + ']')
this.onExecStatusChanged()
}
updateDom (resultCssClass, title) {
if (resultCssClass == null) {
this.btn.className = ''
} else {
this.btn.classList.add(resultCssClass)
}
this.domTitle.innerText = title
}
}

View File

@ -1,12 +1,22 @@
import './ActionButton.js' // To define action-button import './ActionButton.js' // To define action-button
import { ExecutionDialog } from './ExecutionDialog.js'
/**
* This is a weird function that just sets some globals.
*/
export function initMarshaller () {
window.changeDirectory = changeDirectory
window.showSection = showSection
window.executionDialog = new ExecutionDialog()
window.addEventListener('ExecutionFinished', onExecutionFinished)
}
export function marshalDashboardComponentsJsonToHtml (json) { export function marshalDashboardComponentsJsonToHtml (json) {
marshalActionsJsonToHtml(json) marshalActionsJsonToHtml(json)
marshalDashboardStructureToHtml(json) marshalDashboardStructureToHtml(json)
window.changeDirectory = changeDirectory
window.showSection = showSection
changeDirectory(null) changeDirectory(null)
} }
@ -37,6 +47,43 @@ function marshalActionsJsonToHtml (json) {
} }
} }
function onExecutionFinished (evt) {
const logEntry = evt.payload
const actionButton = window.actionButtons[logEntry.actionTitle]
switch (actionButton.popupOnStart) {
case 'execution-button':
document.querySelector('execution-button#execution-' + logEntry.uuid).onExecutionFinished(logEntry)
break
case 'execution-dialog-stdout-only':
case 'execution-dialog':
actionButton.onExecutionFinished(logEntry)
// We don't need to fetch the logEntry for the dialog because we already
// have it, so we open the dialog and it will get updated below.
window.executionDialog.show()
window.executionDialog.executionUuid = logEntry.uuid
break
default:
actionButton.onExecutionFinished(logEntry)
break
}
marshalLogsJsonToHtml({
logs: [logEntry]
})
// If the current execution dialog is open, update that too
if (window.executionDialog.dlg.open && window.executionDialog.executionUuid === logEntry.uuid) {
window.executionDialog.renderExecutionResult({
logEntry: logEntry
})
}
}
function showSection (title) { function showSection (title) {
for (const section of document.querySelectorAll('section')) { for (const section of document.querySelectorAll('section')) {
if (section.title === title) { if (section.title === title) {
@ -117,10 +164,10 @@ function marshalDashboardStructureToHtml (json) {
document.getElementById('navigation-links').appendChild(navigationLi) document.getElementById('navigation-links').appendChild(navigationLi)
} }
if (json.dashboards.length === 0) { if (document.getElementById('root-group').children.length === 0 && json.dashboards.length > 0) {
showSection('Actions')
} else {
showSection(json.dashboards[0].title) showSection(json.dashboards[0].title)
} else {
showSection('Actions')
} }
const rootGroup = document.querySelector('#root-group') const rootGroup = document.querySelector('#root-group')

View File

@ -1,5 +1,3 @@
import { marshalLogsJsonToHtml } from './marshaller.js'
window.ws = null window.ws = null
export function checkWebsocketConnection () { export function checkWebsocketConnection () {
@ -38,30 +36,18 @@ function websocketOnMessage (msg) {
// FIXME check msg status is OK // FIXME check msg status is OK
const j = JSON.parse(msg.data) const j = JSON.parse(msg.data)
const e = new Event(j.type)
e.payload = j.payload
switch (j.type) { switch (j.type) {
case 'ExecutionFinished': case 'ExecutionFinished':
updatePageAfterFinished(j.payload) window.dispatchEvent(e)
break break
default: default:
window.showBigError('Unknown message type from server: ' + j.type) window.showBigError('Unknown message type from server: ' + j.type)
} }
} }
function updatePageAfterFinished (logEntry) {
document.querySelector('execution-button#execution-' + logEntry.uuid).onFinished(logEntry)
marshalLogsJsonToHtml({
logs: [logEntry]
})
// If the current execution dialog is open, update that too
if (window.executionDialog != null && window.executionDialog.dlg.open && window.executionDialog.executionUuid === logEntry.uuid) {
window.executionDialog.renderResult({
logEntry: logEntry
})
}
}
function websocketOnError (err) { function websocketOnError (err) {
window.websocketAvailable = false window.websocketAvailable = false
window.refreshLoop() window.refreshLoop()

View File

@ -1,6 +1,6 @@
'use strict' 'use strict'
import { setupSectionNavigation, marshalDashboardComponentsJsonToHtml, marshalLogsJsonToHtml } from './js/marshaller.js' import { initMarshaller, setupSectionNavigation, marshalDashboardComponentsJsonToHtml, marshalLogsJsonToHtml } from './js/marshaller.js'
import { checkWebsocketConnection } from './js/websocket.js' import { checkWebsocketConnection } from './js/websocket.js'
function searchLogs (e) { function searchLogs (e) {
@ -123,6 +123,7 @@ function processWebuiSettingsJson (settings) {
} }
function main () { function main () {
initMarshaller()
setupLogSearchBox() setupLogSearchBox()
setupSectionNavigation('sidebar') setupSectionNavigation('sidebar')

View File

@ -10,13 +10,21 @@ body {
dialog { dialog {
box-shadow: 0 0 6px 0 #444; box-shadow: 0 0 6px 0 #444;
max-width: 600px; max-width: 600px;
min-width: 60%;
padding: 1em; padding: 1em;
text-align: left; text-align: left;
} }
dialog.big {
max-width: 100%;
width: calc(100vw - 2em);
height: 100vh;
border: none;
}
fieldset { fieldset {
display: grid; display: grid;
grid-template-columns: repeat(auto-fit, 160px); grid-template-columns: repeat(auto-fit, 180px);
grid-template-rows: auto auto auto auto; grid-template-rows: auto auto auto auto;
grid-gap: 1em; grid-gap: 1em;
padding: 0; padding: 0;
@ -29,10 +37,9 @@ fieldset {
display: inline; display: inline;
} }
h1 { footer,
display: inline; footer a {
font-size: small; color: black;
padding-left: .5em;
} }
#sidebar-toggler-button { #sidebar-toggler-button {
@ -55,13 +62,11 @@ h1 {
cursor: pointer; cursor: pointer;
} }
footer, nav {
footer a { background-color: white;
color: black;
} }
nav.sidebar { nav.sidebar {
background-color: white;
position: absolute; position: absolute;
width: 180px; width: 180px;
height: 100vh; height: 100vh;
@ -87,6 +92,16 @@ input:checked ~ nav.sidebar {
left: -250px; left: -250px;
} }
h1 {
display: inline;
font-size: small;
padding-left: .5em;
}
h1 a {
color: black;
}
nav ul { nav ul {
margin: 0; margin: 0;
padding: 0; padding: 0;
@ -106,6 +121,10 @@ nav ul li a {
user-select: none; user-select: none;
} }
h1 a:visited {
color: black;
}
nav ul li a:hover { nav ul li a:hover {
color: black; color: black;
background-color: #efefef; background-color: #efefef;
@ -120,7 +139,6 @@ nav.topbar ul li {
display: inline-block; display: inline-block;
} }
table { table {
background-color: white; background-color: white;
border-collapse: collapse; border-collapse: collapse;
@ -156,6 +174,11 @@ span.icon {
font-size: 3em; font-size: 3em;
} }
.action-header {
display: flex;
align-items: center;
}
.action-header span.icon, .action-header span.icon,
tr.log-row span.icon { tr.log-row span.icon {
display: inline-block; display: inline-block;
@ -189,6 +212,7 @@ h2 {
display: inline-block; display: inline-block;
font-size: 1em; font-size: 1em;
margin-top: 0; margin-top: 0;
flex-grow: 1;
} }
div.entity h2 { div.entity h2 {
@ -404,6 +428,7 @@ pre {
border: 1px solid gray; border: 1px solid gray;
padding: 1em; padding: 1em;
min-height: 1em; min-height: 1em;
overflow: auto;
} }
td.exit-code { td.exit-code {
@ -442,7 +467,7 @@ div.toolbar * {
@media screen and (width <= 600px) { @media screen and (width <= 600px) {
fieldset { fieldset {
grid-template-columns: repeat(auto-fit, minmax(160px, 1fr)); grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
} }
label { label {
@ -460,6 +485,12 @@ div.toolbar * {
margin-left: 0; margin-left: 0;
margin-top: .6em; margin-top: .6em;
} }
dialog {
border-left: 0;
border-right: 0;
width: 100%;
}
} }
@media (prefers-color-scheme: dark) { @media (prefers-color-scheme: dark) {
@ -492,16 +523,21 @@ div.toolbar * {
color: white; color: white;
} }
nav {
background-color: #111;
color: white;
}
footer, footer,
footer a { footer a {
color: gray; color: gray;
} }
h1 a, h1 a:visited {
color: white;
}
nav {
background-color: #111;
color: white;
}
nav ul li a:hover { nav ul li a:hover {
background-color: #666; background-color: #666;
color: white; color: white;