Browse Source

Add ability to set grace period

pull/7/head
Pēteris Caune 9 years ago
parent
commit
0af1fb782a
12 changed files with 384 additions and 102 deletions
  1. +1
    -1
      hc/api/management/commands/ensuretriggers.py
  2. +20
    -0
      hc/api/migrations/0006_check_grace.py
  3. +9
    -2
      hc/api/models.py
  4. +2
    -3
      hc/front/forms.py
  5. +7
    -4
      hc/front/views.py
  6. +4
    -0
      static/css/nouislider.min.css
  7. +98
    -0
      static/css/nouislider.pips.css
  8. +55
    -19
      static/css/style.css
  9. +82
    -24
      static/js/checks.js
  10. +3
    -0
      static/js/nouislider.min.js
  11. +2
    -0
      templates/base.html
  12. +101
    -49
      templates/front/my_checks.html

+ 1
- 1
hc/api/management/commands/ensuretriggers.py View File

@ -13,7 +13,7 @@ CREATE OR REPLACE FUNCTION update_alert_after()
RETURNS trigger AS $update_alert_after$
BEGIN
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;
RETURN NEW;
END;


+ 20
- 0
hc/api/migrations/0006_check_grace.py 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)),
),
]

+ 9
- 2
hc/api/models.py View File

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


+ 2
- 3
hc/front/forms.py View File

@ -1,7 +1,6 @@
from django import forms
from hc.api.models import TIMEOUT_CHOICES
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)

+ 7
- 4
hc/front/views.py View File

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


+ 4
- 0
static/css/nouislider.min.css 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}

+ 98
- 0
static/css/nouislider.pips.css 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;
}

+ 55
- 19
static/css/style.css View File

@ -121,25 +121,16 @@ table.table tr > th.th-name {
vertical-align: middle;
}
.name-edit.inactive .input-name {
.my-checks-name {
border: 1px solid rgba(0, 0, 0, 0);
background: none;
box-shadow: none;
transition: none;
padding: 6px;
display: block;
}
.name-edit.inactive .input-name:hover {
.my-checks-name:hover {
border: 1px dotted #AAA;
}
.name-edit.inactive button {
visibility: hidden;
}
.name-edit button {
opacity: 1;
}
.url-cell {
font-size: small;
}
@ -153,25 +144,70 @@ td.inactive .popover {
position: absolute;
top: auto;
left: auto;
margin-top: 32px;
margin-top: 57px;
margin-left: -77px;
}
.timeout {
#checks-table > tbody > tr > th.th-frequency {
padding-left: 15px;
}
.timeout_grace {
border: 1px solid rgba(0, 0, 0, 0);
padding: 6px;
display: block;
}
.timeout:hover {
color: #337ab7;
.timeout_grace:hover {
border: 1px dotted #AAA;
}
.checks-subline {
color: #888;
}
.check-menu {
visibility: hidden;
}
tr:hover .check-menu {
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;
}

+ 82
- 24
static/js/checks.js View File

@ -1,40 +1,97 @@
$(function () {
$('[data-toggle="tooltip"]').tooltip();
$(".name-edit input").click(function() {
$form = $(this.parentNode);
if (!$form.hasClass("inactive"))
return;
var secsToText = function(total) {
total = Math.floor(total / 60);
var m = total % 60; total = Math.floor(total / 60);
var h = total % 24; total = Math.floor(total / 24);
var d = total % 7; total = Math.floor(total / 7);
var w = total;
var result = "";
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 ");
// Click on all X buttons
$(".name-edit:not(.inactive) .name-edit-cancel").click();
return result;
}
// Make this form editable and store its initial value
$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(){
var $form = $(this.parentNode);
var v = $form.data("originalValue");
frequencySlider.noUiSlider.on("update", function(a, b, value) {
var rounded = Math.round(value);
$("#frequency-slider-value").text(secsToText(rounded));
$("#update-timeout-timeout").val(rounded);
});
$form
.addClass("inactive")
.find(".input-name").val(v);
return false;
var graceSlider = document.getElementById("grace-slider");
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() {}
}
}
});
$(".timeout").click(function() {
$(".timeout-cell").addClass("inactive");
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");
$cell = $(this.parentNode);
$cell.removeClass("inactive");
return false;
});
$(".timeout-edit-cancel").click(function() {
$(this).parents("td").addClass("inactive");
$(".timeout_grace").click(function() {
var $this = $(this);
$("#update-timeout-form").attr("action", $this.data("url"));
frequencySlider.noUiSlider.set($this.data("timeout"))
graceSlider.noUiSlider.set($this.data("grace"))
$('#update-timeout-modal').modal("show");
return false;
});
@ -48,4 +105,5 @@ $(function () {
return false;
});
});

+ 3
- 0
static/js/nouislider.min.js
File diff suppressed because it is too large
View File


+ 2
- 0
templates/base.html View File

@ -9,6 +9,8 @@
{% load staticfiles %}
<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/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/pricing.css' %}" type="text/css">
<link rel="stylesheet" href="{% static 'css/syntax.css' %}" type="text/css">


+ 101
- 49
templates/front/my_checks.html View File

@ -14,7 +14,10 @@
<th></th>
<th class="th-name">Name</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></th>
</tr>
@ -30,62 +33,31 @@
{% endif %}
</td>
<td class="name-cell">
<form
method="post"
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>
<span data-url="{% url 'hc-update-name' check.code %}"
class="my-checks-name">{{ check.name }}</span>
</td>
<td class="url-cell">
<code>{{ check.url }}</code>
</td>
<td class="timeout-cell inactive">
<div class="timeout-dialog popover bottom">
<div class="arrow"></div>
<div class="popover-content">
<form
method="post"
action="{% url 'hc-update-timeout' check.code %}"
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 %}
<span
data-url="{% url 'hc-update-timeout' check.code %}"
data-timeout="{{ check.timeout.total_seconds }}"
data-grace="{{ check.grace.total_seconds }}"
class="timeout_grace">
{% for label, value in duration_choices %}
{% if check.timeout == value %}
{{ label }}
{% endif %}
{% endfor %}
<br />
<span class="checks-subline">
{% for label, value in duration_choices %}
{% if check.grace == value %}
{{ label }}
{% endif %}
{% endfor %}
</span>
</span>
</td>
<td>
@ -135,11 +107,89 @@
</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 class="modal-dialog">
<form id="remove-check-form" method="post">
{% csrf_token %}
<input type="hidden" name="code" class="remove-check-code" />
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal">&times;</span></button>
@ -166,5 +216,7 @@
{% endblock %}
{% 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>
{% endblock %}

Loading…
Cancel
Save