From e0d2f36928a682a95e20e9e2d39f40d96cb9e671 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?P=C4=93teris=20Caune?=
Date: Thu, 4 Nov 2021 12:36:03 +0200
Subject: [PATCH] Improve period and grace controls, allow up to 365 day
periods
Fixes: #281
---
CHANGELOG.md | 1 +
hc/api/schemas.py | 4 +-
hc/api/tests/test_update_check.py | 13 +++
hc/front/forms.py | 4 +-
static/css/my_checks.css | 39 ++++---
static/js/update-timeout-modal.js | 126 ++++++++++++++--------
templates/docs/api.html | 20 ++--
templates/docs/api.md | 20 ++--
templates/front/update_timeout_modal.html | 44 ++++----
9 files changed, 170 insertions(+), 101 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 117ee0bb..1c7ead2f 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -8,6 +8,7 @@ All notable changes to this project will be documented in this file.
- Change outgoing webhook timeout to 10s, but cap the total time to 20s
- Implement automatic `api_ping` and `api_notification` pruning (#556)
- Update Dockerfile to install apprise (#581)
+- Improve period and grace controls, allow up to 365 day periods (#281)
### Bug Fixes
- Fix hc.api.views.ping to handle non-utf8 data in request body (#574)
diff --git a/hc/api/schemas.py b/hc/api/schemas.py
index c7c817ad..7615712d 100644
--- a/hc/api/schemas.py
+++ b/hc/api/schemas.py
@@ -4,8 +4,8 @@ check = {
"name": {"type": "string", "maxLength": 100},
"desc": {"type": "string"},
"tags": {"type": "string", "maxLength": 500},
- "timeout": {"type": "number", "minimum": 60, "maximum": 2592000},
- "grace": {"type": "number", "minimum": 60, "maximum": 2592000},
+ "timeout": {"type": "number", "minimum": 60, "maximum": 31536000},
+ "grace": {"type": "number", "minimum": 60, "maximum": 31536000},
"schedule": {"type": "string", "format": "cron", "maxLength": 100},
"tz": {"type": "string", "format": "timezone", "maxLength": 36},
"channels": {"type": "string"},
diff --git a/hc/api/tests/test_update_check.py b/hc/api/tests/test_update_check.py
index d5ff4939..bbd34b4e 100644
--- a/hc/api/tests/test_update_check.py
+++ b/hc/api/tests/test_update_check.py
@@ -321,3 +321,16 @@ class UpdateCheckTestCase(BaseTestCase):
def test_it_rejects_bad_methods_value(self):
r = self.post(self.check.code, {"api_key": "X" * 32, "methods": "bad-value"})
self.assertEqual(r.status_code, 400)
+
+ def test_it_accepts_60_days_timeout(self):
+ payload = {"api_key": "X" * 32, "timeout": 60 * 24 * 3600}
+ r = self.post(self.check.code, payload)
+ self.assertEqual(r.status_code, 200)
+
+ self.check.refresh_from_db()
+ self.assertEqual(self.check.timeout.total_seconds(), 60 * 24 * 3600)
+
+ def test_it_rejects_out_of_range_timeout(self):
+ payload = {"api_key": "X" * 32, "timeout": 500 * 24 * 3600}
+ r = self.post(self.check.code, payload)
+ self.assertEqual(r.status_code, 400)
diff --git a/hc/front/forms.py b/hc/front/forms.py
index 44ce79c6..2fdfefed 100644
--- a/hc/front/forms.py
+++ b/hc/front/forms.py
@@ -96,8 +96,8 @@ class FilteringRulesForm(forms.Form):
class TimeoutForm(forms.Form):
- timeout = forms.IntegerField(min_value=60, max_value=2592000)
- grace = forms.IntegerField(min_value=60, max_value=2592000)
+ timeout = forms.IntegerField(min_value=60, max_value=31536000)
+ grace = forms.IntegerField(min_value=60, max_value=31536000)
def clean_timeout(self):
return td(seconds=self.cleaned_data["timeout"])
diff --git a/static/css/my_checks.css b/static/css/my_checks.css
index 44bd09b1..f1370e16 100644
--- a/static/css/my_checks.css
+++ b/static/css/my_checks.css
@@ -34,7 +34,7 @@
@media (min-width: 992px) {
#update-timeout-modal .modal-dialog, #ping-details-modal .modal-dialog {
- width: 800px;
+ width: 850px;
}
#update-name-modal .modal-dialog {
width: 650px;
@@ -46,24 +46,37 @@
padding-top: 35px;
}
-.update-timeout-info {
- line-height: 22px;
+.interval-controls {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ font-size: 18px;
}
-.update-timeout-label {
- position: relative;
- right: 3px;
- display: inline-block;
+.interval-controls label {
+ font-weight: normal;
+ padding-right: 16px;
+ width: 120px;
text-align: right;
- width: 100px;
}
-.update-timeout-value {
- font-size: 22px;
- display: inline-block;
+.interval-controls input {
width: 100px;
- text-align: left;
- white-space: nowrap;
+ text-align: center;
+ -moz-appearance: textfield;
+}
+
+.interval-controls input::-webkit-outer-spin-button,
+.interval-controls input::-webkit-inner-spin-button {
+ -webkit-appearance: none;
+}
+
+.interval-controls select.form-control {
+ width: auto;
+ padding-left: 8px;
+ margin-left: 8px;
+ /* Fix dropdown background in Chrome & dark mode */
+ background: var(--panel-bg);
}
.kind-simple, .kind-cron {
diff --git a/static/js/update-timeout-modal.js b/static/js/update-timeout-modal.js
index ca00a11e..1aebe77e 100644
--- a/static/js/update-timeout-modal.js
+++ b/static/js/update-timeout-modal.js
@@ -1,5 +1,9 @@
$(function () {
var base = document.getElementById("base-url").getAttribute("href").slice(0, -1);
+ var period = document.getElementById("period-value");
+ var periodUnit = document.getElementById("period-unit");
+ var grace = document.getElementById("grace-value");
+ var graceUnit = document.getElementById("grace-unit");
$(".rw .timeout-grace").click(function() {
var code = $(this).closest("tr.checks-row").attr("id");
@@ -12,9 +16,19 @@ $(function () {
$("#update-timeout-form").attr("action", url);
$("#update-cron-form").attr("action", url);
- // Simple
+ // Simple, period
+ var parsed = secsToUnits(this.dataset.timeout);
+ period.value = parsed.value;
+ periodUnit.value = parsed.unit;
periodSlider.noUiSlider.set(this.dataset.timeout);
+ $("#update-timeout-timeout").val(this.dataset.timeout);
+
+ // Simple, grace
+ var parsed = secsToUnits(this.dataset.grace);
+ grace.value = parsed.value;
+ graceUnit.value = parsed.unit;
graceSlider.noUiSlider.set(this.dataset.grace);
+ $("#update-timeout-grace").val(this.dataset.grace);
// Cron
currentPreviewHash = "";
@@ -30,35 +44,27 @@ $(function () {
return false;
});
- var MINUTE = {name: "minute", nsecs: 60};
- var HOUR = {name: "hour", nsecs: MINUTE.nsecs * 60};
- var DAY = {name: "day", nsecs: HOUR.nsecs * 24};
- var WEEK = {name: "week", nsecs: DAY.nsecs * 7};
- var UNITS = [WEEK, DAY, HOUR, MINUTE];
-
- var secsToText = function(total) {
- var remainingSeconds = Math.floor(total);
- var result = "";
- for (var i=0, unit; unit=UNITS[i]; i++) {
- if (unit === WEEK && remainingSeconds % unit.nsecs != 0) {
- // Say "8 days" instead of "1 week 1 day"
- continue
- }
-
- var count = Math.floor(remainingSeconds / unit.nsecs);
- remainingSeconds = remainingSeconds % unit.nsecs;
-
- if (count == 1) {
- result += "1 " + unit.name + " ";
- }
-
- if (count > 1) {
- result += count + " " + unit.name + "s ";
- }
+ var secsToUnits = function(secs) {
+ if (secs % 86400 == 0) {
+ return {value: secs / 86400, unit: 86400}
+ }
+ if (secs % 3600 == 0) {
+ return {value: secs / 3600, unit: 3600}
}
- return result;
- };
+ return {value: Math.round(secs / 60), unit: 60}
+ }
+
+ var pipLabels = {
+ 60: "1 minute",
+ 1800: "30 minutes",
+ 3600: "1 hour",
+ 43200: "12 hours",
+ 86400: "1 day",
+ 604800: "1 week",
+ 2592000: "30 days",
+ 31536000: "365 days"
+ }
var periodSlider = document.getElementById("period-slider");
noUiSlider.create(periodSlider, {
@@ -66,54 +72,86 @@ $(function () {
connect: "lower",
range: {
'min': [60, 60],
- '33%': [3600, 3600],
- '66%': [86400, 86400],
- '83%': [604800, 604800],
- 'max': 2592000,
+ '30%': [3600, 3600],
+ '60%': [86400, 86400],
+ '75%': [604800, 86400],
+ '90%': [2592000, 2592000],
+ 'max': 31536000
},
pips: {
mode: 'values',
- values: [60, 1800, 3600, 43200, 86400, 604800, 2592000],
+ values: [60, 1800, 3600, 43200, 86400, 604800, 2592000, 31536000],
density: 4,
format: {
- to: secsToText,
+ to: function(v) { return pipLabels[v] },
from: function() {}
}
}
});
- periodSlider.noUiSlider.on("update", function(a, b, value) {
+ // Update inputs and the hidden field when user slides the period slider
+ periodSlider.noUiSlider.on("slide", function(a, b, value) {
var rounded = Math.round(value);
- $("#period-slider-value").text(secsToText(rounded));
$("#update-timeout-timeout").val(rounded);
+
+ var parsed = secsToUnits(rounded);
+ period.value = parsed.value;
+ periodUnit.value = parsed.unit;
});
+ // Update the slider and the hidden field when user changes period inputs
+ $(".period-input").on("keyup change", function() {
+ var secs = Math.round(period.value * periodUnit.value);
+ period.setCustomValidity(secs <= 31536000 ? "" : "Must not exceed 365 days");
+
+ if (secs >= 60) {
+ periodSlider.noUiSlider.set(secs);
+ $("#update-timeout-timeout").val(secs);
+ }
+ })
+
var graceSlider = document.getElementById("grace-slider");
noUiSlider.create(graceSlider, {
start: [20],
connect: "lower",
range: {
'min': [60, 60],
- '33%': [3600, 3600],
- '66%': [86400, 86400],
- '83%': [604800, 604800],
- 'max': 2592000,
+ '30%': [3600, 3600],
+ '60%': [86400, 86400],
+ '75%': [604800, 86400],
+ '90%': [2592000, 2592000],
+ 'max': 31536000
},
pips: {
mode: 'values',
- values: [60, 1800, 3600, 43200, 86400, 604800, 2592000],
+ values: [60, 1800, 3600, 43200, 86400, 604800, 2592000, 31536000],
density: 4,
format: {
- to: secsToText,
+ to: function(v) { return pipLabels[v] },
from: function() {}
}
}
});
- graceSlider.noUiSlider.on("update", function(a, b, value) {
+ // Update inputs and the hidden field when user slides the grace slider
+ graceSlider.noUiSlider.on("slide", function(a, b, value) {
var rounded = Math.round(value);
- $("#grace-slider-value").text(secsToText(rounded));
$("#update-timeout-grace").val(rounded);
+
+ var parsed = secsToUnits(rounded);
+ grace.value = parsed.value;
+ graceUnit.value = parsed.unit;
+ });
+
+ // Update the slider and the hidden field when user changes grace inputs
+ $(".grace-input").on("keyup change", function() {
+ var secs = Math.round(grace.value * graceUnit.value);
+ grace.setCustomValidity(secs <= 31536000 ? "" : "Must not exceed 365 days");
+
+ if (secs >= 60) {
+ graceSlider.noUiSlider.set(secs);
+ $("#update-timeout-grace").val(secs);
+ }
});
function showSimple() {
diff --git a/templates/docs/api.html b/templates/docs/api.html
index 976187f7..cd1b4b84 100644
--- a/templates/docs/api.html
+++ b/templates/docs/api.html
@@ -302,16 +302,16 @@ Example:
timeout
number, optional, default value: {{ default_timeout }}.
-A number of seconds, the expected period of this check.
-Minimum: 60 (one minute), maximum: 2592000 (30 days).
+The expected period of this check in seconds.
+Minimum: 60 (one minute), maximum: 31536000 (365 days).
Example for a 5-minute timeout:
-
{"kind": "simple", "timeout": 300}
+
{"timeout": 300}
grace
number, optional, default value: {{ default_grace }}.
-A number of seconds, the grace period for this check.
-Minimum: 60 (one minute), maximum: 2592000 (30 days).
+The grace period for this check in seconds.
+Minimum: 60 (one minute), maximum: 31536000 (365 days).
schedule
@@ -461,16 +461,16 @@ parameter, SITE_NAME will leave its value unchanged.
timeout
number, optional.
-A number of seconds, the expected period of this check.
-Minimum: 60 (one minute), maximum: 2592000 (30 days).
+The expected period of this check in seconds.
+Minimum: 60 (one minute), maximum: 31536000 (365 days).
Example for a 5-minute timeout:
-
{"kind": "simple", "timeout": 300}
+
{"timeout": 300}
grace
number, optional.
-A number of seconds, the grace period for this check.
-Minimum: 60 (one minute), maximum: 2592000 (30 days).
+The grace period for this check in seconds.
+Minimum: 60 (one minute), maximum: 31536000 (365 days).
schedule
diff --git a/templates/docs/api.md b/templates/docs/api.md
index f54f6adb..8e281e53 100644
--- a/templates/docs/api.md
+++ b/templates/docs/api.md
@@ -306,20 +306,20 @@ desc
timeout
: number, optional, default value: {{ default_timeout }}.
- A number of seconds, the expected period of this check.
+ The expected period of this check in seconds.
- Minimum: 60 (one minute), maximum: 2592000 (30 days).
+ Minimum: 60 (one minute), maximum: 31536000 (365 days).
Example for a 5-minute timeout:
- {"kind": "simple", "timeout": 300}
+ {"timeout": 300}
grace
: number, optional, default value: {{ default_grace }}.
- A number of seconds, the grace period for this check.
+ The grace period for this check in seconds.
- Minimum: 60 (one minute), maximum: 2592000 (30 days).
+ Minimum: 60 (one minute), maximum: 31536000 (365 days).
schedule
: string, optional, default value: "`* * * * *`".
@@ -506,20 +506,20 @@ desc
timeout
: number, optional.
- A number of seconds, the expected period of this check.
+ The expected period of this check in seconds.
- Minimum: 60 (one minute), maximum: 2592000 (30 days).
+ Minimum: 60 (one minute), maximum: 31536000 (365 days).
Example for a 5-minute timeout:
- {"kind": "simple", "timeout": 300}
+ {"timeout": 300}
grace
: number, optional.
- A number of seconds, the grace period for this check.
+ The grace period for this check in seconds.
- Minimum: 60 (one minute), maximum: 2592000 (30 days).
+ Minimum: 60 (one minute), maximum: 31536000 (365 days).
schedule
: string, optional.
diff --git a/templates/front/update_timeout_modal.html b/templates/front/update_timeout_modal.html
index cdb76ad6..0a8c75ca 100644
--- a/templates/front/update_timeout_modal.html
+++ b/templates/front/update_timeout_modal.html
@@ -8,30 +8,34 @@
-
-
- Period
-
-
- 1 day
-
+
+ Period
+
+
+ minutes
+ hours
+ days
+
-
-
-
- Grace Time
-
-
- 1 day
-
+
+ Grace Time
+
+
+ minutes
+ hours
+ days
+
-