Commit 1265b058af938c1083c9214a7bfa9687be060df3
Exists in
master
and in
1 other branch
Merge remote branch 'boblail/ui-updates' into ui-updates
Conflicts: app/controllers/apps_controller.rb app/models/app.rb app/models/deploy.rb app/models/problem.rb app/views/notices/_backtrace.html.haml app/views/notices/_summary.html.haml public/stylesheets/application.css spec/models/problem_spec.rb
Showing
17 changed files
with
259 additions
and
185 deletions
Show diff stats
app/controllers/apps_controller.rb
| ... | ... | @@ -3,15 +3,19 @@ class AppsController < InheritedResources::Base |
| 3 | 3 | before_filter :parse_email_at_notices_or_set_default, :only => [:create, :update] |
| 4 | 4 | respond_to :html |
| 5 | 5 | |
| 6 | - | |
| 7 | 6 | def show |
| 8 | 7 | respond_to do |format| |
| 9 | 8 | format.html do |
| 10 | 9 | @all_errs = !!params[:all_errs] |
| 11 | 10 | |
| 11 | + @sort = params[:sort] | |
| 12 | + @order = params[:order] | |
| 13 | + @sort = "app" unless %w{message last_notice_at last_deploy_at count}.member?(@sort) | |
| 14 | + @order = "asc" unless (@order == "desc") | |
| 15 | + | |
| 12 | 16 | @problems = resource.problems |
| 13 | 17 | @problems = @problems.unresolved unless @all_errs |
| 14 | - @problems = @problems.in_env(params[:environment]).ordered.paginate(:page => params[:page], :per_page => current_user.per_page) | |
| 18 | + @problems = @problems.in_env(params[:environment]).ordered_by(@sort, @order).paginate(:page => params[:page], :per_page => current_user.per_page) | |
| 15 | 19 | |
| 16 | 20 | @selected_problems = params[:problems] || [] |
| 17 | 21 | @deploys = @app.deploys.order_by(:created_at.desc).limit(5) | ... | ... |
app/controllers/errs_controller.rb
| ... | ... | @@ -9,7 +9,13 @@ class ErrsController < ApplicationController |
| 9 | 9 | |
| 10 | 10 | def index |
| 11 | 11 | app_scope = current_user.admin? ? App.all : current_user.apps |
| 12 | - @problems = Problem.for_apps(app_scope).in_env(params[:environment]).unresolved.ordered | |
| 12 | + | |
| 13 | + @sort = params[:sort] | |
| 14 | + @order = params[:order] | |
| 15 | + @sort = "app" unless %w{message last_notice_at last_deploy_at count}.member?(@sort) | |
| 16 | + @order = "asc" unless (@order == "desc") | |
| 17 | + | |
| 18 | + @problems = Problem.for_apps(app_scope).in_env(params[:environment]).unresolved.ordered_by(@sort, @order) | |
| 13 | 19 | @selected_problems = params[:problems] || [] |
| 14 | 20 | respond_to do |format| |
| 15 | 21 | format.html do | ... | ... |
app/helpers/application_helper.rb
| 1 | 1 | module ApplicationHelper |
| 2 | - | |
| 3 | - def user_agent_graph(error) | |
| 4 | - tallies = tally(error.notices) {|notice| pretty_user_agent(notice.user_agent)} | |
| 5 | - create_percentage_table(tallies, :total => error.notices.count) | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + def message_graph(problem) | |
| 6 | + create_percentage_table_for(problem) {|notice| notice.message} | |
| 6 | 7 | end |
| 7 | - | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + def user_agent_graph(problem) | |
| 12 | + create_percentage_table_for(problem) {|notice| pretty_user_agent(notice.user_agent)} | |
| 13 | + end | |
| 14 | + | |
| 8 | 15 | def pretty_user_agent(user_agent) |
| 9 | 16 | (user_agent.nil? || user_agent.none?) ? "N/A" : "#{user_agent.browser} #{user_agent.version}" |
| 10 | 17 | end |
| 11 | - | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + def tenant_graph(problem) | |
| 22 | + create_percentage_table_for(problem) {|notice| get_host(notice.request['url'])} | |
| 23 | + end | |
| 24 | + | |
| 25 | + def get_host(url) | |
| 26 | + uri = url && URI.parse(url) | |
| 27 | + uri.blank? ? "N/A" : uri.host | |
| 28 | + end | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + def create_percentage_table_for(problem, &block) | |
| 33 | + tallies = tally(problem.notices, &block) | |
| 34 | + create_percentage_table_from_tallies(tallies, :total => problem.notices.count) | |
| 35 | + end | |
| 36 | + | |
| 12 | 37 | def tally(collection, &block) |
| 13 | 38 | collection.inject({}) do |tallies, item| |
| 14 | 39 | value = yield item |
| ... | ... | @@ -16,18 +41,21 @@ module ApplicationHelper |
| 16 | 41 | tallies |
| 17 | 42 | end |
| 18 | 43 | end |
| 19 | - | |
| 20 | - def create_percentage_table(tallies, options={}) | |
| 44 | + | |
| 45 | + def create_percentage_table_from_tallies(tallies, options={}) | |
| 21 | 46 | total = (options[:total] || total_from_tallies(tallies)) |
| 22 | 47 | percent = 100.0 / total.to_f |
| 23 | 48 | rows = tallies.map {|value, count| [(count.to_f * percent), value]} \ |
| 24 | 49 | .sort {|a, b| a[0] <=> b[0]} |
| 25 | 50 | render :partial => "errs/tally_table", :locals => {:rows => rows} |
| 26 | 51 | end |
| 27 | - | |
| 52 | + | |
| 53 | + | |
| 28 | 54 | def total_from_tallies(tallies) |
| 29 | 55 | tallies.values.inject(0) {|sum, n| sum + n} |
| 30 | 56 | end |
| 31 | 57 | private :total_from_tallies |
| 58 | + | |
| 59 | + | |
| 32 | 60 | end |
| 33 | 61 | ... | ... |
| ... | ... | @@ -0,0 +1,14 @@ |
| 1 | +# encoding: utf-8 | |
| 2 | +module SortHelper | |
| 3 | + | |
| 4 | + def link_for_sort(name, field=nil) | |
| 5 | + field ||= name.underscore | |
| 6 | + current = (@sort == field) | |
| 7 | + order = (current && (@order == "asc")) ? "desc" : "asc" | |
| 8 | + url = request.path + "?sort=#{field}&order=#{order}" | |
| 9 | + options = {} | |
| 10 | + options.merge(:class => "current") if current | |
| 11 | + link_to(name, url, options) | |
| 12 | + end | |
| 13 | + | |
| 14 | +end | ... | ... |
app/models/app.rb
| ... | ... | @@ -28,6 +28,7 @@ class App |
| 28 | 28 | |
| 29 | 29 | before_validation :generate_api_key, :on => :create |
| 30 | 30 | before_save :normalize_github_url |
| 31 | + after_update :store_cached_attributes_on_problems | |
| 31 | 32 | |
| 32 | 33 | validates_presence_of :name, :api_key |
| 33 | 34 | validates_uniqueness_of :name, :allow_blank => true |
| ... | ... | @@ -143,6 +144,10 @@ class App |
| 143 | 144 | |
| 144 | 145 | protected |
| 145 | 146 | |
| 147 | + def store_cached_attributes_on_problems | |
| 148 | + problems.each(&:cache_app_attributes) | |
| 149 | + end | |
| 150 | + | |
| 146 | 151 | def generate_api_key |
| 147 | 152 | self.api_key ||= ActiveSupport::SecureRandom.hex |
| 148 | 153 | end |
| ... | ... | @@ -162,6 +167,5 @@ class App |
| 162 | 167 | self.github_url.gsub!(/github\.com:/, 'github.com/') |
| 163 | 168 | self.github_url.gsub!(/\.git$/, '') |
| 164 | 169 | end |
| 165 | - | |
| 166 | 170 | end |
| 167 | 171 | ... | ... |
app/models/deploy.rb
| ... | ... | @@ -14,6 +14,7 @@ class Deploy |
| 14 | 14 | |
| 15 | 15 | after_create :deliver_notification, :if => :should_notify? |
| 16 | 16 | after_create :resolve_app_errs, :if => :should_resolve_app_errs? |
| 17 | + after_create :store_cached_attributes_on_problems | |
| 17 | 18 | |
| 18 | 19 | validates_presence_of :username, :environment |
| 19 | 20 | |
| ... | ... | @@ -39,5 +40,8 @@ class Deploy |
| 39 | 40 | app.resolve_errs_on_deploy? |
| 40 | 41 | end |
| 41 | 42 | |
| 43 | + def store_cached_attributes_on_problems | |
| 44 | + Problem.where(:app_id => app.id).each(&:cache_app_attributes) | |
| 45 | + end | |
| 42 | 46 | end |
| 43 | 47 | ... | ... |
app/models/problem.rb
| ... | ... | @@ -7,40 +7,45 @@ class Problem |
| 7 | 7 | include Mongoid::Timestamps |
| 8 | 8 | |
| 9 | 9 | field :last_notice_at, :type => DateTime |
| 10 | + field :last_deploy_at, :type => Time | |
| 10 | 11 | field :resolved, :type => Boolean, :default => false |
| 11 | 12 | field :issue_link, :type => String |
| 12 | 13 | |
| 13 | 14 | # Cached fields |
| 15 | + field :app_name, :type => String | |
| 14 | 16 | field :notices_count, :type => Integer, :default => 0 |
| 15 | 17 | field :message |
| 16 | 18 | field :environment |
| 17 | 19 | field :klass |
| 18 | 20 | field :where |
| 19 | 21 | |
| 20 | - index :last_notice_at | |
| 21 | 22 | index :app_id |
| 23 | + index :app_name | |
| 24 | + index :message | |
| 25 | + index :last_notice_at | |
| 26 | + index :last_deploy_at | |
| 27 | + index :notices_count | |
| 22 | 28 | |
| 23 | 29 | belongs_to :app |
| 24 | 30 | has_many :errs, :inverse_of => :problem, :dependent => :destroy |
| 25 | 31 | has_many :comments, :inverse_of => :err, :dependent => :destroy |
| 26 | 32 | |
| 33 | + before_create :cache_app_attributes | |
| 34 | + | |
| 27 | 35 | scope :resolved, where(:resolved => true) |
| 28 | 36 | scope :unresolved, where(:resolved => false) |
| 29 | 37 | scope :ordered, order_by(:last_notice_at.desc) |
| 30 | 38 | scope :for_apps, lambda {|apps| where(:app_id.in => apps.all.map(&:id))} |
| 31 | 39 | |
| 40 | + | |
| 32 | 41 | def self.in_env(env) |
| 33 | 42 | env.present? ? where(:environment => env) : scoped |
| 34 | 43 | end |
| 35 | 44 | |
| 36 | - | |
| 37 | - | |
| 38 | 45 | def notices |
| 39 | 46 | Notice.for_errs(errs).ordered |
| 40 | 47 | end |
| 41 | 48 | |
| 42 | - | |
| 43 | - | |
| 44 | 49 | def resolve! |
| 45 | 50 | self.update_attributes!(:resolved => true) |
| 46 | 51 | end |
| ... | ... | @@ -54,7 +59,6 @@ class Problem |
| 54 | 59 | end |
| 55 | 60 | |
| 56 | 61 | |
| 57 | - | |
| 58 | 62 | def self.merge!(*problems) |
| 59 | 63 | problems = problems.flatten.uniq |
| 60 | 64 | merged_problem = problems.shift |
| ... | ... | @@ -82,12 +86,32 @@ class Problem |
| 82 | 86 | end |
| 83 | 87 | |
| 84 | 88 | |
| 89 | + def self.ordered_by(sort, order) | |
| 90 | + case sort | |
| 91 | + when "app"; order_by(["app_name", order]) | |
| 92 | + when "message"; order_by(["message", order]) | |
| 93 | + when "last_notice_at"; order_by(["last_notice_at", order]) | |
| 94 | + when "last_deploy_at"; order_by(["last_deploy_at", order]) | |
| 95 | + when "count"; order_by(["notices_count", order]) | |
| 96 | + else raise("\"#{sort}\" is not a recognized sort") | |
| 97 | + end | |
| 98 | + end | |
| 99 | + | |
| 85 | 100 | |
| 86 | 101 | def reset_cached_attributes |
| 87 | 102 | update_attribute(:notices_count, notices.count) |
| 103 | + cache_app_attributes | |
| 88 | 104 | cache_notice_attributes |
| 89 | 105 | end |
| 90 | 106 | |
| 107 | + def cache_app_attributes | |
| 108 | + if app | |
| 109 | + self.app_name = app.name | |
| 110 | + self.last_deploy_at = app.last_deploy_at | |
| 111 | + self.save if persisted? | |
| 112 | + end | |
| 113 | + end | |
| 114 | + | |
| 91 | 115 | def cache_notice_attributes(notice=nil) |
| 92 | 116 | notice ||= notices.first |
| 93 | 117 | attrs = {:last_notice_at => notices.max(:created_at)} |
| ... | ... | @@ -98,6 +122,5 @@ class Problem |
| 98 | 122 | :where => notice.where) if notice |
| 99 | 123 | update_attributes!(attrs) |
| 100 | 124 | end |
| 101 | - | |
| 102 | - | |
| 103 | 125 | end |
| 126 | + | ... | ... |
app/views/apps/index.html.haml
app/views/errs/_table.html.haml
| ... | ... | @@ -3,11 +3,11 @@ |
| 3 | 3 | %thead |
| 4 | 4 | %tr |
| 5 | 5 | %th |
| 6 | - %th App | |
| 7 | - %th What & Where | |
| 8 | - %th Latest | |
| 9 | - %th Deploy | |
| 10 | - %th Count | |
| 6 | + %th= link_for_sort "App" | |
| 7 | + %th= link_for_sort "What & Where".html_safe, "message" | |
| 8 | + %th= link_for_sort "Latest", "last_notice_at" | |
| 9 | + %th= link_for_sort "Deploy", "last_deploy_at" | |
| 10 | + %th= link_for_sort "Count" | |
| 11 | 11 | %th Resolve |
| 12 | 12 | %tbody |
| 13 | 13 | - errs.each do |problem| |
| ... | ... | @@ -24,7 +24,7 @@ |
| 24 | 24 | = link_to problem.message, app_err_path(problem.app, problem) |
| 25 | 25 | %em= problem.where |
| 26 | 26 | %td.latest #{time_ago_in_words(last_notice_at problem)} ago |
| 27 | - %td.deploy= problem.app.last_deploy_at ? problem.app.last_deploy_at.to_s(:micro) : 'n/a' | |
| 27 | + %td.deploy= problem.last_deploy_at ? problem.last_deploy_at.to_s(:micro) : 'n/a' | |
| 28 | 28 | %td.count= link_to problem.notices.count, app_err_path(problem.app, problem) |
| 29 | 29 | %td.resolve= link_to image_tag("thumbs-up.png"), resolve_app_err_path(problem.app, problem), :title => "Resolve", :method => :put, :confirm => err_confirm, :class => 'resolve' if problem.unresolved? |
| 30 | 30 | - if errs.none? | ... | ... |
app/views/errs/all.html.haml
app/views/errs/index.html.haml
| 1 | -- content_for :title, 'Unresolved Errs' | |
| 1 | +- content_for :title, 'Unresolved Errors' | |
| 2 | 2 | - content_for :head do |
| 3 | 3 | = auto_discovery_link_tag :atom, errs_url(User.token_authentication_key => current_user.authentication_token, :format => "atom"), :title => "Errbit notices at #{root_url}" |
| 4 | 4 | - content_for :action_bar do |
| 5 | 5 | = link_to 'show resolved', all_errs_path, :class => 'button' |
| 6 | -= render 'table', :errs => @problems | |
| 7 | 6 | \ No newline at end of file |
| 7 | += render 'table', :errs => @problems | ... | ... |
app/views/layouts/application.html.haml
| ... | ... | @@ -17,10 +17,8 @@ |
| 17 | 17 | #header |
| 18 | 18 | %div |
| 19 | 19 | = link_to 'Errbit', root_path, :id => 'site-name' |
| 20 | + = render 'shared/navigation' if current_user | |
| 20 | 21 | = render 'shared/session' |
| 21 | - | |
| 22 | - = render 'shared/navigation' if current_user | |
| 23 | - | |
| 24 | 22 | #content-wrapper |
| 25 | 23 | #content-title |
| 26 | 24 | %h1= yield :title | ... | ... |
app/views/notices/_backtrace.html.haml
| 1 | 1 | .window |
| 2 | 2 | %table.backtrace |
| 3 | - %tr.padding | |
| 4 | - %th | |
| 5 | - %td | |
| 6 | 3 | - lines.each do |line| |
| 7 | - %tr | |
| 8 | - %th.line-numbers= line_number_with_link(@app, line) | |
| 9 | - %td.lines{:class => (Notice.in_app_backtrace_line?(line) ? 'in-app' : nil)} | |
| 10 | - = line['file'] | |
| 4 | + - in_app = line['file'].gsub!('[PROJECT_ROOT]','') && !line['file'].match(/^\/vendor\//) | |
| 5 | + - path = File.dirname(line['file']) + '/' | |
| 6 | + - file = File.basename(line['file']) | |
| 7 | + %tr{:class => (in_app ? 'in-app' : nil)} | |
| 8 | + %td.line | |
| 9 | + %span.path>= path | |
| 10 | + %span.file= "#{file}:" << line_number_with_link(@app, line) | |
| 11 | 11 | → |
| 12 | - %strong= line['method'] | |
| 13 | - %tr.padding | |
| 14 | - %th | |
| 15 | - %td | |
| 12 | + %span.method= line['method'] | |
| 16 | 13 | ... | ... |
app/views/notices/_summary.html.haml
| ... | ... | @@ -2,7 +2,7 @@ |
| 2 | 2 | %table.summary |
| 3 | 3 | %tr |
| 4 | 4 | %th Message |
| 5 | - %td.main.nowrap= notice.message | |
| 5 | + %td.main.nowrap= message_graph(notice.problem) | |
| 6 | 6 | - if notice.request['url'].present? |
| 7 | 7 | %tr |
| 8 | 8 | %th URL |
| ... | ... | @@ -20,6 +20,9 @@ |
| 20 | 20 | %th Browser |
| 21 | 21 | %td= user_agent_graph(notice.problem) |
| 22 | 22 | %tr |
| 23 | + %th Tenant | |
| 24 | + %td= tenant_graph(notice.problem) | |
| 25 | + %tr | |
| 23 | 26 | %th App Server |
| 24 | 27 | %td= notice.server_environment && notice.server_environment["hostname"] |
| 25 | 28 | %tr | ... | ... |
app/views/shared/_navigation.html.haml
| ... | ... | @@ -2,7 +2,7 @@ |
| 2 | 2 | %ul |
| 3 | 3 | //%li= link_to 'Dashboard', admin_dashboard_path, :class => active_if_here(:dashboards) |
| 4 | 4 | %li.apps{:class => active_if_here(:apps)}= link_to 'Apps', apps_path |
| 5 | - %li.errs{:class => active_if_here(:errs)}= link_to 'Errs', errs_path | |
| 5 | + %li.errs{:class => active_if_here(:errs)}= link_to 'Errors', errs_path | |
| 6 | 6 | - if user_signed_in? && current_user.admin? |
| 7 | 7 | %li.users{:class => active_if_here(:users)}= link_to 'Users', users_path |
| 8 | 8 | %div.clear |
| 9 | 9 | \ No newline at end of file | ... | ... |
public/stylesheets/application.css
| 1 | 1 | html { |
| 2 | 2 | margin: 0; padding: 0; |
| 3 | - color: #585858; background-color: #E2E2E2; | |
| 3 | + color: #585858; background-color: #d0d0d0; | |
| 4 | 4 | font-size: 62.8%; font-family: Helvetica, "Lucida Grande","Lucida Sans",Arial,sans-serif; |
| 5 | 5 | } |
| 6 | 6 | body { |
| ... | ... | @@ -17,7 +17,7 @@ body { |
| 17 | 17 | .nowrap { white-space: nowrap; } |
| 18 | 18 | |
| 19 | 19 | /* Headings */ |
| 20 | -h1, h2, h3, h4, h5, h6 { padding: 0.2em 0; margin-bottom: 1em; border-bottom: 1px solid #E2E2E2;} | |
| 20 | +h1, h2, h3, h4, h5, h6 { padding: 0.2em 0; margin-bottom: 1em; border-bottom: 1px solid #dedede;} | |
| 21 | 21 | h1 { font-size: 2.0em; line-height: 1.2em; text-shadow: 1px 1px 0px #FFF; -webkit-text-shadow: 1px 1px 0px #FFF;} |
| 22 | 22 | h2 { font-size: 1.7em; line-height: 1.2em; } |
| 23 | 23 | h3 { font-size: 1.5em; line-height: 1.2em; } |
| ... | ... | @@ -42,20 +42,24 @@ a.action { float: right; font-size: 0.9em;} |
| 42 | 42 | |
| 43 | 43 | /* Header */ |
| 44 | 44 | #header { |
| 45 | - height: 75px; | |
| 46 | 45 | margin-bottom: 24px; |
| 47 | 46 | border-bottom: 1px solid #fff; |
| 48 | - background: #000 url(images/header.png) 0 0 repeat-x; | |
| 47 | + background: black; | |
| 48 | + position:relative; | |
| 49 | +} | |
| 50 | +#header > div { | |
| 51 | + height:64px; | |
| 49 | 52 | } |
| 50 | 53 | #header #site-name { |
| 51 | 54 | display: block; |
| 52 | 55 | width: 88px; |
| 53 | 56 | height: 31px; |
| 54 | 57 | position: absolute; |
| 55 | - top: 22px; | |
| 58 | + top: 26px; | |
| 56 | 59 | left: 2px; |
| 57 | - background: transparent url(images/logo.png) 0 0 no-repeat; | |
| 58 | - text-indent: -5000em; | |
| 60 | + font-size: 2.5em; | |
| 61 | + font-weight:bold; | |
| 62 | + color: white !important; | |
| 59 | 63 | } |
| 60 | 64 | #header #session-links { |
| 61 | 65 | position: absolute; top: 18px; right: 0; |
| ... | ... | @@ -64,48 +68,49 @@ a.action { float: right; font-size: 0.9em;} |
| 64 | 68 | #header #session-links li { |
| 65 | 69 | float: right; |
| 66 | 70 | margin-left: 10px; |
| 67 | - color: #FFF; | |
| 71 | + color: #ccc; | |
| 68 | 72 | background-color: #000; |
| 69 | - border-radius: 6px; | |
| 70 | - -moz-border-radius: 6px; | |
| 71 | - -webkit-border-radius: 6px; | |
| 73 | + border-radius: 30px; | |
| 74 | + -moz-border-radius: 30px; | |
| 75 | + -webkit-border-radius: 30px; | |
| 72 | 76 | border: 1px solid #484B4F; |
| 73 | 77 | font-size: 14px; |
| 74 | - font-weight: bold; | |
| 75 | 78 | } |
| 76 | 79 | #header #session-links li:hover { |
| 77 | 80 | box-shadow: 0 0 3px #69c; |
| 78 | 81 | -moz-box-shadow: 0 0 3px #69c; |
| 79 | 82 | -webkit-box-shadow: 0 0 3px #69c; |
| 80 | 83 | } |
| 84 | +#header #session-links li:hover a { | |
| 85 | + color: white; | |
| 86 | +} | |
| 81 | 87 | #header #session-links a { |
| 82 | - color: #FFF; | |
| 83 | - padding: 0 10px 0 30px; | |
| 84 | - line-height: 37px; | |
| 88 | + color: #ccc; | |
| 89 | + padding: 0 14px; | |
| 90 | + line-height: 30px; | |
| 85 | 91 | } |
| 86 | 92 | #header #session-links a:hover { |
| 87 | 93 | text-decoration: none; |
| 88 | 94 | } |
| 89 | -#header #session-links #sign-out { | |
| 90 | - background: transparent url(images/icons/bullet-red-sm.png) 12px 50% no-repeat; | |
| 91 | -} | |
| 92 | -#header #session-links #edit-profile { | |
| 93 | - padding-left: 10px; | |
| 94 | -} | |
| 95 | 95 | |
| 96 | 96 | /* Navigation */ |
| 97 | 97 | #nav-bar { |
| 98 | - margin-bottom: 24px; | |
| 99 | - height: 41px; | |
| 98 | + position: absolute; | |
| 99 | + bottom: 0; | |
| 100 | + left: 128px; | |
| 100 | 101 | } |
| 101 | 102 | #nav-bar li { |
| 102 | 103 | float: left; |
| 103 | - margin-right: 18px; | |
| 104 | + height: 38px; | |
| 105 | + margin-right: 12px; | |
| 104 | 106 | color: #666; |
| 105 | 107 | background: #FFF url(images/button-bg.png) 0 bottom repeat-x; |
| 106 | - border-radius: 50px; | |
| 107 | - -moz-border-radius: 50px; | |
| 108 | - -webkit-border-radius: 50px; | |
| 108 | + border-top-left-radius: 12px; | |
| 109 | + border-top-right-radius: 12px; | |
| 110 | + -moz-border-top-left-radius: 12px; | |
| 111 | + -moz-border-top-right-radius: 12px; | |
| 112 | + -webkit-border-top-left-radius: 12px; | |
| 113 | + -webkit-border-top-right-radius: 12px; | |
| 109 | 114 | border: 1px solid #bbb; |
| 110 | 115 | } |
| 111 | 116 | #nav-bar li a { |
| ... | ... | @@ -120,18 +125,18 @@ a.action { float: right; font-size: 0.9em;} |
| 120 | 125 | #nav-bar li.apps a { background-image: url(images/icons/briefcase.png); } |
| 121 | 126 | #nav-bar li.errs a { background-image: url(images/icons/error.png); } |
| 122 | 127 | #nav-bar li.users a { background-image: url(images/icons/user.png); } |
| 123 | -#nav-bar li:hover { | |
| 128 | +#nav-bar li:not(.active):hover { | |
| 124 | 129 | box-shadow: 0 0 3px #69c; |
| 125 | 130 | -moz-box-shadow: 0 0 3px #69c; |
| 126 | 131 | -webkit-box-shadow: 0 0 3px #69c; |
| 127 | 132 | } |
| 128 | 133 | #nav-bar li.active { |
| 129 | 134 | border-color: #fff; |
| 130 | - background-color: #CCC; | |
| 135 | + background-color: #d0d0d0; | |
| 131 | 136 | background-image: none; |
| 132 | - box-shadow: inset 0 0 5px #999; | |
| 133 | - -moz-box-shadow: inset 0 0 5px #999; | |
| 134 | - -webkit-box-shadow: inset 0 0 5px #999; | |
| 137 | + border-width:1px 1px 0; | |
| 138 | + margin-bottom:-2px; | |
| 139 | + height:40px; | |
| 135 | 140 | } |
| 136 | 141 | |
| 137 | 142 | /* Content Wrapper */ |
| ... | ... | @@ -144,7 +149,7 @@ a.action { float: right; font-size: 0.9em;} |
| 144 | 149 | padding: 30px 20px; |
| 145 | 150 | border-top: 1px solid #FFF; |
| 146 | 151 | border-bottom: 1px solid #FFF; |
| 147 | - background-color: #e2e2e2; | |
| 152 | + background-color: #ececec; | |
| 148 | 153 | } |
| 149 | 154 | #content-comments { |
| 150 | 155 | background-color: #ffffff; |
| ... | ... | @@ -296,7 +301,7 @@ form input[type=submit].button { |
| 296 | 301 | font-size: 1em; |
| 297 | 302 | text-transform: none; |
| 298 | 303 | } |
| 299 | -form div.buttons { | |
| 304 | +form div.buttons { | |
| 300 | 305 | color: #666; |
| 301 | 306 | background: #FFF url(images/button-bg.png) 0 bottom repeat-x; |
| 302 | 307 | border-radius: 50px; |
| ... | ... | @@ -327,11 +332,6 @@ form div.buttons button.sign_in { |
| 327 | 332 | padding-left: 40px; |
| 328 | 333 | background: transparent url(images/icons/right-arrow.png) 3px 3px no-repeat; |
| 329 | 334 | } |
| 330 | -form > div > span { | |
| 331 | - display: block; margin-top: 0.5em; | |
| 332 | - font-size: 0.85em; | |
| 333 | - color: #787878; | |
| 334 | -} | |
| 335 | 335 | form strong.option { |
| 336 | 336 | display: block; |
| 337 | 337 | margin: 0.7em 0; |
| ... | ... | @@ -386,15 +386,18 @@ table thead th { |
| 386 | 386 | border-top: 1px solid #FFF; |
| 387 | 387 | border-bottom: 1px solid #FFF; |
| 388 | 388 | } |
| 389 | -table tbody tr:first-child td { | |
| 390 | - border-top: 1px solid #C6C6C6; | |
| 391 | -} | |
| 392 | 389 | table th, table td { |
| 393 | 390 | border-top: 1px solid #C6C6C6; |
| 394 | 391 | padding: 10px 8px; |
| 395 | 392 | text-align: left; |
| 396 | 393 | } |
| 397 | -table th { background-color: #E2E2E2; font-weight: bold; text-transform: uppercase; white-space: nowrap; } | |
| 394 | +table tbody tr:first-child th, table tbody tr:first-child td { | |
| 395 | + border-top: none; | |
| 396 | +} | |
| 397 | +table thead + tbody tr:first-child td { | |
| 398 | + border-top: 1px solid #C6C6C6; | |
| 399 | +} | |
| 400 | +table th { background-color: #ececec; font-weight: bold; text-transform: uppercase; white-space: nowrap; } | |
| 398 | 401 | table tbody tr:nth-child(odd) td { background-color: #F9F9F9; } |
| 399 | 402 | table .main { width: 100%; } |
| 400 | 403 | |
| ... | ... | @@ -431,56 +434,9 @@ pre { |
| 431 | 434 | margin: 24px 0; |
| 432 | 435 | text-align: center; |
| 433 | 436 | } |
| 434 | -.pagination a, .pagination em, .pagination span { | |
| 435 | - display: inline-block; | |
| 436 | - padding: 0 0.8em; | |
| 437 | - margin: 0 0.2em; | |
| 438 | - line-height: 30px; | |
| 439 | - color: #666; | |
| 440 | - background: #FFF url(images/button-bg.png) 0 50% repeat-x; | |
| 441 | - border: 1px solid #BBB; | |
| 442 | - border-radius: 5px; | |
| 443 | - -moz-border-radius: 5px; | |
| 444 | - -webkit-border-radius: 5px; | |
| 445 | -} | |
| 446 | -.pagination a:hover { | |
| 447 | - text-decoration: none !important; | |
| 448 | - box-shadow: 0px 0px 4px #69C; | |
| 449 | - -moz-box-shadow: 0px 0px 4px #69C; | |
| 450 | - -webkit-box-shadow: 0px 0px 4px #69C | |
| 451 | -} | |
| 452 | -.pagination .previous_page, .pagination .next_page { | |
| 453 | - padding: 0 1em; | |
| 454 | - margin: 0 2.5em 0 0; | |
| 455 | - line-height: 39px; | |
| 456 | - border-radius: 39px; | |
| 457 | - -moz-border-radius: 39px; | |
| 458 | - -webkit-border-radius: 39px; | |
| 459 | -} | |
| 460 | -.pagination .next_page { | |
| 461 | - margin: 0 0 0 2.5em; | |
| 462 | -} | |
| 463 | -.pagination .disabled { | |
| 464 | - opacity: 0.5; | |
| 465 | - -moz-opacity: 0.5; | |
| 466 | - -webkit-opacity: 0.5; | |
| 467 | - cursor: no-drop; | |
| 468 | -} | |
| 469 | 437 | .pagination em { |
| 470 | - padding: 0em 0.6em; | |
| 471 | - line-height: 26px; | |
| 472 | - background-color: #CCC; | |
| 473 | - background-image: none; | |
| 474 | - border-color: #FFF; | |
| 475 | - box-shadow: inset 0 0 5px #999; | |
| 476 | - -moz-box-shadow: inset 0 0 5px #999; | |
| 477 | - -webkit-box-shadow: inset 0 0 5px #999; | |
| 478 | 438 | font-style: normal; |
| 479 | -} | |
| 480 | -.pagination .gap { | |
| 481 | - padding: 0 0.4em; | |
| 482 | - border: none; | |
| 483 | - background: none; | |
| 439 | + font-weight: bold; | |
| 484 | 440 | } |
| 485 | 441 | |
| 486 | 442 | /* Buttons */ |
| ... | ... | @@ -495,10 +451,9 @@ a.button { |
| 495 | 451 | border-radius: 30px; |
| 496 | 452 | -moz-border-radius: 30px; |
| 497 | 453 | -webkit-border-radius: 30px; |
| 498 | - box-shadow: inset 0px 0px 4px #FFF; | |
| 499 | - -moz-box-shadow: inset 0px 0px 4px #FFF; | |
| 500 | - -webkit-box-shadow: inset 0px 0px 4px #FFF; | |
| 501 | 454 | line-height: 30px; |
| 455 | + min-width: 54px; | |
| 456 | + text-align: center; | |
| 502 | 457 | } |
| 503 | 458 | input[type="submit"]:hover.button, |
| 504 | 459 | a:hover.button { |
| ... | ... | @@ -512,26 +467,36 @@ a.button.active { |
| 512 | 467 | border-color: #fff; |
| 513 | 468 | background-color: #CCC; |
| 514 | 469 | background-image: none; |
| 515 | - box-shadow: inset 0 0 5px #999; | |
| 516 | - -moz-box-shadow: inset 0 0 5px #999; | |
| 517 | - -webkit-box-shadow: inset 0 0 5px #999; | |
| 518 | 470 | } |
| 519 | 471 | |
| 520 | 472 | |
| 521 | 473 | /* Tab Bar */ |
| 522 | 474 | .tab-bar { |
| 523 | - margin-bottom: 24px; | |
| 524 | - background-color: #E2E2E2; | |
| 525 | - border: 1px solid #BBB; | |
| 475 | + margin-top: 12px; | |
| 476 | +} | |
| 477 | +#content .tab-bar a.button { | |
| 478 | + border-bottom:0; | |
| 479 | + margin-bottom:0; | |
| 480 | + border-top-left-radius:12px; | |
| 481 | + border-top-right-radius:12px; | |
| 482 | + border-bottom-left-radius:0; | |
| 483 | + border-bottom-right-radius:0; | |
| 484 | + height:30px; | |
| 485 | +} | |
| 486 | +#content .tab-bar a.button.active { | |
| 487 | + background-color:white; | |
| 488 | + border-color:#ccc; | |
| 489 | + border-style:solid; | |
| 490 | + border-width:1px 1px 0; | |
| 491 | + margin-bottom:-1px; | |
| 492 | + height:31px; | |
| 526 | 493 | } |
| 527 | 494 | .tab-bar ul { |
| 528 | - padding: 9px 12px; | |
| 529 | - border-top: 1px solid #FFF; | |
| 530 | - border-bottom: 1px solid #FFF; | |
| 495 | + padding: 9px 0 0; | |
| 496 | + line-height:0; | |
| 531 | 497 | } |
| 532 | 498 | .tab-bar li { |
| 533 | 499 | display: inline-block; |
| 534 | - margin-right: 14px; | |
| 535 | 500 | } |
| 536 | 501 | |
| 537 | 502 | /* Watchers and Issue Tracker Forms */ |
| ... | ... | @@ -586,6 +551,10 @@ table.apps tbody tr:hover td ,table.errs tbody tr:hover td { background-color: # |
| 586 | 551 | table.apps td.name, table.errs td.message { |
| 587 | 552 | width: 100%; |
| 588 | 553 | } |
| 554 | +td.message .line { | |
| 555 | + display:inline-block; | |
| 556 | + margin-left:1em; | |
| 557 | +} | |
| 589 | 558 | td.deploy { |
| 590 | 559 | white-space: nowrap; |
| 591 | 560 | } |
| ... | ... | @@ -687,44 +656,26 @@ table.deploys td.when { |
| 687 | 656 | width: 100%; |
| 688 | 657 | margin-bottom: 1em; |
| 689 | 658 | overflow: auto; |
| 659 | + border:1px solid #ccc; | |
| 660 | + padding:1px; | |
| 690 | 661 | } |
| 691 | 662 | |
| 692 | 663 | .window table { |
| 693 | 664 | margin: 0; |
| 694 | 665 | } |
| 695 | 666 | |
| 696 | -table.backtrace td, table.backtrace th { | |
| 697 | - border-top: none; | |
| 698 | -} | |
| 699 | -table.backtrace td { | |
| 700 | - width: 100%; | |
| 701 | - padding: 0; | |
| 702 | - margin: 0; | |
| 703 | - color: #C7C7C7; | |
| 704 | - background-color: #222; | |
| 667 | +table.backtrace td.line { | |
| 668 | + color: #777; /* 47% */ | |
| 669 | + background-color: #222 !important; | |
| 670 | + border: none; | |
| 671 | + padding: 1px 8px; | |
| 705 | 672 | } |
| 706 | - | |
| 707 | -/* remove alternating color rules */ | |
| 708 | -table.backtrace tr:nth-child(2n+1) td { background-color: #222; } | |
| 709 | -table.backtrace tr:first-child td { border-top: 0; } | |
| 710 | - | |
| 711 | -table.backtrace th.line-numbers { | |
| 712 | - border-bottom: 1px solid #F0F0F0; | |
| 713 | - font-size: 13px; | |
| 714 | - text-align: right; | |
| 715 | - vertical-align: top; | |
| 716 | - padding: 1px 6px 1px 7px; | |
| 673 | +table.backtrace td.line .file { | |
| 674 | + color:#d2d2d2; /* 82% */ | |
| 717 | 675 | } |
| 718 | - | |
| 719 | -table.backtrace td.lines { | |
| 720 | - font-size: 13px; | |
| 721 | - padding-left: 8px; | |
| 722 | - vertical-align: top; | |
| 723 | - white-space: nowrap; | |
| 724 | -} | |
| 725 | -table.backtrace td.lines.in-app { | |
| 726 | - color: #2adb2e; | |
| 727 | - background-color: #2f2f2f; | |
| 676 | +table.backtrace td.line .method { | |
| 677 | + color:#ececec; /* 93% */ | |
| 678 | + font-weight:bold; | |
| 728 | 679 | } |
| 729 | 680 | |
| 730 | 681 | /* Extra empty rows at top and bottom of table */ |
| ... | ... | @@ -789,3 +740,7 @@ table.errs tr td.message .inline_comment em.commenter { |
| 789 | 740 | color: #888888; |
| 790 | 741 | } |
| 791 | 742 | |
| 743 | +table.backtrace tr.in-app td.line { color:#156E16; /* 43% */} | |
| 744 | +table.backtrace tr.in-app td.line .file { color:#25C227; /* 76% */} | |
| 745 | +table.backtrace tr.in-app td.line .method { color:#2adb2e; /* 86% */} | |
| 746 | + | ... | ... |
spec/models/problem_spec.rb
| 1 | 1 | require 'spec_helper' |
| 2 | 2 | |
| 3 | 3 | describe Problem do |
| 4 | - | |
| 5 | - | |
| 6 | 4 | context '#last_notice_at' do |
| 7 | 5 | it "returns the created_at timestamp of the latest notice" do |
| 8 | 6 | err = Factory(:err) |
| ... | ... | @@ -131,7 +129,6 @@ describe Problem do |
| 131 | 129 | |
| 132 | 130 | |
| 133 | 131 | context "notice counter cache" do |
| 134 | - | |
| 135 | 132 | before do |
| 136 | 133 | @app = Factory(:app) |
| 137 | 134 | @problem = Factory(:problem, :app => @app) |
| ... | ... | @@ -158,5 +155,46 @@ describe Problem do |
| 158 | 155 | end |
| 159 | 156 | |
| 160 | 157 | |
| 158 | + context "#app_name" do | |
| 159 | + before do | |
| 160 | + @app = Factory(:app) | |
| 161 | + end | |
| 162 | + | |
| 163 | + it "is set when a problem is created" do | |
| 164 | + problem = Factory(:problem, :app => @app) | |
| 165 | + assert_equal @app.name, problem.app_name | |
| 166 | + end | |
| 167 | + | |
| 168 | + it "is updated when an app is updated" do | |
| 169 | + problem = Factory(:problem, :app => @app) | |
| 170 | + lambda { | |
| 171 | + @app.update_attributes!(:name => "Bar App") | |
| 172 | + problem.reload | |
| 173 | + }.should change(problem, :app_name).to("Bar App") | |
| 174 | + end | |
| 175 | + end | |
| 176 | + | |
| 177 | + | |
| 178 | + context "#last_deploy_at" do | |
| 179 | + before do | |
| 180 | + @app = Factory(:app) | |
| 181 | + @last_deploy = 10.days.ago.localtime.round(0) | |
| 182 | + deploy = Factory(:deploy, :app => @app, :created_at => @last_deploy) | |
| 183 | + end | |
| 184 | + | |
| 185 | + it "is set when a problem is created" do | |
| 186 | + problem = Factory(:problem, :app => @app) | |
| 187 | + assert_equal @last_deploy, problem.last_deploy_at | |
| 188 | + end | |
| 189 | + | |
| 190 | + it "is updated when a deploy is created" do | |
| 191 | + problem = Factory(:problem, :app => @app) | |
| 192 | + next_deploy = 5.minutes.ago.localtime.round(0) | |
| 193 | + lambda { | |
| 194 | + @deploy = Factory(:deploy, :app => @app, :created_at => next_deploy) | |
| 195 | + problem.reload | |
| 196 | + }.should change(problem, :last_deploy_at).from(@last_deploy).to(next_deploy) | |
| 197 | + end | |
| 198 | + end | |
| 161 | 199 | end |
| 162 | 200 | ... | ... |