Merge pull request #38 from Evolix/dashboard

Checks listing
This commit is contained in:
Colin Darie 2018-07-05 12:36:33 +02:00 committed by GitHub
commit d83f618fe2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
38 changed files with 1020 additions and 99 deletions

View File

@ -42,6 +42,8 @@ gem 'whenever', require: false
gem 'octicons'
gem 'kaminari'
gem 'has_scope'
# Reduces boot times through caching; required in config/boot.rb
gem 'bootsnap', '>= 1.1.0', require: false

View File

@ -131,6 +131,9 @@ GEM
guard-minitest (2.4.6)
guard-compat (~> 1.2)
minitest (>= 3.0)
has_scope (0.7.2)
actionpack (>= 4.1)
activesupport (>= 4.1)
i18n (1.0.1)
concurrent-ruby (~> 1.0)
io-like (0.3.0)
@ -138,6 +141,18 @@ GEM
activesupport (>= 4.2.0)
multi_json (>= 1.2)
json (2.1.0)
kaminari (1.1.1)
activesupport (>= 4.1.0)
kaminari-actionview (= 1.1.1)
kaminari-activerecord (= 1.1.1)
kaminari-core (= 1.1.1)
kaminari-actionview (1.1.1)
actionview
kaminari-core (= 1.1.1)
kaminari-activerecord (1.1.1)
activerecord
kaminari-core (= 1.1.1)
kaminari-core (1.1.1)
launchy (2.4.3)
addressable (~> 2.3)
letter_opener (1.6.0)
@ -338,7 +353,9 @@ DEPENDENCIES
factory_bot_rails
guard
guard-minitest
has_scope
jbuilder (~> 2.5)
kaminari
launchy
letter_opener_web
listen (>= 3.0.5, < 3.2)

View File

@ -20,4 +20,14 @@ class ApplicationController < ActionController::Base
def set_locale
I18n.locale = current_user.try(:locale) || I18n.default_locale
end
def not_found
fail ActionController::RoutingError, "Not Found"
rescue StandardError
render_404
end
def render_404
render file: "#{Rails.root}/public/404", status: :not_found
end
end

View File

@ -4,14 +4,24 @@ class ChecksController < ApplicationController
after_action :verify_authorized, except: :index
after_action :verify_policy_scoped, only: :index
has_scope :kind
has_scope :by_domain
has_scope :recurrent_failures, type: :boolean
def index
@checks = policy_scope(Check).order(:domain_expires_at)
@checks = apply_scopes(policy_scope(Check)).order(Hash[*current_sort]).page(params[:page])
end
def new
@check = Check.new
build_empty_notification
authorize @check
if params[:kind].present?
return not_found unless Check.kinds.key?(params[:kind])
@check.kind = params[:kind]
end
build_empty_notification
end
def create
@ -20,10 +30,10 @@ class ChecksController < ApplicationController
authorize @check
if @check.save
flash[:notice] = "Your check has been saved."
flash[:notice] = t(".saved")
redirect_to checks_path
else
flash.now[:alert] = "An error occured."
flash.now[:alert] = t(".invalid")
render :new
end
end
@ -73,4 +83,22 @@ class ChecksController < ApplicationController
def build_empty_notification
@check.notifications.build
end
def current_sort
@current_sort ||= clean_sort || Check.default_sort
end
helper_method :current_sort
def clean_sort
return unless params[:sort].present?
field, _, direction = params[:sort].rpartition("_").map(&:to_sym)
valid_fields = [:domain, :domain_expires_at]
valid_directions = [:asc, :desc]
return unless valid_fields.include?(field)
return unless valid_directions.include?(direction)
[field, direction]
end
end

View File

@ -8,10 +8,18 @@
// layout file, like app/views/layouts/application.html.erb
import Rails from 'rails-ujs';
import Turbolinks from 'turbolinks';
import 'bootstrap/js/dist/collapse';
import 'bootstrap/js/dist/dropdown';
import 'bootstrap/js/dist/button';
import 'bootstrap/js/dist/tooltip';
import '../scss';
Rails.start()
Turbolinks.start()
document.addEventListener("turbolinks:load", () => {
$('[data-toggle="tooltip"]').tooltip();
});

View File

@ -0,0 +1,10 @@
$input-placeholder-color: #adb5bd;
$enable-rounded: false;
$theme-colors: (
"primary": #259EDB,
"secondary": #565554,
"success": #42935C,
"warning": #F6AE2D,
"danger": #E94F37,
"info": #2E86AB,
);

View File

@ -1,4 +1,4 @@
.table-checks {
.checks-table {
.action a {
color: black;
}

View File

@ -1,3 +1,4 @@
@import '_variables';
@import '~bootstrap/scss/bootstrap';
@import 'layout';
@import 'icons';

View File

@ -1,4 +1,8 @@
module ApplicationHelper
def format_date(time, format: :long)
l(time.utc.to_date, format: format)
end
def format_utc(time, format: :default)
l(time.utc, format: format)
end

View File

@ -1,14 +1,78 @@
module ChecksHelper
def check_kind_label(check)
check.kind.upcase
end
# frozen_string_literal: true
module ChecksHelper
def check_row_class(check)
expiry_date = check.domain_expires_at
return unless expiry_date.present?
return "table-danger" if expiry_date <= 2.weeks.from_now
return "table-warning" if expiry_date <= 30.days.from_now
return "table-danger" if expiry_date <= 3.days.from_now
return "table-warning" if expiry_date < 1.month.from_now
end
def checks_sort_links(field)
%i[asc desc].map { |direction|
checks_sort_link(field, direction)
}.join
end
def checks_sort_link(field, direction)
classes = "btn btn-light btn-sm mx-1 mx-1 px-1 py-0"
sort = [field, direction]
icon = direction == :asc ? "chevron-up" : "chevron-down"
html = Octicons::Octicon.new(icon).to_svg.html_safe
sort_path = checks_path(current_criterias.merge(sort: sort.join("_")))
link_to_unless sort == current_sort, html, sort_path, class: classes do
content_tag(:span, html, class: classes + " active")
end
end
def check_in_error(check)
content_tag(
:span,
Octicons::Octicon.new("alert", class: "ml-1").to_svg.html_safe,
class: "in-error text-danger",
data: {
toggle: "tooltip",
placement: "bottom",
title: check_last_success_title(check),
},
)
end
def current_criterias
current_scopes.merge(sort: params[:sort])
end
def scoped_with?(scope)
name, value = scope.first
scope_value = current_scopes[name]
scope_value = scope_value.to_sym if scope_value.respond_to?(:to_sym)
scope_value == value
end
def check_button_criterias(scope)
if scoped_with?(scope)
current_criterias.except(scope.keys.first)
else
current_criterias.merge(scope)
end
end
def check_button_scope_class(scope = nil)
"btn btn-sm " + if scope && scoped_with?(scope)
"btn-info active"
else
"btn-outline-info"
end
end
def check_last_success_title(check)
return t(".never_succeeded") if check.last_success_at.nil?
t(".days_from_last_success", count: check.days_from_last_success)
end
end

View File

@ -27,6 +27,8 @@
#
class Check < ApplicationRecord
ERROR_DELAY_DAYS = 3
belongs_to :user
has_many :logs, class_name: "CheckLog"
has_many :notifications, validate: true, dependent: :destroy
@ -58,6 +60,32 @@ class Check < ApplicationRecord
OR (last_success_at <= DATE_SUB(last_run_at, INTERVAL 5 MINUTE))")
}
scope :kind, ->(kind) { where(kind: kind) }
scope :by_domain, ->(domain) { where("domain LIKE ?", "%#{domain}%") }
scope :recurrent_failures, -> {
interval = "INTERVAL #{ERROR_DELAY_DAYS} DAY"
where("last_run_at IS NOT NULL AND created_at <= DATE_SUB(NOW(), #{interval})")
.where("last_success_at IS NULL OR last_success_at <= DATE_SUB(last_run_at, #{interval})")
}
def self.default_sort
[:domain_expires_at, :asc]
end
def in_error?
return false if created_at > ERROR_DELAY_DAYS.days.ago
return false if last_run_at.nil?
return true if last_success_at.nil?
last_success_at < ERROR_DELAY_DAYS.days.ago
end
def days_from_last_success
return unless last_success_at.present?
(Date.today - last_success_at.to_date).to_i
end
private
def domain_created_at_past

View File

@ -0,0 +1,40 @@
<div class="row justify-content-md-end checks-filters">
<div class="col-md-6 mb-3 d-flex justify-content-between justify-content-md-start">
<div class="btn-group mr-2">
<% Check.kinds.keys.map(&:to_sym).each do |kind_name| %>
<%= link_to t(".kind_#{kind_name}"),
checks_path(check_button_criterias(kind: kind_name)),
class: check_button_scope_class(kind: kind_name) %>
<% end %>
</div>
<%= link_to t(".with_error"),
checks_path(check_button_criterias(recurrent_failures: true)),
class: check_button_scope_class(recurrent_failures: true) %>
</div>
<div class="col-md-6 mb-3">
<%= form_tag(checks_path, method: :get) do %>
<div class="form-row justify-content-around">
<div class="col">
<div class="input-group">
<%= search_field_tag :by_domain, current_scopes[:by_domain], class: "form-control form-control-sm", placeholder: ".com, example.org, …" %>
<div class="input-group-append">
<%= button_tag Octicons::Octicon.new("search").to_svg.html_safe, class: "btn btn-sm btn-outline-secondary" %>
</div>
</div>
<%- current_criterias.except(:by_domain).compact.each_pair do |name, value| %>
<%= hidden_field_tag name, value%>
<% end %>
</div>
<div class="col-auto">
<%= link_to Octicons::Octicon.new("x").to_svg.html_safe, checks_path, class: "btn btn-danger btn-sm btn-outline-danger" %>
</div>
</div>
<% end %>
</div>
</div>

View File

@ -1,8 +1,11 @@
<%= simple_form_for(check) do |f| %>
<%= f.input :domain, autofocus: true, input_html: { autocapitalize: :none, autocorrect: :off } %>
<%= f.input :domain,
autofocus: true,
input_html: { autocapitalize: :none, autocorrect: :off },
label: t(".#{check.kind || "generic" }.domain") %>
<% if check.new_record? %>
<%= f.input :kind, as: :radio_buttons, collection: Check.kinds.keys if check.new_record? %>
<%= f.input :kind, as: check.kind.present? ? :hidden : :radio_buttons, collection: Check.kinds.keys %>
<% end %>
<%= f.input :comment %>

View File

@ -1,26 +1,41 @@
<div class="mb-4">
<table class="table table-checks">
<div class="mb-4 table-responsive">
<table class="table checks-table">
<thead>
<tr>
<th scope="col"></th>
<th scope="col">Domain</th>
<th scope="col">Expiry date</th>
<th scope="col">Edit</th>
<th scope="col">
<%= t(".th.domain") %>
<span class="sort-links mx-sm-2 text-nowrap">
<%== checks_sort_links(:domain) %>
</span>
</th>
<th scope="col">
<span class="d-none d-sm-inline"><%= t(".th.expiry_date") %></span>
<span class="d-inline d-sm-none"><%= t(".th.expiry_date_short") %></span>
<span class="sort-links mx-sm-2 text-nowrap">
<%== checks_sort_links(:domain_expires_at) %>
</span>
</th>
<th scope="col" class="text-right"><%= t(".th.edit") %></th>
</tr>
</thead>
<tbody>
<% checks.each do |check| %>
<tr class="check-row <%= check_row_class(check) %>">
<td>
<span class="badge badge-secondary"><%= check_kind_label(check) %></span>
<td class="kind">
<span class="badge badge-info"><%= t(".kind_labels.#{check.kind}") %></span>
</td>
<td>
<%= check_in_error(check) if check.in_error? %>
<strong><%= check.domain %></strong>
</td>
<td>
<%= format_utc(check.domain_expires_at) if check.domain_expires_at.present? %>
<% if check.domain_expires_at.present? %>
<%= content_tag :span, format_date(check.domain_expires_at), class: "d-none d-md-inline" %>
<%= content_tag :span, format_date(check.domain_expires_at, format: :short), class: "d-inline d-md-none" %>
<% end %>
</td>
<td class="action">
<td class="action text-right">
<%= link_to edit_check_path(check) do %>
<%== Octicons::Octicon.new("pencil").to_svg %>
<% end %>
@ -30,3 +45,5 @@
</tbody>
</table>
</div>
<%= paginate @checks %>

View File

@ -1,13 +1,20 @@
<div class="container">
<div class="container-fluid">
<div class="row justify-content-center">
<div class="col-12 col-lg-10">
<% if @checks.empty? %>
<div class="col-12 col-lg-10 col-xl-9">
<% if @checks.empty? && current_scopes.blank? %>
<div class="alert alert-info">
<%= t(".no_check_yet_html", new_domain_path: new_check_path, new_ssl_path: new_check_path) %>
<%= t(".no_check_yet_html", new_domain_path: new_check_path(kind: :domain), new_ssl_path: new_check_path(kind: :ssl)) %>
</div>
<% else %>
<h1>List of your checks</h1>
<%= render "table", checks: @checks %>
<h1 class="mb-3 mb-sm-5"><%= t(".title") %></h1>
<%= render "filters" %>
<% if @checks.any? %>
<%= render "table", checks: @checks %>
<% else %>
<div class="alert alert-warning"><%= t(".no_matching_check") %></div>
<% end %>
<% end %>
</div>
</div>

View File

@ -1,7 +1,7 @@
<div class="container">
<div class="row justify-content-center">
<div class="col-12 col-lg-10">
<h1>Create a new check</h1>
<h1><%= t(".#{@check.kind}.title") %></h1>
<%= render "form", check: @check %>
</div>

View File

@ -0,0 +1,11 @@
<%# Link to the "First" page
- available local variables
url: url to the first page
current_page: a page object for the currently displayed page
total_pages: total number of pages
per_page: number of items to fetch per page
remote: data-remote
-%>
<li class="page-item <%= 'active' if current_page.first? %>">
<%= link_to t('views.pagination.first').html_safe, url, remote: remote, class: "page-link" %>
</li>

View File

@ -0,0 +1,10 @@
<%# Non-link tag that stands for skipped pages...
- available local variables
current_page: a page object for the currently displayed page
total_pages: total number of pages
per_page: number of items to fetch per page
remote: data-remote
-%>
<li class="page-item">
<span class="page-link"><%= t('views.pagination.truncate').html_safe %></span>
</li>

View File

@ -0,0 +1,11 @@
<%# Link to the "Last" page
- available local variables
url: url to the last page
current_page: a page object for the currently displayed page
total_pages: total number of pages
per_page: number of items to fetch per page
remote: data-remote
-%>
<li class="page-item <%= 'active' if current_page.last? %>">
<%= link_to t('views.pagination.last').html_safe, url, remote: remote, class: "page-link" %>
</li>

View File

@ -0,0 +1,11 @@
<%# Link to the "Next" page
- available local variables
url: url to the next page
current_page: a page object for the currently displayed page
total_pages: total number of pages
per_page: number of items to fetch per page
remote: data-remote
-%>
<li class="page-item <%= 'active' if current_page.last? %>">
<%= link_to t('views.pagination.next').html_safe, url, rel: 'next', remote: remote, class: "page-link" %>
</li>

View File

@ -0,0 +1,15 @@
<%# Link showing page number
- available local variables
page: a page object for "this" page
url: url to this page
current_page: a page object for the currently displayed page
total_pages: total number of pages
per_page: number of items to fetch per page
remote: data-remote
-%>
<li class="page-item <%= ' active' if page.current? %>">
<%= link_to page, url, remote: remote, rel: page.rel, class: "page-link" %>
<% if page.current? %>
<span class="sr-only">(current)</span>
<% end %>
</li>

View File

@ -0,0 +1,27 @@
<%# The container tag
- available local variables
current_page: a page object for the currently displayed page
total_pages: total number of pages
per_page: number of items to fetch per page
remote: data-remote
paginator: the paginator that renders the pagination tags inside
-%>
<%= paginator.render do -%>
<nav role="navigation" aria-label="pager">
<ul class="pagination justify-content-center">
<%= first_page_tag unless current_page.first? %>
<%= prev_page_tag unless current_page.first? %>
<% each_page do |page| -%>
<% if page.display_tag? -%>
<%= page_tag page %>
<% elsif !page.was_truncated? -%>
<%= gap_tag %>
<% end -%>
<% end -%>
<% unless current_page.out_of_range? %>
<%= next_page_tag unless current_page.last? %>
<%= last_page_tag unless current_page.last? %>
<% end %>
</ul>
</nav>
<% end -%>

View File

@ -0,0 +1,11 @@
<%# Link to the "Previous" page
- available local variables
url: url to the previous page
current_page: a page object for the currently displayed page
total_pages: total number of pages
per_page: number of items to fetch per page
remote: data-remote
-%>
<li class="page-item <%= 'active' if current_page.first? %>">
<%= link_to t('views.pagination.previous').html_safe, url, rel: 'prev', remote: remote, class: "page-link" %>
</li>

View File

@ -8,10 +8,13 @@
<ul class="navbar-nav mr-auto">
<% if user_signed_in? %>
<li class="nav-item">
<%= link_to("My checks", checks_path, class: "nav-link") %>
<%= link_to(t(".my_checks"), checks_path, class: "nav-link") %>
</li>
<li class="nav-item">
<%= link_to("Add a check", new_check_path, class: "nav-link") %>
<%= link_to(t(".new_domain_check"), new_check_path(kind: :domain), class: "nav-link") %>
</li>
<li class="nav-item">
<%= link_to(t(".new_ssl_check"), new_check_path(kind: :ssl), class: "nav-link") %>
</li>
<% end %>
</ul>

View File

@ -11,6 +11,6 @@
# end
# These inflection rules are supported but not enabled by default:
# ActiveSupport::Inflector.inflections(:en) do |inflect|
# inflect.acronym 'RESTful'
# end
ActiveSupport::Inflector.inflections(:en) do |inflect|
inflect.acronym 'SSL'
end

View File

@ -0,0 +1,12 @@
# frozen_string_literal: true
Kaminari.configure do |config|
config.default_per_page = 20
config.max_per_page = 200
# config.window = 4
# config.outer_window = 0
# config.left = 0
# config.right = 0
# config.page_method_name = :page
# config.param_name = :page
# config.params_on_first_page = false
end

View File

@ -12,6 +12,11 @@ en:
check:
past: "can't be in the future"
date:
formats:
short: "%-d, %b %Y"
long: "%A, %B %-d, %Y"
devise:
registrations:
new:
@ -53,6 +58,9 @@ en:
en: English
fr: French
navbar:
my_checks: "My checks"
new_domain_check: "New domain check"
new_ssl_check: "New SSL check"
sign_up: "Sign up"
sign_in: "Sign in"
sign_out: "Sign out"
@ -60,11 +68,51 @@ en:
checks:
index:
title: List of your checks
no_matching_check: "No checks match your filters."
no_check_yet_html: |
You have not set up a check yet.
Please add a <a href="%{new_domain_path}">domain</a>
or a <a href="%{new_ssl_path}">ssl</a> !
new:
title: New check
domain:
title: New domain check
ssl:
title: New SSL check
create:
saved: "Your check has been saved."
invalid: "Please check the form."
filters:
kind_domain: Domain
kind_ssl: SSL
with_error: With error
form:
generic:
domain: Domain
domain:
domain: Domain name
ssl:
domain: Hostname
notifications_hint: |
Receive notifications to warn you when our system detects that the
expiration date is coming. The time is set in number of days.
table:
th:
domain: Name
expiry_date: "Expiration date"
expiry_date_short: "Exp."
edit: Edit
kind_labels:
domain: Domain
ssl: SSL
never_succeeded: "Chexpire has never been able to perform a check."
days_from_last_success:
zero: "Last check successful: today"
one: "Last check successful: yesterday"
other: "Last check successful %{count} days ago"

View File

@ -2,8 +2,15 @@ fr:
activerecord:
attributes:
check:
domain: "Domaine"
comment: "Commentaire"
vendor: "Fournisseur"
kind: Type
domain_created_at: "Date de création"
domain_updated_at: "Date de modification"
notification:
interval: Délai
recipient: Destinataire
user:
tos_accepted: "Conditions d'utilisation"
notifications_enabled: "Notifications activées"
@ -11,6 +18,26 @@ fr:
models:
check:
past: "ne peut être dans le futur"
helpers:
submit:
check:
create: "Créer"
update: "Valider"
page_entries_info:
one_page:
display_entries:
zero: "Pas de %{entry_name} trouvé."
one: "Affiche <b>1</b> %{entry_name}"
other: "Affiche <b>les %{count}</b> %{entry_name}"
more_pages:
display_entries: "Affiche %{entry_name} <b>%{first}&nbsp;-&nbsp;%{last}</b> de <b>%{total}</b> au total"
views:
pagination:
first: "&laquo; Début"
last: "Fin &raquo;"
previous: "&lsaquo; Préc"
next: "Suiv &rsaquo;"
truncate: "&hellip;"
time:
am: am
@ -20,6 +47,11 @@ fr:
short: "%d %b %H:%M"
pm: pm
date:
formats:
short: "%d/%m/%Y"
long: "%A %d %B %Y"
devise:
registrations:
new:
@ -61,6 +93,9 @@ fr:
en: Anglais
fr: Français
navbar:
my_checks: "Mes vérifications"
new_domain_check: "Nouveau nom de domaine"
new_ssl_check: "Nouveau certificat SSL"
sign_up: "Enregistrement"
sign_in: "Connexion"
sign_out: "Déconnexion"
@ -68,11 +103,51 @@ fr:
checks:
index:
title: "Liste de vos vérifications"
no_matching_check: "Aucune vérification ne correspond à vos critères."
no_check_yet_html: |
Vous n'avez pas encore créé de vérification.
Vous pouvez en ajouter pour un <a href="%{new_domain_path}">domaine</a>
ou un <a href="%{new_ssl_path}">SSL</a> !
new:
title: Nouvelle vérification
domain:
title: Nouvelle vérification d'un nom de domaine
ssl:
title: Nouvelle vérification d'un certificat SSL
create:
saved: La vérification est enregistrée.
invalid: Veuillez vérifier le formulaire.
filters:
kind_domain: Domaine
kind_ssl: SSL
with_error: En erreur
form:
generic:
domain: Domaine
domain:
domain: Nom de domaine
ssl:
domain: Nom d'hôte
notifications_hint: |
Recevez des notifications pour vous avertir lorsque notre système détecte
que la date d'expiration approche. Le délais est indiqué ennombre de jours.
que la date d'expiration approche. Le délai est indiqué ennombre de jours.
table:
th:
domain: Nom
expiry_date: "Date d'expiration"
expiry_date_short: "Exp."
edit: Modifier
kind_labels:
domain: Domaine
ssl: SSL
never_succeeded: "Chexpire n'a jamais pu effectuer de vérification."
days_from_last_success:
zero: "Dernière vérification réussie : aujourd'hui"
one: "Dernière vérification réussie : hier"
other: "Dernière vérification réussie il y a %{count} jours"

View File

@ -7,9 +7,20 @@ user1 = User.create!(
email: "colin@example.org",
password: "password",
tos_accepted: true,
confirmed_at: Time.now
confirmed_at: Time.now,
locale: :fr,
)
user2 = User.create!(
email: "colin+en@example.org",
password: "password",
tos_accepted: true,
confirmed_at: Time.now,
locale: :en,
)
users = [user1, user2]
check_chexpire_org = Check.create!(
user: user1,
kind: :domain,
@ -24,14 +35,14 @@ check_chexpire_org = Check.create!(
check_chexpire_org_error = Check.create!(
user: user1,
kind: :domain,
domain: "chexpire.org",
domain: "chexpire-error.org",
domain_expires_at: 1.week.from_now,
domain_updated_at: 6.months.ago,
domain_created_at: Time.new(2016, 8, 4, 12, 15, 1),
comment: "The date are fake, this is a seed !",
vendor: "Some random registrar",
last_run_at: 20.minutes.ago,
last_success_at: 4.days.ago,
created_at: 3.weeks.ago,
)
ssl_check_chexpire_org = Check.create!(
@ -48,7 +59,7 @@ ssl_check_chexpire_org = Check.create!(
ssl_check_chexpire_org_error = Check.create!(
user: user1,
kind: :ssl,
domain: "chexpire.org",
domain: "chexpire-error.org",
domain_expires_at: 1.week.from_now,
domain_updated_at: 6.months.ago,
domain_created_at: Time.new(2016, 8, 4, 12, 15, 1),
@ -59,6 +70,33 @@ ssl_check_chexpire_org_error = Check.create!(
)
def check_factory(users)
ext = %w[com net org fr].sample
word = (0...rand(4..12)).map { (97 + rand(26)).chr }.join
Check.new(
user: users.sample,
kind: Check.kinds.keys.sample,
domain: "#{word}.#{ext}",
domain_expires_at: rand(8..300).days.from_now,
domain_updated_at: rand(1..300).days.ago,
domain_created_at: rand(301..3000).days.ago,
)
end
100.times do |i|
check_factory(users).save!
end
# checks with error
10.times do |i|
check_factory(users).update_attributes(
created_at: rand(1..300).days.ago,
last_run_at: 4.hours.ago,
last_success_at: rand(10...100).days.ago,
)
end
Notification.create!(
check: check_chexpire_org,
interval: 15,

View File

@ -7,7 +7,8 @@
"exports-loader": "^0.7.0",
"jquery": "^3.3.1",
"popper.js": "^1.14.3",
"rails-ujs": "^5.2.0"
"rails-ujs": "^5.2.0",
"turbolinks": "^5.1.1"
},
"devDependencies": {
"webpack-dev-server": "2.11.2"

View File

@ -4,7 +4,7 @@ class ApplicationSystemTestCase < ActionDispatch::SystemTestCase
driven_by :headless_chrome
def teardown
Capybara.reset_sessions!
Warden.test_reset!
super
end
end

View File

@ -0,0 +1,7 @@
inherit_from: ../../.rubocop.yml
Metrics/ClassLength:
Enabled: false
Metrics/BlockLength:
Enabled: false

View File

@ -1,7 +1,150 @@
require "test_helper"
class ChecksControllerTest < ActionDispatch::IntegrationTest
# test "the truth" do
# assert true
# end
setup do
@user = create(:user)
login_as(@user)
end
test "no logged users are redirected to signin form" do
logout
get new_check_path
assert_redirected_to new_user_session_path
end
test "new without kind does not trigger an error" do
get new_check_path
assert_response :success
end
test "new with kind domain does not trigger an error" do
get new_check_path(kind: :domain)
assert_response :success
end
test "new with kind ssl does not trigger an error" do
get new_check_path(kind: :ssl)
assert_response :success
end
test "new with an invalid kind returns an error" do
get new_check_path(kind: :invalid)
assert_response :not_found
end
test "checks are ordered by default by expiry date sort" do
c1 = create(:check, user: @user, domain_expires_at: 20.days.from_now)
c2 = create(:check, user: @user, domain_expires_at: 10.days.from_now)
c3 = create(:check, user: @user, domain_expires_at: 1.day.from_now)
get checks_path
assert_equal [c3, c2, c1], current_checks
end
test "checks are ordered by expiry date asc" do
c1 = create(:check, user: @user, domain_expires_at: 20.days.from_now)
c2 = create(:check, user: @user, domain_expires_at: 10.days.from_now)
c3 = create(:check, user: @user, domain_expires_at: 1.day.from_now)
get checks_path(sort: :domain_expires_at_asc)
assert_equal [c3, c2, c1], current_checks
end
test "checks are ordered by reverse expiring date" do
c1 = create(:check, user: @user, domain_expires_at: 1.day.from_now)
c2 = create(:check, user: @user, domain_expires_at: 10.days.from_now)
c3 = create(:check, user: @user, domain_expires_at: 20.days.from_now)
get checks_path(sort: :domain_expires_at_desc)
assert_equal [c3, c2, c1], current_checks
end
test "checks are ordered by domain name asc" do
c1 = create(:check, user: @user, domain: "a")
c2 = create(:check, user: @user, domain: "b")
c3 = create(:check, user: @user, domain: "c")
get checks_path(sort: :domain_asc)
assert_equal [c1, c2, c3], current_checks
end
test "checks are ordered by domain name desc" do
c1 = create(:check, user: @user, domain: "a")
c2 = create(:check, user: @user, domain: "b")
c3 = create(:check, user: @user, domain: "c")
get checks_path(sort: :domain_desc)
assert_equal [c3, c2, c1], current_checks
end
test "invalid sort fallback to default sort" do
c1 = create(:check, user: @user, domain_expires_at: 20.days.from_now)
c2 = create(:check, user: @user, domain_expires_at: 10.days.from_now)
c3 = create(:check, user: @user, domain_expires_at: 1.day.from_now)
get checks_path(sort: :invalid_sort_asc)
assert_equal [c3, c2, c1], current_checks
end
test "checks are filtered by domain kind" do
c1 = create(:check, :domain, user: @user)
c2 = create(:check, :domain, user: @user)
create(:check, :ssl, user: @user)
get checks_path(kind: :domain)
assert_equal [c1, c2], current_checks
end
test "checks are filtered by ssl kind" do
create(:check, :domain, user: @user)
create(:check, :domain, user: @user)
c3 = create(:check, :ssl, user: @user)
get checks_path(kind: :ssl)
assert_equal [c3], current_checks
end
test "checks are filtered by domain name" do
c1 = create(:check, user: @user, domain: "abc")
c2 = create(:check, user: @user, domain: "bcde")
create(:check, user: @user, domain: "hijk")
get checks_path(by_domain: "bc")
assert_equal [c1, c2], current_checks
get checks_path(by_domain: "klm")
assert_empty current_checks
end
test "checks in error are filtered" do
c1 = create(:check, :last_runs_failed, created_at: 1.week.ago, user: @user)
create(:check, user: @user)
get checks_path(recurrent_failures: true)
assert_equal [c1], current_checks
end
test "checks are paginated" do
create_list(:check, 40, user: @user)
get checks_path
assert_equal 1, current_checks.current_page
first_page = current_checks
get checks_path(page: 2)
assert_equal 2, current_checks.current_page
assert_not_equal first_page, current_checks
end
test "checks are scoped to current user" do
c1 = create(:check, user: @user)
create(:check)
get checks_path
assert_equal [c1], current_checks
end
def current_checks
@controller.instance_variable_get("@checks")
end
end

View File

@ -49,4 +49,56 @@ class CheckTest < ActiveSupport::TestCase
assert notification.pending?
assert_nil notification.sent_at
end
test "in_error? for recently added" do
check = build(:check, created_at: 1.day.ago)
refute check.in_error?
check = build(:check, created_at: 1.day.ago, last_run_at: 3.minutes.ago)
refute check.in_error?
check = build(:check, created_at: 1.day.ago, last_success_at: 1.hour.ago)
refute check.in_error?
end
test "in_error? for never success check, with at least 1 run" do
check = build(:check, created_at: 3.weeks.ago, last_run_at: 1.day.ago)
assert check.in_error?
check = build(:check, created_at: 3.weeks.ago, last_run_at: 4.days.ago)
assert check.in_error?
end
test "in_error? ignore check without run" do
check = build(:check, created_at: 3.weeks.ago)
refute check.in_error?
end
test "in_error? for last success a few days ago" do
check = build(:check, created_at: 3.weeks.ago,
last_success_at: 10.days.ago, last_run_at: 1.day.ago)
assert check.in_error?
check = build(:check, created_at: 3.weeks.ago,
last_success_at: 1.days.ago, last_run_at: 1.day.ago)
refute check.in_error?
end
test "days_from_last_success without any success" do
check = build(:check)
assert_nil check.days_from_last_success
check = build(:check, last_run_at: 1.day.ago)
assert_nil check.days_from_last_success
end
test "days_from_last_success" do
check = build(:check, last_success_at: 10.days.ago - 1.hour)
assert_equal 10, check.days_from_last_success
end
test "days_from_last_success with a time" do
check = build(:check, last_success_at: (10.1 * 24).hours.ago)
assert_equal 10, check.days_from_last_success
end
end

4
test/system/.rubocop.yml Normal file
View File

@ -0,0 +1,4 @@
inherit_from: ../../.rubocop.yml
Metrics/ClassLength:
Enabled: false

View File

@ -4,16 +4,263 @@ class ChecksTest < ApplicationSystemTestCase
setup do
@user = create(:user)
login_as(@user)
@check = create(:check, :with_notifications, user: @user)
end
test "create a check and a notification" do
test "create a check and a notification without kind" do
visit new_check_path
choose "domain"
fill_and_valid_new_check
end
test "create a predefined domain check" do
visit new_check_path(kind: :domain)
refute page.has_css? "domain[kind]"
fill_and_valid_new_check
end
test "create a predefined ssl check" do
visit new_check_path(kind: :ssl)
refute page.has_css? "domain[kind]"
fill_and_valid_new_check
end
test "remove a notification" do
check = create(:check, :with_notifications, domain: "dom-with-notif.net", user: @user)
visit edit_check_path(check)
notification = check.notifications.first
selector = "[data-notification-id=\"#{notification.id}\"]"
assert_difference "Notification.where(check_id: #{check.id}).count", -1 do
within selector do
find(".btn-danger").click
end
page.has_no_content?(selector)
end
end
test "update a check" do
check = create(:check, :with_notifications, domain: "dom-with-notif.net", user: @user)
visit edit_check_path(check)
fill_in "check[comment]", with: "My comment"
click_button "Update Check"
assert_equal checks_path, page.current_path
assert page.has_css?(".alert-success")
check.reload
assert_equal "My comment", check.comment
end
test "add a notification" do
check = create(:check, :with_notifications, domain: "dom-with-notif.net", user: @user)
visit edit_check_path(check)
recipient = "recipient2@example.org"
fill_in("check[notifications_attributes][2][recipient]", with: recipient)
fill_in("check[notifications_attributes][2][interval]", with: 55)
assert_difference "Notification.where(check_id: #{check.id}).count", +1 do
click_button "Update Check"
assert_equal checks_path, page.current_path
end
assert page.has_css?(".alert-success")
notification = Notification.last
assert_equal recipient, notification.recipient
assert_equal 55, notification.interval
assert notification.email?
assert notification.pending?
end
test "list my checks" do
create(:check, :domain, domain: "dom.com", domain_expires_at: Time.new(2018, 7, 5, 12), user: @user) # rubocop:disable Metrics/LineLength
create(:check, :ssl, domain: "ssldom.com", user: @user)
create(:check, :ssl, domain: "ssldom2.com", user: @user)
visit checks_path
within ".checks-table" do
assert page.has_content?("SSL", count: 2)
assert page.has_content?("Domain", count: 1)
end
within ".check-row:first-of-type" do
assert page.has_content?("Domain")
assert page.has_content?("dom.com")
assert page.has_content?("Thursday, July 5, 2018")
end
end
test "list filterable by domain and ssl" do
create_list(:check, 2, :domain, domain: "mydom.fr", user: @user)
create_list(:check, 1, :ssl, domain: "ssl.com", user: @user)
visit checks_path
assert page.has_css?(".check-row", count: 3)
within ".checks-filters" do
click_on "Domain"
assert find_link("Domain").matches_css? ".active"
assert find_link("SSL").not_matches_css? ".active"
end
within ".checks-table" do
assert page.has_css?(".check-row", count: 2)
assert page.has_content?("Domain", count: 2)
end
within ".checks-filters" do
click_on "SSL"
assert find_link("SSL").matches_css? ".active"
assert find_link("Domain").not_matches_css? ".active"
end
within ".checks-table" do
assert page.has_css?(".check-row", count: 1)
assert page.has_content?("SSL", count: 1)
assert page.has_content?("ssl.com")
end
end
test "list filterable by check in error" do
create(:check, user: @user)
create(:check, :last_runs_failed, created_at: 1.week.ago, user: @user)
visit checks_path
within ".checks-table" do
assert page.has_css?(".check-row", count: 2)
assert page.has_css?(".in-error", count: 1)
end
within ".checks-filters" do
click_on(I18n.t("checks.filters.with_error"))
end
within ".checks-table" do
assert page.has_css?(".check-row", count: 1)
assert page.has_css?(".in-error", count: 1)
end
end
test "list filterable by name string" do
create(:check, user: @user)
create(:check, domain: "chexpire.org", user: @user)
create(:check, domain: "chexpire.net", user: @user)
visit checks_path
within ".checks-filters" do
fill_in("by_domain", with: "chex")
click_button
end
within ".checks-table" do
assert page.has_css?(".check-row", count: 2)
assert page.has_content?("chexpire.", count: 2)
end
end
test "list is paginated" do
create(:check, user: @user)
visit checks_path
assert page.has_no_css?("ul.pagination")
create_list(:check, 50, user: @user)
visit checks_path
assert page.has_css?("ul.pagination")
end
test "list is sortable by name" do
visit checks_path
create(:check, domain: "a.org", user: @user)
create(:check, domain: "b.org", user: @user)
visit checks_path
within ".checks-table thead th:nth-of-type(2)" do
find(".sort-links:first-child").click
end
within ".check-row:first-of-type" do
page.has_content? "a.org"
end
within ".checks-table thead th:nth-of-type(2)" do
find(".sort-links:last-child").click
end
within ".check-row:first-of-type" do
page.has_content? "b.org"
end
end
test "list is sorted by expiration date by default" do
visit checks_path
create(:check, domain_expires_at: Time.new(2018, 7, 6, 12), user: @user)
create(:check, domain_expires_at: Time.new(2018, 7, 5, 12), user: @user)
visit checks_path
within ".check-row:first-of-type" do
page.has_content? "Thursday, July 5, 2018"
end
end
test "list is sortable by expiration date" do
visit checks_path
create(:check, domain_expires_at: Time.new(2018, 7, 5, 12), user: @user)
create(:check, domain_expires_at: Time.new(2018, 7, 6, 12), user: @user)
visit checks_path
within ".check-row:first-of-type" do
page.has_content? "Thursday, July 5, 2018"
end
# only a desc link because of default sort
within ".checks-table thead th:nth-of-type(3)" do
find(".sort-links a").click
end
within ".check-row:first-of-type" do
page.has_content? "Friday, July 6, 2018"
end
within ".checks-table thead th:nth-of-type(3)" do
find(".sort-links a").click
end
within ".check-row:first-of-type" do
page.has_content? "Thursday, July 5, 2018"
end
end
private
# rubocop:disable Metrics/AbcSize
# rubocop:disable Metrics/MethodLength
def fill_and_valid_new_check
domain = "domain-test.fr"
fill_in("check[domain]", with: domain)
choose "domain"
recipient = "recipient@example.org"
fill_in("check[notifications_attributes][0][recipient]", with: recipient)
@ -32,55 +279,6 @@ class ChecksTest < ApplicationSystemTestCase
assert notification.email?
assert notification.pending?
end
test "remove a notification" do
visit edit_check_path(@check)
notification = @check.notifications.first
selector = "[data-notification-id=\"#{notification.id}\"]"
assert_difference "Notification.where(check_id: #{@check.id}).count", -1 do
within selector do
find(".btn-danger").click
end
page.has_no_content?(selector)
end
end
test "update a check" do
visit edit_check_path(@check)
fill_in "check[comment]", with: "My comment"
click_button "Update Check"
assert_equal checks_path, page.current_path
assert page.has_css?(".alert-success")
@check.reload
assert_equal "My comment", @check.comment
end
test "add a notification" do
visit edit_check_path(@check)
recipient = "recipient2@example.org"
fill_in("check[notifications_attributes][2][recipient]", with: recipient)
fill_in("check[notifications_attributes][2][interval]", with: 55)
assert_difference "Notification.where(check_id: #{@check.id}).count", +1 do
click_button "Update Check"
assert_equal checks_path, page.current_path
end
assert page.has_css?(".alert-success")
notification = Notification.last
assert_equal recipient, notification.recipient
assert_equal 55, notification.interval
assert notification.email?
assert notification.pending?
end
# rubocop:enable Metrics/AbcSize
# rubocop:enable Metrics/MethodLength
end

View File

@ -42,6 +42,7 @@ Capybara.register_driver :headless_chrome do |app|
end
Capybara.save_path = Rails.root.join("tmp/capybara")
Capybara.javascript_driver = :headless_chrome
Capybara.default_driver = :headless_chrome
# Disable Open4 real system calls
require "open4"

View File

@ -5598,6 +5598,10 @@ tunnel-agent@~0.4.1:
version "0.4.3"
resolved "https://registry.yarnpkg.com/tunnel-agent/-/tunnel-agent-0.4.3.tgz#6373db76909fe570e08d73583365ed828a74eeeb"
turbolinks@^5.1.1:
version "5.1.1"
resolved "https://registry.yarnpkg.com/turbolinks/-/turbolinks-5.1.1.tgz#3d418a2d8172edbde5e787bf74cb7bef151ae43f"
tweetnacl@^0.14.3, tweetnacl@~0.14.0:
version "0.14.5"
resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-0.14.5.tgz#5ae68177f192d4456269d108afa93ff8743f4f64"