Skip to content
This repository was archived by the owner on Mar 7, 2018. It is now read-only.

Commit 9e8dfe7

Browse files
committed
Merge pull request #296 from Shopify/updating_gems_and_refactoring
Updating gems and refactoring
2 parents 5b04572 + c49b9bc commit 9e8dfe7

File tree

12 files changed

+601
-377
lines changed

12 files changed

+601
-377
lines changed

.gitignore

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
11
*DS_STORE
22
/Gemfile.lock
3-
*.gem
3+
*.gem
4+
coverage/
5+
.ruby-version

.travis.yml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
language: ruby
22
rvm:
3+
- 2.1.0
34
- 2.0.0
45
- 1.9.3
56

6-
script: "rake test"
7+
script: "rake test"

bin/dashing

Lines changed: 6 additions & 110 deletions
Original file line numberDiff line numberDiff line change
@@ -1,112 +1,8 @@
11
#!/usr/bin/env ruby
2+
require "pathname"
3+
bin_file = Pathname.new(__FILE__).realpath
4+
$:.unshift File.expand_path("../../lib", bin_file)
25

3-
require 'thor'
4-
require 'net/http'
5-
require 'json'
6-
require 'open-uri'
7-
8-
class MockScheduler
9-
def method_missing(*args)
10-
yield
11-
end
12-
end
13-
14-
SCHEDULER = MockScheduler.new
15-
16-
module Dashing
17-
18-
class CLI < Thor
19-
include Thor::Actions
20-
21-
class << self
22-
attr_accessor :auth_token
23-
24-
def hyphenate(str)
25-
return str.downcase if str =~ /^[A-Z-]+$/
26-
str.gsub('_', '-').gsub(/\B[A-Z]/, '-\&').squeeze('-').downcase
27-
end
28-
end
29-
30-
attr_accessor :name
31-
32-
no_tasks do
33-
['widget', 'dashboard', 'job'].each do |type|
34-
define_method "generate_#{type}" do |name|
35-
@name = Thor::Util.snake_case(name)
36-
directory type.to_sym, File.join("#{type}s")
37-
end
38-
end
39-
end
40-
41-
def self.source_root
42-
File.expand_path('../../templates', __FILE__)
43-
end
44-
45-
desc "new PROJECT_NAME", "Sets up ALL THE THINGS needed for your dashboard project."
46-
def new(name)
47-
@name = Thor::Util.snake_case(name)
48-
directory :project, @name
49-
end
50-
51-
desc "generate (widget/dashboard/job) NAME", "Creates a new widget, dashboard, or job."
52-
def generate(type, name)
53-
send("generate_#{type}".to_sym, name)
54-
rescue NoMethodError => e
55-
puts "Invalid generator. Either use widget, dashboard, or job"
56-
end
57-
map "g" => :generate
58-
59-
desc "install GIST_ID", "Installs a new widget from a gist."
60-
def install(gist_id)
61-
public_url = "https://gist.github.com/#{gist_id}"
62-
gist = JSON.parse(open("https://api.github.com/gists/#{gist_id}").read)
63-
64-
gist['files'].each do |filename, contents|
65-
if filename.end_with?(".rb")
66-
create_file File.join(Dir.pwd, 'jobs', filename), contents['content']
67-
elsif filename.end_with?(".coffee", ".html", ".scss")
68-
widget_name = File.basename(filename, '.*')
69-
create_file File.join(Dir.pwd, 'widgets', widget_name, filename), contents['content']
70-
end
71-
end
72-
73-
print set_color("Don't forget to edit the ", :yellow)
74-
print set_color("Gemfile ", :yellow, :bold)
75-
print set_color("and run ", :yellow)
76-
print set_color("bundle install ", :yellow, :bold)
77-
say set_color("if needed. More information for this widget can be found at #{public_url}", :yellow)
78-
79-
rescue OpenURI::HTTPError => e
80-
say set_color("Could not find gist at #{public_url}"), :red
81-
end
82-
map "i" => :install
83-
84-
desc "start", "Starts the server in style!"
85-
method_option :job_path, :desc => "Specify the directory where jobs are stored"
86-
def start(*args)
87-
port_option = args.include?('-p')? '' : ' -p 3030'
88-
args = args.join(" ")
89-
command = "bundle exec thin -R config.ru start #{port_option} #{args}"
90-
command.prepend "export JOB_PATH=#{options[:job_path]}; " if options[:job_path]
91-
system(command)
92-
end
93-
map "s" => :start
94-
95-
desc "stop", "Stops the thin server"
96-
def stop
97-
command = "bundle exec thin stop"
98-
system(command)
99-
end
100-
101-
desc "job JOB_NAME AUTH_TOKEN(optional)", "Runs the specified job. Make sure to supply your auth token if you have one set."
102-
def job(name, auth_token = "")
103-
Dir[File.join(Dir.pwd, 'lib/**/*.rb')].each {|file| require file }
104-
self.class.auth_token = auth_token
105-
f = File.join(Dir.pwd, "jobs", "#{name}.rb")
106-
require f
107-
end
108-
109-
end
110-
end
111-
112-
Dashing::CLI.start
6+
require 'dashing'
7+
Dashing::CLI.source_root(File.expand_path('../../templates', bin_file))
8+
Dashing::CLI.start(ARGV)

