diff --git a/config.yaml b/config.yaml
index 865e3e5..03c2eaa 100644
--- a/config.yaml
+++ b/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
diff --git a/internal/config/config.go b/internal/config/config.go
index d9f4cd4..bde362c 100644
--- a/internal/config/config.go
+++ b/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
}
diff --git a/internal/config/emoji.go b/internal/config/emoji.go
index 6a354a9..b311035 100644
--- a/internal/config/emoji.go
+++ b/internal/config/emoji.go
@@ -11,6 +11,8 @@ var emojis = map[string]string{
"box": "📦",
"ashtonished": "😲",
"clock": "🕒",
+ "disk": "💽",
+ "logs": "🔍",
}
func lookupHTMLIcon(keyToLookup string) string {
diff --git a/internal/config/sanitize.go b/internal/config/sanitize.go
index 8e127ad..b010962 100644
--- a/internal/config/sanitize.go
+++ b/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
diff --git a/webui/index.html b/webui/index.html
index 88cec57..e877f54 100644
--- a/webui/index.html
+++ b/webui/index.html
@@ -83,38 +83,47 @@
+
+ Started: unknown
+
+
+
+ Finished: unknown
+
+
+ Exit Code: unknown
+
+
+ Status: unknown
+
-
- Started: unknown.
- Finished: unknown
-
-
- Exit Code: unknown
-
-
- Status: unknown
-
+
+
+ Standard Output
+
+ ?
+
+
-
- Output
-
- ?
-
-
-
-
- Errors
-
- ?
-
-
+
+ Standard Error
+
+ ?
+
+
+
diff --git a/webui/js/ActionButton.js b/webui/js/ActionButton.js
index 6da3aaa..eaa70e8 100644
--- a/webui/js/ActionButton.js
+++ b/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
}
}
diff --git a/webui/js/ExecutionButton.js b/webui/js/ExecutionButton.js
index 0a5f27f..8acf40e 100644
--- a/webui/js/ExecutionButton.js
+++ b/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()
}
+
+ this.domTitle = this.btn
}
show () {
- if (typeof (window.executionDialog) === 'undefined') {
- window.executionDialog = new ExecutionDialog()
- }
-
- window.executionDialog.show(this.executionUuid)
+ window.executionDialog.reset()
+ window.executionDialog.show()
+ window.executionDialog.fetchExecutionResult(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'
- this.btn.title = this.ellapsed + ' seconds'
- }
+ onExecStatusChanged () {
+ this.domTitle.innerText = this.ellapsed + 's'
+ this.btn.title = this.ellapsed + ' seconds'
}
}
diff --git a/webui/js/ExecutionDialog.js b/webui/js/ExecutionDialog.js
index 028069e..c7fe560 100644
--- a/webui/js/ExecutionDialog.js
+++ b/webui/js/ExecutionDialog.js
@@ -2,21 +2,51 @@
// the 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 {
diff --git a/webui/js/ExecutionFeedbackButton.js b/webui/js/ExecutionFeedbackButton.js
new file mode 100644
index 0000000..50e30fb
--- /dev/null
+++ b/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
+ }
+}
diff --git a/webui/js/marshaller.js b/webui/js/marshaller.js
index a43db09..f97c190 100644
--- a/webui/js/marshaller.js
+++ b/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')
diff --git a/webui/js/websocket.js b/webui/js/websocket.js
index b9ce468..bb81002 100644
--- a/webui/js/websocket.js
+++ b/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()
diff --git a/webui/main.js b/webui/main.js
index fc7e344..cb366f2 100644
--- a/webui/main.js
+++ b/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')
diff --git a/webui/style.css b/webui/style.css
index ecb0699..fa08327 100644
--- a/webui/style.css
+++ b/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;