├── log └── .gitkeep ├── lib ├── tasks │ ├── .gitkeep │ └── scheduler.rake └── assets │ └── .DS_Store ├── public ├── favicon.ico ├── robots.txt ├── 500.html ├── 422.html └── 404.html ├── .rspec ├── app ├── mailers │ └── .gitkeep ├── models │ ├── .gitkeep │ ├── sources │ │ ├── boolean │ │ │ ├── demo.rb │ │ │ ├── http_proxy.rb │ │ │ ├── base.rb │ │ │ ├── shell.rb │ │ │ └── pingdom.rb │ │ ├── number │ │ │ ├── demo.rb │ │ │ ├── http_proxy.rb │ │ │ ├── base.rb │ │ │ ├── datapoints.rb │ │ │ ├── hockey_app.rb │ │ │ └── new_relic.rb │ │ ├── ci │ │ │ ├── demo.rb │ │ │ ├── base.rb │ │ │ ├── travis.rb │ │ │ └── jenkins.rb │ │ ├── exception_tracker │ │ │ ├── demo.rb │ │ │ ├── base.rb │ │ │ └── errbit.rb │ │ ├── alert │ │ │ ├── base.rb │ │ │ └── demo.rb │ │ └── datapoints │ │ │ ├── http_proxy.rb │ │ │ ├── demo.rb │ │ │ └── base.rb │ ├── dashboard.rb │ ├── graphite_url_builder.rb │ ├── demo_helper.rb │ ├── aggregation.rb │ ├── ganglia_url_builder.rb │ ├── http_proxy_resolver.rb │ ├── widget.rb │ └── sources.rb ├── assets │ ├── javascripts │ │ ├── filters │ │ │ ├── .gitkeep │ │ │ ├── metrics_prefix.js │ │ │ └── percentage.js │ │ ├── services │ │ │ ├── .gitkeep │ │ │ ├── dashboard.js │ │ │ ├── widget.js │ │ │ ├── color_factory.js │ │ │ ├── suffix_formatter.js │ │ │ ├── sources.js │ │ │ └── editor_form_options.js │ │ ├── .DS_Store │ │ ├── templates │ │ │ ├── .DS_Store │ │ │ ├── widgets │ │ │ │ ├── example │ │ │ │ │ ├── show.html │ │ │ │ │ └── edit.html │ │ │ │ ├── meter │ │ │ │ │ ├── show.html │ │ │ │ │ └── edit.html │ │ │ │ ├── boolean │ │ │ │ │ ├── show.html │ │ │ │ │ └── edit.html.erb │ │ │ │ ├── ci │ │ │ │ │ ├── show.html │ │ │ │ │ └── edit.html.erb │ │ │ │ ├── alert │ │ │ │ │ ├── show.html │ │ │ │ │ └── edit.html.erb │ │ │ │ ├── number │ │ │ │ │ ├── show.html │ │ │ │ │ └── edit.html.erb │ │ │ │ ├── exception_tracker │ │ │ │ │ ├── show.html │ │ │ │ │ └── edit.html │ │ │ │ └── graph │ │ │ │ │ └── edit.html.erb │ │ │ ├── dashboards │ │ │ │ ├── show.html.erb │ │ │ │ ├── index.html │ │ │ │ └── toolbar.html │ │ │ ├── widget │ │ │ │ ├── edit.html.erb │ │ │ │ ├── show.html │ │ │ │ └── json_response_editor.html │ │ │ ├── targets │ │ │ │ └── index.html │ │ │ └── abouts │ │ │ │ └── show.html │ │ ├── controllers │ │ │ ├── about_ctrl.js │ │ │ ├── dashboard_index_ctrl.js │ │ │ ├── main_ctrl.js │ │ │ ├── widget_ctrl.js │ │ │ ├── targets_ctrl.js │ │ │ ├── widget_edit_ctrl.js │ │ │ └── dashboard_show_ctrl.js.erb │ │ ├── widgets │ │ │ ├── example │ │ │ │ ├── filter.js │ │ │ │ ├── service.js │ │ │ │ ├── controller.js │ │ │ │ └── directive.js │ │ │ ├── ci │ │ │ │ ├── controller.js │ │ │ │ ├── service.js │ │ │ │ └── directive.js │ │ │ ├── exception_tracker │ │ │ │ ├── controller.js │ │ │ │ ├── service.js │ │ │ │ └── directive.js │ │ │ ├── alert │ │ │ │ ├── controller.js │ │ │ │ ├── service.js │ │ │ │ └── directive.js │ │ │ ├── boolean │ │ │ │ ├── controller.js │ │ │ │ ├── service.js │ │ │ │ └── directive.js │ │ │ ├── meter │ │ │ │ ├── controller.js │ │ │ │ └── directive.js │ │ │ ├── graph │ │ │ │ ├── service.js │ │ │ │ ├── controller.js │ │ │ │ └── directive.js │ │ │ └── number │ │ │ │ ├── service.js │ │ │ │ ├── directive.js │ │ │ │ └── controller.js │ │ ├── directives │ │ │ ├── blur.js │ │ │ ├── bind_html_unsafe.js │ │ │ ├── contenteditable.js │ │ │ ├── autocomplete.js │ │ │ ├── widget.js │ │ │ ├── td_field.js │ │ │ └── gridster.js │ │ ├── app.js │ │ └── application.js │ ├── .DS_Store │ ├── stylesheets │ │ ├── widgets │ │ │ ├── exception_tracker │ │ │ │ └── style.css.scss │ │ │ ├── example │ │ │ │ └── style.css.scss │ │ │ ├── meter │ │ │ │ └── style.css.scss │ │ │ ├── boolean │ │ │ │ └── style.css.scss │ │ │ ├── number │ │ │ │ └── style.css.scss │ │ │ ├── ci │ │ │ │ └── style.css.scss │ │ │ ├── alert │ │ │ │ └── style.css.scss │ │ │ └── graph │ │ │ │ └── style.css.scss │ │ ├── theme │ │ │ ├── mixins.less │ │ │ ├── custom_variables.less │ │ │ └── index.less │ │ └── .DS_Store │ └── images │ │ ├── select2.png │ │ ├── spinner.gif │ │ ├── spinner2.gif │ │ ├── spinner-gray-bg.gif │ │ ├── spinner-red-bg.gif │ │ └── spinner-green-bg.gif ├── controllers │ ├── application_controller.rb │ ├── layout_controller.rb │ └── api │ │ ├── data_sources_controller.rb │ │ ├── datapoints_targets_controller.rb │ │ ├── dashboards_controller.rb │ │ ├── widgets_controller.rb │ │ └── base_controller.rb ├── views │ └── layouts │ │ └── application.html.erb.bk └── helpers │ └── application_helper.rb ├── .ruby-version ├── spec ├── javascripts │ ├── helpers │ │ ├── .gitkeep │ │ └── SpecHelper.js │ ├── fixtures │ │ ├── .gitkeep │ │ ├── autocomplete.html │ │ └── widgets │ │ │ ├── meter │ │ │ └── show.html │ │ │ ├── boolean │ │ │ └── show.html │ │ │ ├── ci │ │ │ └── show.html │ │ │ ├── number │ │ │ └── show.html │ │ │ └── exception_tracker │ │ │ └── show.html │ ├── spec.css │ ├── spec.js │ ├── services │ │ ├── color_factory_spec.js │ │ └── suffix_formatter_spec.js │ ├── directives │ │ ├── contenteditable_spec.js │ │ ├── bind_html_unsafe_spec.js │ │ └── autocomplete_spec.js │ ├── models │ │ └── exception_tracker_spec.js │ ├── widgets │ │ ├── meter │ │ │ └── directive_spec.js │ │ ├── boolean │ │ │ └── directive_spec.js │ │ ├── exception_tracker │ │ │ └── directive_spec.js │ │ ├── ci │ │ │ └── directive_spec.js │ │ └── number │ │ │ └── directive_spec.js │ ├── support │ │ └── jasmine.yml │ └── views │ │ └── widgets │ │ └── alert_spec.js ├── factories.rb ├── models │ ├── dashboard_spec.rb │ ├── sources │ │ ├── exception_tracker │ │ │ └── errbit_spec.rb │ │ ├── datapoints │ │ │ ├── graphite_spec.rb │ │ │ └── ganglia_spec.rb │ │ └── ci │ │ │ ├── jenkins_spec.rb │ │ │ └── travis_spec.rb │ ├── graphite_url_builder_spec.rb │ ├── aggregation_spec.rb │ ├── ganglia_url_builder_spec.rb │ └── widget_spec.rb ├── controllers │ ├── datapoints_targets_controller_spec.rb │ └── data_sources_controller_spec.rb └── spec_helper.rb ├── vendor └── assets │ ├── javascripts │ ├── .gitkeep │ ├── .DS_Store │ ├── angular-ui │ │ ├── module.js │ │ ├── bootstrap │ │ │ └── modal.js │ │ └── jq.js │ └── jquery.timeago.en-short.js │ ├── stylesheets │ ├── .gitkeep │ ├── .DS_Store │ ├── jquery.gridster.min.css │ └── jquery.gridster.css │ └── images │ ├── alpha.png │ ├── hue.png │ ├── saturation.png │ ├── glyphicons-halflings.png │ └── glyphicons-halflings-white.png ├── Procfile ├── gh-pages ├── screenshot.png └── screenshot_small.png ├── config.ru ├── config ├── environment.rb ├── boot.rb ├── initializers │ ├── mime_types.rb │ ├── backtrace_silencers.rb │ ├── session_store.rb │ ├── quiet_assets.rb │ ├── secret_token.rb │ ├── wrap_parameters.rb │ └── inflections.rb ├── locales │ ├── en.yml │ └── simple_form.en.yml ├── routes.rb ├── unicorn.rb ├── environments │ ├── development.rb │ ├── test.rb │ └── production.rb └── application.rb ├── db ├── migrate │ ├── 20120616131320_change_time_to_range_column_widgets.rb │ ├── 20130327120046_change_targets_length_widgets.rb │ ├── 20120613162315_add_update_interval_to_widgets_table.rb │ ├── 20120908174014_add_locked_column_to_dashboards_table.rb │ ├── 20120603082856_create_dashboards.rb │ ├── 20130212092935_add_layout_to_widgets_table.rb │ └── 20120603083032_create_widgets.rb ├── seeds.rb └── schema.rb ├── doc └── README_FOR_APP ├── script └── rails ├── .travis.yml ├── Rakefile ├── .gitignore ├── Guardfile ├── Gemfile ├── VERSION2_MIGRATION.markdown ├── HTTP_PROXY.markdown ├── CHANGELOG.md └── API.markdown /log/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /lib/tasks/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --colour 2 | -------------------------------------------------------------------------------- /app/mailers/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/models/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.ruby-version: -------------------------------------------------------------------------------- 1 | ruby-1.9.3-p392 2 | -------------------------------------------------------------------------------- /spec/javascripts/helpers/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /spec/javascripts/fixtures/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /vendor/assets/javascripts/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /vendor/assets/stylesheets/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/assets/javascripts/filters/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/assets/javascripts/services/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /spec/javascripts/spec.css: -------------------------------------------------------------------------------- 1 | /* 2 | *= require application 3 | */ -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | web: bundle exec unicorn -p $PORT -c ./config/unicorn.rb -d 2 | -------------------------------------------------------------------------------- /spec/javascripts/spec.js: -------------------------------------------------------------------------------- 1 | //= require application 2 | //= require_tree ./ -------------------------------------------------------------------------------- /app/assets/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lyric/team_dashboard/master/app/assets/.DS_Store -------------------------------------------------------------------------------- /app/assets/stylesheets/widgets/exception_tracker/style.css.scss: -------------------------------------------------------------------------------- 1 | .exception-tracker { 2 | 3 | } -------------------------------------------------------------------------------- /lib/assets/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lyric/team_dashboard/master/lib/assets/.DS_Store -------------------------------------------------------------------------------- /app/assets/stylesheets/theme/mixins.less: -------------------------------------------------------------------------------- 1 | .myButton(@radius: 5px) { 2 | .border-radius(@radius); 3 | } -------------------------------------------------------------------------------- /gh-pages/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lyric/team_dashboard/master/gh-pages/screenshot.png -------------------------------------------------------------------------------- /app/assets/images/select2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lyric/team_dashboard/master/app/assets/images/select2.png -------------------------------------------------------------------------------- /app/assets/images/spinner.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lyric/team_dashboard/master/app/assets/images/spinner.gif -------------------------------------------------------------------------------- /app/assets/images/spinner2.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lyric/team_dashboard/master/app/assets/images/spinner2.gif -------------------------------------------------------------------------------- /gh-pages/screenshot_small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lyric/team_dashboard/master/gh-pages/screenshot_small.png -------------------------------------------------------------------------------- /vendor/assets/images/alpha.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lyric/team_dashboard/master/vendor/assets/images/alpha.png -------------------------------------------------------------------------------- /vendor/assets/images/hue.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lyric/team_dashboard/master/vendor/assets/images/hue.png -------------------------------------------------------------------------------- /app/assets/javascripts/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lyric/team_dashboard/master/app/assets/javascripts/.DS_Store -------------------------------------------------------------------------------- /app/assets/stylesheets/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lyric/team_dashboard/master/app/assets/stylesheets/.DS_Store -------------------------------------------------------------------------------- /app/assets/stylesheets/widgets/example/style.css.scss: -------------------------------------------------------------------------------- 1 | .example { 2 | 3 | .red { 4 | color: red; 5 | } 6 | } 7 | 8 | -------------------------------------------------------------------------------- /vendor/assets/images/saturation.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lyric/team_dashboard/master/vendor/assets/images/saturation.png -------------------------------------------------------------------------------- /vendor/assets/javascripts/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lyric/team_dashboard/master/vendor/assets/javascripts/.DS_Store -------------------------------------------------------------------------------- /vendor/assets/stylesheets/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lyric/team_dashboard/master/vendor/assets/stylesheets/.DS_Store -------------------------------------------------------------------------------- /app/assets/images/spinner-gray-bg.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lyric/team_dashboard/master/app/assets/images/spinner-gray-bg.gif -------------------------------------------------------------------------------- /app/assets/images/spinner-red-bg.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lyric/team_dashboard/master/app/assets/images/spinner-red-bg.gif -------------------------------------------------------------------------------- /spec/javascripts/fixtures/autocomplete.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/assets/images/spinner-green-bg.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lyric/team_dashboard/master/app/assets/images/spinner-green-bg.gif -------------------------------------------------------------------------------- /app/controllers/application_controller.rb: -------------------------------------------------------------------------------- 1 | class ApplicationController < ActionController::Base 2 | protect_from_forgery 3 | 4 | end 5 | -------------------------------------------------------------------------------- /app/assets/javascripts/templates/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lyric/team_dashboard/master/app/assets/javascripts/templates/.DS_Store -------------------------------------------------------------------------------- /vendor/assets/images/glyphicons-halflings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lyric/team_dashboard/master/vendor/assets/images/glyphicons-halflings.png -------------------------------------------------------------------------------- /app/assets/javascripts/templates/widgets/example/show.html: -------------------------------------------------------------------------------- 1 |
2 |

Hello World: {{counter | dollar}} 3 |

4 | -------------------------------------------------------------------------------- /vendor/assets/images/glyphicons-halflings-white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lyric/team_dashboard/master/vendor/assets/images/glyphicons-halflings-white.png -------------------------------------------------------------------------------- /app/controllers/layout_controller.rb: -------------------------------------------------------------------------------- 1 | class LayoutController < ApplicationController 2 | def index 3 | render :text => "", :layout => "application" 4 | end 5 | end -------------------------------------------------------------------------------- /app/assets/javascripts/controllers/about_ctrl.js: -------------------------------------------------------------------------------- 1 | app.controller("AboutCtrl", ["$scope", "$rootScope", function($scope, $rootScope) { 2 | $rootScope.resolved = true; 3 | }]); -------------------------------------------------------------------------------- /lib/tasks/scheduler.rake: -------------------------------------------------------------------------------- 1 | desc "reset demo data set" 2 | task :reset_demo => :environment do 3 | Rake::Task["cleanup"].invoke 4 | Rake::Task["populate"].invoke 5 | end 6 | -------------------------------------------------------------------------------- /config.ru: -------------------------------------------------------------------------------- 1 | # This file is used by Rack-based servers to start the application. 2 | 3 | require ::File.expand_path('../config/environment', __FILE__) 4 | run TeamDashboard::Application 5 | -------------------------------------------------------------------------------- /app/assets/javascripts/widgets/example/filter.js: -------------------------------------------------------------------------------- 1 | app.filter("dollar", function() { 2 | return function(input) { 3 | if (!input) return ""; 4 | 5 | return "$ " + input; 6 | }; 7 | }); -------------------------------------------------------------------------------- /config/environment.rb: -------------------------------------------------------------------------------- 1 | # Load the rails application 2 | require File.expand_path('../application', __FILE__) 3 | 4 | # Initialize the rails application 5 | TeamDashboard::Application.initialize! 6 | -------------------------------------------------------------------------------- /app/assets/javascripts/widgets/example/service.js: -------------------------------------------------------------------------------- 1 | app.factory("ExampleModel", ["$http", function($http) { 2 | return $http.get("/api/data_sources/number", { params: { source: "demo" } }); 3 | }]); 4 | -------------------------------------------------------------------------------- /db/migrate/20120616131320_change_time_to_range_column_widgets.rb: -------------------------------------------------------------------------------- 1 | class ChangeTimeToRangeColumnWidgets < ActiveRecord::Migration 2 | def change 3 | rename_column :widgets, :time, :range 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20130327120046_change_targets_length_widgets.rb: -------------------------------------------------------------------------------- 1 | class ChangeTargetsLengthWidgets < ActiveRecord::Migration 2 | def change 3 | change_column :widgets, :targets, :string, :limit => 5000 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /app/models/sources/boolean/demo.rb: -------------------------------------------------------------------------------- 1 | module Sources 2 | module Boolean 3 | class Demo < Sources::Boolean::Base 4 | def get(options = {}) 5 | { :value => rand(2) == 1 } 6 | end 7 | end 8 | end 9 | end -------------------------------------------------------------------------------- /db/migrate/20120613162315_add_update_interval_to_widgets_table.rb: -------------------------------------------------------------------------------- 1 | class AddUpdateIntervalToWidgetsTable < ActiveRecord::Migration 2 | def change 3 | add_column :widgets, :update_interval, :integer 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /spec/factories.rb: -------------------------------------------------------------------------------- 1 | FactoryGirl.define do 2 | factory :dashboard do 3 | name 'Example Dashboard' 4 | end 5 | 6 | factory :widget do 7 | name 'Example Widget' 8 | association :dashboard 9 | end 10 | end -------------------------------------------------------------------------------- /config/boot.rb: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | 3 | # Set up gems listed in the Gemfile. 4 | ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__) 5 | 6 | require 'bundler/setup' if File.exists?(ENV['BUNDLE_GEMFILE']) 7 | -------------------------------------------------------------------------------- /doc/README_FOR_APP: -------------------------------------------------------------------------------- 1 | Use this README file to introduce your application and point to useful places in the API for learning more. 2 | Run "rake doc:app" to generate API documentation for your models, controllers, helpers, and libraries. 3 | -------------------------------------------------------------------------------- /db/migrate/20120908174014_add_locked_column_to_dashboards_table.rb: -------------------------------------------------------------------------------- 1 | class AddLockedColumnToDashboardsTable < ActiveRecord::Migration 2 | def change 3 | add_column :dashboards, :locked, :boolean, :default => false 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # See http://www.robotstxt.org/wc/norobots.html for documentation on how to use the robots.txt file 2 | # 3 | # To ban all spiders from the entire site uncomment the next two lines: 4 | # User-Agent: * 5 | # Disallow: / 6 | -------------------------------------------------------------------------------- /app/models/sources/number/demo.rb: -------------------------------------------------------------------------------- 1 | module Sources 2 | module Number 3 | class Demo < Sources::Number::Base 4 | 5 | def get(options = {}) 6 | { :value => rand(100*2) - 100 } 7 | end 8 | 9 | end 10 | end 11 | end -------------------------------------------------------------------------------- /config/initializers/mime_types.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Add new mime types for use in respond_to blocks: 4 | # Mime::Type.register "text/richtext", :rtf 5 | # Mime::Type.register_alias "text/html", :iphone 6 | -------------------------------------------------------------------------------- /config/locales/en.yml: -------------------------------------------------------------------------------- 1 | # Sample localization file for English. Add more files in this directory for other locales. 2 | # See https://github.com/svenfuchs/rails-i18n/tree/master/rails%2Flocale for starting points. 3 | 4 | en: 5 | hello: "Hello world" 6 | -------------------------------------------------------------------------------- /vendor/assets/javascripts/angular-ui/module.js: -------------------------------------------------------------------------------- 1 | angular.module('ui.config', []).value('ui.config', {}); 2 | angular.module('ui.filters', ['ui.config']); 3 | angular.module('ui.directives', ['ui.config']); 4 | angular.module('ui', ['ui.filters', 'ui.directives', 'ui.config']); 5 | -------------------------------------------------------------------------------- /app/assets/javascripts/templates/dashboards/show.html.erb: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /app/assets/javascripts/filters/metrics_prefix.js: -------------------------------------------------------------------------------- 1 | app.filter("metricsPrefix", ["SuffixFormatter", function(SuffixFormatter) { 2 | return function(input) { 3 | if (_.isUndefined(input) || _.isNull(input)) return ""; 4 | 5 | return SuffixFormatter.format(input, 2); 6 | }; 7 | }]); -------------------------------------------------------------------------------- /db/migrate/20120603082856_create_dashboards.rb: -------------------------------------------------------------------------------- 1 | class CreateDashboards < ActiveRecord::Migration 2 | def change 3 | create_table :dashboards do |t| 4 | t.string :name 5 | t.string :time 6 | t.string :layout 7 | t.timestamps 8 | end 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /app/assets/javascripts/widgets/ci/controller.js: -------------------------------------------------------------------------------- 1 | app.controller("CiCtrl", ["$scope", function($scope) { 2 | 3 | var defaults = { 4 | size_x: 1, size_y: 1, 5 | update_interval: 10 6 | }; 7 | 8 | if (!$scope.widget.id) { 9 | _.extend($scope.widget, defaults); 10 | } 11 | 12 | }]); -------------------------------------------------------------------------------- /app/assets/javascripts/directives/blur.js: -------------------------------------------------------------------------------- 1 | // usage example: 2 | // 3 | app.directive('blur', function () { 4 | return function (scope, elem, attrs) { 5 | elem.bind('blur', function () { 6 | scope.$apply(attrs.blur); 7 | }); 8 | }; 9 | }); -------------------------------------------------------------------------------- /app/assets/javascripts/widgets/example/controller.js: -------------------------------------------------------------------------------- 1 | app.controller("ExampleCtrl", ["$scope", function($scope) { 2 | 3 | var defaults = { 4 | size_x: 1, size_y: 1, 5 | update_interval: 10 6 | }; 7 | 8 | if (!$scope.widget.id) { 9 | _.extend($scope.widget, defaults); 10 | } 11 | 12 | }]); -------------------------------------------------------------------------------- /app/assets/stylesheets/theme/custom_variables.less: -------------------------------------------------------------------------------- 1 | @iconSpritePath: image-url("images/glyphicons-halflings.png"); 2 | @iconWhiteSpritePath: image-url("images/glyphicons-halflings-white.png"); 3 | 4 | @sansFontFamily: sans-serif; 5 | 6 | @baseFontSize: 13px; 7 | @baseLineHeight: 18px; 8 | -------------------------------------------------------------------------------- /app/assets/javascripts/templates/widgets/meter/show.html: -------------------------------------------------------------------------------- 1 |
2 | 9 |

{{data.label}}

