exception_hunter 0.2.0 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (56) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +103 -6
  3. data/app/assets/stylesheets/exception_hunter/base.css +66 -8
  4. data/app/assets/stylesheets/exception_hunter/errors.css +188 -25
  5. data/app/assets/stylesheets/exception_hunter/navigation.css +20 -5
  6. data/app/assets/stylesheets/exception_hunter/sessions.css +71 -0
  7. data/app/controllers/concerns/exception_hunter/authorization.rb +23 -0
  8. data/app/controllers/exception_hunter/application_controller.rb +2 -0
  9. data/app/controllers/exception_hunter/errors_controller.rb +29 -4
  10. data/app/controllers/exception_hunter/ignored_errors_controller.rb +25 -0
  11. data/app/controllers/exception_hunter/resolved_errors_controller.rb +11 -0
  12. data/app/helpers/exception_hunter/application_helper.rb +22 -0
  13. data/app/helpers/exception_hunter/sessions_helper.rb +16 -0
  14. data/app/jobs/exception_hunter/send_notification_job.rb +15 -0
  15. data/app/models/exception_hunter/application_record.rb +8 -0
  16. data/app/models/exception_hunter/error.rb +24 -7
  17. data/app/models/exception_hunter/error_group.rb +24 -5
  18. data/app/presenters/exception_hunter/dashboard_presenter.rb +56 -0
  19. data/app/presenters/exception_hunter/error_group_presenter.rb +25 -0
  20. data/app/presenters/exception_hunter/error_presenter.rb +3 -2
  21. data/app/views/exception_hunter/devise/sessions/new.html.erb +24 -0
  22. data/app/views/exception_hunter/errors/_error_row.erb +52 -0
  23. data/app/views/exception_hunter/errors/_error_summary.erb +5 -5
  24. data/app/views/exception_hunter/errors/_errors_table.erb +1 -0
  25. data/app/views/exception_hunter/errors/_last_7_days_errors_table.erb +12 -0
  26. data/app/views/exception_hunter/errors/index.html.erb +84 -29
  27. data/app/views/exception_hunter/errors/pagy/_pagy_nav.html.erb +15 -15
  28. data/app/views/exception_hunter/errors/show.html.erb +58 -22
  29. data/app/views/layouts/exception_hunter/application.html.erb +67 -6
  30. data/app/views/layouts/exception_hunter/exception_hunter_logged_out.html.erb +24 -0
  31. data/config/rails_best_practices.yml +3 -3
  32. data/config/routes.rb +21 -1
  33. data/lib/exception_hunter.rb +44 -2
  34. data/lib/exception_hunter/config.rb +25 -1
  35. data/lib/exception_hunter/data_redacter.rb +27 -0
  36. data/lib/exception_hunter/devise.rb +19 -0
  37. data/lib/exception_hunter/engine.rb +6 -0
  38. data/lib/exception_hunter/error_creator.rb +72 -0
  39. data/lib/exception_hunter/error_reaper.rb +20 -0
  40. data/lib/exception_hunter/middleware/delayed_job_hunter.rb +70 -0
  41. data/lib/exception_hunter/middleware/request_hunter.rb +5 -2
  42. data/lib/exception_hunter/middleware/sidekiq_hunter.rb +1 -1
  43. data/lib/exception_hunter/notifiers/misconfigured_notifiers.rb +10 -0
  44. data/lib/exception_hunter/notifiers/slack_notifier.rb +42 -0
  45. data/lib/exception_hunter/notifiers/slack_notifier_serializer.rb +20 -0
  46. data/lib/exception_hunter/tracking.rb +35 -0
  47. data/lib/exception_hunter/user_attributes_collector.rb +21 -0
  48. data/lib/exception_hunter/version.rb +1 -1
  49. data/lib/generators/exception_hunter/create_users/create_users_generator.rb +8 -1
  50. data/lib/generators/exception_hunter/install/install_generator.rb +3 -1
  51. data/lib/generators/exception_hunter/install/templates/create_exception_hunter_error_groups.rb.erb +3 -0
  52. data/lib/generators/exception_hunter/install/templates/exception_hunter.rb.erb +52 -0
  53. data/lib/tasks/exception_hunter_tasks.rake +6 -4
  54. metadata +46 -12
  55. data/app/services/exception_hunter/error_creator.rb +0 -41
  56. data/config/initializers/exception_hunter.rb +0 -16
