feature: websocket implemenation at last!
This commit is contained in:
parent
6116e954ba
commit
e5a870ed94
|
|
@ -8,16 +8,16 @@ message Action {
|
|||
string id = 1;
|
||||
string title = 2;
|
||||
string icon = 3;
|
||||
bool canExec = 4;
|
||||
bool can_exec = 4;
|
||||
repeated ActionArgument arguments = 5;
|
||||
bool popupOnStart = 6;
|
||||
bool popup_on_start = 6;
|
||||
}
|
||||
|
||||
message ActionArgument {
|
||||
string name = 1;
|
||||
string title = 2;
|
||||
string type = 3;
|
||||
string defaultValue = 4;
|
||||
string default_value = 4;
|
||||
|
||||
repeated ActionArgumentChoice choices = 5;
|
||||
|
||||
|
|
@ -44,9 +44,11 @@ message GetDashboardComponentsResponse {
|
|||
message GetDashboardComponentsRequest {}
|
||||
|
||||
message StartActionRequest {
|
||||
string actionName = 1;
|
||||
string action_name = 1;
|
||||
|
||||
repeated StartActionArgument arguments = 2;
|
||||
|
||||
string uuid = 3;
|
||||
}
|
||||
|
||||
message StartActionArgument {
|
||||
|
|
@ -55,24 +57,25 @@ message StartActionArgument {
|
|||
}
|
||||
|
||||
message StartActionResponse {
|
||||
string executionUuid = 2;
|
||||
string execution_uuid = 2;
|
||||
}
|
||||
|
||||
message GetLogsRequest{};
|
||||
|
||||
message LogEntry {
|
||||
string datetimeStarted = 1;
|
||||
string actionTitle = 2;
|
||||
string datetime_started = 1;
|
||||
string action_title = 2;
|
||||
string stdout = 3;
|
||||
string stderr = 4;
|
||||
bool timedOut = 5;
|
||||
int32 exitCode = 6;
|
||||
bool timed_out = 5;
|
||||
int32 exit_code = 6;
|
||||
string user = 7;
|
||||
string userClass = 8;
|
||||
string actionIcon = 9;
|
||||
string user_class = 8;
|
||||
string action_icon = 9;
|
||||
repeated string tags = 10;
|
||||
string executionUuid = 11;
|
||||
string datetimeFinished = 12;
|
||||
string execution_uuid = 11;
|
||||
string datetime_finished = 12;
|
||||
string uuid = 13;
|
||||
}
|
||||
|
||||
message GetLogsResponse {
|
||||
|
|
@ -90,17 +93,25 @@ message ValidateArgumentTypeResponse {
|
|||
}
|
||||
|
||||
message WatchExecutionRequest {
|
||||
string executionUuid = 1;
|
||||
string execution_uuid = 1;
|
||||
}
|
||||
|
||||
message WatchExecutionUpdate {
|
||||
string update = 1;
|
||||
}
|
||||
|
||||
message ExecutionStatusRequest {
|
||||
string execution_uuid = 1;
|
||||
}
|
||||
|
||||
message ExecutionStatusResponse {
|
||||
LogEntry log_entry = 1;
|
||||
}
|
||||
|
||||
message WhoAmIRequest {}
|
||||
|
||||
message WhoAmIResponse {
|
||||
string authenticatedUser = 1;
|
||||
string authenticated_user = 1;
|
||||
}
|
||||
|
||||
message SosReportRequest {}
|
||||
|
|
@ -109,7 +120,7 @@ message SosReportResponse {
|
|||
string alert = 1;
|
||||
}
|
||||
|
||||
service OliveTinApi {
|
||||
service OliveTinApiService {
|
||||
rpc GetDashboardComponents(GetDashboardComponentsRequest) returns (GetDashboardComponentsResponse) {
|
||||
option (google.api.http) = {
|
||||
get: "/api/GetDashboardComponents"
|
||||
|
|
@ -123,9 +134,9 @@ service OliveTinApi {
|
|||
};
|
||||
}
|
||||
|
||||
rpc WatchExecution(WatchExecutionRequest) returns (stream WatchExecutionUpdate) {
|
||||
rpc ExecutionStatus(ExecutionStatusRequest) returns (ExecutionStatusResponse) {
|
||||
option (google.api.http) = {
|
||||
post: "/api/WatchExecution"
|
||||
post: "/api/ExecutionStatus"
|
||||
body: "*"
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import (
|
|||
"github.com/OliveTin/OliveTin/internal/oncron"
|
||||
"github.com/OliveTin/OliveTin/internal/onstartup"
|
||||
updatecheck "github.com/OliveTin/OliveTin/internal/updatecheck"
|
||||
"github.com/OliveTin/OliveTin/internal/websocket"
|
||||
|
||||
"github.com/OliveTin/OliveTin/internal/httpservers"
|
||||
|
||||
|
|
@ -110,6 +111,7 @@ func main() {
|
|||
log.Debugf("Config: %+v", cfg)
|
||||
|
||||
executor := executor.DefaultExecutor()
|
||||
executor.AddListener(websocket.ExecutionListener)
|
||||
|
||||
go onstartup.Execute(cfg, executor)
|
||||
go oncron.Schedule(cfg, executor)
|
||||
|
|
|
|||
2
go.mod
2
go.mod
|
|
@ -9,6 +9,7 @@ require (
|
|||
github.com/go-critic/go-critic v0.8.2
|
||||
github.com/golang-jwt/jwt/v4 v4.5.0
|
||||
github.com/google/uuid v1.3.0
|
||||
github.com/gorilla/websocket v1.4.1
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.2
|
||||
github.com/robfig/cron/v3 v3.0.1
|
||||
github.com/sirupsen/logrus v1.9.3
|
||||
|
|
@ -20,7 +21,6 @@ require (
|
|||
google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.3.0
|
||||
google.golang.org/protobuf v1.31.0
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
nhooyr.io/websocket v1.8.7
|
||||
)
|
||||
|
||||
require (
|
||||
|
|
|
|||
39
go.sum
39
go.sum
|
|
@ -94,10 +94,6 @@ github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4
|
|||
github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw=
|
||||
github.com/fzipp/gocyclo v0.6.0 h1:lsblElZG7d3ALtGMx9fmxeTKZaLLpU8mET09yN4BBLo=
|
||||
github.com/fzipp/gocyclo v0.6.0/go.mod h1:rXPyn8fnlpa0R2csP/31uerbiVBugk5whMdlyaLkLoA=
|
||||
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
|
||||
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
|
||||
github.com/gin-gonic/gin v1.6.3 h1:ahKqKTFpO5KTPHxWZjEdPScmYaGtLo8Y4DMHoEsnp14=
|
||||
github.com/gin-gonic/gin v1.6.3/go.mod h1:75u5sXoLsGZoRN5Sgbi1eraJ4GU3++wFwWzhwvtwp4M=
|
||||
github.com/go-chi/chi/v5 v5.0.10 h1:rLz5avzKpjqxrYwXNfmjkrYYXOyLJd37pz53UFHC6vk=
|
||||
github.com/go-chi/chi/v5 v5.0.10/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
|
||||
github.com/go-critic/go-critic v0.8.2 h1:mekhZ9jw5NBEj3I8o/EywXw5zBfGAJuMo4VVVjtxF80=
|
||||
|
|
@ -110,13 +106,6 @@ github.com/go-logr/logr v1.2.4 h1:g01GSCwiDw2xSZfjJ2/T9M+S6pFdcNtFYsp+Y43HYDQ=
|
|||
github.com/go-logr/logr v1.2.4/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
||||
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
||||
github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
||||
github.com/go-playground/locales v0.13.0 h1:HyWk6mgj5qFqCT5fjGBuRArbVDfE4hi8+e8ceBS/t7Q=
|
||||
github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8=
|
||||
github.com/go-playground/universal-translator v0.17.0 h1:icxd5fm+REJzpZx7ZfpaD876Lmtgy7VtROAbHHXk8no=
|
||||
github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA=
|
||||
github.com/go-playground/validator/v10 v10.2.0 h1:KgJ0snyC2R9VXYN2rneOtQcw5aHQB1Vv0sFl1UcHBOY=
|
||||
github.com/go-playground/validator/v10 v10.2.0/go.mod h1:uOYAAleCW8F/7oMFd6aG0GOhaH6EGOAJShg8Id5JGkI=
|
||||
github.com/go-toolsmith/astcast v1.1.0 h1:+JN9xZV1A+Re+95pgnMgDboWNVnIMMQXwfBwLRPgSC8=
|
||||
github.com/go-toolsmith/astcast v1.1.0/go.mod h1:qdcuFWeGGS2xX5bLM/c3U9lewg7+Zu4mr+xPwZIB4ZU=
|
||||
github.com/go-toolsmith/astcopy v1.1.0 h1:YGwBN0WM+ekI/6SS6+52zLDEf8Yvp3n2seZITCUBt5s=
|
||||
|
|
@ -135,12 +124,6 @@ github.com/go-toolsmith/strparse v1.1.0 h1:GAioeZUK9TGxnLS+qfdqNbA4z0SSm5zVNtCQi
|
|||
github.com/go-toolsmith/strparse v1.1.0/go.mod h1:7ksGy58fsaQkGQlY8WVoBFNyEPMGuJin1rfoPS4lBSQ=
|
||||
github.com/go-toolsmith/typep v1.1.0 h1:fIRYDyF+JywLfqzyhdiHzRop/GQDxxNhLGQ6gFUNHus=
|
||||
github.com/go-toolsmith/typep v1.1.0/go.mod h1:fVIw+7zjdsMxDA3ITWnH1yOiw1rnTQKCsF/sk2H/qig=
|
||||
github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee h1:s+21KNqlpePfkah2I+gwHF8xmJWRjooY+5248k6m4A0=
|
||||
github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee/go.mod h1:L0fX3K22YWvt/FAX9NnzrNzcI4wNYi9Yku4O0LKYflo=
|
||||
github.com/gobwas/pool v0.2.0 h1:QEmUOlnSjWtnpRGHF3SauEiOsy82Cup83Vf2LcMlnc8=
|
||||
github.com/gobwas/pool v0.2.0/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw=
|
||||
github.com/gobwas/ws v1.0.2 h1:CoAavW/wd/kulfZmSIBt6p24n4j7tHgNVCjsfHVNUbo=
|
||||
github.com/gobwas/ws v1.0.2/go.mod h1:szmBTxLgaFppYjEmNtny/v3w89xOydFnnZMcgRRu/EM=
|
||||
github.com/gofrs/flock v0.8.1 h1:+gYjHKf32LDeiEEFhQaotPbLuUXjY5ZqxKgXy7n59aw=
|
||||
github.com/gofrs/uuid/v5 v5.0.0 h1:p544++a97kEL+svbcFbCQVM9KFu0Yo25UoISXGNNH9M=
|
||||
github.com/gofrs/uuid/v5 v5.0.0/go.mod h1:CDOjlDMVAtN56jqyRUZh58JT31Tiw7/oQyEXZV+9bD8=
|
||||
|
|
@ -194,7 +177,6 @@ github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeN
|
|||
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
|
||||
github.com/google/go-containerregistry v0.16.1 h1:rUEt426sR6nyrL3gt+18ibRcvYpKYdpsa5ZW7MA08dQ=
|
||||
github.com/google/go-containerregistry v0.16.1/go.mod h1:u0qB2l7mvtWVR5kNcbFIhFY1hLbf8eeGapA+vbFDCtQ=
|
||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
|
||||
github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
|
||||
github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
|
||||
|
|
@ -234,13 +216,10 @@ github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLf
|
|||
github.com/jdxcode/netrc v0.0.0-20221124155335-4616370d1a84 h1:2uT3aivO7NVpUPGcQX7RbHijHMyWix/yCnIrCWc+5co=
|
||||
github.com/jdxcode/netrc v0.0.0-20221124155335-4616370d1a84/go.mod h1:Zi/ZFkEqFHTm7qkjyNJjaWH4LQA9LQhGJyF0lTYGpxw=
|
||||
github.com/jhump/protoreflect v1.15.1 h1:HUMERORf3I3ZdX05WaQ6MIpd/NJ434hTp5YiKgfCL6c=
|
||||
github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
|
||||
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
||||
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
|
||||
github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
|
||||
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
|
||||
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||
github.com/klauspost/compress v1.10.3/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs=
|
||||
github.com/klauspost/compress v1.16.7 h1:2mk3MPGNzKyxErAw8YaohYh69+pa4sIQSC0fPGCFR9I=
|
||||
github.com/klauspost/compress v1.16.7/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE=
|
||||
github.com/klauspost/pgzip v1.2.6 h1:8RXeL5crjEUFnR2/Sn6GJNWtSQ3Dk8pq4CL3jvdDyjU=
|
||||
|
|
@ -251,22 +230,14 @@ github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
|||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y=
|
||||
github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII=
|
||||
github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY=
|
||||
github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
|
||||
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
|
||||
github.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng=
|
||||
github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
|
||||
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
|
||||
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
|
||||
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
|
||||
github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0=
|
||||
github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y=
|
||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
||||
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
||||
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
|
||||
github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=
|
||||
github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
|
||||
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
|
||||
|
|
@ -320,7 +291,6 @@ github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+
|
|||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
|
|
@ -333,10 +303,6 @@ github.com/subosito/gotenv v1.4.2 h1:X1TuBLAMDFbaTAChgCBLu3DU3UPyELpnF2jjJ2cz/S8
|
|||
github.com/subosito/gotenv v1.4.2/go.mod h1:ayKnFf/c6rvx/2iiLrJUk1e6plDbT3edrFNGqEflhK0=
|
||||
github.com/tetratelabs/wazero v1.4.0 h1:9/MirYvmkJ/zSUOygKY/ia3t+e+RqIZXKbylIby1WYk=
|
||||
github.com/tetratelabs/wazero v1.4.0/go.mod h1:0U0G41+ochRKoPKCJlh0jMg1CHkyfK8kDqiirMmKY8A=
|
||||
github.com/ugorji/go v1.1.7 h1:/68gy2h+1mWMrwZFeD1kQialdSzAb432dtpeJ42ovdo=
|
||||
github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw=
|
||||
github.com/ugorji/go/codec v1.1.7 h1:2SvQaVZ1ouYrrKKwoSk2pzd4A9evlKJb9oTL+OaLUSs=
|
||||
github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY=
|
||||
github.com/vbatts/tar-split v0.11.5 h1:3bHCTIheBm1qFTcgh9oPu+nNBtX+XJIupG/vacinCts=
|
||||
github.com/vbatts/tar-split v0.11.5/go.mod h1:yZbwRsSeGjusneWgA781EKej9HF8vme8okylkAeNKLk=
|
||||
github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
|
|
@ -482,7 +448,6 @@ golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7w
|
|||
golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
|
|
@ -691,8 +656,6 @@ gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
|
|||
gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
|
||||
gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10=
|
||||
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
|
|
@ -704,8 +667,6 @@ honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWh
|
|||
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
|
||||
honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
|
||||
honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
|
||||
nhooyr.io/websocket v1.8.7 h1:usjR2uOr/zjjkVMy0lW+PPohFok7PCow5sDjLgX4P4g=
|
||||
nhooyr.io/websocket v1.8.7/go.mod h1:B70DZP8IakI65RVQ51MsWP/8jndNma26DVA/nFSCgW0=
|
||||
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
|
||||
rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
|
||||
rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=
|
||||
|
|
|
|||
|
|
@ -90,7 +90,7 @@ func UserFromContext(ctx context.Context, cfg *config.Config) *AuthenticatedUser
|
|||
log.WithFields(log.Fields{
|
||||
"username": ret.Username,
|
||||
"usergroup": ret.Usergroup,
|
||||
}).Infof("UserFromContext")
|
||||
}).Debugf("UserFromContext")
|
||||
|
||||
return ret
|
||||
}
|
||||
|
|
|
|||
|
|
@ -22,8 +22,9 @@ var (
|
|||
|
||||
func parseActionArguments(rawShellCommand string, values map[string]string, action *config.Action) (string, error) {
|
||||
log.WithFields(log.Fields{
|
||||
"cmd": rawShellCommand,
|
||||
}).Infof("Before Parse Args")
|
||||
"actionTitle": action.Title,
|
||||
"cmd": rawShellCommand,
|
||||
}).Infof("Action parse args - Before")
|
||||
|
||||
r := regexp.MustCompile("{{ *?([a-zA-Z0-9_]+?) *?}}")
|
||||
matches := r.FindAllStringSubmatch(rawShellCommand, -1)
|
||||
|
|
@ -51,8 +52,9 @@ func parseActionArguments(rawShellCommand string, values map[string]string, acti
|
|||
}
|
||||
|
||||
log.WithFields(log.Fields{
|
||||
"cmd": rawShellCommand,
|
||||
}).Infof("After Parse Args")
|
||||
"actionTitle": action.Title,
|
||||
"cmd": rawShellCommand,
|
||||
}).Infof("Action parse args - After")
|
||||
|
||||
return rawShellCommand, nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@ import (
|
|||
pb "github.com/OliveTin/OliveTin/gen/grpc"
|
||||
acl "github.com/OliveTin/OliveTin/internal/acl"
|
||||
config "github.com/OliveTin/OliveTin/internal/config"
|
||||
"github.com/google/uuid"
|
||||
log "github.com/sirupsen/logrus"
|
||||
|
||||
"bytes"
|
||||
|
|
@ -15,56 +14,59 @@ import (
|
|||
"time"
|
||||
)
|
||||
|
||||
// Executor represents a helper class for executing commands. It's main method
|
||||
// is ExecRequest
|
||||
type Executor struct {
|
||||
Logs map[string]*InternalLogEntry
|
||||
|
||||
listeners []listener
|
||||
|
||||
chainOfCommand []executorStepFunc
|
||||
}
|
||||
|
||||
// ExecutionRequest is a request to execute an action. It's passed to an
|
||||
// Executor. They're created from the grpcapi.
|
||||
type ExecutionRequest struct {
|
||||
ActionName string
|
||||
Arguments map[string]string
|
||||
UUID string
|
||||
Tags []string
|
||||
action *config.Action
|
||||
Cfg *config.Config
|
||||
AuthenticatedUser *acl.AuthenticatedUser
|
||||
logEntry *InternalLogEntry
|
||||
finalParsedCommand string
|
||||
uuid string
|
||||
executor *Executor
|
||||
}
|
||||
|
||||
// InternalLogEntry objects are created by an Executor, and represent the final
|
||||
// state of execution (even if the command is not executed). It's designed to be
|
||||
// easily serializable.
|
||||
type InternalLogEntry struct {
|
||||
DatetimeStarted string
|
||||
DatetimeFinished string
|
||||
Stdout string
|
||||
Stderr string
|
||||
StdoutBuffer io.ReadCloser
|
||||
StderrBuffer io.ReadCloser
|
||||
TimedOut bool
|
||||
ExitCode int32
|
||||
Tags []string
|
||||
DatetimeStarted string
|
||||
DatetimeFinished string
|
||||
Stdout string
|
||||
Stderr string
|
||||
StdoutBuffer io.ReadCloser
|
||||
StderrBuffer io.ReadCloser
|
||||
TimedOut bool
|
||||
ExitCode int32
|
||||
Tags []string
|
||||
ExecutionStarted bool
|
||||
ExecutionCompleted bool
|
||||
|
||||
/*
|
||||
The following two properties are obviously on Action normally, but it's useful
|
||||
The following 3 properties are obviously on Action normally, but it's useful
|
||||
that logs are lightweight (so we don't need to have an action associated to
|
||||
logs, etc. Therefore, we duplicate those values here.
|
||||
*/
|
||||
ActionTitle string
|
||||
ActionIcon string
|
||||
|
||||
ExecutionStarted bool
|
||||
ExecutionCompleted bool
|
||||
UUID string
|
||||
}
|
||||
|
||||
type executorStepFunc func(*ExecutionRequest) bool
|
||||
|
||||
// Executor represents a helper class for executing commands. It's main method
|
||||
// is ExecRequest
|
||||
type Executor struct {
|
||||
Logs map[string]*InternalLogEntry
|
||||
|
||||
chainOfCommand []executorStepFunc
|
||||
}
|
||||
|
||||
// DefaultExecutor returns an Executor, with a sensible "chain of command" for
|
||||
// executing actions.
|
||||
func DefaultExecutor() *Executor {
|
||||
|
|
@ -72,23 +74,38 @@ func DefaultExecutor() *Executor {
|
|||
e.Logs = make(map[string]*InternalLogEntry)
|
||||
|
||||
e.chainOfCommand = []executorStepFunc{
|
||||
stepLogRequested,
|
||||
stepFindAction,
|
||||
stepACLCheck,
|
||||
stepParseArgs,
|
||||
stepLogStart,
|
||||
stepExec,
|
||||
stepNotifyListeners,
|
||||
stepLogFinish,
|
||||
}
|
||||
|
||||
return &e
|
||||
}
|
||||
|
||||
type listener interface {
|
||||
OnExecutionStarted(actionName string)
|
||||
OnExecutionFinished(logEntry *InternalLogEntry)
|
||||
}
|
||||
|
||||
func (e *Executor) AddListener(m listener) {
|
||||
e.listeners = append(e.listeners, m)
|
||||
}
|
||||
|
||||
// ExecRequest processes an ExecutionRequest
|
||||
func (e *Executor) ExecRequest(req *ExecutionRequest) *pb.StartActionResponse {
|
||||
req.uuid = uuid.New().String()
|
||||
// req.UUID is now set by the client, so that they can track the request
|
||||
// from start to finish. This means that a malicious client could send
|
||||
// duplicate UUIDs (or just random strings), but this is the only way.
|
||||
req.executor = e
|
||||
req.logEntry = &InternalLogEntry{
|
||||
DatetimeStarted: time.Now().Format("2006-01-02 15:04:05"),
|
||||
ActionTitle: req.ActionName,
|
||||
UUID: req.UUID,
|
||||
Stdout: "",
|
||||
Stderr: "",
|
||||
ExitCode: -1337, // If an Action is not actually executed, this is the default exit code.
|
||||
|
|
@ -96,12 +113,16 @@ func (e *Executor) ExecRequest(req *ExecutionRequest) *pb.StartActionResponse {
|
|||
ExecutionCompleted: false,
|
||||
}
|
||||
|
||||
e.Logs[req.uuid] = req.logEntry
|
||||
e.Logs[req.UUID] = req.logEntry
|
||||
|
||||
for _, listener := range e.listeners {
|
||||
listener.OnExecutionStarted(req.ActionName)
|
||||
}
|
||||
|
||||
go e.execChain(req)
|
||||
|
||||
return &pb.StartActionResponse{
|
||||
ExecutionUuid: req.uuid,
|
||||
ExecutionUuid: req.UUID,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -152,10 +173,18 @@ func stepParseArgs(req *ExecutionRequest) bool {
|
|||
return true
|
||||
}
|
||||
|
||||
func stepLogRequested(req *ExecutionRequest) bool {
|
||||
log.WithFields(log.Fields{
|
||||
"actionTitle": req.ActionName,
|
||||
}).Infof("Action requested")
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func stepLogStart(req *ExecutionRequest) bool {
|
||||
log.WithFields(log.Fields{
|
||||
"title": req.action.Title,
|
||||
"timeout": req.action.Timeout,
|
||||
"actionTitle": req.action.Title,
|
||||
"timeout": req.action.Timeout,
|
||||
}).Infof("Action starting")
|
||||
|
||||
return true
|
||||
|
|
@ -163,16 +192,24 @@ func stepLogStart(req *ExecutionRequest) bool {
|
|||
|
||||
func stepLogFinish(req *ExecutionRequest) bool {
|
||||
log.WithFields(log.Fields{
|
||||
"title": req.action.Title,
|
||||
"stdout": req.logEntry.Stdout,
|
||||
"stderr": req.logEntry.Stderr,
|
||||
"timedOut": req.logEntry.TimedOut,
|
||||
"exit": req.logEntry.ExitCode,
|
||||
"actionTitle": req.action.Title,
|
||||
"stdout": req.logEntry.Stdout,
|
||||
"stderr": req.logEntry.Stderr,
|
||||
"timedOut": req.logEntry.TimedOut,
|
||||
"exit": req.logEntry.ExitCode,
|
||||
}).Infof("Action finished")
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func stepNotifyListeners(req *ExecutionRequest) bool {
|
||||
for _, listener := range req.executor.listeners {
|
||||
listener.OnExecutionFinished(req.logEntry)
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func wrapCommandInShell(ctx context.Context, finalParsedCommand string) *exec.Cmd {
|
||||
if runtime.GOOS == "windows" {
|
||||
return exec.CommandContext(ctx, "cmd", "/C", finalParsedCommand)
|
||||
|
|
@ -217,6 +254,7 @@ func stepExec(req *ExecutionRequest) bool {
|
|||
}
|
||||
|
||||
req.logEntry.Tags = req.Tags
|
||||
req.logEntry.DatetimeFinished = time.Now().Format("2006-01-02 15:04:05")
|
||||
|
||||
return true
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,8 +6,8 @@ import (
|
|||
log "github.com/sirupsen/logrus"
|
||||
"google.golang.org/grpc"
|
||||
|
||||
"io"
|
||||
"net"
|
||||
"sort"
|
||||
|
||||
acl "github.com/OliveTin/OliveTin/internal/acl"
|
||||
config "github.com/OliveTin/OliveTin/internal/config"
|
||||
|
|
@ -20,7 +20,7 @@ var (
|
|||
)
|
||||
|
||||
type oliveTinAPI struct {
|
||||
pb.UnimplementedOliveTinApiServer
|
||||
pb.UnimplementedOliveTinApiServiceServer
|
||||
|
||||
executor *executor.Executor
|
||||
}
|
||||
|
|
@ -36,6 +36,7 @@ func (api *oliveTinAPI) StartAction(ctx ctx.Context, req *pb.StartActionRequest)
|
|||
|
||||
execReq := executor.ExecutionRequest{
|
||||
ActionName: req.ActionName,
|
||||
UUID: req.Uuid,
|
||||
Arguments: args,
|
||||
AuthenticatedUser: acl.UserFromContext(ctx, cfg),
|
||||
Cfg: cfg,
|
||||
|
|
@ -44,6 +45,32 @@ func (api *oliveTinAPI) StartAction(ctx ctx.Context, req *pb.StartActionRequest)
|
|||
return api.executor.ExecRequest(&execReq), nil
|
||||
}
|
||||
|
||||
func (api *oliveTinAPI) ExecutionStatus(ctx ctx.Context, req *pb.ExecutionStatusRequest) (*pb.ExecutionStatusResponse, error) {
|
||||
res := &pb.ExecutionStatusResponse{}
|
||||
|
||||
logEntry, ok := api.executor.Logs[req.ExecutionUuid]
|
||||
|
||||
if !ok {
|
||||
return res, nil
|
||||
}
|
||||
|
||||
res.LogEntry = &pb.LogEntry{
|
||||
ActionTitle: logEntry.ActionTitle,
|
||||
ActionIcon: logEntry.ActionIcon,
|
||||
DatetimeStarted: logEntry.DatetimeStarted,
|
||||
DatetimeFinished: logEntry.DatetimeFinished,
|
||||
Stdout: logEntry.Stdout,
|
||||
Stderr: logEntry.Stderr,
|
||||
TimedOut: logEntry.TimedOut,
|
||||
ExitCode: logEntry.ExitCode,
|
||||
Tags: logEntry.Tags,
|
||||
ExecutionUuid: logEntry.UUID,
|
||||
}
|
||||
|
||||
return res, nil
|
||||
}
|
||||
|
||||
/**
|
||||
func (api *oliveTinAPI) WatchExecution(req *pb.WatchExecutionRequest, srv pb.OliveTinApi_WatchExecutionServer) error {
|
||||
log.Infof("Watch")
|
||||
|
||||
|
|
@ -69,6 +96,7 @@ func (api *oliveTinAPI) WatchExecution(req *pb.WatchExecutionRequest, srv pb.Oli
|
|||
return nil
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
func (api *oliveTinAPI) GetDashboardComponents(ctx ctx.Context, req *pb.GetDashboardComponentsRequest) (*pb.GetDashboardComponentsResponse, error) {
|
||||
user := acl.UserFromContext(ctx, cfg)
|
||||
|
|
@ -104,6 +132,12 @@ func (api *oliveTinAPI) GetLogs(ctx ctx.Context, req *pb.GetLogsRequest) (*pb.Ge
|
|||
})
|
||||
}
|
||||
|
||||
sorter := func(i, j int) bool {
|
||||
return ret.Logs[i].DatetimeStarted < ret.Logs[j].DatetimeStarted
|
||||
}
|
||||
|
||||
sort.Slice(ret.Logs, sorter);
|
||||
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
|
|
@ -159,7 +193,7 @@ func Start(globalConfig *config.Config, ex *executor.Executor) {
|
|||
}
|
||||
|
||||
grpcServer := grpc.NewServer()
|
||||
pb.RegisterOliveTinApiServer(grpcServer, newServer(ex))
|
||||
pb.RegisterOliveTinApiServiceServer(grpcServer, newServer(ex))
|
||||
|
||||
err = grpcServer.Serve(lis)
|
||||
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ func init() {
|
|||
|
||||
lis = bufconn.Listen(bufSize)
|
||||
s := grpc.NewServer()
|
||||
pb.RegisterOliveTinApiServer(s, newServer(ex))
|
||||
pb.RegisterOliveTinApiServiceServer(s, newServer(ex))
|
||||
|
||||
go func() {
|
||||
if err := s.Serve(lis); err != nil {
|
||||
|
|
@ -39,7 +39,7 @@ func bufDialer(context.Context, string) (net.Conn, error) {
|
|||
return lis.Dial()
|
||||
}
|
||||
|
||||
func getNewTestServerAndClient(t *testing.T, injectedConfig *config.Config) (*grpc.ClientConn, pb.OliveTinApiClient) {
|
||||
func getNewTestServerAndClient(t *testing.T, injectedConfig *config.Config) (*grpc.ClientConn, pb.OliveTinApiServiceClient) {
|
||||
cfg = injectedConfig
|
||||
|
||||
ctx := context.Background()
|
||||
|
|
@ -50,7 +50,7 @@ func getNewTestServerAndClient(t *testing.T, injectedConfig *config.Config) (*gr
|
|||
t.Fatalf("Failed to dial bufnet: %v", err)
|
||||
}
|
||||
|
||||
client := pb.NewOliveTinApiClient(conn)
|
||||
client := pb.NewOliveTinApiServiceClient(conn)
|
||||
|
||||
return conn, client
|
||||
}
|
||||
|
|
|
|||
|
|
@ -80,7 +80,7 @@ func startRestAPIServer(globalConfig *config.Config) error {
|
|||
runtime.WithMarshalerOption(runtime.MIMEWildcard, &runtime.HTTPBodyMarshaler{
|
||||
Marshaler: &runtime.JSONPb{
|
||||
MarshalOptions: protojson.MarshalOptions{
|
||||
UseProtoNames: true,
|
||||
UseProtoNames: false, // eg: canExec for js instead of can_exec from protobuf
|
||||
EmitUnpopulated: true,
|
||||
},
|
||||
},
|
||||
|
|
@ -88,7 +88,7 @@ func startRestAPIServer(globalConfig *config.Config) error {
|
|||
)
|
||||
opts := []grpc.DialOption{grpc.WithInsecure()}
|
||||
|
||||
err := gw.RegisterOliveTinApiHandlerFromEndpoint(ctx, mux, cfg.ListenAddressGrpcActions, opts)
|
||||
err := gw.RegisterOliveTinApiServiceHandlerFromEndpoint(ctx, mux, cfg.ListenAddressGrpcActions, opts)
|
||||
|
||||
if err != nil {
|
||||
log.Errorf("Could not register REST API Handler %v", err)
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ away, and several other issues.
|
|||
|
||||
import (
|
||||
config "github.com/OliveTin/OliveTin/internal/config"
|
||||
"github.com/OliveTin/OliveTin/internal/websocket"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"net/http"
|
||||
"net/http/httputil"
|
||||
|
|
@ -39,7 +40,7 @@ func StartSingleHTTPFrontend(cfg *config.Config) {
|
|||
|
||||
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
||||
if strings.Contains(r.Header.Get("Connection"), "Upgrade") {
|
||||
handleWebsocket(w, r)
|
||||
websocket.HandleWebsocket(w, r)
|
||||
} else {
|
||||
log.Debugf("ui req: %q", r.URL)
|
||||
webuiProxy.ServeHTTP(w, r)
|
||||
|
|
|
|||
|
|
@ -1,39 +0,0 @@
|
|||
package httpservers
|
||||
|
||||
import (
|
||||
"context"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"net/http"
|
||||
"nhooyr.io/websocket"
|
||||
"nhooyr.io/websocket/wsjson"
|
||||
"time"
|
||||
)
|
||||
|
||||
func handleWebsocket(w http.ResponseWriter, r *http.Request) bool {
|
||||
c, err := websocket.Accept(w, r, nil)
|
||||
|
||||
if err != nil {
|
||||
log.Warnf("Websocket issue: %v", err)
|
||||
return false
|
||||
}
|
||||
|
||||
defer c.Close(websocket.StatusInternalError, "Goodbye")
|
||||
|
||||
ctx, cancel := context.WithTimeout(r.Context(), time.Second*10)
|
||||
|
||||
defer cancel()
|
||||
|
||||
var v interface{}
|
||||
|
||||
err = wsjson.Read(ctx, c, v)
|
||||
|
||||
log.Printf("recv: %v", v)
|
||||
|
||||
if err != nil {
|
||||
log.Warnf("Websocket issue: %v", err)
|
||||
return false
|
||||
}
|
||||
|
||||
c.Close(websocket.StatusNormalClosure, "")
|
||||
return true
|
||||
}
|
||||
|
|
@ -0,0 +1,119 @@
|
|||
package websocket
|
||||
|
||||
import (
|
||||
pb "github.com/OliveTin/OliveTin/gen/grpc"
|
||||
"github.com/OliveTin/OliveTin/internal/executor"
|
||||
ws "github.com/gorilla/websocket"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"google.golang.org/protobuf/encoding/protojson"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
var upgrader = ws.Upgrader{}
|
||||
|
||||
type WebsocketClient struct {
|
||||
conn *ws.Conn
|
||||
}
|
||||
|
||||
var clients []*WebsocketClient
|
||||
|
||||
var marshalOptions = protojson.MarshalOptions{
|
||||
UseProtoNames: false, // eg: canExec for js instead of can_exec from protobuf
|
||||
EmitUnpopulated: true,
|
||||
}
|
||||
|
||||
var ExecutionListener WebsocketExecutionListener
|
||||
|
||||
type WebsocketExecutionListener struct{}
|
||||
|
||||
func (WebsocketExecutionListener) OnExecutionStarted(title string) {
|
||||
/*
|
||||
broadcast(ExecutionStarted{
|
||||
Type: "ExecutionStarted",
|
||||
Action: title,
|
||||
});
|
||||
*/
|
||||
}
|
||||
|
||||
func (WebsocketExecutionListener) OnExecutionFinished(logEntry *executor.InternalLogEntry) {
|
||||
le := &pb.LogEntry{
|
||||
ActionTitle: logEntry.ActionTitle,
|
||||
ActionIcon: logEntry.ActionIcon,
|
||||
DatetimeStarted: logEntry.DatetimeStarted,
|
||||
DatetimeFinished: logEntry.DatetimeFinished,
|
||||
Stdout: logEntry.Stdout,
|
||||
Stderr: logEntry.Stderr,
|
||||
TimedOut: logEntry.TimedOut,
|
||||
ExitCode: logEntry.ExitCode,
|
||||
Tags: logEntry.Tags,
|
||||
Uuid: logEntry.UUID,
|
||||
}
|
||||
|
||||
broadcast("ExecutionFinished", le)
|
||||
}
|
||||
|
||||
func broadcast(messageType string, pbmsg *pb.LogEntry) {
|
||||
payload, err := marshalOptions.Marshal(pbmsg)
|
||||
|
||||
// <EVIL>
|
||||
// So, the websocket wants to encode messages using the same protomarshaller
|
||||
// as the REST API - this gives consistency instead of using encoding/json
|
||||
// and allows us to set specific marshalOptions.
|
||||
//
|
||||
// However, the protomarshaller will marshal the type, but the JavaScript at
|
||||
// the other end has no idea what type this object is - as we're just sending
|
||||
// it as JSON over the websocket.
|
||||
//
|
||||
// Therefore, we wrap the nicely marsheled bytes in a hacky JSON string
|
||||
// literal and encode that string just with a byte array cast.
|
||||
hackyMessageEnvelope := "{\"type\": \"" + messageType + "\", \"payload\": "
|
||||
|
||||
hackyMessage := []byte{}
|
||||
hackyMessage = append(hackyMessage, []byte(hackyMessageEnvelope)...)
|
||||
hackyMessage = append(hackyMessage, payload...)
|
||||
hackyMessage = append(hackyMessage, []byte("}")...)
|
||||
// </EVIL>
|
||||
|
||||
if err != nil {
|
||||
log.Errorf("websocket marshal error: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
for _, client := range clients {
|
||||
client.conn.WriteMessage(1, hackyMessage)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *WebsocketClient) messageLoop() {
|
||||
for {
|
||||
mt, message, err := c.conn.ReadMessage()
|
||||
|
||||
if err != nil {
|
||||
log.Printf("err: %v", err)
|
||||
break
|
||||
}
|
||||
|
||||
log.Infof("websocket recv: %s %d", message, mt)
|
||||
}
|
||||
}
|
||||
|
||||
func HandleWebsocket(w http.ResponseWriter, r *http.Request) bool {
|
||||
c, err := upgrader.Upgrade(w, r, nil)
|
||||
|
||||
if err != nil {
|
||||
log.Warnf("Websocket issue: %v", err)
|
||||
return false
|
||||
}
|
||||
|
||||
// defer c.Close()
|
||||
|
||||
wsclient := &WebsocketClient{
|
||||
conn: c,
|
||||
}
|
||||
|
||||
clients = append(clients, wsclient)
|
||||
|
||||
go wsclient.messageLoop()
|
||||
|
||||
return true
|
||||
}
|
||||
|
|
@ -62,38 +62,83 @@
|
|||
</main>
|
||||
|
||||
<footer title = "footer">
|
||||
<p><img title = "application icon" src = "OliveTinLogo.png" height = "1em" class = "logo" /> OliveTin</p>
|
||||
<p><img title = "application icon" src = "OliveTinLogo.png" alt = "OliveTin logo" height = "1em" class = "logo" /> OliveTin</p>
|
||||
<p>
|
||||
<a href = "https://docs.olivetin.app" target = "_new">Documentation</a> |
|
||||
<a href = "https://github.com/OliveTin/OliveTin/issues/new/choose" target = "_new">Raise an issue on GitHub</a> |
|
||||
<span id = "currentVersion">Version: ?</p>
|
||||
<span>Version: <span id = "currentVersion">?</span></span> |
|
||||
<span>Server connection: <span id = "serverConnection">?</span></span>
|
||||
</p>
|
||||
<p>
|
||||
<a id = "available-version" href = "http://olivetin.app" target = "_blank" hidden>?</a>
|
||||
</p>
|
||||
</footer>
|
||||
|
||||
<dialog id = "executionResults">
|
||||
<div class = "action-header">
|
||||
<span class = "icon" role = "icon"></span>
|
||||
|
||||
<h2>Log:
|
||||
<span class = "title">?</span>
|
||||
</h2>
|
||||
</div>
|
||||
<p>
|
||||
<strong>Started: </strong><span class = "datetimeStarted">unknown</span>
|
||||
<strong>Finished: </strong><span class = "datetimeFinished">unknown</span>
|
||||
</p>
|
||||
<p>
|
||||
<strong>Exit Code: </strong><span class = "exitCode">unknown</span>
|
||||
</p>
|
||||
|
||||
<details>
|
||||
<summary>stdout</summary>
|
||||
<pre class = "stdout">
|
||||
?
|
||||
</pre>
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>stderr</summary>
|
||||
<pre class = "stderr">
|
||||
?
|
||||
</pre>
|
||||
</details>
|
||||
|
||||
<form method = "dialog">
|
||||
<button name = "cancel">Close</button>
|
||||
</form>
|
||||
</dialog>
|
||||
|
||||
<template id = "tplArgumentForm">
|
||||
<form class = "action-arguments">
|
||||
<div class = "wrapper">
|
||||
<div>
|
||||
<span class = "icon" role = "icon"></span>
|
||||
<h2>Argument form</h2>
|
||||
</div>
|
||||
<dialog>
|
||||
<form class = "action-arguments">
|
||||
<div class = "wrapper">
|
||||
<div class = "action-header">
|
||||
<span class = "icon" role = "icon"></span>
|
||||
<h2>Argument form</h2>
|
||||
</div>
|
||||
|
||||
<div class = "arguments"></div>
|
||||
<div class = "arguments"></div>
|
||||
|
||||
<div class = "buttons">
|
||||
<input name = "start" type = "submit" value = "Start">
|
||||
<button name = "cancel">Cancel</button>
|
||||
<div class = "buttons">
|
||||
<input name = "start" type = "submit" value = "Start">
|
||||
<button name = "cancel">Cancel</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<form>
|
||||
</form>
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<template id = "tplActionButton">
|
||||
<button>
|
||||
<span role = "icon" title = "button icon" class = "icon">💩</span>
|
||||
<p role = "title" class = "title">Untitled Button</p>
|
||||
</button>
|
||||
<div>
|
||||
<button>
|
||||
<span role = "icon" title = "button icon" class = "icon">💩</span>
|
||||
<span role = "title" class = "title">Untitled Button</span>
|
||||
</button>
|
||||
|
||||
<div class = "executions">
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template id = "tplLogRow">
|
||||
|
|
@ -129,7 +174,10 @@
|
|||
to at least display a helpful error message if we can't use OliveTin.
|
||||
*/
|
||||
function showBigError (type, friendlyType, message) {
|
||||
clearInterval(window.buttonInterval)
|
||||
for (const oldError of document.querySelectorAll('div.error').values()) {
|
||||
window.old = oldError;
|
||||
oldError.remove();
|
||||
}
|
||||
|
||||
console.error('Error ' + type + ': ', message)
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { marshalLogsJsonToHtml } from './marshaller.js'
|
||||
import './ArgumentForm.js'
|
||||
import './ExecutionButton.js'
|
||||
|
||||
class ActionButton extends window.HTMLElement {
|
||||
constructFromJson (json) {
|
||||
|
|
@ -25,6 +25,7 @@ class ActionButton extends window.HTMLElement {
|
|||
})
|
||||
|
||||
document.body.appendChild(frm)
|
||||
frm.querySelector('dialog').showModal()
|
||||
} else {
|
||||
this.startAction()
|
||||
}
|
||||
|
|
@ -32,7 +33,8 @@ class ActionButton extends window.HTMLElement {
|
|||
|
||||
this.updateFromJson(json)
|
||||
|
||||
this.updateDom()
|
||||
this.domTitle.innerText = this.btn.title
|
||||
this.domIcon.innerHTML = this.unicodeIcon
|
||||
|
||||
this.setAttribute('id', 'actionButton_' + json.id)
|
||||
}
|
||||
|
|
@ -52,20 +54,27 @@ class ActionButton extends window.HTMLElement {
|
|||
}
|
||||
|
||||
startAction (actionArgs) {
|
||||
this.btn.disabled = true
|
||||
this.isWaiting = true
|
||||
this.updateDom()
|
||||
// this.btn.disabled = true
|
||||
// this.isWaiting = true
|
||||
// this.updateDom()
|
||||
this.btn.classList = [] // Removes old animation classes
|
||||
|
||||
if (actionArgs === undefined) {
|
||||
actionArgs = []
|
||||
}
|
||||
|
||||
// UUIDs are create client side, so that we can setup a "execution-button"
|
||||
// to track the execution before we send the request to the server.
|
||||
const startActionArgs = {
|
||||
actionName: this.btn.title,
|
||||
arguments: actionArgs
|
||||
arguments: actionArgs,
|
||||
uuid: window.crypto.randomUUID()
|
||||
}
|
||||
|
||||
const btnExecution = document.createElement('execution-button')
|
||||
btnExecution.constructFromJson(startActionArgs.uuid)
|
||||
this.querySelector('div.executions').appendChild(btnExecution)
|
||||
|
||||
window.fetch(this.actionCallUrl, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
|
|
@ -80,45 +89,12 @@ class ActionButton extends window.HTMLElement {
|
|||
}
|
||||
}
|
||||
).then((json) => {
|
||||
marshalLogsJsonToHtml({ logs: [json.logEntry] })
|
||||
|
||||
if (json.logEntry.timedOut) {
|
||||
this.onActionResult('action-timeout', 'Timed out')
|
||||
} else if (json.logEntry.exitCode === -1337) {
|
||||
this.onActionError('Error')
|
||||
} else if (json.logEntry.exitCode !== 0) {
|
||||
this.onActionResult('action-nonzero-exit', 'Exit code ' + json.logEntry.exitCode)
|
||||
} else {
|
||||
this.onActionResult('action-success', 'Success!')
|
||||
}
|
||||
// The button used to wait for the action to finish, but now it is fire & forget
|
||||
}).catch(err => {
|
||||
this.onActionError(err)
|
||||
})
|
||||
}
|
||||
|
||||
onActionResult (cssClass, temporaryStatusMessage) {
|
||||
this.btn.disabled = false
|
||||
this.temporaryStatusMessage = '[ ' + temporaryStatusMessage + ' ]'
|
||||
this.updateDom()
|
||||
this.btn.classList.add(cssClass)
|
||||
|
||||
setTimeout(() => {
|
||||
this.btn.classList.remove(cssClass)
|
||||
}, 1000)
|
||||
}
|
||||
|
||||
onActionError (err) {
|
||||
console.error('callback error', err)
|
||||
this.btn.disabled = false
|
||||
this.isWaiting = false
|
||||
this.updateDom()
|
||||
this.btn.classList.add('action-failed')
|
||||
|
||||
setTimeout(() => {
|
||||
this.btn.classList.remove('action-failed')
|
||||
}, 1000)
|
||||
}
|
||||
|
||||
constructDomFromTemplate () {
|
||||
const tpl = document.getElementById('tplActionButton')
|
||||
const content = tpl.content.cloneNode(true)
|
||||
|
|
@ -134,27 +110,6 @@ class ActionButton extends window.HTMLElement {
|
|||
this.domTitle = this.btn.querySelector('.title')
|
||||
this.domIcon = this.btn.querySelector('.icon')
|
||||
}
|
||||
|
||||
updateDom () {
|
||||
if (this.temporaryStatusMessage != null) {
|
||||
this.domTitle.innerText = this.temporaryStatusMessage
|
||||
this.domTitle.classList.add('temporary-status-message')
|
||||
this.isWaiting = false
|
||||
this.disabled = false
|
||||
|
||||
setTimeout(() => {
|
||||
this.temporaryStatusMessage = null
|
||||
this.domTitle.classList.remove('temporary-status-message')
|
||||
this.updateDom()
|
||||
}, 2000)
|
||||
} else if (this.isWaiting) {
|
||||
this.domTitle.innerText = 'Waiting...'
|
||||
} else {
|
||||
this.domTitle.innerText = this.btn.title
|
||||
}
|
||||
|
||||
this.domIcon.innerHTML = this.unicodeIcon
|
||||
}
|
||||
}
|
||||
|
||||
window.customElements.define('action-button', ActionButton)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,106 @@
|
|||
import { ExecutionDialog } from './ExecutionDialog.js'
|
||||
|
||||
class ExecutionButton extends window.HTMLElement {
|
||||
constructFromJson (json) {
|
||||
this.executionUuid = json
|
||||
|
||||
this.appendChild(document.createElement('button'))
|
||||
this.isWaiting = true
|
||||
|
||||
this.setAttribute('id', 'execution-' + json)
|
||||
|
||||
this.btn = this.querySelector('button')
|
||||
this.btn.innerText = 'Executing...'
|
||||
this.btn.onclick = () => {
|
||||
this.show()
|
||||
}
|
||||
}
|
||||
|
||||
show () {
|
||||
if (window.executionDialog === undefined) {
|
||||
window.executionDialog = new ExecutionDialog()
|
||||
}
|
||||
|
||||
const executionStatusArgs = {
|
||||
executionUuid: this.executionUuid
|
||||
}
|
||||
|
||||
window.executionDialog.constructFromJson(this.executionUuid)
|
||||
window.executionDialog.show()
|
||||
|
||||
window.fetch(window.restBaseUrl + 'ExecutionStatus', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(executionStatusArgs)
|
||||
}).then((res) => {
|
||||
if (res.ok) {
|
||||
return res.json()
|
||||
} else {
|
||||
throw new Error(res.statusText)
|
||||
}
|
||||
}
|
||||
).then((json) => {
|
||||
window.executionDialog.renderResult(json)
|
||||
}).catch(err => {
|
||||
window.executionDialog.renderError(err)
|
||||
})
|
||||
}
|
||||
|
||||
onFinished (LogEntry) {
|
||||
if (LogEntry.timedOut) {
|
||||
this.onActionResult('action-timeout', 'Timed out')
|
||||
} else if (LogEntry.exitCode === -1337) {
|
||||
this.onActionError('Error')
|
||||
} else if (LogEntry.exitCode !== 0) {
|
||||
this.onActionResult('action-nonzero-exit', 'Exit code ' + LogEntry.exitCode)
|
||||
} else {
|
||||
this.onActionResult('action-success', 'Success!')
|
||||
}
|
||||
}
|
||||
|
||||
onActionResult (cssClass, temporaryStatusMessage) {
|
||||
this.btn.disabled = false
|
||||
this.temporaryStatusMessage = '[ ' + temporaryStatusMessage + ' ]'
|
||||
this.updateDom()
|
||||
this.btn.classList.add(cssClass)
|
||||
|
||||
setTimeout(() => {
|
||||
this.btn.classList.remove(cssClass)
|
||||
}, 1000)
|
||||
}
|
||||
|
||||
onActionError (err) {
|
||||
console.error('callback error', err)
|
||||
this.btn.disabled = false
|
||||
this.isWaiting = false
|
||||
this.updateDom()
|
||||
this.btn.classList.add('action-failed')
|
||||
|
||||
setTimeout(() => {
|
||||
this.btn.classList.remove('action-failed')
|
||||
}, 1000)
|
||||
}
|
||||
|
||||
updateDom () {
|
||||
if (this.temporaryStatusMessage != null) {
|
||||
this.btn.innerText = this.temporaryStatusMessage
|
||||
this.btn.classList.add('temporary-status-message')
|
||||
this.isWaiting = false
|
||||
this.disabled = 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 = 'Finished'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
window.customElements.define('execution-button', ExecutionButton)
|
||||
|
|
@ -0,0 +1,58 @@
|
|||
// This ExecutionDialog is NOT a custom HTML element, but rather just picks up
|
||||
// the <dialog /> element out of index.html and just re-uses that - as only
|
||||
// one dialog can be shown at a time.
|
||||
export class ExecutionDialog {
|
||||
constructFromJson (json) {
|
||||
this.executionUuid = json
|
||||
|
||||
this.dlg = document.querySelector('dialog#executionResults')
|
||||
|
||||
this.domIcon = this.dlg.querySelector('.icon')
|
||||
this.domTitle = this.dlg.querySelector('.title')
|
||||
this.domStdout = this.dlg.querySelector('.stdout')
|
||||
this.domStderr = this.dlg.querySelector('.stderr')
|
||||
this.domDatetimeStarted = this.dlg.querySelector('.datetimeStarted')
|
||||
this.domDatetimeFinished = this.dlg.querySelector('.datetimeFinished')
|
||||
this.domExitCode = this.dlg.querySelector('.exitCode')
|
||||
}
|
||||
|
||||
show () {
|
||||
this.dlg.showModal()
|
||||
}
|
||||
|
||||
renderResult (res) {
|
||||
this.executionUuid = res.logEntry.executionUuid
|
||||
|
||||
if (res.logEntry.datetimeFinished === '') {
|
||||
this.domExitCode.innerText = 'Still running...'
|
||||
this.domDatetimeFinished.innerText = 'Still running...'
|
||||
} else {
|
||||
if (res.logEntry.timedOut) {
|
||||
this.domExitCode.innerText = 'Timed out'
|
||||
} else {
|
||||
this.domExitCode.innerText = res.logEntry.exitCode
|
||||
}
|
||||
|
||||
this.domDatetimeFinished.innerText = res.logEntry.datetimeFinished
|
||||
}
|
||||
|
||||
this.domIcon.innerHTML = res.logEntry.actionIcon
|
||||
this.domTitle.innerText = res.logEntry.actionTitle
|
||||
|
||||
this.domStdout.innerText = res.logEntry.stdout
|
||||
|
||||
if (res.logEntry.stderr === '') {
|
||||
this.domStderr.parentElement.hidden = true
|
||||
this.domStderr.innerText = res.logEntry.stderr
|
||||
} else {
|
||||
this.domStderr.parentElement.hidden = false
|
||||
this.domStderr.innerText = res.logEntry.stderr
|
||||
}
|
||||
|
||||
this.domDatetimeStarted.innerText = res.logEntry.datetimeStarted
|
||||
}
|
||||
|
||||
renderError (err) {
|
||||
this.dlg.querySelector('pre').innerText = JSON.stringify(err)
|
||||
}
|
||||
}
|
||||
|
|
@ -50,7 +50,7 @@ export function marshalLogsJsonToHtml (json) {
|
|||
logTableExitCode += ' (timed out)'
|
||||
}
|
||||
|
||||
row.querySelector('.timestamp').innerText = logEntry.datetime
|
||||
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
|
||||
|
|
|
|||
|
|
@ -1,4 +1,14 @@
|
|||
export function setupWebsocket () {
|
||||
import { marshalLogsJsonToHtml } from './marshaller.js'
|
||||
|
||||
window.ws = null
|
||||
|
||||
export function checkWebsocketConnection () {
|
||||
if (window.ws === null || window.ws.readyState === 3) {
|
||||
reconnectWebsocket()
|
||||
}
|
||||
}
|
||||
|
||||
function reconnectWebsocket () {
|
||||
window.websocketAvailable = false
|
||||
|
||||
let proto = 'ws:'
|
||||
|
|
@ -18,20 +28,44 @@ export function setupWebsocket () {
|
|||
|
||||
function websocketOnOpen (evt) {
|
||||
window.websocketAvailable = true
|
||||
console.log('open')
|
||||
|
||||
const foo = '{}'
|
||||
window.ws.send('monitor')
|
||||
|
||||
window.ws.send(foo)
|
||||
window.refreshLoop()
|
||||
}
|
||||
|
||||
function websocketOnMessage (msg) {
|
||||
console.log(msg)
|
||||
// FIXME check msg status is OK
|
||||
const j = JSON.parse(msg.data)
|
||||
|
||||
switch (j.type) {
|
||||
case 'ExecutionFinished':
|
||||
updatePageAfterFinished(j.payload)
|
||||
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
|
||||
console.log(err)
|
||||
window.refreshLoop()
|
||||
console.error(err)
|
||||
}
|
||||
|
||||
function websocketOnClose () {
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
'use strict'
|
||||
|
||||
import { marshalActionButtonsJsonToHtml, marshalLogsJsonToHtml } from './js/marshaller.js'
|
||||
import { setupWebsocket } from './js/websocket.js'
|
||||
import { checkWebsocketConnection } from './js/websocket.js'
|
||||
|
||||
function showSection (name) {
|
||||
for (const otherName of ['Actions', 'Logs']) {
|
||||
|
|
@ -20,15 +20,42 @@ function setupSections () {
|
|||
showSection('Actions')
|
||||
}
|
||||
|
||||
function refreshLoop () {
|
||||
if (window.websocketAvailable) {
|
||||
document.querySelector('#serverConnection').classList.remove('error')
|
||||
document.querySelector('#serverConnection').innerText = 'websocket'
|
||||
|
||||
// Websocket updates are streamed live, not updated on a loop.
|
||||
} else if (window.restAvailable) {
|
||||
// Fallback to rest, but try to reconnect the websocket anyway.
|
||||
|
||||
fetchGetDashboardComponents()
|
||||
checkWebsocketConnection()
|
||||
|
||||
document.querySelector('#serverConnection').classList.remove('error')
|
||||
document.querySelector('#serverConnection').innerText = 'rest'
|
||||
|
||||
fetchGetLogs()
|
||||
} else {
|
||||
document.querySelector('#serverConnection').innerText = 'disconnected, trying to reconnect...'
|
||||
document.querySelector('#serverConnection').classList.add('error')
|
||||
|
||||
// Still try to fetch the dashboard, if successfull window.restAvailable = true
|
||||
fetchGetDashboardComponents()
|
||||
}
|
||||
}
|
||||
|
||||
function fetchGetDashboardComponents () {
|
||||
window.fetch(window.restBaseUrl + 'GetDashboardComponents', {
|
||||
cors: 'cors'
|
||||
}).then(res => {
|
||||
return res.json()
|
||||
}).then(res => {
|
||||
window.restAvailable = true
|
||||
marshalActionButtonsJsonToHtml(res)
|
||||
}).catch(err => {
|
||||
window.showBigError('fetch-buttons', 'getting buttons', err, 'blat')
|
||||
}).catch(() => { // err is 1st arg
|
||||
window.restAvailable = false
|
||||
// window.showBigError('fetch-buttons', 'getting buttons', err, 'blat')
|
||||
})
|
||||
}
|
||||
|
||||
|
|
@ -56,7 +83,7 @@ function processWebuiSettingsJson (settings) {
|
|||
document.head.appendChild(themeCss)
|
||||
}
|
||||
|
||||
document.querySelector('#currentVersion').innerText = 'Version: ' + settings.CurrentVersion
|
||||
document.querySelector('#currentVersion').innerText = settings.CurrentVersion
|
||||
|
||||
if (settings.ShowNewVersions && settings.AvailableVersion !== 'none') {
|
||||
document.querySelector('#available-version').innerText = 'New Version Available: ' + settings.AvailableVersion
|
||||
|
|
@ -76,17 +103,16 @@ function processWebuiSettingsJson (settings) {
|
|||
function main () {
|
||||
setupSections()
|
||||
|
||||
setupWebsocket()
|
||||
|
||||
window.fetch('webUiSettings.json').then(res => {
|
||||
return res.json()
|
||||
}).then(res => {
|
||||
processWebuiSettingsJson(res)
|
||||
|
||||
fetchGetDashboardComponents()
|
||||
fetchGetLogs()
|
||||
window.restAvailable = true
|
||||
window.refreshLoop = refreshLoop
|
||||
window.refreshLoop()
|
||||
|
||||
window.buttonInterval = setInterval(fetchGetDashboardComponents, 3000)
|
||||
setInterval(refreshLoop, 3000)
|
||||
}).catch(err => {
|
||||
window.showBigError('fetch-webui-settings', 'getting webui settings', err)
|
||||
})
|
||||
|
|
|
|||
|
|
@ -7,6 +7,13 @@ body {
|
|||
margin: 0;
|
||||
}
|
||||
|
||||
dialog {
|
||||
min-width: 600px;
|
||||
text-align: left;
|
||||
padding: 1em;
|
||||
box-shadow: 0 0 6px 0 #444;
|
||||
}
|
||||
|
||||
fieldset {
|
||||
padding: 0;
|
||||
}
|
||||
|
|
@ -120,7 +127,7 @@ span[role="icon"] {
|
|||
vertical-align: middle;
|
||||
}
|
||||
|
||||
form span[role="icon"],
|
||||
.action-header span[role="icon"],
|
||||
tr.log-row span[role="icon"] {
|
||||
display: inline-block;
|
||||
padding-right: 0.2em;
|
||||
|
|
@ -128,6 +135,9 @@ tr.log-row span[role="icon"] {
|
|||
|
||||
.error {
|
||||
background-color: salmon;
|
||||
}
|
||||
|
||||
div.error {
|
||||
padding: 1em;
|
||||
}
|
||||
|
||||
|
|
@ -146,6 +156,7 @@ div.entity {
|
|||
}
|
||||
|
||||
h2 {
|
||||
margin-top: 0;
|
||||
font-size: 1em;
|
||||
display: inline-block;
|
||||
}
|
||||
|
|
@ -185,6 +196,11 @@ input[type="submit"]:disabled {
|
|||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
div.executions button {
|
||||
margin-top: .2em;
|
||||
margin-bottom: .2em;
|
||||
}
|
||||
|
||||
fieldset#section-switcher {
|
||||
border: 0;
|
||||
text-align: right;
|
||||
|
|
@ -282,21 +298,9 @@ details[open] {
|
|||
display: block;
|
||||
}
|
||||
|
||||
form.action-arguments {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
padding: 1em;
|
||||
box-shadow: 0 0 6px 0 #aaa;
|
||||
background-color: #dee3e7;
|
||||
}
|
||||
|
||||
form div.wrapper {
|
||||
border-radius: 1em;
|
||||
box-shadow: 0 0 10px 0 #444;
|
||||
background-color: white;
|
||||
border: 1px solid #999;
|
||||
text-align: left;
|
||||
padding: 1em;
|
||||
}
|
||||
|
||||
label {
|
||||
|
|
|
|||
Loading…
Reference in New Issue