feature: Live command output! (#325)
* feature: Live command output WIP * cicd: Fix live command output build errors * feature: Live command output WIP, remove stdout/stderr split * feature: Live command output * Restore output im stepExecAfter, rename so to ost for OutputStreamer * Update executor.go * feature: Remove output from log message, bad for long messages, binary output, and security * feature: Live command output WIP
This commit is contained in:
parent
4bac315568
commit
ac0f3ab6f8
|
|
@ -102,8 +102,7 @@ message GetLogsRequest{};
|
|||
message LogEntry {
|
||||
string datetime_started = 1;
|
||||
string action_title = 2;
|
||||
string stdout = 3;
|
||||
string stderr = 4;
|
||||
string output = 3;
|
||||
bool timed_out = 5;
|
||||
int32 exit_code = 6;
|
||||
string user = 7;
|
||||
|
|
@ -185,6 +184,12 @@ message GetReadyzResponse {
|
|||
string status = 1;
|
||||
}
|
||||
|
||||
message EventOutputChunk {
|
||||
string execution_tracking_id = 1;
|
||||
|
||||
string output = 2;
|
||||
}
|
||||
|
||||
message EventEntityChanged {}
|
||||
message EventConfigChanged {}
|
||||
message EventExecutionFinished {
|
||||
|
|
|
|||
|
|
@ -20,8 +20,9 @@ actions:
|
|||
# If you are running OliveTin in a container remember to pass through the
|
||||
# docker socket! https://docs.olivetin.app/action-container-control.html
|
||||
- title: Ping the Internet
|
||||
shell: ping -c 1 1.1.1.1
|
||||
shell: ping -c 3 1.1.1.1
|
||||
icon: ping
|
||||
popupOnStart: execution-dialog-stdout-only
|
||||
|
||||
# This uses `popupOnStart: execution-dialog-stdout-only` to simply show just
|
||||
# the command output.
|
||||
|
|
@ -65,9 +66,10 @@ actions:
|
|||
shell: ping {{ host }} -c {{ count }}
|
||||
icon: ping
|
||||
timeout: 100
|
||||
popupOnStart: execution-dialog-stdout-only
|
||||
arguments:
|
||||
- name: host
|
||||
title: host
|
||||
title: Host
|
||||
type: ascii_identifier
|
||||
default: example.com
|
||||
description: The host that you want to ping
|
||||
|
|
@ -75,7 +77,7 @@ actions:
|
|||
- name: count
|
||||
title: Count
|
||||
type: int
|
||||
default: 1
|
||||
default: 3
|
||||
description: How many times to do you want to ping?
|
||||
|
||||
# OliveTin can control containers - docker is just a command line app.
|
||||
|
|
|
|||
|
|
@ -14,7 +14,6 @@ import (
|
|||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path"
|
||||
|
|
@ -76,10 +75,7 @@ type ExecutionRequest struct {
|
|||
type InternalLogEntry struct {
|
||||
DatetimeStarted time.Time
|
||||
DatetimeFinished time.Time
|
||||
Stdout string
|
||||
Stderr string
|
||||
StdoutBuffer io.ReadCloser
|
||||
StderrBuffer io.ReadCloser
|
||||
Output string
|
||||
TimedOut bool
|
||||
Blocked bool
|
||||
ExitCode int32
|
||||
|
|
@ -130,6 +126,7 @@ func DefaultExecutor(cfg *config.Config) *Executor {
|
|||
type listener interface {
|
||||
OnExecutionStarted(actionTitle string)
|
||||
OnExecutionFinished(logEntry *InternalLogEntry)
|
||||
OnOutputChunk(o []byte, executionTrackingId string)
|
||||
OnActionMapRebuilt()
|
||||
}
|
||||
|
||||
|
|
@ -148,8 +145,7 @@ func (e *Executor) ExecRequest(req *ExecutionRequest) (*sync.WaitGroup, string)
|
|||
req.logEntry = &InternalLogEntry{
|
||||
DatetimeStarted: time.Now(),
|
||||
ExecutionTrackingID: req.TrackingID,
|
||||
Stdout: "",
|
||||
Stderr: "",
|
||||
Output: "",
|
||||
ExitCode: -1337, // If an Action is not actually executed, this is the default exit code.
|
||||
ExecutionStarted: false,
|
||||
ExecutionFinished: false,
|
||||
|
|
@ -214,7 +210,7 @@ func stepConcurrencyCheck(req *ExecutionRequest) bool {
|
|||
"actionTitle": req.logEntry.ActionTitle,
|
||||
}).Warnf(msg)
|
||||
|
||||
req.logEntry.Stdout = msg
|
||||
req.logEntry.Output = msg
|
||||
req.logEntry.Blocked = true
|
||||
return false
|
||||
}
|
||||
|
|
@ -262,7 +258,7 @@ func stepRateCheck(req *ExecutionRequest) bool {
|
|||
"actionTitle": req.logEntry.ActionTitle,
|
||||
}).Infof(msg)
|
||||
|
||||
req.logEntry.Stdout = msg
|
||||
req.logEntry.Output = msg
|
||||
req.logEntry.Blocked = true
|
||||
return false
|
||||
}
|
||||
|
|
@ -281,7 +277,7 @@ func stepParseArgs(req *ExecutionRequest) bool {
|
|||
req.finalParsedCommand, err = parseActionArguments(req.Action.Shell, req.Arguments, req.Action, req.logEntry.ActionTitle, req.EntityPrefix)
|
||||
|
||||
if err != nil {
|
||||
req.logEntry.Stdout = err.Error()
|
||||
req.logEntry.Output = err.Error()
|
||||
|
||||
log.Warnf(err.Error())
|
||||
|
||||
|
|
@ -305,7 +301,7 @@ func stepRequestAction(req *ExecutionRequest) bool {
|
|||
"actionTitle": req.ActionTitle,
|
||||
}).Warnf("Action requested, but not found")
|
||||
|
||||
req.logEntry.Stderr = "Action not found: " + req.ActionTitle
|
||||
req.logEntry.Output = "Action not found: " + req.ActionTitle
|
||||
|
||||
return false
|
||||
}
|
||||
|
|
@ -345,8 +341,7 @@ func stepLogFinish(req *ExecutionRequest) bool {
|
|||
|
||||
log.WithFields(log.Fields{
|
||||
"actionTitle": req.logEntry.ActionTitle,
|
||||
"stdout": req.logEntry.Stdout,
|
||||
"stderr": req.logEntry.Stderr,
|
||||
"outputLength": len(req.logEntry.Output),
|
||||
"timedOut": req.logEntry.TimedOut,
|
||||
"exit": req.logEntry.ExitCode,
|
||||
}).Infof("Action finished")
|
||||
|
|
@ -370,10 +365,27 @@ func wrapCommandInShell(ctx context.Context, finalParsedCommand string) *exec.Cm
|
|||
|
||||
func appendErrorToStderr(err error, logEntry *InternalLogEntry) {
|
||||
if err != nil {
|
||||
logEntry.Stderr = err.Error() + "\n\n" + logEntry.Stderr
|
||||
logEntry.Output = err.Error() + "\n\n" + logEntry.Output
|
||||
}
|
||||
}
|
||||
|
||||
type OutputStreamer struct {
|
||||
Req *ExecutionRequest
|
||||
output bytes.Buffer
|
||||
}
|
||||
|
||||
func (ost *OutputStreamer) Write(o []byte) (n int, err error) {
|
||||
for _, listener := range ost.Req.executor.listeners {
|
||||
listener.OnOutputChunk(o, ost.Req.TrackingID)
|
||||
}
|
||||
|
||||
return ost.output.Write(o)
|
||||
}
|
||||
|
||||
func (ost *OutputStreamer) String() string {
|
||||
return ost.output.String()
|
||||
}
|
||||
|
||||
func buildEnv(req *ExecutionRequest) []string {
|
||||
ret := append(os.Environ(), "OLIVETIN=1")
|
||||
|
||||
|
|
@ -388,15 +400,12 @@ func stepExec(req *ExecutionRequest) bool {
|
|||
ctx, cancel := context.WithTimeout(context.Background(), time.Duration(req.Action.Timeout)*time.Second)
|
||||
defer cancel()
|
||||
|
||||
var stdout bytes.Buffer
|
||||
var stderr bytes.Buffer
|
||||
streamer := &OutputStreamer{Req: req}
|
||||
|
||||
cmd := wrapCommandInShell(ctx, req.finalParsedCommand)
|
||||
cmd.Stdout = streamer
|
||||
cmd.Stderr = streamer
|
||||
cmd.Env = buildEnv(req)
|
||||
cmd.Stdout = &stdout
|
||||
cmd.Stderr = &stderr
|
||||
req.logEntry.StdoutBuffer, _ = cmd.StdoutPipe()
|
||||
req.logEntry.StderrBuffer, _ = cmd.StderrPipe()
|
||||
|
||||
req.logEntry.ExecutionStarted = true
|
||||
|
||||
|
|
@ -407,8 +416,7 @@ func stepExec(req *ExecutionRequest) bool {
|
|||
waiterr := cmd.Wait()
|
||||
|
||||
req.logEntry.ExitCode = int32(cmd.ProcessState.ExitCode())
|
||||
req.logEntry.Stdout = stdout.String()
|
||||
req.logEntry.Stderr = stderr.String()
|
||||
req.logEntry.Output = streamer.String()
|
||||
|
||||
appendErrorToStderr(runerr, req.logEntry)
|
||||
appendErrorToStderr(waiterr, req.logEntry)
|
||||
|
|
@ -435,7 +443,7 @@ func stepExecAfter(req *ExecutionRequest) bool {
|
|||
var stderr bytes.Buffer
|
||||
|
||||
args := map[string]string{
|
||||
"stdout": req.logEntry.Stdout,
|
||||
"output": req.logEntry.Output,
|
||||
"exitCode": fmt.Sprintf("%v", req.logEntry.ExitCode),
|
||||
}
|
||||
|
||||
|
|
@ -449,17 +457,17 @@ func stepExecAfter(req *ExecutionRequest) bool {
|
|||
|
||||
waiterr := cmd.Wait()
|
||||
|
||||
req.logEntry.Stdout += "---\n" + stdout.String()
|
||||
req.logEntry.Stderr += "---\n" + stderr.String()
|
||||
req.logEntry.Output += "---\n" + stdout.String()
|
||||
req.logEntry.Output += "---\n" + stderr.String()
|
||||
|
||||
appendErrorToStderr(runerr, req.logEntry)
|
||||
appendErrorToStderr(waiterr, req.logEntry)
|
||||
|
||||
if ctx.Err() == context.DeadlineExceeded {
|
||||
req.logEntry.Stderr += "Your shellAfterCommand command timed out."
|
||||
req.logEntry.Output += "Your shellAfterCommand command timed out."
|
||||
}
|
||||
|
||||
req.logEntry.Stdout += fmt.Sprintf("Your shellAfterCommand exited with code %v", cmd.ProcessState.ExitCode())
|
||||
req.logEntry.Output += fmt.Sprintf("Your shellAfterCommand exited with code %v", cmd.ProcessState.ExitCode())
|
||||
|
||||
return true
|
||||
}
|
||||
|
|
@ -520,7 +528,7 @@ func saveLogOutput(req *ExecutionRequest, filename string) {
|
|||
dir := firstNonEmpty(req.Action.SaveLogs.OutputDirectory, req.Cfg.SaveLogs.OutputDirectory)
|
||||
|
||||
if dir != "" {
|
||||
data := req.logEntry.Stdout + "\n" + req.logEntry.Stderr
|
||||
data := req.logEntry.Output
|
||||
filepath := path.Join(dir, filename+".log")
|
||||
err := os.WriteFile(filepath, []byte(data), 0644)
|
||||
|
||||
|
|
|
|||
|
|
@ -154,8 +154,7 @@ func internalLogEntryToPb(logEntry *executor.InternalLogEntry) *pb.LogEntry {
|
|||
ActionId: logEntry.ActionId,
|
||||
DatetimeStarted: logEntry.DatetimeStarted.Format("2006-01-02 15:04:05"),
|
||||
DatetimeFinished: logEntry.DatetimeFinished.Format("2006-01-02 15:04:05"),
|
||||
Stdout: logEntry.Stdout,
|
||||
Stderr: logEntry.Stderr,
|
||||
Output: logEntry.Output,
|
||||
TimedOut: logEntry.TimedOut,
|
||||
Blocked: logEntry.Blocked,
|
||||
ExitCode: logEntry.ExitCode,
|
||||
|
|
|
|||
|
|
@ -68,6 +68,17 @@ func checkOriginPermissive(r *http.Request) bool {
|
|||
return true
|
||||
}
|
||||
|
||||
func (WebsocketExecutionListener) OnOutputChunk(chunk []byte, executionTrackingId string) {
|
||||
log.Tracef("outputchunk: %s", string(chunk))
|
||||
|
||||
oc := &pb.EventOutputChunk{
|
||||
Output: string(chunk),
|
||||
ExecutionTrackingId: executionTrackingId,
|
||||
}
|
||||
|
||||
broadcast(oc)
|
||||
}
|
||||
|
||||
func (WebsocketExecutionListener) OnExecutionFinished(logEntry *executor.InternalLogEntry) {
|
||||
evt := &pb.EventExecutionFinished{
|
||||
LogEntry: &pb.LogEntry{
|
||||
|
|
@ -76,8 +87,7 @@ func (WebsocketExecutionListener) OnExecutionFinished(logEntry *executor.Interna
|
|||
ActionId: logEntry.ActionId,
|
||||
DatetimeStarted: logEntry.DatetimeStarted.Format("2006-01-02 15:04:05"),
|
||||
DatetimeFinished: logEntry.DatetimeFinished.Format("2006-01-02 15:04:05"),
|
||||
Stdout: logEntry.Stdout,
|
||||
Stderr: logEntry.Stderr,
|
||||
Output: logEntry.Output,
|
||||
TimedOut: logEntry.TimedOut,
|
||||
Blocked: logEntry.Blocked,
|
||||
ExitCode: logEntry.ExitCode,
|
||||
|
|
|
|||
|
|
@ -2,13 +2,16 @@
|
|||
|
||||
<html lang = "en">
|
||||
<head>
|
||||
<meta name = "viewport" content = "width=device-width, initial-scale=1.0">
|
||||
<meta charset = "UTF-8" />
|
||||
<meta name = "viewport" content = "width=device-width, initial-scale=1.0" />
|
||||
<meta name = "description" content = "Give safe and simple access to predefined shell commands from a web interface." />
|
||||
|
||||
<title>OliveTin</title>
|
||||
|
||||
<link rel = "stylesheet" type = "text/css" href = "style.css" />
|
||||
<link rel = "stylesheet" type = "text/css" href = "theme.css" />
|
||||
<link rel = "stylesheet" href = "node_modules/@xterm/xterm/css/xterm.css" />
|
||||
|
||||
<link rel = "shortcut icon" type = "image/png" href = "OliveTinLogo.png" />
|
||||
|
||||
<link rel = "apple-touch-icon" sizes="57x57" href="OliveTinLogo-57px.png" />
|
||||
|
|
@ -84,12 +87,12 @@
|
|||
</p>
|
||||
</footer>
|
||||
|
||||
<dialog title = "Big Error Message" id = "big-error" class = "error">
|
||||
<dialog title = "Big Error Message" id = "big-error" class = "error padded-content">
|
||||
|
||||
</dialog>
|
||||
|
||||
<dialog title = "Execution Results" id = "execution-results-popup">
|
||||
<div class = "action-header">
|
||||
<div class = "action-header padded-content">
|
||||
<span id = "execution-dialog-icon" class = "icon" role = "img"></span>
|
||||
|
||||
<h2>
|
||||
|
|
@ -98,10 +101,10 @@
|
|||
|
||||
<button id = "execution-dialog-toggle-size" title = "Toggle dialog size">🗖</button>
|
||||
</div>
|
||||
<div id = "execution-dialog-basics">
|
||||
<div id = "execution-dialog-basics" class = "padded-content">
|
||||
<strong>Started: </strong><span id = "execution-dialog-datetime-started">unknown</span>
|
||||
</div>
|
||||
<div id = "execution-dialog-details">
|
||||
<div id = "execution-dialog-details" class = "padded-content">
|
||||
<p>
|
||||
<strong>Finished: </strong><span id = "execution-dialog-datetime-finished">unknown</span>
|
||||
</p>
|
||||
|
|
@ -114,18 +117,13 @@
|
|||
</div>
|
||||
|
||||
<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 id = "execution-dialog-output-details">
|
||||
<summary class = "padded-content">Output</summary>
|
||||
<div id = "execution-dialog-xterm" />
|
||||
</details>
|
||||
</div>
|
||||
|
||||
<div class = "buttons">
|
||||
<div class = "buttons padded-content">
|
||||
<button name = "kill" title = "Kill" id = "execution-dialog-kill-action">Kill</button>
|
||||
|
||||
<form method = "dialog">
|
||||
|
|
@ -136,7 +134,7 @@
|
|||
|
||||
<template id = "tplArgumentForm">
|
||||
<dialog title = "Arguments" id = "argument-popup">
|
||||
<form class = "action-arguments">
|
||||
<form class = "action-arguments padded-content">
|
||||
<div class = "wrapper">
|
||||
<div class = "action-header">
|
||||
<span class = "icon" role = "img"></span>
|
||||
|
|
@ -170,16 +168,6 @@
|
|||
<span role = "img" class = "icon"></span>
|
||||
<a href = "#" class = "content">?</a>
|
||||
|
||||
<details>
|
||||
<summary>stdout</summary>
|
||||
<pre class = "stdout">?</pre>
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>stderr</summary>
|
||||
<pre class = "stderr">?</pre>
|
||||
</details>
|
||||
|
||||
<div class = "tags"></div>
|
||||
</td>
|
||||
<td class = "exit-code">?</td>
|
||||
|
|
|
|||
|
|
@ -7,10 +7,10 @@ export class ExecutionDialog {
|
|||
|
||||
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.domOutput = document.getElementById('execution-dialog-xterm')
|
||||
this.domOutputDetails = document.getElementById('execution-dialog-output-details')
|
||||
this.domOutputToggleBig = document.getElementById('execution-dialog-toggle-size')
|
||||
this.domOutputToggleBig.onclick = () => {
|
||||
this.toggleSize()
|
||||
}
|
||||
|
||||
|
|
@ -24,15 +24,23 @@ export class ExecutionDialog {
|
|||
this.domExecutionBasics = document.getElementById('execution-dialog-basics')
|
||||
this.domExecutionDetails = document.getElementById('execution-dialog-details')
|
||||
this.domExecutionOutput = document.getElementById('execution-dialog-output')
|
||||
|
||||
window.terminal.open(this.domOutput)
|
||||
}
|
||||
|
||||
showOutput () {
|
||||
this.domOutput.hidden = false
|
||||
this.domOutputDetails.open = true
|
||||
this.domExecutionOutput.hidden = false
|
||||
}
|
||||
|
||||
toggleSize () {
|
||||
if (this.dlg.classList.contains('big')) {
|
||||
this.dlg.classList.remove('big')
|
||||
this.domStdout.parentElement.open = false
|
||||
this.domOutputDetails.open = false
|
||||
} else {
|
||||
this.dlg.classList.add('big')
|
||||
this.domStdout.parentElement.open = true
|
||||
this.domOutputDetails.open = true
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -41,12 +49,8 @@ export class ExecutionDialog {
|
|||
this.executionTrackingId = 'notset'
|
||||
|
||||
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.domOutputToggleBig.hidden = false
|
||||
|
||||
this.domIcon.innerText = ''
|
||||
this.domTitle.innerText = 'Waiting for result... '
|
||||
|
|
@ -54,8 +58,8 @@ export class ExecutionDialog {
|
|||
this.domStatus.className = ''
|
||||
this.domDatetimeStarted.innerText = ''
|
||||
this.domDatetimeFinished.innerText = ''
|
||||
this.domStdout.innerText = ''
|
||||
this.domStderr.innerText = ''
|
||||
|
||||
// window.terminal.close()
|
||||
|
||||
this.domBtnKill.disabled = true
|
||||
this.domBtnKill.onclick = () => {}
|
||||
|
|
@ -64,7 +68,9 @@ export class ExecutionDialog {
|
|||
this.domExecutionBasics.hidden = false
|
||||
|
||||
this.domExecutionDetails.hidden = true
|
||||
this.domStdout.parentElement.open = false
|
||||
this.domOutputDetails.open = false
|
||||
|
||||
window.terminal.reset()
|
||||
|
||||
this.domExecutionOutput.hidden = true
|
||||
}
|
||||
|
|
@ -105,7 +111,7 @@ export class ExecutionDialog {
|
|||
},
|
||||
body: JSON.stringify(killActionArgs)
|
||||
}).then((res) => {
|
||||
console.log(res)
|
||||
console.log(res.json())
|
||||
}).catch(err => {
|
||||
throw err
|
||||
})
|
||||
|
|
@ -157,7 +163,7 @@ export class ExecutionDialog {
|
|||
if (!this.hideDetailsOnResult) {
|
||||
this.domExecutionDetails.hidden = false
|
||||
} else {
|
||||
this.domStdout.parentElement.open = true
|
||||
this.domOutputDetails.open = true
|
||||
}
|
||||
|
||||
this.executionTrackingId = res.logEntry.executionTrackingId
|
||||
|
|
@ -190,18 +196,10 @@ export class ExecutionDialog {
|
|||
this.domIcon.innerHTML = res.logEntry.actionIcon
|
||||
this.domTitle.innerText = res.logEntry.actionTitle
|
||||
|
||||
this.domStdout.innerText = res.logEntry.stdout
|
||||
this.domStdout.innerText = res.logEntry.stdout
|
||||
|
||||
if (res.logEntry.stderr === '(empty)') {
|
||||
this.domStderr.parentElement.style.display = 'none'
|
||||
this.domStderr.innerText = res.logEntry.stderr
|
||||
} else {
|
||||
this.domStderr.parentElement.style.display = 'block'
|
||||
this.domStderr.innerText = res.logEntry.stderr
|
||||
}
|
||||
|
||||
this.domDatetimeStarted.innerText = res.logEntry.datetimeStarted
|
||||
|
||||
window.terminal.reset()
|
||||
window.terminal.write(res.logEntry.output)
|
||||
}
|
||||
|
||||
renderError (err) {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import './ActionButton.js' // To define action-button
|
||||
import { ExecutionDialog } from './ExecutionDialog.js'
|
||||
import { Terminal } from '@xterm/xterm'
|
||||
|
||||
/**
|
||||
* This is a weird function that just sets some globals.
|
||||
|
|
@ -8,6 +9,10 @@ export function initMarshaller () {
|
|||
window.changeDirectory = changeDirectory
|
||||
window.showSection = showSection
|
||||
|
||||
window.terminal = new Terminal({
|
||||
convertEol: true
|
||||
})
|
||||
|
||||
window.executionDialog = new ExecutionDialog()
|
||||
|
||||
window.logEntries = {}
|
||||
|
|
@ -17,6 +22,7 @@ export function initMarshaller () {
|
|||
window.currentSection = ''
|
||||
|
||||
window.addEventListener('EventExecutionFinished', onExecutionFinished)
|
||||
window.addEventListener('EventOutputChunk', onOutputChunk)
|
||||
}
|
||||
|
||||
export function marshalDashboardComponentsJsonToHtml (json) {
|
||||
|
|
@ -57,6 +63,16 @@ function marshalActionsJsonToHtml (json) {
|
|||
}
|
||||
}
|
||||
|
||||
function onOutputChunk (evt) {
|
||||
const chunk = evt.payload
|
||||
|
||||
if (chunk.executionTrackingId === window.executionDialog.executionTrackingId) {
|
||||
window.terminal.write(chunk.output)
|
||||
|
||||
window.executionDialog.showOutput()
|
||||
}
|
||||
}
|
||||
|
||||
function onExecutionFinished (evt) {
|
||||
const logEntry = evt.payload.logEntry
|
||||
|
||||
|
|
@ -78,7 +94,7 @@ function onExecutionFinished (evt) {
|
|||
// have it, so we open the dialog and it will get updated below.
|
||||
|
||||
window.executionDialog.show()
|
||||
window.executionDialog.executionUuid = logEntry.uuid
|
||||
window.executionDialog.executionTrackingId = logEntry.uuid
|
||||
|
||||
break
|
||||
default:
|
||||
|
|
@ -476,14 +492,6 @@ export function marshalLogsJsonToHtml (json) {
|
|||
const tpl = document.getElementById('tplLogRow')
|
||||
const row = tpl.content.querySelector('tr').cloneNode(true)
|
||||
|
||||
if (logEntry.stdout.length === 0) {
|
||||
logEntry.stdout = '(empty)'
|
||||
}
|
||||
|
||||
if (logEntry.stderr.length === 0) {
|
||||
logEntry.stderr = '(empty)'
|
||||
}
|
||||
|
||||
let logTableExitCode = logEntry.exitCode
|
||||
|
||||
if (logEntry.exitCode === 0) {
|
||||
|
|
@ -497,8 +505,6 @@ export function marshalLogsJsonToHtml (json) {
|
|||
row.querySelector('.timestamp').innerText = logEntry.datetimeStarted
|
||||
row.querySelector('.content').innerText = logEntry.actionTitle
|
||||
row.querySelector('.icon').innerHTML = logEntry.actionIcon
|
||||
row.querySelector('pre.stdout').innerText = logEntry.stdout
|
||||
row.querySelector('pre.stderr').innerText = logEntry.stderr
|
||||
row.querySelector('.exit-code').innerText = logTableExitCode
|
||||
row.setAttribute('title', logEntry.actionTitle)
|
||||
|
||||
|
|
|
|||
|
|
@ -51,6 +51,7 @@ function websocketOnMessage (msg) {
|
|||
e.payload = j.payload
|
||||
|
||||
switch (j.type) {
|
||||
case 'EventOutputChunk':
|
||||
case 'EventConfigChanged':
|
||||
case 'EventExecutionFinished':
|
||||
case 'EventEntityChanged':
|
||||
|
|
|
|||
|
|
@ -8,6 +8,9 @@
|
|||
"name": "olivetin-webui",
|
||||
"version": "1.0.0",
|
||||
"license": "AGPL-3.0-only",
|
||||
"dependencies": {
|
||||
"@xterm/xterm": "^5.5.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"eslint": "^7.25.0",
|
||||
"eslint-config-standard": "^16.0.2",
|
||||
|
|
@ -16,6 +19,7 @@
|
|||
"eslint-plugin-promise": "^4.3.1",
|
||||
"parcel": "^2.11.0",
|
||||
"parcel-resolver-ignore": "^2.2.0",
|
||||
"process": "^0.11.10",
|
||||
"stylelint": "^15.6.0",
|
||||
"stylelint-config-standard": "^33.0.0"
|
||||
}
|
||||
|
|
@ -2233,6 +2237,11 @@
|
|||
"integrity": "sha512-Gj7cI7z+98M282Tqmp2K5EIsoouUEzbBJhQQzDE3jSIRk6r9gsz0oUokqIUR4u1R3dMHo0pDHM7sNOHyhulypw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@xterm/xterm": {
|
||||
"version": "5.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@xterm/xterm/-/xterm-5.5.0.tgz",
|
||||
"integrity": "sha512-hqJHYaQb5OptNunnyAnkHyM8aCjZ1MEIDTQu1iIbbTD/xops91NB5yq1ZK/dC2JDbVWtF23zUtl9JE2NqwT87A=="
|
||||
},
|
||||
"node_modules/abortcontroller-polyfill": {
|
||||
"version": "1.7.5",
|
||||
"resolved": "https://registry.npmjs.org/abortcontroller-polyfill/-/abortcontroller-polyfill-1.7.5.tgz",
|
||||
|
|
@ -5371,6 +5380,15 @@
|
|||
"node": ">= 0.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/process": {
|
||||
"version": "0.11.10",
|
||||
"resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz",
|
||||
"integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">= 0.6.0"
|
||||
}
|
||||
},
|
||||
"node_modules/progress": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz",
|
||||
|
|
@ -7739,6 +7757,11 @@
|
|||
"integrity": "sha512-Gj7cI7z+98M282Tqmp2K5EIsoouUEzbBJhQQzDE3jSIRk6r9gsz0oUokqIUR4u1R3dMHo0pDHM7sNOHyhulypw==",
|
||||
"dev": true
|
||||
},
|
||||
"@xterm/xterm": {
|
||||
"version": "5.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@xterm/xterm/-/xterm-5.5.0.tgz",
|
||||
"integrity": "sha512-hqJHYaQb5OptNunnyAnkHyM8aCjZ1MEIDTQu1iIbbTD/xops91NB5yq1ZK/dC2JDbVWtF23zUtl9JE2NqwT87A=="
|
||||
},
|
||||
"abortcontroller-polyfill": {
|
||||
"version": "1.7.5",
|
||||
"resolved": "https://registry.npmjs.org/abortcontroller-polyfill/-/abortcontroller-polyfill-1.7.5.tgz",
|
||||
|
|
@ -9933,6 +9956,12 @@
|
|||
"integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==",
|
||||
"dev": true
|
||||
},
|
||||
"process": {
|
||||
"version": "0.11.10",
|
||||
"resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz",
|
||||
"integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==",
|
||||
"dev": true
|
||||
},
|
||||
"progress": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz",
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@
|
|||
"eslint-plugin-promise": "^4.3.1",
|
||||
"parcel": "^2.11.0",
|
||||
"parcel-resolver-ignore": "^2.2.0",
|
||||
"process": "^0.11.10",
|
||||
"stylelint": "^15.6.0",
|
||||
"stylelint-config-standard": "^33.0.0"
|
||||
},
|
||||
|
|
@ -26,5 +27,8 @@
|
|||
"OliveTinLogo-120px.png",
|
||||
"OliveTinLogo-180px.png"
|
||||
],
|
||||
"license": "AGPL-3.0-only"
|
||||
"license": "AGPL-3.0-only",
|
||||
"dependencies": {
|
||||
"@xterm/xterm": "^5.5.0"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,15 +13,16 @@ dialog {
|
|||
box-shadow: 0 0 6px 0 #444;
|
||||
max-width: 600px;
|
||||
min-width: 60%;
|
||||
padding: 1em;
|
||||
text-align: left;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
dialog.big {
|
||||
max-width: 100%;
|
||||
width: calc(100vw - 2em);
|
||||
max-width: 100vw;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
border: none;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
fieldset {
|
||||
|
|
@ -393,10 +394,6 @@ form .wrapper span.icon {
|
|||
vertical-align: middle;
|
||||
}
|
||||
|
||||
form input[type="submit"]:first-child {
|
||||
margin-right: 1em;
|
||||
}
|
||||
|
||||
button[name="cancel"]:hover {
|
||||
background-color: salmon;
|
||||
color: black;
|
||||
|
|
@ -421,14 +418,6 @@ div.buttons {
|
|||
gap: 1em;
|
||||
}
|
||||
|
||||
pre {
|
||||
border: 1px solid gray;
|
||||
padding: 1em;
|
||||
min-height: 1em;
|
||||
overflow: auto;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
td.exit-code {
|
||||
text-align: center;
|
||||
}
|
||||
|
|
@ -475,6 +464,22 @@ div.display {
|
|||
font-size: small;
|
||||
}
|
||||
|
||||
#execution-dialog-xterm {
|
||||
overflow: scroll;
|
||||
}
|
||||
|
||||
.padded-content {
|
||||
padding: 1em;
|
||||
}
|
||||
|
||||
.xterm {
|
||||
padding: 1em;
|
||||
margin-bottom: 1em;
|
||||
margin-left: 1em;
|
||||
margin-right: 1em;
|
||||
width: fit-content;
|
||||
}
|
||||
|
||||
@media screen and (width <= 600px) {
|
||||
fieldset {
|
||||
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
|
||||
|
|
@ -499,7 +504,15 @@ div.display {
|
|||
dialog {
|
||||
border-left: 0;
|
||||
border-right: 0;
|
||||
width: 100%;
|
||||
margin-left: 0;
|
||||
margin-right: 0;
|
||||
width: 100vw;
|
||||
max-width: 100vw;
|
||||
}
|
||||
|
||||
.xterm {
|
||||
margin-left: 0;
|
||||
margin-right: 0;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue