Add ability to set grace period

This commit is contained in:
Pēteris Caune 2015-07-16 01:24:56 +03:00
parent 29e36fc14d
commit 0af1fb782a
12 changed files with 385 additions and 103 deletions

View File

@ -13,7 +13,7 @@ CREATE OR REPLACE FUNCTION update_alert_after()
RETURNS trigger AS $update_alert_after$ RETURNS trigger AS $update_alert_after$
BEGIN BEGIN
IF NEW.last_ping IS NOT NULL THEN IF NEW.last_ping IS NOT NULL THEN
NEW.alert_after := NEW.last_ping + NEW.timeout + '1 hour'; NEW.alert_after := NEW.last_ping + NEW.timeout + NEW.grace;
END IF; END IF;
RETURN NEW; RETURN NEW;
END; END;

View File

@ -0,0 +1,20 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import models, migrations
import datetime
class Migration(migrations.Migration):
dependencies = [
('api', '0005_auto_20150630_2021'),
]
operations = [
migrations.AddField(
model_name='check',
name='grace',
field=models.DurationField(default=datetime.timedelta(0, 3600)),
),
]

View File

@ -10,7 +10,13 @@ from hc.lib.emails import send
STATUSES = (("up", "Up"), ("down", "Down"), ("new", "New")) STATUSES = (("up", "Up"), ("down", "Down"), ("new", "New"))
DEFAULT_TIMEOUT = td(days=1) DEFAULT_TIMEOUT = td(days=1)
TIMEOUT_CHOICES = ( DEFAULT_GRACE = td(hours=1)
DURATION_CHOICES = (
("1 minute", td(minutes=1)),
("2 minutes", td(minutes=2)),
("5 minutes", td(minutes=5)),
("10 minutes", td(minutes=10)),
("15 minutes", td(minutes=15)), ("15 minutes", td(minutes=15)),
("30 minutes", td(minutes=30)), ("30 minutes", td(minutes=30)),
("1 hour", td(hours=1)), ("1 hour", td(hours=1)),
@ -30,6 +36,7 @@ class Check(models.Model):
user = models.ForeignKey(User, blank=True, null=True) user = models.ForeignKey(User, blank=True, null=True)
created = models.DateTimeField(auto_now_add=True) created = models.DateTimeField(auto_now_add=True)
timeout = models.DurationField(default=DEFAULT_TIMEOUT) timeout = models.DurationField(default=DEFAULT_TIMEOUT)
grace = models.DurationField(default=DEFAULT_GRACE)
last_ping = models.DateTimeField(null=True, blank=True) last_ping = models.DateTimeField(null=True, blank=True)
alert_after = models.DateTimeField(null=True, blank=True, editable=False) alert_after = models.DateTimeField(null=True, blank=True, editable=False)
status = models.CharField(max_length=6, choices=STATUSES, default="new") status = models.CharField(max_length=6, choices=STATUSES, default="new")
@ -39,7 +46,7 @@ class Check(models.Model):
def send_alert(self): def send_alert(self):
ctx = { ctx = {
"timeout_choices": TIMEOUT_CHOICES, "timeout_choices": DURATION_CHOICES,
"check": self, "check": self,
"checks": self.user.check_set.order_by("created"), "checks": self.user.check_set.order_by("created"),
"now": timezone.now() "now": timezone.now()

View File

@ -1,7 +1,6 @@
from django import forms from django import forms
from hc.api.models import TIMEOUT_CHOICES
class TimeoutForm(forms.Form): class TimeoutForm(forms.Form):
timeout = forms.ChoiceField(choices=TIMEOUT_CHOICES) timeout = forms.IntegerField(min_value=60, max_value=604800)
grace = forms.IntegerField(min_value=60, max_value=604800)

View File

@ -1,10 +1,12 @@
from datetime import timedelta as td
from django.contrib.auth.decorators import login_required from django.contrib.auth.decorators import login_required
from django.http import HttpResponseForbidden from django.http import HttpResponseForbidden
from django.shortcuts import redirect, render from django.shortcuts import redirect, render
from django.utils import timezone from django.utils import timezone
from hc.api.models import Check from hc.api.models import Check, DURATION_CHOICES
from hc.front.forms import TimeoutForm, TIMEOUT_CHOICES from hc.front.forms import TimeoutForm
def _welcome(request): def _welcome(request):
@ -42,7 +44,7 @@ def _my_checks(request):
ctx = { ctx = {
"checks": checks, "checks": checks,
"now": timezone.now(), "now": timezone.now(),
"timeout_choices": TIMEOUT_CHOICES "duration_choices": DURATION_CHOICES
} }
return render(request, "front/my_checks.html", ctx) return render(request, "front/my_checks.html", ctx)
@ -100,7 +102,8 @@ def update_timeout(request, code):
form = TimeoutForm(request.POST) form = TimeoutForm(request.POST)
if form.is_valid(): if form.is_valid():
check.timeout = form.cleaned_data["timeout"] check.timeout = td(seconds=form.cleaned_data["timeout"])
check.grace = td(seconds=form.cleaned_data["grace"])
check.save() check.save()
return redirect("hc-index") return redirect("hc-index")

4
static/css/nouislider.min.css vendored Normal file
View File

@ -0,0 +1,4 @@
/*! nouislider - 8.0.2 - 2015-07-06 13:22:09 */
.noUi-target,.noUi-target *{-webkit-touch-callout:none;-webkit-user-select:none;-ms-touch-action:none;-ms-user-select:none;-moz-user-select:none;-moz-box-sizing:border-box;box-sizing:border-box}.noUi-target{position:relative;direction:ltr}.noUi-base{width:100%;height:100%;position:relative;z-index:1}.noUi-origin{position:absolute;right:0;top:0;left:0;bottom:0}.noUi-handle{position:relative;z-index:1}.noUi-stacking .noUi-handle{z-index:10}.noUi-state-tap .noUi-origin{-webkit-transition:left .3s,top .3s;transition:left .3s,top .3s}.noUi-state-drag *{cursor:inherit!important}.noUi-base{-webkit-transform:translate3d(0,0,0);transform:translate3d(0,0,0)}.noUi-horizontal{height:18px}.noUi-horizontal .noUi-handle{width:34px;height:28px;left:-17px;top:-6px}.noUi-vertical{width:18px}.noUi-vertical .noUi-handle{width:28px;height:34px;left:-6px;top:-17px}.noUi-background{background:#FAFAFA;box-shadow:inset 0 1px 1px #f0f0f0}.noUi-connect{background:#3FB8AF;box-shadow:inset 0 0 3px rgba(51,51,51,.45);-webkit-transition:background 450ms;transition:background 450ms}.noUi-origin{border-radius:2px}.noUi-target{border-radius:4px;border:1px solid #D3D3D3;box-shadow:inset 0 1px 1px #F0F0F0,0 3px 6px -5px #BBB}.noUi-target.noUi-connect{box-shadow:inset 0 0 3px rgba(51,51,51,.45),0 3px 6px -5px #BBB}.noUi-dragable{cursor:w-resize}.noUi-vertical .noUi-dragable{cursor:n-resize}.noUi-handle{border:1px solid #D9D9D9;border-radius:3px;background:#FFF;cursor:default;box-shadow:inset 0 0 1px #FFF,inset 0 1px 7px #EBEBEB,0 3px 6px -3px #BBB}.noUi-active{box-shadow:inset 0 0 1px #FFF,inset 0 1px 7px #DDD,0 3px 6px -3px #BBB}.noUi-handle:after,.noUi-handle:before{content:"";display:block;position:absolute;height:14px;width:1px;background:#E8E7E6;left:14px;top:6px}.noUi-handle:after{left:17px}.noUi-vertical .noUi-handle:after,.noUi-vertical .noUi-handle:before{width:14px;height:1px;left:6px;top:14px}.noUi-vertical .noUi-handle:after{top:17px}[disabled] .noUi-connect,[disabled].noUi-connect{background:#B8B8B8}[disabled] .noUi-handle,[disabled].noUi-origin{cursor:not-allowed}.noUi-pips,.noUi-pips *{-moz-box-sizing:border-box;box-sizing:border-box}.noUi-pips{position:absolute;font:400 12px Arial;color:#999}.noUi-value{width:40px;position:absolute;text-align:center}.noUi-value-sub{color:#ccc;font-size:10px}.noUi-marker{position:absolute;background:#CCC}.noUi-marker-large,.noUi-marker-sub{background:#AAA}.noUi-pips-horizontal{padding:10px 0;height:50px;top:100%;left:0;width:100%}.noUi-value-horizontal{margin-left:-20px;padding-top:20px}.noUi-value-horizontal.noUi-value-sub{padding-top:15px}.noUi-marker-horizontal.noUi-marker{margin-left:-1px;width:2px;height:5px}.noUi-marker-horizontal.noUi-marker-sub{height:10px}.noUi-marker-horizontal.noUi-marker-large{height:15px}.noUi-pips-vertical{padding:0 10px;height:100%;top:0;left:100%}.noUi-value-vertical{width:15px;margin-left:20px;margin-top:-5px}.noUi-marker-vertical.noUi-marker{width:5px;height:2px;margin-top:-1px}.noUi-marker-vertical.noUi-marker-sub{width:10px}.noUi-marker-vertical.noUi-marker-large{width:15px}

View File

@ -0,0 +1,98 @@
/* Base;
*
*/
.noUi-pips,
.noUi-pips * {
-moz-box-sizing: border-box;
box-sizing: border-box;
}
.noUi-pips {
position: absolute;
font: 400 12px Arial;
color: #999;
}
/* Values;
*
*/
.noUi-value {
width: 40px;
position: absolute;
text-align: center;
}
.noUi-value-sub {
color: #ccc;
font-size: 10px;
}
/* Markings;
*
*/
.noUi-marker {
position: absolute;
background: #CCC;
}
.noUi-marker-sub {
background: #AAA;
}
.noUi-marker-large {
background: #AAA;
}
/* Horizontal layout;
*
*/
.noUi-pips-horizontal {
padding: 10px 0;
height: 50px;
top: 100%;
left: 0;
width: 100%;
}
.noUi-value-horizontal {
margin-left: -20px;
padding-top: 20px;
}
.noUi-value-horizontal.noUi-value-sub {
padding-top: 15px;
}
.noUi-marker-horizontal.noUi-marker {
margin-left: -1px;
width: 2px;
height: 5px;
}
.noUi-marker-horizontal.noUi-marker-sub {
height: 10px;
}
.noUi-marker-horizontal.noUi-marker-large {
height: 15px;
}
/* Vertical layout;
*
*/
.noUi-pips-vertical {
padding: 0 10px;
height: 100%;
top: 0;
left: 100%;
}
.noUi-value-vertical {
width: 15px;
margin-left: 20px;
margin-top: -5px;
}
.noUi-marker-vertical.noUi-marker {
width: 5px;
height: 2px;
margin-top: -1px;
}
.noUi-marker-vertical.noUi-marker-sub {
width: 10px;
}
.noUi-marker-vertical.noUi-marker-large {
width: 15px;
}

View File

@ -121,25 +121,16 @@ table.table tr > th.th-name {
vertical-align: middle; vertical-align: middle;
} }
.name-edit.inactive .input-name { .my-checks-name {
border: 1px solid rgba(0, 0, 0, 0); border: 1px solid rgba(0, 0, 0, 0);
background: none; padding: 6px;
box-shadow: none; display: block;
transition: none;
} }
.name-edit.inactive .input-name:hover { .my-checks-name:hover {
border: 1px dotted #AAA; border: 1px dotted #AAA;
} }
.name-edit.inactive button {
visibility: hidden;
}
.name-edit button {
opacity: 1;
}
.url-cell { .url-cell {
font-size: small; font-size: small;
} }
@ -153,21 +144,28 @@ td.inactive .popover {
position: absolute; position: absolute;
top: auto; top: auto;
left: auto; left: auto;
margin-top: 32px; margin-top: 57px;
margin-left: -77px; margin-left: -77px;
} }
.timeout { #checks-table > tbody > tr > th.th-frequency {
border: 1px solid rgba(0, 0, 0, 0); padding-left: 15px;
padding: 6px;
} }
.timeout_grace {
border: 1px solid rgba(0, 0, 0, 0);
padding: 6px;
display: block;
}
.timeout:hover { .timeout_grace:hover {
color: #337ab7;
border: 1px dotted #AAA; border: 1px dotted #AAA;
} }
.checks-subline {
color: #888;
}
.check-menu { .check-menu {
visibility: hidden; visibility: hidden;
} }
@ -175,3 +173,41 @@ td.inactive .popover {
tr:hover .check-menu { tr:hover .check-menu {
visibility: visible; visibility: visible;
} }
.update-timeout-info {
line-height: 22px;
}
.update-timeout-label {
position: relative;
right: 3px;
display: inline-block;
text-align: right;
width: 100px;
}
.update-timeout-value {
font-size: 22px;
display: inline-block;
width: 100px;
text-align: left;
white-space: nowrap;
}
#frequency-slider {
margin: 20px 50px 80px 50px;
}
#frequency-slider.noUi-connect {
background: #5cb85c;
}
#grace-slider {
margin: 20px 50px 60px 50px;
}
#grace-slider.noUi-connect {
background: #f0ad4e;
}

View File

@ -1,40 +1,97 @@
$(function () { $(function () {
$('[data-toggle="tooltip"]').tooltip();
$(".name-edit input").click(function() { var secsToText = function(total) {
$form = $(this.parentNode); total = Math.floor(total / 60);
if (!$form.hasClass("inactive")) var m = total % 60; total = Math.floor(total / 60);
return; var h = total % 24; total = Math.floor(total / 24);
var d = total % 7; total = Math.floor(total / 7);
var w = total;
// Click on all X buttons var result = "";
$(".name-edit:not(.inactive) .name-edit-cancel").click(); if (w) result += w + (w == 1 ? " week " : " weeks ");
if (d) result += d + (d == 1 ? " day " : " days ");
if (h) result += h + (h == 1 ? " hour " : " hours ");
if (m) result += m + (m == 1 ? " minute " : " minutes ");
// Make this form editable and store its initial value return result;
$form }
.removeClass("inactive")
.data("originalValue", this.value); var frequencySlider = document.getElementById("frequency-slider");
noUiSlider.create(frequencySlider, {
start: [20],
connect: "lower",
range: {
'min': [60, 60],
'30%': [3600, 3600],
'82.80%': [86400, 86400],
'max': 604800
},
pips: {
mode: 'values',
values: [60, 1800, 3600, 43200, 86400, 604800],
density: 5,
format: {
to: secsToText,
from: function() {}
}
}
}); });
$(".name-edit-cancel").click(function(){ frequencySlider.noUiSlider.on("update", function(a, b, value) {
var $form = $(this.parentNode); var rounded = Math.round(value);
var v = $form.data("originalValue"); $("#frequency-slider-value").text(secsToText(rounded));
$("#update-timeout-timeout").val(rounded);
});
$form
.addClass("inactive") var graceSlider = document.getElementById("grace-slider");
.find(".input-name").val(v); noUiSlider.create(graceSlider, {
start: [20],
connect: "lower",
range: {
'min': [60, 60],
'30%': [3600, 3600],
'82.80%': [86400, 86400],
'max': 604800
},
pips: {
mode: 'values',
values: [60, 1800, 3600, 43200, 86400, 604800],
density: 5,
format: {
to: secsToText,
from: function() {}
}
}
});
graceSlider.noUiSlider.on("update", function(a, b, value) {
var rounded = Math.round(value);
$("#grace-slider-value").text(secsToText(rounded));
$("#update-timeout-grace").val(rounded);
});
$('[data-toggle="tooltip"]').tooltip();
$(".my-checks-name").click(function() {
var $this = $(this);
$("#update-name-form").attr("action", $this.data("url"));
$("#update-name-input").val($this.text());
$('#update-name-modal').modal("show");
return false; return false;
}); });
$(".timeout").click(function() { $(".timeout_grace").click(function() {
$(".timeout-cell").addClass("inactive"); var $this = $(this);
$cell = $(this.parentNode); $("#update-timeout-form").attr("action", $this.data("url"));
$cell.removeClass("inactive"); frequencySlider.noUiSlider.set($this.data("timeout"))
}); graceSlider.noUiSlider.set($this.data("grace"))
$('#update-timeout-modal').modal("show");
$(".timeout-edit-cancel").click(function() {
$(this).parents("td").addClass("inactive");
return false; return false;
}); });
@ -48,4 +105,5 @@ $(function () {
return false; return false;
}); });
}); });

