0
0
Fork 0
mirror of https://github.com/healthchecks/healthchecks.git synced 2025-04-08 14:40:05 +00:00

Rearrange the log page to make room for more filters

* Switch from nouislider to simpler <input type="range">
* Move it to a sidebar

Also, fix a bug in _get_events where the "start" local variable
got clobbered, and made the date range for the Notification
query wrong.
This commit is contained in:
Pēteris Caune 2024-03-19 18:47:13 +02:00
parent 22325565b5
commit 47894f6add
No known key found for this signature in database
GPG key ID: E28D7679E9A9EDE2
8 changed files with 67 additions and 176 deletions

View file

@ -381,16 +381,10 @@ class SearchForm(forms.Form):
q = forms.RegexField(regex=r"^[0-9a-zA-Z\s]{3,100}$")
class SeekForm(forms.Form):
class LogFiltersForm(forms.Form):
# min_value is 2010-01-01, max_value is 2030-01-01
start = forms.FloatField(min_value=1262296800, max_value=1893448800, required=False)
end = forms.FloatField(min_value=1262296800, max_value=1893448800, required=False)
def clean_start(self) -> datetime | None:
if self.cleaned_data["start"]:
return datetime.fromtimestamp(self.cleaned_data["start"], tz=timezone.utc)
return None
def clean_end(self) -> datetime | None:
if self.cleaned_data["end"]:
return datetime.fromtimestamp(self.cleaned_data["end"], tz=timezone.utc)

View file

@ -187,27 +187,10 @@ class LogTestCase(BaseTestCase):
# older than the oldest visible ping:
self.assertNotContains(r, "Sent email to alice@example.org", status_code=200)
def test_it_accepts_start_query_parameter(self) -> None:
dt = datetime(2020, 1, 1, tzinfo=timezone.utc)
ts = str(dt.timestamp())
self.client.login(username="alice@example.org", password="password")
r = self.client.get(self.url + "?start=" + ts)
self.assertContains(r, f'data-start="{ts}"', status_code=200)
def test_it_accepts_end_query_parameter(self) -> None:
dt = datetime(2020, 1, 1, tzinfo=timezone.utc)
ts = str(dt.timestamp())
self.client.login(username="alice@example.org", password="password")
r = self.client.get(self.url + "?end=" + ts)
self.assertContains(r, f'data-end="{ts}"', status_code=200)
def test_it_ignores_bad_time_filter(self) -> None:
self.ping.refresh_from_db()
smin = str(self.ping.created.timestamp())
for sample in ["surprise", "0"]:
self.client.login(username="alice@example.org", password="password")
r = self.client.get(self.url + "?start=" + sample)
self.assertContains(r, f'data-start="{smin}"', status_code=200)
self.assertContains(r, f'value="{ts}"', status_code=200)

View file

@ -11,6 +11,7 @@ from collections import Counter, defaultdict
from collections.abc import Iterable
from datetime import datetime
from datetime import timedelta as td
from datetime import timezone
from email.message import EmailMessage
from secrets import token_urlsafe
from typing import Literal, TypedDict, cast
@ -874,10 +875,10 @@ def _get_events(
num_misses += 1
else:
ping.duration = None
start = starts[ping.rid]
if start is not None:
if ping.created - start < MAX_DURATION:
ping.duration = ping.created - start
matching_start = starts[ping.rid]
if matching_start is not None:
if ping.created - matching_start < MAX_DURATION:
ping.duration = ping.created - matching_start
starts[ping.rid] = None
@ -917,13 +918,11 @@ def log(request: AuthenticatedHttpRequest, code: UUID) -> HttpResponse:
smin = min(smin, oldest_ping.created)
# Align slider steps to full hours
smin = smin.replace(minute=0, second=0)
smin = smin.replace(minute=0, second=0, microsecond=0)
form = forms.SeekForm(request.GET)
form = forms.LogFiltersForm(request.GET)
start, end = smin, smax
if form.is_valid():
if form.cleaned_data["start"]:
start = form.cleaned_data["start"]
if form.cleaned_data["end"]:
end = form.cleaned_data["end"]
@ -2749,12 +2748,15 @@ def verify_signal_number(request: AuthenticatedHttpRequest) -> HttpResponse:
def log_events(request: AuthenticatedHttpRequest, code: UUID) -> HttpResponse:
check, rw = _get_check_for_user(request, code, preload_owner_profile=True)
form = forms.SeekForm(request.GET)
if not form.is_valid():
return HttpResponseBadRequest()
if form.cleaned_data["start"]:
start = form.cleaned_data["start"] + td(microseconds=1)
if "start" in request.GET:
try:
v = float(request.GET["start"])
except ValueError:
return HttpResponseBadRequest()
# min_value is 2010-01-01, max_value is 2030-01-01
if v < 1262296800 or v > 1893448800:
return HttpResponseBadRequest()
start = datetime.fromtimestamp(v, tz=timezone.utc) + td(microseconds=1)
else:
start = check.created

View file

@ -146,11 +146,18 @@ body {
}
.page-checks .container-fluid, .page-details .container-fluid {
.page-checks .container-fluid,
.page-details .container-fluid {
/* Fluid below 1320px, but max width capped to 1320px ... */
max-width: 1320px;
}
.page-log .container-fluid {
/* Fluid below 1520px, but max width capped to 1520px ... */
max-width: 1520px;
}
.status {
font-size: 24px;
display: inline-block;

View file

@ -95,15 +95,15 @@
text-overflow: ellipsis;
}
#log-slider {
margin-bottom: 60px;
#end-formatted {
float: right;
}
#log-slider .noUi-connect {
background-color: var(--link-color);
#end-help {
font-size: 12px;
line-height: 1;
}
#slider-from-formatted,
#slider-to-formatted {
font-weight: bold;
}
#filters-ping-types {
margin-left: 20px;
}