@@ -0,0 +1,25 @@
1
+ module ExceptionHunter
2
+ class ErrorGroupPresenter
3
+ delegate_missing_to :error_group
4
+
5
+ def initialize(error_group)
6
+ @error_group = error_group
7
+ end
8
+
9
+ def self.wrap_collection(collection)
10
+ collection.map { |error_group| new(error_group) }
11
+ end
12
+
13
+ def self.format_occurrence_day(day)
14
+ day.to_date.strftime('%A, %B %d')
15
+ end
16
+
17
+ def show_for_day?(day)
18
+ last_occurrence.in_time_zone.to_date == day.to_date
19
+ end
20
+
21
+ private
22
+
23
+ attr_reader :error_group
24
+ end
25
+ end
@@ -1,6 +1,7 @@
1
1
  module ExceptionHunter
2
2
  class ErrorPresenter
3
3
  delegate_missing_to :error
4
+ delegate :tags, to: :error_group
4
5
 
5
6
  BacktraceLine = Struct.new(:path, :file_name, :line_number, :method_call)
6
7
 
@@ -9,13 +10,13 @@ module ExceptionHunter
9
10
  end
10
11
 
11
12
  def backtrace
12
- error.backtrace.map do |line|
13
+ (error.backtrace || []).map do |line|
13
14
  format_backtrace_line(line)
14
15
  end
15
16
  end
16
17
 
17
18
  def environment_data
18
- error.environment_data.except('params')
19
+ error.environment_data&.except('params') || {}
19
20
  end
20
21
 
21
22
  def tracked_params
@@ -0,0 +1,24 @@
1
+ <div class="login_form_container">
2
+ <div class="login_left_container">
3
+ <div class="left_column">Exception Hunter</div>
4
+ </div>
5
+ <div class="login_right_container">
6
+ <%= form_for(resource, as: resource_name, url: exception_hunter_create_session_path) do |f| %>
7
+ <div class="row">
8
+ Sign into your account
9
+ </div>
10
+ <div class="login_row row"></div>
11
+ <div class="login_row row">
12
+ <%= f.email_field :email, autofocus: true, autocomplete: "email", placeholder: 'Email' %>
13
+ </div>
14
+ <div class="login_row row">
15
+ <%= f.password_field :password, autocomplete: "current-password", placeholder: 'Password'%>
16
+ </div>
17
+ <div class="login_button row">
18
+ <div class="column column-50 column-offset-50">
19
+ <input class="button-log-in" type="submit" value="Log in">
20
+ </div>
21
+ </div>
22
+ <% end %>
23
+ </div>
24
+ </div>
@@ -0,0 +1,52 @@
1
+ <div class="row error-row">
2
+ <div class="column column-10 error-cell">
3
+ <% error.tags.each do |tag| %>
4
+ <div class="error-cell__tags">
5
+ <span class="error-tag"><%= tag %></span>
6
+ </div>
7
+ <% end %>
8
+ </div>
9
+ <div class="column column-33 error-cell error-cell__message">
10
+ <%= link_to error.message, error_path(error.id), class: %w[error-message] %>
11
+ </div>
12
+
13
+ <div class="column column-15 error-cell">
14
+ <% if error.first_occurrence.present? %>
15
+ <%= time_ago_in_words(error.first_occurrence) %> ago
16
+ <% else %>
17
+ Never
18
+ <% end %>
19
+ </div>
20
+
21
+ <div class="column column-15 error-cell">
22
+ <% if error.last_occurrence.present? %>
23
+ <%= time_ago_in_words(error.last_occurrence) %> ago
24
+ <% else %>
25
+ Never
26
+ <% end %>
27
+ </div>
28
+
29
+ <div class="column column-8 error-cell">
30
+ <%= error.total_occurrences %>
31
+ </div>
32
+
33
+ <div class="column column-18 error-cell">
34
+ <div class="row">
35
+ <% if error.active? %>
36
+ <div class="column mr-5">
37
+ <%= display_action_button('resolve', error) %>
38
+ </div>
39
+ <div class="column">
40
+ <%= display_action_button('ignore', error) %>
41
+ </div>
42
+ <% elsif error.ignored? %>
43
+ <div class="column">
44
+ <%= button_to('Reopen', reopen_path(error_group: { id: error.id }),
45
+ class: %w[button button-outline resolve-button],
46
+ data: { confirm: 'Are you sure you want to reopen this error?' }) %>
47
+ </div>
48
+ <% end %>
49
+ </div>
50
+ </div>
51
+ </div>
52
+
@@ -1,9 +1,9 @@
1
1
  <% if error.environment_data.empty? %>
