├── 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 |
--------------------------------------------------------------------------------
/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 |
9 |
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 |
8 |
9 |
10 |
11 |
12 |
13 |
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 |
9 |
10 |
11 |
12 |
13 |
14 |
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 |
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 |
9 |
10 |
11 |
12 |
13 |
14 |
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 |
9 |
10 |
11 |
12 |
13 |
14 |
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 |
9 |
10 |
11 |
12 |
13 | Name
14 | Last Modified
15 | Created At
16 |
17 |
18 |
19 |
20 |
21 | {{dashboard.name}}
22 |
23 | {{dashboard.updated_at | date:'medium' }}
24 | {{dashboard.created_at | date:'medium' }}
25 |
26 |
27 |
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 |
9 |
10 |
11 |
12 |
13 |
14 |
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 |
--------------------------------------------------------------------------------
/app/assets/javascripts/templates/targets/index.html:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
10 |
11 |
12 |
22 |
23 |
24 |
28 |
29 |
30 |
31 |
35 |
36 |
--------------------------------------------------------------------------------
/vendor/assets/stylesheets/jquery.gridster.min.css:
--------------------------------------------------------------------------------
1 | /*! gridster.js - v0.1.0 - 2012-10-20
2 | * http://gridster.net/
3 | * Copyright (c) 2012 ducksboard; Licensed MIT */.gridster{position:relative}.gridster>*{margin:0 auto;-webkit-transition:height .4s;-moz-transition:height .4s;-o-transition:height .4s;-ms-transition:height .4s;transition:height .4s}.gridster .gs_w{z-index:2;position:absolute}.ready .gs_w:not(.preview-holder){-webkit-transition:opacity .3s,left .3s,top .3s;-moz-transition:opacity .3s,left .3s,top .3s;-o-transition:opacity .3s,left .3s,top .3s;transition:opacity .3s,left .3s,top .3s}.ready .gs_w:not(.preview-holder){-webkit-transition:opacity .3s,left .3s,top .3s,width .3s,height .3s;-moz-transition:opacity .3s,left .3s,top .3s,width .3s,height .3s;-o-transition:opacity .3s,left .3s,top .3s,width .3s,height .3s;transition:opacity .3s,left .3s,top .3s,width .3s,height .3s}.gridster .preview-holder{z-index:1;position:absolute;background-color:#fff;border-color:#fff;opacity:.3}.gridster .player-revert{z-index:10!important;-webkit-transition:left .3s,top .3s!important;-moz-transition:left .3s,top .3s!important;-o-transition:left .3s,top .3s!important;transition:left .3s,top .3s!important}.gridster .dragging{z-index:10!important;-webkit-transition:all 0s!important;-moz-transition:all 0s!important;-o-transition:all 0s!important;transition:all 0s!important}
--------------------------------------------------------------------------------
/app/assets/javascripts/widgets/number/directive.js:
--------------------------------------------------------------------------------
1 | app.directive("number", ["NumberModel", "SuffixFormatter", function(NumberModel, SuffixFormatter) {
2 |
3 | var linkFn = function(scope, element, attrs) {
4 |
5 | function calculatePercentage(value, previousValue) {
6 | console.log("previous", previousValue, "value", value);
7 | return ((value - previousValue) / value) * 100;
8 | }
9 |
10 | function onSuccess(data) {
11 | scope.data = data;
12 | scope.data.label = scope.data.label || scope.widget.label;
13 |
14 | scope.data.stringValue = scope.widget.use_metric_suffix ? SuffixFormatter.format(scope.data.value, 1) : scope.data.value.toString();
15 |
16 | var previousData = scope.previousData;
17 | if (previousData) {
18 | scope.data.secondaryValue = calculatePercentage(scope.data.value, previousData.value);
19 | scope.data.arrow = scope.data.secondaryValue > 0 ? "arrow-up" : "arrow-down";
20 | scope.data.color = scope.data.secondaryValue > 0 ? "color-up" : "color-down";
21 | }
22 | }
23 |
24 | function update() {
25 | return NumberModel.getData(scope.widget).success(onSuccess);
26 | }
27 |
28 | scope.init(update);
29 | };
30 |
31 | return {
32 | template: $("#templates-widgets-number-show").html(),
33 | link: linkFn
34 | };
35 | }]);
--------------------------------------------------------------------------------
/app/assets/stylesheets/widgets/graph/style.css.scss:
--------------------------------------------------------------------------------
1 | .graph-container {
2 | height: 265px;
3 | }
4 |
5 | .graph {
6 | display: inline-block;
7 | }
8 |
9 | /************************* flotr2 graph */
10 |
11 | .flotr-grid-label {
12 | font-size: 12px;
13 | color: #666;
14 | }
15 |
16 | /************************* rickshaw graph */
17 |
18 | .y-axis {
19 | float: left;
20 | width: 40px;
21 | /*width: 40px;*/
22 | }
23 |
24 | /* plot width */
25 | .graph svg path {
26 | stroke-width: 2;
27 | }
28 |
29 | /* move x-axis title below graph */
30 | .rickshaw_graph .x_tick {
31 | margin-bottom: -25px;
32 | }
33 |
34 | /* text color of x-axis label */
35 | .rickshaw_graph .x_tick .title {
36 | color: #FFF;
37 | }
38 |
39 | /* hover details */
40 | .rickshaw_graph .detail .x_label { display: none }
41 | .rickshaw_graph .detail .item { line-height: 1.4; padding: 0.5em }
42 | .detail_swatch { float: right; width: 10px; height: 10px; margin: 2px 4px 0 4px}
43 | .rickshaw_graph .detail .date { color: #a0a0a0; }
44 |
45 | .rickshaw_graph text {
46 | fill: #FFF;
47 | }
48 |
49 | /* hide y-axis matrix */
50 | .rickshaw_graph .x_tick {
51 | stroke: 0;
52 | border: 0;
53 | }
54 |
55 | /* hide y-axis big line */
56 | .rickshaw_graph .y_ticks path {
57 | fill: none;
58 | stroke: none;
59 | }
60 |
61 | .rickshaw_graph .y_grid {
62 | display:none;
63 | }
--------------------------------------------------------------------------------
/spec/models/ganglia_url_builder_spec.rb:
--------------------------------------------------------------------------------
1 | require "spec_helper"
2 |
3 | describe GangliaUrlBuilder do
4 |
5 | before do
6 | @url_builder = GangliaUrlBuilder.new("http://localhost:3000")
7 | @target = "hostname@cluster(metric)"
8 | @from = 1.day.ago.to_i
9 | @to = 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("%m/%d/%Y %H:%M"))
15 | end
16 | end
17 |
18 | describe "#datapoints_url" do
19 | before do
20 | @params = @url_builder.datapoints_url(@target, @from, @to)[:params]
21 | end
22 |
23 | it "contains json params" do
24 | @params[:json].should == 1
25 | end
26 |
27 | it "contains cluster param" do
28 | @params[:c].should == "cluster"
29 | end
30 |
31 | it "contains host param" do
32 | @params[:h].should == "hostname"
33 | end
34 |
35 | it "contains metric param" do
36 | @params[:m].should == "metric"
37 | end
38 |
39 | it "contains custom range param" do
40 | @params[:r].should == "custom"
41 | end
42 |
43 | it "contains from param" do
44 | @params[:cs].should == @url_builder.format(@from)
45 | end
46 |
47 | it "contains to param" do
48 | @params[:ce].should == @url_builder.format(@to)
49 | end
50 | end
51 |
52 | end
--------------------------------------------------------------------------------
/app/models/sources/exception_tracker/errbit.rb:
--------------------------------------------------------------------------------
1 | require 'open-uri'
2 |
3 | # You must provide the server URL and API Key for your registered application.
4 | #
5 | # Example for Errbit: 3139359fa87f81665add733ba173bbd4
6 | #
7 | module Sources
8 | module ExceptionTracker
9 | class Errbit < Sources::ExceptionTracker::Base
10 |
11 | def fields
12 | [
13 | { :name => "server_url", :title => "Errbit Server Url", :mandatory => true },
14 | { :name => "api_key", :title => "API Key", :mandatory => true }
15 | ]
16 | end
17 |
18 | # Returns ruby hash:
19 | def get(options = {})
20 | widget = Widget.find(options.fetch(:widget_id))
21 | server_url = widget.settings.fetch(:server_url);
22 | api_key = widget.settings.fetch(:api_key);
23 |
24 | result = request_stats(server_url, api_key)
25 | {
26 | :label => result["name"],
27 | :last_error_time => result["last_error_time"],
28 | :unresolved_errors => result["unresolved_errors"]
29 | }
30 | end
31 |
32 | def request_stats(server_url, api_key)
33 | url = "#{server_url}/api/v1/stats/app.json?api_key=#{api_key}"
34 | Rails.logger.debug("Requesting from #{url} ...")
35 | ::HttpService.request(url)
36 | end
37 | end
38 | end
39 | end
--------------------------------------------------------------------------------
/app/assets/javascripts/services/sources.js:
--------------------------------------------------------------------------------
1 | app.factory("Sources", function() {
2 |
3 | // TODO: kind mismatch graph/datapoints
4 | function kindMapping(kind) {
5 | if (kind === "graph") kind = "datapoints";
6 | if (kind === "meter") kind = "number";
7 | return kind;
8 | }
9 |
10 | function sourceConfig(widget) {
11 | return $.Sources[kindMapping(widget.kind)][widget.source];
12 | }
13 |
14 | function sourceMapping(source) {
15 | return {
16 | value: source.name,
17 | label: source.name,
18 | disabled: !source.available,
19 | supports_functions: source.supports_functions,
20 | supports_target_browsing: source.supports_target_browsing
21 | };
22 | }
23 |
24 | // TODO: handle disabled sources
25 | // disabled attribute not available in current Angular select ng-options directive
26 | function availableSources(kind) {
27 | var sources = $.Sources[kindMapping(kind)];
28 |
29 | return _.compact(_.map(sources, function(source) {
30 | if (source.available) return sourceMapping(source);
31 | }));
32 | }
33 |
34 | function supportsTargetBrowsing(widget) {
35 | var config = sourceConfig(widget);
36 | return config ? config.supports_target_browsing : false;
37 | }
38 |
39 | return {
40 | availableSources: availableSources,
41 | supportsTargetBrowsing: supportsTargetBrowsing
42 | };
43 | });
--------------------------------------------------------------------------------
/spec/models/sources/datapoints/graphite_spec.rb:
--------------------------------------------------------------------------------
1 | require "spec_helper"
2 |
3 | describe Sources::Datapoints::Graphite do
4 |
5 | let(:source) { Sources::Datapoints::Graphite.new }
6 | let(:widget) { FactoryGirl.create(:widget, :kind => "datapoints", :source => "graphite", :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 = [{ 'target' => 'test1', 'datapoints' => [[1, 123]] }]
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.should eq(input)
20 | end
21 | end
22 |
23 | describe "#available_targets" do
24 | it "searches for given pattern" do
25 | input = %w(adam ada zebra)
26 | source.expects(:request_available_targets).returns(input)
27 | result = source.available_targets(:pattern => "ada")
28 | result.size.should == 2
29 | end
30 |
31 | it "limits returned targets" do
32 | input = %w(adam ada zebra)
33 | source.expects(:request_available_targets).returns(input)
34 | result = source.available_targets(:pattern => "ada", :limit => 1)
35 | result.size.should == 1
36 | end
37 | end
38 | end
39 |
--------------------------------------------------------------------------------
/app/assets/javascripts/application.js:
--------------------------------------------------------------------------------
1 | // This is a manifest file that'll be compiled into application.js, which will include all the files
2 | // listed below.
3 | //
4 | // Any JavaScript/Coffee file within this directory, lib/assets/javascripts, vendor/assets/javascripts,
5 | // or vendor/assets/javascripts of plugins, if any, can be referenced here using a relative path.
6 | //
7 | // It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the
8 | // the compiled file.
9 | //
10 | // WARNING: THE FIRST BLANK LINE MARKS THE END OF WHAT'S TO BE PROCESSED, ANY BLANK LINE SHOULD
11 | // GO AFTER THE REQUIRES BELOW.
12 | //
13 |
14 | //= require underscore
15 |
16 | //= require jquery
17 | //= require jquery_ujs
18 | //= require jquery.gridster
19 |
20 | //= require twitter/bootstrap/dropdown
21 | //= require twitter/bootstrap/typeahead
22 | //= require twitter/bootstrap/modal
23 |
24 | //= require bootbox
25 | //= require bigscreen.min
26 |
27 | //= require flotr2
28 | //= require moment.min
29 | //= require jquery.timeago
30 | //= require jquery.timeago.en-short
31 | //= require jquery.knob
32 |
33 | //= require angular
34 | //= require angular-resource
35 | //= require angular-sanitize
36 | //= require angular-ui/module
37 | //= require angular-ui/jq
38 | //= require angular-ui/bootstrap/modal
39 | //= require angular-ui/bootstrap/dialog
40 | //= require angular-ui/bootstrap/transitions
41 |
42 | //= require_tree .
43 |
--------------------------------------------------------------------------------
/app/models/widget.rb:
--------------------------------------------------------------------------------
1 | class Widget < ActiveRecord::Base
2 | belongs_to :dashboard
3 |
4 | serialize :settings
5 |
6 | validates :name, :presence => true
7 | validates :dashboard_id, :presence => true
8 |
9 | after_initialize :set_defaults
10 |
11 | attr_accessible :name, :kind, :size, :source, :targets, :range, :update_interval, :dashboard_id, :dashboard, :col, :row, :size_x, :size_y, :settings
12 |
13 | class << self
14 |
15 | def list_available
16 | path = Rails.root.join("app/assets/javascripts/widgets")
17 | Dir["#{path}/*"].map { |f| File.basename(f, '.*') }
18 | end
19 |
20 | def for_dashboard(id)
21 | where(:dashboard_id => id)
22 | end
23 |
24 | # settings specific attributes handling
25 | def slice_attributes(input)
26 | input.symbolize_keys!
27 | default_set = accessible_attributes.to_a.map(&:to_sym)
28 | input.slice(*default_set).merge(:settings => input.except(*default_set))
29 | end
30 | end
31 |
32 | # flatten settings hash
33 | def as_json(options = {})
34 | result = super(:except => :settings)
35 | result.merge!((settings || {}).stringify_keys)
36 | result
37 | end
38 |
39 | protected
40 |
41 | def set_defaults
42 | self.size = 1 unless self.size
43 | self.range = '30-minutes' unless self.range
44 | self.kind = 'graph' unless self.kind
45 | self.update_interval = 10 unless self.update_interval
46 | end
47 |
48 | end
49 |
--------------------------------------------------------------------------------
/app/assets/javascripts/widgets/ci/directive.js:
--------------------------------------------------------------------------------
1 | app.directive("ci", ["CiModel", function(CiModel) {
2 |
3 | var linkFn = function(scope, element, attrs, WidgetCtrl) {
4 |
5 | function current_status_message(current_status) {
6 | switch(current_status) {
7 | case 0:
8 | return "Sleeping...";
9 | case 1:
10 | return "Building...";
11 | default:
12 | return "";
13 | }
14 | }
15 |
16 | function lastBuildStatusClass(last_build_status) {
17 | switch(last_build_status) {
18 | case 0:
19 | return "green";
20 | case 1:
21 | return "red";
22 | case -1:
23 | return "gray";
24 | default:
25 | return "gray";
26 | }
27 | }
28 |
29 | function onSuccess(data) {
30 | scope.data = data;
31 | scope.data.label = scope.data.label || scope.widget.label;
32 | scope.data.current_status_message = current_status_message(scope.data.current_status);
33 | scope.data.lastBuildStatusClass = lastBuildStatusClass(scope.data.last_build_status);
34 | scope.data.buildingClass = scope.data.current_status === 1 ? "building" : "";
35 | }
36 |
37 | function update() {
38 | return CiModel.getData(scope.widget).success(onSuccess);
39 | }
40 |
41 | scope.init(update);
42 | };
43 |
44 | return {
45 | template: $("#templates-widgets-ci-show").html(),
46 | link: linkFn
47 | };
48 | }]);
--------------------------------------------------------------------------------
/app/assets/javascripts/widgets/number/controller.js:
--------------------------------------------------------------------------------
1 | app.controller("NumberCtrl", ["$scope", "Sources", "EditorFormOptions", "$dialog", function($scope, Sources, EditorFormOptions, $dialog) {
2 |
3 | var defaults = {
4 | size_x: 1, size_y: 1,
5 | update_interval: 10,
6 | use_metric_suffix: true,
7 | range: "30-minutes",
8 | graph_type: "area"
9 | };
10 |
11 | if (!$scope.widget.id) {
12 | _.extend($scope.widget, defaults);
13 | }
14 |
15 | $scope.datapointsSources = Sources.availableSources("datapoints");
16 |
17 | $scope.graphTypes = EditorFormOptions.graphTypes;
18 | $scope.aggregate_functions = EditorFormOptions.aggregate_functions;
19 |
20 | $scope.aggregateFunction = [
21 | { value: "average", label: "Average" },
22 | { value: "sum", label: "Sum" },
23 | { value: "difference", label: "Difference" }
24 | ];
25 |
26 | $scope.supportsTargetBrowsing = function() {
27 | return Sources.supportsTargetBrowsing($scope.widget);
28 | };
29 |
30 | $scope.editTargets = function() {
31 | var templateUrl = "/assets/templates/targets/index.html";
32 |
33 | dialog.targets = $scope.widget.targets;
34 | dialog.open(templateUrl, "TargetsCtrl").then(convertTargetsArrayToString);
35 | };
36 |
37 | function convertTargetsArrayToString(result) {
38 | $scope.widget.targets = _.map(dialog.$scope.targets, function(t) {
39 | return t.content;
40 | }).join(";");
41 | }
42 |
43 | }]);
--------------------------------------------------------------------------------
/app/assets/javascripts/templates/widget/json_response_editor.html:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 |
7 |
8 |
9 |
10 |
Value Label
11 |
12 |
13 |
14 | Test
15 |
16 |
17 |
18 |
19 |
20 |
Response Body
21 |
22 |
23 |
24 |
25 |
26 |
27 |
Result
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 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
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 | '
{{label}} ' +
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 |
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 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 | Use Metric Suffix
72 |
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 | }]);
--------------------------------------------------------------------------------