forked from GithubBackups/healthchecks
Show a red "!" in project's top navigation if any integration is not working
This commit is contained in:
parent
8e455965c4
commit
4ee2646539
@ -1,6 +1,12 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
All notable changes to this project will be documented in this file.
|
All notable changes to this project will be documented in this file.
|
||||||
|
|
||||||
|
## v1.13.0-dev - Unreleased
|
||||||
|
|
||||||
|
### Improvements
|
||||||
|
- Show a red "!" in project's top navigation if any integration is not working
|
||||||
|
|
||||||
|
|
||||||
## v1.12.0 - 2020-01-02
|
## v1.12.0 - 2020-01-02
|
||||||
|
|
||||||
### Improvements
|
### Improvements
|
||||||
|
@ -282,6 +282,9 @@ class Project(models.Model):
|
|||||||
break
|
break
|
||||||
return status
|
return status
|
||||||
|
|
||||||
|
def have_broken_channels(self):
|
||||||
|
return self.channel_set.exclude(last_error="").exists()
|
||||||
|
|
||||||
|
|
||||||
class Member(models.Model):
|
class Member(models.Model):
|
||||||
user = models.ForeignKey(User, models.CASCADE, related_name="memberships")
|
user = models.ForeignKey(User, models.CASCADE, related_name="memberships")
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
from hc.test import BaseTestCase
|
from hc.test import BaseTestCase
|
||||||
from hc.accounts.models import Project
|
from hc.accounts.models import Project
|
||||||
from hc.api.models import Check
|
from hc.api.models import Check, Channel
|
||||||
|
|
||||||
|
|
||||||
class ProjectModelTestCase(BaseTestCase):
|
class ProjectModelTestCase(BaseTestCase):
|
||||||
@ -13,3 +13,11 @@ class ProjectModelTestCase(BaseTestCase):
|
|||||||
Check.objects.create(project=p2)
|
Check.objects.create(project=p2)
|
||||||
|
|
||||||
self.assertEqual(self.project.num_checks_available(), 18)
|
self.assertEqual(self.project.num_checks_available(), 18)
|
||||||
|
|
||||||
|
def test_it_handles_zero_broken_channels(self):
|
||||||
|
self.assertFalse(self.project.have_broken_channels())
|
||||||
|
|
||||||
|
def test_it_handles_one_broken_channel(self):
|
||||||
|
Channel.objects.create(kind="webhook", last_error="x", project=self.project)
|
||||||
|
|
||||||
|
self.assertTrue(self.project.have_broken_channels())
|
||||||
|
18
hc/api/migrations/0066_channel_last_error.py
Normal file
18
hc/api/migrations/0066_channel_last_error.py
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
# Generated by Django 3.0.1 on 2020-01-02 12:25
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('api', '0065_auto_20191127_1240'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='channel',
|
||||||
|
name='last_error',
|
||||||
|
field=models.CharField(blank=True, max_length=200),
|
||||||
|
),
|
||||||
|
]
|
27
hc/api/migrations/0067_last_error_values.py
Normal file
27
hc/api/migrations/0067_last_error_values.py
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
# Generated by Django 3.0.1 on 2020-01-02 14:28
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
|
def fill_last_errors(apps, schema_editor):
|
||||||
|
Channel = apps.get_model("api", "Channel")
|
||||||
|
Notification = apps.get_model("api", "Notification")
|
||||||
|
for ch in Channel.objects.all():
|
||||||
|
error = ""
|
||||||
|
try:
|
||||||
|
n = Notification.objects.filter(channel=ch).latest()
|
||||||
|
error = n.error
|
||||||
|
except Notification.DoesNotExist:
|
||||||
|
pass
|
||||||
|
|
||||||
|
ch.last_error = error
|
||||||
|
ch.save()
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("api", "0066_channel_last_error"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [migrations.RunPython(fill_last_errors, migrations.RunPython.noop)]
|
@ -338,6 +338,7 @@ class Channel(models.Model):
|
|||||||
kind = models.CharField(max_length=20, choices=CHANNEL_KINDS)
|
kind = models.CharField(max_length=20, choices=CHANNEL_KINDS)
|
||||||
value = models.TextField(blank=True)
|
value = models.TextField(blank=True)
|
||||||
email_verified = models.BooleanField(default=False)
|
email_verified = models.BooleanField(default=False)
|
||||||
|
last_error = models.CharField(max_length=200, blank=True)
|
||||||
checks = models.ManyToManyField(Check)
|
checks = models.ManyToManyField(Check)
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
@ -441,6 +442,9 @@ class Channel(models.Model):
|
|||||||
n.error = error
|
n.error = error
|
||||||
n.save()
|
n.save()
|
||||||
|
|
||||||
|
self.last_error = error
|
||||||
|
self.save()
|
||||||
|
|
||||||
return error
|
return error
|
||||||
|
|
||||||
def icon_path(self):
|
def icon_path(self):
|
||||||
|
@ -60,6 +60,7 @@ class NotifyTestCase(BaseTestCase):
|
|||||||
|
|
||||||
n = Notification.objects.get()
|
n = Notification.objects.get()
|
||||||
self.assertEqual(n.error, "Connection timed out")
|
self.assertEqual(n.error, "Connection timed out")
|
||||||
|
self.assertEqual(self.channel.last_error, "Connection timed out")
|
||||||
|
|
||||||
@patch("hc.api.transports.requests.request", side_effect=ConnectionError)
|
@patch("hc.api.transports.requests.request", side_effect=ConnectionError)
|
||||||
def test_webhooks_handle_connection_errors(self, mock_get):
|
def test_webhooks_handle_connection_errors(self, mock_get):
|
||||||
|
@ -125,3 +125,10 @@ class ChannelsTestCase(BaseTestCase):
|
|||||||
self.client.login(username="alice@example.org", password="password")
|
self.client.login(username="alice@example.org", password="password")
|
||||||
r = self.client.get("/integrations/")
|
r = self.client.get("/integrations/")
|
||||||
self.assertRedirects(r, "/")
|
self.assertRedirects(r, "/")
|
||||||
|
|
||||||
|
def test_it_shows_broken_channel_indicator(self):
|
||||||
|
Channel.objects.create(kind="sms", project=self.project, last_error="x")
|
||||||
|
|
||||||
|
self.client.login(username="alice@example.org", password="password")
|
||||||
|
r = self.client.get("/integrations/")
|
||||||
|
self.assertContains(r, "broken-channels", status_code=200)
|
||||||
|
@ -88,6 +88,9 @@ body {
|
|||||||
border-color: #22bc66;
|
border-color: #22bc66;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#broken-channels > a {
|
||||||
|
color: #a94442;
|
||||||
|
}
|
||||||
|
|
||||||
.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 ... */
|
/* Fluid below 1320px, but max width capped to 1320px ... */
|
||||||
|
@ -97,9 +97,14 @@
|
|||||||
<a href="{% url 'hc-checks' project.code %}">Checks</a>
|
<a href="{% url 'hc-checks' project.code %}">Checks</a>
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
<li {% if page == 'channels' %} class="active" {% endif %}>
|
{% with b=project.have_broken_channels %}
|
||||||
<a href="{% url 'hc-channels' %}">Integrations</a>
|
<li {% if b %}id="broken-channels"{% endif %} {% if page == 'channels' %}class="active"{% endif %}>
|
||||||
|
<a href="{% url 'hc-channels' %}">
|
||||||
|
Integrations
|
||||||
|
{% if b %}<span class="icon-grace"></span>{% endif %}
|
||||||
|
</a>
|
||||||
</li>
|
</li>
|
||||||
|
{% endwith %}
|
||||||
|
|
||||||
<li {% if page == 'badges' %} class="active" {% endif %}>
|
<li {% if page == 'badges' %} class="active" {% endif %}>
|
||||||
<a href="{% url 'hc-badges' project.code %}">Badges</a>
|
<a href="{% url 'hc-badges' project.code %}">Badges</a>
|
||||||
|
Loading…
x
Reference in New Issue
Block a user