mirror of
https://github.com/healthchecks/healthchecks.git
synced 2025-04-06 21:58:48 +00:00
Redesign the "Status Badges" page
This commit is contained in:
parent
6686147cb1
commit
4959856e58
9 changed files with 225 additions and 216 deletions
|
@ -7,6 +7,7 @@ All notable changes to this project will be documented in this file.
|
||||||
- Add support for $NAME_JSON and $BODY_JSON placeholders in webhook payloads
|
- Add support for $NAME_JSON and $BODY_JSON placeholders in webhook payloads
|
||||||
- Update the WhatsApp integration to use Twilio Content Templates
|
- Update the WhatsApp integration to use Twilio Content Templates
|
||||||
- Add auto-refresh functionality to the Log page (#957, @mickBoat00)
|
- Add auto-refresh functionality to the Log page (#957, @mickBoat00)
|
||||||
|
- Redesign the "Status Badges" page
|
||||||
|
|
||||||
### Bug Fixes
|
### Bug Fixes
|
||||||
- Fix Gotify integration to handle Gotify server URLs with paths (#964)
|
- Fix Gotify integration to handle Gotify server URLs with paths (#964)
|
||||||
|
|
|
@ -27,6 +27,10 @@ def _is_latin1(s: str) -> bool:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def _choices(csv: str) -> list[tuple[str, str]]:
|
||||||
|
return [(v, v) for v in csv.split(",")]
|
||||||
|
|
||||||
|
|
||||||
class LaxURLField(forms.URLField):
|
class LaxURLField(forms.URLField):
|
||||||
default_validators = [WebhookValidator()]
|
default_validators = [WebhookValidator()]
|
||||||
|
|
||||||
|
@ -85,9 +89,7 @@ class NameTagsForm(forms.Form):
|
||||||
|
|
||||||
|
|
||||||
class AddCheckForm(NameTagsForm):
|
class AddCheckForm(NameTagsForm):
|
||||||
kind = forms.ChoiceField(
|
kind = forms.ChoiceField(choices=_choices("simple,cron,oncalendar"))
|
||||||
choices=(("simple", "simple"), ("cron", "cron"), ("oncalendar", "oncalendar"))
|
|
||||||
)
|
|
||||||
timeout = forms.IntegerField(min_value=60, max_value=31536000)
|
timeout = forms.IntegerField(min_value=60, max_value=31536000)
|
||||||
schedule = forms.CharField(required=False, max_length=100)
|
schedule = forms.CharField(required=False, max_length=100)
|
||||||
tz = forms.CharField(max_length=36, validators=[TimezoneValidator()])
|
tz = forms.CharField(max_length=36, validators=[TimezoneValidator()])
|
||||||
|
@ -198,19 +200,16 @@ class AddUrlForm(forms.Form):
|
||||||
value = LaxURLField(max_length=1000)
|
value = LaxURLField(max_length=1000)
|
||||||
|
|
||||||
|
|
||||||
METHODS = ("GET", "POST", "PUT")
|
|
||||||
|
|
||||||
|
|
||||||
class WebhookForm(forms.Form):
|
class WebhookForm(forms.Form):
|
||||||
error_css_class = "has-error"
|
error_css_class = "has-error"
|
||||||
name = forms.CharField(max_length=100, required=False)
|
name = forms.CharField(max_length=100, required=False)
|
||||||
|
|
||||||
method_down = forms.ChoiceField(initial="GET", choices=zip(METHODS, METHODS))
|
method_down = forms.ChoiceField(initial="GET", choices=_choices("GET,POST,PUT"))
|
||||||
body_down = forms.CharField(max_length=1000, required=False)
|
body_down = forms.CharField(max_length=1000, required=False)
|
||||||
headers_down = HeadersField(required=False)
|
headers_down = HeadersField(required=False)
|
||||||
url_down = LaxURLField(max_length=1000, required=False)
|
url_down = LaxURLField(max_length=1000, required=False)
|
||||||
|
|
||||||
method_up = forms.ChoiceField(initial="GET", choices=zip(METHODS, METHODS))
|
method_up = forms.ChoiceField(initial="GET", choices=_choices("GET,POST,PUT"))
|
||||||
body_up = forms.CharField(max_length=1000, required=False)
|
body_up = forms.CharField(max_length=1000, required=False)
|
||||||
headers_up = HeadersField(required=False)
|
headers_up = HeadersField(required=False)
|
||||||
url_up = LaxURLField(max_length=1000, required=False)
|
url_up = LaxURLField(max_length=1000, required=False)
|
||||||
|
@ -404,3 +403,10 @@ class TransferForm(forms.Form):
|
||||||
|
|
||||||
class AddTelegramForm(forms.Form):
|
class AddTelegramForm(forms.Form):
|
||||||
project = forms.UUIDField()
|
project = forms.UUIDField()
|
||||||
|
|
||||||
|
|
||||||
|
class BadgeSettingsForm(forms.Form):
|
||||||
|
target = forms.ChoiceField(choices=_choices("all,tag"))
|
||||||
|
tag = forms.CharField(max_length=100, required=False)
|
||||||
|
fmt = forms.ChoiceField(choices=_choices("svg,json,shields"))
|
||||||
|
states = forms.ChoiceField(choices=_choices("2,3"))
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from django.test.utils import override_settings
|
||||||
|
|
||||||
from hc.api.models import Check
|
from hc.api.models import Check
|
||||||
from hc.test import BaseTestCase
|
from hc.test import BaseTestCase
|
||||||
|
|
||||||
|
@ -8,38 +10,71 @@ class BadgesTestCase(BaseTestCase):
|
||||||
def setUp(self) -> None:
|
def setUp(self) -> None:
|
||||||
super().setUp()
|
super().setUp()
|
||||||
|
|
||||||
self.url = f"/projects/{self.project.code}/badges/"
|
|
||||||
|
|
||||||
def test_it_shows_badges(self) -> None:
|
|
||||||
Check.objects.create(project=self.project, tags="foo a-B_1 baz@")
|
|
||||||
Check.objects.create(project=self.bobs_project, tags="bobs-tag")
|
|
||||||
|
|
||||||
self.client.login(username="alice@example.org", password="password")
|
|
||||||
r = self.client.get(self.url)
|
|
||||||
self.assertContains(r, "foo.svg")
|
|
||||||
self.assertContains(r, "a-B_1.svg")
|
|
||||||
|
|
||||||
# Expect badge URLs only for tags that match \w+
|
|
||||||
self.assertNotContains(r, "baz@.svg")
|
|
||||||
|
|
||||||
# Expect only Alice's tags
|
|
||||||
self.assertNotContains(r, "bobs-tag.svg")
|
|
||||||
|
|
||||||
def test_it_uses_badge_key(self) -> None:
|
|
||||||
Check.objects.create(project=self.project, tags="foo bar")
|
|
||||||
Check.objects.create(project=self.bobs_project, tags="bobs-tag")
|
|
||||||
|
|
||||||
self.project.badge_key = "alices-badge-key"
|
self.project.badge_key = "alices-badge-key"
|
||||||
self.project.save()
|
self.project.save()
|
||||||
|
|
||||||
|
Check.objects.create(project=self.project, tags="foo a-B_1 baz@")
|
||||||
|
|
||||||
|
self.url = f"/projects/{self.project.code}/badges/"
|
||||||
|
|
||||||
|
def test_it_shows_form(self) -> None:
|
||||||
self.client.login(username="alice@example.org", password="password")
|
self.client.login(username="alice@example.org", password="password")
|
||||||
r = self.client.get(self.url)
|
r = self.client.get(self.url)
|
||||||
|
self.assertContains(r, "foo")
|
||||||
|
self.assertContains(r, "a-B_1")
|
||||||
|
self.assertContains(r, self.project.badge_key)
|
||||||
|
|
||||||
|
def test_it_checks_ownership(self) -> None:
|
||||||
|
self.client.login(username="charlie@example.org", password="password")
|
||||||
|
r = self.client.get(self.url)
|
||||||
|
self.assertEqual(r.status_code, 404)
|
||||||
|
|
||||||
|
def test_team_access_works(self) -> None:
|
||||||
|
# Logging in as bob, not alice. Bob has team access so this
|
||||||
|
# should work.
|
||||||
|
self.client.login(username="bob@example.org", password="password")
|
||||||
|
r = self.client.get(self.url)
|
||||||
|
self.assertEqual(r.status_code, 200)
|
||||||
|
|
||||||
|
@override_settings(MASTER_BADGE_LABEL="Overall Status")
|
||||||
|
def test_it_previews_master_svg(self) -> None:
|
||||||
|
self.client.login(username="alice@example.org", password="password")
|
||||||
|
payload = {"target": "all", "fmt": "svg", "states": "2"}
|
||||||
|
r = self.client.post(self.url, payload)
|
||||||
|
|
||||||
|
self.assertContains(r, "![Overall Status]")
|
||||||
|
|
||||||
|
def test_it_previews_svg(self) -> None:
|
||||||
|
self.client.login(username="alice@example.org", password="password")
|
||||||
|
payload = {"target": "tag", "tag": "foo", "fmt": "svg", "states": "2"}
|
||||||
|
r = self.client.post(self.url, payload)
|
||||||
|
|
||||||
self.assertContains(r, "badge/alices-badge-key/")
|
self.assertContains(r, "badge/alices-badge-key/")
|
||||||
self.assertContains(r, "badge/alices-badge-key/")
|
self.assertContains(r, "foo.svg")
|
||||||
|
self.assertContains(r, "![foo]")
|
||||||
|
|
||||||
def test_it_handles_special_characters_in_tags(self) -> None:
|
def test_it_handles_special_characters_in_tags(self) -> None:
|
||||||
Check.objects.create(project=self.project, tags="db@dc1")
|
|
||||||
|
|
||||||
self.client.login(username="alice@example.org", password="password")
|
self.client.login(username="alice@example.org", password="password")
|
||||||
r = self.client.get(self.url)
|
payload = {"target": "tag", "tag": "db@dc1", "fmt": "svg", "states": "2"}
|
||||||
|
r = self.client.post(self.url, payload)
|
||||||
self.assertContains(r, "db%2540dc1.svg")
|
self.assertContains(r, "db%2540dc1.svg")
|
||||||
|
self.assertContains(r, "![db@dc1]")
|
||||||
|
|
||||||
|
def test_it_previews_json(self) -> None:
|
||||||
|
self.client.login(username="alice@example.org", password="password")
|
||||||
|
payload = {"target": "tag", "tag": "foo", "fmt": "json", "states": "2"}
|
||||||
|
r = self.client.post(self.url, payload)
|
||||||
|
|
||||||
|
self.assertContains(r, "fetch-json")
|
||||||
|
self.assertContains(r, "foo.json")
|
||||||
|
self.assertNotContains(r, "![foo]")
|
||||||
|
|
||||||
|
def test_it_previews_shields(self) -> None:
|
||||||
|
self.client.login(username="alice@example.org", password="password")
|
||||||
|
payload = {"target": "tag", "tag": "foo", "fmt": "shields", "states": "2"}
|
||||||
|
r = self.client.post(self.url, payload)
|
||||||
|
|
||||||
|
self.assertContains(r, "https://img.shields.io/endpoint")
|
||||||
|
self.assertContains(r, "%3A%2F%2F") # url-encoded "://"
|
||||||
|
self.assertContains(r, "foo.shields")
|
||||||
|
self.assertContains(r, "![foo]")
|
||||||
|
|
|
@ -11,7 +11,6 @@ from collections import Counter, defaultdict
|
||||||
from collections.abc import Iterable
|
from collections.abc import Iterable
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from datetime import timedelta as td
|
from datetime import timedelta as td
|
||||||
from datetime import timezone
|
|
||||||
from email.message import EmailMessage
|
from email.message import EmailMessage
|
||||||
from secrets import token_urlsafe
|
from secrets import token_urlsafe
|
||||||
from typing import Literal, TypedDict, cast
|
from typing import Literal, TypedDict, cast
|
||||||
|
@ -1097,35 +1096,38 @@ def status_single(request: AuthenticatedHttpRequest, code: UUID) -> HttpResponse
|
||||||
def badges(request: AuthenticatedHttpRequest, code: UUID) -> HttpResponse:
|
def badges(request: AuthenticatedHttpRequest, code: UUID) -> HttpResponse:
|
||||||
project, rw = _get_project_for_user(request, code)
|
project, rw = _get_project_for_user(request, code)
|
||||||
|
|
||||||
|
if request.method == "POST":
|
||||||
|
form = forms.BadgeSettingsForm(request.POST)
|
||||||
|
if not form.is_valid():
|
||||||
|
return HttpResponseBadRequest()
|
||||||
|
|
||||||
|
if form.cleaned_data["target"] == "all":
|
||||||
|
label = settings.MASTER_BADGE_LABEL
|
||||||
|
tag = "*"
|
||||||
|
else:
|
||||||
|
label = form.cleaned_data["tag"]
|
||||||
|
tag = form.cleaned_data["tag"]
|
||||||
|
fmt = form.cleaned_data["fmt"]
|
||||||
|
with_late = True if form.cleaned_data["states"] == "3" else False
|
||||||
|
url = get_badge_url(project.badge_key, tag, fmt, with_late)
|
||||||
|
if fmt == "shields":
|
||||||
|
url = "https://img.shields.io/endpoint?" + urlencode({"url": url})
|
||||||
|
|
||||||
|
ctx = {"fmt": fmt, "label": label, "url": url}
|
||||||
|
return render(request, "front/badges_preview.html", ctx)
|
||||||
|
|
||||||
tags = set()
|
tags = set()
|
||||||
for check in Check.objects.filter(project=project):
|
for check in Check.objects.filter(project=project):
|
||||||
tags.update(check.tags_list())
|
tags.update(check.tags_list())
|
||||||
|
|
||||||
sorted_tags = sorted(tags, key=lambda s: s.lower())
|
sorted_tags = sorted(tags, key=lambda s: s.lower())
|
||||||
sorted_tags.append("*") # For the "overall status" badge
|
|
||||||
|
|
||||||
key = project.badge_key
|
|
||||||
urls = []
|
|
||||||
for tag in sorted_tags:
|
|
||||||
urls.append(
|
|
||||||
{
|
|
||||||
"tag": tag,
|
|
||||||
"svg": get_badge_url(key, tag),
|
|
||||||
"svg3": get_badge_url(key, tag, with_late=True),
|
|
||||||
"json": get_badge_url(key, tag, fmt="json"),
|
|
||||||
"json3": get_badge_url(key, tag, fmt="json", with_late=True),
|
|
||||||
"shields": get_badge_url(key, tag, fmt="shields"),
|
|
||||||
"shields3": get_badge_url(key, tag, fmt="shields", with_late=True),
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
ctx = {
|
ctx = {
|
||||||
"have_tags": len(urls) > 1,
|
"tags": sorted_tags,
|
||||||
"page": "badges",
|
"fmt": "svg",
|
||||||
"project": project,
|
"label": settings.MASTER_BADGE_LABEL,
|
||||||
"badges": urls,
|
"url": get_badge_url(project.badge_key, "*"),
|
||||||
}
|
}
|
||||||
|
|
||||||
return render(request, "front/badges.html", ctx)
|
return render(request, "front/badges.html", ctx)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -99,21 +99,21 @@ def get_badge_svg(tag: str, status: str) -> str:
|
||||||
return render_to_string("badge.svg", ctx)
|
return render_to_string("badge.svg", ctx)
|
||||||
|
|
||||||
|
|
||||||
def check_signature(username: str, tag: str, sig: str) -> bool:
|
def check_signature(badge_key: str, tag: str, sig: str) -> bool:
|
||||||
ours = base64_hmac(str(username), tag, settings.SECRET_KEY)
|
ours = base64_hmac(str(badge_key), tag, settings.SECRET_KEY)
|
||||||
return ours[:8] == sig[:8]
|
return ours[:8] == sig[:8]
|
||||||
|
|
||||||
|
|
||||||
def get_badge_url(
|
def get_badge_url(
|
||||||
username: str, tag: str, fmt: str = "svg", with_late: bool = False
|
badge_key: str, tag: str, fmt: str = "svg", with_late: bool = False
|
||||||
) -> str:
|
) -> str:
|
||||||
sig = base64_hmac(str(username), tag, settings.SECRET_KEY)[:8]
|
sig = base64_hmac(str(badge_key), tag, settings.SECRET_KEY)[:8]
|
||||||
if not with_late:
|
if not with_late:
|
||||||
sig += "-2"
|
sig += "-2"
|
||||||
|
|
||||||
if tag == "*":
|
if tag == "*":
|
||||||
url = reverse("hc-badge-all", args=[username, sig, fmt])
|
url = reverse("hc-badge-all", args=[badge_key, sig, fmt])
|
||||||
else:
|
else:
|
||||||
url = reverse("hc-badge", args=[username, sig, tag, fmt])
|
url = reverse("hc-badge", args=[badge_key, sig, tag, fmt])
|
||||||
|
|
||||||
return settings.SITE_ROOT + url
|
return settings.SITE_ROOT + url
|
||||||
|
|
|
@ -1,16 +1,10 @@
|
||||||
.table.badge-preview th {
|
#status-badges #tag {
|
||||||
border-top: 0;
|
margin-left: 49px;
|
||||||
color: var(--text-muted);
|
width: 200px
|
||||||
font-weight: normal;
|
|
||||||
font-size: 12px;
|
|
||||||
padding-top: 32px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#badges-json .fetch-json {
|
#status-badges #preview img,
|
||||||
background: var(--pre-bg);
|
#status-badges #preview pre,
|
||||||
padding: 3px;
|
#status-badges #preview input {
|
||||||
}
|
margin-bottom: 20px;
|
||||||
|
|
||||||
#badges-json, #badges-shields, .badge-preview .with-late {
|
|
||||||
display: none;
|
|
||||||
}
|
}
|
|
@ -1,37 +1,23 @@
|
||||||
$(function() {
|
$(function() {
|
||||||
|
function updatePreview() {
|
||||||
$(".fetch-json").each(function(idx, el) {
|
var params = $("#badge-settings-form").serialize();
|
||||||
$.getJSON(el.dataset.url, function(data) {
|
var token = $('input[name=csrfmiddlewaretoken]').val();
|
||||||
el.innerText = JSON.stringify(data);
|
$.ajax({
|
||||||
|
url: window.location.href,
|
||||||
|
type: "post",
|
||||||
|
headers: {"X-CSRFToken": token},
|
||||||
|
data: params,
|
||||||
|
success: function(data) {
|
||||||
|
document.getElementById("preview").innerHTML = data;
|
||||||
|
$(".fetch-json").each(function(idx, el) {
|
||||||
|
$.getJSON(el.dataset.url, function(data) {
|
||||||
|
el.innerText = JSON.stringify(data);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
});
|
});
|
||||||
});
|
}
|
||||||
|
|
||||||
$("#show-svg").click(function() {
|
|
||||||
$("#badges-svg").show();
|
|
||||||
$("#badges-json").hide();
|
|
||||||
$("#badges-shields").hide();
|
|
||||||
})
|
|
||||||
|
|
||||||
$("#show-json").click(function() {
|
|
||||||
$("#badges-svg").hide();
|
|
||||||
$("#badges-json").show();
|
|
||||||
$("#badges-shields").hide();
|
|
||||||
})
|
|
||||||
|
|
||||||
$("#show-shields").click(function() {
|
|
||||||
$("#badges-svg").hide();
|
|
||||||
$("#badges-json").hide();
|
|
||||||
$("#badges-shields").show();
|
|
||||||
})
|
|
||||||
|
|
||||||
$("#show-with-late").click(function() {
|
|
||||||
$(".no-late").hide();
|
|
||||||
$(".with-late").show();
|
|
||||||
})
|
|
||||||
|
|
||||||
$("#show-no-late").click(function() {
|
|
||||||
$(".with-late").hide();
|
|
||||||
$(".no-late").show();
|
|
||||||
})
|
|
||||||
|
|
||||||
|
$("input[type=radio]").change(updatePreview);
|
||||||
|
$("select").change(updatePreview);
|
||||||
});
|
});
|
||||||
|
|
|
@ -4,132 +4,86 @@
|
||||||
{% block title %}Status Badges - {{ site_name }}{% endblock %}
|
{% block title %}Status Badges - {{ site_name }}{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="row">
|
<div id="status-badges" class="row">
|
||||||
<div class="col-sm-10">
|
<div class="col-sm-12">
|
||||||
<h1>Status Badges</h1>
|
<h1>Status Badges</h1>
|
||||||
|
<p>
|
||||||
<p id="badges-description">
|
|
||||||
{{ site_name }} provides status badges for each of the tags
|
{{ site_name }} provides status badges for each of the tags
|
||||||
you have used. The badges have public, but hard-to-guess
|
you have used. The badges have public, but hard-to-guess
|
||||||
URLs. You can use them in your READMEs,
|
URLs. You can use them in your READMEs,
|
||||||
dashboards or status pages.
|
dashboards or status pages.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p>Each badge can be in one of the following states:</p>
|
<p>Each badge can be in one of the following states:</p>
|
||||||
<ul>
|
<ul>
|
||||||
<li><strong>up</strong> – all matching checks are up.</li>
|
<li><strong>up</strong> – all matching checks are up.</li>
|
||||||
<li><strong>down</strong> – at least one check is currently down.</li>
|
<li><strong>down</strong> – at least one check is currently down.</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
As an option, the badges can report a third state:
|
As an option, the badges can report a third state:
|
||||||
<strong>late</strong> (when at least one check is running late but has not
|
<strong>late</strong> (when at least one check is running late but has not
|
||||||
exceeded its grace time yet).
|
exceeded its grace time yet).
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<br />
|
|
||||||
|
|
||||||
<div class="btn-group" data-toggle="buttons">
|
|
||||||
<label id="show-svg" class="btn btn-default active">
|
|
||||||
<input type="radio" checked> SVG
|
|
||||||
</label>
|
|
||||||
<label id="show-json" class="btn btn-default">
|
|
||||||
<input type="radio"> JSON
|
|
||||||
</label>
|
|
||||||
<label id="show-shields" class="btn btn-default">
|
|
||||||
<input type="radio"> Shields.io
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
|
||||||
<div class="btn-group" data-toggle="buttons">
|
|
||||||
<label id="show-no-late" class="btn btn-default active">
|
|
||||||
<input type="radio" checked> Badge states: <b>up</b> or <b>down</b>
|
|
||||||
</label>
|
|
||||||
<label id="show-with-late" class="btn btn-default">
|
|
||||||
<input type="radio"> Badge states: <b>up</b>, <b>late</b> or <b>down</b>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<table id="badges-svg" class="table badge-preview">
|
|
||||||
{% if have_tags %}
|
|
||||||
<tr><th colspan="2">Tags</th></tr>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
{% for urldict in badges %}
|
|
||||||
{% if urldict.tag == "*" %}
|
|
||||||
<tr>
|
|
||||||
<th colspan="2">Overall Status</th>
|
|
||||||
</tr>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
<tr>
|
|
||||||
<td>
|
|
||||||
<img class="no-late" src="{{ urldict.svg }}" alt="" />
|
|
||||||
<img class="with-late" src="{{ urldict.svg3 }}" alt="" />
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<code class="no-late">{{ urldict.svg }}</code>
|
|
||||||
<code class="with-late">{{ urldict.svg3 }}</code>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
{% endfor %}
|
|
||||||
</table>
|
|
||||||
<table id="badges-json" class="table badge-preview">
|
|
||||||
{% if have_tags %}
|
|
||||||
<tr>
|
|
||||||
<th colspan="2">Tags</th>
|
|
||||||
</tr>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
{% for urldict in badges %}
|
|
||||||
{% if urldict.tag == "*" %}
|
|
||||||
<tr>
|
|
||||||
<th colspan="2">Overall Status</th>
|
|
||||||
</tr>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
<tr>
|
|
||||||
<td>
|
|
||||||
<code class="fetch-json no-late" data-url="{{ urldict.json }}"></code>
|
|
||||||
<code class="fetch-json with-late" data-url="{{ urldict.json3 }}"></code>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<code class="no-late">{{ urldict.json }}</code>
|
|
||||||
<code class="with-late">{{ urldict.json3 }}</code>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
{% endfor %}
|
|
||||||
</table>
|
|
||||||
|
|
||||||
<table id="badges-shields" class="table badge-preview">
|
|
||||||
{% if have_tags %}
|
|
||||||
<tr>
|
|
||||||
<th>Shields.io badge</th>
|
|
||||||
<th>JSON endpoint for Shields.io <a href="https://shields.io/endpoint">(how to use)</a></th>
|
|
||||||
</tr>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
{% for urldict in badges %}
|
|
||||||
{% if urldict.tag == "*" %}
|
|
||||||
<tr>
|
|
||||||
<th colspan="2">Overall Status</th>
|
|
||||||
</tr>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
<tr>
|
|
||||||
<td>
|
|
||||||
<img class="no-late" src="https://img.shields.io/endpoint?url={{ urldict.shields|urlencode:"" }}" alt="" />
|
|
||||||
<img class="with-late" src="https://img.shields.io/endpoint?url={{ urldict.shields3|urlencode:"" }}" alt="" />
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<code class="no-late">{{ urldict.shields }}</code>
|
|
||||||
<code class="with-late">{{ urldict.shields3 }}</code>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
{% endfor %}
|
|
||||||
</table>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-4">
|
||||||
|
<h2>Badge Generator</h2>
|
||||||
|
{% csrf_token %}
|
||||||
|
<form id="badge-settings-form">
|
||||||
|
<div class="form-group">
|
||||||
|
<p>Generate a badge for</p>
|
||||||
|
|
||||||
|
<label class="radio-container">
|
||||||
|
<input type="radio" name="target" value="all" autocomplete="off" checked>
|
||||||
|
<span class="radiomark"></span>
|
||||||
|
All checks in the project
|
||||||
|
</label>
|
||||||
|
{% if tags %}
|
||||||
|
<label class="radio-container">
|
||||||
|
<input type="radio" name="target" value="tag" autocomplete="off">
|
||||||
|
<span class="radiomark"></span>
|
||||||
|
The checks tagged with this tag:
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<select
|
||||||
|
id="tag"
|
||||||
|
name="tag"
|
||||||
|
class="form-control"
|
||||||
|
autocomplete="off">
|
||||||
|
{% for tag in tags %}
|
||||||
|
<option>{{ tag }}</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<p>Badge format</p>
|
||||||
|
<label class="radio-container">
|
||||||
|
<input type="radio" name="fmt" value="svg" autocomplete="off" checked>
|
||||||
|
<span class="radiomark"></span> SVG
|
||||||
|
</label>
|
||||||
|
<label class="radio-container">
|
||||||
|
<input type="radio" name="fmt" value="json" autocomplete="off">
|
||||||
|
<span class="radiomark"></span> JSON
|
||||||
|
</label>
|
||||||
|
<label class="radio-container">
|
||||||
|
<input type="radio" name="fmt" value="shields" autocomplete="off">
|
||||||
|
<span class="radiomark"></span> Shields.io
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<p>Badge states</p>
|
||||||
|
<label class="radio-container">
|
||||||
|
<input type="radio" name="states" value="2" autocomplete="off" checked>
|
||||||
|
<span class="radiomark"></span> Two states (<strong>up</strong>, <strong>down</strong>)
|
||||||
|
</label>
|
||||||
|
<label class="radio-container">
|
||||||
|
<input type="radio" name="states" value="3" autocomplete="off">
|
||||||
|
<span class="radiomark"></span> Three states (<strong>up</strong>, <strong>late</strong> or <strong>down</strong>)
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<div id="preview" class="col-md-8">{% include "front/badges_preview.html" %}</div>
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
|
|
31
templates/front/badges_preview.html
Normal file
31
templates/front/badges_preview.html
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
<h2>Preview</h2>
|
||||||
|
|
||||||
|
{% if fmt == "json" %}
|
||||||
|
<pre class="fetch-json" data-url="{{ url }}"></pre>
|
||||||
|
|
||||||
|
<p>Badge URL</p>
|
||||||
|
<input
|
||||||
|
class="form-control"
|
||||||
|
type="text" name=""
|
||||||
|
value="{{ url }}" readonly>
|
||||||
|
|
||||||
|
{% else %}
|
||||||
|
<img src="{{ url }}">
|
||||||
|
<p>Badge URL</p>
|
||||||
|
<input
|
||||||
|
class="form-control"
|
||||||
|
type="text" name=""
|
||||||
|
value="{{ url }}" readonly>
|
||||||
|
<p>HTML code</p>
|
||||||
|
<input
|
||||||
|
class="form-control"
|
||||||
|
type="text"
|
||||||
|
name=""
|
||||||
|
value="<a href="{{ url }}">{{ label }}</a>" readonly>
|
||||||
|
<p>Markdown code</p>
|
||||||
|
<input
|
||||||
|
class="form-control"
|
||||||
|
type="text"
|
||||||
|
name=""
|
||||||
|
value="" readonly>
|
||||||
|
{% endif %}
|
Loading…
Add table
Reference in a new issue