10 |
-------------------------------------------------------------------------------- /app/controllers/api/data_sources_controller.rb: -------------------------------------------------------------------------------- 1 | module Api 2 | class DataSourcesController < BaseController 3 | 4 | def index 5 | plugin = Sources.plugin_clazz(params[:kind], params[:source]) 6 | result = plugin.new.get(params) 7 | respond_with(result.to_json) 8 | end 9 | 10 | end 11 | end -------------------------------------------------------------------------------- /db/migrate/20130212092935_add_layout_to_widgets_table.rb: -------------------------------------------------------------------------------- 1 | class AddLayoutToWidgetsTable < ActiveRecord::Migration 2 | def change 3 | add_column :widgets, :col, :integer 4 | add_column :widgets, :row, :integer 5 | add_column :widgets, :size_x, :integer 6 | add_column :widgets, :size_y, :integer 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /script/rails: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # This command will automatically be run when you run "rails" with Rails 3 gems installed from the root of your application. 3 | 4 | APP_PATH = File.expand_path('../../config/application', __FILE__) 5 | require File.expand_path('../../config/boot', __FILE__) 6 | require 'rails/commands' 7 | -------------------------------------------------------------------------------- /app/assets/javascripts/widgets/exception_tracker/controller.js: -------------------------------------------------------------------------------- 1 | app.controller("ExceptionTrackerCtrl", ["$scope", function($scope) { 2 | 3 | var defaults = { 4 | size_x: 1, size_y: 1, 5 | update_interval: 10 6 | }; 7 | 8 | if (!$scope.widget.id) { 9 | _.extend($scope.widget, defaults); 10 | } 11 | 12 | }]); -------------------------------------------------------------------------------- /app/assets/javascripts/templates/widgets/boolean/show.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 | {{data.label}} 5 |
6 |
7 | -------------------------------------------------------------------------------- /app/controllers/api/datapoints_targets_controller.rb: -------------------------------------------------------------------------------- 1 | module Api 2 | class DatapointsTargetsController < BaseController 3 | respond_to :json 4 | 5 | def index 6 | targets = Sources.datapoints_plugin(params[:source]).available_targets(params) 7 | respond_with targets.to_json 8 | end 9 | 10 | end 11 | end -------------------------------------------------------------------------------- /app/assets/javascripts/widgets/alert/controller.js: -------------------------------------------------------------------------------- 1 | app.controller("AlertCtrl", ["$scope", function($scope) { 2 | 3 | var defaults = { 4 | size_x: 2, 5 | size_y: 1, 6 | update_interval: 10 7 | }; 8 | 9 | if (!$scope.widget.id) { 10 | _.extend($scope.widget, defaults); 11 | } 12 | 13 | }]); -------------------------------------------------------------------------------- /app/assets/javascripts/widgets/boolean/controller.js: -------------------------------------------------------------------------------- 1 | app.controller("BooleanCtrl", ["$scope", function($scope) { 2 | 3 | var defaults = { 4 | size_x: 1, 5 | size_y: 1, 6 | update_interval: 10 7 | }; 8 | 9 | if (!$scope.widget.id) { 10 | _.extend($scope.widget, defaults); 11 | } 12 | 13 | }]); -------------------------------------------------------------------------------- /app/assets/javascripts/widgets/meter/controller.js: -------------------------------------------------------------------------------- 1 | app.controller("MeterCtrl", ["$scope", function($scope) { 2 | 3 | var defaults = { 4 | size_x: 1, size_y: 2, 5 | update_interval: 10, 6 | min: 0, 7 | max: 100 8 | }; 9 | 10 | if (!$scope.widget.id) { 11 | _.extend($scope.widget, defaults); 12 | } 13 | 14 | }]); -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: ruby 2 | before_script: 3 | - mysql -e 'create database team_dashboard_test' 4 | script: 5 | - cp config/database.example.yml config/database.yml 6 | - RAILS_ENV=test bundle exec rake db:migrate 7 | - bundle exec rake db:test:prepare 8 | - bundle exec rspec 9 | after_script: 'bundle exec guard-jasmine' 10 | rvm: 11 | - 1.9.3 12 | -------------------------------------------------------------------------------- /db/seeds.rb: -------------------------------------------------------------------------------- 1 | # This file should contain all the record creation needed to seed the database with its default values. 2 | # The data can then be loaded with the rake db:seed (or created alongside the db with db:setup). 3 | # 4 | # Examples: 5 | # 6 | # cities = City.create([{ name: 'Chicago' }, { name: 'Copenhagen' }]) 7 | # Mayor.create(name: 'Emanuel', city: cities.first) 8 | -------------------------------------------------------------------------------- /app/assets/javascripts/filters/percentage.js: -------------------------------------------------------------------------------- 1 | app.filter("percentage", function() { 2 | return function(input) { 3 | if (_.isUndefined(input) || _.isNull(input)) return ""; 4 | 5 | var result = null; 6 | 7 | if ( input % 1 === 0) { 8 | result = input; 9 | } else { 10 | result = input.toFixed(2); 11 | } 12 | return result + " %"; 13 | }; 14 | }); -------------------------------------------------------------------------------- /spec/models/dashboard_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | describe Dashboard do 4 | describe "#set_defaults" do 5 | it "should initialize layout" do 6 | FactoryGirl.build(:dashboard).layout.should eq([]) 7 | end 8 | 9 | it "name attribute is mandatory" do 10 | FactoryGirl.build(:dashboard, :name => nil).should_not be_valid 11 | end 12 | end 13 | end -------------------------------------------------------------------------------- /app/assets/javascripts/widgets/ci/service.js: -------------------------------------------------------------------------------- 1 | app.factory("CiModel", ["$http", function($http) { 2 | 3 | function getParams(config) { 4 | return { source: config.source, widget_id: config.id }; 5 | } 6 | 7 | function getData(config) { 8 | return $http.get("/api/data_sources/ci", { params: getParams(config) }); 9 | } 10 | 11 | return { 12 | getData: getData 13 | }; 14 | }]); -------------------------------------------------------------------------------- /app/models/dashboard.rb: -------------------------------------------------------------------------------- 1 | class Dashboard < ActiveRecord::Base 2 | has_many :widgets, :dependent => :destroy 3 | 4 | serialize :layout 5 | validates :name, :presence => true 6 | 7 | after_initialize :set_defaults 8 | 9 | attr_accessible :name, :time, :layout, :locked 10 | 11 | protected 12 | 13 | def set_defaults 14 | self.layout = [] unless self.layout 15 | end 16 | 17 | end 18 | -------------------------------------------------------------------------------- /app/assets/javascripts/widgets/alert/service.js: -------------------------------------------------------------------------------- 1 | app.factory("AlertModel", ["$http", function($http) { 2 | 3 | function getParams(config) { 4 | return { source: config.source, widget_id: config.id }; 5 | } 6 | 7 | function getData(config) { 8 | return $http.get("/api/data_sources/alert", { params: getParams(config) }); 9 | } 10 | 11 | return { 12 | getData: getData 13 | }; 14 | }]); -------------------------------------------------------------------------------- /app/assets/javascripts/services/dashboard.js: -------------------------------------------------------------------------------- 1 | app.factory("Dashboard", ["$resource", function($resource) { 2 | return $resource("/api/dashboards/:id", { id: "@id" }, 3 | { 4 | 'create': { method: 'POST' }, 5 | 'index': { method: 'GET', isArray: true }, 6 | 'show': { method: 'GET', isArray: false }, 7 | 'update': { method: 'PUT' }, 8 | 'destroy': { method: 'DELETE' } 9 | }); 10 | }]); -------------------------------------------------------------------------------- /app/assets/javascripts/widgets/boolean/service.js: -------------------------------------------------------------------------------- 1 | app.factory("BooleanModel", ["$http", function($http) { 2 | 3 | function getParams(config) { 4 | return { source: config.source, widget_id: config.id }; 5 | } 6 | 7 | function getData(config) { 8 | return $http.get("/api/data_sources/boolean", { params: getParams(config) }); 9 | } 10 | 11 | return { 12 | getData: getData 13 | }; 14 | }]); -------------------------------------------------------------------------------- /spec/javascripts/fixtures/widgets/meter/show.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /db/migrate/20120603083032_create_widgets.rb: -------------------------------------------------------------------------------- 1 | class CreateWidgets < ActiveRecord::Migration 2 | def change 3 | create_table :widgets do |t| 4 | t.string :name 5 | t.string :kind 6 | t.string :size 7 | t.string :source 8 | t.string :targets 9 | t.string :time 10 | t.text :settings 11 | t.references :dashboard 12 | t.timestamps 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /spec/javascripts/fixtures/widgets/boolean/show.html: -------------------------------------------------------------------------------- 1 | 9 | -------------------------------------------------------------------------------- /app/assets/javascripts/templates/widgets/ci/show.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
{{data.label}}
5 |
6 | {{data.current_status_message}} 7 |
8 |
9 |
-------------------------------------------------------------------------------- /app/assets/javascripts/directives/bind_html_unsafe.js: -------------------------------------------------------------------------------- 1 | app.directive('tdBindHtmlUnsafe', function($compile) { 2 | return { 3 | scope: { 4 | tdBindHtmlUnsafe: '=' 5 | }, 6 | replace: true, 7 | link: function(scope, element, attrs) { 8 | scope.$watch('tdBindHtmlUnsafe', function(value) { 9 | element.html(value); 10 | $compile(element.contents())(scope.$parent); 11 | }); 12 | } 13 | }; 14 | }); -------------------------------------------------------------------------------- /app/assets/javascripts/widgets/exception_tracker/service.js: -------------------------------------------------------------------------------- 1 | app.factory("ExceptionTrackerModel", ["$http", function($http) { 2 | 3 | function getParams(config) { 4 | return { source: config.source, widget_id: config.id }; 5 | } 6 | 7 | function getData(config) { 8 | return $http.get("/api/data_sources/exception_tracker", { params: getParams(config) }); 9 | } 10 | 11 | return { 12 | getData: getData 13 | }; 14 | }]); 15 | -------------------------------------------------------------------------------- /app/assets/javascripts/templates/widgets/alert/show.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 | 5 |
6 |
-------------------------------------------------------------------------------- /app/models/sources/ci/demo.rb: -------------------------------------------------------------------------------- 1 | require 'open-uri' 2 | 3 | module Sources 4 | module Ci 5 | class Demo < Sources::Ci::Base 6 | 7 | # Returns ruby hash: 8 | def get(options = {}) 9 | { 10 | :label => "Demo name", 11 | :last_build_time => Time.now, 12 | :last_build_status => rand(2), 13 | :current_status => rand(2) 14 | } 15 | end 16 | 17 | end 18 | end 19 | end -------------------------------------------------------------------------------- /app/models/sources/number/http_proxy.rb: -------------------------------------------------------------------------------- 1 | # 2 | # Use the "Value Path" setting to select nested value from JSON structure: 3 | # { 4 | # "parent" : { 5 | # "child" : { 6 | # "child2" : "myValue" 7 | # } 8 | # } 9 | # } 10 | # 11 | # Example: parent.child.nestedChild.value 12 | module Sources 13 | module Number 14 | class HttpProxy < Sources::Number::Base 15 | include HttpProxyResolver 16 | 17 | end 18 | end 19 | end -------------------------------------------------------------------------------- /config/initializers/backtrace_silencers.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # You can add backtrace silencers for libraries that you're using but don't wish to see in your backtraces. 4 | # Rails.backtrace_cleaner.add_silencer { |line| line =~ /my_noisy_library/ } 5 | 6 | # You can also remove all the silencers if you're trying to debug a problem that might stem from framework code. 7 | # Rails.backtrace_cleaner.remove_silencers! 8 | -------------------------------------------------------------------------------- /app/models/sources/boolean/http_proxy.rb: -------------------------------------------------------------------------------- 1 | # 2 | # Use the "Value Path" setting to select nested value from JSON structure: 3 | # { 4 | # "parent" : { 5 | # "child" : { 6 | # "child2" : "myValue" 7 | # } 8 | # } 9 | # } 10 | # 11 | # Example: parent.child.nestedChild.value 12 | module Sources 13 | module Boolean 14 | class HttpProxy < Sources::Boolean::Base 15 | include HttpProxyResolver 16 | 17 | end 18 | end 19 | end -------------------------------------------------------------------------------- /spec/javascripts/services/color_factory_spec.js: -------------------------------------------------------------------------------- 1 | describe("ColorFactory", function() { 2 | 3 | var colorFactory = null; 4 | 5 | beforeEach(inject(function(ColorFactory) { 6 | colorFactory = ColorFactory; 7 | })); 8 | 9 | it("returns color hex codes", function() { 10 | expect(colorFactory.get()).toEqual("#DEFFA1"); 11 | expect(colorFactory.get()).toEqual("#6CCC70"); 12 | expect(colorFactory.get()).toEqual("#FF8900"); 13 | }); 14 | 15 | }); -------------------------------------------------------------------------------- /app/models/sources/exception_tracker/demo.rb: -------------------------------------------------------------------------------- 1 | require 'open-uri' 2 | 3 | module Sources 4 | module ExceptionTracker 5 | class Demo < Sources::ExceptionTracker::Base 6 | 7 | # Returns ruby hash: 8 | def get(options = {}) 9 | { 10 | :label => "Demo application", 11 | :last_error_time => Time.now, 12 | :unresolved_errors => rand(10) 13 | } 14 | end 15 | 16 | end 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /app/assets/javascripts/services/widget.js: -------------------------------------------------------------------------------- 1 | app.factory("Widget", ["$resource", function($resource) { 2 | return $resource("/api/dashboards/:dashboard_id/widgets/:id", { id: "@id", dashboard_id: "@dashboard_id" }, 3 | { 4 | 'create': { method: 'POST' }, 5 | 'index': { method: 'GET', isArray: true }, 6 | 'show': { method: 'GET', isArray: false }, 7 | 'update': { method: 'PUT' }, 8 | 'destroy': { method: 'DELETE' } 9 | } 10 | ); 11 | }]); -------------------------------------------------------------------------------- /config/initializers/session_store.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | TeamDashboard::Application.config.session_store :cookie_store, key: '_team_dashboard_session' 4 | 5 | # Use the database for sessions instead of the cookie-based default, 6 | # which shouldn't be used to store highly confidential information 7 | # (create the session table with "rails generate session_migration") 8 | # TeamDashboard::Application.config.session_store :active_record_store 9 | -------------------------------------------------------------------------------- /spec/javascripts/fixtures/widgets/ci/show.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/assets/stylesheets/widgets/meter/style.css.scss: -------------------------------------------------------------------------------- 1 | .meter { 2 | margin-left: 25px; 3 | margin-right: 25px; 4 | margin-top: 30px; 5 | 6 | .label { 7 | font-size: 22px; 8 | line-height: 22px; 9 | 10 | font-weight: bold; 11 | text-shadow: 1px 1px 1px #330, -1px -1px 1px #330; 12 | 13 | background: 0; 14 | color: #ccc; 15 | 16 | width: 200px; 17 | display: block; 18 | margin-left: auto; 19 | margin-right: auto; 20 | text-align: center; 21 | } 22 | } -------------------------------------------------------------------------------- /vendor/assets/javascripts/jquery.timeago.en-short.js: -------------------------------------------------------------------------------- 1 | // English shortened 2 | jQuery.timeago.settings.strings = { 3 | prefixAgo: null, 4 | prefixFromNow: null, 5 | suffixAgo: "ago", 6 | suffixFromNow: "from now", 7 | seconds: "< 1m", 8 | minute: "1m", 9 | minutes: "%dm", 10 | hour: "1h", 11 | hours: "%dh", 12 | day: "1d", 13 | days: "%dd", 14 | month: "1mo", 15 | months: "%dmo", 16 | year: "1yr", 17 | years: "%dyr", 18 | wordSeparator: " ", 19 | numbers: [] 20 | }; 21 | -------------------------------------------------------------------------------- /app/assets/javascripts/templates/widgets/number/show.html: -------------------------------------------------------------------------------- 1 |
2 |
{{data.stringValue}}
3 | 4 |
5 | {{data.label}} 6 |
7 | {{data.secondaryValue | percentage}} 8 |
9 |
10 |
-------------------------------------------------------------------------------- /config/initializers/quiet_assets.rb: -------------------------------------------------------------------------------- 1 | if Rails.env.development? 2 | Rails.application.assets.logger = Logger.new('/dev/null') 3 | Rails::Rack::Logger.class_eval do 4 | def call_with_quiet_assets(env) 5 | previous_level = Rails.logger.level 6 | Rails.logger.level = Logger::ERROR if env['PATH_INFO'] =~ %r{^/assets/} 7 | call_without_quiet_assets(env) 8 | ensure 9 | Rails.logger.level = previous_level 10 | end 11 | alias_method_chain :call, :quiet_assets 12 | end 13 | end -------------------------------------------------------------------------------- /app/assets/javascripts/templates/widgets/example/edit.html: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 | 5 |
6 | 7 |
8 | 10 |
11 | 12 |
13 | 14 |
-------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env rake 2 | # Add your own tasks in files placed in lib/tasks ending in .rake, 3 | # for example lib/tasks/capistrano.rake, and they will automatically be available to Rake. 4 | 5 | require File.expand_path('../config/application', __FILE__) 6 | 7 | TeamDashboard::Application.load_tasks 8 | 9 | unless Rails.env.production? 10 | require 'guard/jasmine/task' 11 | 12 | Guard::JasmineTask.new 13 | Guard::JasmineTask.new(:jasmine_no_server, '-s none') 14 | 15 | task :default => %w[spec guard:jasmine] 16 | end 17 | -------------------------------------------------------------------------------- /config/initializers/secret_token.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Your secret key for verifying the integrity of signed cookies. 4 | # If you change this key, all old signed cookies will become invalid! 5 | # Make sure the secret is at least 30 characters and all random, 6 | # no regular words or you'll be exposed to dictionary attacks. 7 | TeamDashboard::Application.config.secret_token = '65421220ccde7f61a1e51fa17866281a631bb987f82e64b7022886d5ade16ffff176bebf8a63f9cde5b550b0617a6fcc2e2296c4826a1a4e576ff4714ad8f4ef' 8 | -------------------------------------------------------------------------------- /app/assets/javascripts/widgets/example/directive.js: -------------------------------------------------------------------------------- 1 | app.directive("example", ["$http", "ExampleModel", function($http, ExampleModel) { 2 | 3 | function link(scope, element, attrs) { 4 | 5 | function onSuccess(data) { 6 | scope.counter = data.value; 7 | } 8 | 9 | function update() { 10 | return ExampleModel.success(onSuccess); 11 | } 12 | 13 | scope.counter = 0; 14 | scope.init(update); 15 | } 16 | 17 | return { 18 | template: $("#templates-widgets-example-show").html(), 19 | link: link 20 | }; 21 | }]); -------------------------------------------------------------------------------- /app/assets/javascripts/templates/widgets/exception_tracker/show.html: -------------------------------------------------------------------------------- 1 |
2 |
{{data.unresolved_errors}}
3 | 4 |
5 |
{{data.label}}
6 |
7 | last error occurred {{data.last_error_time}} 8 |
9 |
10 |
11 | -------------------------------------------------------------------------------- /app/models/sources/boolean/base.rb: -------------------------------------------------------------------------------- 1 | module Sources 2 | module Boolean 3 | class Base 4 | 5 | def available? 6 | true 7 | end 8 | 9 | def supports_target_browsing? 10 | false 11 | end 12 | 13 | def supports_functions? 14 | false 15 | end 16 | 17 | def fields 18 | [] 19 | end 20 | 21 | # Returns ruby hash: 22 | # * value (boolean) mandatory 23 | # * label (string) optional 24 | def get(options = {}) 25 | end 26 | 27 | end 28 | end 29 | end -------------------------------------------------------------------------------- /config/initializers/wrap_parameters.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | # 3 | # This file contains settings for ActionController::ParamsWrapper which 4 | # is enabled by default. 5 | 6 | # Enable parameter wrapping for JSON. You can disable this by setting :format to an empty array. 7 | ActiveSupport.on_load(:action_controller) do 8 | wrap_parameters format: [:json] 9 | end 10 | 11 | # Disable root element in JSON by default. 12 | ActiveSupport.on_load(:active_record) do 13 | self.include_root_in_json = false 14 | end 15 | -------------------------------------------------------------------------------- /config/routes.rb: -------------------------------------------------------------------------------- 1 | TeamDashboard::Application.routes.draw do 2 | 3 | mount Jasminerice::Engine => '/jasmine' if Rails.env.test? 4 | 5 | namespace :api do 6 | resources :dashboards do 7 | resources :widgets 8 | end 9 | 10 | resources :datapoints_targets, :only => :index 11 | match "data_sources/:kind" => "data_sources#index" 12 | end 13 | 14 | match "dashboards" => "layout#index" 15 | match "dashboards/:id" => "layout#index" 16 | match "about" => "layout#index" 17 | 18 | root :to => 'layout#index' 19 | end 20 | -------------------------------------------------------------------------------- /app/models/sources/number/base.rb: -------------------------------------------------------------------------------- 1 | module Sources 2 | module Number 3 | class Base 4 | 5 | def available? 6 | true 7 | end 8 | 9 | def supports_target_browsing? 10 | false 11 | end 12 | 13 | def supports_functions? 14 | false 15 | end 16 | 17 | def fields 18 | [] 19 | end 20 | 21 | # Returns ruby hash: 22 | # * value (boolean) mandatory 23 | # * label (string) optional 24 | def get(options = {}) 25 | end 26 | 27 | end 28 | end 29 | end -------------------------------------------------------------------------------- /app/assets/javascripts/widgets/graph/service.js: -------------------------------------------------------------------------------- 1 | app.factory("GraphModel", ["$http", "TimeSelector", function($http, TimeSelector) { 2 | 3 | function getParams(config) { 4 | return { 5 | from: TimeSelector.getFrom(config.range), 6 | to: TimeSelector.getCurrent(config.range), 7 | source: config.source, 8 | widget_id: config.id 9 | }; 10 | } 11 | 12 | function getData(config) { 13 | return $http.get("/api/data_sources/datapoints", { params: getParams(config) }); 14 | } 15 | 16 | return { 17 | getData: getData 18 | }; 19 | }]); -------------------------------------------------------------------------------- /app/assets/javascripts/widgets/number/service.js: -------------------------------------------------------------------------------- 1 | app.factory("NumberModel", ["$http", "TimeSelector", function($http, TimeSelector) { 2 | 3 | function getParams(config) { 4 | return { 5 | from: TimeSelector.getFrom(config.range), 6 | to: TimeSelector.getCurrent(config.range), 7 | source: config.source, 8 | widget_id: config.id 9 | }; 10 | } 11 | 12 | function getData(config) { 13 | return $http.get("/api/data_sources/number", { params: getParams(config) }); 14 | } 15 | 16 | return { 17 | getData: getData 18 | }; 19 | }]); -------------------------------------------------------------------------------- /app/assets/javascripts/templates/widget/edit.html.erb: -------------------------------------------------------------------------------- 1 | 4 | 5 | 14 | 15 | 19 | 20 | -------------------------------------------------------------------------------- /app/models/graphite_url_builder.rb: -------------------------------------------------------------------------------- 1 | class GraphiteUrlBuilder 2 | 3 | def initialize(base_url) 4 | @base_url = base_url 5 | end 6 | 7 | def datapoints_url(targets, from, to) 8 | params = { :target => Array(targets), :format => "json", :from => format(from), :until => format(to) } 9 | { :url => "#{@base_url}/render", :params => params } 10 | end 11 | 12 | def metrics_url 13 | "#{@base_url}/metrics/index.json" 14 | end 15 | 16 | def format(timestamp) 17 | time = Time.at(timestamp) 18 | time.strftime("%H:%M_%Y%m%d") 19 | end 20 | 21 | end 22 | -------------------------------------------------------------------------------- /app/models/sources/alert/base.rb: -------------------------------------------------------------------------------- 1 | module Sources 2 | module Alert 3 | class Base 4 | 5 | def available? 6 | true 7 | end 8 | 9 | def supports_target_browsing? 10 | false 11 | end 12 | 13 | def supports_functions? 14 | false 15 | end 16 | 17 | def fields 18 | [] 19 | end 20 | 21 | # Returns ruby hash: 22 | # * value (green,orange,red,blue) mandatory 23 | # * label (alert string) mandatory 24 | def get(options = {}) 25 | end 26 | 27 | end 28 | end 29 | end -------------------------------------------------------------------------------- /app/assets/javascripts/controllers/dashboard_index_ctrl.js: -------------------------------------------------------------------------------- 1 | app.controller("DashboardIndexCtrl", ["$scope", "$rootScope", "$location", "Dashboard", function($scope, $rootScope, $location, Dashboard) { 2 | 3 | $rootScope.resolved = false; 4 | 5 | $scope.dashboards = Dashboard.query(function() { 6 | $rootScope.resolved = true; 7 | }); 8 | 9 | $scope.createDashboard = function() { 10 | var dashboard = new Dashboard({ name: "Undefined name" }); 11 | dashboard.$create(function(data) { 12 | $location.url("/dashboards/" + data.id); 13 | }); 14 | }; 15 | 16 | }]); -------------------------------------------------------------------------------- /app/models/sources/boolean/shell.rb: -------------------------------------------------------------------------------- 1 | module Sources 2 | module Boolean 3 | class Shell < Sources::Boolean::Base 4 | 5 | def fields 6 | [ { :name => "command", :title => "Shell Command", :mandatory => true } ] 7 | end 8 | 9 | def get(options = {}) 10 | widget = Widget.find(options.fetch(:widget_id)) 11 | cmd = widget.settings.fetch(:command) 12 | { :value => execute_command(cmd) } 13 | end 14 | 15 | private 16 | 17 | def execute_command(cmd) 18 | system(cmd) 19 | end 20 | 21 | end 22 | end 23 | end -------------------------------------------------------------------------------- /app/assets/javascripts/directives/contenteditable.js: -------------------------------------------------------------------------------- 1 | app.directive("contenteditable", function() { 2 | return { 3 | require: "ngModel", 4 | link: function(scope, element, attrs, ngModel) { 5 | 6 | function read() { 7 | ngModel.$setViewValue(element.html()); 8 | } 9 | 10 | ngModel.$render = function() { 11 | element.html(ngModel.$viewValue || ""); 12 | }; 13 | 14 | element.bind("blur", function() { 15 | scope.$apply(read); 16 | if (attrs.onChange) scope.$apply(attrs.onChange); 17 | }); 18 | 19 | } 20 | }; 21 | }); -------------------------------------------------------------------------------- /spec/javascripts/directives/contenteditable_spec.js: -------------------------------------------------------------------------------- 1 | describe("contenteditable", function() { 2 | 3 | var compile, rootScope, element; 4 | 5 | beforeEach(inject(function($compile, $rootScope) { 6 | compile = $compile; 7 | rootScope = $rootScope; 8 | element = angular.element('
'); 9 | })); 10 | 11 | it("should render correctly", function() { 12 | compile(element)(rootScope); 13 | rootScope.myContent = 'Hello World'; 14 | rootScope.$apply(); 15 | 16 | expect(element).toHaveText("Hello World"); 17 | }); 18 | 19 | }); -------------------------------------------------------------------------------- /app/assets/javascripts/widgets/boolean/directive.js: -------------------------------------------------------------------------------- 1 | app.directive("boolean", ["BooleanModel", function(BooleanModel) { 2 | 3 | var linkFn = function(scope, element, attrs) { 4 | 5 | function onSuccess(data) { 6 | scope.data = data; 7 | scope.data.label = scope.data.label || scope.widget.label; 8 | } 9 | 10 | function update() { 11 | return BooleanModel.getData(scope.widget).success(onSuccess); 12 | } 13 | 14 | scope.init(update); 15 | }; 16 | 17 | return { 18 | template: $("#templates-widgets-boolean-show").html(), 19 | link: linkFn 20 | }; 21 | }]); -------------------------------------------------------------------------------- /spec/javascripts/fixtures/widgets/number/show.html: -------------------------------------------------------------------------------- 1 | 13 | -------------------------------------------------------------------------------- /app/assets/javascripts/services/color_factory.js: -------------------------------------------------------------------------------- 1 | app.factory("ColorFactory", function() { 2 | 3 | var colorPalette = [ 4 | '#DEFFA1', 5 | // '#D26771', 6 | '#6CCC70', 7 | '#FF8900', 8 | '#A141C5', 9 | '#4A556C', 10 | '#239928' 11 | ]; 12 | 13 | var currentColorIndex = 0; 14 | 15 | var getFn = function() { 16 | if (currentColorIndex >= colorPalette.length-1) { 17 | currentColorIndex = 0; 18 | } 19 | var color = colorPalette[currentColorIndex]; 20 | currentColorIndex++; 21 | return color; 22 | }; 23 | 24 | return { 25 | get: getFn 26 | }; 27 | }); -------------------------------------------------------------------------------- /spec/controllers/datapoints_targets_controller_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | describe Api::DatapointsTargetsController do 4 | describe "#index" do 5 | it "responds successfully using the selected plugin" do 6 | plugin = mock('mock') 7 | plugin.expects(:available_targets).returns(['test1', 'test2']) 8 | Sources.expects(:datapoints_plugin).with('demo').returns(plugin) 9 | get :index, :source => 'demo', :format => :json 10 | 11 | assert_response :success 12 | result = JSON.parse(@response.body) 13 | result.should == ['test1', 'test2'] 14 | end 15 | end 16 | end -------------------------------------------------------------------------------- /config/initializers/inflections.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Add new inflection rules using the following format 4 | # (all these examples are active by default): 5 | # ActiveSupport::Inflector.inflections do |inflect| 6 | # inflect.plural /^(ox)$/i, '\1en' 7 | # inflect.singular /^(ox)en/i, '\1' 8 | # inflect.irregular 'person', 'people' 9 | # inflect.uncountable %w( fish sheep ) 10 | # end 11 | # 12 | # These inflection rules are supported but not enabled by default: 13 | # ActiveSupport::Inflector.inflections do |inflect| 14 | # inflect.acronym 'RESTful' 15 | # end 16 | -------------------------------------------------------------------------------- /spec/javascripts/directives/bind_html_unsafe_spec.js: -------------------------------------------------------------------------------- 1 | describe("BindHtmlUnsafe", function() { 2 | 3 | var compile, rootScope, fixture, element; 4 | 5 | beforeEach(inject(function($compile, $rootScope) { 6 | compile = $compile; 7 | rootScope = $rootScope; 8 | element = angular.element('
'); 9 | })); 10 | 11 | it("should render correctly", function() { 12 | compile(element)(rootScope); 13 | rootScope.myContent = '

