Improved "Update Timeout" form with dynamic preview for cron mode

This commit is contained in:
Pēteris Caune 2016-12-23 20:19:06 +02:00
parent a412f05651
commit 1163364989
7 changed files with 271 additions and 109 deletions

View File

@ -1,6 +1,5 @@
from django import forms from django import forms
from hc.front.validators import CronExpressionValidator, WebhookValidator from hc.front.validators import CronExpressionValidator, WebhookValidator
from hc.api.models import CHECK_KINDS
class NameTagsForm(forms.Form): class NameTagsForm(forms.Form):
@ -19,12 +18,15 @@ class NameTagsForm(forms.Form):
class TimeoutForm(forms.Form): class TimeoutForm(forms.Form):
kind = forms.ChoiceField(choices=CHECK_KINDS)
timeout = forms.IntegerField(min_value=60, max_value=2592000) timeout = forms.IntegerField(min_value=60, max_value=2592000)
grace = forms.IntegerField(min_value=60, max_value=2592000)
class CronForm(forms.Form):
schedule = forms.CharField(required=False, max_length=100, schedule = forms.CharField(required=False, max_length=100,
validators=[CronExpressionValidator()]) validators=[CronExpressionValidator()])
tz = forms.CharField(required=False, max_length=36) tz = forms.CharField(required=False, max_length=36)
grace = forms.IntegerField(min_value=60, max_value=2592000) grace = forms.IntegerField(min_value=1, max_value=43200)
class AddPdForm(forms.Form): class AddPdForm(forms.Form):

View File

@ -32,6 +32,7 @@ urlpatterns = [
url(r'^$', views.index, name="hc-index"), url(r'^$', views.index, name="hc-index"),
url(r'^checks/$', views.my_checks, name="hc-checks"), url(r'^checks/$', views.my_checks, name="hc-checks"),
url(r'^checks/add/$', views.add_check, name="hc-add-check"), url(r'^checks/add/$', views.add_check, name="hc-add-check"),
url(r'^checks/cron_preview/$', views.cron_preview),
url(r'^checks/([\w-]+)/', include(check_urls)), url(r'^checks/([\w-]+)/', include(check_urls)),
url(r'^integrations/', include(channel_urls)), url(r'^integrations/', include(channel_urls)),

View File

@ -1,5 +1,6 @@
from collections import Counter from collections import Counter
from datetime import timedelta as td from croniter import croniter
from datetime import datetime, timedelta as td
from itertools import tee from itertools import tee
import requests import requests
@ -12,13 +13,14 @@ from django.shortcuts import get_object_or_404, redirect, render
from django.urls import reverse from django.urls import reverse
from django.utils import timezone from django.utils import timezone
from django.utils.crypto import get_random_string from django.utils.crypto import get_random_string
from django.views.decorators.csrf import csrf_exempt
from django.utils.six.moves.urllib.parse import urlencode from django.utils.six.moves.urllib.parse import urlencode
from hc.api.decorators import uuid_or_400 from hc.api.decorators import uuid_or_400
from hc.api.models import (DEFAULT_GRACE, DEFAULT_TIMEOUT, Channel, Check, from hc.api.models import (DEFAULT_GRACE, DEFAULT_TIMEOUT, Channel, Check,
Ping, Notification) Ping, Notification)
from hc.front.forms import (AddWebhookForm, NameTagsForm, from hc.front.forms import (AddWebhookForm, NameTagsForm,
TimeoutForm, AddUrlForm, AddPdForm, AddEmailForm, TimeoutForm, AddUrlForm, AddPdForm, AddEmailForm,
AddOpsGenieForm) AddOpsGenieForm, CronForm)
from pytz import all_timezones from pytz import all_timezones
@ -167,22 +169,55 @@ def update_timeout(request, code):
if check.user != request.team.user: if check.user != request.team.user:
return HttpResponseForbidden() return HttpResponseForbidden()
form = TimeoutForm(request.POST) kind = request.POST.get("kind")
if form.is_valid(): if kind == "simple":
check.kind = form.cleaned_data["kind"] form = TimeoutForm(request.POST)
if not form.is_valid():
return redirect("hc-checks")
check.kind = "simple"
check.timeout = td(seconds=form.cleaned_data["timeout"]) check.timeout = td(seconds=form.cleaned_data["timeout"])
check.grace = td(seconds=form.cleaned_data["grace"]) check.grace = td(seconds=form.cleaned_data["grace"])
elif kind == "cron":
form = CronForm(request.POST)
if not form.is_valid():
return redirect("hc-checks")
check.kind = "cron"
check.schedule = form.cleaned_data["schedule"] check.schedule = form.cleaned_data["schedule"]
check.tz = form.cleaned_data["tz"] check.tz = form.cleaned_data["tz"]
check.grace = td(minutes=form.cleaned_data["grace"])
if check.last_ping: if check.last_ping:
check.alert_after = check.get_alert_after() check.alert_after = check.get_alert_after()
check.save()
check.save()
return redirect("hc-checks") return redirect("hc-checks")
@csrf_exempt
def cron_preview(request):
schedule = request.POST.get("schedule")
tz = request.POST.get("tz")
ctx = {
"tz": tz,
"dates": []
}
try:
with timezone.override(tz):
now_naive = timezone.make_naive(timezone.now())
it = croniter(schedule, now_naive)
for i in range(0, 6):
date_naive = it.get_next(datetime)
ctx["dates"].append(timezone.make_aware(date_naive))
except:
ctx["error"] = True
return render(request, "front/cron_preview.html", ctx)
@login_required @login_required
@uuid_or_400 @uuid_or_400
def pause(request, code): def pause(request, code):

View File

@ -1,5 +1,9 @@
#update-timeout-modal .modal-body { #update-timeout-modal .modal-dialog {
padding-top: 40px; width: 800px;
}
#update-timeout-form .modal-body {
padding-top: 35px;
} }
.update-timeout-info { .update-timeout-info {
@ -20,21 +24,59 @@
width: 100px; width: 100px;
text-align: left; text-align: left;
white-space: nowrap; white-space: nowrap;
} }
#type-simple, #type-cron { .kind-simple, .kind-cron {
width: 70px; width: 70px;
} }
#schedule-block { #update-timeout-simple {
margin: 0 50px; display: none;
}
#update-cron-form .modal-body {
padding: 40px;
}
#update-cron-form label {
font-weight: normal;
} }
#tz { #tz {
width: 300px; width: 300px;
} }
#cron-preview {
background: #f8f8f8;
height: 256px;
}
#cron-preview p {
padding: 8px;
font-size: small;
}
#cron-preview th {
border-top: 0;
font-weight: normal;
font-size: small;
}
.cron-preview-date {
width: 120px;
}
.cron-preview-rel {
font-size: small;
}
.cron-preview-timestamp {
font-size: small;
font-family: monospace;
text-align: right;
color: #888;
}
#period-slider { #period-slider {
margin: 20px 50px 80px 50px; margin: 20px 50px 80px 50px;
} }

