forked from GithubBackups/healthchecks
New feature: attaching tags to checks, and filtering checks list by their tags.
This commit is contained in:
parent
d51d7ed181
commit
35aed93b7f
@ -28,7 +28,7 @@ class ChecksAdmin(admin.ModelAdmin):
|
|||||||
}
|
}
|
||||||
|
|
||||||
search_fields = ["name", "user__email"]
|
search_fields = ["name", "user__email"]
|
||||||
list_display = ("id", "name", "created", "code", "status", "email",
|
list_display = ("id", "name_tags", "created", "code", "status", "email",
|
||||||
"last_ping")
|
"last_ping")
|
||||||
list_select_related = ("user", )
|
list_select_related = ("user", )
|
||||||
list_filter = ("status", OwnershipListFilter, "last_ping")
|
list_filter = ("status", OwnershipListFilter, "last_ping")
|
||||||
@ -38,6 +38,13 @@ class ChecksAdmin(admin.ModelAdmin):
|
|||||||
def email(self, obj):
|
def email(self, obj):
|
||||||
return obj.user.email if obj.user else None
|
return obj.user.email if obj.user else None
|
||||||
|
|
||||||
|
def name_tags(self, obj):
|
||||||
|
if not obj.tags:
|
||||||
|
return obj.name
|
||||||
|
|
||||||
|
return "%s [%s]" % (obj.name, obj.tags)
|
||||||
|
|
||||||
|
|
||||||
def send_alert(self, request, qs):
|
def send_alert(self, request, qs):
|
||||||
for check in qs:
|
for check in qs:
|
||||||
check.send_alert()
|
check.send_alert()
|
||||||
|
19
hc/api/migrations/0019_check_tags.py
Normal file
19
hc/api/migrations/0019_check_tags.py
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('api', '0018_remove_ping_body'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='check',
|
||||||
|
name='tags',
|
||||||
|
field=models.CharField(max_length=500, blank=True),
|
||||||
|
),
|
||||||
|
]
|
@ -42,6 +42,7 @@ class Check(models.Model):
|
|||||||
index_together = ["status", "user", "alert_after"]
|
index_together = ["status", "user", "alert_after"]
|
||||||
|
|
||||||
name = models.CharField(max_length=100, blank=True)
|
name = models.CharField(max_length=100, blank=True)
|
||||||
|
tags = models.CharField(max_length=500, blank=True)
|
||||||
code = models.UUIDField(default=uuid.uuid4, editable=False, db_index=True)
|
code = models.UUIDField(default=uuid.uuid4, editable=False, db_index=True)
|
||||||
user = models.ForeignKey(User, blank=True, null=True)
|
user = models.ForeignKey(User, blank=True, null=True)
|
||||||
created = models.DateTimeField(auto_now_add=True)
|
created = models.DateTimeField(auto_now_add=True)
|
||||||
@ -89,6 +90,9 @@ class Check(models.Model):
|
|||||||
channels = Channel.objects.filter(user=self.user)
|
channels = Channel.objects.filter(user=self.user)
|
||||||
self.channel_set.add(*channels)
|
self.channel_set.add(*channels)
|
||||||
|
|
||||||
|
def tags_list(self):
|
||||||
|
return self.tags.split(" ")
|
||||||
|
|
||||||
|
|
||||||
class Ping(models.Model):
|
class Ping(models.Model):
|
||||||
owner = models.ForeignKey(Check)
|
owner = models.ForeignKey(Check)
|
||||||
|
@ -2,6 +2,21 @@ from django import forms
|
|||||||
from hc.api.models import Channel
|
from hc.api.models import Channel
|
||||||
|
|
||||||
|
|
||||||
|
class NameTagsForm(forms.Form):
|
||||||
|
name = forms.CharField(max_length=100, required=False)
|
||||||
|
tags = forms.CharField(max_length=500, required=False)
|
||||||
|
|
||||||
|
def clean_tags(self):
|
||||||
|
l = []
|
||||||
|
|
||||||
|
for part in self.cleaned_data["tags"].split(" "):
|
||||||
|
part = part.strip()
|
||||||
|
if part != "":
|
||||||
|
l.append(part)
|
||||||
|
|
||||||
|
return " ".join(l)
|
||||||
|
|
||||||
|
|
||||||
class TimeoutForm(forms.Form):
|
class TimeoutForm(forms.Form):
|
||||||
timeout = forms.IntegerField(min_value=60, max_value=604800)
|
timeout = forms.IntegerField(min_value=60, max_value=604800)
|
||||||
grace = forms.IntegerField(min_value=60, max_value=604800)
|
grace = forms.IntegerField(min_value=60, max_value=604800)
|
||||||
|
@ -53,3 +53,13 @@ class UpdateNameTestCase(TestCase):
|
|||||||
self.client.login(username="alice", password="password")
|
self.client.login(username="alice", password="password")
|
||||||
r = self.client.post(url, data=payload)
|
r = self.client.post(url, data=payload)
|
||||||
assert r.status_code == 404
|
assert r.status_code == 404
|
||||||
|
|
||||||
|
def test_it_sanitizes_tags(self):
|
||||||
|
url = "/checks/%s/name/" % self.check.code
|
||||||
|
payload = {"tags": " foo bar\r\t \n baz \n"}
|
||||||
|
|
||||||
|
self.client.login(username="alice", password="password")
|
||||||
|
self.client.post(url, data=payload)
|
||||||
|
|
||||||
|
check = Check.objects.get(id=self.check.id)
|
||||||
|
self.assertEqual(check.tags, "foo bar baz")
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
from collections import Counter
|
||||||
from datetime import timedelta as td
|
from datetime import timedelta as td
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
@ -10,17 +11,35 @@ from django.utils.six.moves.urllib.parse import urlencode
|
|||||||
from django.utils.crypto import get_random_string
|
from django.utils.crypto import get_random_string
|
||||||
from hc.api.decorators import uuid_or_400
|
from hc.api.decorators import uuid_or_400
|
||||||
from hc.api.models import Channel, Check, Ping
|
from hc.api.models import Channel, Check, Ping
|
||||||
from hc.front.forms import AddChannelForm, TimeoutForm
|
from hc.front.forms import AddChannelForm, NameTagsForm, TimeoutForm
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
@login_required
|
||||||
def my_checks(request):
|
def my_checks(request):
|
||||||
checks = Check.objects.filter(user=request.user).order_by("created")
|
checks = Check.objects.filter(user=request.user).order_by("created")
|
||||||
|
|
||||||
|
counter = Counter()
|
||||||
|
down_tags, grace_tags = set(), set()
|
||||||
|
for check in checks:
|
||||||
|
status = check.get_status()
|
||||||
|
for tag in check.tags_list():
|
||||||
|
if tag == "":
|
||||||
|
continue
|
||||||
|
|
||||||
|
counter[tag] += 1
|
||||||
|
|
||||||
|
if status == "down":
|
||||||
|
down_tags.add(tag)
|
||||||
|
elif status == "grace":
|
||||||
|
grace_tags.add(tag)
|
||||||
|
|
||||||
ctx = {
|
ctx = {
|
||||||
"page": "checks",
|
"page": "checks",
|
||||||
"checks": checks,
|
"checks": checks,
|
||||||
"now": timezone.now()
|
"now": timezone.now(),
|
||||||
|
"tags": counter.most_common(),
|
||||||
|
"down_tags": down_tags,
|
||||||
|
"grace_tags": grace_tags
|
||||||
}
|
}
|
||||||
|
|
||||||
return render(request, "front/my_checks.html", ctx)
|
return render(request, "front/my_checks.html", ctx)
|
||||||
@ -89,11 +108,14 @@ def update_name(request, code):
|
|||||||
assert request.method == "POST"
|
assert request.method == "POST"
|
||||||
|
|
||||||
check = get_object_or_404(Check, code=code)
|
check = get_object_or_404(Check, code=code)
|
||||||
if check.user != request.user:
|
if check.user_id != request.user.id:
|
||||||
return HttpResponseForbidden()
|
return HttpResponseForbidden()
|
||||||
|
|
||||||
check.name = request.POST["name"]
|
form = NameTagsForm(request.POST)
|
||||||
check.save()
|
if form.is_valid():
|
||||||
|
check.name = form.cleaned_data["name"]
|
||||||
|
check.tags = form.cleaned_data["tags"]
|
||||||
|
check.save()
|
||||||
|
|
||||||
return redirect("hc-checks")
|
return redirect("hc-checks")
|
||||||
|
|
||||||
|
2
static/css/bootstrap.css
vendored
2
static/css/bootstrap.css
vendored
@ -1517,7 +1517,7 @@ code,
|
|||||||
kbd,
|
kbd,
|
||||||
pre,
|
pre,
|
||||||
samp {
|
samp {
|
||||||
font-family: Menlo, Monaco, Consolas, "Courier New", monospace;
|
font-family: "Lucida Console", Monaco, monospace;
|
||||||
}
|
}
|
||||||
code {
|
code {
|
||||||
padding: 2px 4px;
|
padding: 2px 4px;
|
||||||
|
@ -56,3 +56,8 @@
|
|||||||
.update-timeout-terms span {
|
.update-timeout-terms span {
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.label-tag {
|
||||||
|
background-color: #eee;
|
||||||
|
color: #555;
|
||||||
|
}
|
||||||
|
@ -2,6 +2,11 @@
|
|||||||
margin-top: 36px;
|
margin-top: 36px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#checks-table .checks-row:hover {
|
||||||
|
background-color: #f5f5f5;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
.my-checks-name.unnamed {
|
.my-checks-name.unnamed {
|
||||||
color: #999;
|
color: #999;
|
||||||
font-style: italic;
|
font-style: italic;
|
||||||
|
@ -79,6 +79,7 @@ $(function () {
|
|||||||
|
|
||||||
$("#update-name-form").attr("action", $this.data("url"));
|
$("#update-name-form").attr("action", $this.data("url"));
|
||||||
$("#update-name-input").val($this.data("name"));
|
$("#update-name-input").val($this.data("name"));
|
||||||
|
$("#update-tags-input").val($this.data("tags"));
|
||||||
$('#update-name-modal').modal("show");
|
$('#update-name-modal').modal("show");
|
||||||
$("#update-name-input").focus();
|
$("#update-name-input").focus();
|
||||||
|
|
||||||
@ -122,5 +123,45 @@ $(function () {
|
|||||||
$(".my-checks-email").show();
|
$(".my-checks-email").show();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
$("#my-checks-tags button").click(function() {
|
||||||
|
// .active has not been updated yet by bootstrap code,
|
||||||
|
// so cannot use it
|
||||||
|
$(this).toggleClass('checked');
|
||||||
|
|
||||||
|
// Make a list of currently checked tags:
|
||||||
|
var checked = [];
|
||||||
|
$("#my-checks-tags button.checked").each(function(index, el) {
|
||||||
|
checked.push(el.textContent);
|
||||||
|
});
|
||||||
|
|
||||||
|
// No checked tags: show all
|
||||||
|
if (checked.length == 0) {
|
||||||
|
$("#checks-table tr.checks-row").show();
|
||||||
|
$("#checks-list > li").show();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyFilters(index, element) {
|
||||||
|
var tags = $(".my-checks-name", element).data("tags").split(" ");
|
||||||
|
for (var i=0, tag; tag=checked[i]; i++) {
|
||||||
|
if (tags.indexOf(tag) == -1) {
|
||||||
|
$(element).hide();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$(element).show();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Desktop: for each row, see if it needs to be shown or hidden
|
||||||
|
$("#checks-table tr.checks-row").each(applyFilters);
|
||||||
|
// Mobile: for each list item, see if it needs to be shown or hidden
|
||||||
|
$("#checks-list > li").each(applyFilters);
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
});
|
});
|
@ -45,7 +45,8 @@
|
|||||||
@font-family-sans-serif: "Open Sans", Arial, sans-serif;
|
@font-family-sans-serif: "Open Sans", Arial, sans-serif;
|
||||||
@font-family-serif: Georgia, "Times New Roman", Times, serif;
|
@font-family-serif: Georgia, "Times New Roman", Times, serif;
|
||||||
//** Default monospace fonts for `<code>`, `<kbd>`, and `<pre>`.
|
//** Default monospace fonts for `<code>`, `<kbd>`, and `<pre>`.
|
||||||
@font-family-monospace: Menlo, Monaco, Consolas, "Courier New", monospace;
|
@font-family-monospace: "Lucida Console", Monaco, monospace;
|
||||||
|
|
||||||
@font-family-base: @font-family-sans-serif;
|
@font-family-base: @font-family-sans-serif;
|
||||||
|
|
||||||
@font-size-base: 14px;
|
@font-size-base: 14px;
|
||||||
|
@ -7,7 +7,27 @@
|
|||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-sm-12">
|
<div class="col-sm-12">
|
||||||
<h1>My Checks</h1>
|
<h1>My Checks</h1>
|
||||||
|
</div>
|
||||||
|
{% if tags %}
|
||||||
|
<div id="my-checks-tags" class="col-sm-12">
|
||||||
|
{% for tag, count in tags %}
|
||||||
|
{% if tag in down_tags %}
|
||||||
|
<button class="btn btn-danger btn-xs" data-toggle="button">{{ tag }}</button>
|
||||||
|
{% elif tag in grace_tags %}
|
||||||
|
<button class="btn btn-warning btn-xs" data-toggle="button">{{ tag }}</button>
|
||||||
|
{% else %}
|
||||||
|
<button class="btn btn-default btn-xs" data-toggle="button">{{ tag }}</button>
|
||||||
|
{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-sm-12">
|
||||||
|
|
||||||
|
|
||||||
{% if checks %}
|
{% if checks %}
|
||||||
{% include "front/my_checks_mobile.html" %}
|
{% include "front/my_checks_mobile.html" %}
|
||||||
{% include "front/my_checks_desktop.html" %}
|
{% include "front/my_checks_desktop.html" %}
|
||||||
@ -28,23 +48,53 @@
|
|||||||
|
|
||||||
<div id="update-name-modal" class="modal">
|
<div id="update-name-modal" class="modal">
|
||||||
<div class="modal-dialog">
|
<div class="modal-dialog">
|
||||||
<form id="update-name-form" method="post">
|
<form id="update-name-form" class="form-horizontal" method="post">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
<div class="modal-content">
|
<div class="modal-content">
|
||||||
<div class="modal-header">
|
<div class="modal-header">
|
||||||
<button type="button" class="close" data-dismiss="modal">×</span></button>
|
<button type="button" class="close" data-dismiss="modal">×</span></button>
|
||||||
<h4 class="update-timeout-title">Name</h4>
|
<h4 class="update-timeout-title">Name and Tags</h4>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-body">
|
<div class="modal-body">
|
||||||
<p>Give this check a human-friendly name, so you can
|
<div class="form-group">
|
||||||
easily recognize it later:</p>
|
<label for="update-name-input" class="col-sm-2 control-label">
|
||||||
<input
|
Name
|
||||||
id="update-name-input"
|
</label>
|
||||||
name="name"
|
<div class="col-sm-9">
|
||||||
type="text"
|
<input
|
||||||
value="---"
|
id="update-name-input"
|
||||||
placeholder="unnamed"
|
name="name"
|
||||||
class="input-name form-control" />
|
type="text"
|
||||||
|
value="---"
|
||||||
|
placeholder="unnamed"
|
||||||
|
class="input-name form-control" />
|
||||||
|
|
||||||
|
<span class="help-block">
|
||||||
|
Give this check a human-friendly name,
|
||||||
|
so you can easily recognize it later.
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="update-tags-input" class="col-sm-2 control-label">
|
||||||
|
Tags
|
||||||
|
</label>
|
||||||
|
<div class="col-sm-9">
|
||||||
|
<input
|
||||||
|
id="update-tags-input"
|
||||||
|
name="tags"
|
||||||
|
type="text"
|
||||||
|
value=""
|
||||||
|
placeholder="production www"
|
||||||
|
class="form-control" />
|
||||||
|
|
||||||
|
<span class="help-block">
|
||||||
|
Optionally, assign tags for easy filtering.
|
||||||
|
Separate multiple tags with spaces.
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-footer">
|
<div class="modal-footer">
|
||||||
<button type="button" class="btn btn-default" data-dismiss="modal">Cancel</button>
|
<button type="button" class="btn btn-default" data-dismiss="modal">Cancel</button>
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
{% load hc_extras humanize %}
|
{% load hc_extras humanize %}
|
||||||
|
|
||||||
<table id="checks-table" class="table table-hover hidden-xs">
|
<table id="checks-table" class="table hidden-xs">
|
||||||
<tr>
|
<tr>
|
||||||
<th></th>
|
<th></th>
|
||||||
<th class="th-name">Name</th>
|
<th class="th-name">Name</th>
|
||||||
@ -31,12 +31,15 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
<td class="name-cell">
|
<td class="name-cell">
|
||||||
<span
|
<div data-name="{{ check.name }}"
|
||||||
data-name="{{ check.name }}"
|
data-tags="{{ check.tags }}"
|
||||||
data-url="{% url 'hc-update-name' check.code %}"
|
data-url="{% url 'hc-update-name' check.code %}"
|
||||||
class="my-checks-name {% if not check.name %}unnamed{% endif %}">
|
class="my-checks-name {% if not check.name %}unnamed{% endif %}">
|
||||||
{{ check.name|default:"unnamed" }}
|
<div>{{ check.name|default:"unnamed" }}</div>
|
||||||
</span>
|
{% for tag in check.tags_list %}
|
||||||
|
<span class="label label-tag">{{ tag }}</span>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td class="url-cell">
|
<td class="url-cell">
|
||||||
<code class="my-checks-url">{{ check.url }}</code>
|
<code class="my-checks-url">{{ check.url }}</code>
|
||||||
|
@ -36,6 +36,16 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
{% if check.tags %}
|
||||||
|
<tr>
|
||||||
|
<th>Tags</th>
|
||||||
|
<td>
|
||||||
|
{% for tag in check.tags_list %}
|
||||||
|
<span class="label label-tag">{{ tag }}</span>
|
||||||
|
{% endfor %}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endif %}
|
||||||
<tr>
|
<tr>
|
||||||
<th>Period</th>
|
<th>Period</th>
|
||||||
<td>{{ check.timeout|hc_duration }}</td>
|
<td>{{ check.timeout|hc_duration }}</td>
|
||||||
@ -61,6 +71,7 @@
|
|||||||
<a
|
<a
|
||||||
href="#"
|
href="#"
|
||||||
data-name="{{ check.name }}"
|
data-name="{{ check.name }}"
|
||||||
|
data-tags="{{ check.tags }}"
|
||||||
data-url="{% url 'hc-update-name' check.code %}"
|
data-url="{% url 'hc-update-name' check.code %}"
|
||||||
class="btn btn-default my-checks-name">
|
class="btn btn-default my-checks-name">
|
||||||
Rename
|
Rename
|
||||||
|
Loading…
x
Reference in New Issue
Block a user