easel-dashboard 0.4.2

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: e0a7fbceaa1d9f616dd3a373aa892d371b8d4a77df06d66cb91734a4e581055f
4
+ data.tar.gz: 38cbd403c831aff841ba9e1a2bb6a10581f938f6491775123de3a6bcbf8b47d3
5
+ SHA512:
6
+ metadata.gz: 94f4e6a90899dc3c06817fc6eb4ce10484ebc58d3f3b9d79009167aca58701878c05ba3d1a9715d305196cb5ea76d9eeacfe9b7d05c61c3bf4e3b3c929474d83
7
+ data.tar.gz: 9f6af45a6fc1c55092e680fdd5a5a1b1e93801d8db6ee895ea77e7fc387098a619eea603f8cee7ce3d32b7ddd5ae4dda8f20a647ebac28734fa87ce5f4657a5e
data/bin/easel ADDED
@@ -0,0 +1,4 @@
1
+ #!/usr/bin/env ruby
2
+ require 'easel-dashboard'
3
+
4
+ launch
@@ -0,0 +1,56 @@
1
+ #!/snap/bin/ruby
2
+ #
3
+ # Author: Eric Power
4
+
5
+ # Imports
6
+ require 'erb'
7
+
8
+ # Key Variables
9
+ @code_names = {
10
+ 200 => "OK",
11
+ 404 => "Not Found",
12
+ 405 => "Forbidden",
13
+ 418 => "I'm a teapot",
14
+ 500 => "Internal Server Error"
15
+ }
16
+
17
+ # build_app
18
+ #
19
+ #
20
+ def build_app
21
+ app_erb = File.new("#{File.dirname(__FILE__)}/../html/app.html.erb").read
22
+ page = ERB.new(app_erb).result()
23
+
24
+ "HTTP/1.1 200 OK\r\n" +
25
+ "Content-Type: text/html; charset=UTF-8\r\n" +
26
+ "Content-Length: #{page.bytesize}\r\n" +
27
+ "Connection: close\r\n" +
28
+ "\r\n" +
29
+ page
30
+ end
31
+
32
+
33
+ def build_error code
34
+ error_erb = File.new("#{File.dirname(__FILE__)}/../html/error.html.erb").read
35
+ page = ERB.new(error_erb).result(binding)
36
+
37
+ "HTTP/1.1 #{code} #{@code_names[code]}\r\n" +
38
+ "Content-Type: text/html; charset=UTF-8\r\n" +
39
+ "Content-Length: #{page.bytesize}\r\n" +
40
+ "Connection: close\r\n" +
41
+ "\r\n" +
42
+ page
43
+ end
44
+
45
+
46
+ def build_css
47
+ error_erb = File.new("#{File.dirname(__FILE__)}/../html/app.css.erb").read
48
+ css = ERB.new(error_erb).result(binding)
49
+
50
+ "HTTP/1.1 200 OK\r\n" +
51
+ "Content-Type: text/css; charset=UTF-8\r\n" +
52
+ "Content-Length: #{css.bytesize}\r\n" +
53
+ "Connection: close\r\n" +
54
+ "\r\n" +
55
+ css
56
+ end
@@ -0,0 +1,43 @@
1
+ #!/snap/bin/ruby
2
+ #
3
+ # Author: Eric Power
4
+ #
5
+ # Description:
6
+ # This file contains the global variable $config. These values are only
7
+ # defaults. If the YAML file passed to launch.rb contains the same keys,
8
+ # then these values are overwritten.
9
+
10
+
11
+ # Global Variables
12
+ $config = {
13
+ logging: 2, # 0=Fatal, 1=Error, 2=Warning, 3=Info
14
+ port: 4200, # Default port
15
+ hostname: 'localhost', # Default hostname
16
+ log_file: STDOUT, # Default logging to STDOUT
17
+ title: 'Easel - Your Custom Dashboard',
18
+ colours: {
19
+ surface: '#222222',
20
+ background: '#000000',
21
+ primary: '#7DF9FF',
22
+ secondary: '#00FF00',
23
+ on_surface: '#ffffff',
24
+ on_background: '#ffffff',
25
+ on_primary: '#000000',
26
+ on_secondary: '#000000',
27
+ shadow: '#000000',
28
+ stdout_colour: '#ffffff',
29
+ stderr_colour: '#00FF00'
30
+ },
31
+ commands: [
32
+ {
33
+ name: 'Test 1',
34
+ cmd: 'echo "this is the output of Test 1"',
35
+ desc: 'Simple output test #1'
36
+ },
37
+ {
38
+ name: 'Test 2',
39
+ cmd: 'echo "this is the output of Test 2"',
40
+ desc: 'Simple output test #2'
41
+ }
42
+ ]
43
+ }
@@ -0,0 +1,35 @@
1
+ #!/snap/bin/ruby
2
+ #
3
+ # Author: Eric Power
4
+ #
5
+ # Description:
6
+ # Provides the functions to provide easy logging for the Easel Dashboard.
7
+
8
+ # Imports
9
+ require 'time'
10
+
11
+
12
+ def log_fatal *msg
13
+ unless $config[:logging] == 0
14
+ $config[:log_file].puts "[#{Time.new.strftime("%Y-%m-%d-%H:%M:%S")}] FATAL: " + msg.join(" ")
15
+ end
16
+ exit 1
17
+ end
18
+
19
+ def log_error *msg
20
+ unless $config[:logging] < 1
21
+ $config[:log_file].puts "[#{Time.new.strftime("%Y-%m-%d-%H:%M:%S")}] ERROR: " + msg.join(" ")
22
+ end
23
+ end
24
+
25
+ def log_warning *msg
26
+ unless $config[:logging] < 2
27
+ $config[:log_file].puts "[#{Time.new.strftime("%Y-%m-%d-%H:%M:%S")}] WARNING: " + msg.join(" ")
28
+ end
29
+ end
30
+
31
+ def log_info *msg
32
+ unless $config[:logging] < 3
33
+ $config[:log_file].puts "[#{Time.new.strftime("%Y-%m-%d-%H:%M:%S")}] INFO: " + msg.join(" ")
34
+ end
35
+ end
@@ -0,0 +1,114 @@
1
+ #!/snap/bin/ruby
2
+ #
3
+ # Author: Eric Power
4
+ #
5
+ # Description:
6
+ # Provides the server that serves Easel Dashboard.
7
+
8
+ # Imports
9
+ require 'socket'
10
+ require_relative './build_pages.rb'
11
+ require_relative './websocket'
12
+
13
+
14
+ def launch_server
15
+
16
+ # Lauch the TCPServer
17
+ begin
18
+ server = TCPServer.new($config[:hostname], $config[:port])
19
+ rescue Exception => e
20
+ log_fatal "Server could not start. Error message: #{e}"
21
+ end
22
+
23
+ Thread.abort_on_exception = true
24
+
25
+ # Main Loop
26
+ begin
27
+ loop {
28
+ Thread.start(server.accept) do |client|
29
+ handle_request client
30
+ end
31
+ }
32
+
33
+ # Handle shutting down.
34
+ rescue Interrupt
35
+ log_info "Interrupt received, server shutting down..."
36
+ end
37
+ end
38
+
39
+
40
+ def handle_request socket
41
+
42
+ log_info "Receieved request: #{socket}"
43
+ request = read_HTTP_message socket
44
+
45
+ # TODO: check what the minimum allow handling is. I think there's one more method I need to handle.
46
+ case request[:method]
47
+ when "GET"
48
+ # TODO: respond with app, css file, favicon, or 404 error.
49
+ if request[:fields][:Upgrade] == "websocket\r\n"
50
+ run_websocket(socket, request)
51
+ else
52
+ handle_get(socket, request)
53
+ end
54
+ #when "HEAD"
55
+ # TODO: Deal with HEAD request. https://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html#sec9.4
56
+ else
57
+ # TODO: respond with an appropriate error.
58
+ response "I don't understand what you sent - go away."
59
+ socket.print "HTTP/1.1 200 OK\r\n" +
60
+ "Content-Type: text/plain\r\n" +
61
+ "Content-Length: #{response.bytesize}\r\n" +
62
+ "Connection: close\r\n" +
63
+ "\r\n" +
64
+ response
65
+ socket.close
66
+ end
67
+
68
+ end
69
+
70
+
71
+
72
+ # handle_get
73
+ #
74
+ # Handle a get request.
75
+ def handle_get(socket, request)
76
+
77
+ case request[:url]
78
+ when "/", "/index.html"
79
+ socket.print build_app
80
+ socket.close
81
+ when "/app.css"
82
+ socket.print build_css
83
+ socket.close
84
+ else
85
+ socket.print build_error 404
86
+ socket.close
87
+ end
88
+
89
+ end
90
+
91
+
92
+ # read_HTTP_message
93
+ #
94
+ # Read an HTTP message from the socket, and parse it into a request Hash.
95
+ def read_HTTP_message socket
96
+ message = []
97
+ loop do
98
+ line = socket.gets
99
+ message << line
100
+ if line == "\r\n"
101
+ break
102
+ end
103
+ end
104
+
105
+
106
+ request = {fields: {}}
107
+ (request[:method], request[:url], request[:protocol]) = message[0].split(" ")
108
+
109
+ message[1..-1].each{ |line|
110
+ (key, value) = line.split(": ")
111
+ request[:fields][key.split("-").join("_").to_sym] = value
112
+ }
113
+ request
114
+ end
@@ -0,0 +1,207 @@
1
+ #!/snap/bin/ruby
2
+ #
3
+ # Author: Eric Power
4
+ #
5
+ # Description:
6
+ # Contains the functions that control the websocket. The WebSocket protocol
7
+ # that I'm implementing/creating has a max frame size of MAX_WS_FRAME_SIZE
8
+ # bytes.
9
+ #
10
+ # Note: Websocket code is based on the code provided in the following article:
11
+ # https://www.honeybadger.io/blog/building-a-simple-websockets-server-from-scratch-in-ruby/
12
+
13
+
14
+ # Imports
15
+ require 'digest'
16
+ require 'open3'
17
+
18
+ # Key Variables
19
+ MAX_WS_FRAME_SIZE = 50.0
20
+
21
+ # run_websocket
22
+ #
23
+ #
24
+ def run_websocket(socket, initial_request)
25
+
26
+ accept_connection(socket, initial_request[:fields][:Sec_WebSocket_Key][0..-3])
27
+ child_threads = {}
28
+
29
+ loop {
30
+ msg = receive_msg socket
31
+ break if msg.nil? # The socket was closed by the client.
32
+
33
+ case msg.split(":")[0]
34
+ when "RUN"
35
+ cmd_id = msg.match(/^RUN:(.*)$/)[1].to_i
36
+
37
+ unless child_threads[cmd_id]
38
+ child_threads[cmd_id] = Thread.new do
39
+ run_command_and_stream(socket, cmd_id)
40
+ child_threads[cmd_id] = nil
41
+ end
42
+ end
43
+ when "STOP"
44
+
45
+ cmd_id = msg.match(/^STOP:(.*)$/)[1].to_i
46
+ unless child_threads[cmd_id].nil?
47
+ child_threads[cmd_id].kill
48
+ child_threads[cmd_id] = nil
49
+ end
50
+
51
+ else
52
+ log_error "Received an unrecognized message over the websocket: #{msg}"
53
+ end
54
+ }
55
+
56
+ end
57
+
58
+
59
+ # run_command_and_stream
60
+ #
61
+ # Run a command and stream the stdout and stderr through the websocket.
62
+ def run_command_and_stream(socket, cmd_id)
63
+
64
+ cmd = get_command cmd_id
65
+ if cmd.nil?
66
+ log_error "Client requested command ID #{cmd_id} be run, but that ID does not exist."
67
+ return
68
+ end
69
+ Open3::popen3(cmd) do |stdin, stdout, stderr, cmd_thread|
70
+
71
+ continue = true
72
+
73
+ while ready_fds = IO.select([stdout, stderr])[0]
74
+ ready_fds.each{ |fd|
75
+ resp = fd.gets
76
+ if resp.nil?
77
+ continue = false
78
+ break
79
+ end
80
+ if fd == stdout
81
+ send_msg(socket, cmd_id, "OUT", resp, )
82
+ elsif fd == stderr
83
+ send_msg(socket, cmd_id, "ERR", resp, )
84
+ else
85
+ raise "Received output from popen3(#{cmd}) that was not via stdout or stderr."
86
+ end
87
+ }
88
+ break unless continue
89
+ end
90
+
91
+ cmd_thread.join
92
+ send_msg(socket, cmd_id, "FINISHED")
93
+ end
94
+ end
95
+
96
+
97
+
98
+ # get_command
99
+ #
100
+ #
101
+ def get_command cmd_id
102
+ $config[:commands].each { |cmd|
103
+ if cmd[:id] == cmd_id
104
+ return cmd[:cmd]
105
+ end
106
+ }
107
+ nil
108
+ end
109
+
110
+ # accept_connection
111
+ #
112
+ # Sends back the HTTP header to initialize the websocket connection.
113
+ def accept_connection(socket, ws_key)
114
+
115
+ ws_accept_key = Digest::SHA1.base64digest(
116
+ ws_key + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11")
117
+ socket.write "HTTP/1.1 101 Switching Protocols\r\n" +
118
+ "Upgrade: websocket\r\n" +
119
+ "Connection: Upgrade\r\n" +
120
+ "Sec-WebSocket-Accept: #{ws_accept_key}\r\n" +
121
+ "\r\n"
122
+ end
123
+
124
+
125
+ # receive_msg
126
+ #
127
+ #
128
+ def receive_msg socket
129
+
130
+ # Check first two bytes
131
+ byte1 = socket.getbyte
132
+ byte2 = socket.getbyte
133
+ if byte1 == 0x88 # Client is requesting that we close the connection.
134
+ # TODO: Unsure how to properly handle this case. Right now the socket will close and
135
+ # everything here will shut down - eventually? Kill all child threads first?
136
+ log_info "Client requested the websocket be closed."
137
+ socket.close
138
+ return
139
+ end
140
+ fin = byte1 & 0b10000000
141
+ opcode = byte1 & 0b00001111
142
+ msg_size = byte2 & 0b01111111
143
+ is_masked = byte2 & 0b10000000
144
+ unless fin and opcode == 1 and is_masked and msg_size < MAX_WS_FRAME_SIZE
145
+ log_error "Invalid websocket message received. #{byte1}-#{byte2}"
146
+ puts socket.gets
147
+ msg_size.times.map { socket.getbyte } # Read message from socket.
148
+ return
149
+ end
150
+
151
+ # Get message
152
+ mask = 4.times.map { socket.getbyte }
153
+ msg = msg_size.times.map { socket.getbyte }.each_with_index.map {
154
+ |byte, i| byte ^ mask[i % 4]
155
+ }.pack('C*').force_encoding('utf-8').inspect
156
+ msg[1..-2] # Remove quotation marks from message
157
+
158
+ end
159
+
160
+
161
+ # send_msg
162
+ #
163
+ #
164
+ def send_msg(socket, cmd_id, msg_type, msg=nil)
165
+
166
+
167
+ # TODO: Figure out the proper frame size (MAX_WS_FRAME_SIZE).
168
+ def send_frame(socket, fmsg)
169
+ output = [0b10000001, fmsg.size, fmsg]
170
+ socket.write output.pack("CCA#{fmsg.size}")
171
+ end
172
+
173
+ case msg_type
174
+ when "OUT", "ERR"
175
+ header = "#{cmd_id}:#{msg_type}:"
176
+ if header.length > MAX_WS_FRAME_SIZE
177
+ log_error "Message header '#{msg_type}' is too long. Msg: #{msg}."
178
+ elsif msg.nil?
179
+ log_error "Message of type '#{msg_type}' sent without a message."
180
+ else
181
+ if msg.length > MAX_WS_FRAME_SIZE - header.length
182
+ msg_part_len = MAX_WS_FRAME_SIZE - header.length
183
+ msg_parts = (0..(msg.length-1)/msg_part_len).map{ |i|
184
+ msg[i*msg_part_len,msg_part_len]
185
+ }
186
+ msg_parts.each{ |part|
187
+ send_frame(socket, header + part)
188
+ }
189
+ else
190
+ send_frame(socket, header + msg)
191
+ end
192
+ end
193
+
194
+ when "CLEAR", "FINISHED"
195
+ to_send = "#{cmd_id}:#{msg_type}"
196
+ if to_send.length > MAX_WS_FRAME_SIZE
197
+ log_error "Message of type '#{msg_type}' is too long. Msg: #{to_send}."
198
+ elsif !msg.nil?
199
+ log_error "Message of type '#{msg_type}' passed a message. Msg: #{msg}."
200
+ else
201
+ send_frame(socket, to_send)
202
+ end
203
+ else
204
+ log_error "Trying to send a websocket message with unrecognized type: #{msg_type}"
205
+ end
206
+
207
+ end
data/lib/easel.rb ADDED
@@ -0,0 +1,135 @@
1
+ #!/snap/bin/ruby
2
+ #
3
+ # Author: Eric Power
4
+ #
5
+ # Description:
6
+ # Easel Dashboard turns a YAML file into a custom dashboard. See docs/configuration for
7
+ # a description of how to set up the YAML file.
8
+
9
+ # Imports
10
+ require 'optparse'
11
+ require 'yaml'
12
+
13
+ require_relative 'easel/logging'
14
+ require_relative 'easel/server'
15
+ require_relative 'easel/configuration'
16
+
17
+ # launch
18
+ #
19
+ # Launches the Easel Dashboard. Check the $config variable for defaults, although
20
+ # everything can be overridden by the YAML file (and some by the command line
21
+ # arguments).
22
+ def launch
23
+
24
+ parse_ARGV
25
+
26
+ # Load the provided YAML
27
+ overwrite_config $config[:yaml_file]
28
+ log_info("YAML loaded successfully (from: #{$config[:yaml_file]})")
29
+ $config[:commands].each_with_index{ |cmd, i| cmd[:id] = i } # Give commands an ID.
30
+ $config.freeze # Set config to read only
31
+
32
+ # Lauch the server
33
+ log_info("Launching server at #{$config[:hostname]}:#{$config[:port]}")
34
+ launch_server
35
+ end
36
+
37
+
38
+ # parse_ARGV
39
+ #
40
+ # Parses the command line arguments (ARGV) using the optparse gem. Optional
41
+ # command line arguments can be seen by running this program with the -h (or
42
+ # --help) flag.
43
+ def parse_ARGV
44
+ opt_parser = OptionParser.new do |opts|
45
+ opts.banner = "Useage: launch.rb [flags] configuration.yaml"
46
+
47
+ opts.on("-h", "--help", "Prints this help message.") do
48
+ puts opts
49
+ exit
50
+ end
51
+
52
+ opts.on("-l LOG_LEVEL", "--log LOG_LEVEL", Integer, "Sets the logging level (default=2). 0=Silent, 1=Errors, 2=Warnings, 3=Info.") do |lvl|
53
+ if [0, 1, 2, 3].include?(lvl)
54
+ $config[:logging] = lvl
55
+ else
56
+ log_fatal "Command argument LOG_LEVEL '#{lvl}' not recognized. Expected 0, 1, 2, or 3."
57
+ end
58
+ end
59
+
60
+ opts.on("-p PORT", "--port PORT", Integer, "Sets the port to bind to. Default is #{$config[:port]}.") do |port|
61
+ if port >= 0 and port <= 65535
62
+ $config[:port] = port
63
+ else
64
+ log_fatal "Command argument PORT '#{port}' not a valid port. Must be between 0 and 65535 (inclusive)"
65
+ end
66
+ end
67
+
68
+ opts.on("-h HOST", "--hostname HOST", "Sets the hostname. Default is '#{$config[:hostname]}'.") do |port|
69
+ if port >= 0 and port <= 65535
70
+ $config[:port] = port
71
+ else
72
+ log_fatal "Command argument PORT '#{port}' not a valid port. Must be between 0 and 65535 (inclusive)"
73
+ end
74
+ end
75
+
76
+ opts.on("-o [FILE]", "--output [FILE]", "Set a log file.") do |filename|
77
+ begin
78
+ $config[:log_file] = File.new(filename, "a")
79
+ rescue Exception => e
80
+ log_error "Log file could not be open. Sending log to STDIN. Error message: #{e}"
81
+ end
82
+ end
83
+ end.parse!
84
+
85
+ if ARGV.length != 1
86
+ log_fatal "launch.rb takes exactly one file. Try -h for more details."
87
+ else
88
+ $config[:yaml_file] = ARGV[0]
89
+ end
90
+ end
91
+
92
+
93
+ # overwrite_config
94
+ #
95
+ # Overwrites the $config fields that are set in the YAML file provided on input.
96
+ # TODO: Log (error?) every key in the YAML file that does not exist in $config.
97
+ def overwrite_config yaml_filename
98
+
99
+ # TODO: Rewrite using pattern matching to allow checking if the
100
+ # yaml_contents.each is one of the base keys. If so, check that the associated
101
+ # value matches the expected nesting. Do that 'no other values' check.
102
+
103
+ # TODO: Ensure that the command names are less than 1020 bytes (because I'm
104
+ # setting the max length of a single websocket message to 1024 (minus 'STOP:'))
105
+
106
+ begin
107
+ yaml_contents = YAML.load_file $config[:yaml_file]
108
+ rescue Exception => e
109
+ log_fatal "YAML failed to load. Error Message: #{e}"
110
+ end
111
+
112
+ def loop_overwrite (config, yaml)
113
+ yaml.each_key { |key|
114
+ if yaml[key].is_a? Hash
115
+ loop_overwrite(config[key.to_sym], yaml[key])
116
+ elsif yaml[key].is_a? Array
117
+ config[key.to_sym] = []
118
+ yaml[key].each { |elmnt|
119
+ element = {}
120
+ loop_overwrite(element, elmnt)
121
+ config[key.to_sym] << element
122
+ }
123
+ else
124
+ config[key.to_sym] = yaml[key]
125
+ end
126
+ }
127
+ end
128
+
129
+ loop_overwrite($config, yaml_contents)
130
+ end
131
+
132
+
133
+ if __FILE__ == $0
134
+ launch
135
+ end
@@ -0,0 +1,204 @@
1
+ html, body {
2
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
3
+ 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
4
+ sans-serif;
5
+ -webkit-font-smoothing: antialiased;
6
+ -moz-osx-font-smoothing: grayscale;
7
+
8
+ --size-tiny: 5px;
9
+ --size-small: 10px;
10
+ --size-medium: 20px;
11
+ --size-large: 40px;
12
+ --size-xlarge: 80px;
13
+
14
+ --surface: <%= $config[:colours][:surface] %>;
15
+ --background: <%= $config[:colours][:background] %>;
16
+ --primary: <%= $config[:colours][:primary] %>;
17
+ --secondary: <%= $config[:colours][:secondary] %>;
18
+ --on-background: <%= $config[:colours][:on_background] %>;
19
+ --on-surface: <%= $config[:colours][:on_surface] %>;
20
+ --on-primary: <%= $config[:colours][:on_primary] %>;
21
+ --on-secondary: <%= $config[:colours][:on_secondary] %>;
22
+ --shadow: <%= $config[:colours][:shadow] %>;
23
+ --stdout-colour: <%= $config[:colours][:stdout_colour] %>;
24
+ --stderr-colour: <%= $config[:colours][:stderr_colour] %>;
25
+
26
+
27
+ --text-size-small: 12pt;
28
+ --text-size: 14pt;
29
+ --text-size-large: 18pt;
30
+ --text-size-xlarge: 28pt;
31
+
32
+ --text-weight: 300;
33
+ --text-weight-semi-bold:450;
34
+ --text-weight-bold: 600;
35
+
36
+ background-color: var(--background);
37
+ color: var(--on-background);
38
+
39
+ font-size: var(--text-size);
40
+ font-weight: var(--text-weight);
41
+ line-height: 1;
42
+
43
+ margin: 0;
44
+ box-sizing: border-box;
45
+ }
46
+ span.stderr { color: var(--stderr-colour); }
47
+ span.stdout { color: var(--stdout-colour); }
48
+
49
+ h1, .h1 {font-size: var(--text-size-xlarge); font-weight: var(--text-weight-bold);}
50
+ h2, .h2 {font-size: var(--text-size-large); font-weight: var(--text-weight-bold);}
51
+ h3, .h3 {font-size: var(--text-size-large); font-weight: var(--text-weight-semi-bold);}
52
+ h4, .h4 {font-size: var(--text-size); font-weight: var(--text-weight-semi-bold);}
53
+
54
+ *{padding: 0; margin: 0;}
55
+
56
+
57
+
58
+ .screen {
59
+ height: 100vh;
60
+ width: 100vw;
61
+ display: flex;
62
+ flex-direction: row;
63
+ justify-content: stretch;
64
+ align-items: stretch;
65
+ }
66
+
67
+ .cmd-picker {
68
+ width: 320px;
69
+ height: 100%;
70
+ overflow-y: scroll;
71
+ }
72
+ .cmd-picker .command:not(:last-child) {border-bottom: solid 1px var(--accent);}
73
+
74
+ .command-box{
75
+ display: flex;
76
+ flex-direction: column;
77
+ background-color: var(--surface);
78
+ border-radius: var(--size-small);
79
+ align-items: stretch;
80
+ margin: var(--size-small);
81
+ overflow: hidden;
82
+ }
83
+
84
+ .command-info {
85
+ background-color: var(--surface);
86
+ color: var(--on-surface);
87
+ padding: var(--size-medium);
88
+ padding-bottom: var(--size-small);
89
+ display: flex;
90
+ flex-direction: column;
91
+ align-items: flex-start;
92
+ border-bottom: solid 1px var(--primary);
93
+ z-index: 3;
94
+ box-shadow: 0 0 var(--size-small) var(--shadow);
95
+ cursor: pointer;
96
+ }
97
+ .command-info:hover { color: var(--primary); }
98
+
99
+ .command-controls {
100
+ display: flex;
101
+ flex-direction: row;
102
+ justify-content: stretch;
103
+ align-items: baseline;
104
+ box-shadow: inset 0 var(--size-tiny) var(--size-small) #000000;
105
+ }
106
+
107
+ .command-icon {
108
+ flex-grow: 2;
109
+ text-align: center;
110
+ padding: var(--size-tiny);
111
+ background-color: var(--primary);
112
+ color: var(--on-primary);
113
+ }
114
+ .command-icon:hover {
115
+ background-color: var(--secondary);
116
+ color: var(--on-secondary);
117
+ cursor: pointer;
118
+ }
119
+
120
+ ::-webkit-scrollbar {
121
+ width: var(--size-small);
122
+ height: var(--size-small);
123
+ background-color: var(--background);
124
+ }
125
+ ::-webkit-scrollbar-thumb {
126
+ background: var(--primary);
127
+ border-radius: var(--size-tiny);
128
+ border: solid 2px var(--background);
129
+ }
130
+
131
+
132
+
133
+
134
+
135
+
136
+
137
+
138
+
139
+ .interface {
140
+ /* margin: var(--size-large); */
141
+ flex-grow: 2;
142
+ display: flex;
143
+ flex-direction: column;
144
+ justify-content: stretch;
145
+ align-items: stretch;
146
+ overflow: hidden;
147
+ border-radius: var(--size-small);
148
+ padding: var(--size-medium);
149
+ margin: var(--size-small);
150
+ }
151
+ #interface-title {
152
+ padding-bottom: var(--size-medium);
153
+ }
154
+ .interface-output {
155
+ flex-grow: 2;
156
+ flex-basis: 0;
157
+ padding: var(--size-medium);
158
+ box-shadow: inset 0 0 var(--size-medium) var(--background);
159
+ }
160
+ .interface-footer{
161
+ display: flex;
162
+ flex-direction: row;
163
+ justify-content: flex-end;
164
+ padding-top: var(--size-medium);
165
+ }
166
+ .interface-commands {
167
+ width: 33%;
168
+ position: relative;
169
+ display: flex;
170
+ flex-direction: row;
171
+ justify-content: stretch;
172
+ border-radius: var(--size-small);
173
+ overflow: hidden;
174
+ }
175
+ .interface-button {
176
+ flex-grow: 2;
177
+ flex-basis: 0;
178
+ flex-shrink: 2;
179
+ text-align: center;
180
+ padding: var(--size-tiny) var(--size-small);
181
+ background-color: var(--primary);
182
+ color: var(--on-primary);
183
+ overflow: hidden;
184
+ }
185
+ .interface-button:hover {
186
+ background-color: var(--secondary);
187
+ color: var(--on-secondary);
188
+ cursor: pointer;
189
+ }
190
+ .top-shadow {
191
+ position: absolute;
192
+ width: 100%;
193
+ transform: translate(calc( -1 * var(--size-small)),calc( -1 * calc( var(--size-large))));
194
+ height: var(--size-large);
195
+ background-color: none;
196
+ z-index: 50;
197
+ box-shadow: 0 0 var(--size-small) var(--shadow);
198
+ }
199
+
200
+
201
+ .surface{ background-color: var(--surface); color: var(--on-surface); }
202
+ .background{ background-color: var(--background); color: var(--on-background); }
203
+
204
+ .scroll-content { min-height: min-content; overflow: auto; }
@@ -0,0 +1,181 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en" dir="ltr">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <title>
6
+ <%= $config[:title] %>
7
+ </title>
8
+ <link rel="stylesheet" href="/app.css">
9
+ <script type="text/javascript">
10
+
11
+ // TODO: Add a RUN and STOP variable, that point to a div as a play button
12
+ // or a square.
13
+ let ws_socket = new WebSocket("ws://" + location.hostname + ":" + location.port);
14
+ let current_cmd = null;
15
+ let keep_pane_contents = false; // Remove default contents when you run a command.
16
+
17
+ let streams = { <%$config[:commands].each do |command|%>
18
+ <%= command[:id] %>: { is_running: false, content: "", name: "<%= command[:name] %>" },<% end %>
19
+ };
20
+
21
+ // Receive message
22
+ ws_socket.onmessage = (event) => {
23
+
24
+ pane = document.getElementById('output-pane');
25
+ console.log("Received: " + event.data);
26
+
27
+ // Clear pane if appropriate
28
+ if (!keep_pane_contents) {
29
+ pane.innerHTML = "";
30
+ keep_pane_contents = true;
31
+ }
32
+
33
+ // Split message into [ID, CMD, MSG]
34
+ msg_frags = event.data.split(":");
35
+ id = msg_frags[0];
36
+ cmd_type = msg_frags[1];
37
+ msg = msg_frags.slice(2).join(":");
38
+
39
+
40
+ // Validate
41
+ if (!(id in streams) || !(["ERR", "OUT", "CLEAR", "FINISHED"].includes(cmd_type)) ) {
42
+ console.log("Error validating message: " + events.data);
43
+ return;
44
+ }
45
+
46
+ // Process message
47
+ switch (cmd_type) {
48
+ case "ERR":
49
+ append_stream_content(id, "<span class=\"stderr\">" + msg + "</span>");
50
+ break;
51
+ case "OUT":
52
+ append_stream_content(id, msg);
53
+ break;
54
+ case "CLEAR":
55
+ streams[id]['content'] = "";
56
+ update_pane();
57
+ break;
58
+ case "FINISHED":
59
+ document.getElementById('cmd-' + id + '-run-icon').innerHTML = "Run";
60
+ streams[id]['is_running'] = false;
61
+ update_pane();
62
+ break;
63
+ default:
64
+ console.log("Error: Message not understood. Id: " + id + ", cmd_type: " + cmd_type + ", msg:" + msg);
65
+ }
66
+ }
67
+
68
+ // append_stream_content
69
+ function append_stream_content(id, content){
70
+ console.log("Id: " + id + ", current_cmd: " + current_cmd + ", content: " + content);
71
+ streams[id]['content'] += content;
72
+ update_pane();
73
+ }
74
+
75
+ function toggle_run(id){
76
+ if (streams[id]['is_running']) { // STOP (already running)
77
+ ws_socket.send("STOP:" + id);
78
+ document.getElementById('cmd-' + id + '-run-icon').innerHTML = "Run";
79
+ } else { // RUN (currently not running)
80
+ streams[id]['content'] = "";
81
+ ws_socket.send("RUN:" + id);
82
+ document.getElementById('cmd-' + id + '-run-icon').innerHTML = "Stop";
83
+ current_cmd = id;
84
+ }
85
+ streams[id]['is_running'] = !streams[id]['is_running'];
86
+ update_pane();
87
+ }
88
+
89
+ function load_pane(id) {
90
+ current_cmd = id;
91
+ update_pane();
92
+ }
93
+
94
+ function update_pane() {
95
+ pane = document.getElementById('output-pane');
96
+ pane_state = document.getElementById('interface-state');
97
+ pane_state_details = document.getElementById('interface-state-details');
98
+
99
+ pane.innerHTML = streams[current_cmd]['content'];
100
+ if (streams[current_cmd]['is_running']) {
101
+ pane_state.innerHTML = "Running: ";
102
+ } else {
103
+ pane_state.innerHTML = "Output Of: ";
104
+ }
105
+ pane_state_details.innerHTML = streams[current_cmd]['name'];
106
+ }
107
+
108
+ function show_help_message() {
109
+ current_cmd = null;
110
+ pane = document.getElementById('output-pane');
111
+ pane_state = document.getElementById('interface-state');
112
+ pane_state_details = document.getElementById('interface-state-details');
113
+
114
+
115
+ pane.innerHTML =
116
+ "Welcome to the CDash Dashboard! Programs are set up via a YAML file on the server.\n\n" +
117
+ "You can run these programs by clicking 'Run' under a program's information.\n\n" +
118
+ "Programs' output will update even if you are looking at the output of another program.\n\n" +
119
+ "Use the 'Show' button to select which programs' output you are seeing.";
120
+ pane_state.innerHTML = "Showing: ";
121
+ pane_state_details = "Help Message";
122
+ }
123
+
124
+ </script>
125
+ </head>
126
+ <body>
127
+ <div class="screen">
128
+ <div class="cmd-picker">
129
+
130
+ <% $config[:commands].each do |command| %>
131
+ <div class="command-box">
132
+ <div class="command-info"
133
+ onclick="load_pane(<%=command[:id]%>)"
134
+ id="cmd-<%=command[:id]%>"
135
+ data-id="<%=command[:id]%>"
136
+ data-cmd="<%=command[:cmd]%>">
137
+ <h2><%= command[:name] %></h2>
138
+ <p><%= command[:desc] %></p>
139
+ </div>
140
+ <div class="command-controls">
141
+ <div class="command-icon"
142
+ id="cmd-<%=command[:id]%>-run-icon"
143
+ onclick="toggle_run(<%= command[:id] %>)">
144
+ Run
145
+ </div>
146
+ <div class="command-icon"
147
+ id="cmd-<%=command[:id]%>-log-icon"
148
+ onclick="load_pane(<%=command[:id]%>)">
149
+ Show
150
+ </div>
151
+ </div>
152
+ </div>
153
+ <% end %>
154
+ </div>
155
+ <div class="interface surface">
156
+ <div id="interface-title">
157
+ <span class="h2" id="interface-state">Waiting:</span>
158
+ <span id="interface-state-details"></span>
159
+ </div>
160
+ <div class="interface-output scroll-content">
161
+ <pre class="output-pane" id="output-pane">Click 'Run' to run a command and see it's output.
162
+
163
+ Click 'Help' (below) to see more.</pre>
164
+ </div>
165
+ <div class="interface-footer">
166
+ <div class="interface-commands">
167
+ <div class="top-shadow"></div>
168
+ <div class="interface-button"
169
+ onclick="show_help_message()">
170
+ Help
171
+ </div>
172
+ <div class="interface-button"
173
+ onclick="console.log('Save output button has not been implemented.')">
174
+ Save Output
175
+ </div>
176
+ </div>
177
+ </div>
178
+ </div>
179
+ </div>
180
+ </body>
181
+ </html>
@@ -0,0 +1,18 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en" dir="ltr">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <title>
6
+ <%= "CDash - #{code} + Error" %>
7
+ </title>
8
+ <link rel="stylesheet" href="/app.css">
9
+ </head>
10
+ <body>
11
+ <h2>
12
+ <%= "#{code} + Error" %>
13
+ </h2>
14
+ <p>
15
+ <%= @code_names[404] %>
16
+ </p>
17
+ </body>
18
+ </html>
metadata ADDED
@@ -0,0 +1,56 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: easel-dashboard
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.4.2
5
+ platform: ruby
6
+ authors:
7
+ - Eric Power
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2021-09-16 00:00:00.000000000 Z
12
+ dependencies: []
13
+ description: Use a YAML file to set up a dashboard, and change everything from the
14
+ page colours to the commands that can be run. The dashboard shows the output of
15
+ each command, which can be used to monitor server health, running processes, and
16
+ much more.
17
+ email: ericpower@outlook.com
18
+ executables:
19
+ - easel
20
+ extensions: []
21
+ extra_rdoc_files: []
22
+ files:
23
+ - bin/easel
24
+ - lib/easel.rb
25
+ - lib/easel/build_pages.rb
26
+ - lib/easel/configuration.rb
27
+ - lib/easel/logging.rb
28
+ - lib/easel/server.rb
29
+ - lib/easel/websocket.rb
30
+ - lib/html/app.css.erb
31
+ - lib/html/app.html.erb
32
+ - lib/html/error.html.erb
33
+ homepage: https://github.com/epwr/cdash
34
+ licenses:
35
+ - MIT
36
+ metadata: {}
37
+ post_install_message:
38
+ rdoc_options: []
39
+ require_paths:
40
+ - lib
41
+ required_ruby_version: !ruby/object:Gem::Requirement
42
+ requirements:
43
+ - - ">="
44
+ - !ruby/object:Gem::Version
45
+ version: '0'
46
+ required_rubygems_version: !ruby/object:Gem::Requirement
47
+ requirements:
48
+ - - ">="
49
+ - !ruby/object:Gem::Version
50
+ version: '0'
51
+ requirements: []
52
+ rubygems_version: 3.2.22
53
+ signing_key:
54
+ specification_version: 4
55
+ summary: Easily set up and serve a dashboard using only a YAML file.
56
+ test_files: []