3
static/js/nouislider.min.js vendored Normal file

File diff suppressed because one or more lines are too long

View File

@ -9,6 +9,8 @@
{% load staticfiles %} {% load staticfiles %}
<link href='//fonts.googleapis.com/css?family=Open+Sans:400,300,600' rel='stylesheet' type='text/css'> <link href='//fonts.googleapis.com/css?family=Open+Sans:400,300,600' rel='stylesheet' type='text/css'>
<link rel="stylesheet" href="{% static 'css/bootstrap.css' %}" type="text/css"> <link rel="stylesheet" href="{% static 'css/bootstrap.css' %}" type="text/css">
<link rel="stylesheet" href="{% static 'css/nouislider.min.css' %}" type="text/css">
<link rel="stylesheet" href="{% static 'css/nouislider.pips.css' %}" type="text/css">
<link rel="stylesheet" href="{% static 'css/style.css' %}" type="text/css"> <link rel="stylesheet" href="{% static 'css/style.css' %}" type="text/css">
<link rel="stylesheet" href="{% static 'css/pricing.css' %}" type="text/css"> <link rel="stylesheet" href="{% static 'css/pricing.css' %}" type="text/css">
<link rel="stylesheet" href="{% static 'css/syntax.css' %}" type="text/css"> <link rel="stylesheet" href="{% static 'css/syntax.css' %}" type="text/css">

