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