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
|
# 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: "🥱"
|
|
||||||
|
|
||||||
- title: Broken Script (timeout)
|
|
||||||
shell: sleep 5
|
|
||||||
timeout: 5
|
|
||||||
icon: "😪"
|
|
||||||
|
|
||||||
- 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
|
||||||
|
|
|
||||||
|
|
@ -51,10 +51,10 @@ type HelperAction struct {
|
||||||
// Entity represents a "thing" that can have multiple actions associated with it.
|
// Entity represents a "thing" that can have multiple actions associated with it.
|
||||||
// for example, a media player with a start and stop action.
|
// for example, a media player with a start and stop action.
|
||||||
type EntityFile struct {
|
type EntityFile struct {
|
||||||
File string
|
File string
|
||||||
Name string
|
Name string
|
||||||
Icon string
|
Icon string
|
||||||
CSS map[string]string
|
CSS map[string]string
|
||||||
}
|
}
|
||||||
|
|
||||||
// PermissionsList defines what users can do with an action.
|
// PermissionsList defines what users can do with an action.
|
||||||
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -83,38 +83,47 @@
|
||||||
<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">🗖</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>Finished: </strong><span id = "execution-dialog-datetime-finished">unknown</span>
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<strong>Exit Code: </strong><span id = "execution-dialog-exit-code">unknown</span>
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<strong>Status: </strong><span id = "execution-dialog-status">unknown</span>
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<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>
|
|
||||||
<strong>Exit Code: </strong><span id = "execution-dialog-exit-code">unknown</span>
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
<strong>Status: </strong><span id = "execution-dialog-status">unknown</span>
|
|
||||||
</p>
|
|
||||||
|
|
||||||
|
<div id = "execution-dialog-output">
|
||||||
|
<details>
|
||||||
|
<summary>Standard Output</summary>
|
||||||
|
<pre id = "execution-dialog-stdout">
|
||||||
|
?
|
||||||
|
</pre>
|
||||||
|
</details>
|
||||||
|
|
||||||
<details>
|
<details>
|
||||||
<summary>Output</summary>
|
<summary>Standard Error</summary>
|
||||||
<pre id = "execution-dialog-stdout">
|
<pre id = "execution-dialog-stderr">
|
||||||
?
|
?
|
||||||
</pre>
|
</pre>
|
||||||
</details>
|
</details>
|
||||||
|
</div>
|
||||||
<details>
|
|
||||||
<summary>Errors</summary>
|
|
||||||
<pre id = "execution-dialog-stderr">
|
|
||||||
?
|
|
||||||
</pre>
|
|
||||||
</details>
|
|
||||||
|
|
||||||
<form method = "dialog">
|
<form method = "dialog">
|
||||||
<button name = "Cancel">Close</button>
|
<div class = "buttons">
|
||||||
|
<button name = "Cancel">Close</button>
|
||||||
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</dialog>
|
</dialog>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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,60 +15,19 @@ 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)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onFinished (LogEntry) {
|
onExecStatusChanged () {
|
||||||
if (LogEntry.timedOut) {
|
this.domTitle.innerText = this.ellapsed + 's'
|
||||||
this.onActionResult('action-timeout', 'Timed out')
|
this.btn.title = this.ellapsed + ' seconds'
|
||||||
} 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'
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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 './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')
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
|
|
|
||||||
|
|
@ -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')
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue