jamesread před 5 roky
rodič
revize
c072e16cc4

+ 21 - 4
OliveTin.proto

@@ -22,10 +22,22 @@ message StartActionRequest {
 }
 
 message StartActionResponse {
-	string stdout = 1;
-	string stderr = 2;
-	bool timedOut = 3;
-	int32 exitCode = 4;
+	LogEntry logEntry = 1;
+}
+
+message GetLogsRequest{};
+
+message LogEntry {
+	string datetime = 1;
+	string actionTitle = 2;
+	string stdout = 3;
+	string stderr = 4;
+	bool timedOut = 5;
+	int32 exitCode = 6;
+}
+
+message GetLogsResponse {
+	repeated LogEntry logs = 1;
 }
 
 service OliveTinApi {
@@ -41,4 +53,9 @@ service OliveTinApi {
 		};
 	}
 
+	rpc GetLogs(GetLogsRequest) returns (GetLogsResponse) {
+		option (google.api.http) = {
+			get: "/api/GetLogs"
+		};
+	}
 }

+ 35 - 6
internal/executor/executor.go

@@ -11,8 +11,22 @@ import (
 	"time"
 )
 
+type InternalLogEntry struct {
+	Datetime string
+	Content string
+	Stdout string
+	Stderr string
+	TimedOut bool
+	ExitCode int32
+	ActionTitle string
+}
+
+type Executor struct {
+	Logs []InternalLogEntry
+}
+
 // ExecAction executes an action.
-func ExecAction(cfg *config.Config, action string) *pb.StartActionResponse {
+func (e *Executor) ExecAction(cfg *config.Config, action string) *pb.StartActionResponse {
 	log.WithFields(log.Fields{
 		"actionName": action,
 	}).Infof("StartAction")
@@ -23,16 +37,31 @@ func ExecAction(cfg *config.Config, action string) *pb.StartActionResponse {
 		log.Errorf("Error finding action %s, %s", err, action)
 
 		return &pb.StartActionResponse{
-			TimedOut: false,
+			LogEntry: nil,
 		}
 	}
 
-	return execAction(cfg, actualAction)
+	res := execAction(cfg, actualAction)
+
+	e.Logs = append(e.Logs, *res);
+
+	return &pb.StartActionResponse{
+		LogEntry: &pb.LogEntry {
+			ActionTitle: actualAction.Title,
+			TimedOut: res.TimedOut,
+			Stderr: res.Stderr,
+			Stdout: res.Stdout,
+			ExitCode: res.ExitCode,
+		},
+	};
 }
 
-func execAction(cfg *config.Config, actualAction *config.ActionButton) *pb.StartActionResponse {
-	res := &pb.StartActionResponse{}
-	res.TimedOut = false
+func execAction(cfg *config.Config, actualAction *config.ActionButton) *InternalLogEntry {
+	res := &InternalLogEntry {
+		Datetime: time.Now().Format("2006-01-02 15:04:05"),
+		TimedOut: false,
+		ActionTitle: actualAction.Title,
+	}
 
 	log.WithFields(log.Fields{
 		"title":   actualAction.Title,

+ 21 - 1
internal/grpcapi/grpcApi.go

@@ -15,6 +15,7 @@ import (
 
 var (
 	cfg *config.Config
+	ex = executor.Executor{}
 )
 
 type oliveTinAPI struct {
@@ -22,7 +23,7 @@ type oliveTinAPI struct {
 }
 
 func (api *oliveTinAPI) StartAction(ctx ctx.Context, req *pb.StartActionRequest) (*pb.StartActionResponse, error) {
-	return executor.ExecAction(cfg, req.ActionName), nil
+	return ex.ExecAction(cfg, req.ActionName), nil
 }
 
 func (api *oliveTinAPI) GetButtons(ctx ctx.Context, req *pb.GetButtonsRequest) (*pb.GetButtonsResponse, error) {
@@ -43,6 +44,25 @@ func (api *oliveTinAPI) GetButtons(ctx ctx.Context, req *pb.GetButtonsRequest) (
 	return res, nil
 }
 
+func (api *oliveTinAPI) GetLogs(ctx ctx.Context, req *pb.GetLogsRequest) (*pb.GetLogsResponse, error) {
+	ret := &pb.GetLogsResponse{};
+	
+	// TODO Limit to 10 entries or something to prevent browser lag.
+
+	for _, logEntry := range ex.Logs {
+		ret.Logs = append(ret.Logs, &pb.LogEntry{
+			ActionTitle: logEntry.ActionTitle,
+			Datetime: logEntry.Datetime,
+			Stdout: logEntry.Stdout,
+			Stderr: logEntry.Stderr,
+			TimedOut: logEntry.TimedOut,
+			ExitCode: logEntry.ExitCode,
+		})
+	}
+
+	return ret, nil
+}
+
 // Start will start the GRPC API.
 func Start(globalConfig *config.Config) {
 	cfg = globalConfig

+ 36 - 2
webui/index.html

@@ -12,9 +12,27 @@
 
 	<body>
 		<main title = "main content">
-			<fieldset id = "rootGroup">
-				<legend>OliveTin Actions</legend>
+			<fieldset id = "switcher">
+				<button id = "showActions">Actions</button>
+				<button id = "showLogs">Logs</button>
 			</fieldset>
+
+			<section id = "contentLogs" title = "Logs" hidden>
+				<table>
+					<thead>
+						<tr>
+							<th>Timestamp</th>
+							<th>Log</th>
+						</tr>
+					</thead>
+					<tbody id = "logTableBody" />
+				</table>
+			</section>
+
+			<section id = "contentActions" title = "Actions" hidden >
+				<fieldset id = "rootGroup">
+				</fieldset>
+			</section>
 		</main>
 		<footer title = "footer">
 			<p><img title = "application icon" src = "OliveTinLogo.png" height = "1em" class = "logo" /> OliveTin</p>
@@ -29,6 +47,22 @@
 			<p role = "title" class = "title">Untitled Button</p>
 		</template>
 
+		<template id = "tplLogRow">
+			<tr>
+				<td class = "timestamp">?</td> 
+				<td>
+					<span class = "content">?</span>
+				
+					<details>
+						<summary>stdout</summary>
+						<pre>
+							?
+						</pre>
+					</details>
+				</td>
+			</tr>
+		</template>
+
 		<script type = "module" src = "main.js"></script>
 	</body>
 </html>

+ 15 - 3
webui/js/ActionButton.js

@@ -1,3 +1,5 @@
+import { marshalLogsJsonToHtml } from './marshaller.js';
+
 class ActionButton extends window.HTMLButtonElement {
   constructFromJson (json) {
     this.updateIterationTimestamp = 0;
@@ -40,10 +42,12 @@ class ActionButton extends window.HTMLButtonElement {
 
     window.fetch(this.actionCallUrl).then(res => res.json()
     ).then((json) => {
-      if (json.timedOut) {
+      marshalLogsJsonToHtml({"logs": [json.logEntry]})
+
+      if (json.logEntry.timedOut) {
         this.onActionResult('actionTimeout', 'Timed out')
-      } else if (json.exitCode !== 0) {
-        this.onActionResult('actionNonZeroExit', 'Exit code ' + json.exitCode)
+      } else if (json.logEntry.exitCode !== 0) {
+        this.onActionResult('actionNonZeroExit', 'Exit code ' + json.logEntry.exitCode)
       } else {
         this.onActionResult('actionSuccess', 'Success!')
       }
@@ -56,6 +60,10 @@ class ActionButton extends window.HTMLButtonElement {
     this.temporaryStatusMessage = '[ ' + temporaryStatusMessage + ' ]'
     this.updateHtml()
     this.classList.add(cssClass)
+
+    setTimeout(() => {
+      this.classList.remove(cssClass)
+    }, 1000);
   }
 
   onActionError (err) {
@@ -64,6 +72,10 @@ class ActionButton extends window.HTMLButtonElement {
     this.isWaiting = false
     this.updateHtml()
     this.classList.add('actionFailed')
+
+    setTimeout(() => {
+      this.classList.remove('actionFailed')
+    }, 1000);
   }
 
   constructTemplate () {

+ 0 - 14
webui/js/logger.js

@@ -1,14 +0,0 @@
-window.logs = []
-
-export function addToLog (evt) {
-	window.logs.append(evt)
-
-	showLog(evt)
-}
-
-function showLog (evt) {
-	let msg = document.createElement('pre')
-	msg.innerText = evt;
-
-	document.body.appendChild(msg)
-}

+ 14 - 2
webui/js/marshaller.js

@@ -16,13 +16,25 @@ export function marshalActionButtonsJsonToHtml (json) {
       htmlButton.updateHtml()
     }
 
-    console.log("action", jsonButton.title)
     htmlButton.updateIterationTimestamp = currentIterationTimestamp;
   }
 
-  for (const existingButton of document.querySelectorAll('button')) {
+  for (const existingButton of document.querySelector('#contentActions').querySelectorAll('button')) {
     if (existingButton.updateIterationTimestamp != currentIterationTimestamp) {
       existingButton.remove();
     }
   }
 }
+
+export function marshalLogsJsonToHtml (json) {
+  for (const logEntry of json.logs) {
+    const tpl = document.getElementById('tplLogRow')
+    const row = tpl.content.cloneNode(true)
+
+    row.querySelector('.timestamp').innerText = logEntry.datetime
+    row.querySelector('.content').innerText = logEntry.actionTitle
+    row.querySelector('pre').innerText = logEntry.stdout
+
+    document.querySelector('#logTableBody').prepend(row)
+  }
+}

+ 35 - 3
webui/main.js

@@ -1,6 +1,6 @@
 'use strict'
 
-import { marshalActionButtonsJsonToHtml } from './js/marshaller.js'
+import { marshalActionButtonsJsonToHtml, marshalLogsJsonToHtml } from './js/marshaller.js'
 
 function showBigError (type, friendlyType, message) {
   clearInterval(window.buttonInterval)
@@ -14,10 +14,26 @@ function showBigError (type, friendlyType, message) {
   document.getElementById('rootGroup').appendChild(domErr)
 }
 
+function showSection (name) {
+  for (let otherName of ["Actions", "Logs"]) {
+    document.getElementById('show' + otherName).classList.remove('activeSection');
+    document.getElementById('content' + otherName).hidden = true;
+  }
+
+  document.getElementById('show' + name).classList.add('activeSection')
+  document.getElementById('content' + name).hidden = false;
+}
+
+function setupSections() {
+  document.getElementById('showActions').onclick = () => { showSection('Actions') };
+  document.getElementById('showLogs').onclick = () => { showSection('Logs') }
+  
+  showSection('Actions');
+}
+
 function fetchGetButtons() {
- window.fetch(window.restBaseUrl + 'GetButtons', {
+  window.fetch(window.restBaseUrl + 'GetButtons', {
     cors: 'cors'
-    // No fetch options
   }).then(res => {
     return res.json()
   }).then(res => {
@@ -27,6 +43,18 @@ function fetchGetButtons() {
   })
 }
 
+function fetchGetLogs() {
+  window.fetch(window.restBaseUrl + 'GetLogs', {
+    cors: 'cors'
+  }).then(res => {
+    return res.json()
+  }).then(res => {
+    marshalLogsJsonToHtml(res)
+  }).catch(err => {
+    showBigError('fetch-buttons', 'getting buttons', err, 'blat')
+  })
+}
+
 function processWebuiSettingsJson (settings) {
   window.restBaseUrl = settings.Rest
 
@@ -40,14 +68,18 @@ function processWebuiSettingsJson (settings) {
   }
 }
 
+setupSections();
+
 window.fetch('webUiSettings.json').then(res => {
   return res.json()
 }).then(res => {
   processWebuiSettingsJson(res)
 
   fetchGetButtons()
+  fetchGetLogs()
 
   window.buttonInterval = setInterval(fetchGetButtons, 3000);
 }).catch(err => {
   showBigError('fetch-webui-settings', 'getting webui settings', err)
 })
+

+ 17 - 1
webui/style.css

@@ -15,7 +15,6 @@ fieldset#rootGroup {
   display: grid;
   grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
   grid-template-rows: auto auto auto auto;
-  padding: 1em;
   grid-gap: 1em;
   text-align: center;
   border: 0;
@@ -231,3 +230,20 @@ details[open] {
 	margin-top: 1em;
 	display: block;
 }
+
+main {
+	padding: 1em;
+}
+
+summary {
+	cursor: pointer;
+}
+
+details {
+	display: inline-block;
+}
+
+details[open] {
+	margin-top: 1em;
+	display: block;
+}

+ 3 - 0
webui/themes/waffles/theme.css

@@ -0,0 +1,3 @@
+body {
+	color: green;
+}