summaryrefslogtreecommitdiff
path: root/lib/dashing
diff options
context:
space:
mode:
authorDaniel Beauchamp <daniel.beauchamp@shopify.com>2014-01-12 23:17:39 -0800
committerDaniel Beauchamp <daniel.beauchamp@shopify.com>2014-01-12 23:17:39 -0800
commit9e8dfe7d4290f0150fdef10230cd0ca7a75c9755 (patch)
treeccd82c459d988ac2846db6178710b47113b377a4 /lib/dashing
parent5b045724acd44e691552c0fb8f86b61aa2e0cd06 (diff)
parentc49b9bc5d47fe02d26836dee5034fe28490f0ebd (diff)
Merge pull request #296 from Shopify/updating_gems_and_refactoring
Updating gems and refactoring
Diffstat (limited to 'lib/dashing')
-rw-r--r--lib/dashing/app.rb167
-rw-r--r--lib/dashing/cli.rb109
-rw-r--r--lib/dashing/downloader.rb18
3 files changed, 294 insertions, 0 deletions
diff --git a/lib/dashing/app.rb b/lib/dashing/app.rb
new file mode 100644
index 0000000..033849c
--- /dev/null
+++ b/lib/dashing/app.rb
@@ -0,0 +1,167 @@
+require 'sinatra'
+require 'sprockets'
+require 'sinatra/content_for'
+require 'rufus/scheduler'
+require 'coffee-script'
+require 'sass'
+require 'json'
+require 'yaml'
+
+SCHEDULER = Rufus::Scheduler.new
+
+def development?
+ ENV['RACK_ENV'] == 'development'
+end
+
+def production?
+ ENV['RACK_ENV'] == 'production'
+end
+
+helpers Sinatra::ContentFor
+helpers do
+ def protected!
+ # override with auth logic
+ end
+end
+
+set :root, Dir.pwd
+set :sprockets, Sprockets::Environment.new(settings.root)
+set :assets_prefix, '/assets'
+set :digest_assets, false
+set server: 'thin', connections: [], history_file: 'history.yml'
+set :public_folder, File.join(settings.root, 'public')
+set :views, File.join(settings.root, 'dashboards')
+set :default_dashboard, nil
+set :auth_token, nil
+
+if File.exists?(settings.history_file)
+ set history: YAML.load_file(settings.history_file)
+else
+ set history: {}
+end
+
+%w(javascripts stylesheets fonts images).each do |path|
+ settings.sprockets.append_path("assets/#{path}")
+end
+
+['widgets', File.expand_path('../../../javascripts', __FILE__)]. each do |path|
+ settings.sprockets.append_path(path)
+end
+
+not_found do
+ send_file File.join(settings.public_folder, '404.html')
+end
+
+at_exit do
+ File.write(settings.history_file, settings.history.to_yaml)
+end
+
+get '/' do
+ protected!
+ dashboard = settings.default_dashboard || first_dashboard
+ raise Exception.new('There are no dashboards available') if not dashboard
+
+ redirect "/" + dashboard
+end
+
+get '/events', provides: 'text/event-stream' do
+ protected!
+ response.headers['X-Accel-Buffering'] = 'no' # Disable buffering for nginx
+ stream :keep_open do |out|
+ settings.connections << out
+ out << latest_events
+ out.callback { settings.connections.delete(out) }
+ end
+end
+
+get '/:dashboard' do
+ protected!
+ tilt_html_engines.each do |suffix, _|
+ file = File.join(settings.views, "#{params[:dashboard]}.#{suffix}")
+ return render(suffix.to_sym, params[:dashboard].to_sym) if File.exist? file
+ end
+
+ halt 404
+end
+
+post '/dashboards/:id' do
+ request.body.rewind
+ body = JSON.parse(request.body.read)
+ body['dashboard'] ||= params['id']
+ auth_token = body.delete("auth_token")
+ if !settings.auth_token || settings.auth_token == auth_token
+ send_event(params['id'], body, 'dashboards')
+ 204 # response without entity body
+ else
+ status 401
+ "Invalid API key\n"
+ end
+end
+
+post '/widgets/:id' do
+ request.body.rewind
+ body = JSON.parse(request.body.read)
+ auth_token = body.delete("auth_token")
+ if !settings.auth_token || settings.auth_token == auth_token
+ send_event(params['id'], body)
+ 204 # response without entity body
+ else
+ status 401
+ "Invalid API key\n"
+ end
+end
+
+get '/views/:widget?.html' do
+ protected!
+ tilt_html_engines.each do |suffix, engines|
+ file = File.join(settings.root, "widgets", params[:widget], "#{params[:widget]}.#{suffix}")
+ return engines.first.new(file).render if File.exist? file
+ end
+end
+
+def send_event(id, body, target=nil)
+ body[:id] = id
+ body[:updatedAt] ||= Time.now.to_i
+ event = format_event(body.to_json, target)
+ Sinatra::Application.settings.history[id] = event unless target == 'dashboards'
+ Sinatra::Application.settings.connections.each { |out| out << event }
+end
+
+def format_event(body, name=nil)
+ str = ""
+ str << "event: #{name}\n" if name
+ str << "data: #{body}\n\n"
+end
+
+def latest_events
+ settings.history.inject("") do |str, (id, body)|
+ str << body
+ end
+end
+
+def first_dashboard
+ files = Dir[File.join(settings.views, '*')].collect { |f| File.basename(f, '.*') }
+ files -= ['layout']
+ files.sort.first
+end
+
+def tilt_html_engines
+ Tilt.mappings.select do |_, engines|
+ default_mime_type = engines.first.default_mime_type
+ default_mime_type.nil? || default_mime_type == 'text/html'
+ end
+end
+
+def require_glob(relative_glob)
+ Dir[File.join(settings.root, relative_glob)].each do |file|
+ require file
+ end
+end
+
+settings_file = File.join(settings.root, 'config/settings.rb')
+require settings_file if File.exists?(settings_file)
+
+{}.to_json # Forces your json codec to initialize (in the event that it is lazily loaded). Does this before job threads start.
+job_path = ENV["JOB_PATH"] || 'jobs'
+require_glob(File.join('lib', '**', '*.rb'))
+require_glob(File.join(job_path, '**', '*.rb'))
diff --git a/lib/dashing/cli.rb b/lib/dashing/cli.rb
new file mode 100644
index 0000000..27d8f6b
--- /dev/null
+++ b/lib/dashing/cli.rb
@@ -0,0 +1,109 @@
+require 'thor'
+require 'open-uri'
+
+module Dashing
+ class CLI < Thor
+ include Thor::Actions
+
+ attr_reader :name
+
+ class << self
+ attr_accessor :auth_token
+
+ def hyphenate(str)
+ return str.downcase if str =~ /^[A-Z-]+$/
+ str.gsub('_', '-').gsub(/\B[A-Z]/, '-\&').squeeze('-').downcase
+ end
+ end
+
+ no_tasks do
+ %w(widget dashboard job).each do |type|
+ define_method "generate_#{type}" do |name|
+ @name = Thor::Util.snake_case(name)
+ directory(type.to_sym, "#{type}s")
+ end
+ end
+ end
+
+ desc "new PROJECT_NAME", "Sets up ALL THE THINGS needed for your dashboard project."
+ def new(name)
+ @name = Thor::Util.snake_case(name)
+ directory(:project, @name)
+ end
+
+ desc "generate (widget/dashboard/job) NAME", "Creates a new widget, dashboard, or job."
+ def generate(type, name)
+ public_send("generate_#{type}".to_sym, name)
+ rescue NoMethodError => e
+ puts "Invalid generator. Either use widget, dashboard, or job"
+ end
+
+ desc "install GIST_ID", "Installs a new widget from a gist."
+ def install(gist_id)
+ gist = Downloader.get_gist(gist_id)
+ public_url = "https://gist.github.com/#{gist_id}"
+
+ install_widget_from_gist(gist)
+
+ print set_color("Don't forget to edit the ", :yellow)
+ print set_color("Gemfile ", :yellow, :bold)
+ print set_color("and run ", :yellow)
+ print set_color("bundle install ", :yellow, :bold)
+ say set_color("if needed. More information for this widget can be found at #{public_url}", :yellow)
+ rescue OpenURI::HTTPError => http_error
+ say set_color("Could not find gist at #{public_url}"), :red
+ end
+
+ desc "start", "Starts the server in style!"
+ method_option :job_path, :desc => "Specify the directory where jobs are stored"
+ def start(*args)
+ port_option = args.include?('-p') ? '' : ' -p 3030'
+ args = args.join(' ')
+ command = "bundle exec thin -R config.ru start#{port_option} #{args}"
+ command.prepend "export JOB_PATH=#{options[:job_path]}; " if options[:job_path]
+ run_command(command)
+ end
+
+ desc "stop", "Stops the thin server"
+ def stop
+ command = "bundle exec thin stop"
+ run_command(command)
+ end
+
+ desc "job JOB_NAME AUTH_TOKEN(optional)", "Runs the specified job. Make sure to supply your auth token if you have one set."
+ def job(name, auth_token = "")
+ Dir[File.join(Dir.pwd, 'lib/**/*.rb')].each {|file| require_file(file) }
+ self.class.auth_token = auth_token
+ f = File.join(Dir.pwd, "jobs", "#{name}.rb")
+ require_file(f)
+ end
+
+ # map some commands
+ map 'g' => :generate
+ map 'i' => :install
+ map 's' => :start
+
+ private
+
+ def run_command(command)
+ system(command)
+ end
+
+ def install_widget_from_gist(gist)
+ gist['files'].each do |file, details|
+ if file =~ /\.(html|coffee|scss)\z/
+ widget_name = File.basename(file, '.*')
+ new_path = File.join(Dir.pwd, 'widgets', widget_name, file)
+ create_file(new_path, details['content'])
+ elsif file.end_with?('.rb')
+ new_path = File.join(Dir.pwd, 'jobs', file)
+ create_file(new_path, details['content'])
+ end
+ end
+ end
+
+ def require_file(file)
+ require file
+ end
+ end
+end
diff --git a/lib/dashing/downloader.rb b/lib/dashing/downloader.rb
new file mode 100644
index 0000000..140e862
--- /dev/null
+++ b/lib/dashing/downloader.rb
@@ -0,0 +1,18 @@
+require 'net/http'
+require 'open-uri'
+require 'json'
+
+module Dashing
+ module Downloader
+ extend self
+
+ def get_gist(gist_id)
+ get_json("https://api.github.com/gists/#{gist_id}")
+ end
+
+ def get_json(url)
+ response = open(url).read
+ JSON.parse(response)
+ end
+ end
+end