Hello World

'; 14 | rootScope.$apply(); 15 | 16 | expect(element.find("p")).toHaveClass("red"); 17 | }); 18 | 19 | }); -------------------------------------------------------------------------------- /spec/javascripts/fixtures/widgets/exception_tracker/show.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See http://help.github.com/ignore-files/ for more about ignoring files. 2 | # 3 | # If you find yourself ignoring temporary files generated by your text editor 4 | # or operating system, you probably want to add a global ignore instead: 5 | # git config --global core.excludesfile ~/.gitignore_global 6 | 7 | # Ignore bundler config 8 | /.bundle 9 | 10 | # Ignore the default SQLite database. 11 | /db/*.sqlite3 12 | 13 | # Ignore all logfiles and tempfiles. 14 | /log/*.log 15 | /tmp 16 | 17 | /public/assets 18 | 19 | .DS_Store 20 | 21 | .jhw-cache 22 | 23 | /config/database.yml 24 | 25 | # Ignore net beans project file 26 | /nbproject 27 | .project 28 | -------------------------------------------------------------------------------- /app/models/sources/exception_tracker/base.rb: -------------------------------------------------------------------------------- 1 | module Sources 2 | module ExceptionTracker 3 | class Base 4 | 5 | def available? 6 | true 7 | end 8 | 9 | def supports_target_browsing? 10 | false 11 | end 12 | 13 | def supports_functions? 14 | false 15 | end 16 | 17 | def fields 18 | [] 19 | end 20 | 21 | # Returns ruby hash: 22 | # * label name of application 23 | # * last_error_time time of last error 24 | # * unresolved_errors number of unresolved errors 25 | def get(options = {}) 26 | end 27 | 28 | end 29 | end 30 | end -------------------------------------------------------------------------------- /spec/javascripts/models/exception_tracker_spec.js: -------------------------------------------------------------------------------- 1 | describe("ExceptionTracker Model", function() { 2 | 3 | var model, httpBackend = null; 4 | beforeEach(inject(function(ExceptionTrackerModel, $httpBackend) { 5 | httpBackend = $httpBackend; 6 | model = ExceptionTrackerModel; 7 | })); 8 | 9 | it("builds url for given source param", function() { 10 | var mockData = { value: "test" }; 11 | httpBackend.expectGET("/api/data_sources/exception_tracker?source=demo").respond(mockData); 12 | 13 | model.getData({ source: "demo" }).success(function (data) { 14 | expect(data).toEqual(mockData); 15 | }); 16 | 17 | httpBackend.flush(); 18 | }); 19 | 20 | }); -------------------------------------------------------------------------------- /Guardfile: -------------------------------------------------------------------------------- 1 | # A sample Guardfile 2 | # More info at https://github.com/guard/guard#readme 3 | 4 | guard 'jasmine' do 5 | watch(%r{spec/javascripts/spec\.(js\.coffee|js|coffee)$}) { "spec/javascripts" } 6 | watch(%r{spec/javascripts/.+_spec\.(js\.coffee|js|coffee)$}) 7 | watch(%r{app/assets/javascripts/(.+?)\.(js|coffee)}) { |m| "spec/javascripts/#{m[1]}_spec.#{m[2]}" } 8 | end 9 | 10 | guard 'jasmine' do 11 | watch(%r{spec/javascripts/spec\.(js\.coffee|js|coffee)$}) { "spec/javascripts" } 12 | watch(%r{spec/javascripts/.+_spec\.(js\.coffee|js|coffee)$}) 13 | watch(%r{app/assets/javascripts/(.+?)\.(js|coffee)}) { |m| "spec/javascripts/#{m[1]}_spec.#{m[2]}" } 14 | end 15 | -------------------------------------------------------------------------------- /app/assets/stylesheets/widgets/boolean/style.css.scss: -------------------------------------------------------------------------------- 1 | .boolean { 2 | 3 | .boolean-value { 4 | float: left; 5 | width: 70px; 6 | height: 60px; 7 | margin-top: 10px; 8 | margin-right: 10px; 9 | } 10 | 11 | .label { 12 | margin-top: 20px; 13 | font-size: 22px; 14 | line-height: 22px; 15 | 16 | font-weight: bold; 17 | text-shadow: 1px 1px 1px #330, -1px -1px 1px #330; 18 | 19 | background: 0; 20 | color: #ccc; 21 | } 22 | 23 | .red { 24 | background: #D26771; 25 | border: 2px solid darken(#D26771, 8%); 26 | } 27 | 28 | .green { 29 | background: #8ec15c; 30 | border: 2px solid darken(#8ec15a, 10%); 31 | } 32 | 33 | } -------------------------------------------------------------------------------- /app/assets/javascripts/templates/widgets/ci/edit.html.erb: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 |
5 | 6 |
7 | 9 |
10 | 11 |
12 | 14 |
15 | 16 |
17 | 18 |
-------------------------------------------------------------------------------- /app/models/demo_helper.rb: -------------------------------------------------------------------------------- 1 | module DemoHelper 2 | extend self 3 | 4 | def generate_datapoints(from, to) 5 | range = to - from 6 | interval = case range 7 | when 60*30 then 10 8 | when 60*60 then 10 9 | when 3600*3 then 10*3 10 | when 3600*6 then 10*6 11 | when 3600*12 then 10*12 12 | when 3600*24 then 10*24 13 | when 3600*24*3 then 10*12*3 14 | when 3600*24*7 then 10*12*7 15 | when 3600*24*7*4 then 10*12*7*4 16 | else 10 end 17 | 18 | result = [] 19 | timestamp = from 20 | while (timestamp < to) 21 | result << [1+rand(100), timestamp] 22 | timestamp = timestamp + interval*5 #* (1+rand(5)) 23 | end 24 | result 25 | end 26 | end -------------------------------------------------------------------------------- /app/assets/javascripts/templates/widgets/alert/edit.html.erb: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 | 5 |
6 | 7 |
8 | 10 |
11 | 12 |
13 | 15 |
16 | 17 |
18 | 19 |
20 | -------------------------------------------------------------------------------- /app/assets/javascripts/widgets/exception_tracker/directive.js: -------------------------------------------------------------------------------- 1 | app.directive("exceptionTracker", ["ExceptionTrackerModel", function(ExceptionTrackerModel) { 2 | 3 | var linkFn = function(scope, element, attrs) { 4 | 5 | function onSuccess(data) { 6 | scope.data = data; 7 | scope.data.label = scope.data.label || scope.widget.label; 8 | 9 | scope.data.color = scope.data.unresolved_errors === 0 ? "color-up" : "color-down"; 10 | } 11 | 12 | function update() { 13 | return ExceptionTrackerModel.getData(scope.widget).success(onSuccess); 14 | } 15 | 16 | scope.init(update); 17 | }; 18 | 19 | return { 20 | template: $("#templates-widgets-exception-tracker-show").html(), 21 | link: linkFn 22 | }; 23 | }]); -------------------------------------------------------------------------------- /public/500.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | We're sorry, but something went wrong (500) 5 | 17 | 18 | 19 | 20 | 21 |
22 |

We're sorry, but something went wrong.

23 |
24 | 25 | 26 | -------------------------------------------------------------------------------- /app/models/aggregation.rb: -------------------------------------------------------------------------------- 1 | module Aggregation 2 | extend self 3 | 4 | def aggregated_result(result, aggregate_function) 5 | dps = [] 6 | result.each { |r| dps += r.with_indifferent_access[:datapoints] } 7 | aggregated_result = aggregate(dps, aggregate_function) 8 | end 9 | 10 | def aggregate(dps, aggregate_function) 11 | case aggregate_function.to_sym 12 | when :average 13 | sum = dps.inject(0) { |result, dp| result += dp.first if dp.first; result } 14 | sum / dps.size 15 | when :sum 16 | dps.inject(0) { |result, dp| result += dp.first if dp.first; result } 17 | when :delta 18 | dps.last.first - dps.first.first 19 | else 20 | raise ArgumentError, "Unknown aggregate function: #{aggregate_function}" 21 | end 22 | end 23 | 24 | end -------------------------------------------------------------------------------- /app/assets/javascripts/templates/widget/show.html: -------------------------------------------------------------------------------- 1 |
2 | {{widget.name}} 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 |
13 |
14 |

Request Error: {{widget.message}}

