mirror of
https://github.com/healthchecks/healthchecks.git
synced 2025-04-07 06:05:34 +00:00
Decouple check's name from slug, allow users to set hand-picked slugs
This commit is contained in:
parent
132873826a
commit
002bc9b083
14 changed files with 114 additions and 8 deletions
|
@ -6,6 +6,7 @@ All notable changes to this project will be documented in this file.
|
||||||
### Improvements
|
### Improvements
|
||||||
- Configure logging to log unhandled exceptions to console even when DEBUG=False (#835)
|
- Configure logging to log unhandled exceptions to console even when DEBUG=False (#835)
|
||||||
- Make hc.lib.emails raise exceptions when EMAIL_ settings are not set
|
- Make hc.lib.emails raise exceptions when EMAIL_ settings are not set
|
||||||
|
- Decouple check's name from slug, allow users to set hand-picked slugs
|
||||||
|
|
||||||
## v2.9.2 - 2023-06-05
|
## v2.9.2 - 2023-06-05
|
||||||
|
|
||||||
|
|
|
@ -69,6 +69,7 @@ class HeadersField(forms.Field):
|
||||||
|
|
||||||
class NameTagsForm(forms.Form):
|
class NameTagsForm(forms.Form):
|
||||||
name = forms.CharField(max_length=100, required=False)
|
name = forms.CharField(max_length=100, required=False)
|
||||||
|
slug = forms.SlugField(max_length=100, required=False)
|
||||||
tags = forms.CharField(max_length=500, required=False)
|
tags = forms.CharField(max_length=500, required=False)
|
||||||
desc = forms.CharField(required=False)
|
desc = forms.CharField(required=False)
|
||||||
|
|
||||||
|
|
|
@ -14,6 +14,7 @@ class AddCheckTestCase(BaseTestCase):
|
||||||
def _payload(self, **kwargs):
|
def _payload(self, **kwargs):
|
||||||
payload = {
|
payload = {
|
||||||
"name": "Test",
|
"name": "Test",
|
||||||
|
"slug": "custom-slug",
|
||||||
"tags": "foo bar",
|
"tags": "foo bar",
|
||||||
"kind": "simple",
|
"kind": "simple",
|
||||||
"timeout": "120",
|
"timeout": "120",
|
||||||
|
@ -31,7 +32,7 @@ class AddCheckTestCase(BaseTestCase):
|
||||||
check = Check.objects.get()
|
check = Check.objects.get()
|
||||||
self.assertEqual(check.project, self.project)
|
self.assertEqual(check.project, self.project)
|
||||||
self.assertEqual(check.name, "Test")
|
self.assertEqual(check.name, "Test")
|
||||||
self.assertEqual(check.slug, "test")
|
self.assertEqual(check.slug, "custom-slug")
|
||||||
self.assertEqual(check.tags, "foo bar")
|
self.assertEqual(check.tags, "foo bar")
|
||||||
self.assertEqual(check.kind, "simple")
|
self.assertEqual(check.kind, "simple")
|
||||||
self.assertEqual(check.timeout.total_seconds(), 120)
|
self.assertEqual(check.timeout.total_seconds(), 120)
|
||||||
|
|
|
@ -14,12 +14,13 @@ class UpdateNameTestCase(BaseTestCase):
|
||||||
|
|
||||||
def test_it_works(self):
|
def test_it_works(self):
|
||||||
self.client.login(username="alice@example.org", password="password")
|
self.client.login(username="alice@example.org", password="password")
|
||||||
r = self.client.post(self.url, data={"name": "Alice Was Here"})
|
form = {"name": "Alice Was Here", "slug": "custom-slug"}
|
||||||
|
r = self.client.post(self.url, data=form)
|
||||||
self.assertRedirects(r, self.redirect_url)
|
self.assertRedirects(r, self.redirect_url)
|
||||||
|
|
||||||
self.check.refresh_from_db()
|
self.check.refresh_from_db()
|
||||||
self.assertEqual(self.check.name, "Alice Was Here")
|
self.assertEqual(self.check.name, "Alice Was Here")
|
||||||
self.assertEqual(self.check.slug, "alice-was-here")
|
self.assertEqual(self.check.slug, "custom-slug")
|
||||||
|
|
||||||
def test_redirect_preserves_querystring(self):
|
def test_redirect_preserves_querystring(self):
|
||||||
referer = self.redirect_url + "?tag=foo"
|
referer = self.redirect_url + "?tag=foo"
|
||||||
|
|
|
@ -481,7 +481,8 @@ def add_check(request, code):
|
||||||
return HttpResponseBadRequest()
|
return HttpResponseBadRequest()
|
||||||
|
|
||||||
check = Check(project=project)
|
check = Check(project=project)
|
||||||
check.set_name_slug(form.cleaned_data["name"])
|
check.name = form.cleaned_data["name"]
|
||||||
|
check.slug = form.cleaned_data["slug"]
|
||||||
check.tags = form.cleaned_data["tags"]
|
check.tags = form.cleaned_data["tags"]
|
||||||
check.kind = form.cleaned_data["kind"]
|
check.kind = form.cleaned_data["kind"]
|
||||||
check.timeout = form.cleaned_data["timeout"]
|
check.timeout = form.cleaned_data["timeout"]
|
||||||
|
@ -504,7 +505,8 @@ def update_name(request, code):
|
||||||
|
|
||||||
form = forms.NameTagsForm(request.POST)
|
form = forms.NameTagsForm(request.POST)
|
||||||
if form.is_valid():
|
if form.is_valid():
|
||||||
check.set_name_slug(form.cleaned_data["name"])
|
check.name = form.cleaned_data["name"]
|
||||||
|
check.slug = form.cleaned_data["slug"]
|
||||||
check.tags = form.cleaned_data["tags"]
|
check.tags = form.cleaned_data["tags"]
|
||||||
check.desc = form.cleaned_data["desc"]
|
check.desc = form.cleaned_data["desc"]
|
||||||
check.save()
|
check.save()
|
||||||
|
|
4
static/css/slug-suggestions.css
Normal file
4
static/css/slug-suggestions.css
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
.slug-help-block code {
|
||||||
|
color: var(--text-muted);
|
||||||
|
background: var(--pre-bg);
|
||||||
|
}
|
|
@ -7,6 +7,7 @@ $(function () {
|
||||||
|
|
||||||
$("#update-name-form").attr("action", url);
|
$("#update-name-form").attr("action", url);
|
||||||
$("#update-name-input").val(this.dataset.name);
|
$("#update-name-input").val(this.dataset.name);
|
||||||
|
$("#update-slug-input").val(this.dataset.slug);
|
||||||
|
|
||||||
var tagsSelectize = document.getElementById("update-tags-input").selectize;
|
var tagsSelectize = document.getElementById("update-tags-input").selectize;
|
||||||
tagsSelectize.setValue(this.dataset.tags.split(" "));
|
tagsSelectize.setValue(this.dataset.tags.split(" "));
|
||||||
|
|
40
static/js/slug-suggestions.js
Normal file
40
static/js/slug-suggestions.js
Normal file
|
@ -0,0 +1,40 @@
|
||||||
|
$(function () {
|
||||||
|
function slugify(text) {
|
||||||
|
return text
|
||||||
|
.normalize("NFKD")
|
||||||
|
.split("")
|
||||||
|
.map(ch => ch.charCodeAt(0) < 256 ? ch : "")
|
||||||
|
.join("")
|
||||||
|
.toLowerCase()
|
||||||
|
.replace(/[^\w\s-]/g, "")
|
||||||
|
.replace(/[-\s]+/g, "-")
|
||||||
|
.replace(/^-+/, "")
|
||||||
|
.replace(/-+$/, "");
|
||||||
|
}
|
||||||
|
|
||||||
|
$(".with-slug-suggestions").each(function() {
|
||||||
|
var nameInput = $("input[name='name']", this);
|
||||||
|
var slugInput = $("input[name='slug']", this);
|
||||||
|
var btn = $(".use-suggested-slug", this);
|
||||||
|
var help = $(".slug-help-block", this);
|
||||||
|
|
||||||
|
function update() {
|
||||||
|
var suggested = slugify(nameInput.val());
|
||||||
|
if (suggested) {
|
||||||
|
help.html(`Suggested value: <code>${suggested}</code>`);
|
||||||
|
} else {
|
||||||
|
help.text("Allowed characters: a-z, A-Z, 0-9, hyphens, underscores.");
|
||||||
|
}
|
||||||
|
|
||||||
|
btn.attr("disabled", !suggested);
|
||||||
|
}
|
||||||
|
|
||||||
|
$(nameInput).on("keyup change", update);
|
||||||
|
$(this).on("shown.bs.modal", update);
|
||||||
|
|
||||||
|
btn.click(function() {
|
||||||
|
slugInput.val(slugify(nameInput.val()));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
|
@ -59,6 +59,7 @@
|
||||||
<link rel="stylesheet" href="{% static 'css/signal_form.css' %}" type="text/css">
|
<link rel="stylesheet" href="{% static 'css/signal_form.css' %}" type="text/css">
|
||||||
<link rel="stylesheet" href="{% static 'css/project.css' %}" type="text/css">
|
<link rel="stylesheet" href="{% static 'css/project.css' %}" type="text/css">
|
||||||
<link rel="stylesheet" href="{% static 'css/signup.css' %}" type="text/css">
|
<link rel="stylesheet" href="{% static 'css/signup.css' %}" type="text/css">
|
||||||
|
<link rel="stylesheet" href="{% static 'css/slug-suggestions.css' %}" type="text/css">
|
||||||
{% endcompress %}
|
{% endcompress %}
|
||||||
</head>
|
</head>
|
||||||
<body class="page-{{ page }}{% if request.user.is_authenticated and request.profile.theme == 'dark' %} dark{% endif%}">
|
<body class="page-{{ page }}{% if request.user.is_authenticated and request.profile.theme == 'dark' %} dark{% endif%}">
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
<div id="add-check-modal" class="modal simple">
|
<div id="add-check-modal" class="modal simple with-slug-suggestions">
|
||||||
<div class="modal-dialog">
|
<div class="modal-dialog">
|
||||||
<form class="form-horizontal" method="post" action="{% url 'hc-add-check' project.code %}">
|
<form class="form-horizontal" method="post" action="{% url 'hc-add-check' project.code %}">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
|
@ -28,6 +28,30 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="add-check-slug" class="col-sm-3 control-label">
|
||||||
|
Slug
|
||||||
|
</label>
|
||||||
|
<div class="col-sm-9">
|
||||||
|
<div class="input-group">
|
||||||
|
<input
|
||||||
|
id="add-check-slug"
|
||||||
|
name="slug"
|
||||||
|
type="text"
|
||||||
|
maxlength="100"
|
||||||
|
value="{{ check.slug }}"
|
||||||
|
pattern="[a-zA-Z0-9_-]+"
|
||||||
|
class="form-control" />
|
||||||
|
<span class="input-group-btn">
|
||||||
|
<button
|
||||||
|
class="btn btn-default use-suggested-slug"
|
||||||
|
type="button">Use Suggested</button>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<span class="help-block slug-help-block"></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="add-check-tags" class="col-sm-3 control-label">
|
<label for="add-check-tags" class="col-sm-3 control-label">
|
||||||
Tags
|
Tags
|
||||||
|
|
|
@ -363,5 +363,6 @@
|
||||||
<script src="{% static 'js/ping_details.js' %}"></script>
|
<script src="{% static 'js/ping_details.js' %}"></script>
|
||||||
<script src="{% static 'js/details.js' %}"></script>
|
<script src="{% static 'js/details.js' %}"></script>
|
||||||
<script src="{% static 'js/initialize-timezone-selects.js' %}"></script>
|
<script src="{% static 'js/initialize-timezone-selects.js' %}"></script>
|
||||||
|
<script src="{% static 'js/slug-suggestions.js' %}"></script>
|
||||||
{% endcompress %}
|
{% endcompress %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
|
@ -102,5 +102,6 @@
|
||||||
<script src="{% static 'js/checks.js' %}"></script>
|
<script src="{% static 'js/checks.js' %}"></script>
|
||||||
<script src="{% static 'js/add-check-modal.js' %}"></script>
|
<script src="{% static 'js/add-check-modal.js' %}"></script>
|
||||||
<script src="{% static 'js/initialize-timezone-selects.js' %}"></script>
|
<script src="{% static 'js/initialize-timezone-selects.js' %}"></script>
|
||||||
|
<script src="{% static 'js/slug-suggestions.js' %}"></script>
|
||||||
{% endcompress %}
|
{% endcompress %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
|
@ -66,6 +66,7 @@
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<div data-name="{{ check.name }}"
|
<div data-name="{{ check.name }}"
|
||||||
|
data-slug="{{ check.slug }}"
|
||||||
data-tags="{{ check.tags }}"
|
data-tags="{{ check.tags }}"
|
||||||
data-desc="{{ check.desc }}"
|
data-desc="{{ check.desc }}"
|
||||||
class="my-checks-name {% if not check.name %}unnamed{% endif %}">
|
class="my-checks-name {% if not check.name %}unnamed{% endif %}">
|
||||||
|
@ -77,7 +78,7 @@
|
||||||
</td>
|
</td>
|
||||||
<td class="hidden-xs hidden-sm hidden-md">
|
<td class="hidden-xs hidden-sm hidden-md">
|
||||||
{% if project.show_slugs and not check.slug %}
|
{% if project.show_slugs and not check.slug %}
|
||||||
<span class="unavailable">unavailable, set name first</span>
|
<span class="unavailable">unavailable, slug not set</span>
|
||||||
{% else %}
|
{% else %}
|
||||||
<span class="my-checks-url">{{ check.url|format_ping_endpoint }}</span>
|
<span class="my-checks-url">{{ check.url|format_ping_endpoint }}</span>
|
||||||
{% if project.show_slugs and check.slug in ambiguous %}
|
{% if project.show_slugs and check.slug in ambiguous %}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
<div id="update-name-modal" class="modal">
|
<div id="update-name-modal" class="modal with-slug-suggestions">
|
||||||
<div class="modal-dialog">
|
<div class="modal-dialog">
|
||||||
<form
|
<form
|
||||||
id="update-name-form"
|
id="update-name-form"
|
||||||
|
@ -33,6 +33,33 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="update-slug-input" class="col-sm-2 control-label">
|
||||||
|
Slug
|
||||||
|
</label>
|
||||||
|
<div class="col-sm-10">
|
||||||
|
<div class="input-group">
|
||||||
|
<input
|
||||||
|
id="update-slug-input"
|
||||||
|
name="slug"
|
||||||
|
type="text"
|
||||||
|
maxlength="100"
|
||||||
|
value="{{ check.slug }}"
|
||||||
|
pattern="[a-zA-Z0-9_-]+"
|
||||||
|
data-src-id="update-name-input"
|
||||||
|
data-btn-id="use-suggested"
|
||||||
|
data-preview-id="slug-help"
|
||||||
|
class="form-control" />
|
||||||
|
<span class="input-group-btn">
|
||||||
|
<button
|
||||||
|
class="btn btn-default use-suggested-slug"
|
||||||
|
type="button">Use Suggested</button>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<span class="help-block slug-help-block"></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="update-tags-input" class="col-sm-2 control-label">
|
<label for="update-tags-input" class="col-sm-2 control-label">
|
||||||
Tags
|
Tags
|
||||||
|
|
Loading…
Add table
Reference in a new issue