View File

@ -14,7 +14,10 @@
<th></th> <th></th>
<th class="th-name">Name</th> <th class="th-name">Name</th>
<th>URL</th> <th>URL</th>
<th>Frequency</th> <th class="th-frequency">
Frequency <br />
<span class="checks-subline">Grace</span>
</th>
<th>Last Ping</th> <th>Last Ping</th>
<th></th> <th></th>
</tr> </tr>
@ -30,62 +33,31 @@
{% endif %} {% endif %}
</td> </td>
<td class="name-cell"> <td class="name-cell">
<form <span data-url="{% url 'hc-update-name' check.code %}"
method="post" class="my-checks-name">{{ check.name }}</span>
action="{% url 'hc-update-name' check.code %}"
class="name-edit form-inline inactive">
{% csrf_token %}
<input
name="name"
type="text"
value="{{ check.name }}"
placeholder="unnamed"
class="input-name form-control" />
<button class="btn btn-primary" type="submit">
<span class="glyphicon glyphicon-ok"></span>
</button>
<button class="btn btn-default name-edit-cancel">
<span class="glyphicon glyphicon-remove"></span>
</button>
</form>
</td> </td>
<td class="url-cell"> <td class="url-cell">
<code>{{ check.url }}</code> <code>{{ check.url }}</code>
</td> </td>
<td class="timeout-cell inactive"> <td class="timeout-cell inactive">
<div class="timeout-dialog popover bottom"> <span
<div class="arrow"></div> data-url="{% url 'hc-update-timeout' check.code %}"
<div class="popover-content"> data-timeout="{{ check.timeout.total_seconds }}"
<form data-grace="{{ check.grace.total_seconds }}"
method="post" class="timeout_grace">
action="{% url 'hc-update-timeout' check.code %}" {% for label, value in duration_choices %}
class="form-inline">
{% csrf_token %}
<select class="form-control" name="timeout">
{% for label, value in timeout_choices %}
{% if check.timeout == value %}
<option selected>{{ label }}</option>
{% else %}
<option>{{ label }}</option>
{% endif %}
{% endfor %}
</select>
<button class="btn btn-primary" type="submit">
<span class="glyphicon glyphicon-ok"></span>
</button>
<button class="btn btn-default timeout-edit-cancel">
<span class="glyphicon glyphicon-remove"></span>
</button>
</form>
</div>
</div>
<span class="timeout">
{% for label, value in timeout_choices %}
{% if check.timeout == value %} {% if check.timeout == value %}
{{ label }} {{ label }}
{% endif %} {% endif %}
{% endfor %} {% endfor %}
<br />
<span class="checks-subline">
{% for label, value in duration_choices %}
{% if check.grace == value %}
{{ label }}
{% endif %}
{% endfor %}
</span>
</span> </span>
</td> </td>
<td> <td>
@ -135,11 +107,89 @@
</div> </div>
<div id="update-name-modal" class="modal fade">
<div class="modal-dialog">
<form id="update-name-form" method="post">
{% csrf_token %}
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal">&times;</span></button>
<h4 class="update-timeout-title">Update Name</h4>
</div> <div class="modal-body">
<p>Name:</p>
<input
id="update-name-input"
name="name"
type="text"
value="---"
placeholder="unnamed"
class="input-name form-control" />
</div>
<div class="modal-footer">
<button type="button" class="btn btn-default" data-dismiss="modal">Cancel</button>
<button type="submit" class="btn btn-primary">Save</button>
</div>
</div>
</form>
</div>
</div>
<div id="update-timeout-modal" class="modal fade">
<div class="modal-dialog">
<form id="update-timeout-form" method="post">
{% csrf_token %}
<input type="hidden" name="timeout" id="update-timeout-timeout" />
<input type="hidden" name="grace" id="update-timeout-grace" />
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal">&times;</span></button>
<h4 class="update-timeout-title">Update Frequency and Grace Period</h4>
</div>
<div class="modal-body">
<div class="update-timeout-info text-center">
<span
class="update-timeout-label"
data-toggle="tooltip"
title="Expected time between pings.">
Frequency
</span>
<span
id="frequency-slider-value"
class="update-timeout-value">
1 day
</span>
</div>
<div id="frequency-slider"></div>
<div class="update-timeout-info text-center">
<span
class="update-timeout-label"
data-toggle="tooltip"
title="When check is late, how much time to wait until alert is sent">
Grace Period
</span>
<span
id="grace-slider-value"
class="update-timeout-value">
1 day
</span>
</div>
<div id="grace-slider"></div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-default" data-dismiss="modal">Cancel</button>
<button type="submit" class="btn btn-primary">Save</button>
</div>
</div>
</form>
</div>
</div>
<div id="remove-check-modal" class="modal fade"> <div id="remove-check-modal" class="modal fade">
<div class="modal-dialog"> <div class="modal-dialog">
<form id="remove-check-form" method="post"> <form id="remove-check-form" method="post">
{% csrf_token %} {% csrf_token %}
<input type="hidden" name="code" class="remove-check-code" />
<div class="modal-content"> <div class="modal-content">
<div class="modal-header"> <div class="modal-header">
<button type="button" class="close" data-dismiss="modal">&times;</span></button> <button type="button" class="close" data-dismiss="modal">&times;</span></button>
@ -166,5 +216,7 @@
{% endblock %} {% endblock %}
{% block scripts %} {% block scripts %}
<script src="{% static 'js/moment.min.js' %}"></script>
<script src="{% static 'js/nouislider.min.js' %}"></script>
<script src="{% static 'js/checks.js' %}"></script> <script src="{% static 'js/checks.js' %}"></script>
{% endblock %} {% endblock %}