15 |
16 |
17 | -------------------------------------------------------------------------------- /app/assets/stylesheets/widgets/number/style.css.scss: -------------------------------------------------------------------------------- 1 | .number { 2 | 3 | .secondary-value-container { 4 | margin-top: 10px; 5 | } 6 | 7 | .secondary-value { 8 | font-size: 22px; 9 | margin-left: 5px; 10 | 11 | font-weight: bold; 12 | text-shadow: 1px 1px 1px #330, -1px -1px 1px #330; 13 | } 14 | 15 | .arrow-up, .arrow-down { 16 | display: inline-block; 17 | margin-bottom: 5px; 18 | } 19 | 20 | .arrow-up { 21 | width: 0; 22 | height: 0; 23 | border-left: 10px solid transparent; 24 | border-right: 10px solid transparent; 25 | border-bottom: 10px solid #8ec15c; 26 | } 27 | 28 | .arrow-down { 29 | height: 0; 30 | border-left: 10px solid transparent; 31 | border-right: 10px solid transparent; 32 | border-top: 10px solid #D26771; 33 | } 34 | 35 | } -------------------------------------------------------------------------------- /config/locales/simple_form.en.yml: -------------------------------------------------------------------------------- 1 | en: 2 | simple_form: 3 | "yes": 'Yes' 4 | "no": 'No' 5 | required: 6 | text: 'required' 7 | mark: '*' 8 | # You can uncomment the line below if you need to overwrite the whole required html. 9 | # When using html, text and mark won't be used. 10 | # html: '*' 11 | error_notification: 12 | default_message: "Some errors were found, please take a look:" 13 | # Labels and hints examples 14 | # labels: 15 | # password: 'Password' 16 | # user: 17 | # new: 18 | # email: 'E-mail para efetuar o sign in.' 19 | # edit: 20 | # email: 'E-mail.' 21 | # hints: 22 | # username: 'User name to sign in.' 23 | # password: 'No special characters, please.' 24 | 25 | -------------------------------------------------------------------------------- /public/422.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | The change you wanted was rejected (422) 5 | 17 | 18 | 19 | 20 | 21 |
22 |

The change you wanted was rejected.

23 |

Maybe you tried to change something you didn't have access to.

24 |
25 | 26 | 27 | -------------------------------------------------------------------------------- /app/assets/javascripts/templates/widgets/boolean/edit.html.erb: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 | 5 |
6 | 7 |
8 | 10 |
11 | 12 |
13 | 15 |
16 | 17 |
18 | 19 |
20 | 21 |
22 | 23 |
24 | -------------------------------------------------------------------------------- /app/assets/javascripts/templates/widgets/exception_tracker/edit.html: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 | 5 |
6 | 7 |
8 | 10 |
11 | 12 |
13 | 15 |
16 | 17 |
18 | 19 |
20 | 21 |
22 | 23 |
-------------------------------------------------------------------------------- /public/404.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | The page you were looking for doesn't exist (404) 5 | 17 | 18 | 19 | 20 | 21 |
22 |

The page you were looking for doesn't exist.

23 |

You may have mistyped the address or the page may have moved.

24 |
25 | 26 | 27 | -------------------------------------------------------------------------------- /app/models/sources/ci/base.rb: -------------------------------------------------------------------------------- 1 | module Sources 2 | module Ci 3 | class Base 4 | 5 | def available? 6 | true 7 | end 8 | 9 | def supports_target_browsing? 10 | false 11 | end 12 | 13 | def supports_functions? 14 | false 15 | end 16 | 17 | def fields 18 | [] 19 | end 20 | 21 | # Returns ruby hash: 22 | # * label optional label 23 | # * last_build_time time of last build 24 | # * last_build_status last finished build status 25 | # Integer value: 0 (success), 1 (failure), -1 (else) 26 | # * current_status current status 27 | # Integer value: 1 (building), -1 (else) 28 | def get(options = {}) 29 | end 30 | 31 | end 32 | end 33 | end -------------------------------------------------------------------------------- /app/models/sources/datapoints/http_proxy.rb: -------------------------------------------------------------------------------- 1 | module Sources 2 | module Datapoints 3 | class HttpProxy < Sources::Datapoints::Base 4 | include HttpProxyResolver 5 | 6 | def fields 7 | [ 8 | { :name => "proxy_url", :title => "Proxy Url", :mandatory => true } 9 | ] 10 | end 11 | 12 | def get(options = {}) 13 | from = (options[:from]).to_i 14 | to = (options[:to] || Time.now).to_i 15 | widget = Widget.find(options.fetch(:widget_id)) 16 | targets = targetsArray(widget.targets) 17 | 18 | params = { :from => from, :to => to, :targets => targets } 19 | 20 | widget = Widget.find(options.fetch(:widget_id)) 21 | response_body = ::HttpService.request(widget.settings.fetch(:proxy_url), :params => params) 22 | end 23 | 24 | end 25 | end 26 | end -------------------------------------------------------------------------------- /app/assets/stylesheets/widgets/ci/style.css.scss: -------------------------------------------------------------------------------- 1 | .ci { 2 | 3 | .ci-value { 4 | float: left; 5 | margin-right: 10px; 6 | margin-top: 10px; 7 | 8 | width: 70px; 9 | height: 60px; 10 | 11 | &.building { 12 | background-repeat: no-repeat; 13 | background-position: 20px 14px; 14 | &.red { background-image: image-url("spinner-red-bg.gif"); } 15 | &.green { background-image: image-url("spinner-green-bg.gif"); } 16 | &.gray { background-image: image-url("spinner-gray-bg.gif"); } 17 | } 18 | } 19 | 20 | .red { 21 | background: #D26771; 22 | border: 2px solid darken(#D26771, 8%); 23 | } 24 | 25 | .green { 26 | background: #8ec15c; 27 | border: 2px solid darken(#8ec15a, 10%); 28 | } 29 | 30 | .gray { 31 | background: #ccc; 32 | border: 2px solid darken(#ccc, 10%); 33 | } 34 | 35 | } -------------------------------------------------------------------------------- /spec/controllers/data_sources_controller_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | describe Api::DataSourcesController do 4 | describe "#index" do 5 | it "responds successfully using the selected plugin" do 6 | # plugin = mock('mock') 7 | # plugin.expects(:get).returns({ :value => true, :label => "demo" }) 8 | 9 | Sources::Boolean::Demo.any_instance.expects(:get).returns({ :value => true, :label => "demo" }) 10 | # Sources.expects(:plugin_clazz).with("boolean", "demo").returns(Sources::Boolean::Demo) 11 | 12 | # Sources.expects(:boolean_plugin).with('demo').returns(plugin) 13 | get :index, :kind => 'boolean', :source => 'demo', :format => :json 14 | 15 | assert_response :success 16 | result = JSON.parse(@response.body) 17 | assert_equal({ "value" => true, "label" => "demo" }, result) 18 | end 19 | end 20 | end -------------------------------------------------------------------------------- /app/assets/javascripts/widgets/alert/directive.js: -------------------------------------------------------------------------------- 1 | app.directive("alert", ["$window", "AlertModel", function($window, AlertModel){ 2 | 3 | var linkFn = function(scope, element, attrs, WidgetCtrl) { 4 | 5 | scope.showAlertMessage = function() { 6 | var messages = scope.data.label; 7 | var bootboxMessages = messages.replace(/\n/g, '
'); 8 | $window.bootbox.animate(false); 9 | $window.bootbox.alert("

"+bootboxMessages+"

"); 10 | }; 11 | 12 | function onSuccess(data) { 13 | scope.data = data; 14 | scope.data.label = scope.data.label; 15 | } 16 | 17 | function update() { 18 | return AlertModel.getData(scope.widget).success(onSuccess); 19 | } 20 | 21 | scope.init(update); 22 | }; 23 | 24 | return { 25 | template: $("#templates-widgets-alert-show").html(), 26 | link: linkFn 27 | }; 28 | }]); -------------------------------------------------------------------------------- /app/assets/javascripts/templates/dashboards/index.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |

Dashboards

4 |
5 |
6 | 7 |
8 |
9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 23 | 24 | 25 | 26 | 27 |
NameLast ModifiedCreated At
21 | {{dashboard.name}} 22 | {{dashboard.updated_at | date:'medium' }}{{dashboard.created_at | date:'medium' }}
28 | -------------------------------------------------------------------------------- /config/unicorn.rb: -------------------------------------------------------------------------------- 1 | # based on https://devcenter.heroku.com/articles/rails-unicorn 2 | worker_processes Integer(ENV["WEB_CONCURRENCY"] || 3) 3 | preload_app true 4 | timeout 30 5 | listen Integer(ENV["PORT"] || 3000) 6 | 7 | after_fork do |server, worker| 8 | 9 | Signal.trap 'TERM' do 10 | puts 'Unicorn master intercepting TERM and sending myself QUIT instead' 11 | Process.kill 'QUIT', Process.pid 12 | end 13 | 14 | if defined?(ActiveRecord::Base) 15 | ActiveRecord::Base.establish_connection 16 | puts 'Connected to ActiveRecord' 17 | end 18 | end 19 | 20 | before_fork do |server, worker| 21 | 22 | Signal.trap 'TERM' do 23 | puts 'Unicorn worker intercepting TERM and doing nothing. Wait for master to send QUIT' 24 | end 25 | 26 | if defined?(ActiveRecord::Base) 27 | ActiveRecord::Base.connection.disconnect! 28 | puts 'Disconnected from ActiveRecord' 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /app/assets/javascripts/directives/autocomplete.js: -------------------------------------------------------------------------------- 1 | // usage example: 2 | // 3 | app.directive("tdAutocomplete", ["$http", function ($http) { 4 | return { 5 | require: "ngModel", 6 | replace: true, 7 | link: function(scope, element, attrs, ngModel) { 8 | 9 | function query(searchTerm, processCallback) { 10 | var url = scope.$eval(attrs.tdAutocomplete).replace("%QUERY", searchTerm); 11 | $http.get(url).success(function(result) { 12 | processCallback(result); 13 | }); 14 | } 15 | 16 | element.typeahead({ 17 | source: query, 18 | minLength: 3 19 | }); 20 | 21 | function read() { 22 | ngModel.$setViewValue(element.val()); 23 | } 24 | 25 | element.bind("blur change", function() { 26 | scope.$apply(read); 27 | }); 28 | } 29 | }; 30 | }]); -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | gem "rails", "~> 3.2.11" 4 | 5 | gem "jquery-rails", "~> 2.2.1" 6 | gem "less-rails", "~> 2.2.6" 7 | gem "less-rails-bootstrap", "~> 2.3.0" 8 | 9 | gem 'angular-rails' 10 | 11 | gem 'pg' 12 | 13 | gem "unicorn" 14 | gem "foreman" 15 | gem "faraday", "~> 0.8.4" 16 | gem "faraday_middleware", "~> 0.8.8" 17 | gem "multi_xml", "~> 0.5.1" 18 | gem "libxml-ruby", "~> 2.3.3" 19 | gem "nokogiri", "~> 1.5.5" 20 | 21 | group :test, :development do 22 | gem "rspec-rails" 23 | gem "jasmine" 24 | gem "factory_girl_rails" 25 | gem "jasminerice" 26 | gem "guard-jasmine" 27 | gem "rb-fsevent", "~> 0.9.1" 28 | gem "mocha", :require => false 29 | end 30 | 31 | group :assets do 32 | gem "sass-rails", "~> 3.2.5" 33 | 34 | # See https://github.com/sstephenson/execjs#readme for more supported runtimes 35 | gem "therubyracer", "~> 0.10.2" 36 | 37 | gem "uglifier", ">= 1.0.3" 38 | end 39 | -------------------------------------------------------------------------------- /spec/javascripts/services/suffix_formatter_spec.js: -------------------------------------------------------------------------------- 1 | describe("SuffixFormatter", function() { 2 | 3 | var suffixFormatter = null; 4 | 5 | beforeEach(inject(function(SuffixFormatter) { 6 | suffixFormatter = SuffixFormatter; 7 | })); 8 | 9 | it("return number as is if < 1000", function() { 10 | expect(suffixFormatter.format(90, 1)).toEqual("90"); 11 | }); 12 | 13 | it("return number and k if > 1000", function() { 14 | expect(suffixFormatter.format(1090, 1)).toEqual("1k"); 15 | }); 16 | 17 | it("return number and k if > 1000000", function() { 18 | expect(suffixFormatter.format(1000090, 1)).toEqual("1M"); 19 | }); 20 | 21 | it("return number and k if > 1000000000", function() { 22 | expect(suffixFormatter.format(1000000090, 1)).toEqual("1G"); 23 | }); 24 | 25 | it("return number with 2 fixed points if < 1", function() { 26 | expect(suffixFormatter.format(0.009, 1)).toEqual("0.01"); 27 | }); 28 | 29 | }); -------------------------------------------------------------------------------- /app/models/ganglia_url_builder.rb: -------------------------------------------------------------------------------- 1 | class GangliaUrlBuilder 2 | 3 | def initialize(base_url) 4 | @base_url = base_url 5 | end 6 | 7 | def datapoints_url(target, from, to) 8 | cluster, host, metric = parse_target(target) 9 | url = "#{@base_url}/graph.php" 10 | params = { :c => cluster, :h => host, :m => metric, :json => 1 } 11 | { :url => url, :params => params.merge(custom_range_params(from, to)) } 12 | end 13 | 14 | def metrics_url(query) 15 | "#{@base_url}/search.php?q=#{query}" 16 | end 17 | 18 | def parse_target(target) 19 | target =~ /(.*)@(.*)\((.*)\)/ 20 | host = $1 21 | cluster = $2 22 | metric = $3 23 | [cluster, host, metric] 24 | end 25 | 26 | def custom_range_params(from, to) 27 | { :r => "custom", :cs => format(from), :ce => format(to) } 28 | end 29 | 30 | def format(timestamp) 31 | time = Time.at(timestamp) 32 | time.strftime("%m/%d/%Y %H:%M") 33 | end 34 | 35 | end -------------------------------------------------------------------------------- /VERSION2_MIGRATION.markdown: -------------------------------------------------------------------------------- 1 | # Migration Guide for Team Dashboard 2 2 | 3 | Team Dashboard 2 has quite a different data model compared to version 1. It includes the following changes: 4 | * `number`, `boolean`, `exception_tracker` and `ci` widget actually consisted of three widgets packaged as a single widget. Instead of a single widget, you will end up with 3 small widgets instead. 5 | * `counter` widget obsoleted, use the `number` widget instead 6 | * simplified attribute names for forms, for example `http_proxy_url` instead `http_proxy-http_proxy_url` 7 | 8 | ## Migrate using the Rake Task 9 | You can get the new version with a single "git pull" since it is already available on master branch. 10 | 11 | Additionally, there is a Rake Task provided to migrate your database. 12 | 13 | First create the new db schema: 14 | 15 | bundle exec rake db:migrate 16 | 17 | then convert your old configuration to the new version: 18 | 19 | bundle exec rake migrate_widgets -------------------------------------------------------------------------------- /spec/models/sources/exception_tracker/errbit_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | describe Sources::ExceptionTracker::Errbit do 4 | 5 | let(:server_url) { "http://localhost" } 6 | let(:api_key) { "12345" } 7 | let(:errbit) { Sources::ExceptionTracker::Errbit.new } 8 | let(:widget) { FactoryGirl.create(:widget, :kind => "exception_tracker", :source => "errbit", :settings => { :server_url => server_url, :api_key => api_key }) } 9 | 10 | describe "#get" do 11 | it "returns a hash" do 12 | time = Time.now 13 | input = { "name" => "name", "last_error_time" => time.iso8601, "unresolved_errors" => 7 } 14 | errbit.expects(:request_stats).with(server_url, api_key).returns(input) 15 | result = errbit.get(:widget_id => widget.id) 16 | result.should == { 17 | :label => "name", 18 | :last_error_time => time.iso8601.to_s, 19 | :unresolved_errors => 7 20 | } 21 | end 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /app/models/http_proxy_resolver.rb: -------------------------------------------------------------------------------- 1 | module HttpProxyResolver 2 | 3 | def fields 4 | [ 5 | { :name => "proxy_url", :title => "Proxy Url", :mandatory => true }, 6 | { :name => "proxy_value_path", :title => "Value Path" } 7 | ] 8 | end 9 | 10 | def get(options = {}) 11 | widget = Widget.find(options.fetch(:widget_id)) 12 | response_body = ::HttpService.request(widget.settings.fetch(:proxy_url)) 13 | value_path = widget.settings.fetch(:proxy_value_path); 14 | 15 | if value_path.present? 16 | result = { :value => resolve_value(response_body, value_path) } 17 | result 18 | else 19 | response_body 20 | end 21 | end 22 | 23 | private 24 | 25 | def resolve_value(document, value_path) 26 | paths = value_path.split("."); 27 | current_element = document 28 | paths.each do |path| 29 | current_element = current_element[path] 30 | end 31 | current_element 32 | end 33 | 34 | end -------------------------------------------------------------------------------- /app/assets/javascripts/directives/widget.js: -------------------------------------------------------------------------------- 1 | app.directive("widget", ["$compile", function($compile) { 2 | 3 | var linkFn = function(scope, element, attrs, gridsterController) { 4 | gridsterController.add(element, scope.widget); 5 | 6 | // TODO: cleanup, an attribute can't be created in the template with expression 7 | var elm = element.find(".widget-content"); 8 | elm.append('
'); 9 | $compile(elm)(scope); 10 | 11 | element.bind("$destroy", function() { 12 | gridsterController.remove(element); 13 | }); 14 | 15 | scope.$watch("widget.size_x", function(newValue, oldValue) { 16 | if (newValue === oldValue) return; 17 | gridsterController.resize(element, newValue, scope.widget.size_y); 18 | }, true); 19 | }; 20 | 21 | return { 22 | require: "^gridster", 23 | controller: "WidgetCtrl", 24 | template: $("#templates-widget-show").html(), 25 | link: linkFn 26 | }; 27 | }]); -------------------------------------------------------------------------------- /app/assets/javascripts/app.js: -------------------------------------------------------------------------------- 1 | var app = angular.module("TeamDashboard", ["ngResource", "ngSanitize", "ui", "ui.bootstrap.modal", "ui.bootstrap.dialog", "ui.bootstrap.transition"]); 2 | 3 | app.config(function($routeProvider, $locationProvider) { 4 | $locationProvider.html5Mode(true); 5 | $routeProvider 6 | .when("/dashboards", { template: $('#templates-dashboards-index').html(), controller: "DashboardIndexCtrl" }) 7 | .when("/dashboards/:id", { template: $('#templates-dashboards-show').html(), controller: "DashboardShowCtrl" }) 8 | .when("/about", { template: $('#templates-abouts-show').html(), controller: "AboutCtrl" }) 9 | .otherwise({ redirectTo: "/dashboards" }); 10 | }); 11 | 12 | 13 | app.config(function($httpProvider) { 14 | $httpProvider.defaults.headers.common['X-CSRF-Token'] = $('meta[name=csrf-token]').attr('content'); 15 | }); 16 | 17 | // use angular/mustache style {{variable}} interpolation 18 | _.templateSettings = { 19 | interpolate : /\{\{(.+?)\}\}/g 20 | }; -------------------------------------------------------------------------------- /app/assets/javascripts/services/suffix_formatter.js: -------------------------------------------------------------------------------- 1 | app.factory("SuffixFormatter", function() { 2 | 3 | // as defined by http://en.wikipedia.org/wiki/Metric_prefix 4 | var formatFn = function(val, digits) { 5 | var negative = val < 0 ? true : false; 6 | val = parseFloat(val); 7 | val = Math.abs(val); 8 | 9 | var result = null; 10 | if (val > 1000000000) { 11 | result = Math.round(val/1000000000) + "G"; 12 | } else if (val >= 1000000) { 13 | result = Math.round(val/1000000) + "M"; 14 | } else if (val >= 1000) { 15 | result = Math.round(val/1000) + "k"; 16 | } else if (val < 1.0) { 17 | result = parseFloat(Math.round(val * 100) / 100).toFixed(2); 18 | } else { 19 | val = val % 1 === 0 ? val.toString() : val.toFixed(digits); 20 | result = val; 21 | } 22 | 23 | if (negative) { 24 | result = "-" + result; 25 | } 26 | 27 | return result; 28 | }; 29 | 30 | return { 31 | format: formatFn 32 | }; 33 | }); -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | # This file is copied to spec/ when you run 'rails generate rspec:install' 2 | ENV["RAILS_ENV"] ||= 'test' 3 | require File.expand_path("../../config/environment", __FILE__) 4 | require 'rspec/rails' 5 | require 'rspec/autorun' 6 | 7 | # Requires supporting ruby files with custom matchers and macros, etc, 8 | # in spec/support/ and its subdirectories. 9 | Dir[Rails.root.join("spec/support/**/*.rb")].each {|f| require f} 10 | 11 | RSpec.configure do |config| 12 | config.mock_with :mocha 13 | 14 | # If you're not using ActiveRecord, or you'd prefer not to run each of your 15 | # examples within a transaction, remove the following line or assign false 16 | # instead of true. 17 | config.use_transactional_fixtures = true 18 | 19 | # If true, the base class of anonymous controllers will be inferred 20 | # automatically. This will be the default behavior in future versions of 21 | # rspec-rails. 22 | config.infer_base_class_for_anonymous_controllers = false 23 | end 24 | -------------------------------------------------------------------------------- /app/assets/javascripts/controllers/main_ctrl.js: -------------------------------------------------------------------------------- 1 | app.controller("MainCtrl", ["$scope", "$rootScope", "$timeout", "$location", function($scope, $rootScope, $timeout, $location) { 2 | 3 | $scope.fullscreen = false; 4 | 5 | BigScreen.onenter = function() { 6 | $scope.fullscreen = true; 7 | $scope.$apply(function() { $(".navbar").slideUp("fast"); }); 8 | $scope.showFullscreenNotification(); 9 | }; 10 | 11 | BigScreen.onexit = function() { 12 | $scope.fullscreen = false; 13 | $scope.$apply(function() { $(".navbar").slideDown("fast"); }); 14 | }; 15 | 16 | $scope.toggleFullscreen = function() { 17 | $scope.fullscreen = !$scope.fullscreen; 18 | if (BigScreen.enabled) { 19 | BigScreen.toggle(); 20 | } 21 | }; 22 | 23 | $scope.showFullscreenNotification = function() { 24 | var el = $('#fullscreen-notification'); 25 | $scope.$apply(function() { 26 | el.modal('show'); 27 | }); 28 | $timeout(function(){ el.modal('hide'); }, 2000); 29 | }; 30 | 31 | }]); 32 | -------------------------------------------------------------------------------- /app/models/sources/datapoints/demo.rb: -------------------------------------------------------------------------------- 1 | module Sources 2 | module Datapoints 3 | class Demo < Sources::Datapoints::Base 4 | 5 | def supports_target_browsing? 6 | true 7 | end 8 | 9 | def get(options = {}) 10 | from = (options[:from]).to_i 11 | to = (options[:to] || Time.now).to_i 12 | 13 | widget = Widget.find(options.fetch(:widget_id)) 14 | targets = targetsArray(widget.targets) 15 | source = options[:source] 16 | 17 | datapoints = [] 18 | targets.each do |target| 19 | datapoints << { :target => "demo.example1", :datapoints => ::DemoHelper.generate_datapoints(from, to) } 20 | end 21 | datapoints 22 | end 23 | 24 | def available_targets(options = {}) 25 | pattern = options[:pattern] || "" 26 | limit = options[:limit] || 200 27 | 28 | targets = [] 29 | targets << "demo.example1" 30 | targets << "demo.example2" 31 | targets 32 | end 33 | 34 | end 35 | end 36 | end -------------------------------------------------------------------------------- /spec/javascripts/directives/autocomplete_spec.js: -------------------------------------------------------------------------------- 1 | // describe("Autocomplete", function() { 2 | 3 | // var compile, rootScope, httpBackend, mockData, element; 4 | 5 | // beforeEach(inject(function($compile, $rootScope, $httpBackend) { 6 | // compile = $compile; 7 | // rootScope = $rootScope; 8 | // httpBackend = $httpBackend; 9 | // mockData = ["Peter", "John"]; 10 | // rootScope.myUrl = "api/test.json"; 11 | // element = angular.element(''); 12 | // })); 13 | 14 | // it("should render correctly", function() { 15 | 16 | // compile(element)(rootScope); 17 | // rootScope.newTarget = "P"; 18 | // rootScope.$apply(); 19 | // rootScope.newTarget = "Pete"; 20 | // rootScope.$apply(); 21 | // console.log(element.html()) 22 | 23 | // httpBackend.expectGET(rootScope.myUrl).respond(mockData); 24 | // // element.val("pet"); 25 | // httpBackend.flush(); 26 | 27 | // console.log("element", element); 28 | // }); 29 | // }); -------------------------------------------------------------------------------- /app/assets/javascripts/templates/widgets/meter/edit.html: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 | 5 |
6 | 7 |
8 | 10 |
11 | 12 |
13 | 15 |
16 | 17 |
18 | 19 |
20 | 21 |
22 | 23 |
24 | 25 |
26 | 27 |
28 | 29 |
30 | 31 |
-------------------------------------------------------------------------------- /app/assets/stylesheets/widgets/alert/style.css.scss: -------------------------------------------------------------------------------- 1 | .alert-widget { 2 | 3 | .alert-value { 4 | float: left; 5 | width: 70px; 6 | height: 60px; 7 | margin-top: 10px; 8 | margin-right: 20px; 9 | } 10 | 11 | .label { 12 | 13 | margin-top: 10px; 14 | float: left; 15 | width: 500px; 16 | height: 62px; 17 | 18 | font-size: 19px; 19 | line-height: 15px; 20 | 21 | font-weight: bold; 22 | text-shadow: 1px 1px 1px #330, -1px -1px 1px #330; 23 | 24 | background: 0; 25 | color: #ccc; 26 | 27 | white-space: nowrap; 28 | overflow: hidden; 29 | text-overflow: ellipsis; 30 | } 31 | 32 | .red { 33 | background: #D26771; 34 | border: 2px solid darken(#D26771, 8%); 35 | } 36 | 37 | .green { 38 | background: #8ec15c; 39 | border: 2px solid darken(#8ec15a, 10%); 40 | } 41 | 42 | .blue { 43 | background: #447ab2; 44 | border: 2px solid darken(#2f547b, 10%); 45 | } 46 | 47 | .orange { 48 | background: #f89406; 49 | border: 2px solid darken(#c67605, 10%); 50 | } 51 | 52 | } -------------------------------------------------------------------------------- /spec/models/graphite_url_builder_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | describe GraphiteUrlBuilder do 4 | 5 | before do 6 | @url_builder = GraphiteUrlBuilder.new("http://localhost:3000") 7 | @targets = ['test1', 'test2'] 8 | @from = 1.day.ago.to_i 9 | @until = Time.now.to_i 10 | end 11 | 12 | describe "#format" do 13 | it "formats timestamp" do 14 | @url_builder.format(@from).should eq(Time.at(@from).strftime("%H:%M_%Y%m%d")) 15 | end 16 | end 17 | 18 | describe "#datapoints_url" do 19 | before do 20 | @params = @url_builder.datapoints_url(@targets, @from, @until)[:params] 21 | end 22 | 23 | it "contains json format param" do 24 | @params[:format].should == "json" 25 | end 26 | 27 | it "contains target param" do 28 | @params[:target].should == ["test1", "test2"] 29 | end 30 | 31 | it "contains from param" do 32 | @params[:from].should == @url_builder.format(@from) 33 | end 34 | 35 | it "contains until param" do 36 | @params[:until].should == @url_builder.format(@until) 37 | end 38 | end 39 | 40 | end -------------------------------------------------------------------------------- /app/models/sources/number/datapoints.rb: -------------------------------------------------------------------------------- 1 | module Sources 2 | module Number 3 | class Datapoints < Sources::Number::Base 4 | 5 | def get(options = {}) 6 | widget = Widget.find(options.fetch(:widget_id)) 7 | datapoints_source = widget.settings.fetch(:datapoints_source) 8 | aggregate_function = widget.settings.fetch(:aggregate_function) 9 | 10 | plugin = Sources.plugin_clazz('datapoints', datapoints_source).new 11 | result = plugin.get(options.merge(:source => datapoints_source)) 12 | 13 | datapoints_for_all_targets = result.map { |r| r[:datapoints] } 14 | datapoints = datapoints_for_all_targets.inject([]) { |result, v| result += v; result } 15 | values = datapoints.map { |dp| dp.last } 16 | 17 | value = case aggregate_function 18 | when "average" 19 | values.inject(0, &:+) / values.size 20 | when "sum" 21 | values.inject(0, &:+) 22 | else 23 | raise "Unsupported aggregate function: #{aggregate_function}" 24 | end 25 | 26 | { value: value } 27 | end 28 | 29 | end 30 | end 31 | end -------------------------------------------------------------------------------- /app/models/sources/datapoints/base.rb: -------------------------------------------------------------------------------- 1 | module Sources 2 | module Datapoints 3 | 4 | class Error < StandardError; end 5 | class NotFoundError < Error; end 6 | 7 | class Base 8 | 9 | def available? 10 | true 11 | end 12 | 13 | def supports_target_browsing? 14 | false 15 | end 16 | 17 | def supports_functions? 18 | false 19 | end 20 | 21 | def fields 22 | [] 23 | end 24 | 25 | def get(options = {}) 26 | end 27 | 28 | protected 29 | 30 | def targetsArray(targets) 31 | targets.split(";").map { |t| t.strip } 32 | end 33 | 34 | @@cache = {} 35 | 36 | def cached_get(key) 37 | return yield if Rails.env.test? 38 | 39 | time = Time.now.to_i 40 | if entry = @@cache[key] 41 | if entry[:time] > 5.minutes.ago.to_i 42 | Rails.logger.info("Sources::Datapoints - CACHE HIT for #{key}") 43 | return entry[:value] 44 | end 45 | end 46 | 47 | value = yield 48 | @@cache[key] = { :time => time, :value => value } 49 | value 50 | end 51 | end 52 | end 53 | end -------------------------------------------------------------------------------- /app/assets/javascripts/widgets/graph/controller.js: -------------------------------------------------------------------------------- 1 | app.controller("GraphCtrl", ["$scope", "$dialog", "EditorFormOptions", "Sources", function($scope, $dialog, EditorFormOptions, Sources) { 2 | 3 | var dialog = $dialog.dialog(); 4 | 5 | var defaults = { 6 | size_x: 2, size_y: 2, 7 | update_interval: 10, 8 | range: "30-minutes", 9 | graph_type: "area" 10 | }; 11 | 12 | if (!$scope.widget.id) { 13 | _.extend($scope.widget, defaults); 14 | } 15 | 16 | $scope.graphTypes = EditorFormOptions.graphTypes; 17 | $scope.aggregate_functions = EditorFormOptions.aggregate_functions; 18 | 19 | $scope.supportsTargetBrowsing = function() { 20 | return Sources.supportsTargetBrowsing($scope.widget); 21 | }; 22 | 23 | $scope.editTargets = function() { 24 | var templateUrl = "/assets/templates/targets/index.html"; 25 | 26 | dialog.targets = $scope.widget.targets; 27 | dialog.open(templateUrl, "TargetsCtrl").then(convertTargetsArrayToString); 28 | }; 29 | 30 | function convertTargetsArrayToString(result) { 31 | $scope.widget.targets = _.map(dialog.$scope.targets, function(t) { 32 | return t.content; 33 | }).join(";"); 34 | } 35 | 36 | }]); -------------------------------------------------------------------------------- /spec/models/aggregation_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | describe Aggregation do 4 | 5 | describe "#aggregated_result" do 6 | 7 | before do 8 | @input = [ 9 | { :target => 'test1', :datapoints => [[1, 123]]}, 10 | { :target => 'test2', :datapoints => [[2, 123]]} 11 | ] 12 | end 13 | 14 | it "aggregates results using sum function" do 15 | Aggregation.aggregated_result(@input, 'sum').should eq(3) 16 | end 17 | 18 | it "aggregates results using average function" do 19 | Aggregation.aggregated_result(@input, 'average').should eq(1) 20 | end 21 | end 22 | 23 | describe "#aggregate" do 24 | 25 | before do 26 | @input = [[1, 123], [2, 123]] 27 | end 28 | 29 | it "aggregates datapoints using sum function" do 30 | Aggregation.aggregate(@input, 'sum').should eq(3) 31 | end 32 | 33 | it "aggregates datapoints using average function" do 34 | Aggregation.aggregate(@input, 'average').should eq(1) 35 | end 36 | 37 | it "aggregates datapoints using delta function" do 38 | input = [[1, 123], [5, 123]] 39 | Aggregation.aggregate(input, 'delta').should eq(4) 40 | end 41 | end 42 | end -------------------------------------------------------------------------------- /app/assets/javascripts/widgets/meter/directive.js: -------------------------------------------------------------------------------- 1 | app.directive("meter", ["NumberModel", function(NumberModel) { 2 | 3 | var linkFn = function(scope, element, attrs) { 4 | var knob = element.find("input"); 5 | 6 | knob.knob(); 7 | 8 | function onSuccess(data) { 9 | scope.data = data; 10 | scope.data.label = scope.data.label || scope.widget.label; 11 | scope.data.min = scope.data.min || scope.widget.min || 0; 12 | scope.data.max = scope.data.max || scope.widget.max || 100; 13 | } 14 | 15 | function update() { 16 | return NumberModel.getData(scope.widget).success(onSuccess); 17 | } 18 | 19 | scope.init(update); 20 | 21 | scope.$watch("data.value", function(newValue, oldValue) { 22 | knob.val(newValue).trigger("change"); 23 | }); 24 | 25 | scope.$watch("data.min", function(newValue, oldValue) { 26 | knob.trigger("configure", { min: newValue }); 27 | }); 28 | 29 | scope.$watch("data.max", function(newValue, oldValue) { 30 | knob.trigger("configure", { max: newValue }); 31 | }); 32 | 33 | }; 34 | 35 | return { 36 | template: $("#templates-widgets-meter-show").html(), 37 | link: linkFn 38 | }; 39 | }]); -------------------------------------------------------------------------------- /app/assets/javascripts/widgets/graph/directive.js: -------------------------------------------------------------------------------- 1 | app.directive("graph", ["FlotrGraphHelper", "GraphModel", function(FlotrGraphHelper, GraphModel) { 2 | 3 | var currentColors = []; 4 | 5 | var linkFn = function(scope, element, attrs) { 6 | 7 | function onSuccess(data) { 8 | element.height(265); 9 | Flotr.draw(element[0], FlotrGraphHelper.transformSeriesOfDatapoints(data, scope.widget, currentColors), FlotrGraphHelper.defaultOptions(scope.widget)); 10 | } 11 | 12 | function update() { 13 | return GraphModel.getData(scope.widget).success(onSuccess); 14 | } 15 | 16 | function calculateWidth(size_x) { 17 | var widthMapping = { 1: 290, 2: 630, 3: 965 }; 18 | return widthMapping[size_x]; 19 | } 20 | 21 | scope.init(update); 22 | 23 | // changing the widget config width should redraw flotr2 graph 24 | scope.$watch("config.size_x", function(newValue, oldValue) { 25 | if (newValue !== oldValue) { 26 | element.width(calculateWidth(scope.widget.size_x)); 27 | scope.init(update); 28 | } 29 | 30 | }); 31 | 32 | }; 33 | 34 | return { 35 | template: '
', 36 | link: linkFn 37 | }; 38 | }]); -------------------------------------------------------------------------------- /config/environments/development.rb: -------------------------------------------------------------------------------- 1 | TeamDashboard::Application.configure do 2 | # Settings specified here will take precedence over those in config/application.rb 3 | 4 | # In the development environment your application's code is reloaded on 5 | # every request. This slows down response time but is perfect for development 6 | # since you don't have to restart the web server when you make code changes. 7 | config.cache_classes = false 8 | 9 | # Log error messages when you accidentally call methods on nil. 10 | config.whiny_nils = true 11 | 12 | # Show full error reports and disable caching 13 | config.consider_all_requests_local = true 14 | config.action_controller.perform_caching = false 15 | 16 | # Don't care if the mailer can't send 17 | config.action_mailer.raise_delivery_errors = false 18 | 19 | # Print deprecation notices to the Rails logger 20 | config.active_support.deprecation = :log 21 | 22 | # Only use best-standards-support built into browsers 23 | config.action_dispatch.best_standards_support = :builtin 24 | 25 | # Do not compress assets 26 | config.assets.compress = false 27 | 28 | # Expands the lines which load the assets 29 | config.assets.debug = true 30 | end 31 | -------------------------------------------------------------------------------- /app/controllers/api/dashboards_controller.rb: -------------------------------------------------------------------------------- 1 | module Api 2 | class DashboardsController < BaseController 3 | 4 | def show 5 | dashboard = Dashboard.find(params[:id]) 6 | respond_with dashboard 7 | end 8 | 9 | def index 10 | dashboards = Dashboard.order("NAME ASC").all 11 | respond_with dashboards 12 | end 13 | 14 | def create 15 | input = JSON.parse(request.body.read.to_s) 16 | dashboard = Dashboard.new(input.slice(*Dashboard.accessible_attributes)) 17 | if dashboard.save 18 | render :json => dashboard, :status => :created, :location => api_dashboards_url(dashboard) 19 | else 20 | render :json => dashboard.errors, :status => :unprocessable_entity 21 | end 22 | end 23 | 24 | def update 25 | dashboard = Dashboard.find(params[:id]) 26 | input = JSON.parse(request.body.read.to_s) 27 | if dashboard.update_attributes(input.slice(*Dashboard.accessible_attributes)) 28 | render :json => dashboard 29 | else 30 | render :json => dashboard.errors, :status => :unprocessable_entity 31 | end 32 | end 33 | 34 | def destroy 35 | Dashboard.destroy(params[:id]) 36 | head :no_content 37 | end 38 | 39 | end 40 | end -------------------------------------------------------------------------------- /app/models/sources/number/hockey_app.rb: -------------------------------------------------------------------------------- 1 | # See http://support.hockeyapp.net/kb/api/api-crash-log-description-and-stack-trace for more details 2 | # 3 | # Note, that you have to provide an API token and an app identifier. 4 | # 5 | # Create an api_token here: https://rink.hockeyapp.net/manage/auth_tokens 6 | # 7 | # Authors: Marno Krahmer, Frederik Dietz(@fdietz) 8 | # 9 | module Sources 10 | module Number 11 | class HockeyApp < Sources::Number::Base 12 | 13 | def fields 14 | [ 15 | { :name => "app_identifier", :title => "App Identifier", :mandatory => true }, 16 | { :name => "app_token", :title => "App Token", :mandatory => true } 17 | ] 18 | end 19 | 20 | def get(options = {}) 21 | widget = Widget.find(options.fetch(:widget_id)) 22 | app_identifier = widget.settings.fetch(:app_identifier) 23 | api_token = widget.settings.fetch(:api_token) 24 | 25 | url = "https://rink.hockeyapp.net/api/2/apps/#{app_identifier}/crashes?symbolicated=1&page=1" 26 | Rails.logger.debug("Requesting from #{url} ...") 27 | response = HttpService.request(url, :headers => { "X-HockeyAppToken" => @api_token }) 28 | { :value => response["total_entries"] } 29 | end 30 | end 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /app/assets/javascripts/templates/dashboards/toolbar.html: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 |

{{dashboard.name}}

5 |
6 | 7 |
8 | 9 |
10 | 13 |
14 | 15 |
16 | 17 | 18 | Add Widget... 19 | 20 | 21 | 26 | 27 |
28 | 29 |
30 | 33 |
34 | 35 |
36 | 37 |
-------------------------------------------------------------------------------- /app/assets/javascripts/templates/targets/index.html: -------------------------------------------------------------------------------- 1 | 4 | 25 | 26 |
27 | 28 |
29 | 30 |
31 |
32 | 33 | 34 |
35 | 36 | 40 | -------------------------------------------------------------------------------- /app/assets/javascripts/controllers/widget_ctrl.js: -------------------------------------------------------------------------------- 1 | app.controller("WidgetCtrl", ["$scope", "$element", "$timeout", "$rootScope", function($scope, $element, $timeout, $rootScope) { 2 | 3 | var previousData = null; 4 | var timer = null; 5 | var abortTimer = false; 6 | var updateFunction = null; 7 | 8 | $rootScope.$on('$routeChangeStart', function(ngEvent, route) { 9 | abortTimer = true; 10 | if (timer) $timeout.cancel(timer); 11 | }); 12 | 13 | function onError(response) { 14 | $scope.showError = true; 15 | if (response.status === 0) { 16 | $scope.widget.message = "Could not connect to rails app"; 17 | } else { 18 | $scope.widget.message = response.data.message; 19 | } 20 | } 21 | 22 | function onSuccess(response) { 23 | $scope.showError = false; 24 | 25 | if (response && response.data) $scope.previousData = response.data; 26 | } 27 | 28 | function updateTimer() { 29 | $scope.widget.enableSpinner = false; 30 | 31 | if (!abortTimer) timer = $timeout(startTimer, $scope.widget.update_interval * 200); 32 | } 33 | 34 | function startTimer() { 35 | $scope.widget.enableSpinner = true; 36 | 37 | var result = updateFunction(); 38 | if (result && result.then) { 39 | result.then(onSuccess, onError).then(updateTimer); 40 | } else { 41 | onSuccess(result); 42 | updateTimer(); 43 | } 44 | } 45 | 46 | $scope.init = function(updateFn) { 47 | updateFunction = updateFn; 48 | startTimer(); 49 | }; 50 | 51 | }]); -------------------------------------------------------------------------------- /app/controllers/api/widgets_controller.rb: -------------------------------------------------------------------------------- 1 | module Api 2 | class WidgetsController < BaseController 3 | 4 | def show 5 | widget = Widget.for_dashboard(params[:dashboard_id]).find(params[:id]) 6 | 7 | respond_with(widget) 8 | end 9 | 10 | def index 11 | widgets = Widget.for_dashboard(params[:dashboard_id]).all 12 | respond_with(widgets) 13 | end 14 | 15 | def create 16 | dashboard = Dashboard.find(params[:dashboard_id]) 17 | input = JSON.parse(request.body.read.to_s) 18 | widget = dashboard.widgets.build(Widget.slice_attributes(input)) 19 | if widget.save 20 | render :json => widget, :status => :created, :location => api_dashboard_widget_url(:dashboard_id => dashboard.id, :id => widget.id) 21 | else 22 | render :json => widget.errors, :status => :unprocessable_entity 23 | end 24 | end 25 | 26 | def update 27 | dashboard = Dashboard.find(params[:dashboard_id]) 28 | widget = dashboard.widgets.find(params[:id]) 29 | input = JSON.parse(request.body.read.to_s) 30 | if widget.update_attributes(Widget.slice_attributes(input)) 31 | head :no_content 32 | else 33 | render :json => widget.errors, :status => :unprocessable_entity 34 | end 35 | end 36 | 37 | def destroy 38 | dashboard = Dashboard.find(params[:dashboard_id]) 39 | widget = dashboard.widgets.find(params[:id]) 40 | widget.destroy 41 | head :no_content 42 | end 43 | 44 | end 45 | end -------------------------------------------------------------------------------- /app/models/sources/ci/travis.rb: -------------------------------------------------------------------------------- 1 | require 'open-uri' 2 | 3 | module Sources 4 | module Ci 5 | class Travis < Sources::Ci::Base 6 | 7 | def fields 8 | [ 9 | { :name => "server_url", :title => "Server Url", :mandatory => true }, 10 | { :name => "project", :title => "Project", :mandatory => true }, 11 | ] 12 | end 13 | 14 | # Returns ruby hash: 15 | def get(options = {}) 16 | widget = Widget.find(options.fetch(:widget_id)) 17 | project = widget.settings.fetch(:project) 18 | server_url = widget.settings.fetch(:server_url) 19 | result = request_build_status(server_url, project) 20 | { 21 | :label => result["slug"], 22 | :last_build_time => result["last_build_finished_at"], 23 | :last_build_status => status(result["last_build_status"]), 24 | :current_status => current_status(result["last_build_finished_at"]) 25 | } 26 | end 27 | 28 | def request_build_status(server_url, project) 29 | url = "#{server_url}/#{project}.json" 30 | Rails.logger.debug("Requesting from #{url} ...") 31 | ::HttpService.request(url) 32 | end 33 | 34 | def status(status) 35 | status || -1 36 | end 37 | 38 | def current_status(status) 39 | return 1 if status.blank? 40 | DateTime.parse(status) 41 | 0 42 | rescue ArgumentError 43 | -1 44 | end 45 | 46 | end 47 | end 48 | end -------------------------------------------------------------------------------- /app/assets/stylesheets/theme/index.less: -------------------------------------------------------------------------------- 1 | @import "twitter/bootstrap/reset"; 2 | @import "twitter/bootstrap/variables"; 3 | 4 | @import "theme/variables"; 5 | @import "theme/custom_variables"; 6 | 7 | @import "twitter/bootstrap/mixins"; 8 | @import "theme/mixins"; 9 | 10 | @import "twitter/bootstrap/scaffolding"; 11 | @import "twitter/bootstrap/grid"; 12 | @import "twitter/bootstrap/layouts"; 13 | @import "twitter/bootstrap/type"; 14 | @import "twitter/bootstrap/code"; 15 | @import "twitter/bootstrap/forms"; 16 | @import "twitter/bootstrap/tables"; 17 | @import "twitter/bootstrap/sprites"; 18 | @import "twitter/bootstrap/dropdowns"; 19 | @import "twitter/bootstrap/wells"; 20 | @import "twitter/bootstrap/component-animations"; 21 | @import "twitter/bootstrap/close"; 22 | @import "twitter/bootstrap/buttons"; 23 | @import "twitter/bootstrap/button-groups"; 24 | @import "twitter/bootstrap/alerts"; 25 | @import "twitter/bootstrap/navs"; 26 | @import "twitter/bootstrap/navbar"; 27 | @import "twitter/bootstrap/breadcrumbs"; 28 | @import "twitter/bootstrap/pagination"; 29 | @import "twitter/bootstrap/pager"; 30 | @import "twitter/bootstrap/modals"; 31 | @import "twitter/bootstrap/tooltip"; 32 | @import "twitter/bootstrap/popovers"; 33 | @import "twitter/bootstrap/thumbnails"; 34 | @import "twitter/bootstrap/progress-bars"; 35 | @import "twitter/bootstrap/accordion"; 36 | @import "twitter/bootstrap/carousel"; 37 | @import "twitter/bootstrap/hero-unit"; 38 | @import "twitter/bootstrap/utilities"; 39 | @import "twitter/bootstrap/responsive"; 40 | 41 | 42 | //@import "theme/bootswatch"; 43 | -------------------------------------------------------------------------------- /app/models/sources/alert/demo.rb: -------------------------------------------------------------------------------- 1 | module Sources 2 | module Alert 3 | class Demo < Sources::Alert::Base 4 | 5 | WARNING_MESSAGES = ["Disk is 50% full!", 6 | "Responce time is 30% lower than yesterday", 7 | "Usage ratio increased by 50%", 8 | "CPU workload 48%", 9 | "Database responce time is above the maximal"] 10 | 11 | ERROR_MESSAGES = ["Process XXX is not responding!", 12 | "Can't write to the disk!", 13 | "Web page is down!", 14 | "Jenkins build failed!", 15 | "Data writing aborted!"] 16 | 17 | def available? 18 | true 19 | end 20 | 21 | def get(options = {}) 22 | 23 | rand_value = rand(0..3) 24 | which_message = rand(0..4) 25 | 26 | label = case rand_value 27 | when 0 28 | "System status OK" 29 | when 1 30 | WARNING_MESSAGES[which_message] 31 | when 2 32 | ERROR_MESSAGES[which_message] 33 | else 34 | "Unknown System Status!" 35 | end 36 | 37 | Rails.logger.debug("The value is #{rand_value} and the label is #{label}") 38 | 39 | {:value => rand_value ,:label =>"DemoClient: RandomClient
DemoCheck: RandomCheck
DemoMessage: #{label}

"*5 } 40 | end 41 | 42 | end 43 | end 44 | end -------------------------------------------------------------------------------- /spec/models/widget_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | describe Widget do 4 | 5 | before do 6 | @dashboard = FactoryGirl.create(:dashboard) 7 | end 8 | 9 | describe "#set_defaults" do 10 | it "should initialize size" do 11 | FactoryGirl.build(:widget).size.should == 1 12 | end 13 | 14 | it "should initialize kind" do 15 | FactoryGirl.build(:widget).kind.should == "graph" 16 | end 17 | 18 | it "should initialize range" do 19 | FactoryGirl.build(:widget).range.should == "30-minutes" 20 | end 21 | 22 | it "name attribute is mandatory" do 23 | FactoryGirl.build(:widget, :name => nil).should_not be_valid 24 | end 25 | 26 | it "dashboard_id attribute is mandatory" do 27 | FactoryGirl.build(:widget, :dashboard_id => nil).should_not be_valid 28 | end 29 | end 30 | 31 | describe "#settings" do 32 | describe "#as_json" do 33 | it "flattens settings attributes into widgets attributes" do 34 | widget = FactoryGirl.build(:widget, :name => "name", :settings => { :graph_type => "line" }) 35 | result = widget.as_json 36 | result["name"].should == "name" 37 | result["graph_type"].should == "line" 38 | end 39 | end 40 | 41 | describe "#slice_attributes" do 42 | it "returns settings attributes as nested settings hash" do 43 | input = { :name => "name", :graph_type => "line" } 44 | result = Widget.slice_attributes(input) 45 | result[:name].should == "name" 46 | result[:settings][:graph_type].should == "line" 47 | end 48 | end 49 | end 50 | end -------------------------------------------------------------------------------- /spec/models/sources/ci/jenkins_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | describe Sources::Ci::Jenkins do 4 | 5 | let(:server_url) { "http://localhost" } 6 | let(:project) { "test-build" } 7 | let(:ci) { Sources::Ci::Jenkins.new } 8 | let(:widget) { FactoryGirl.create(:widget, :kind => "ci", :source => "jenkins", :settings => { :server_url => server_url, :project => project }) } 9 | 10 | describe "#get" do 11 | it "returns a hash" do 12 | time = Time.now 13 | input = { "Projects" => { "Project" => [{"name" => project, "lastBuildTime" => time.iso8601, "lastBuildStatus" => "SUCCESS", "activity" => "SLEEPING" }]}} 14 | ci.expects(:request_build_status).with(server_url).returns(input) 15 | result = ci.get(:widget_id => widget.id) 16 | result.should == { 17 | :label => project, 18 | :last_build_time => time.iso8601.to_s, 19 | :last_build_status => 0, 20 | :current_status => 0, 21 | } 22 | end 23 | end 24 | 25 | describe "#request_build_status" do 26 | it "calls HttpService" do 27 | ::HttpService.expects(:request).with("#{server_url}/cc.xml") 28 | ci.request_build_status(server_url) 29 | end 30 | end 31 | 32 | describe "#current_status" do 33 | it "returns 0 for sleeping status" do 34 | ci.current_status('sleeping').should == 0 35 | end 36 | 37 | it "returns 1 for building status" do 38 | ci.current_status('building').should == 1 39 | end 40 | 41 | it "returns -1 otherwise" do 42 | ci.current_status(nil).should == -1 43 | end 44 | end 45 | 46 | end 47 | -------------------------------------------------------------------------------- /config/environments/test.rb: -------------------------------------------------------------------------------- 1 | TeamDashboard::Application.configure do 2 | # Settings specified here will take precedence over those in config/application.rb 3 | 4 | # The test environment is used exclusively to run your application's 5 | # test suite. You never need to work with it otherwise. Remember that 6 | # your test database is "scratch space" for the test suite and is wiped 7 | # and recreated between test runs. Don't rely on the data there! 8 | config.cache_classes = true 9 | 10 | # Configure static asset server for tests with Cache-Control for performance 11 | config.serve_static_assets = true 12 | config.static_cache_control = "public, max-age=3600" 13 | 14 | # Log error messages when you accidentally call methods on nil 15 | config.whiny_nils = true 16 | 17 | # Show full error reports and disable caching 18 | config.consider_all_requests_local = true 19 | config.action_controller.perform_caching = false 20 | 21 | # Raise exceptions instead of rendering exception templates 22 | config.action_dispatch.show_exceptions = false 23 | 24 | # Disable request forgery protection in test environment 25 | config.action_controller.allow_forgery_protection = false 26 | 27 | # Tell Action Mailer not to deliver emails to the real world. 28 | # The :test delivery method accumulates sent emails in the 29 | # ActionMailer::Base.deliveries array. 30 | config.action_mailer.delivery_method = :test 31 | 32 | # Print deprecation notices to the stderr 33 | config.active_support.deprecation = :stderr 34 | 35 | # Expands the lines which load the assets 36 | config.assets.debug = true 37 | end 38 | -------------------------------------------------------------------------------- /spec/javascripts/widgets/meter/directive_spec.js: -------------------------------------------------------------------------------- 1 | describe("meter widget directive", function() { 2 | var element, compile, rootScope, fixture, ctrl, httpBackend; 3 | 4 | beforeEach(inject(function($compile, $rootScope, $controller, $httpBackend) { 5 | compile = $compile; 6 | rootScope = $rootScope; 7 | httpBackend = $httpBackend; 8 | 9 | element = angular.element('
'); 10 | fixture = loadFixtures("widgets/meter/show.html"); 11 | rootScope.widget = { label: "Default Text", source: "demo" }; 12 | ctrl = $controller("WidgetCtrl", { $scope: rootScope, $element: null }); 13 | })); 14 | 15 | it("renders value", function() { 16 | mockData = { value: 10, label: "Hello World" }; 17 | httpBackend.expectGET("/api/data_sources/number?source=demo").respond(mockData); 18 | compile(element)(rootScope); 19 | httpBackend.flush(); 20 | 21 | expect(element.find("input")).toHaveValue(10); 22 | }); 23 | 24 | it("renders label", function() { 25 | mockData = { value: 10, label: "Hello World" }; 26 | httpBackend.expectGET("/api/data_sources/number?source=demo").respond(mockData); 27 | compile(element)(rootScope); 28 | httpBackend.flush(); 29 | 30 | expect(element.find(".label")).toHaveText("Hello World"); 31 | }); 32 | 33 | it("renders default label if none given", function() { 34 | mockData = { value: 10 }; 35 | httpBackend.expectGET("/api/data_sources/number?source=demo").respond(mockData); 36 | compile(element)(rootScope); 37 | httpBackend.flush(); 38 | 39 | expect(element.find(".label")).toHaveText("Default Text"); 40 | }); 41 | 42 | }); 43 | -------------------------------------------------------------------------------- /spec/javascripts/support/jasmine.yml: -------------------------------------------------------------------------------- 1 | # src_files 2 | # 3 | # Return an array of filepaths relative to src_dir to include before jasmine specs. 4 | # Default: [] 5 | # 6 | # EXAMPLE: 7 | # 8 | # src_files: 9 | # - lib/source1.js 10 | # - lib/source2.js 11 | # - dist/**/*.js 12 | # 13 | src_files: 14 | - assets/application.js 15 | 16 | # stylesheets 17 | # 18 | # Return an array of stylesheet filepaths relative to src_dir to include before jasmine specs. 19 | # Default: [] 20 | # 21 | # EXAMPLE: 22 | # 23 | # stylesheets: 24 | # - css/style.css 25 | # - stylesheets/*.css 26 | # 27 | stylesheets: 28 | - assets/stylesheets/**/*.css 29 | 30 | # helpers 31 | # 32 | # Return an array of filepaths relative to spec_dir to include before jasmine specs. 33 | # Default: ["helpers/**/*.js"] 34 | # 35 | # EXAMPLE: 36 | # 37 | # helpers: 38 | # - helpers/**/*.js 39 | # 40 | helpers: 41 | - helpers/**/*.js 42 | 43 | # spec_files 44 | # 45 | # Return an array of filepaths relative to spec_dir to include. 46 | # Default: ["**/*[sS]pec.js"] 47 | # 48 | # EXAMPLE: 49 | # 50 | # spec_files: 51 | # - **/*[sS]pec.js 52 | # 53 | spec_files: 54 | - '**/*[sS]pec.js' 55 | 56 | # src_dir 57 | # 58 | # Source directory path. Your src_files must be returned relative to this path. Will use root if left blank. 59 | # Default: project root 60 | # 61 | # EXAMPLE: 62 | # 63 | # src_dir: public 64 | # 65 | src_dir: 66 | 67 | # spec_dir 68 | # 69 | # Spec directory path. Your spec_files must be returned relative to this path. 70 | # Default: spec/javascripts 71 | # 72 | # EXAMPLE: 73 | # 74 | # spec_dir: spec/javascripts 75 | # 76 | spec_dir: spec/javascripts 77 | -------------------------------------------------------------------------------- /app/models/sources.rb: -------------------------------------------------------------------------------- 1 | module Sources 2 | extend self 3 | 4 | class UnknownPluginError < StandardError; end 5 | 6 | TYPES = %w(alert boolean datapoints number ci exception_tracker) 7 | 8 | TYPES.each do |type| 9 | define_method("#{type}_plugin") do |name| 10 | plugin_clazz(type, name).new 11 | end 12 | end 13 | 14 | def sources 15 | result = {} 16 | TYPES.each do |type| 17 | type_result = {} 18 | source_names(type).each do |name| 19 | type_result[name] = source_properties(type, name) 20 | end 21 | result[type] = type_result 22 | end 23 | result 24 | end 25 | 26 | def custom_fields(type) 27 | sources[type] || [] 28 | end 29 | 30 | def plugin_clazz(type, name) 31 | raise ArgumentError, "source name param missing" if name.blank? 32 | "Sources::#{type.camelize}::#{name.camelize}".constantize 33 | rescue NameError => e 34 | raise UnknownPluginError, "Unknown Plugin: #{type} - #{name}: #{e}" 35 | end 36 | 37 | def source_names(type) 38 | path = Rails.root.join("app/models/sources/#{type}") 39 | Dir["#{path}/*"].map { |f| File.basename(f, '.*') }.reject! { |name| name == "base" } 40 | end 41 | 42 | protected 43 | 44 | def source_properties(type, name) 45 | plugin = plugin_clazz(type, name).new 46 | { 47 | "name" => name, 48 | "available" => plugin.available?, 49 | "supports_target_browsing" => plugin.supports_target_browsing?, 50 | "supports_functions" => plugin.supports_functions?, 51 | "fields" => plugin.fields 52 | } 53 | end 54 | 55 | end -------------------------------------------------------------------------------- /spec/models/sources/ci/travis_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | describe Sources::Ci::Travis do 4 | 5 | let(:server_url) { "http://localhost" } 6 | let(:project) { "test-build" } 7 | let(:ci) { Sources::Ci::Travis.new } 8 | let(:widget) { FactoryGirl.create(:widget, :kind => "ci", :source => "travis", :settings => { :server_url => server_url, :project => project }) } 9 | 10 | describe "#get" do 11 | it "returns a hash" do 12 | time = Time.now 13 | input = { "slug" => "name", "last_build_finished_at" => time.iso8601, "last_build_status" => 0, "last_build_finished_at" => time.iso8601 } 14 | ci.expects(:request_build_status).with(server_url, project).returns(input) 15 | result = ci.get(:widget_id => widget.id) 16 | result.should == { 17 | :label => "name", 18 | :last_build_time => time.iso8601.to_s, 19 | :last_build_status => 0, 20 | :current_status => 0, 21 | } 22 | end 23 | end 24 | 25 | describe "#status" do 26 | it "returns 0 for sleeping status" do 27 | ci.status(0).should == 0 28 | end 29 | 30 | it "returns 1 for building status" do 31 | ci.status(1).should == 1 32 | end 33 | 34 | it "returns -1 otherwise" do 35 | ci.status(nil).should == -1 36 | end 37 | end 38 | 39 | describe "#current_status" do 40 | it "returns 0 for sleeping status" do 41 | ci.current_status(Time.now.iso8601).should == 0 42 | end 43 | 44 | it "returns 1 for building status" do 45 | ci.current_status(nil).should == 1 46 | end 47 | 48 | it "returns -1 otherwise" do 49 | ci.current_status("BLA").should == -1 50 | end 51 | end 52 | 53 | end 54 | -------------------------------------------------------------------------------- /HTTP_PROXY.markdown: -------------------------------------------------------------------------------- 1 | # HTTP Proxy Source 2 | 3 | As described in the [data source plugin guide](SOURCE_PLUGINS.markdown) you can easily add your own data source implementions. 4 | 5 | On the other hand you might prefer to offer a service on your server instead. The HTTP proxy source requests data on the server side, the Rails app being the "proxy" of the web app. The JSON format for the specific sources is described below. 6 | 7 | ## HTTP Proxy URL 8 | Since we want to support generic JSON documents as data source for various kinds of widgets we use a simple path notation to support selection of a single value. This path selection is currently supported in the Number and Boolean data source. 9 | 10 | { 11 | "parent" : { 12 | "child" : { 13 | "child2" : "myValue" 14 | } 15 | } 16 | } 17 | 18 | A value path of "parent.child.child2" would resolve "myValue". 19 | 20 | ### Datapoints 21 | The datapoints source supports data for rendering graphs and aggregated values 22 | 23 | [ 24 | { 25 | "target" : "demo.example", 26 | "datapoints" : [ 27 | [1,123456], [7,23466] 28 | ] 29 | }, 30 | { 31 | "target" : "demo.example2", 32 | "datapoints" : [ 33 | [-6,123456], [8,23466] 34 | ] 35 | } 36 | ] 37 | 38 | ### Number 39 | The number data source supports a single integer value and an optional label. 40 | 41 | { 42 | "value" : 8, 43 | "label" : "This is an example label" 44 | } 45 | 46 | ### Boolean 47 | The boolean data source supports a single boolean value and an optional label. 48 | 49 | { 50 | "value" : true, 51 | "label" : "This is an example label" 52 | } 53 | -------------------------------------------------------------------------------- /db/schema.rb: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | # This file is auto-generated from the current state of the database. Instead 3 | # of editing this file, please use the migrations feature of Active Record to 4 | # incrementally modify your database, and then regenerate this schema definition. 5 | # 6 | # Note that this schema.rb definition is the authoritative source for your 7 | # database schema. If you need to create the application database on another 8 | # system, you should be using db:schema:load, not running all the migrations 9 | # from scratch. The latter is a flawed and unsustainable approach (the more migrations 10 | # you'll amass, the slower it'll run and the greater likelihood for issues). 11 | # 12 | # It's strongly recommended to check this file into your version control system. 13 | 14 | ActiveRecord::Schema.define(:version => 20130327120046) do 15 | 16 | create_table "dashboards", :force => true do |t| 17 | t.string "name" 18 | t.string "time" 19 | t.string "layout" 20 | t.datetime "created_at", :null => false 21 | t.datetime "updated_at", :null => false 22 | t.boolean "locked", :default => false 23 | end 24 | 25 | create_table "widgets", :force => true do |t| 26 | t.string "name" 27 | t.string "kind" 28 | t.string "size" 29 | t.string "source" 30 | t.string "targets", :limit => 5000 31 | t.string "range" 32 | t.text "settings" 33 | t.integer "dashboard_id" 34 | t.datetime "created_at", :null => false 35 | t.datetime "updated_at", :null => false 36 | t.integer "update_interval" 37 | t.integer "col" 38 | t.integer "row" 39 | t.integer "size_x" 40 | t.integer "size_y" 41 | end 42 | 43 | end 44 | -------------------------------------------------------------------------------- /app/models/sources/number/new_relic.rb: -------------------------------------------------------------------------------- 1 | # Gives you the "basic" new relic stats as numbers. All mushed into one file so that it 2 | # makes a standalone plugin for the dashboard 3 | # 4 | # Valid Names are 5 | # 6 | # * Apdex 7 | # * Response Time 8 | # * Throughput 9 | # * Memory 10 | # * CPU 11 | # * DB 12 | # 13 | module Sources 14 | module Number 15 | class NewRelic < Sources::Number::Base 16 | 17 | # Internal class for the connection 18 | class NewRelicConnection 19 | 20 | attr_reader :api_key 21 | 22 | def initialize(api_key) 23 | @api_key = api_key 24 | end 25 | 26 | def self.instance(api_key) 27 | @instances ||= {} 28 | @instances[api_key] ||= self.new(api_key) 29 | @instances[api_key] 30 | end 31 | 32 | def account 33 | NewRelicApi.api_key = api_key 34 | @account ||= NewRelicApi::Account.find(:first) 35 | end 36 | 37 | def application 38 | @application ||= account.applications.first 39 | end 40 | 41 | def threshold_value(name) 42 | application.threshold_values.find { |tv| tv.name == name } 43 | end 44 | 45 | end 46 | 47 | def fields 48 | [ 49 | { :name => "api_key", :title => "Api Key", :mandatory => true }, 50 | { :name => "value_name", :title => "Value Name", :mandatory => true }, 51 | ] 52 | end 53 | 54 | def get(options = {}) 55 | widget = Widget.find(options.fetch(:widget_id)) 56 | api_key = widget.settings.fetch(:api_key) 57 | value_name = widget.settings.fetch(:value_name) 58 | 59 | { :value => NewRelicConnection.instance(api_key).threshold_value(value_name).metric_value } 60 | end 61 | 62 | end 63 | end 64 | end -------------------------------------------------------------------------------- /app/assets/javascripts/templates/widgets/graph/edit.html.erb: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 | 5 |
6 | 7 |
8 | 10 |
11 | 12 |
13 | 15 |
16 | 17 |
18 | 20 |
21 | 22 |
23 | 25 |
26 | 27 |
28 | 29 |
30 | 31 |
32 | 34 |
35 | 36 |
37 | 38 |
39 |
40 | 41 | 42 |
43 | 44 | 45 |
46 | 47 |
-------------------------------------------------------------------------------- /app/assets/javascripts/services/editor_form_options.js: -------------------------------------------------------------------------------- 1 | app.factory("EditorFormOptions", function() { 2 | 3 | var updateIntervals = [ 4 | { value: 10, label: "10 sec" }, 5 | { value: 600, label: "1 min" }, 6 | { value: 6000, label: "10 min" }, 7 | { value: 36000, label: "1 hour" } 8 | ]; 9 | 10 | var periods = [ 11 | { value: "30-minutes", label: "Last 30 minutes" }, 12 | { value: "60-minutes", label: "Last 60 minutes" }, 13 | { value: "3-hours", label: "Last 3 hours" }, 14 | { value: "12-hours", label: "Last 12 hours" }, 15 | { value: "24-hours", label: "Last 24 hours" }, 16 | { value: "today", label: "Today" }, 17 | { value: "yesterday", label: "Yesterday" }, 18 | { value: "3-days", label: "Last 3 days" }, 19 | { value: "7-days", label: "Last 7 days" }, 20 | { value: "this-week", label: "This Week" }, 21 | { value: "previous-week", label: "Previous Week" }, 22 | { value: "4-weeks", label: "Last 4 weeks" }, 23 | { value: "this-month", label: "This Month" }, 24 | { value: "previous-month", label: "Previous Month" }, 25 | { value: "this-year", label: "This Year" }, 26 | { value: "previous-year", label: "Previous Year" } 27 | ]; 28 | 29 | var sizes = [ 30 | { value: 1, label: "1 Column" }, 31 | { value: 2, label: "2 Column" }, 32 | { value: 3, label: "3 Column" } 33 | ]; 34 | 35 | var graphTypes = [ 36 | { value: "line", label: "Line Graph" }, 37 | { value: "area", label: "Area Graph" } 38 | ]; 39 | 40 | var aggregate_functions = [ 41 | { value: "sum", label: "Sum" }, 42 | { value: "average", label: "Average" }, 43 | { value: "delta", label: "Delta" } 44 | ]; 45 | 46 | return { 47 | updateIntervals: updateIntervals, 48 | periods: periods, 49 | sizes: sizes, 50 | graphTypes: graphTypes, 51 | aggregate_functions: aggregate_functions 52 | }; 53 | }); -------------------------------------------------------------------------------- /app/models/sources/ci/jenkins.rb: -------------------------------------------------------------------------------- 1 | require 'open-uri' 2 | 3 | module Sources 4 | module Ci 5 | class Jenkins < Sources::Ci::Base 6 | 7 | def fields 8 | [ 9 | { :name => "server_url", :title => "Server Url", :mandatory => true }, 10 | { :name => "project", :title => "Project", :mandatory => true }, 11 | ] 12 | end 13 | 14 | # Returns ruby hash: 15 | def get(options = {}) 16 | widget = Widget.find(options.fetch(:widget_id)) 17 | project = widget.settings.fetch(:project) 18 | server_url = widget.settings.fetch(:server_url) 19 | result = request_build_status(server_url) 20 | result = XML::Parser.string(result).parse rescue result 21 | 22 | result["Projects"]["Project"].each do |data| 23 | if data['name'] == project 24 | return { 25 | :label => data["name"], 26 | :last_build_time => Time.parse(data["lastBuildTime"]), 27 | :last_build_status => status(data["lastBuildStatus"]), 28 | :current_status => current_status(data["activity"]) 29 | } 30 | end 31 | end 32 | end 33 | 34 | def request_build_status(server_url) 35 | url = "#{server_url}/cc.xml" 36 | Rails.logger.debug("Requesting from #{url} ...") 37 | ::HttpService.request(url) 38 | end 39 | 40 | def status(status) 41 | case status 42 | when /success/i 43 | 0 44 | when /failure/i 45 | 1 46 | else 47 | -1 48 | end 49 | end 50 | 51 | def current_status(status) 52 | case status 53 | when /sleeping/i 54 | 0 55 | when /building/i 56 | 1 57 | else 58 | -1 59 | end 60 | end 61 | 62 | end 63 | end 64 | end 65 | -------------------------------------------------------------------------------- /app/assets/javascripts/controllers/targets_ctrl.js: -------------------------------------------------------------------------------- 1 | app.controller("TargetsCtrl", ["$scope", "$timeout", "dialog", "$dialog", function($scope, $timeout, dialog, $dialog) { 2 | 3 | function prefillTargets() { 4 | if (dialog.targets) { 5 | return _.map(dialog.targets.split(";"), function(t) { 6 | return { content: t, editing: false }; 7 | }); 8 | } 9 | 10 | return []; 11 | } 12 | 13 | $scope.targets = prefillTargets(); 14 | if ($scope.targets.length > 0) $scope.selectedTarget = $scope.targets[0]; 15 | 16 | $scope.newTarget = ""; 17 | 18 | $scope.addTarget = function() { 19 | $scope.targets.push({ content: $scope.newTarget, editing: false }); 20 | $scope.selectedTarget = $scope.targets[$scope.targets.length-1]; 21 | $scope.newTarget = ""; 22 | }; 23 | 24 | $scope.removeTarget = function(target) { 25 | _.each($scope.targets, function(t, index) { 26 | if (t === target) { 27 | $scope.targets.splice(index, 1); 28 | if ($scope.selectedTarget === t) { 29 | $scope.selectedTarget = (index > $scope.targets.length-1) ? $scope.targets[$scope.targets.length-1] : $scope.targets[index]; 30 | } 31 | return; 32 | } 33 | }); 34 | 35 | }; 36 | 37 | $scope.selectedClass = function(target) { 38 | return (target === $scope.selectedTarget) ? "selected" : ""; 39 | }; 40 | 41 | function encodedParams(source) { 42 | var result = []; 43 | result.push("source="+encodeURIComponent(source)); 44 | result.push("pattern=%QUERY"); 45 | return result.join("&"); 46 | } 47 | 48 | $scope.searchUrl = function() { 49 | return "/api/datapoints_targets?"+encodedParams("demo"); 50 | }; 51 | 52 | $scope.selectTarget = function(target) { 53 | $scope.selectedTarget = target; 54 | }; 55 | 56 | $scope.cancel = function() { 57 | dialog.close(); 58 | }; 59 | 60 | $scope.save = function() { 61 | dialog.close(); 62 | }; 63 | }]); -------------------------------------------------------------------------------- /app/assets/javascripts/templates/abouts/show.html: -------------------------------------------------------------------------------- 1 |

About Team Dashboard

2 | 3 |

Team Dashboard lets you visualize your team's metrics all in one place.

4 | 5 |

Support

6 | You can get support in our Google Group. If you find a bug or have ideas for new features, please create an issue on github. 7 | 8 |

Documentation

9 |

You can find the most up-to-date information on github.

10 |

For developers there is the Data Source Plugin Guide and the Widget Developers Guide.

11 | 12 |

Open Source

13 |

Team Dashboard was built using lots of open-source libraries including:

14 | 15 | 27 | 28 |

Team Dashboard started as an idea from a XING AG sponsored innovation week project.

29 | 30 |

Copyright 2012 Frederik Dietz fdietz@gmail.com

-------------------------------------------------------------------------------- /vendor/assets/stylesheets/jquery.gridster.css: -------------------------------------------------------------------------------- 1 | /*! gridster.js - v0.1.0 - 2012-10-20 2 | * http://gridster.net/ 3 | * Copyright (c) 2012 ducksboard; Licensed MIT */ 4 | 5 | .gridster { 6 | position:relative; 7 | } 8 | 9 | .gridster > * { 10 | margin: 0 auto; 11 | -webkit-transition: height .4s; 12 | -moz-transition: height .4s; 13 | -o-transition: height .4s; 14 | -ms-transition: height .4s; 15 | transition: height .4s; 16 | } 17 | 18 | .gridster .gs_w{ 19 | z-index: 2; 20 | position: absolute; 21 | } 22 | 23 | .ready .gs_w:not(.preview-holder) { 24 | -webkit-transition: opacity .3s, left .3s, top .3s; 25 | -moz-transition: opacity .3s, left .3s, top .3s; 26 | -o-transition: opacity .3s, left .3s, top .3s; 27 | transition: opacity .3s, left .3s, top .3s; 28 | } 29 | 30 | .ready .gs_w:not(.preview-holder) { 31 | -webkit-transition: opacity .3s, left .3s, top .3s, width .3s, height .3s; 32 | -moz-transition: opacity .3s, left .3s, top .3s, width .3s, height .3s; 33 | -o-transition: opacity .3s, left .3s, top .3s, width .3s, height .3s; 34 | transition: opacity .3s, left .3s, top .3s, width .3s, height .3s; 35 | } 36 | 37 | .gridster .preview-holder { 38 | z-index: 1; 39 | position: absolute; 40 | background-color: #fff; 41 | border-color: #fff; 42 | opacity: 0.3; 43 | } 44 | 45 | .gridster .player-revert { 46 | z-index: 10!important; 47 | -webkit-transition: left .3s, top .3s!important; 48 | -moz-transition: left .3s, top .3s!important; 49 | -o-transition: left .3s, top .3s!important; 50 | transition: left .3s, top .3s!important; 51 | } 52 | 53 | .gridster .dragging { 54 | z-index: 10!important; 55 | -webkit-transition: all 0s !important; 56 | -moz-transition: all 0s !important; 57 | -o-transition: all 0s !important; 58 | transition: all 0s !important; 59 | } 60 | 61 | /* Uncomment this if you set helper : "clone" in draggable options */ 62 | /*.gridster .player { 63 | opacity:0; 64 | }*/ 65 | -------------------------------------------------------------------------------- /spec/javascripts/widgets/boolean/directive_spec.js: -------------------------------------------------------------------------------- 1 | describe("boolean widget directive", function() { 2 | 3 | var element, compile, rootScope, fixture, ctrl, httpBackend; 4 | 5 | beforeEach(inject(function($compile, $rootScope, $controller, $httpBackend) { 6 | compile = $compile; 7 | rootScope = $rootScope; 8 | httpBackend = $httpBackend; 9 | 10 | element = angular.element('
Hello World
'); 11 | fixture = loadFixtures("widgets/boolean/show.html"); 12 | rootScope.widget = { label: "Default Text", source: "demo" }; 13 | ctrl = $controller("WidgetCtrl", { $scope: rootScope, $element: null }); 14 | })); 15 | 16 | it("renders label", function() { 17 | mockData = { value: true, label: "Hello World" }; 18 | httpBackend.expectGET("/api/data_sources/boolean?source=demo").respond(mockData); 19 | compile(element)(rootScope); 20 | httpBackend.flush(); 21 | 22 | expect(element.find(".label")).toHaveText("Hello World"); 23 | }); 24 | 25 | it("renders default label if none given", function() { 26 | mockData = { value: true }; 27 | httpBackend.expectGET("/api/data_sources/boolean?source=demo").respond(mockData); 28 | compile(element)(rootScope); 29 | httpBackend.flush(); 30 | 31 | expect(element.find(".label")).toHaveText("Default Text"); 32 | }); 33 | 34 | it("renders green box if value true", function() { 35 | mockData = { value: true, label: "Hello World" }; 36 | httpBackend.expectGET("/api/data_sources/boolean?source=demo").respond(mockData); 37 | compile(element)(rootScope); 38 | httpBackend.flush(); 39 | 40 | expect(element.find(".boolean-value")).toHaveClass("green"); 41 | }); 42 | 43 | it("renders red box if value false", function() { 44 | mockData = { value: false, label: "Hello World" }; 45 | httpBackend.expectGET("/api/data_sources/boolean?source=demo").respond(mockData); 46 | compile(element)(rootScope); 47 | httpBackend.flush(); 48 | 49 | expect(element.find(".boolean-value")).toHaveClass("red"); 50 | }); 51 | 52 | }); 53 | -------------------------------------------------------------------------------- /app/assets/javascripts/controllers/widget_edit_ctrl.js: -------------------------------------------------------------------------------- 1 | app.controller("WidgetEditCtrl", ["$scope", "$compile", "dialog", "$dialog", "Widget", "EditorFormOptions", "Sources", function($scope, $compile, dialog, $dialog, Widget, EditorFormOptions, Sources) { 2 | 3 | function initWidget() { 4 | var defaults = { 5 | row: null, col: null, 6 | kind: dialog.kind, 7 | dashboard_id: dialog.dashboard.id 8 | }; 9 | 10 | var widget = null; 11 | if (dialog.widget) { 12 | widget = angular.copy(dialog.widget); 13 | } else { 14 | widget = new Widget(defaults); 15 | } 16 | 17 | $scope.template = dialog.editTemplate; 18 | $scope.customFieldsTemplate = dialog.customFieldsTemplate; 19 | 20 | return widget; 21 | } 22 | 23 | $scope.widget = initWidget(); 24 | $scope.updateIntervals = EditorFormOptions.updateIntervals; 25 | $scope.periods = EditorFormOptions.periods; 26 | $scope.sizes = EditorFormOptions.sizes; 27 | $scope.sources = Sources.availableSources($scope.widget.kind); 28 | 29 | function setValidity(field, error) { 30 | $scope.form[field].$dirty = true; 31 | $scope.form[field].$setValidity(error, false); 32 | } 33 | 34 | function handleValidationErrors(response) { 35 | console.log("create error", response); 36 | 37 | _.each(response.data, function(errors, key) { 38 | _.each(errors, function(e) { 39 | setValidity(key, e); 40 | }); 41 | }); 42 | } 43 | 44 | function handleSuccess(data) { 45 | dialog.close(true); 46 | } 47 | 48 | $scope.save = function(widget) { 49 | if ($scope.form.$invalid) return; 50 | 51 | if (widget.id) { 52 | widget.$update(handleSuccess, handleValidationErrors); 53 | } else { 54 | widget.$create(handleSuccess, handleValidationErrors); 55 | } 56 | }; 57 | 58 | $scope.isSaveDisabled = function() { 59 | return $scope.form.$invalid; 60 | }; 61 | 62 | $scope.cancel = function(widget) { 63 | dialog.close(false); 64 | }; 65 | 66 | }]); -------------------------------------------------------------------------------- /app/controllers/api/base_controller.rb: -------------------------------------------------------------------------------- 1 | module Api 2 | class BaseController < ApplicationController 3 | respond_to :json 4 | 5 | rescue_from Exception, :with => :show_internal_server_error 6 | 7 | rescue_from ActiveRecord::RecordNotFound, :with => :show_not_found_error 8 | rescue_from Timeout::Error, :with => :show_timeout_error 9 | rescue_from Errno::ECONNREFUSED, Errno::EHOSTUNREACH, :with => :show_connection_error 10 | rescue_from "Sources::Datapoints::NotFoundError", :with => :show_no_datapoints_available_error 11 | rescue_from "Sources::Datapoints::Error", :with => :show_datapoints_error 12 | 13 | protected 14 | 15 | def show_timeout_error(e) 16 | logger.error("Timeout Error: #{e}") 17 | respond_with({ :message => "Time out error" }.to_json, :status => 500) 18 | end 19 | 20 | def show_connection_error(e) 21 | logger.error("Connection error: #{e}") 22 | respond_with({ :message => "#{e}" }.to_json, :status => 500) 23 | end 24 | 25 | def show_internal_server_error(e) 26 | response = e.response if e.respond_to?(:response) 27 | logger.error(error_log_message(e)) 28 | error_hash = { :message => "Internal server error: #{e}", :response => response } 29 | respond_with(error_hash.to_json, :status => 500) 30 | end 31 | 32 | def show_datapoints_error(e) 33 | logger.error(error_log_message(e)) 34 | error_hash = { :message => "Datapoints Error: #{e}" } 35 | respond_with(error_hash.to_json, :status => 500) 36 | end 37 | 38 | def show_not_found_error(e) 39 | logger.error("Record not found: #{e}") 40 | render :json => { :message => "Record not found error" }, :status => :unprocessable_entity 41 | end 42 | 43 | def show_no_datapoints_available_error(e) 44 | respond_with({ :message => "No datapoints available for query params" }.to_json, :status => 500) 45 | end 46 | 47 | private 48 | 49 | def error_log_message(e) 50 | backtrace = e.backtrace.join("\n") 51 | error = "Internal Server Error: #{e.inspect} \n#{backtrace}" 52 | end 53 | end 54 | end -------------------------------------------------------------------------------- /spec/javascripts/widgets/exception_tracker/directive_spec.js: -------------------------------------------------------------------------------- 1 | describe("exception_tracker widget directive", function() { 2 | 3 | var element, compile, rootScope, fixture, ctrl, httpBackend; 4 | 5 | beforeEach(inject(function($compile, $rootScope, $controller, $httpBackend) { 6 | compile = $compile; 7 | rootScope = $rootScope; 8 | httpBackend = $httpBackend; 9 | 10 | element = angular.element('
hello
'); 11 | fixture = loadFixtures("widgets/exception_tracker/show.html"); 12 | 13 | rootScope.widget = { label: "Default Text", source: "demo" }; 14 | ctrl = $controller("WidgetCtrl", { $scope: rootScope, $element: null }); 15 | })); 16 | 17 | it("renders label", function() { 18 | mockData = { unresolved_errors: 0, label: "Hello World" }; 19 | httpBackend.expectGET("/api/data_sources/exception_tracker?source=demo").respond(mockData); 20 | compile(element)(rootScope); 21 | httpBackend.flush(); 22 | 23 | expect(element.find(".label")).toHaveText("Hello World"); 24 | }); 25 | 26 | it("renders default label if none given", function() { 27 | mockData = { unresolved_errors: 0 }; 28 | httpBackend.expectGET("/api/data_sources/exception_tracker?source=demo").respond(mockData); 29 | compile(element)(rootScope); 30 | httpBackend.flush(); 31 | 32 | expect(element.find(".label")).toHaveText("Default Text"); 33 | }); 34 | 35 | it("renders green box if unresolved_errors equals 0", function() { 36 | mockData = { unresolved_errors: 0, label: "Hello World" }; 37 | httpBackend.expectGET("/api/data_sources/exception_tracker?source=demo").respond(mockData); 38 | compile(element)(rootScope); 39 | httpBackend.flush(); 40 | 41 | expect(element.find(".default-value")).toHaveClass("color-up"); 42 | }); 43 | 44 | it("renders red box if unresolved_errors > 1", function() { 45 | mockData = { unresolved_errors: 1, label: "Hello World" }; 46 | httpBackend.expectGET("/api/data_sources/exception_tracker?source=demo").respond(mockData); 47 | compile(element)(rootScope); 48 | httpBackend.flush(); 49 | 50 | expect(element.find(".default-value")).toHaveClass("color-down"); 51 | }); 52 | 53 | }); 54 | -------------------------------------------------------------------------------- /app/assets/javascripts/directives/td_field.js: -------------------------------------------------------------------------------- 1 | app.directive("tdField", ["$compile", function($compile) { 2 | return { 3 | replace: true, 4 | transclude: true, 5 | scope: { 6 | label: "@", 7 | help: "@" 8 | }, 9 | template: '
' + 10 | '' + 11 | '
' + 12 | '
' + 13 | '
' + 14 | '
{{helpText()}}
' + 15 | '
' + 16 | '
', 17 | link: function(scope, element, attrs) { 18 | var form = element.parent().controller("form"); 19 | var transcludeParent = element.find("div[ng-transclude]"); 20 | 21 | function name() { 22 | return transcludeParent.find("*[name]").attr("name"); 23 | } 24 | 25 | scope.errorClass = function() { 26 | return scope.hasError() ? "error" : ""; 27 | }; 28 | 29 | scope.hasError = function() { 30 | return form[name()].$invalid && form[name()].$dirty; 31 | }; 32 | 33 | var availableErrors = ["required", "minlength", "maxlength", "pattern"]; 34 | var errorMessages = { 35 | "required": "Mandatory input field", 36 | "minlength": "Minimum length required", 37 | "maxlength": "Maximum length reached", 38 | "pattern": "Invalid input" 39 | }; 40 | 41 | // TODO: refactor helpText 42 | scope.helpText = function() { 43 | if (scope.help) { 44 | return scope.help; 45 | } else { 46 | var error = form[name()].$error; 47 | var result = []; 48 | 49 | _.each(error, function(value, key) { 50 | var match = _.find(availableErrors, function(ae) { 51 | if (key === ae) { 52 | return errorMessages[key]; 53 | } 54 | }); 55 | 56 | result.push(match ? match : key); 57 | }); 58 | 59 | if (result) return result.join(", "); 60 | } 61 | }; 62 | 63 | } 64 | }; 65 | }]); -------------------------------------------------------------------------------- /spec/models/sources/datapoints/ganglia_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | describe Sources::Datapoints::Ganglia do 4 | 5 | let(:source) { Sources::Datapoints::Ganglia.new } 6 | let(:widget) { FactoryGirl.create(:widget, :kind => "datapoints", :source => "ganglia", :targets => "test1;test2", :settings => {}) } 7 | let(:targets) { ['test1', 'test2'] } 8 | 9 | before do 10 | @from = 1.day.ago.to_i 11 | @to = Time.now.to_i 12 | end 13 | 14 | describe "#get" do 15 | it "calls request_datapoints" do 16 | input = [[[1, 123]],[[1, 456]]] 17 | source.expects(:request_datapoints).with(targets, @from, @to).returns(input) 18 | result = source.get(:from => @from, :to => @to, :widget_id => widget.id) 19 | result.first["target"].should == "test1" 20 | result.first["datapoints"].should == [[1, 123]] 21 | result.last["target"].should == "test2" 22 | result.last["datapoints"].should == [[1, 456]] 23 | end 24 | end 25 | 26 | describe "#available_targets" do 27 | before do 28 | @input = <<-EOF 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | EOF 41 | end 42 | 43 | it "returns array of target names" do 44 | source.expects(:request_available_targets).returns(@input) 45 | result = source.available_targets 46 | assert result.include?("host1@ENV(metric1)") 47 | assert result.include?("host1@ENV(metric2)") 48 | assert result.include?("host2@ENV(metric1)") 49 | end 50 | 51 | it "searches for given pattern" do 52 | source.expects(:request_available_targets).returns(@input) 53 | result = source.available_targets(:pattern => "metric1") 54 | result.size.should == 2 55 | end 56 | 57 | it "limits returned targets" do 58 | source.expects(:request_available_targets).returns(@input) 59 | result = source.available_targets(:pattern => "metric1", :limit => 1) 60 | result.size.should == 1 61 | end 62 | end 63 | end -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## Team Dashboard 2 Release Candidate 1 4 | * I've replaced Backbone.js and now use Angular.js. Some of you may already know that I'm working on a leanpub book [Recipes with Angular.js](https://leanpub.com/recipes-with-angular-js). So, this shouldn't be really surprising. I don't want to start an emotional discussion about what's the better Javascript MVC Framework, but for me using Angular.js leads to less code which is much easier to maintain and understand. 5 | * Moving to Angular.js I was finally able to make it super straight forward to add new widgets. Read more in the [Widgets Developer Guide](https://github.com/fdietz/team_dashboard/blob/master/WIDGETS.markdown) 6 | * Data source plugin implementations are now simplified too in order to make adding new sources even easier. All data source plugins use a single controller to expose the JSON API to the rails app. Additionally, I've deprecated the plugin repository since there wasn't much going on anyways. Instead I will focus to have good quality plugins shipped with Team Dashboard out of the box. There's a [Data Source Plugin Developer Guide](https://github.com/fdietz/team_dashboard/blob/master/SOURCE_PLUGINS.markdown) available as well. 7 | * The counter and number widgets were combined into the number widget. This was a source of confusion for beginners anyways. Additionally, the number widget optionally displays a metric suffix. 8 | * There is a new meter widget (using jQuery knob) which uses the number data source. 9 | * The graph widget has a max y axis value option which is useful if the autoscaling is confused by very large outliers 10 | * The dashboard uses [gridster.js](http://gridster.net/) instead of [jQuery UI Sortable](http://jqueryui.com/sortable/) which finally makes it possible to have different widget sizes in both dimensions. This also means that all widgets support only a single data source - leading to much simpler configuration and API usage. 11 | * New data source plugins: [Pingdom](https://www.pingdom.com/), Shell script, [Hockey App](http://hockeyapp.net/), [Errbit](https://github.com/errbit/errbit) and [New Relic](http://newrelic.com/) 12 | * A more fine tuned look and feel for the widgets 13 | * Unicorn is the new default rack server. This makes deployment on a free Heroku plan a viable option. 14 | 15 | 16 | -------------------------------------------------------------------------------- /app/models/sources/boolean/pingdom.rb: -------------------------------------------------------------------------------- 1 | # Pingdom Datasource Plug-in 2 | # Version: 1.0 3 | # Date: 22.10.2012 4 | # Implemented by Dragan Mileski 5 | # e-mail: dragan.mileski@gmail.com 6 | 7 | # Find the configuration variables in your application.rb: 8 | # 9 | # config.pingdom_username = ENV['PINGDOM_USERNAME'] 10 | # config.pingdom_password = ENV['PINGDOM_PASSWORD'] 11 | # 12 | module Sources 13 | module Boolean 14 | class Pingdom < Sources::Boolean::Base 15 | 16 | def available? 17 | Rails.configuration.pingdom_username.present? && Rails.configuration.pingdom_password.present? 18 | end 19 | 20 | def fields 21 | [ 22 | { :name => "check_name", :title => "Check name", :mandatory => true } 23 | ] 24 | end 25 | 26 | def get(options = {}) 27 | pingdom_username = Rails.configuration.pingdom_username 28 | pingdom_password = Rails.configuration.pingdom_password 29 | widget = Widget.find(options.fetch(:widget_id)) 30 | check_name = widget.settings.fetch(:check_name) 31 | 32 | check_state = false #It will show false unless the conditions below are fulfilled and the blocks set "check_state" variable to TRUE 33 | 34 | url = "https://#{CGI.escape(pingdom_username)}:#{pingdom_password}@api.pingdom.com/api/2.0/checks" 35 | 36 | response = ::HttpService.request(url, :headers => { 'App-Key' => '9ucbwe7se1uf61l59h2s0zm6ogjzpd7v'} ) 37 | 38 | response["checks"].each do |item| 39 | if item["name"].eql? check_name 40 | case item["status"] 41 | when "up" 42 | check_state = true 43 | else 44 | Rails.logger.debug("\n********************** WARNING **********************") 45 | Rails.logger.debug("SOMETHING IS WRONG WITH YOUR PINGDOM CHECK. PLEASE CHECK YOUR PINGDOM ACCOUNT!!!\n") 46 | raise Sources::Booleans::NotFoundError 47 | end 48 | else 49 | Rails.logger.debug("\n********************** ERROR **********************") 50 | Rails.logger.debug("THE CHECK_NAME YOU ENTERED IS PROBABLY INCORRECT!!!\n") 51 | raise Sources::Booleans::NotFoundError 52 | end 53 | end 54 | { :value => check_state } 55 | end 56 | 57 | end 58 | end 59 | end 60 | -------------------------------------------------------------------------------- /spec/javascripts/helpers/SpecHelper.js: -------------------------------------------------------------------------------- 1 | // initialize all sources, this happens usually in application.html.erb 2 | beforeEach(function() { 3 | $.Sources = JSON.parse('{"boolean":{"demo":{"name":"demo","available":true,"supports_target_browsing":false,"supports_functions":false,"fields":[]},"http_proxy":{"name":"http_proxy","available":true,"supports_target_browsing":false,"supports_functions":false,"fields":[{"name":"http_proxy_url","title":"Proxy Url","mandatory":true},{"name":"value_path","title":"Value Path"}]}},"datapoints":{"ganglia":{"name":"ganglia","available":false,"supports_target_browsing":true,"supports_functions":false,"fields":[]},"graphite":{"name":"graphite","available":false,"supports_target_browsing":true,"supports_functions":true,"fields":[]},"demo":{"name":"demo","available":true,"supports_target_browsing":true,"supports_functions":false,"fields":[]}},"number":{"demo":{"name":"demo","available":true,"supports_target_browsing":false,"supports_functions":false,"fields":[]},"http_proxy":{"name":"http_proxy","available":true,"supports_target_browsing":false,"supports_functions":false,"fields":[{"name":"http_proxy_url","title":"Proxy Url","mandatory":true},{"name":"value_path","title":"Value Path"}]}},"ci":{"demo":{"name":"demo","available":true,"supports_target_browsing":false,"supports_functions":false,"fields":[]},"jenkins":{"name":"jenkins","available":true,"supports_target_browsing":false,"supports_functions":false,"fields":[{"name":"server_url","title":"Server Url","mandatory":true},{"name":"project","title":"Project","mandatory":true}]},"travis":{"name":"travis","available":true,"supports_target_browsing":false,"supports_functions":false,"fields":[{"name":"server_url","title":"Server Url","mandatory":true},{"name":"project","title":"Project","mandatory":true}]}},"exception_tracker":{"demo":{"name":"demo","available":true,"supports_target_browsing":false,"supports_functions":false,"fields":[]},"errbit":{"name":"errbit","available":true,"supports_target_browsing":false,"supports_functions":false,"fields":[{"name":"server_url","title":"Errbit Server Url","mandatory":true},{"name":"api_key","title":"API Key","mandatory":true}]}}}'); 4 | }); 5 | 6 | beforeEach(function() { module("TeamDashboard"); }); 7 | 8 | // remove bootstrap modal backdrop 9 | // since it will render the jasminerice test page all black otherwise 10 | afterEach(function() { 11 | $(".modal-backdrop").remove(); 12 | }); 13 | -------------------------------------------------------------------------------- /app/views/layouts/application.html.erb.bk: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | <%= content_for?(:title) ? yield(:title) : "BrewHub" %> 5 | 6 | 7 | <%= stylesheet_link_tag "application" %> 8 | 9 | <%= csrf_meta_tag %> 10 | 11 | <%= yield(:head) %> 12 | 13 | 14 | 19 | 20 | 21 | 22 | 23 | 24 | 41 | 42 |
43 |
44 |
45 | 46 |
47 | <%= image_tag('spinner2.gif') %>  Loading 48 |
49 | 50 |
51 |
52 |
53 | 54 |
55 |
56 |
57 | 58 |
59 |

Press ESC to exit fullscreen.

60 |
61 | 62 | <%= script_tag_for_all_templates %> 63 | <%= script_tag_for_all_custom_fields %> 64 | 65 | <%= javascript_include_tag "application" %> 66 | 67 | 73 | 74 | 75 | 76 | -------------------------------------------------------------------------------- /spec/javascripts/views/widgets/alert_spec.js: -------------------------------------------------------------------------------- 1 | // describe("Boolean Widget View", function() { 2 | 3 | // describe("render", function() { 4 | // beforeEach(function() { 5 | // this.model = new window.app.models.Widget({ 6 | // name: "widget 1", kind: 'boolean', id: 1, 7 | // source1: "demo1", label1: "demo1", 8 | // source2: "demo2", label2: "demo2", 9 | // source3: "demo3", label3: "demo3" 10 | // }); 11 | 12 | // this.view = new window.app.views.widgets.Boolean({ model: this.model }); 13 | // }); 14 | 15 | // it("renders default html correctly", function() { 16 | // this.view.render(); 17 | // var firstRow = this.view.$(".triple-row:nth-child(1)"); 18 | // var secondRow = this.view.$(".triple-row:nth-child(2)"); 19 | // var thirdRow = this.view.$(".triple-row:nth-child(3)"); 20 | // expect(firstRow).toExist(); 21 | // expect(secondRow).toExist(); 22 | // expect(thirdRow).toExist(); 23 | // }); 24 | // }); 25 | 26 | // describe("update", function() { 27 | // beforeEach(function() { 28 | // this.model = new window.app.models.Widget({ 29 | // name: "widget 1", kind: 'boolean', id: 1, 30 | // source1: "demo1", label1: "demo1" 31 | // }); 32 | // this.view = new window.app.views.widgets.Boolean({ model: this.model }); 33 | // }); 34 | 35 | // it("fetches model again and updates view with red status", function() { 36 | // this.view.render(); 37 | // spyOn($, "ajax").andCallFake(function(options) { 38 | // expect(options.url).toEqual("/api/boolean?source=demo1&include_response_body=false"); 39 | // options.success({ value: false }); 40 | // }); 41 | 42 | // this.view.update(); 43 | // var firstRow = this.view.$(".triple-row:nth-child(1)"); 44 | // expect(firstRow.find(".boolean-value")).toHaveClass("red"); 45 | // }); 46 | 47 | // it("fetches model again and updates view with green status", function() { 48 | // this.view.render(); 49 | // spyOn($, "ajax").andCallFake(function(options) { 50 | // expect(options.url).toEqual("/api/boolean?source=demo1&include_response_body=false"); 51 | // options.success({ value: true }); 52 | // }); 53 | 54 | // this.view.update(); 55 | // var firstRow = this.view.$(".triple-row:nth-child(1)"); 56 | // expect(firstRow.find(".boolean-value")).toHaveClass("green"); 57 | // }); 58 | // }); 59 | // }); -------------------------------------------------------------------------------- /API.markdown: -------------------------------------------------------------------------------- 1 | # Team Dashboard REST API 2 | Of course there's a REST API for accessing the dashboard and widget configuration. 3 | 4 | ## Dashboard 5 | 6 | ### GET /api/dashboards 7 | Retrieve list of all dashboards 8 | 9 | Example: 10 | 11 | curl -H "Accept: application/json" http://localhost:3000/api/dashboards 12 | 13 | ### GET /api/dashboards/id 14 | Retrieve details of specific dashboard 15 | 16 | Example URL: 17 | 18 | curl -H "Accept: application/json" http://localhost:3000/api/dashboards/1 19 | 20 | Example Response: 21 | 22 | { 23 | created_at: 2012-09-05T08:38:09Z 24 | id: 2 25 | layout: [ 26 | 4 27 | 5 28 | 6 29 | 7 30 | ] 31 | name: Example 2 (Counters, Numbers, Boolean and Graph Widgets) 32 | updated_at: 2012-09-05T08:38:10Z 33 | } 34 | 35 | ### POST /api/dashboards 36 | Creates a new dashboard. 37 | 38 | Example: 39 | 40 | curl -v -H "Content-type: application/json" -X POST -d '{ "name": "test" }' http://localhost:3000/api/dashboards 41 | 42 | ### DELETE /api/dashboards/id 43 | Deletes a specific dashboard 44 | 45 | Example: 46 | 47 | curl -X DELETE http://localhost:3000/api/dashboards/1 48 | 49 | ## Widget 50 | 51 | 52 | ### GET /api/dashboards/id/widgets 53 | Retrieve list of all widgets for specific dashboards 54 | 55 | Example: 56 | 57 | curl -H "Accept: application/json" http://localhost:3000/api/dashboards/1/widgets 58 | 59 | ### GET /api/dashboards/id/widgets/id 60 | Retrieve details of specific widgets for specific dashboards 61 | 62 | Example: 63 | 64 | curl -H "Accept: application/json" http://localhost:3000/api/dashboards/1/widgets/1 65 | 66 | Example Response: 67 | 68 | { 69 | created_at: 2012-09-05T11:44:34Z 70 | dashboard_id: 1 71 | id: 9 72 | kind: graph 73 | name: Undefined name 74 | range: 30-minutes 75 | size: 1 76 | source: demo 77 | targets: demo.example1 78 | update_interval: 10 79 | updated_at: 2012-09-05T11:44:34Z 80 | graph_type: line 81 | } 82 | 83 | ### POST /api/dashboards/id/widgets 84 | Creates widget for specific dashboard 85 | 86 | Example: 87 | 88 | curl -v -H "Content-type: application/json" -X POST -d '{ "name": "test", "source": "demo" }' http://localhost:3000/api/dashboards/1/widgets 89 | 90 | 91 | ### DELETE /api/dashboards/id/widgets/id 92 | Deletes specific widget 93 | 94 | Example: 95 | 96 | curl -X DELETE http://localhost:3000/api/dashboards/1/widgets/1 97 | -------------------------------------------------------------------------------- /app/assets/javascripts/directives/gridster.js: -------------------------------------------------------------------------------- 1 | app.directive("gridster", ["Widget", function(Widget) { 2 | 3 | function controllerFn($scope, $element, $attrs) { 4 | var gridster = null; 5 | var draggable = { 6 | stop: function(event, ui) { 7 | saveLayout(); 8 | } 9 | }; 10 | var options = { 11 | widget_margins: [8, 8], 12 | widget_base_dimensions: [320, 150], 13 | min_cols: 4, 14 | avoid_overlapped_widgets: true, 15 | serialize_params: serializeParamsFn, 16 | draggable: draggable 17 | }; 18 | 19 | function serializeParamsFn($w, wgd) { 20 | return { 21 | col: wgd.col, 22 | row: wgd.row, 23 | size_x: wgd.size_x, 24 | size_y: wgd.size_y, 25 | id: $w.find("div").data("id") 26 | }; 27 | } 28 | 29 | function getWidget(id) { 30 | return _.find($scope.widgets, function(w) { 31 | return w.id === id; 32 | }); 33 | } 34 | 35 | function saveLayout() { 36 | var layouts = gridster.serialize_changed(); 37 | console.log("draggable stop layout", $scope.dashboard.id, layouts, $scope.widgets); 38 | 39 | angular.forEach(layouts, function(layout) { 40 | var w = getWidget(layout.id); 41 | w.col = layout.col; 42 | w.row = layout.row; 43 | w.size_x = layout.size_x; 44 | w.size_y = layout.size_y; 45 | w.$update(); 46 | }); 47 | } 48 | 49 | return { 50 | init: function() { 51 | var ul = $element.find("ul"); 52 | gridster = ul.gridster(options).data("gridster"); 53 | }, 54 | add: function(elm, options) { 55 | // ensure col and row are set for new widgets 56 | var pos = gridster.next_position(options.size_x, options.size_y); 57 | if (!options.col && !options.row) { options = _.extend(options, pos); } 58 | gridster.add_widget(elm, options.size_x, options.size_y, options.col, options.row); 59 | }, 60 | remove: function(elm) { 61 | gridster.remove_widget(elm); 62 | }, 63 | resize: function(elm, size_x, size_y) { 64 | gridster.resize_widget(elm, size_x, size_y); 65 | } 66 | }; 67 | } 68 | 69 | return { 70 | restrict: "E", 71 | transclude: true, 72 | replace: true, 73 | template: '
', 74 | controller: controllerFn, 75 | link: function(scope, element, attrs, controller) { 76 | controller.init(element); 77 | } 78 | }; 79 | }]); -------------------------------------------------------------------------------- /vendor/assets/javascripts/angular-ui/bootstrap/modal.js: -------------------------------------------------------------------------------- 1 | angular.module('ui.bootstrap.modal', []).directive('modal', ['$parse',function($parse) { 2 | var backdropEl; 3 | var body = angular.element(document.getElementsByTagName('body')[0]); 4 | var defaultOpts = { 5 | backdrop: true, 6 | escape: true 7 | }; 8 | return { 9 | restrict: 'EA', 10 | link: function(scope, elm, attrs) { 11 | var opts = angular.extend(defaultOpts, scope.$eval(attrs.uiOptions || attrs.bsOptions || attrs.options)); 12 | var shownExpr = attrs.modal || attrs.show; 13 | var setClosed; 14 | 15 | if (attrs.close) { 16 | setClosed = function() { 17 | scope.$apply(attrs.close); 18 | }; 19 | } else { 20 | setClosed = function() { 21 | scope.$apply(function() { 22 | $parse(shownExpr).assign(scope, false); 23 | }); 24 | }; 25 | } 26 | elm.addClass('modal'); 27 | 28 | if (opts.backdrop && !backdropEl) { 29 | backdropEl = angular.element(''); 30 | backdropEl.css('display','none'); 31 | body.append(backdropEl); 32 | } 33 | 34 | function setShown(shown) { 35 | scope.$apply(function() { 36 | model.assign(scope, shown); 37 | }); 38 | } 39 | 40 | function escapeClose(evt) { 41 | if (evt.which === 27) { setClosed(); } 42 | } 43 | function clickClose() { 44 | setClosed(); 45 | } 46 | 47 | function close() { 48 | if (opts.escape) { body.unbind('keyup', escapeClose); } 49 | if (opts.backdrop) { 50 | backdropEl.css('display', 'none').removeClass('in'); 51 | backdropEl.unbind('click', clickClose); 52 | } 53 | elm.css('display', 'none').removeClass('in'); 54 | body.removeClass('modal-open'); 55 | } 56 | function open() { 57 | if (opts.escape) { body.bind('keyup', escapeClose); } 58 | if (opts.backdrop) { 59 | backdropEl.css('display', 'block').addClass('in'); 60 | if(opts.backdrop != "static") { 61 | backdropEl.bind('click', clickClose); 62 | } 63 | } 64 | elm.css('display', 'block').addClass('in'); 65 | body.addClass('modal-open'); 66 | } 67 | 68 | scope.$watch(shownExpr, function(isShown, oldShown) { 69 | if (isShown) { 70 | open(); 71 | } else { 72 | close(); 73 | } 74 | }); 75 | } 76 | }; 77 | }]); -------------------------------------------------------------------------------- /config/environments/production.rb: -------------------------------------------------------------------------------- 1 | TeamDashboard::Application.configure do 2 | # Settings specified here will take precedence over those in config/application.rb 3 | 4 | # Code is not reloaded between requests 5 | config.cache_classes = true 6 | 7 | # Full error reports are disabled and caching is turned on 8 | config.consider_all_requests_local = false 9 | config.action_controller.perform_caching = true 10 | 11 | # Disable Rails's static asset server (Apache or nginx will already do this) 12 | config.serve_static_assets = false 13 | 14 | # Compress JavaScripts and CSS 15 | config.assets.compress = true 16 | 17 | # Don't fallback to assets pipeline if a precompiled asset is missed 18 | config.assets.compile = true 19 | 20 | # Generate digests for assets URLs 21 | config.assets.digest = true 22 | 23 | # Defaults to Rails.root.join("public/assets") 24 | # config.assets.manifest = YOUR_PATH 25 | 26 | # Specifies the header that your server uses for sending files 27 | # config.action_dispatch.x_sendfile_header = "X-Sendfile" # for apache 28 | # config.action_dispatch.x_sendfile_header = 'X-Accel-Redirect' # for nginx 29 | 30 | # Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies. 31 | # config.force_ssl = true 32 | 33 | # See everything in the log (default is :info) 34 | # config.log_level = :debug 35 | 36 | # Prepend all log lines with the following tags 37 | # config.log_tags = [ :subdomain, :uuid ] 38 | 39 | # Use a different logger for distributed setups 40 | # config.logger = ActiveSupport::TaggedLogging.new(SyslogLogger.new) 41 | 42 | # Use a different cache store in production 43 | # config.cache_store = :mem_cache_store 44 | 45 | # Enable serving of images, stylesheets, and JavaScripts from an asset server 46 | # config.action_controller.asset_host = "http://assets.example.com" 47 | 48 | # Precompile additional assets (application.js, application.css, and all non-JS/CSS are already added) 49 | # config.assets.precompile += %w( search.js ) 50 | 51 | # Disable delivery errors, bad email addresses will be ignored 52 | # config.action_mailer.raise_delivery_errors = false 53 | 54 | # Enable threaded mode 55 | # config.threadsafe! 56 | 57 | # Enable locale fallbacks for I18n (makes lookups for any locale fall back to 58 | # the I18n.default_locale when a translation can not be found) 59 | config.i18n.fallbacks = true 60 | 61 | # Send deprecation notices to registered listeners 62 | config.active_support.deprecation = :notify 63 | 64 | # Log the query plan for queries taking more than this (works 65 | # with SQLite, MySQL, and PostgreSQL) 66 | # config.active_record.auto_explain_threshold_in_seconds = 0.5 67 | end 68 | -------------------------------------------------------------------------------- /spec/javascripts/widgets/ci/directive_spec.js: -------------------------------------------------------------------------------- 1 | describe("ci widget directive", function() { 2 | 3 | var element, compile, rootScope, fixture, ctrl, httpBackend; 4 | 5 | beforeEach(inject(function($compile, $rootScope, $controller, $httpBackend) { 6 | compile = $compile; 7 | rootScope = $rootScope; 8 | httpBackend = $httpBackend; 9 | 10 | element = angular.element('
Hello World
'); 11 | fixture = loadFixtures("widgets/ci/show.html"); 12 | rootScope.widget = { label: "Default Text", source: "demo" }; 13 | ctrl = $controller("WidgetCtrl", { $scope: rootScope, $element: null }); 14 | })); 15 | 16 | it("renders label", function() { 17 | mockData = { last_build_status: 0, label: "Hello World" }; 18 | httpBackend.expectGET("/api/data_sources/ci?source=demo").respond(mockData); 19 | compile(element)(rootScope); 20 | httpBackend.flush(); 21 | 22 | expect(element.find(".label")).toHaveText("Hello World"); 23 | }); 24 | 25 | it("renders default label if none given", function() { 26 | mockData = { last_build_status: 0 }; 27 | httpBackend.expectGET("/api/data_sources/ci?source=demo").respond(mockData); 28 | compile(element)(rootScope); 29 | httpBackend.flush(); 30 | 31 | expect(element.find(".label")).toHaveText("Default Text"); 32 | }); 33 | 34 | it("renders green box if last_build_status is 0", function() { 35 | mockData = { last_build_status: 0, label: "Hello World" }; 36 | httpBackend.expectGET("/api/data_sources/ci?source=demo").respond(mockData); 37 | compile(element)(rootScope); 38 | httpBackend.flush(); 39 | 40 | expect(element.find(".ci-value")).toHaveClass("green"); 41 | }); 42 | 43 | it("renders red box if last_build_status is 1", function() { 44 | mockData = { last_build_status: 1, label: "Hello World" }; 45 | httpBackend.expectGET("/api/data_sources/ci?source=demo").respond(mockData); 46 | compile(element)(rootScope); 47 | httpBackend.flush(); 48 | 49 | expect(element.find(".ci-value")).toHaveClass("red"); 50 | }); 51 | 52 | it("renders gray box if last_build_status is -1", function() { 53 | mockData = { last_build_status: 1, label: "Hello World" }; 54 | httpBackend.expectGET("/api/data_sources/ci?source=demo").respond(mockData); 55 | compile(element)(rootScope); 56 | httpBackend.flush(); 57 | 58 | expect(element.find(".ci-value")).toHaveClass("red"); 59 | }); 60 | 61 | it("renders current_status message", function() { 62 | mockData = { last_build_status: 1, current_status: 0, label: "Hello World" }; 63 | httpBackend.expectGET("/api/data_sources/ci?source=demo").respond(mockData); 64 | compile(element)(rootScope); 65 | httpBackend.flush(); 66 | 67 | expect(element.find(".secondary-label")).toHaveText("Sleeping..."); 68 | }); 69 | 70 | }); 71 | -------------------------------------------------------------------------------- /vendor/assets/javascripts/angular-ui/jq.js: -------------------------------------------------------------------------------- 1 | /** 2 | * General-purpose jQuery wrapper. Simply pass the plugin name as the expression. 3 | * 4 | * It is possible to specify a default set of parameters for each jQuery plugin. 5 | * Under the jq key, namespace each plugin by that which will be passed to ui-jq. 6 | * Unfortunately, at this time you can only pre-define the first parameter. 7 | * @example { jq : { datepicker : { showOn:'click' } } } 8 | * 9 | * @param ui-jq {string} The $elm.[pluginName]() to call. 10 | * @param [ui-options] {mixed} Expression to be evaluated and passed as options to the function 11 | * Multiple parameters can be separated by commas 12 | * @param [ui-refresh] {expression} Watch expression and refire plugin on changes 13 | * 14 | * @example 15 | */ 16 | angular.module('ui.directives').directive('uiJq', ['ui.config', '$timeout', function uiJqInjectingFunction(uiConfig, $timeout) { 17 | 18 | return { 19 | restrict: 'A', 20 | compile: function uiJqCompilingFunction(tElm, tAttrs) { 21 | 22 | if (!angular.isFunction(tElm[tAttrs.uiJq])) { 23 | throw new Error('ui-jq: The "' + tAttrs.uiJq + '" function does not exist'); 24 | } 25 | var options = uiConfig.jq && uiConfig.jq[tAttrs.uiJq]; 26 | 27 | return function uiJqLinkingFunction(scope, elm, attrs) { 28 | 29 | var linkOptions = []; 30 | 31 | // If ui-options are passed, merge (or override) them onto global defaults and pass to the jQuery method 32 | if (attrs.uiOptions) { 33 | linkOptions = scope.$eval('[' + attrs.uiOptions + ']'); 34 | if (angular.isObject(options) && angular.isObject(linkOptions[0])) { 35 | linkOptions[0] = angular.extend({}, options, linkOptions[0]); 36 | } 37 | } else if (options) { 38 | linkOptions = [options]; 39 | } 40 | // If change compatibility is enabled, the form input's "change" event will trigger an "input" event 41 | if (attrs.ngModel && elm.is('select,input,textarea')) { 42 | elm.on('change', function() { 43 | elm.trigger('input'); 44 | }); 45 | } 46 | 47 | // Call jQuery method and pass relevant options 48 | function callPlugin() { 49 | $timeout(function() { 50 | elm[attrs.uiJq].apply(elm, linkOptions); 51 | }, 0, false); 52 | } 53 | 54 | // If ui-refresh is used, re-fire the the method upon every change 55 | if (attrs.uiRefresh) { 56 | scope.$watch(attrs.uiRefresh, function(newVal) { 57 | console.log("ui refresh", newVal); 58 | callPlugin(); 59 | }); 60 | } 61 | callPlugin(); 62 | }; 63 | } 64 | }; 65 | }]); 66 | -------------------------------------------------------------------------------- /config/application.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path('../boot', __FILE__) 2 | 3 | require "action_controller/railtie" 4 | require "action_mailer/railtie" 5 | require "active_resource/railtie" 6 | require "active_record/railtie" 7 | require "sprockets/railtie" 8 | 9 | if defined?(Bundler) 10 | # If you precompile assets before deploying to production, use this line 11 | Bundler.require(*Rails.groups(:assets => %w(development test))) 12 | # If you want your assets lazily compiled in production, use this line 13 | # Bundler.require(:default, :assets, Rails.env) 14 | end 15 | 16 | module TeamDashboard 17 | class Application < Rails::Application 18 | # Settings in config/environments/* take precedence over those specified here. 19 | # Application configuration should go into files in config/initializers 20 | # -- all .rb files in that directory are automatically loaded. 21 | 22 | # Custom directories with classes and modules you want to be autoloadable. 23 | # config.autoload_paths += %W(#{config.root}/extras) 24 | 25 | # Only load the plugins named here, in the order given (default is alphabetical). 26 | # :all can be used as a placeholder for all plugins not explicitly named. 27 | # config.plugins = [ :exception_notification, :ssl_requirement, :all ] 28 | 29 | # Set Time.zone default to the specified zone and make Active Record auto-convert to this zone. 30 | # Run "rake -D time" for a list of tasks for finding time zone names. Default is UTC. 31 | # config.time_zone = 'Central Time (US & Canada)' 32 | 33 | # The default locale is :en and all translations from config/locales/*.rb,yml are auto loaded. 34 | # config.i18n.load_path += Dir[Rails.root.join('my', 'locales', '*.{rb,yml}').to_s] 35 | # config.i18n.default_locale = :de 36 | 37 | config.generators do |g| 38 | g.orm :active_record 39 | end 40 | 41 | # Configure the default encoding used in templates for Ruby 1.9. 42 | config.encoding = "utf-8" 43 | 44 | # Configure sensitive parameters which will be filtered from the log file. 45 | config.filter_parameters += [:password] 46 | 47 | # Enable the asset pipeline 48 | config.assets.enabled = true 49 | 50 | # Version of your assets, change this if you want to expire all your assets 51 | config.assets.version = '1.0' 52 | 53 | # change minification options to fix Angular.js dependency injection 54 | config.assets.js_compressor = Sprockets::LazyCompressor.new { Uglifier.new(:mangle => false) } 55 | 56 | config.graphite_url = ENV['GRAPHITE_URL'] 57 | config.ganglia_web_url = ENV['GANGLIA_WEB_URL'] 58 | config.ganglia_host = ENV['GANGLIA_HOST'] 59 | config.pingdom_username = ENV['PINGDOM_USERNAME'] 60 | config.pingdom_password = ENV['PINGDOM_PASSWORD'] 61 | config.sensu_events = ENV['SENSU_EVENTS_URL'] 62 | end 63 | end 64 | -------------------------------------------------------------------------------- /app/helpers/application_helper.rb: -------------------------------------------------------------------------------- 1 | module ApplicationHelper 2 | def humanize_timestamp(timestamp) 3 | Time.at(timestamp) 4 | end 5 | 6 | # TODO: cleanup all!! 7 | 8 | # Performance optimization, since we can't precompile Angular.js templates 9 | # but don't want to use inline templates, we create 29 | EOF 30 | end.join("\n").html_safe 31 | end 32 | 33 | def script_tag_for_all_custom_fields 34 | Widget.list_available.map do |widget| 35 | id = "templates-custom_fields-#{widget}" 36 | <<-EOF 37 | 40 | EOF 41 | end.join("\n").html_safe 42 | end 43 | 44 | def normalized_filename(file) 45 | file.to_s.gsub(".html", "").gsub(".erb", "") 46 | end 47 | 48 | def normalize_template_name(name) 49 | normalized_filename(name.to_s).gsub("/", "-") 50 | end 51 | 52 | def control_group(key, field) 53 | name = "#{field[:name]}" 54 | mandatory = field.fetch(:mandatory, false) 55 | 56 | <<-EOF 57 |
58 | 59 |
60 | EOF 61 | end 62 | 63 | # @source for example: number, ci, etc. 64 | # using ng-switch doesn't work, it breaks the form validation 65 | def control_groups(source) 66 | # TODO: fix mapping 67 | source = source == "graph" ? "datapoints" : source 68 | source = Sources.custom_fields(source) 69 | result = [] 70 | source.each do |key, value| 71 | result << "
" 72 | value["fields"].each do |field| 73 | result << control_group(key, field).html_safe 74 | end 75 | result << "
" 76 | end 77 | 78 | result.join.html_safe 79 | end 80 | end -------------------------------------------------------------------------------- /spec/javascripts/widgets/number/directive_spec.js: -------------------------------------------------------------------------------- 1 | describe("number widget directive", function() { 2 | var element, compile, rootScope, fixture, ctrl, httpBackend; 3 | 4 | beforeEach(inject(function($compile, $rootScope, $controller, $httpBackend) { 5 | compile = $compile; 6 | rootScope = $rootScope; 7 | httpBackend = $httpBackend; 8 | 9 | element = angular.element('
Hello World
'); 10 | fixture = loadFixtures("widgets/number/show.html"); 11 | rootScope.widget = { label: "Default Text", source: "demo" }; 12 | ctrl = $controller("WidgetCtrl", { $scope: rootScope, $element: null }); 13 | })); 14 | 15 | it("renders value", function() { 16 | mockData = { value: 10, label: "Hello World" }; 17 | httpBackend.expectGET("/api/data_sources/number?source=demo").respond(mockData); 18 | compile(element)(rootScope); 19 | httpBackend.flush(); 20 | 21 | expect(element.find(".default-value")).toHaveText("10"); 22 | }); 23 | 24 | it("renders label", function() { 25 | mockData = { value: 10, label: "Hello World" }; 26 | httpBackend.expectGET("/api/data_sources/number?source=demo").respond(mockData); 27 | compile(element)(rootScope); 28 | httpBackend.flush(); 29 | 30 | expect(element.find(".label")).toHaveText("Hello World"); 31 | }); 32 | 33 | it("renders default label if none given", function() { 34 | mockData = { value: 10 }; 35 | httpBackend.expectGET("/api/data_sources/number?source=demo").respond(mockData); 36 | compile(element)(rootScope); 37 | httpBackend.flush(); 38 | 39 | expect(element.find(".label")).toHaveText("Default Text"); 40 | }); 41 | 42 | it("renders arrow-up class if value > 0", function() { 43 | rootScope.previousData = { value: 5 }; 44 | mockData = { value: 10 }; 45 | httpBackend.expectGET("/api/data_sources/number?source=demo").respond(mockData); 46 | compile(element)(rootScope); 47 | httpBackend.flush(); 48 | 49 | expect(element.find(".label")).toHaveText("Default Text"); 50 | expect(element.find(".secondary-value-container span")).toHaveClass("arrow-up"); 51 | }); 52 | 53 | it("renders arrow-down class if value > 0", function() { 54 | rootScope.previousData = { value: 15 }; 55 | mockData = { value: 10 }; 56 | httpBackend.expectGET("/api/data_sources/number?source=demo").respond(mockData); 57 | compile(element)(rootScope); 58 | httpBackend.flush(); 59 | 60 | expect(element.find(".label")).toHaveText("Default Text"); 61 | expect(element.find(".secondary-value-container span")).toHaveClass("arrow-down"); 62 | }); 63 | 64 | it("calculates percentage of change", function() { 65 | rootScope.previousData = { value: 5 }; 66 | mockData = { value: 10 }; 67 | httpBackend.expectGET("/api/data_sources/number?source=demo").respond(mockData); 68 | compile(element)(rootScope); 69 | httpBackend.flush(); 70 | 71 | expect(element.find(".secondary-value")).toHaveText("50 %"); 72 | }); 73 | 74 | }); 75 | -------------------------------------------------------------------------------- /app/assets/javascripts/templates/widgets/number/edit.html.erb: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 | 5 |
6 | 7 |
8 | 10 |
11 | 12 |
13 | 15 |
16 | 17 |
18 | 20 |
21 | 22 |
23 |
24 | 26 |
27 | 28 |
29 | 31 |
32 | 33 |
34 | 36 |
37 | 38 |
39 | 41 |
42 | 43 |
44 |
45 | 46 | 47 |
48 | 49 | 50 |
51 | 52 |
53 | 55 |
56 | 57 |
58 | 59 |
60 |
61 | 62 |
63 | 64 |
65 | 66 |
67 | 68 |
69 |
70 | 73 |
74 |
75 | 76 |
-------------------------------------------------------------------------------- /app/assets/javascripts/controllers/dashboard_show_ctrl.js.erb: -------------------------------------------------------------------------------- 1 | app.controller("DashboardShowCtrl", ["$scope", "$rootScope", "$routeParams", "$location", "$timeout", "$dialog", "$window", "Dashboard", "Widget", function($scope, $rootScope, $routeParams, $location, $timeout, $dialog, $window, Dashboard, Widget) { 2 | 3 | $rootScope.resolved = false; 4 | 5 | $scope.dashboard = Dashboard.get({ id: $routeParams.id }); 6 | $scope.widgets = Widget.query({ dashboard_id: $routeParams.id }, function() { 7 | $rootScope.resolved = true; 8 | }); 9 | 10 | // defined in application.html.erb 11 | $scope.available_widgets = $.available_widgets; 12 | 13 | function saveDashboardChanges() { 14 | $scope.dashboard.$update(function(data) { 15 | console.log(data); 16 | }, function(data) { 17 | console.log("save error", data); 18 | }); 19 | } 20 | 21 | function destroyDashboard() { 22 | $scope.dashboard.$destroy(function() { 23 | $location.url("/dashboards"); 24 | }); 25 | } 26 | 27 | function destroyWidget(widget) { 28 | widget.$destroy(function() { 29 | _.each($scope.widgets, function(w, index) { 30 | if (w.id === widget.id) { 31 | $scope.widgets.splice(index, 1); 32 | return; 33 | } 34 | }); 35 | }); 36 | } 37 | 38 | function replaceWidget(id, widget) { 39 | _.each($scope.widgets, function(w, index) { 40 | if (w.id === id) { 41 | _.extend(w, widget); 42 | return; 43 | } 44 | }); 45 | } 46 | 47 | $scope.addWidget = function(kind) { 48 | var dialog = $dialog.dialog(), 49 | templateUrl = "<%= asset_path('templates/widget/edit.html') %>"; 50 | 51 | dialog.kind = kind; 52 | dialog.dashboard = $scope.dashboard; 53 | dialog.editTemplate = $("#templates-widgets-" + kind + "-edit").html(); 54 | dialog.customFieldsTemplate = $("#templates-custom_fields-" + kind).html(); 55 | 56 | dialog.open(templateUrl, "WidgetEditCtrl").then(function(result) { 57 | if (result) $scope.widgets.push(dialog.$scope.widget); 58 | }); 59 | }; 60 | 61 | $scope.editWidget = function(widget) { 62 | var dialog = $dialog.dialog(), 63 | templateUrl = "<%= asset_path('templates/widget/edit.html') %>"; 64 | 65 | dialog.kind = widget.kind; 66 | dialog.widget = widget; 67 | dialog.dashboard = $scope.dashboard; 68 | dialog.editTemplate = $("#templates-widgets-" + widget.kind + "-edit").html(); 69 | dialog.customFieldsTemplate = $("#templates-custom_fields-" + widget.kind).html(); 70 | 71 | dialog.open(templateUrl, "WidgetEditCtrl").then(function(result) { 72 | if (result) replaceWidget(widget.id, dialog.$scope.widget); 73 | }); 74 | }; 75 | 76 | $scope.removeWidget = function(widget) { 77 | var text = "Want to delete widget?"; 78 | $window.bootbox.animate(false); 79 | $window.bootbox.confirm(text, "Cancel", "Delete", function(result) { 80 | if (result) destroyWidget(widget); 81 | }); 82 | }; 83 | 84 | $scope.deleteDashboard = function() { 85 | var text = "Want to delete Dashboard?"; 86 | $window.bootbox.animate(false); 87 | $window.bootbox.confirm(text, "Cancel", "Delete", function(result) { 88 | if (result) destroyDashboard(); 89 | }); 90 | }; 91 | 92 | $scope.save = function() { 93 | saveDashboardChanges(); 94 | }; 95 | 96 | }]); --------------------------------------------------------------------------------