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 @@