diff options
author | Daniel Beauchamp <daniel.beauchamp@shopify.com> | 2014-01-12 23:17:39 -0800 |
---|---|---|
committer | Daniel Beauchamp <daniel.beauchamp@shopify.com> | 2014-01-12 23:17:39 -0800 |
commit | 9e8dfe7d4290f0150fdef10230cd0ca7a75c9755 (patch) | |
tree | ccd82c459d988ac2846db6178710b47113b377a4 /lib/dashing | |
parent | 5b045724acd44e691552c0fb8f86b61aa2e0cd06 (diff) | |
parent | c49b9bc5d47fe02d26836dee5034fe28490f0ebd (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.rb | 167 | ||||
-rw-r--r-- | lib/dashing/cli.rb | 109 | ||||
-rw-r--r-- | lib/dashing/downloader.rb | 18 |
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 |