feature: popupOnStart allows for many feedback options when actions are started (#227)
This commit is contained in:
parent
dc0cf33d37
commit
c12431d8a3
100
config.yaml
100
config.yaml
|
|
@ -8,36 +8,38 @@ listenAddressSingleHTTPFrontend: 0.0.0.0:1337
|
|||
# Choose from INFO (default), WARN and DEBUG
|
||||
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:
|
||||
- title: date
|
||||
shell: date
|
||||
icon: clock
|
||||
popupOnStart: true
|
||||
|
||||
- title: start
|
||||
shell: docker start {{ container.name }}
|
||||
icon: start
|
||||
- title: Restart media container
|
||||
shell: docker restart mediacontainer
|
||||
icon: box
|
||||
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.
|
||||
- title: Run backup script
|
||||
shell: /opt/backupScript.sh
|
||||
shellAfterCompleted: "apprise -t 'Notification: Backup script completed' -b 'The backup script completed with code {{ exitCode}}. The log is: \n {{ stdout }} '"
|
||||
maxConcurrent: 1
|
||||
timeout: 10
|
||||
icon: backup
|
||||
popupOnStart: execution-dialog
|
||||
|
||||
- title: date
|
||||
shell: date
|
||||
timeout: 6
|
||||
icon: clock
|
||||
popupOnStart: execution-button
|
||||
|
||||
# This will send 1 ping (-c 1)
|
||||
# Docs: https://docs.olivetin.app/action-ping.html
|
||||
|
|
@ -58,13 +60,6 @@ actions:
|
|||
default: 1
|
||||
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.
|
||||
#
|
||||
# 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
|
||||
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.
|
||||
#
|
||||
# However, if you are running in a container you will need to do some setup,
|
||||
|
|
@ -93,19 +96,42 @@ actions:
|
|||
- value: traefik
|
||||
- value: grafana
|
||||
|
||||
- title: Slow Script
|
||||
shell: sleep 3
|
||||
timeout: 5
|
||||
icon: "🥱"
|
||||
|
||||
- title: Broken Script (timeout)
|
||||
shell: sleep 5
|
||||
timeout: 5
|
||||
icon: "😪"
|
||||
|
||||
- title: Delete old backups
|
||||
icon: ashtonished
|
||||
shell: rm -rf /opt/oldBackups/
|
||||
arguments:
|
||||
- type: confirmation
|
||||
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
|
||||
|
|
|
|||
|
|
@ -103,6 +103,7 @@ type Config struct {
|
|||
HelperActions []HelperAction `mapstructure:"helperActions"`
|
||||
CronSupportForSeconds bool
|
||||
SectionNavigationStyle string
|
||||
DefaultPopupOnStart string
|
||||
}
|
||||
|
||||
type DashboardItem struct {
|
||||
|
|
@ -134,6 +135,7 @@ func DefaultConfig() *Config {
|
|||
config.WebUIDir = "./webui"
|
||||
config.CronSupportForSeconds = false
|
||||
config.SectionNavigationStyle = "sidebar"
|
||||
config.DefaultPopupOnStart = "nothing"
|
||||
|
||||
return &config
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,6 +11,8 @@ var emojis = map[string]string{
|
|||
"box": "📦",
|
||||
"ashtonished": "😲",
|
||||
"clock": "🕒",
|
||||
"disk": "💽",
|
||||
"logs": "🔍",
|
||||
}
|
||||
|
||||
func lookupHTMLIcon(keyToLookup string) string {
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ func (cfg *Config) Sanitize() {
|
|||
// log.Infof("cfg %p", cfg)
|
||||
|
||||
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 {
|
||||
action.Timeout = 3
|
||||
}
|
||||
|
||||
action.Icon = lookupHTMLIcon(action.Icon)
|
||||
action.PopupOnStart = sanitizePopupOnStart(action.PopupOnStart, cfg)
|
||||
|
||||
if 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() {
|
||||
if arg.Title == "" {
|
||||
arg.Title = arg.Name
|
||||
|
|
|
|||
|
|
@ -83,12 +83,17 @@
|
|||
<div class = "action-header">
|
||||
<span id = "execution-dialog-icon" class = "icon" role = "img"></span>
|
||||
|
||||
<h2>Log:
|
||||
<h2>
|
||||
<span id = "execution-dialog-title">?</span>
|
||||
</h2>
|
||||
|
||||
<button id = "execution-dialog-toggle-size">🗖</button>
|
||||
</div>
|
||||
<div id = "execution-dialog-basics">
|
||||
<strong>Started: </strong><span id = "execution-dialog-datetime-started">unknown</span>
|
||||
</div>
|
||||
<div id = "execution-dialog-details">
|
||||
<p>
|
||||
<strong>Started: </strong><span id = "execution-dialog-datetime-started">unknown</span>.
|
||||
<strong>Finished: </strong><span id = "execution-dialog-datetime-finished">unknown</span>
|
||||
</p>
|
||||
<p>
|
||||
|
|
@ -97,24 +102,28 @@
|
|||
<p>
|
||||
<strong>Status: </strong><span id = "execution-dialog-status">unknown</span>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
|
||||
<div id = "execution-dialog-output">
|
||||
<details>
|
||||
<summary>Output</summary>
|
||||
<summary>Standard Output</summary>
|
||||
<pre id = "execution-dialog-stdout">
|
||||
?
|
||||
</pre>
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>Errors</summary>
|
||||
<summary>Standard Error</summary>
|
||||
<pre id = "execution-dialog-stderr">
|
||||
?
|
||||
</pre>
|
||||
</details>
|
||||
</div>
|
||||
|
||||
<form method = "dialog">
|
||||
<div class = "buttons">
|
||||
<button name = "Cancel">Close</button>
|
||||
</div>
|
||||
</form>
|
||||
</dialog>
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,24 @@
|
|||
import './ArgumentForm.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) {
|
||||
this.updateIterationTimestamp = 0
|
||||
|
||||
|
|
@ -35,6 +52,8 @@ class ActionButton extends window.HTMLElement {
|
|||
}
|
||||
}
|
||||
|
||||
this.popupOnStart = json.popupOnStart
|
||||
|
||||
this.updateFromJson(json)
|
||||
|
||||
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 () {
|
||||
if (window.isSecureContext) {
|
||||
return window.crypto.randomUUID()
|
||||
|
|
@ -82,9 +109,7 @@ class ActionButton extends window.HTMLElement {
|
|||
uuid: this.getUniqueId()
|
||||
}
|
||||
|
||||
const btnExecution = document.createElement('execution-button')
|
||||
btnExecution.constructFromJson(startActionArgs.uuid)
|
||||
this.querySelector('.action-button-footer').prepend(btnExecution)
|
||||
this.onActionStarted(startActionArgs.uuid)
|
||||
|
||||
window.fetch(this.actionCallUrl, {
|
||||
method: 'POST',
|
||||
|
|
@ -102,24 +127,30 @@ class ActionButton extends window.HTMLElement {
|
|||
).then((json) => {
|
||||
// The button used to wait for the action to finish, but now it is fire & forget
|
||||
}).catch(err => {
|
||||
btnExecution.onActionError(err)
|
||||
throw err // We used to flash buttons red, but now hand to the global error handler
|
||||
})
|
||||
}
|
||||
|
||||
constructDomFromTemplate () {
|
||||
const tpl = document.getElementById('tplActionButton')
|
||||
const content = tpl.content.cloneNode(true)
|
||||
onActionStarted (executionUuid) {
|
||||
if (this.popupOnStart === 'execution-button') {
|
||||
const btnExecution = document.createElement('execution-button')
|
||||
btnExecution.constructFromJson(executionUuid)
|
||||
this.querySelector('.action-button-footer').prepend(btnExecution)
|
||||
|
||||
/*
|
||||
* FIXME: Should probably be using a shadowdom here, but seem to
|
||||
* get an error when combined with custom elements.
|
||||
*/
|
||||
return
|
||||
}
|
||||
|
||||
this.appendChild(content)
|
||||
if (this.popupOnStart.includes('execution-dialog')) {
|
||||
window.executionDialog.reset()
|
||||
|
||||
this.btn = this.querySelector('button')
|
||||
this.domTitle = this.btn.querySelector('.title')
|
||||
this.domIcon = this.btn.querySelector('.icon')
|
||||
if (this.popupOnStart === 'execution-dialog-stdout-only') {
|
||||
window.executionDialog.hideEverythingApartFromOutput()
|
||||
}
|
||||
|
||||
window.executionDialog.show(this)
|
||||
}
|
||||
|
||||
this.btn.disabled = true
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
this.executionUuid = json
|
||||
this.ellapsed = 0
|
||||
|
|
@ -15,61 +15,20 @@ class ExecutionButton extends window.HTMLElement {
|
|||
this.btn.onclick = () => {
|
||||
this.show()
|
||||
}
|
||||
|
||||
this.domTitle = this.btn
|
||||
}
|
||||
|
||||
show () {
|
||||
if (typeof (window.executionDialog) === 'undefined') {
|
||||
window.executionDialog = new ExecutionDialog()
|
||||
window.executionDialog.reset()
|
||||
window.executionDialog.show()
|
||||
window.executionDialog.fetchExecutionResult(this.executionUuid)
|
||||
}
|
||||
|
||||
window.executionDialog.show(this.executionUuid)
|
||||
}
|
||||
|
||||
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'
|
||||
onExecStatusChanged () {
|
||||
this.domTitle.innerText = this.ellapsed + 's'
|
||||
this.btn.title = this.ellapsed + ' seconds'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
window.customElements.define('execution-button', ExecutionButton)
|
||||
|
|
|
|||
|
|
@ -2,21 +2,51 @@
|
|||
// the <dialog /> element out of index.html and just re-uses that - as only
|
||||
// one dialog can be shown at a time.
|
||||
export class ExecutionDialog {
|
||||
show (json) {
|
||||
this.executionUuid = json
|
||||
|
||||
constructor () {
|
||||
this.dlg = document.querySelector('dialog#execution-results-popup')
|
||||
|
||||
this.domIcon = document.getElementById('execution-dialog-icon')
|
||||
this.domTitle = document.getElementById('execution-dialog-title')
|
||||
this.domStdout = document.getElementById('execution-dialog-stdout')
|
||||
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.domDatetimeFinished = document.getElementById('execution-dialog-datetime-finished')
|
||||
this.domExitCode = document.getElementById('execution-dialog-exit-code')
|
||||
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.domStatus.className = ''
|
||||
this.domDatetimeStarted.innerText = ''
|
||||
|
|
@ -24,12 +54,44 @@ export class ExecutionDialog {
|
|||
this.domStdout.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 = {
|
||||
executionUuid: this.executionUuid
|
||||
}
|
||||
|
|
@ -48,13 +110,23 @@ export class ExecutionDialog {
|
|||
}
|
||||
}
|
||||
).then((json) => {
|
||||
this.renderResult(json)
|
||||
this.renderExecutionResult(json)
|
||||
}).catch(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
|
||||
|
||||
if (res.logEntry.executionFinished) {
|
||||
|
|
@ -86,7 +158,7 @@ export class ExecutionDialog {
|
|||
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.innerText = res.logEntry.stderr
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -1,12 +1,22 @@
|
|||
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) {
|
||||
marshalActionsJsonToHtml(json)
|
||||
marshalDashboardStructureToHtml(json)
|
||||
|
||||
window.changeDirectory = changeDirectory
|
||||
window.showSection = showSection
|
||||
|
||||
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) {
|
||||
for (const section of document.querySelectorAll('section')) {
|
||||
if (section.title === title) {
|
||||
|
|
@ -117,10 +164,10 @@ function marshalDashboardStructureToHtml (json) {
|
|||
document.getElementById('navigation-links').appendChild(navigationLi)
|
||||
}
|
||||
|
||||
if (json.dashboards.length === 0) {
|
||||
showSection('Actions')
|
||||
} else {
|
||||
if (document.getElementById('root-group').children.length === 0 && json.dashboards.length > 0) {
|
||||
showSection(json.dashboards[0].title)
|
||||
} else {
|
||||
showSection('Actions')
|
||||
}
|
||||
|
||||
const rootGroup = document.querySelector('#root-group')
|
||||
|
|
|
|||
|
|
@ -1,5 +1,3 @@
|
|||
import { marshalLogsJsonToHtml } from './marshaller.js'
|
||||
|
||||
window.ws = null
|
||||
|
||||
export function checkWebsocketConnection () {
|
||||
|
|
@ -38,30 +36,18 @@ function websocketOnMessage (msg) {
|
|||
// FIXME check msg status is OK
|
||||
const j = JSON.parse(msg.data)
|
||||
|
||||
const e = new Event(j.type)
|
||||
e.payload = j.payload
|
||||
|
||||
switch (j.type) {
|
||||
case 'ExecutionFinished':
|
||||
updatePageAfterFinished(j.payload)
|
||||
window.dispatchEvent(e)
|
||||
break
|
||||
default:
|
||||
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) {
|
||||
window.websocketAvailable = false
|
||||
window.refreshLoop()
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
'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'
|
||||
|
||||
function searchLogs (e) {
|
||||
|
|
@ -123,6 +123,7 @@ function processWebuiSettingsJson (settings) {
|
|||
}
|
||||
|
||||
function main () {
|
||||
initMarshaller()
|
||||
setupLogSearchBox()
|
||||
setupSectionNavigation('sidebar')
|
||||
|
||||
|
|
|
|||
|
|
@ -10,13 +10,21 @@ body {
|
|||
dialog {
|
||||
box-shadow: 0 0 6px 0 #444;
|
||||
max-width: 600px;
|
||||
min-width: 60%;
|
||||
padding: 1em;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
dialog.big {
|
||||
max-width: 100%;
|
||||
width: calc(100vw - 2em);
|
||||
height: 100vh;
|
||||
border: none;
|
||||
}
|
||||
|
||||
fieldset {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, 160px);
|
||||
grid-template-columns: repeat(auto-fit, 180px);
|
||||
grid-template-rows: auto auto auto auto;
|
||||
grid-gap: 1em;
|
||||
padding: 0;
|
||||
|
|
@ -29,10 +37,9 @@ fieldset {
|
|||
display: inline;
|
||||
}
|
||||
|
||||
h1 {
|
||||
display: inline;
|
||||
font-size: small;
|
||||
padding-left: .5em;
|
||||
footer,
|
||||
footer a {
|
||||
color: black;
|
||||
}
|
||||
|
||||
#sidebar-toggler-button {
|
||||
|
|
@ -55,13 +62,11 @@ h1 {
|
|||
cursor: pointer;
|
||||
}
|
||||
|
||||
footer,
|
||||
footer a {
|
||||
color: black;
|
||||
nav {
|
||||
background-color: white;
|
||||
}
|
||||
|
||||
nav.sidebar {
|
||||
background-color: white;
|
||||
position: absolute;
|
||||
width: 180px;
|
||||
height: 100vh;
|
||||
|
|
@ -87,6 +92,16 @@ input:checked ~ nav.sidebar {
|
|||
left: -250px;
|
||||
}
|
||||
|
||||
h1 {
|
||||
display: inline;
|
||||
font-size: small;
|
||||
padding-left: .5em;
|
||||
}
|
||||
|
||||
h1 a {
|
||||
color: black;
|
||||
}
|
||||
|
||||
nav ul {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
|
|
@ -106,6 +121,10 @@ nav ul li a {
|
|||
user-select: none;
|
||||
}
|
||||
|
||||
h1 a:visited {
|
||||
color: black;
|
||||
}
|
||||
|
||||
nav ul li a:hover {
|
||||
color: black;
|
||||
background-color: #efefef;
|
||||
|
|
@ -120,7 +139,6 @@ nav.topbar ul li {
|
|||
display: inline-block;
|
||||
}
|
||||
|
||||
|
||||
table {
|
||||
background-color: white;
|
||||
border-collapse: collapse;
|
||||
|
|
@ -156,6 +174,11 @@ span.icon {
|
|||
font-size: 3em;
|
||||
}
|
||||
|
||||
.action-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.action-header span.icon,
|
||||
tr.log-row span.icon {
|
||||
display: inline-block;
|
||||
|
|
@ -189,6 +212,7 @@ h2 {
|
|||
display: inline-block;
|
||||
font-size: 1em;
|
||||
margin-top: 0;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
div.entity h2 {
|
||||
|
|
@ -404,6 +428,7 @@ pre {
|
|||
border: 1px solid gray;
|
||||
padding: 1em;
|
||||
min-height: 1em;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
td.exit-code {
|
||||
|
|
@ -442,7 +467,7 @@ div.toolbar * {
|
|||
|
||||
@media screen and (width <= 600px) {
|
||||
fieldset {
|
||||
grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
|
||||
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
|
||||
}
|
||||
|
||||
label {
|
||||
|
|
@ -460,6 +485,12 @@ div.toolbar * {
|
|||
margin-left: 0;
|
||||
margin-top: .6em;
|
||||
}
|
||||
|
||||
dialog {
|
||||
border-left: 0;
|
||||
border-right: 0;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
|
|
@ -492,16 +523,21 @@ div.toolbar * {
|
|||
color: white;
|
||||
}
|
||||
|
||||
nav {
|
||||
background-color: #111;
|
||||
color: white;
|
||||
}
|
||||
|
||||
footer,
|
||||
footer a {
|
||||
color: gray;
|
||||
}
|
||||
|
||||
|
||||
h1 a, h1 a:visited {
|
||||
color: white;
|
||||
}
|
||||
|
||||
nav {
|
||||
background-color: #111;
|
||||
color: white;
|
||||
}
|
||||
|
||||
nav ul li a:hover {
|
||||
background-color: #666;
|
||||
color: white;
|
||||
|
|
|
|||
Loading…
Reference in New Issue