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 | ... | ... |