2
- <div class="row error-row-no-border data-title">
2
+ <div class="row error-row data-title">
3
3
  Unfortunately, no environment information has been registered for this error.
4
4
  </div>
5
5
  <% else %>
6
- <div class="row error-row-no-border data-title">
6
+ <div class="row error-row data-title">
7
7
  Environment Data
8
8
  </div>
9
9
  <pre class="tracked-data">
@@ -13,7 +13,7 @@
13
13
  <% end %>
14
14
 
15
15
  <% unless error.tracked_params.nil? %>
16
- <div class="row error-row-no-border data-title">
16
+ <div class="row error-row data-title">
17
17
  Tracked Params
18
18
  </div>
19
19
  <pre class="tracked-data">
@@ -23,11 +23,11 @@
23
23
  <% end %>
24
24
 
25
25
  <% if error.custom_data.nil? %>
26
- <div class="row error-row-no-border data-title">
26
+ <div class="row error-row data-title">
27
27
  No custom data included.
28
28
  </div>
29
29
  <% else %>
30
- <div class="row error-row-no-border data-title">
30
+ <div class="row error-row data-title">
31
31
  Custom Data
32
32
  </div>
33
33
  <pre class="tracked-data">
@@ -0,0 +1 @@
1
+ <%= render partial: 'exception_hunter/errors/error_row', collection: errors, as: :error %>
@@ -0,0 +1,12 @@
1
+ <% today_errors = errors.select { |error| error.show_for_day?(Date.current) } %>
2
+ <%= render partial: 'exception_hunter/errors/error_row', collection: today_errors, as: :error %>
3
+
4
+ <% yesterday_errors = errors.select { |error| error.show_for_day?(Date.yesterday) } %>
5
+ <div class="errors-date-group">Yesterday</div>
6
+ <%= render partial: 'exception_hunter/errors/error_row', collection: yesterday_errors, as: :error %>
7
+
8
+ <% (2..6).each do |i| %>
9
+ <% errors_on_day = errors.select { |error| error.show_for_day?(i.days.ago) } %>
10
+ <div class="errors-date-group"><%= ExceptionHunter::ErrorGroupPresenter.format_occurrence_day(i.days.ago) %></div>
11
+ <%= render partial: 'exception_hunter/errors/error_row', collection: errors_on_day, as: :error %>
12
+ <% end %>
@@ -1,36 +1,91 @@
1
- <div class="row statistics-row">
2
- <div class="column column-offset-50 column-25">
3
- <div class="statistics__cell">
4
- <%= number_with_delimiter(@errors_count) %> Total errors
1
+ <div class="row">
2
+ <div class="column column-80">
3
+ <div class="errors-tabs">
4
+ <div class="errors-tab <%= 'errors-tab--active' if @dashboard.tab_active?(@dashboard.class::LAST_7_DAYS_TAB) %>">
5
+ <%= link_to errors_path(tab: @dashboard.class::LAST_7_DAYS_TAB) do %>
6
+ <div class="errors-tab__content">
7
+ <div class="errors-tab__badge">
8
+ <%= @dashboard.errors_count(@dashboard.class::LAST_7_DAYS_TAB) %>
9
+ </div>
10
+ <div class="errors-tab__description">
11
+ Errors in the last 7 days
12
+ </div>
13
+ </div>
14
+ <% end %>
15
+ </div>
16
+
17
+ <div class="errors-tab <%= 'errors-tab--active' if @dashboard.tab_active?(@dashboard.class::CURRENT_MONTH_TAB) %>">
18
+ <%= link_to errors_path(tab: @dashboard.class::CURRENT_MONTH_TAB) do %>
19
+ <div class="errors-tab__content">
20
+ <div class="errors-tab__badge">
21
+ <%= @dashboard.errors_count(@dashboard.class::CURRENT_MONTH_TAB) %>
22
+ </div>
23
+ <div class="errors-tab__description">
24
+ Errors this month
25
+ </div>
26
+ </div>
27
+ <% end %>
28
+ </div>
29
+
30
+ <div class="errors-tab <%= 'errors-tab--active' if @dashboard.tab_active?(@dashboard.class::TOTAL_ERRORS_TAB) %>">
31
+ <%= link_to errors_path(tab: @dashboard.class::TOTAL_ERRORS_TAB) do %>
32
+ <div class="errors-tab__content">
33
+ <div class="errors-tab__badge">
34
+ <%= @dashboard.errors_count(@dashboard.class::TOTAL_ERRORS_TAB) %>
35
+ </div>
36
+ <div class="errors-tab__description">
37
+ Total errors
38
+ </div>
39
+ </div>
40
+ <% end %>
41
+ </div>
42
+
43
+ <div class="errors-tab errors-tab__resolved <%= 'errors-tab--active' if @dashboard.tab_active?(@dashboard.class::RESOLVED_ERRORS_TAB) %>">
44
+ <%= link_to errors_path(tab: @dashboard.class::RESOLVED_ERRORS_TAB) do %>
45
+ <div class="errors-tab__content">
46
+ <div class="errors-tab__badge">
47
+ <%= @dashboard.errors_count(@dashboard.class::RESOLVED_ERRORS_TAB) || '-' %>
48
+ </div>
49
+ <div class="errors-tab__description">
50
+ Resolved
51
+ </div>
52
+ </div>
53
+ <% end %>
54
+ </div>
55
+
56
+ <div class="errors-tab errors-tab__ignored <%= 'errors-tab--active' if @dashboard.tab_active?(@dashboard.class::IGNORED_ERRORS_TAB) %>">
57
+ <%= link_to errors_path(tab: @dashboard.class::IGNORED_ERRORS_TAB) do %>
58
+ <div class="errors-tab__content">
59
+ <div class="errors-tab__badge">
60
+ <%= @dashboard.errors_count(@dashboard.class::IGNORED_ERRORS_TAB) || '-' %>
61
+ </div>
62
+ <div class="errors-tab__description">
63
+ Ignored
64
+ </div>
65
+ </div>
66
+ <% end %>
67
+ </div>
5
68
  </div>