View file

@ -1,10 +1,5 @@
$(function () {
var BIG_LABEL = 1;
var SMALL_LABEL = 2;
var PIP = 0;
var NO_PIP = -1;
var slider = document.getElementById("log-slider");
var slider = document.getElementById("end");
// Look up the active tz switch to determine the initial display timezone:
var dateFormat = $(".active", "#format-switcher").data("format");
@ -15,97 +10,18 @@ $(function () {
}
function updateSliderPreview() {
var values = slider.noUiSlider.get();
$("#slider-from-formatted").text(fromUnix(values[0]).format("MMMM D, HH:mm"));
var toFormatted = "now";
if (values[1] < parseInt(slider.dataset.max)) {
toFormatted = fromUnix(values[1]).format("MMMM D, HH:mm");
if (slider.value < parseInt(slider.max)) {
toFormatted = fromUnix(slider.value).format("MMM D, HH:mm");
}
$("#slider-to-formatted").text(toFormatted);
$("#end-formatted").text(toFormatted);
}
function setupSlider() {
var smin = parseInt(slider.dataset.min);
var smax = parseInt(slider.dataset.max);
var pixelsPerSecond = slider.clientWidth / (smax - smin);
var pixelsPerHour = pixelsPerSecond * 3600;
var pixelsPerDay = pixelsPerHour * 24;
var dayGap = Math.round(0.5 + 80 / pixelsPerDay);
function filterPips(value, type) {
var m = fromUnix(value);
if (m.minute() != 0)
return NO_PIP;
// Date labels on every day
if (pixelsPerDay > 60 && m.hour() == 0)
return BIG_LABEL;
// Date labels every "dayGap" days
if (m.hour() == 0 && m.dayOfYear() % dayGap == 0)
return BIG_LABEL;
// Hour labels on every hour:
if (pixelsPerHour > 40)
return SMALL_LABEL;
// Hour labels every 3 hours:
if (pixelsPerHour > 15 && m.hour() % 3 == 0)
return SMALL_LABEL;
// Hour labels every 6 hours:
if (pixelsPerHour > 5 && m.hour() % 6 == 0)
return SMALL_LABEL;
// Pip on every hour
if (pixelsPerHour > 5)
return PIP;
// Pip on every day
if (pixelsPerDay > 10 && m.hour() == 0)
return PIP;
return NO_PIP;
}
function fmt(ts) {
var pipType = filterPips(ts);
return fromUnix(ts).format(pipType == 2 ? "HH:mm" : "MMM D");
}
if (slider.noUiSlider) {
slider.noUiSlider.destroy();
}
noUiSlider.create(slider, {
start: [parseInt(slider.dataset.start), parseInt(slider.dataset.end)],
range: {'min': smin, 'max': smax},
connect: true,
step: 3600,
pips: {
mode: "steps",
density: 3,
filter: filterPips,
format: {
to: fmt,
from: function() {}
}
}
});
slider.noUiSlider.on("slide", updateSliderPreview);
slider.noUiSlider.on("change", function(a, b, value) {
var start = Math.round(value[0]);
$("#seek-start").val(start).attr("disabled", start == smin);
var end = Math.round(value[1]);
$("#seek-end").val(end).attr("disabled", end == smax);
$("#seek-form").submit();
});
}
setupSlider();
updateSliderPreview();
$("#end").on("input", updateSliderPreview);
$("#end").on("change", function() {
$("#end").attr("disabled", slider.value == parseInt(slider.max));
$("#filters").submit();
});
$("#log").on("click", "tr.ok", function() {
var n = $("td", this).first().text();
@ -116,7 +32,6 @@ $(function () {
function switchDateFormat(format, rows) {
dateFormat = format;
slider.noUiSlider.updateOptions({}, true);
updateSliderPreview();
rows.forEach(function(row) {
@ -148,18 +63,6 @@ $(function () {
dataType: "json",
timeout: 2000,
success: function(data) {
// Has a minute has passed since last slider refresh?
if (data.max - slider.dataset.max > 60) {
// If the right handle was all the way to the right,
// make sure it is still all the way to the right after
// the update:
if (slider.dataset.end == slider.dataset.max) {
slider.dataset.end = data.max;
}
slider.dataset.max = data.max;
setupSlider();
}
if (data.events) {
var tbody = document.createElement("tbody");
tbody.setAttribute("class", "new");
@ -172,8 +75,9 @@ $(function () {
});
}
if (slider.dataset.end == slider.dataset.max) {
if (slider.value == slider.max) {
adaptiveSetInterval(fetchNewEvents, false);
}
});

View file

@ -72,7 +72,7 @@
{% endif %}
<nav class="navbar navbar-default">
<div class="container{% if page == "checks" or page == "details" %}-fluid{% endif %}">
<div class="container{% if page == "checks" or page == "details" or page == "log" %}-fluid{% endif %}">
<div class="navbar-header">
<button
type="button"
@ -206,13 +206,13 @@
</nav>
{% block containers %}
<div class="container{% if page == "checks" or page == "details" %}-fluid{% endif %}">
<div class="container{% if page == "checks" or page == "details" or page == "log" %}-fluid{% endif %}">
{% block content %}{% endblock %}
</div>
{% endblock %}
<footer class="footer">
<div class="container{% if page == "checks" or page == "details" %}-fluid{% endif %}">
<div class="container{% if page == "checks" or page == "details" or page == "log" %}-fluid{% endif %}">
<ul>
<li>
Healthchecks {% site_version %}

View file

@ -40,21 +40,6 @@
</div>
</li>
</ol>
<p>Filter events from <strong id="slider-from-formatted"></strong> to <strong id="slider-to-formatted"></strong>.</p>
<div
id="log-slider"
data-min="{{ min|timestamp }}"
data-max="{{ max|timestamp }}"
data-start="{{ start|timestamp }}"
data-end="{{ end|timestamp }}">
</div>
<form id="seek-form" method="get">
<input type="hidden" name="start" id="seek-start">
<input type="hidden" name="end" id="seek-end">
</form>
<div class="alert alert-info">
<span id="events-count">
{% if num_total > 1000 %}
@ -71,7 +56,8 @@
Updating live, new events will load automatically.
{% endif %}
</div>
</div>
<div class="col-sm-9">
<div class="table-responsive">
<table
class="table"
@ -85,7 +71,23 @@
</tbody>
</table>
</div>
</div>
<div class="col-sm-3">
<form id="filters" method="get">
<div>
<label>Show events older than:</label>
<span id="end-formatted">now</span>
</div>
<input
id="end"
type="range"
name="end"
min="{{ min|timestamp }}"
max="{{ max|timestamp }}"
value="{{ end|timestamp }}">
<p id="end-help" class="text-muted"><br>{{ min|date:"M d, Y"}}</p>
</form>
</div>
</div>
@ -104,7 +106,6 @@
{% block scripts %}
{% compress js %}
<script src="{% static 'js/nouislider.min.js' %}"></script>
<script src="{% static 'js/moment.min.js' %}"></script>
<script src="{% static 'js/moment-timezone-with-data-10-year-range.min.js' %}"></script>
<script src="{% static 'js/purify.min.js' %}"></script>