diff options
Diffstat (limited to 'lib')
-rw-r--r-- | lib/dashing.rb | 6 | ||||
-rw-r--r-- | lib/dashing/app.rb | 181 | ||||
-rw-r--r-- | lib/dashing/cli.rb | 109 | ||||
-rw-r--r-- | lib/dashing/downloader.rb | 18 |
4 files changed, 314 insertions, 0 deletions
diff --git a/lib/dashing.rb b/lib/dashing.rb new file mode 100644 index 0000000..3527034 --- /dev/null +++ b/lib/dashing.rb @@ -0,0 +1,6 @@ +require 'dashing/cli' +require 'dashing/downloader' +require 'dashing/app' + +module Dashing +end
\ No newline at end of file diff --git a/lib/dashing/app.rb b/lib/dashing/app.rb new file mode 100644 index 0000000..b11352d --- /dev/null +++ b/lib/dashing/app.rb @@ -0,0 +1,181 @@ +require 'sinatra' +require 'sprockets' +require 'sinatra/content_for' +require 'rufus/scheduler' +require 'coffee-script' +require 'sass' +require 'json' +require 'yaml' +require 'thin' + +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 + + def authenticated?(token) + return true unless settings.auth_token + token && Rack::Utils.secure_compare(settings.auth_token, token) + 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'), status: 404 +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'] + if authenticated?(body.delete("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) + if authenticated?(body.delete("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 + +Thin::Server.class_eval do + def stop_with_connection_closing + Sinatra::Application.settings.connections.dup.each(&:close) + stop_without_connection_closing + end + + alias_method :stop_without_connection_closing, :stop + alias_method :stop, :stop_with_connection_closing +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..4b93f89 --- /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 [--skip]", "Installs a new widget from a gist (skip overwrite)." + def install(gist_id, *args) + gist = Downloader.get_gist(gist_id) + public_url = "https://gist.github.com/#{gist_id}" + + install_widget_from_gist(gist, args.include?('--skip')) + + 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, skip_overwrite) + 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'], :skip => skip_overwrite) + elsif file.end_with?('.rb') + new_path = File.join(Dir.pwd, 'jobs', file) + create_file(new_path, details['content'], :skip => skip_overwrite) + 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 |