6
69
  </div>
7
- <div class="column column-25">
8
- <div class="statistics__cell">
9
- <%= number_with_delimiter(@month_errors) %> Errors this month
10
- </div>
70
+
71
+ <div class="column column-10 column-offset-10">
72
+ <%= button_to 'Purge', purge_errors_path,
73
+ class: %w[button purge-button],
74
+ method: :delete,
75
+ data: { confirm: 'This will delete all stale errors, do you want to continue?' } %>
11
76
  </div>
12
77
  </div>
13
78
 
14
- <div class="row error-row error-row--header">
15
- <div class="column column-75">Message</div>
16
- <div class="column column-15">Last Occurrence</div>
17
- <div class="column column-10">Occurrences</div>
18
- </div>
79
+ <div class="errors__container">
80
+ <div class="row error-row error-row--header">
81
+ <div class="column column-10">Tags</div>
82
+ <div class="column column-33">Message</div>
83
+ <div class="column column-15">First Occurrence</div>
84
+ <div class="column column-15">Last Occurrence</div>
85
+ <div class="column column-8">Total</div>
86
+ <div class="column column-18"></div>
19
87
 
20
- <% @errors.each do |error| %>
21
- <div class="row error-row">
22
- <div class="column column-75 error-cell">
23
- <%= link_to error.message, error_path(error.id) %>
24
- </div>
25
- <div class="column column-15 error-cell error-cell--highlight">
26
- <% if error.last_occurrence.present? %>
27
- <%= time_ago_in_words(error.last_occurrence) %> ago
28
- <% else %>
29
- Never
30
- <% end %>
31
- </div>
32
- <div class="column column-10 error-cell error-cell--highlight">
33
- <%= error.total_occurrences %>
34
- </div>
35
88
  </div>
36
- <% end %>
89
+
90
+ <%= render partial: @dashboard.partial_for_tab, locals: { errors: @errors } %>
91
+ </div>
@@ -1,17 +1,17 @@
1
- <% link = pagy_link_proc(pagy) %>
2
- <nav aria-label="pager" class="pagy_nav pagination" role="navigation">
3
- <% if pagy.prev %>
4
- <span class="page prev"><%== link.call(pagy.prev, '◄', 'aria-label="previous"') %></span>
5
- <% else %>
6
- <span class="page prev disabled">
7
-
8
- </span>
1
+ <div class="error-occurrences__nav">
2
+ <%= link_to pagy_url_for(1, pagy) do %>
3
+ <%= button_tag 'First', class: %w[button button-outline error-occurrences__nav-link], disabled: pagy.prev.nil? %>
9
4
  <% end %>