View File

@ -1,5 +1,15 @@
$(function () { $(function () {
$(".my-checks-name").click(function() {
$("#update-name-form").attr("action", this.dataset.url);
$("#update-name-input").val(this.dataset.name);
$("#update-tags-input").val(this.dataset.tags);
$('#update-name-modal').modal("show");
$("#update-name-input").focus();
return false;
});
var MINUTE = {name: "minute", nsecs: 60}; var MINUTE = {name: "minute", nsecs: 60};
var HOUR = {name: "hour", nsecs: MINUTE.nsecs * 60}; var HOUR = {name: "hour", nsecs: MINUTE.nsecs * 60};
var DAY = {name: "day", nsecs: HOUR.nsecs * 24}; var DAY = {name: "day", nsecs: HOUR.nsecs * 24};
@ -58,7 +68,6 @@ $(function () {
$("#update-timeout-timeout").val(rounded); $("#update-timeout-timeout").val(rounded);
}); });
var graceSlider = document.getElementById("grace-slider"); var graceSlider = document.getElementById("grace-slider");
noUiSlider.create(graceSlider, { noUiSlider.create(graceSlider, {
start: [20], start: [20],
@ -87,55 +96,73 @@ $(function () {
$("#update-timeout-grace").val(rounded); $("#update-timeout-grace").val(rounded);
}); });
function showSimple() {
console.log("show simple");
$("#update-timeout-form").show();
$("#update-cron-form").hide();
}
$('[data-toggle="tooltip"]').tooltip(); function showCron() {
console.log("show cron");
$("#update-timeout-form").hide();
$("#update-cron-form").show();
}
$(".my-checks-name").click(function() { var currentPreviewHash = "";
$("#update-name-form").attr("action", this.dataset.url); function updateCronPreview() {
$("#update-name-input").val(this.dataset.name); console.log("requested to update cron preview");
$("#update-tags-input").val(this.dataset.tags);
$('#update-name-modal').modal("show");
$("#update-name-input").focus();
return false; var schedule = $("#schedule").val();
}); var tz = $("#tz").val();
var hash = schedule + tz;
// Don't try preview with empty values, or if values have not changed
if (!schedule || !tz || hash == currentPreviewHash)
return;
// OK, we're good
currentPreviewHash = hash;
$("#cron-preview-title").text("Updating...");
$.post("/checks/cron_preview/", {schedule: schedule, tz: tz},
function(data) {
if (hash != currentPreviewHash) {
return; // ignore stale results
}
$("#cron-preview" ).html(data);
var haveError = $("#invalid-cron-expression").size() > 0;
$("#update-cron-submit").prop("disabled", haveError);
}
);
}
$(".timeout-grace").click(function() { $(".timeout-grace").click(function() {
$("#update-timeout-form").attr("action", this.dataset.url); $("#update-timeout-form").attr("action", this.dataset.url);
$("#update-cron-form").attr("action", this.dataset.url);
// Simple
periodSlider.noUiSlider.set(this.dataset.timeout); periodSlider.noUiSlider.set(this.dataset.timeout);
graceSlider.noUiSlider.set(this.dataset.grace); graceSlider.noUiSlider.set(this.dataset.grace);
// Cron
$("#cron-preview").html("<p>Updating...</p>");
$("#schedule").val(this.dataset.schedule); $("#schedule").val(this.dataset.schedule);
document.getElementById("tz").selectize.setValue(this.dataset.tz); document.getElementById("tz").selectize.setValue(this.dataset.tz);
var minutes = parseInt(this.dataset.grace / 60);
$("#update-timeout-grace-cron").val(minutes);
if (this.dataset.kind == "cron") { this.dataset.kind == "simple" ? showSimple() : showCron();
$("#type-simple").removeClass("active");
$("#type-cron").addClass("active");
$("#type-cron input").prop("checked", true);
$("#period-block").hide();
$("#schedule-block").show();
} else {
$("#type-simple").addClass("active");
$("#type-simple input").prop("checked", true);
$("#type-cron").removeClass("active");
$("#period-block").show();
$("#schedule-block").hide();
}
$('#update-timeout-modal').modal({"show":true, "backdrop":"static"}); $('#update-timeout-modal').modal({"show":true, "backdrop":"static"});
return false; return false;
}); });
$("#type-simple").click(function() { // Wire up events for Timeout/Cron forms
$("#period-block").show(); $(".kind-simple").click(showSimple);
$("#schedule-block").hide(); $(".kind-cron").click(showCron);
});
$("#schedule").on("keyup", updateCronPreview);
$("#tz").on("change", updateCronPreview);
$("#type-cron").click(function() {
$("#period-block").hide();
$("#schedule-block").show();
});
$(".check-menu-remove").click(function() { $(".check-menu-remove").click(function() {
$("#remove-check-form").attr("action", this.dataset.url); $("#remove-check-form").attr("action", this.dataset.url);
@ -189,6 +216,8 @@ $(function () {
return false; return false;
}); });
$('[data-toggle="tooltip"]').tooltip();
$("#tz").selectize();
$(".usage-examples").click(function(e) { $(".usage-examples").click(function(e) {
var a = e.target; var a = e.target;
@ -202,8 +231,6 @@ $(function () {
return false; return false;
}); });
$("#tz").selectize();
var clipboard = new Clipboard('button.copy-link'); var clipboard = new Clipboard('button.copy-link');
$("button.copy-link").mouseout(function(e) { $("button.copy-link").mouseout(function(e) {
setTimeout(function() { setTimeout(function() {

View File

@ -0,0 +1,20 @@
{% load humanize tz %}
{% if error %}
<p id="invalid-cron-expression">Invalid cron expression</p>
{% else %}
<table class="table">
<tr>
<th id="cron-preview-title" colspan="3">Expected Ping Dates</th>
</tr>
{% for date in dates %}
<tr>
{% timezone tz %}
<td class="cron-preview-date">{{ date|date:"M j, H:i" }}</td>
{% endtimezone %}
<td class="cron-preview-rel">{{ date|naturaltime }}</td>
<td class="cron-preview-timestamp">{{ date|date:"c" }}</td>
</tr>
{% endfor %}
</table>
{% endif %}

View File

@ -110,52 +110,30 @@
<div id="update-timeout-modal" class="modal"> <div id="update-timeout-modal" class="modal">
<div class="modal-dialog"> <div class="modal-dialog">
<form id="update-timeout-form" method="post"> <div class="modal-content">
{% csrf_token %} <form id="update-timeout-form" method="post">
<input type="hidden" name="timeout" id="update-timeout-timeout" /> {% csrf_token %}
<input type="hidden" name="grace" id="update-timeout-grace" /> <input type="hidden" name="kind" value="simple" />
<div class="modal-content"> <input type="hidden" name="timeout" id="update-timeout-timeout" />
<input type="hidden" name="grace" id="update-timeout-grace" />
<div class="modal-body"> <div class="modal-body">
<div id="period-block"> <div class="update-timeout-info text-center">
<div class="update-timeout-info text-center"> <span
<span class="update-timeout-label"
class="update-timeout-label" data-toggle="tooltip"
data-toggle="tooltip" title="Expected time between pings.">
title="Expected time between pings."> Period
Period </span>
</span> <span
<span id="period-slider-value"
id="period-slider-value" class="update-timeout-value">
class="update-timeout-value"> 1 day
1 day </span>
</span>
</div>
<div id="period-slider"></div>
</div>
<div id="schedule-block">
<div class="form-group">
<label for="schedule">
Cron expression
<a href="https://en.wikipedia.org/wiki/Cron#Overview">(reference)</a>
</label>
<input
type="text"
class="form-control input-lg"
id="schedule"
name="schedule"
placeholder="* * * * *">
</div>
<div class="form-group">
<label for="schedule">Server's Timezone</label>
<br />
<select id="tz" name="tz" class="form-control">
<option>UTC</option>
{% for tz in timezones %}
<option>{{ tz }}</option>{% endfor %}
</select>
</div>
</div> </div>
<div id="period-slider"></div>
<div class="update-timeout-info text-center"> <div class="update-timeout-info text-center">
<span <span
class="update-timeout-label" class="update-timeout-label"
@ -182,25 +160,82 @@
When a check is late, how much time to wait until alert is sent. When a check is late, how much time to wait until alert is sent.
</p> </p>
</div> </div>
</div> </div>
<div class="modal-footer"> <div class="modal-footer">
<div class="btn-group pull-left" data-toggle="buttons"> <div class="btn-group pull-left">
<label id="type-simple" class="btn btn-default"> <label class="btn btn-default kind-simple active">Simple</label>
<input type="radio" name="kind" value="simple" autocomplete="off"> <label class="btn btn-default kind-cron">Cron</label>
Simple
</label>
<label id="type-cron" class="btn btn-default">
<input type="radio" name="kind" value="cron" autocomplete="off">
Cron
</label>
</div> </div>
<button type="button" class="btn btn-default" data-dismiss="modal">Cancel</button> <button type="button" class="btn btn-default" data-dismiss="modal">Cancel</button>
<button type="submit" class="btn btn-primary">Save</button> <button type="submit" class="btn btn-primary">Save</button>
</div> </div>
</div> </form>
</form>
<form id="update-cron-form" method="post">
{% csrf_token %}
<input type="hidden" name="kind" value="cron" />
<div class="modal-body">
<div class="row">
<div class="col-sm-4">
<div class="form-group">
<label for="schedule">Cron Expression</label>
<input
type="text"
class="form-control"
id="schedule"
name="schedule"
placeholder="* * * * *">
</div>
</div>
<div class="col-sm-4">
<div class="form-group">
<label for="tz">Server's Timezone</label>
<br />
<select id="tz" name="tz" class="form-control">
<option>UTC</option>
{% for tz in timezones %}
<option>{{ tz }}</option>{% endfor %}
</select>
</div>
</div>
<div class="col-sm-4">
<div class="form-group">
<label for="cron-grace">Grace Time</label>
<div class="input-group">
<input
type="number"
min="1"
max="43200"
class="form-control"
id="update-timeout-grace-cron"
name="grace">
<div class="input-group-addon">minutes</div>
</div>
</div>
</div>
</div>
<div class="row">
<div class="col-sm-12">
<div id="cron-preview"></div>
</div>
</div>
</div>
<div class="modal-footer">
<div class="btn-group pull-left">
<label class="btn btn-default kind-simple">Simple</label>
<label class="btn btn-default active kind-cron">Cron</label>
</div>
<button type="button" class="btn btn-default" data-dismiss="modal">Cancel</button>
<button id="update-cron-submit" type="submit" class="btn btn-primary">
Save
</button>
</div>
</form>
</div>
</div> </div>
</div> </div>