diff --git a/hc/front/forms.py b/hc/front/forms.py index 0783523b..0e1c7363 100644 --- a/hc/front/forms.py +++ b/hc/front/forms.py @@ -1,6 +1,5 @@ from django import forms from hc.front.validators import CronExpressionValidator, WebhookValidator -from hc.api.models import CHECK_KINDS class NameTagsForm(forms.Form): @@ -19,12 +18,15 @@ class NameTagsForm(forms.Form): class TimeoutForm(forms.Form): - kind = forms.ChoiceField(choices=CHECK_KINDS) 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, validators=[CronExpressionValidator()]) 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): diff --git a/hc/front/urls.py b/hc/front/urls.py index ca9a6f04..88bf9038 100644 --- a/hc/front/urls.py +++ b/hc/front/urls.py @@ -32,6 +32,7 @@ urlpatterns = [ url(r'^$', views.index, name="hc-index"), url(r'^checks/$', views.my_checks, name="hc-checks"), 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'^integrations/', include(channel_urls)), diff --git a/hc/front/views.py b/hc/front/views.py index 50d6c52b..c5dea32a 100644 --- a/hc/front/views.py +++ b/hc/front/views.py @@ -1,5 +1,6 @@ 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 import requests @@ -12,13 +13,14 @@ from django.shortcuts import get_object_or_404, redirect, render from django.urls import reverse from django.utils import timezone 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 hc.api.decorators import uuid_or_400 from hc.api.models import (DEFAULT_GRACE, DEFAULT_TIMEOUT, Channel, Check, Ping, Notification) from hc.front.forms import (AddWebhookForm, NameTagsForm, TimeoutForm, AddUrlForm, AddPdForm, AddEmailForm, - AddOpsGenieForm) + AddOpsGenieForm, CronForm) from pytz import all_timezones @@ -167,22 +169,55 @@ def update_timeout(request, code): if check.user != request.team.user: return HttpResponseForbidden() - form = TimeoutForm(request.POST) - if form.is_valid(): - check.kind = form.cleaned_data["kind"] + kind = request.POST.get("kind") + if kind == "simple": + 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.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.tz = form.cleaned_data["tz"] + check.grace = td(minutes=form.cleaned_data["grace"]) - if check.last_ping: - check.alert_after = check.get_alert_after() - - check.save() + if check.last_ping: + check.alert_after = check.get_alert_after() + check.save() 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 @uuid_or_400 def pause(request, code): diff --git a/static/css/my_checks.css b/static/css/my_checks.css index 0dfe3e92..2d1b7c5c 100644 --- a/static/css/my_checks.css +++ b/static/css/my_checks.css @@ -1,5 +1,9 @@ -#update-timeout-modal .modal-body { - padding-top: 40px; +#update-timeout-modal .modal-dialog { + width: 800px; +} + +#update-timeout-form .modal-body { + padding-top: 35px; } .update-timeout-info { @@ -20,21 +24,59 @@ width: 100px; text-align: left; white-space: nowrap; - } -#type-simple, #type-cron { +.kind-simple, .kind-cron { width: 70px; } -#schedule-block { - margin: 0 50px; +#update-timeout-simple { + display: none; +} + +#update-cron-form .modal-body { + padding: 40px; +} + +#update-cron-form label { + font-weight: normal; } #tz { 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 { margin: 20px 50px 80px 50px; } diff --git a/static/js/checks.js b/static/js/checks.js index 473af3cc..ebd212ac 100644 --- a/static/js/checks.js +++ b/static/js/checks.js @@ -1,5 +1,15 @@ $(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 HOUR = {name: "hour", nsecs: MINUTE.nsecs * 60}; var DAY = {name: "day", nsecs: HOUR.nsecs * 24}; @@ -58,7 +68,6 @@ $(function () { $("#update-timeout-timeout").val(rounded); }); - var graceSlider = document.getElementById("grace-slider"); noUiSlider.create(graceSlider, { start: [20], @@ -87,55 +96,73 @@ $(function () { $("#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() { - $("#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(); + var currentPreviewHash = ""; + function updateCronPreview() { + console.log("requested to update cron preview"); - 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() { $("#update-timeout-form").attr("action", this.dataset.url); + $("#update-cron-form").attr("action", this.dataset.url); + + // Simple periodSlider.noUiSlider.set(this.dataset.timeout); graceSlider.noUiSlider.set(this.dataset.grace); + + // Cron + $("#cron-preview").html("

Updating...

"); $("#schedule").val(this.dataset.schedule); 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") { - $("#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(); - } - + this.dataset.kind == "simple" ? showSimple() : showCron(); $('#update-timeout-modal').modal({"show":true, "backdrop":"static"}); return false; }); - $("#type-simple").click(function() { - $("#period-block").show(); - $("#schedule-block").hide(); - }); + // Wire up events for Timeout/Cron forms + $(".kind-simple").click(showSimple); + $(".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() { $("#remove-check-form").attr("action", this.dataset.url); @@ -189,6 +216,8 @@ $(function () { return false; }); + $('[data-toggle="tooltip"]').tooltip(); + $("#tz").selectize(); $(".usage-examples").click(function(e) { var a = e.target; @@ -202,8 +231,6 @@ $(function () { return false; }); - $("#tz").selectize(); - var clipboard = new Clipboard('button.copy-link'); $("button.copy-link").mouseout(function(e) { setTimeout(function() { diff --git a/templates/front/cron_preview.html b/templates/front/cron_preview.html new file mode 100644 index 00000000..60b2e1bb --- /dev/null +++ b/templates/front/cron_preview.html @@ -0,0 +1,20 @@ +{% load humanize tz %} + +{% if error %} +

Invalid cron expression

+{% else %} + + + + + {% for date in dates %} + + {% timezone tz %} + + {% endtimezone %} + + + + {% endfor %} +
Expected Ping Dates
{{ date|date:"M j, H:i" }}{{ date|naturaltime }}{{ date|date:"c" }}
+{% endif %} \ No newline at end of file diff --git a/templates/front/my_checks.html b/templates/front/my_checks.html index d24153c2..da74e2b2 100644 --- a/templates/front/my_checks.html +++ b/templates/front/my_checks.html @@ -110,52 +110,30 @@