10
- <% if pagy.next %>
11
- <span class="page next"><%== link.call(pagy.next, '►', 'aria-label="next"') %></span>
12
- <% else %>
13
- <span class="page next disabled">
14
-
15
- </span>
5
+ <%= link_to pagy_url_for(pagy.prev, pagy) do %>
6
+ <%= button_tag '<', class: %w[button button-outline error-occurrences__nav-link], disabled: pagy.prev.nil? %>
16
7
  <% end %>
17
- </nav>
8
+ <div class="error-occurrences__nav-current">
9
+ <%= occurred_at %> (<%= pagy.page %>/<%= pagy.last %>)
10
+ </div>
11
+ <%= link_to pagy_url_for(pagy.next, pagy) do %>
12
+ <%= button_tag '>', class: %w[button button-outline error-occurrences__nav-link], disabled: pagy.next.nil? %>
13
+ <% end %>
14
+ <%= link_to pagy_url_for(pagy.last, pagy) do %>
15
+ <%= button_tag 'Last', class: %w[button button-outline error-occurrences__nav-link], disabled: pagy.next.nil? %>
16
+ <% end %>
17
+ </div>
@@ -1,33 +1,69 @@
1
- <div class="row">
2
- <div class="column column-offset-80 column-20">
3
- <%= render partial: 'exception_hunter/errors/pagy/pagy_nav', locals: { pagy: @pagy } %>
1
+ <div class="row error-occurrence__header">
2
+ <div class="column column-10">
3
+ <% @error.tags.each do|tag| %>
4
+ <div class="error-cell__tags">
5
+ <span class="error-tag"><%= tag %></span>
6
+ </div>
7
+ <% end %>
8
+ </div>
9
+ <div class="column column-80">
10
+ <div class="error-title">
11
+ <%= @error.class_name %>: <%= @error.message %>
12
+ </div>
13
+ </div>
14
+ <div class="column column-10">
15
+ <%= button_to('Resolve', resolved_errors_path(error_group: { id: @error.error_group_id }),
16
+ method: :post,
17
+ class: %w[button resolve-button],
18
+ data: { confirm: 'Are you sure you want to resolve this error?' }) %>
4
19
  </div>
5
20
  </div>
6
21
 
7
- <div class="error-title">
8
- <%= @error.class_name %>: <%= @error.message %>
9
- </div>
10
- <div class="error-occurred_at">
11
- <%= @error.occurred_at %>
22
+ <div class="row">
23
+ <div class="column column-50">
24
+ <ul data-tabs class="errors-tabs">
25
+ <li class="errors-tab">
26
+ <a data-tabby-default href="#summary">
27
+ <div class="errors-tab__content">
28
+ Summary
29
+ </div>
30
+ </a>
31
+ </li>
32
+ <li class="errors-tab">
33
+ <a href="#backtrace">
34
+ <div class="errors-tab__content">
35
+ Backtrace
36
+ </div>
37
+ </a>
38
+ </li>
39
+ <li class="errors-tab">
40
+ <a href="#user-data">
41
+ <div class="errors-tab__content">
42
+ User Data
43
+ </div>
44
+ </a>
45
+ </li>
46
+ </ul>
47
+ </div>
48
+ <div class="column column-50">
49
+ <%= render partial: 'exception_hunter/errors/pagy/pagy_nav', locals: { pagy: @pagy, occurred_at: @error.occurred_at } %>
50
+ </div>
12
51
  </div>
13
52
 
14
- <ul data-tabs>
15
- <li><a data-tabby-default href="#summary">Summary</a></li>
16
- <li><a href="#backtrace">Backtrace</a></li>
17
- <li><a href="#user-data">User Data</a></li>
18
- </ul>
19
53
 
20
- <div class="tab-content">
21
- <div id="summary">
22
- <%= render partial: 'exception_hunter/errors/error_summary', locals: { error: @error } %>
23
- </div>
54
+ <div class="errors__container">
55
+ <div class="tab-content">
56
+ <div id="summary">
57
+ <%= render partial: 'exception_hunter/errors/error_summary', locals: { error: @error } %>
58
+ </div>
24
59
 
25
- <div id="backtrace">
26
- <%= render partial: 'exception_hunter/errors/error_backtrace', locals: { error: @error } %>
27
- </div>
60
+ <div id="backtrace">
61
+ <%= render partial: 'exception_hunter/errors/error_backtrace', locals: { error: @error } %>
62
+ </div>
28
63
 
