mirror of
https://github.com/healthchecks/healthchecks.git
synced 2025-04-11 15:51:19 +00:00
Improve period and grace controls, allow up to 365 day periods
Fixes: #281
This commit is contained in:
parent
a127ab0f0c
commit
e0d2f36928
9 changed files with 170 additions and 101 deletions
|
@ -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)
|
||||
|
|
|
@ -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"},
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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"])
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -302,16 +302,16 @@ Example:</p>
|
|||
<dt>timeout</dt>
|
||||
<dd>
|
||||
<p>number, optional, default value: {{ default_timeout }}.</p>
|
||||
<p>A number of seconds, the expected period of this check.</p>
|
||||
<p>Minimum: 60 (one minute), maximum: 2592000 (30 days).</p>
|
||||
<p>The expected period of this check in seconds.</p>
|
||||
<p>Minimum: 60 (one minute), maximum: 31536000 (365 days).</p>
|
||||
<p>Example for a 5-minute timeout:</p>
|
||||
<p><pre>{"kind": "simple", "timeout": 300}</pre></p>
|
||||
<p><pre>{"timeout": 300}</pre></p>
|
||||
</dd>
|
||||
<dt>grace</dt>
|
||||
<dd>
|
||||
<p>number, optional, default value: {{ default_grace }}.</p>
|
||||
<p>A number of seconds, the grace period for this check.</p>
|
||||
<p>Minimum: 60 (one minute), maximum: 2592000 (30 days).</p>
|
||||
<p>The grace period for this check in seconds.</p>
|
||||
<p>Minimum: 60 (one minute), maximum: 31536000 (365 days).</p>
|
||||
</dd>
|
||||
<dt>schedule</dt>
|
||||
<dd>
|
||||
|
@ -461,16 +461,16 @@ parameter, SITE_NAME will leave its value unchanged.</p>
|
|||
<dt>timeout</dt>
|
||||
<dd>
|
||||
<p>number, optional.</p>
|
||||
<p>A number of seconds, the expected period of this check.</p>
|
||||
<p>Minimum: 60 (one minute), maximum: 2592000 (30 days).</p>
|
||||
<p>The expected period of this check in seconds.</p>
|
||||
<p>Minimum: 60 (one minute), maximum: 31536000 (365 days).</p>
|
||||
<p>Example for a 5-minute timeout:</p>
|
||||
<p><pre>{"kind": "simple", "timeout": 300}</pre></p>
|
||||
<p><pre>{"timeout": 300}</pre></p>
|
||||
</dd>
|
||||
<dt>grace</dt>
|
||||
<dd>
|
||||
<p>number, optional.</p>
|
||||
<p>A number of seconds, the grace period for this check.</p>
|
||||
<p>Minimum: 60 (one minute), maximum: 2592000 (30 days).</p>
|
||||
<p>The grace period for this check in seconds.</p>
|
||||
<p>Minimum: 60 (one minute), maximum: 31536000 (365 days).</p>
|
||||
</dd>
|
||||
<dt>schedule</dt>
|
||||
<dd>
|
||||
|
|
|
@ -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:
|
||||
|
||||
<pre>{"kind": "simple", "timeout": 300}</pre>
|
||||
<pre>{"timeout": 300}</pre>
|
||||
|
||||
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:
|
||||
|
||||
<pre>{"kind": "simple", "timeout": 300}</pre>
|
||||
<pre>{"timeout": 300}</pre>
|
||||
|
||||
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.
|
||||
|
|
|
@ -8,30 +8,34 @@
|
|||
<input type="hidden" name="grace" id="update-timeout-grace" />
|
||||
|
||||
<div class="modal-body">
|
||||
<div class="update-timeout-info text-center">
|
||||
<span class="update-timeout-label">
|
||||
Period
|
||||
</span>
|
||||
<span
|
||||
id="period-slider-value"
|
||||
class="update-timeout-value">
|
||||
1 day
|
||||
</span>
|
||||
<div class="interval-controls">
|
||||
<label>Period</label>
|
||||
<input
|
||||
id="period-value"
|
||||
type="number"
|
||||
min="1"
|
||||
class="period-input form-control input-lg" />
|
||||
<select id="period-unit" class="period-input form-control input-lg">
|
||||
<option value="60">minutes</option>
|
||||
<option value="3600">hours</option>
|
||||
<option value="86400">days</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div id="period-slider"></div>
|
||||
|
||||
<div class="update-timeout-info text-center">
|
||||
<span class="update-timeout-label">
|
||||
Grace Time
|
||||
</span>
|
||||
<span
|
||||
id="grace-slider-value"
|
||||
class="update-timeout-value">
|
||||
1 day
|
||||
</span>
|
||||
<div class="interval-controls">
|
||||
<label>Grace Time</label>
|
||||
<input
|
||||
id="grace-value"
|
||||
type="number"
|
||||
min="1"
|
||||
class="grace-input form-control input-lg" />
|
||||
<select id="grace-unit" class="grace-input form-control input-lg">
|
||||
<option value="60">minutes</option>
|
||||
<option value="3600">hours</option>
|
||||
<option value="86400">days</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div id="grace-slider"></div>
|
||||
|
||||
<div class="update-timeout-terms">
|
||||
|
|
Loading…
Add table
Reference in a new issue