statusboard 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 1d8b58e432750151202b3c026d2dd4a4e631f37d
4
+ data.tar.gz: 511a95a6e5adcc91189b04d4173cd0c8245d8d60
5
+ SHA512:
6
+ metadata.gz: 97aa5838ef052af60bdafa2197b0e92d36163fd33b4c5170d2dc238a895889d9a9e69bbf2b3ba4e78533dad0e4cb2395f11fbb3cc4178c2e4a9013237f739a8b
7
+ data.tar.gz: 2ca1585052559e091ef5fba019b3006eb17a0866ce24d8ca212645dade9f41075ab2dc7aae1b35b71a92f060edc16aaab0ec7bb6496eab51f409f046aaa60c8b
Binary file
@@ -0,0 +1,22 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
18
+ *.bundle
19
+ *.so
20
+ *.o
21
+ *.a
22
+ mkmf.log
@@ -0,0 +1 @@
1
+ 2.1.1
@@ -0,0 +1,14 @@
1
+ ## Architecture
2
+
3
+ The gem differntiates between the application layer and the DSL.
4
+
5
+ ### The DSL
6
+ The DSL is located in the `dsl` directory. All DSL-related classes normally end with `Description` (e.g. `GraphDescription`) and inherite from the class DSLBase.
7
+
8
+ As the "Description" suffix already states, the DSL objects can be seen as a description of what the application layer should do - similar to a config file in a "normal" application, but in a dynamic fashion. The task of the DSL part is to take the user input (which is achieved by being a DSL), sanitizing it where required and passing it with a useful structure to the application layer.
9
+
10
+ ### The application layer
11
+ The application layer uses the description it received by evaluating the DSL and uses the information to produce the desired output (in this case graph widgets).
12
+
13
+ ### Discussion
14
+ It should be discussed if the separation expalined above is useful or not.
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in statusboard.gemspec
4
+ gemspec
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2014 Julian Schuh
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,296 @@
1
+ # Statusboard
2
+
3
+ The statusboard gem provides a **simple, expressive DSL** which was purpose-built to feed your Panic-powered Status Board with data that matters to you. The DSL handles table, graph and DIY widgets in a way that renders messing around with raw data unnecessary.
4
+
5
+ The included server module makes serving the data to the app a simple and straight-forward process that doesn't require you to write any server-related code. The Rack-compliance of the server module makes the integration with existing systems a breeze.
6
+
7
+ [Visit the Panic website for more information about the Status Board app.](https://panic.com/statusboard/)
8
+
9
+ ## Getting Started
10
+
11
+ ### Installation
12
+ Install the gem by either running `gem install statusboard` or by adding the line `gem "statusboard"` to your Gemfile and running the `bundle` command.
13
+
14
+ ### Your first Status Board data source:
15
+
16
+ Create a file called statusboard.rb with the following contents:
17
+
18
+ ```ruby
19
+ require "statusboard/main"
20
+
21
+ widget "yequalsx", :graph do
22
+ title "My first graph"
23
+ type :line
24
+
25
+ data do
26
+ data_sequence do
27
+ title "f(x) = x"
28
+
29
+ (0..15).each do |n|
30
+ datapoint n, n
31
+ end
32
+ end
33
+ end
34
+ end
35
+ ```
36
+
37
+ and run `ruby statusboard.rb`. A webserver which serves the widget will automatically be started on port 8080. In your Status Board App, add a graph widget and set the URL to `your.ip:8080/widget/yequalsx`. A graph widget containing the plot of the mathematical function `f(x) = x` will be displayed.
38
+
39
+ For further and more complex examples **take a look at the `examples` directory**.
40
+
41
+ ## DSL
42
+ The statusboard gem features a simple and expressive DSL which is used to configure and feed the widgets with data. Supported statements of the DSL are explained in the following paragraphs.
43
+
44
+ ### widget
45
+ The **widget** statement is used to define a new widget with a specified _name_ and _type_. The widgets name is used as the identifier of the widget and hence has to be unique. A block containing further DSL statements which describe the widget and its contents must be specified.
46
+
47
+ ```ruby
48
+ widget name, type do
49
+ ...
50
+ end
51
+ ```
52
+
53
+ Supported types are `:table`, `:diy` and `:graph`. The specified type directly translates to the corresponding class, e.g. `:table` will use the class `Statusboard::TableWidget`.
54
+
55
+ Example:
56
+ ```ruby
57
+ widget :sales, :graph do
58
+ ...
59
+ end
60
+ ```
61
+ The above code will define a graph-widget with the name `sales`. The widget will be available at the URL `http://your.ip:8080/widget/sales/`.
62
+
63
+ #### Advanced Features
64
+ - Custom widget types are supported: Create a subclass of `Statusboard::WidgetBase` in the `Statusboard` module with a name like `MycustomWidget` (replace `Mycustom`). Then use the corresponding identifier (e.g. `:mycustom`) with the widget statement.
65
+ - You can create a widget manually (by instanciating the widgets class) and pass the resulting object directly to the widget statement as the second parameter. In this case, the block must not be specified.
66
+
67
+ ```ruby
68
+ my_diy = Statusboard::DiyWidget.new do
69
+ ...
70
+ end
71
+
72
+ widget :mycustom1, my_diy
73
+ ```
74
+
75
+ ### Table widget
76
+ A table widget provides one top-level statement: `data`, which accepts either a block or a proc. The specified block/proc should contain the code which is responsible to fetche the data which should be displayed. The block/proc will be executed every time the content of the widget is requested by the app.
77
+ Within the block/proc, the DSL can be used to specify the data:
78
+
79
+ The `row` statement creates a new row. A block must be specified in which the cells of the row are specified. The only statement that is accepted within `row` is the `cell` statement.
80
+
81
+ The `cell` statement creates a cell within a row. A `cell` can have different properties (represented by the corresponding DSL statements) as listed below:
82
+
83
+ | Statement | Description |
84
+ | ------------- | ------------- |
85
+ | type | Type of the cell. (Sell table below for supported types)|
86
+ | content | Main content of the cell. Depends on the cell type.|
87
+ | width | Width of the cell. Can be specified in px or percent. |
88
+ | colspan | Colspan of the cell |
89
+ | percentage | Percentage which should be displayed. Only used if cell type is `:percentage`|
90
+ | imageurl | URL of the image that should be displayed. Only used if cell type is `:image`|
91
+ | noresize | Indicates if the image should be resized or not. Only used if cell type is `:image`|
92
+
93
+ The following cell types are supported:
94
+
95
+ | Type | Description |
96
+ | ------------- | ------------- |
97
+ | `:text` | Displays the text specified as `content` |
98
+ | `:percentage` | Displays a percentage indicator. Percentage must be specified using the `percentage` statement |
99
+ | `:image` | Displays an image. URL must be specified using the `imageurl` statement. |
100
+ | `:cutsom` | Enables the use of custom cell. The cell (including all necessary HTML markup) must be specified using the `content` statement. |
101
+
102
+ Example:
103
+
104
+ ```ruby
105
+ data_proc = Proc.new do
106
+ row do
107
+ cell do
108
+ type :text
109
+ content "First row with 20%"
110
+ end
111
+
112
+ cell do
113
+ type :percentage
114
+ percentage 20
115
+ end
116
+ end
117
+
118
+ row do
119
+ cell do
120
+ type :text
121
+ content "Second row with 80%"
122
+ end
123
+
124
+ cell do
125
+ type :percentage
126
+ percentage 80
127
+ end
128
+ end
129
+ end
130
+
131
+ widget :testtable, :table do
132
+ data data_proc
133
+ end
134
+ ```
135
+
136
+ ### Graph Widget
137
+
138
+ A graph widget provides seven top-level statements:
139
+
140
+ | Statement | Description |
141
+ | ------------- | ------------- |
142
+ | `data` | A block or proc which handles fetching of the data. |
143
+ | `x_axis` | A block which can be used to configure the behavior of the X axis. |
144
+ | `y_axis` | A block which can be used to configure the behavior of the Y axis. |
145
+ | `refresh_interval` | Specifies how often the data should be refreshed by the app (in seconds). |
146
+ | `title` | Specified the title of the widget. |
147
+ | `type` | Specifies the type of the graph. Supported values: `:bar` and `:line` |
148
+ | `display_totals` | Specified wether or not the totals of all data sequences should be displayed. Can be called without parameter. |
149
+
150
+
151
+ #### The `data` statement
152
+
153
+ The `data` statement is used to feed the graph widget with data. It provides exactly one statement:
154
+ `data_sequence`.
155
+ The statement defines a new data sequence, which is a collection of data points that belong together. The data points of a sequence taken together yield to the corresponding line in the graph.The statement can be called multiple times if multiple data sequences should be specified.
156
+
157
+ The `data_sequence` again takes a block which is used to configure the data sequence and to specify the data which should be displayed:
158
+
159
+ | Statement | Description |
160
+ | ------------- | ------------- |
161
+ | `title` | Title describing the data sequence. E.g. "Sales per Day" |
162
+ | `color` | The color in which the line or bars should appear. |
163
+ | `datapoint` | Adds a data point to the current data sequence. The statement accespts *two* parameters: The **X** coordinate and the **Y** coordinate. The statement can be called multiple times in order to add multiple data points. Data points will displayed in the order they are added, _not_ sorted by the X coordinate. |
164
+
165
+ #### The `x_axis` statement
166
+
167
+ The following statement(s) are supported to configure the behavior of the **X** axis.
168
+
169
+ | Statement | Description |
170
+ | ------------- | ------------- |
171
+ | `show_every_label` | Forces every x datapoints to be displayed on the axis. Can be called without parameter. |
172
+
173
+ #### The `y_axis` statement
174
+
175
+ The following statement(s) are supported to configure the behavior of the **Y** axis.
176
+
177
+ | Statement | Description |
178
+ | ------------- | ------------- |
179
+ | `min_value` | Minimum value of the Y coordinate of every datapoint which should be displayed. |
180
+ | `max_value` | Maximum value of the Y coordinate of every datapoint which should be displayed. |
181
+ | `units_suffix` | Suffix which is appended to the Y axis labels. Can be used to add a unit to the raw data. |
182
+ | `units_prefix` | Prefix which is prepended to the Y axis labels. Can be used to add a unit to the raw data. |
183
+ | `scale_to` | Scales the Y coordinates by the specified value. |
184
+ | `hide_labels` | Specified if no lebsl should be displayed at all for the Y axis. Can be called without parameter. |
185
+
186
+ #### Example
187
+
188
+ ```ruby
189
+ widget "yequalsx", :graph do
190
+ title "My first graph"
191
+ type :line
192
+
193
+ data do
194
+ data_sequence do
195
+ title "f(x) = x"
196
+
197
+ (0..15).each do |n|
198
+ datapoint n, n
199
+ end
200
+ end
201
+ end
202
+ end
203
+ ```
204
+
205
+ ### DIY Widget
206
+
207
+ The DIY widget allows to create completely custom-made, HTML-based widgets. As a further abstraction is not possible, the widgets offers only _one_ top-level statement: `content`. `content` awaits either a block, a proc or an arbitrary object which can be converted to a string.
208
+
209
+ Example:
210
+
211
+ ```ruby
212
+ widget "custom", :diy do
213
+ content do
214
+ %w[this is a test].join(" ")
215
+ end
216
+ end
217
+ ```
218
+
219
+ ## Advanced Deployment
220
+
221
+ The gem can be deployed in _three_ different ways:
222
+
223
+ ### **Standalone**
224
+ In this scenario, the gem is used to create a standalone server application whose only purpose is to serve data to the Status Board app. As this is the prevalent case, the gem was designed to support this scenario without having to write any code other than the code that acts as the data source.
225
+
226
+ If this scenario fits your needs best, just add the line
227
+
228
+ ```ruby
229
+ require "statusboard/main"
230
+ ```
231
+
232
+ to the top of your application file and define the widgets you want your Status Board to display using the DSL:
233
+
234
+ ```ruby
235
+ widget "widget-name", :widget-type do
236
+ ... use the DSL to describe the widget here
237
+ end
238
+ ```
239
+
240
+ Run the file and a server is started automatically.
241
+
242
+ ### As a module within an existing Rack-based app
243
+ In this scenario, the gem is used within an existing, Rack-based server application. The included Rack module is mounted in the application (e.g. using the routes.rb file of a rails app).
244
+
245
+ To create a server instance which can be used in a Rack-based environment, include the required files by adding the line
246
+
247
+ ```ruby
248
+ require "statusboard"
249
+ ```
250
+ to your application file.
251
+
252
+ Afterwards create the server instance and use its constructors block to describe the widgets which should be served:
253
+
254
+ ```ruby
255
+ app = Statusboard::StatusboardServer.new do
256
+ widget ... do
257
+ ...
258
+ end
259
+ end
260
+ ```
261
+
262
+ Use `app` as a parameter to the mount call to integrate the Status Board serving with your existing app.
263
+
264
+ ### Without a server
265
+ The gem can be used to describe a widget and construct the Status Board compatible ouput without using any of its server components. This allows for an easy integration into existing applications like non-Rack-based web servers or the generation of data which is used and/or served statically later on.
266
+
267
+ To use _only_ the data construction capabilities of the statusboard gem, add the following line to your application file:
268
+
269
+ ```ruby
270
+ require "statusboard"
271
+ ```
272
+
273
+ Afterwards you can instanciate the classes `Statusboard::DiyWidget`, `Statusboard::GraphWidget` or `Statusboard::TableWidget` by passing a block containing the DSL statements to the constructor. Call the `render` method on a newly created object to retrieve the Status Board compatible output:
274
+
275
+ ```ruby
276
+ my_widget = Statusboard::DiyWidget.new do
277
+ content "My first widget with custom content!"
278
+ end
279
+
280
+ puts my_widget.render
281
+ ```
282
+
283
+ ## Todo
284
+
285
+ * Verify/discuss architecture of the gem
286
+ * Write meaningful tests
287
+ * Improve language of this document
288
+
289
+ ## Contributing
290
+
291
+ 1. Fork it ( https://github.com/julianschuh/statusboard/fork )
292
+ 2. Read the ARCHITECTURE.md file
293
+ 3. Create your feature branch (`git checkout -b my-new-feature`)
294
+ 4. Commit your changes (`git commit -am 'Add some feature'`)
295
+ 5. Push to the branch (`git push origin my-new-feature`)
296
+ 6. Create a new Pull Request
@@ -0,0 +1,2 @@
1
+ require "bundler/gem_tasks"
2
+
File without changes
@@ -0,0 +1,74 @@
1
+ require "statusboard"
2
+
3
+ # Required so we can run the statusboard server
4
+ require "rack/handler"
5
+
6
+ # Creates a Rackcompatible object which will serve the status board widgets when mounted or run.
7
+ mountable_module = Statusboard::StatusboardServer.new! do |variable|
8
+
9
+ # Specify a graph that plots the f(x)=x function
10
+ widget "yequalsx", :graph do
11
+
12
+ # Configure basic settings of the graph
13
+ title "My first graph"
14
+ type :line
15
+
16
+ # Specify the "data source" - in this case a block. (alternative: a proc)
17
+ data do
18
+ # One graph can have multiple data sequences (which translates to multiple lines/bar colors), we define one
19
+ data_sequence do
20
+ title "f(x) = x"
21
+
22
+ # The data sequence consists of 16 datapoints which represent the function f(x)=x from 0 to 15
23
+ (0..15).each do |n|
24
+ datapoint n, n
25
+ end
26
+ end
27
+ end
28
+ end
29
+
30
+ # Specify a table widget which contains the values of the function f(x) from 0 tp 15 (as plotted in the graph widget)
31
+ widget "yequalsxtable", :table do
32
+
33
+ # We specify the data as a block
34
+ data do
35
+
36
+ # Table header
37
+ row do
38
+ cell do
39
+ type :text
40
+ content "x"
41
+ end
42
+ cell do
43
+ type :text
44
+ content "f(x)"
45
+ end
46
+ end
47
+
48
+ # Add a row for each value to the table
49
+ (0..15).each do |n|
50
+ row do
51
+ cell do
52
+ type :text
53
+ content n
54
+ end
55
+ cell do
56
+ type :text
57
+ content n
58
+ end
59
+ end
60
+ end
61
+ end
62
+ end
63
+ end
64
+
65
+ puts "Loaded widgets: "
66
+ mountable_module.server_description.widgets.keys.each do |widget_name|
67
+ puts widget_name
68
+ end
69
+
70
+ # Serve the widget using a Rack handler (could be mounted in an existing application, too).
71
+ # This is just for educational purposes; if you want to run the statusboard server standalone, you should
72
+ # require the file main.rb. When using main.rb, all initialization and server-related stuff is done
73
+ # automatically.
74
+ Rack::Handler.default.run(mountable_module)
@@ -0,0 +1,21 @@
1
+ require "statusboard"
2
+
3
+ # In this example, the Statusboard-compatible output will be generated without any server-related code
4
+ # being involved. Could be used to integrate the serving of Statusboard widgets into existing applications.
5
+
6
+ widget = Statusboard::GraphWidget.new do
7
+ title "My first graph"
8
+ type :line
9
+
10
+ data do
11
+ data_sequence do
12
+ title "f(x) = x"
13
+
14
+ (0..15).each do |n|
15
+ datapoint n, n
16
+ end
17
+ end
18
+ end
19
+ end
20
+
21
+ puts widget.render
@@ -0,0 +1,59 @@
1
+ require "statusboard/main"
2
+
3
+ # Server listens on port 7777
4
+ server_settings :Port => 7777
5
+
6
+ # Specify a graph that plots the f(x)=x function
7
+ widget "yequalsx", :graph do
8
+
9
+ # Configure basic settings of the graph
10
+ title "My first graph"
11
+ type :line
12
+
13
+ # Specify the "data source" - in this case a block. (alternative: a proc)
14
+ data do
15
+ # One graph can have multiple data sequences (which translates to multiple lines/bar colors), we define one
16
+ data_sequence do
17
+ title "f(x) = x"
18
+
19
+ # The data sequence consists of 16 datapoints which represent the function f(x)=x from 0 to 15
20
+ (0..15).each do |n|
21
+ datapoint n, n
22
+ end
23
+ end
24
+ end
25
+ end
26
+
27
+ # Specify a table widget which contains the values of the function f(x) from 0 tp 15 (as plotted in the graph widget)
28
+ widget "yequalsxtable", :table do
29
+
30
+ # We specify the data as a block
31
+ data do
32
+
33
+ # Table header
34
+ row do
35
+ cell do
36
+ type :text
37
+ content "x"
38
+ end
39
+ cell do
40
+ type :text
41
+ content "f(x)"
42
+ end
43
+ end
44
+
45
+ # Add a row for each value to the table
46
+ (0..15).each do |n|
47
+ row do
48
+ cell do
49
+ type :text
50
+ content n
51
+ end
52
+ cell do
53
+ type :text
54
+ content n
55
+ end
56
+ end
57
+ end
58
+ end
59
+ end
Binary file
@@ -0,0 +1,15 @@
1
+ require "statusboard/version"
2
+
3
+ require "statusboard/errors"
4
+
5
+ require "statusboard/dsl/dsl"
6
+
7
+ require "statusboard/widgets/graph"
8
+ require "statusboard/widgets/diy"
9
+ require "statusboard/widgets/table"
10
+
11
+ require "statusboard/server"
12
+
13
+ module Statusboard
14
+ VIEW_PATH = File.join(File.dirname(__FILE__), "statusboard", "views")
15
+ end
Binary file
@@ -0,0 +1,40 @@
1
+ module Statusboard
2
+ module DSL
3
+ class DSLBase
4
+
5
+ # Automatically creates DSL-like setters for the specified fields.
6
+ # Fields must be specified as symbols.
7
+ def self.setter(*method_names)
8
+ method_names.each do |name|
9
+ send :define_method, name do |data|
10
+ instance_variable_set "@#{name}".to_sym, data
11
+ end
12
+ end
13
+ end
14
+
15
+ # Automatically creates a DSL-like setter for a specified field. If
16
+ # the setter is called by the *user without an argument*, the specified
17
+ # default value will be used as the value.
18
+ #
19
+ # The method will _NOT_ use the specified value as a default value for
20
+ # the field. If a default value is needed, the field should be set in
21
+ # the constructor.
22
+ #
23
+ # ==== Attributes
24
+ #
25
+ # * +method_name+ - Name of the field for which a setter should be created
26
+ # * +default_value+ - Default value of the argument which is used if the method is called without an argument
27
+ def self.setter_with_default_value(method_name, default_value)
28
+ send :define_method, method_name do |data = default_value|
29
+ instance_variable_set "@#{method_name}".to_sym, data
30
+ end
31
+ end
32
+
33
+ # Default constructor. Executes the given block within its own context, so the block
34
+ # contents behave as a DSL.
35
+ def initialize(&block)
36
+ instance_eval &block
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,262 @@
1
+ require "statusboard/dsl/base"
2
+
3
+ module Statusboard
4
+ # Module whoch contains the definition of the DSL that is used to
5
+ # describe and configure the widgets and teir data(sources).
6
+ module DSL
7
+ class ServerDescription < DSLBase
8
+
9
+ attr_reader :widgets, :server_settings
10
+
11
+ def initialize(&block)
12
+ @widgets = {}
13
+
14
+ @server_settings = {
15
+ :Port => 8080,
16
+ :Host => "0.0.0.0"
17
+ }
18
+
19
+ super &block unless block.nil?
20
+ end
21
+
22
+ def server_settings(settings={})
23
+ @server_settings.merge!(settings)
24
+ end
25
+
26
+ # Registers a new widget which will be served by the server.
27
+ #
28
+ # ==== Attributes
29
+ #
30
+ # * +name+ - Unique identifier of the widget. The widget will be accessible using the URL /widget/+name+
31
+ # * +type_or_widget+ - Either the type of the widget as a symbol (:table, :graph, :diy) or an already initialized widget object
32
+ # * +&block+ - If only the type of the widget was specified in the previous paremeter, the block must be specified and contain the DSL statements which descibe the widget
33
+ def widget(name, type_or_widget, &block)
34
+ raise ArgumentError, "Widget name " + name.to_s + " already taken" unless @widgets[name.to_sym].nil?
35
+
36
+ if type_or_widget.respond_to?(:render)
37
+ @widgets[name.to_sym] = type_or_widget
38
+ else
39
+ raise ArgumentError, "Widget " + name.to_s + " specified without block." if block.nil?
40
+
41
+ begin
42
+ klass = Statusboard.const_get(type_or_widget.to_s.capitalize + "Widget")
43
+ rescue NameError
44
+ raise ArgumentError, "Invalid widget type " + type_or_widget.to_s + " specified."
45
+ end
46
+
47
+ @widgets[name.to_sym] = klass.new(&block)
48
+ end
49
+ end
50
+ end
51
+
52
+ class GraphDescription < DSLBase
53
+
54
+ setter :refresh_interval, :title, :type
55
+ setter_with_default_value :display_totals, true
56
+
57
+ def data(proc = nil, &block)
58
+ @data = if proc.nil? then block else proc end
59
+ end
60
+
61
+ def x_axis(&block)
62
+ @x_axis = XAxis.new(&block)
63
+ end
64
+
65
+ def y_axis(&block)
66
+ @y_axis = YAxis.new(&block)
67
+ end
68
+
69
+ def construct
70
+ constructed = {
71
+ "graph" => {
72
+ "title" => @title,
73
+ "refreshEveryNSeconds" => @refresh_interval,
74
+ "totals" => @display_totals,
75
+ "type" => @type
76
+ }
77
+ }
78
+
79
+ begin
80
+ data = GraphData.new(&@data)
81
+ constructed["graph"]["datasequences"] = data.construct
82
+ rescue DataSourceError => e
83
+ constructed["graph"]["error"] = {
84
+ "message" => e.message,
85
+ "detail" => e.message
86
+ }
87
+ end
88
+
89
+ constructed["graph"]["xAxis"] = @x_axis.construct unless @x_axis.nil?
90
+ constructed["graph"]["yAxis"] = @y_axis.construct unless @y_axis.nil?
91
+
92
+ constructed
93
+ end
94
+
95
+ protected
96
+
97
+ class XAxis < DSLBase
98
+ setter_with_default_value :show_every_label, true
99
+
100
+ def construct
101
+ {
102
+ "showEveryLabel" => @show_every_label
103
+ }
104
+ end
105
+ end
106
+
107
+ class YAxis < DSLBase
108
+
109
+ setter :min_value, :max_value, :units_suffix, :units_prefix, :scale_to
110
+ setter_with_default_value :hide_labels, true
111
+
112
+ def construct
113
+ constructed = {
114
+ "scaleTo" => @scale_to,
115
+ "hide" => @hide_labels,
116
+ "units" => { }
117
+ }
118
+
119
+ constructed["minValue"] = @min_value unless @min_value.nil?
120
+ constructed["maxValue"] = @max_value unless @max_value.nil?
121
+ constructed["units"]["prefix"] = @units_prefix unless @units_prefix.nil?
122
+ constructed["units"]["suffix"] = @units_suffix unless @units_suffix.nil?
123
+
124
+ constructed
125
+ end
126
+ end
127
+
128
+ class GraphData < DSLBase
129
+ def initialize(&block)
130
+ @data_sequences = []
131
+
132
+ super &block
133
+ end
134
+
135
+ def data_sequence(title = nil, &block)
136
+ @data_sequences << DataSequence.new(title, &block)
137
+ end
138
+
139
+ def construct
140
+ @data_sequences.map(&:construct)
141
+ end
142
+ end
143
+
144
+ class DataSequence < DSLBase
145
+ def initialize(title, &block)
146
+ @datapoints = []
147
+ @title = title
148
+
149
+ super &block
150
+ end
151
+
152
+ setter :title, :color
153
+
154
+ def datapoint(x, y)
155
+ @datapoints << {title: x.to_s, value: y.to_s}
156
+ end
157
+
158
+ def construct
159
+ constructed = {
160
+ "title" => @title,
161
+ "datapoints" => @datapoints
162
+ }
163
+ constructed["color"] = @color unless @color.nil?
164
+
165
+ constructed
166
+ end
167
+ end
168
+ end
169
+
170
+ class DiyDescription < DSLBase
171
+ def content(proc_or_content = nil, &block)
172
+ @content = if proc_or_content.nil? then block else proc_or_content end
173
+ end
174
+
175
+ def construct
176
+ content = if @content.respond_to?(:call) then @content.call() else @content end
177
+
178
+ {
179
+ content: content
180
+ }
181
+ end
182
+ end
183
+
184
+ class TableDescription < DSLBase
185
+ def data(proc = nil, &block)
186
+ @data = if proc.nil? then block else proc end
187
+ end
188
+
189
+ def construct
190
+ data = TableData.new(&@data).construct
191
+
192
+ {
193
+ data: data
194
+ }
195
+ end
196
+
197
+ protected
198
+
199
+ class TableData < DSLBase
200
+ def initialize(&block)
201
+ @rows = []
202
+
203
+ super &block
204
+ end
205
+
206
+ def row(&block)
207
+ @rows << TableRow.new(&block)
208
+ end
209
+
210
+ def construct
211
+ {
212
+ rows: @rows.map(&:construct)
213
+ }
214
+ end
215
+ end
216
+
217
+ class TableRow < DSLBase
218
+ def initialize(&block)
219
+ @cells = []
220
+
221
+ super &block
222
+ end
223
+
224
+ def cell(type = :text, &block)
225
+ @cells << TableCell.new(type, &block)
226
+ end
227
+
228
+ def construct
229
+ {
230
+ cells: @cells.map(&:construct)
231
+ }
232
+ end
233
+ end
234
+
235
+ class TableCell < DSLBase
236
+ setter :content, :colspan, :width, :percentage, :imageurl, :type
237
+ setter_with_default_value :noresize, true
238
+
239
+ def initialize(type, &block)
240
+
241
+ @type = :text
242
+ @type = type unless type.nil?
243
+
244
+ @noresize = false
245
+
246
+ super &block
247
+ end
248
+
249
+ def construct
250
+ {
251
+ content: @content,
252
+ type: @type,
253
+ width: @width,
254
+ percentage: @percentage,
255
+ imageurl: @imageurl,
256
+ noresize: @noresize
257
+ }
258
+ end
259
+ end
260
+ end
261
+ end
262
+ end
@@ -0,0 +1,2 @@
1
+ class DataSourceError < StandardError
2
+ end
@@ -0,0 +1,22 @@
1
+ require "statusboard"
2
+ require "statusboard/server"
3
+
4
+ require "rack/handler"
5
+ require "rack-handlers"
6
+
7
+ app = Statusboard::StatusboardServer.new! # create a new, _unwrapped_ instance of the server class
8
+
9
+ # Make the top-level syntax work by delegating the right function calls to the right destination
10
+ Sinatra::Delegator.target = app.server_description # Why this? We want all top level function calls to be DSL calls -> So we have to call them in the actual DSL instance, and not the app class itself (which gets its settings from its DSL class instance)
11
+ Sinatra::Delegator.delegate :widget, :server_settings # Allowed calls
12
+
13
+ # include would include the module in Object
14
+ # extend only extends the main object
15
+ extend Sinatra::Delegator
16
+
17
+ class Rack::Builder
18
+ include Sinatra::Delegator
19
+ end
20
+
21
+ # Run the app _after_ the applications main file (where this file was included in) was executed successfully to allow for configuration
22
+ at_exit { Rack::Handler.default.run(app, app.server_description.server_settings) if $!.nil? }
@@ -0,0 +1,29 @@
1
+ require "sinatra/base"
2
+
3
+ module Statusboard
4
+
5
+ # Simple Sinatra-based server whose task it is to serve widgets to the app(s)
6
+ # using http. Widgets can be defined directly using the DSL or by passing already
7
+ # initialized Widget-objects. The widgets are identified using a unique name for
8
+ # each defined widget.
9
+ class StatusboardServer < Sinatra::Base
10
+
11
+ attr_reader :server_description
12
+
13
+ # Initializes a new instance of the server using the configuration specified via
14
+ # the DSL in the block. The server will be initialized without any widgets if no
15
+ # block is specified.
16
+ def initialize(*args, &block)
17
+
18
+ super(*args, &nil) # Dont pass the block to super as it would result in errors because the dsl methods aren't available if not instance_eval'd
19
+
20
+ @server_description = DSL::ServerDescription.new &block
21
+ end
22
+
23
+ get "/widget/:name/?" do |widget|
24
+ raise Sinatra::NotFound if @server_description.widgets[widget.to_sym].nil?
25
+
26
+ @server_description.widgets[widget.to_sym].render
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,3 @@
1
+ module Statusboard
2
+ VERSION = "0.1.0"
3
+ end
@@ -0,0 +1 @@
1
+ <%= content %>
@@ -0,0 +1,6 @@
1
+ <td
2
+ <%= if noresize then "class=\"noresize\"" end %>
3
+ <%= if width then "style=\"width: " + width + ";\"" end %>
4
+ >
5
+ <img src="<%= imageurl %>" />
6
+ </td>
@@ -0,0 +1,7 @@
1
+ <td class="projectsBars">
2
+
3
+ <% (1..(percentage / (100 / 8.0)).round).each do |bar_num| %>
4
+ <div class="barSegment value<%= bar_num %>"></div>
5
+ <% end %>
6
+
7
+ </td>
@@ -0,0 +1,5 @@
1
+ <tr>
2
+ <% cells.each do |cell| %>
3
+ <%= render_template cell[:type].to_s + "_cell.erb", cell %>
4
+ <% end %>
5
+ </tr>
@@ -0,0 +1,7 @@
1
+ <table>
2
+
3
+ <% data[:rows].each do |row| %>
4
+ <%= render_template "row.erb", row %>
5
+ <% end %>
6
+
7
+ </table>
@@ -0,0 +1,5 @@
1
+ <td
2
+ <%= if width then "style=\"width: " + width + ";\"" end %>
3
+ >
4
+ <%= content %>
5
+ </td>
@@ -0,0 +1,37 @@
1
+ require "erb"
2
+ require "tilt/erb"
3
+
4
+ module Statusboard
5
+
6
+ # (Abstract) class that represents a widget which can be displayed
7
+ # using the Status Board app.
8
+ # The class must be subclassed for each supported widget type.
9
+ class WidgetBase
10
+
11
+ # Each widget must be initialized using a DSL. When creating the
12
+ # widget, the block containing the DSL statements must be specified
13
+ # in the constructor.
14
+ def initialize(&block)
15
+ raise "Not implemented."
16
+ end
17
+
18
+ # Method that renders the specific widget into a (text-)format understandable
19
+ # by the Status Board app.
20
+ def render
21
+ raise "Not implemented."
22
+ end
23
+
24
+ protected
25
+ # Renders a specified template with the specified local variables in the context
26
+ # of the object itself. The method will search for the template in a subdirectory
27
+ # of the gems VIEW_PATH. The subdirectory will be derived from the class name.
28
+ #
29
+ # Example:
30
+ # GraphWidget => views/graph/,
31
+ # TestWidget => views/test/
32
+ def render_template(template, locals = {})
33
+ widget_type = self.class.name.split('::').last.sub(/Widget$/, '').downcase
34
+ Tilt::ERBTemplate.new(File.join(Statusboard::VIEW_PATH, widget_type, template)).render(self, locals)
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,17 @@
1
+ require "statusboard/widgets/base"
2
+
3
+ module Statusboard
4
+
5
+ # Represents do-it-yourself (DIY) widgets. The widget is configured and
6
+ # filled with data using a DSL which must be passed to the constructor.
7
+ class DiyWidget < WidgetBase
8
+
9
+ def initialize(&block)
10
+ @diy_description = DSL::DiyDescription.new(&block)
11
+ end
12
+
13
+ def render
14
+ @diy_description.construct[:content]
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,22 @@
1
+ require "statusboard/widgets/base"
2
+
3
+ require "json"
4
+
5
+ module Statusboard
6
+ # Class which represents graph widgets for Status Board.
7
+ # The widget is configured and filled with data using a DSL
8
+ # whoch is passed to the constructor.
9
+ class GraphWidget < WidgetBase
10
+
11
+ # Initializes a new graph widget instance using the configuration
12
+ # and data source specified in the block. The block is excepted to
13
+ # use the DSL.
14
+ def initialize(&block)
15
+ @graph_description = DSL::GraphDescription.new(&block)
16
+ end
17
+
18
+ def render
19
+ @graph_description.construct.to_json
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,20 @@
1
+ require "statusboard/widgets/base"
2
+
3
+ module Statusboard
4
+
5
+ # Class which represents table widgets for Status Board.
6
+ # The widget is configured and filled with data using a DSL
7
+ # which is passed to the constructor.
8
+ class TableWidget < WidgetBase
9
+
10
+ def initialize(&block)
11
+ @table_description = DSL::TableDescription.new(&block)
12
+ end
13
+
14
+ def render
15
+ table_data = @table_description.construct
16
+
17
+ render_template("table.erb", table_data)
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,28 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'statusboard/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "statusboard"
8
+ spec.version = Statusboard::VERSION
9
+ spec.authors = ["Julian Schuh"]
10
+ spec.email = ["julez@julez.in"]
11
+ spec.summary = %q{Feed the Status Board App by Panic with custom data.}
12
+ spec.description = %q{Use a convenient and expressive DSL to feed the Status Board App by Panic with custom data. Benefit directly from rubys expressiveness without having to touch any server-related code.}
13
+ spec.homepage = "http://julez.io"
14
+ spec.license = "MIT"
15
+
16
+ spec.files = `git ls-files -z`.split("\x0")
17
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
18
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
+ spec.require_paths = ["lib"]
20
+
21
+ spec.add_development_dependency "bundler", "~> 1.6"
22
+ spec.add_development_dependency "rake"
23
+
24
+ spec.add_dependency "tilt", ">= 1.4.0" # Simple Template parsing
25
+ spec.add_dependency "rack-handlers", "~> 0.7" # Rack handler
26
+ spec.add_dependency "sinatra", "~> 1.4.5" # Server part is based on sinatra
27
+ spec.add_dependency "rack", "~> 1.5.2"
28
+ end
metadata ADDED
@@ -0,0 +1,163 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: statusboard
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Julian Schuh
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2014-05-25 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: bundler
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '1.6'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '1.6'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rake
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: tilt
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: 1.4.0
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: 1.4.0
55
+ - !ruby/object:Gem::Dependency
56
+ name: rack-handlers
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '0.7'
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '0.7'
69
+ - !ruby/object:Gem::Dependency
70
+ name: sinatra
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: 1.4.5
76
+ type: :runtime
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: 1.4.5
83
+ - !ruby/object:Gem::Dependency
84
+ name: rack
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - "~>"
88
+ - !ruby/object:Gem::Version
89
+ version: 1.5.2
90
+ type: :runtime
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - "~>"
95
+ - !ruby/object:Gem::Version
96
+ version: 1.5.2
97
+ description: Use a convenient and expressive DSL to feed the Status Board App by Panic
98
+ with custom data. Benefit directly from rubys expressiveness without having to touch
99
+ any server-related code.
100
+ email:
101
+ - julez@julez.in
102
+ executables: []
103
+ extensions: []
104
+ extra_rdoc_files: []
105
+ files:
106
+ - ".DS_Store"
107
+ - ".gitignore"
108
+ - ".ruby-version"
109
+ - ARCHITECTURE.md
110
+ - Gemfile
111
+ - LICENSE.txt
112
+ - README.md
113
+ - Rakefile
114
+ - examples/.gitkeep
115
+ - examples/mountable.rb
116
+ - examples/rendering_only.rb
117
+ - examples/standalone.rb
118
+ - lib/.DS_Store
119
+ - lib/statusboard.rb
120
+ - lib/statusboard/.DS_Store
121
+ - lib/statusboard/dsl/base.rb
122
+ - lib/statusboard/dsl/dsl.rb
123
+ - lib/statusboard/errors.rb
124
+ - lib/statusboard/main.rb
125
+ - lib/statusboard/server.rb
126
+ - lib/statusboard/version.rb
127
+ - lib/statusboard/views/table/custom_cell.erb
128
+ - lib/statusboard/views/table/image_cell.erb
129
+ - lib/statusboard/views/table/percentage_cell.erb
130
+ - lib/statusboard/views/table/row.erb
131
+ - lib/statusboard/views/table/table.erb
132
+ - lib/statusboard/views/table/text_cell.erb
133
+ - lib/statusboard/widgets/.DS_Store
134
+ - lib/statusboard/widgets/base.rb
135
+ - lib/statusboard/widgets/diy.rb
136
+ - lib/statusboard/widgets/graph.rb
137
+ - lib/statusboard/widgets/table.rb
138
+ - statusboard.gemspec
139
+ homepage: http://julez.io
140
+ licenses:
141
+ - MIT
142
+ metadata: {}
143
+ post_install_message:
144
+ rdoc_options: []
145
+ require_paths:
146
+ - lib
147
+ required_ruby_version: !ruby/object:Gem::Requirement
148
+ requirements:
149
+ - - ">="
150
+ - !ruby/object:Gem::Version
151
+ version: '0'
152
+ required_rubygems_version: !ruby/object:Gem::Requirement
153
+ requirements:
154
+ - - ">="
155
+ - !ruby/object:Gem::Version
156
+ version: '0'
157
+ requirements: []
158
+ rubyforge_project:
159
+ rubygems_version: 2.2.2
160
+ signing_key:
161
+ specification_version: 4
162
+ summary: Feed the Status Board App by Panic with custom data.
163
+ test_files: []