Commit 1265b058af938c1083c9214a7bfa9687be060df3

Authored by Nathan Broadbent
2 parents 79cbe005 e5498764
Exists in master and in 1 other branch production

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
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  
... ...
app/helpers/sort_helper.rb 0 → 100644
... ... @@ -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
... ... @@ -7,7 +7,7 @@
7 7 %tr
8 8 %th Name
9 9 %th Last Deploy
10   - %th Errs
  10 + %th Errors
11 11 %tbody
12 12 - @apps.each do |app|
13 13 %tr
... ...
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 &amp; Where
8   - %th Latest
9   - %th Deploy
10   - %th Count
  6 + %th= link_for_sort "App"
  7 + %th= link_for_sort "What &amp; 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
1   -- content_for :title, 'All Errs'
  1 +- content_for :title, 'All Errors'
2 2 - content_for :action_bar do
3 3 = link_to 'hide resolved', errs_path, :class => 'button'
4   -= render 'table', :errs => @problems
5 4 \ No newline at end of file
  5 += render 'table', :errs => @problems
... ...
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 &rarr;
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  
... ...