Explorar el Código

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

James Read hace 2 años
padre
commit
c12431d8a3

+ 63 - 37
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: "&#x1F971"
-
-  - title: Broken Script (timeout)
-    shell: sleep 5
-    timeout: 5
-    icon: "&#x1F62A"
-
   - 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

+ 6 - 4
internal/config/config.go

@@ -51,10 +51,10 @@ type HelperAction struct {
 // Entity represents a "thing" that can have multiple actions associated with it.
 // for example, a media player with a start and stop action.
 type EntityFile struct {
-	File    string
-	Name    string
-	Icon    string
-	CSS     map[string]string
+	File string
+	Name string
+	Icon string
+	CSS  map[string]string
 }
 
 // PermissionsList defines what users can do with an action.
@@ -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
 }

+ 2 - 0
internal/config/emoji.go

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

+ 16 - 2
internal/config/sanitize.go

@@ -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

+ 35 - 26
webui/index.html

@@ -83,38 +83,47 @@
 			<div class = "action-header">
 				<span id = "execution-dialog-icon" class = "icon" role = "img"></span>
 
-				<h2>Log:
+				<h2>
 					<span id = "execution-dialog-title">?</span>
 				</h2>
-			</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>
 
+				<button id = "execution-dialog-toggle-size">&#128470;</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>
 
-			<details>
-				<summary>Output</summary>
-				<pre id = "execution-dialog-stdout">
-					?
-				</pre>
-			</details>
-
-			<details>
-				<summary>Errors</summary>
-				<pre id = "execution-dialog-stderr">
-					?
-				</pre>
-			</details>
+			<div id = "execution-dialog-output">
+				<details>
+					<summary>Standard Output</summary>
+					<pre id = "execution-dialog-stdout">
+						?
+					</pre>
+				</details>
+
+				<details>
+					<summary>Standard Error</summary>
+					<pre id = "execution-dialog-stderr">
+						?
+					</pre>
+				</details>
+			</div>
 
 			<form method = "dialog">
-				<button name = "Cancel">Close</button>
+				<div class = "buttons">
+					<button name = "Cancel">Close</button>
+				</div>
 			</form>
 		</dialog>
 

+ 48 - 17
webui/js/ActionButton.js

@@ -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
   }
 }
 

+ 10 - 51
webui/js/ExecutionButton.js

@@ -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,60 +15,19 @@ class ExecutionButton extends window.HTMLElement {
     this.btn.onclick = () => {
       this.show()
     }
-  }
-
-  show () {
-    if (typeof (window.executionDialog) === 'undefined') {
-      window.executionDialog = new ExecutionDialog()
-    }
-
-    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)
+    this.domTitle = this.btn
   }
 
-  onActionError (err) {
-    console.error('callback error', err)
-    this.isWaiting = false
-    this.updateDom()
-    this.btn.classList.add('action-failed')
+  show () {
+    window.executionDialog.reset()
+    window.executionDialog.show()
+    window.executionDialog.fetchExecutionResult(this.executionUuid)
   }
 
-  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'
-    }
+  onExecStatusChanged () {
+    this.domTitle.innerText = this.ellapsed + 's'
+    this.btn.title = this.ellapsed + ' seconds'
   }
 }
 

+ 81 - 9
webui/js/ExecutionDialog.js

@@ -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.hideDetailsOnResult = false
+    this.domExecutionBasics.hidden = false
+
+    this.domExecutionDetails.hidden = true
+    this.domStdout.parentElement.open = false
+
+    this.domExecutionOutput.hidden = true
+  }
+
+  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'
+  }
 
-    this.fetchExecutionResult()
+  hideEverythingApartFromOutput () {
+    this.hideDetailsOnResult = true
+    this.domExecutionBasics.hidden = true
   }
 
-  fetchExecutionResult () {
+  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 {

+ 29 - 0
webui/js/ExecutionFeedbackButton.js

@@ -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
+  }
+}

+ 53 - 6
webui/js/marshaller.js

@@ -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')

+ 4 - 18
webui/js/websocket.js

@@ -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()

+ 2 - 1
webui/main.js

@@ -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')
 

+ 52 - 16
webui/style.css

@@ -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;