dashing.gemspec

Lines changed: 17 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -11,20 +11,25 @@ Gem::Specification.new do |s|
1111
s.description = "This framework lets you build & easily layout dashboards with your own custom widgets. Use it to make a status boards for your ops team, or use it to track signups, conversion rates, or whatever else metrics you'd like to see in one spot. Included with the framework are ready-made widgets for you to use or customize. All of this code was extracted out of a project at Shopify that displays dashboards on TVs around the office."
1212
s.author = "Daniel Beauchamp"
1313
s.email = '[email protected]'
14-
s.files = ["lib/Dashing.rb"]
1514
s.homepage = 'http://shopify.github.com/dashing'
1615

1716
s.files = Dir['README.md', 'javascripts/**/*', 'templates/**/*','templates/**/.[a-z]*', 'lib/**/*']
1817

19-
s.add_dependency('sass')
20-
s.add_dependency('coffee-script', '>=1.6.2')
21-
s.add_dependency('execjs', '>=2.0.0')
22-
s.add_dependency('sinatra')
23-
s.add_dependency('sinatra-contrib')
24-
s.add_dependency('thin')
25-
s.add_dependency('rufus-scheduler', '~> 2.0')
26-
s.add_dependency('thor')
27-
s.add_dependency('sprockets')
28-
s.add_dependency('rack')
18+
s.add_dependency('sass', '~> 3.2.12')
19+
s.add_dependency('coffee-script', '~> 2.2.0')
20+
s.add_dependency('execjs', '~> 2.0.2')
21+
s.add_dependency('sinatra', '~> 1.4.4')
22+
s.add_dependency('sinatra-contrib', '~> 1.4.2')
23+
s.add_dependency('thin', '~> 1.6.1')
24+
s.add_dependency('rufus-scheduler', '~> 3.0.3')
25+
s.add_dependency('thor', '~> 0.18.1')
26+
s.add_dependency('sprockets', '~> 2.10.1')
27+
s.add_dependency('rack', '~> 1.5.2')
2928

30-
end
29+
s.add_development_dependency('rake', '~> 10.1.0')
30+
s.add_development_dependency('haml', '~> 4.0.4')
31+
s.add_development_dependency('minitest', '~> 5.2.0')
32+
s.add_development_dependency('mocha', '~> 0.14.0')
33+
s.add_development_dependency('fakeweb', '~> 1.3.0')
34+
s.add_development_dependency('simplecov', '~> 0.8.2')
35+
end

lib/dashing.rb

Lines changed: 4 additions & 165 deletions
Original file line numberDiff line numberDiff line change
@@ -1,167 +1,6 @@
1-
require 'sinatra'
2-
require 'sprockets'
3-
require 'sinatra/content_for'
4-
require 'rufus/scheduler'
5-
require 'coffee-script'
6-
require 'sass'
7-
require 'json'
8-
require 'yaml'
1+
require 'dashing/cli'
2+
require 'dashing/downloader'
3+
require 'dashing/app'
94