29
- <div id="user-data">
30
- <%= render partial: 'exception_hunter/errors/error_user_data', locals: { error: @error } %>
64
+ <div id="user-data">
65
+ <%= render partial: 'exception_hunter/errors/error_user_data', locals: { error: @error } %>
66
+ </div>
31
67
  </div>
32
68
  </div>
33
69
 
@@ -7,12 +7,11 @@
7
7
 
8
8
  <%= favicon_link_tag 'exception_hunter/logo.png' %>
9
9
 
10
- <link rel="stylesheet" href="//fonts.googleapis.com/css?family=Roboto:300,300italic,700,700italic">
10
+ <link rel="stylesheet" href="https://rsms.me/inter/inter.css">
11
11
  <link rel="stylesheet" href="//cdnjs.cloudflare.com/ajax/libs/normalize/5.0.0/normalize.css">
12
12
  <link rel="stylesheet" href="//cdnjs.cloudflare.com/ajax/libs/milligram/1.3.0/milligram.css">
13
13
 
14
14
  <!-- Get patch fixes within a minor version -->
15
- <link rel="stylesheet" type="text/css" href="https://cdn.jsdelivr.net/gh/cferdinandi/tabby@12.0/dist/css/tabby-ui.min.css">
16
15
  <script src="https://cdn.jsdelivr.net/gh/cferdinandi/tabby@12.0/dist/js/tabby.polyfills.min.js"></script>
17
16
 
18
17
  <%= stylesheet_link_tag "exception_hunter/application", media: "all" %>
@@ -22,18 +21,80 @@
22
21
  <div class="wrapper">
23
22
  <nav class="nav">
24
23
  <div class="container container__nav">
25
- <div class="nav__logo">
26
- <%= link_to errors_path do %>
27
- <%= image_tag 'exception_hunter/logo.png', alt: 'Logo' %>
28
- <% end %>
24
+ <div class="row">
25
+ <div class="column column-40">
26
+ <%= link_to errors_path do %>
27
+ <div class="nav__title">
28
+ Exception Hunter / <%= application_name %>
29
+ </div>
30
+ <% end %>
31
+ </div>
32
+ <div class="column column-10 column-offset-50">
33
+ <div class="logout">
34
+ <% if current_admin_user? %>
35
+ <div class="sign_out">
36
+ <%= link_to exception_hunter_logout_path do %>
37
+ Sign out
38
+ <% end %>
39
+ </div>
40
+ <% end %>
41
+ </div>
42
+ </div>
29
43
  </div>
30
44
  </div>
31
45
  </nav>
32
46
 
33
47
  <div class="container">
48
+ <% if flash[:notice] %>
49
+ <div id="flash-notice" class="flash flash--notice row">
50
+ <div class="column column-80">
51
+ <%= flash[:notice] %>
52
+ </div>
53
+ <div class="column column-20">
54
+ <div class="button button-clear button-dismiss" data-dismiss="#flash-notice">
55
+ <%= ['Cool!', 'Nice!', 'Ok', 'Dismiss', 'Fine. Whatever.', 'I know that'].sample %>
56
+ </div>
57
+ </div>
58
+ </div>
59
+ <% end %>
60
+
34
61
  <%= yield %>
35
62
  </div>
63
+
64
+ <div class="footer">
65
+ Made with ♥ at <a href="https://www.rootstrap.com/" class="text--underline">Rootstrap</a>
66
+ </div>
36
67
  </div>
37
68
 
69
+ <script type="text/javascript" charset="utf-8">
70
+ function confirmable(element) {
71
+ const message = element.getAttribute('data-confirm')
72
+ element.form.addEventListener('submit', confirm(message))
73
+
74
+ function confirm(message) {
75
+ return function (event) {
76
+ if (!window.confirm(message)) {
77
+ event.preventDefault()
78
+ }
79
+ }
80
+ }
81
+ }
82
+
83
+ function dismissible(element) {
84
+ element.addEventListener('click', dismiss(element))
85
+
86
+ function dismiss(element) {
87
+ return function () {
88
+ const selector = element.getAttribute('data-dismiss')
89
+ const elementToDismiss = document.querySelector(selector)
90
+ elementToDismiss.remove()
91
+ }
92
+ }
93
+ }
94
+
95
+ Array.from(document.querySelectorAll('[data-confirm]')).forEach(confirmable)
96
+ Array.from(document.querySelectorAll('[data-dismiss]')).forEach(dismissible)
97
+ </script>
98
+
38
99
  </body>
39
100
  </html>