10-
SCHEDULER = Rufus::Scheduler.start_new
11-
12-
set :root, Dir.pwd
13-
14-
set :sprockets, Sprockets::Environment.new(settings.root)
15-
set :assets_prefix, '/assets'
16-
set :digest_assets, false
17-
['assets/javascripts', 'assets/stylesheets', 'assets/fonts', 'assets/images', 'widgets', File.expand_path('../../javascripts', __FILE__)]. each do |path|
18-
settings.sprockets.append_path path
19-
end
20-
21-
set server: 'thin', connections: [], history_file: 'history.yml'
22-
23-
# Persist history in tmp file at exit
24-
at_exit do
25-
File.open(settings.history_file, 'w') do |f|
26-
f.puts settings.history.to_yaml
27-
end
28-
end
29-
30-
if File.exists?(settings.history_file)
31-
set history: YAML.load_file(settings.history_file)
32-
else
33-
set history: {}
34-
end
35-
36-
set :public_folder, File.join(settings.root, 'public')
37-
set :views, File.join(settings.root, 'dashboards')
38-
set :default_dashboard, nil
39-
set :auth_token, nil
40-
41-
helpers Sinatra::ContentFor
42-
helpers do
43-
def protected!
44-
# override with auth logic
45-
end
46-
end
47-
48-
get '/events', provides: 'text/event-stream' do
49-
protected!
50-
response.headers['X-Accel-Buffering'] = 'no' # Disable buffering for nginx
51-
stream :keep_open do |out|
52-
settings.connections << out
53-
out << latest_events
54-
out.callback { settings.connections.delete(out) }
55-
end
56-
end
57-
58-
get '/' do
59-
protected!
60-
begin
61-
redirect "/" + (settings.default_dashboard || first_dashboard).to_s
62-
rescue NoMethodError => e
63-
raise Exception.new("There are no dashboards in your dashboard directory.")
64-
end
65-
end
66-
67-
get '/:dashboard' do
68-
protected!
69-
tilt_html_engines.each do |suffix, _|
70-
file = File.join(settings.views, "#{params[:dashboard]}.#{suffix}")
71-
return render(suffix.to_sym, params[:dashboard].to_sym) if File.exist? file
72-
end
73-
74-
halt 404
5+
module Dashing
756
end
76-
77-
get '/views/:widget?.html' do
78-
protected!
79-
tilt_html_engines.each do |suffix, engines|
80-
file = File.join(settings.root, "widgets", params[:widget], "#{params[:widget]}.#{suffix}")
81-
return engines.first.new(file).render if File.exist? file
82-
end
83-
end
84-
85-
post '/dashboards/:id' do
86-
request.body.rewind
87-
body = JSON.parse(request.body.read)
88-
body['dashboard'] ||= params['id']
89-
auth_token = body.delete("auth_token")
90-
if !settings.auth_token || settings.auth_token == auth_token
91-
send_event(params['id'], body, 'dashboards')
92-
204 # response without entity body
93-
else
94-
status 401
95-
"Invalid API key\n"
96-
end
97-
end
98-
99-
post '/widgets/:id' do
100-
request.body.rewind
101-
body = JSON.parse(request.body.read)
102-
auth_token = body.delete("auth_token")
103-
if !settings.auth_token || settings.auth_token == auth_token
104-
send_event(params['id'], body)
105-
204 # response without entity body
106-
else
107-
status 401
108-
"Invalid API key\n"
109-
end
110-
end
111-
112-
not_found do
113-
send_file File.join(settings.public_folder, '404.html')
114-
end
115-
116-
def development?
117-
ENV['RACK_ENV'] == 'development'
118-
end
119-
120-
def production?
121-
ENV['RACK_ENV'] == 'production'
122-
end
123-
124-
def send_event(id, body, target=nil)
125-
body[:id] = id
126-
body[:updatedAt] ||= Time.now.to_i
127-
event = format_event(body.to_json, target)
128-
Sinatra::Application.settings.history[id] = event unless target == 'dashboards'
129-
Sinatra::Application.settings.connections.each { |out| out << event }
130-
end
131-
132-
def format_event(body, name=nil)
133-
str = ""
134-
str << "event: #{name}\n" if name
135-
str << "data: #{body}\n\n"
136-
end
137-
138-
def latest_events
139-
settings.history.inject("") do |str, (id, body)|
140-
str << body
141-
end
142-
end
143-
144-
def first_dashboard
145-
files = Dir[File.join(settings.views, '*')].collect { |f| File.basename(f, '.*') }
146-
files -= ['layout']
147-
files.sort.first
148-
end
149-
150-
def tilt_html_engines
151-
Tilt.mappings.select do |_, engines|
152-
default_mime_type = engines.first.default_mime_type
153-
default_mime_type.nil? || default_mime_type == 'text/html'
154-
end
155-
end
156-
157-
settings_file = File.join(settings.root, 'config/settings.rb')
158-
if (File.exists?(settings_file))
159-
require settings_file
160-
end
161-
162-
Dir[File.join(settings.root, 'lib', '**', '*.rb')].each {|file| require file }
163-
{}.to_json # Forces your json codec to initialize (in the event that it is lazily loaded). Does this before job threads start.
164-
165-
job_path = ENV["JOB_PATH"] || 'jobs'
166-
files = Dir[File.join(settings.root, job_path, '**', '/*.rb')]
167-
files.each { |job| require(job) }

0 commit comments

Comments
 (0)