Add Channel.name field, users can now name integrations.

This commit is contained in:
Pēteris Caune 2018-11-20 23:31:15 +02:00
parent c78ed91335
commit 21de50d84e
No known key found for this signature in database
GPG Key ID: E28D7679E9A9EDE2
16 changed files with 391 additions and 282 deletions

View File

@ -14,6 +14,7 @@ All notable changes to this project will be documented in this file.
- Add "channels" attribute to the Check API resource - Add "channels" attribute to the Check API resource
- Can specify channel codes when updating a check via API - Can specify channel codes when updating a check via API
- Added a workaround for email agents automatically opening "Unsubscribe" links - Added a workaround for email agents automatically opening "Unsubscribe" links
- Add Channel.name field, users can now name integrations
### Bug Fixes ### Bug Fixes
- During DST transition, handle ambiguous dates as pre-transition - During DST transition, handle ambiguous dates as pre-transition

View File

@ -0,0 +1,18 @@
# Generated by Django 2.1.3 on 2018-11-20 16:50
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('api', '0042_auto_20181029_1522'),
]
operations = [
migrations.AddField(
model_name='channel',
name='name',
field=models.CharField(blank=True, max_length=100),
),
]

View File

@ -0,0 +1,25 @@
# Generated by Django 2.1.3 on 2018-11-20 20:04
import json
from django.db import migrations
def combine_channel_names(apps, schema_editor):
Channel = apps.get_model('api', 'Channel')
for channel in Channel.objects.filter(kind="sms"):
if channel.value.startswith("{"):
doc = json.loads(channel.value)
channel.name = doc.get("label", "")
channel.save()
class Migration(migrations.Migration):
dependencies = [
('api', '0043_channel_name'),
]
operations = [
migrations.RunPython(combine_channel_names),
]

View File

@ -231,6 +231,7 @@ class Ping(models.Model):
class Channel(models.Model): class Channel(models.Model):
name = models.CharField(max_length=100, blank=True)
code = models.UUIDField(default=uuid.uuid4, editable=False) code = models.UUIDField(default=uuid.uuid4, editable=False)
user = models.ForeignKey(User, models.CASCADE) user = models.ForeignKey(User, models.CASCADE)
created = models.DateTimeField(auto_now_add=True) created = models.DateTimeField(auto_now_add=True)
@ -240,11 +241,11 @@ class Channel(models.Model):
checks = models.ManyToManyField(Check) checks = models.ManyToManyField(Check)
def __str__(self): def __str__(self):
if self.name:
return self.name
if self.kind == "email": if self.kind == "email":
return "Email to %s" % self.value return "Email to %s" % self.value
elif self.kind == "sms": elif self.kind == "sms":
if self.sms_label:
return "SMS to %s" % self.sms_label
return "SMS to %s" % self.sms_number return "SMS to %s" % self.sms_number
elif self.kind == "slack": elif self.kind == "slack":
return "Slack %s" % self.slack_channel return "Slack %s" % self.slack_channel
@ -327,6 +328,9 @@ class Channel(models.Model):
return error return error
def icon_path(self):
return 'img/integrations/%s.png' % self.kind
@property @property
def po_value(self): def po_value(self):
assert self.kind == "po" assert self.kind == "po"

View File

@ -108,3 +108,7 @@ class AddSmsForm(forms.Form):
error_css_class = "has-error" error_css_class = "has-error"
label = forms.CharField(max_length=100, required=False) label = forms.CharField(max_length=100, required=False)
value = forms.CharField(max_length=16, validators=[phone_validator]) value = forms.CharField(max_length=16, validators=[phone_validator])
class ChannelNameForm(forms.Form):
name = forms.CharField(max_length=100, required=False)

View File

@ -31,7 +31,7 @@ class AddSmsTestCase(BaseTestCase):
c = Channel.objects.get() c = Channel.objects.get()
self.assertEqual(c.kind, "sms") self.assertEqual(c.kind, "sms")
self.assertEqual(c.sms_number, "+1234567890") self.assertEqual(c.sms_number, "+1234567890")
self.assertEqual(c.sms_label, "My Phone") self.assertEqual(c.name, "My Phone")
def test_it_rejects_bad_number(self): def test_it_rejects_bad_number(self):
form = {"value": "not a phone number address"} form = {"value": "not a phone number address"}

View File

@ -33,9 +33,9 @@ class ChannelsTestCase(BaseTestCase):
self.assertEqual(r.status_code, 200) self.assertEqual(r.status_code, 200)
# These are inside a modal: # These are inside a modal:
self.assertContains(r, "<code>http://down.example.com</code>") self.assertContains(r, "http://down.example.com")
self.assertContains(r, "<code>http://up.example.com</code>") self.assertContains(r, "http://up.example.com")
self.assertContains(r, "<code>foobar</code>") self.assertContains(r, "foobar")
def test_it_shows_pushover_details(self): def test_it_shows_pushover_details(self):
ch = Channel(kind="po", user=self.alice) ch = Channel(kind="po", user=self.alice)
@ -46,7 +46,6 @@ class ChannelsTestCase(BaseTestCase):
r = self.client.get("/integrations/") r = self.client.get("/integrations/")
self.assertEqual(r.status_code, 200) self.assertEqual(r.status_code, 200)
self.assertContains(r, "fake-key")
self.assertContains(r, "(normal priority)") self.assertContains(r, "(normal priority)")
def test_it_shows_disabled_email(self): def test_it_shows_disabled_email(self):
@ -63,7 +62,7 @@ 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.assertEqual(r.status_code, 200) self.assertEqual(r.status_code, 200)
self.assertContains(r, "(bounced, disabled)") self.assertContains(r, "Disabled")
def test_it_shows_unconfirmed_email(self): def test_it_shows_unconfirmed_email(self):
channel = Channel(user=self.alice, kind="email") channel = Channel(user=self.alice, kind="email")
@ -73,7 +72,7 @@ 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.assertEqual(r.status_code, 200) self.assertEqual(r.status_code, 200)
self.assertContains(r, "(unconfirmed)") self.assertContains(r, "Unconfirmed")
def test_it_shows_sms_label(self): def test_it_shows_sms_label(self):
ch = Channel(kind="sms", user=self.alice) ch = Channel(kind="sms", user=self.alice)
@ -84,4 +83,4 @@ class ChannelsTestCase(BaseTestCase):
r = self.client.get("/integrations/") r = self.client.get("/integrations/")
self.assertEqual(r.status_code, 200) self.assertEqual(r.status_code, 200)
self.assertContains(r, "My Phone (+123)") self.assertContains(r, "SMS to +123")

View File

@ -0,0 +1,54 @@
from hc.api.models import Channel
from hc.test import BaseTestCase
class UpdateChannelNameTestCase(BaseTestCase):
def setUp(self):
super(UpdateChannelNameTestCase, self).setUp()
self.channel = Channel(kind="email", user=self.alice)
self.channel.save()
self.url = "/integrations/%s/name/" % self.channel.code
def test_it_works(self):
payload = {"name": "My work email"}
self.client.login(username="alice@example.org", password="password")
r = self.client.post(self.url, data=payload)
self.assertRedirects(r, "/integrations/")
self.channel.refresh_from_db()
self.assertEqual(self.channel.name, "My work email")
def test_team_access_works(self):
payload = {"name": "Bob was here"}
# Logging in as bob, not alice. Bob has team access so this
# should work.
self.client.login(username="bob@example.org", password="password")
self.client.post(self.url, data=payload)
self.channel.refresh_from_db()
self.assertEqual(self.channel.name, "Bob was here")
def test_it_checks_ownership(self):
payload = {"name": "Charlie Sent This"}
self.client.login(username="charlie@example.org", password="password")
r = self.client.post(self.url, data=payload)
self.assertEqual(r.status_code, 403)
def test_it_handles_missing_uuid(self):
# Valid UUID but there is no check for it:
url = "/integrations/6837d6ec-fc08-4da5-a67f-08a9ed1ccf62/name/"
payload = {"name": "Alice Was Here"}
self.client.login(username="alice@example.org", password="password")
r = self.client.post(url, data=payload)
self.assertEqual(r.status_code, 404)
def test_it_rejects_get(self):
self.client.login(username="alice@example.org", password="password")
r = self.client.get(self.url)
self.assertEqual(r.status_code, 405)

View File

@ -9,36 +9,35 @@ class UpdateNameTestCase(BaseTestCase):
self.check = Check(user=self.alice) self.check = Check(user=self.alice)
self.check.save() self.check.save()
self.url = "/checks/%s/name/" % self.check.code
def test_it_works(self): def test_it_works(self):
url = "/checks/%s/name/" % self.check.code
payload = {"name": "Alice Was Here"} payload = {"name": "Alice Was Here"}
self.client.login(username="alice@example.org", password="password") self.client.login(username="alice@example.org", password="password")
r = self.client.post(url, data=payload) r = self.client.post(self.url, data=payload)
self.assertRedirects(r, "/checks/") self.assertRedirects(r, "/checks/")
check = Check.objects.get(code=self.check.code) self.check.refresh_from_db()
assert check.name == "Alice Was Here" self.assertEqual(self.check.name, "Alice Was Here")
def test_team_access_works(self): def test_team_access_works(self):
url = "/checks/%s/name/" % self.check.code
payload = {"name": "Bob Was Here"} payload = {"name": "Bob Was Here"}
# Logging in as bob, not alice. Bob has team access so this # Logging in as bob, not alice. Bob has team access so this
# should work. # should work.
self.client.login(username="bob@example.org", password="password") self.client.login(username="bob@example.org", password="password")
self.client.post(url, data=payload) self.client.post(self.url, data=payload)
check = Check.objects.get(code=self.check.code) self.check.refresh_from_db()
assert check.name == "Bob Was Here" self.assertEqual(self.check.name, "Bob Was Here")
def test_it_checks_ownership(self): def test_it_checks_ownership(self):
url = "/checks/%s/name/" % self.check.code
payload = {"name": "Charlie Sent This"} payload = {"name": "Charlie Sent This"}
self.client.login(username="charlie@example.org", password="password") self.client.login(username="charlie@example.org", password="password")
r = self.client.post(url, data=payload) r = self.client.post(self.url, data=payload)
assert r.status_code == 403 self.assertEqual(r.status_code, 403)
def test_it_handles_bad_uuid(self): def test_it_handles_bad_uuid(self):
url = "/checks/not-uuid/name/" url = "/checks/not-uuid/name/"
@ -55,20 +54,18 @@ class UpdateNameTestCase(BaseTestCase):
self.client.login(username="alice@example.org", password="password") self.client.login(username="alice@example.org", password="password")
r = self.client.post(url, data=payload) r = self.client.post(url, data=payload)
assert r.status_code == 404 self.assertEqual(r.status_code, 404)
def test_it_sanitizes_tags(self): def test_it_sanitizes_tags(self):
url = "/checks/%s/name/" % self.check.code
payload = {"tags": " foo bar\r\t \n baz \n"} payload = {"tags": " foo bar\r\t \n baz \n"}
self.client.login(username="alice@example.org", password="password") self.client.login(username="alice@example.org", password="password")
self.client.post(url, data=payload) self.client.post(self.url, data=payload)
check = Check.objects.get(id=self.check.id) self.check.refresh_from_db()
self.assertEqual(check.tags, "foo bar baz") self.assertEqual(self.check.tags, "foo bar baz")
def test_it_rejects_get(self): def test_it_rejects_get(self):
url = "/checks/%s/name/" % self.check.code
self.client.login(username="alice@example.org", password="password") self.client.login(username="alice@example.org", password="password")
r = self.client.get(url) r = self.client.get(self.url)
self.assertEqual(r.status_code, 405) self.assertEqual(r.status_code, 405)

View File

@ -38,6 +38,7 @@ channel_urls = [
path('add_trello/', views.add_trello, name="hc-add-trello"), path('add_trello/', views.add_trello, name="hc-add-trello"),
path('add_trello/settings/', views.trello_settings, name="hc-trello-settings"), path('add_trello/settings/', views.trello_settings, name="hc-trello-settings"),
path('<uuid:code>/checks/', views.channel_checks, name="hc-channel-checks"), path('<uuid:code>/checks/', views.channel_checks, name="hc-channel-checks"),
path('<uuid:code>/name/', views.update_channel_name, name="hc-channel-name"),
path('<uuid:code>/remove/', views.remove_channel, name="hc-remove-channel"), path('<uuid:code>/remove/', views.remove_channel, name="hc-remove-channel"),
path('<uuid:code>/verify/<slug:token>/', views.verify_email, path('<uuid:code>/verify/<slug:token>/', views.verify_email,
name="hc-verify-email"), name="hc-verify-email"),

View File

@ -22,7 +22,8 @@ from hc.api.models import (DEFAULT_GRACE, DEFAULT_TIMEOUT, Channel, Check,
from hc.api.transports import Telegram from hc.api.transports import Telegram
from hc.front.forms import (AddWebhookForm, NameTagsForm, from hc.front.forms import (AddWebhookForm, NameTagsForm,
TimeoutForm, AddUrlForm, AddEmailForm, TimeoutForm, AddUrlForm, AddEmailForm,
AddOpsGenieForm, CronForm, AddSmsForm) AddOpsGenieForm, CronForm, AddSmsForm,
ChannelNameForm)
from hc.front.schemas import telegram_callback from hc.front.schemas import telegram_callback
from hc.front.templatetags.hc_extras import (num_down_title, down_title, from hc.front.templatetags.hc_extras import (num_down_title, down_title,
sortchecks) sortchecks)
@ -498,6 +499,21 @@ def channel_checks(request, code):
return render(request, "front/channel_checks.html", ctx) return render(request, "front/channel_checks.html", ctx)
@require_POST
@login_required
def update_channel_name(request, code):
channel = get_object_or_404(Channel, code=code)
if channel.user_id != request.team.user.id:
return HttpResponseForbidden()
form = ChannelNameForm(request.POST)
if form.is_valid():
channel.name = form.cleaned_data["name"]
channel.save()
return redirect("hc-channels")
def verify_email(request, code, token): def verify_email(request, code, token):
channel = get_object_or_404(Channel, code=code) channel = get_object_or_404(Channel, code=code)
if channel.make_token() == token: if channel.make_token() == token:
@ -1001,8 +1017,8 @@ def add_sms(request):
form = AddSmsForm(request.POST) form = AddSmsForm(request.POST)
if form.is_valid(): if form.is_valid():
channel = Channel(user=request.team.user, kind="sms") channel = Channel(user=request.team.user, kind="sms")
channel.name = form.cleaned_data["label"]
channel.value = json.dumps({ channel.value = json.dumps({
"label": form.cleaned_data["label"],
"value": form.cleaned_data["value"] "value": form.cleaned_data["value"]
}) })
channel.save() channel.save()

View File

@ -9,6 +9,26 @@
border-top: 1px solid #f1f1f1; border-top: 1px solid #f1f1f1;
} }
.channels-table .icon-cell {
width: 40px;
}
.channels-table .icon-cell img {
margin-left: 16px;
height: 40px;
}
.channels-table .th-name,
.channels-table .th-checks {
padding-left: 15px;
}
.channels-table .unnamed {
font-style: italic;
color: #999;
}
.channels-table .value-cell { .channels-table .value-cell {
max-width: 400px; max-width: 400px;
overflow: hidden; overflow: hidden;
@ -20,22 +40,10 @@
background: #f5f5f5; background: #f5f5f5;
} }
table.channels-table > tbody > tr > th { table.channels-table > tbody > tr > th {
border-top: 0; border-top: 0;
} }
.channels-table .channels-add-title {
border-top: 0;
padding-top: 20px
}
.channels-table .channels-add-help {
color: #888;
border-top: 0;
}
.word-up { .word-up {
color: #5cb85c; color: #5cb85c;
font-weight: bold font-weight: bold
@ -58,20 +66,32 @@ table.channels-table > tbody > tr > th {
font-size: small; font-size: small;
} }
.edit-name, .edit-checks {
padding: 12px 6px;
border: 1px solid #FFF;
cursor: pointer;
}
.channels-help-hidden { .edit-name .channel-details-mini {
display: none; font-size: 11.7px;
color: #888;
max-width: 500px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.channel-details-mini span {
color: #111;
}
.channel-row:hover .edit-name,
.channel-row:hover .edit-checks {
border: 1px dotted #AAA;
} }
.edit-checks { .edit-checks {
display: inline-block;
width: 120px;
padding: 6px;
text-align: center;
line-height: 28px;
border: 1px solid #FFF;
color: #333; color: #333;
} }
.edit-checks:hover { .edit-checks:hover {
@ -79,10 +99,6 @@ table.channels-table > tbody > tr > th {
color: #000; color: #000;
} }
.channel-row:hover .edit-checks {
border: 1px dotted #AAA;
}
.channel-remove { .channel-remove {
visibility: hidden; visibility: hidden;
} }
@ -91,6 +107,11 @@ table.channels-table > tbody > tr > th {
visibility: visible; visibility: visible;
} }
.webhook-details td {
max-width: 300px;
}
.ai-title { .ai-title {
margin-top: 2em; margin-top: 2em;
} }
@ -212,6 +233,10 @@ table.channels-table > tbody > tr > th {
font-style: italic; font-style: italic;
} }
.channel-modal .modal-body {
padding: 15px 40px;
}
/* Add Webhook */ /* Add Webhook */
@ -230,4 +255,4 @@ table.channels-table > tbody > tr > th {
.zendesk-subdomain input { .zendesk-subdomain input {
border-right: 0; border-right: 0;
} }

View File

@ -36,7 +36,7 @@
#checks-table .integrations, #checks-table .integrations,
#checks-table .timeout-grace, #checks-table .timeout-grace,
#checks-table .last-ping { #checks-table .last-ping {
border: 1px solid rgba(0, 0, 0, 0); border: 1px solid #FFF;
padding: 6px; padding: 6px;
} }

View File

@ -1,30 +1,12 @@
$(function() { $(function() {
var placeholders = {
email: "address@example.org",
webhook: "http://",
slack: "https://hooks.slack.com/...",
hipchat: "https://api.hipchat.com/...",
pd: "service key"
}
$("#add-channel-kind").change(function() {
$(".channels-add-help p").hide();
var v = $("#add-channel-kind").val();
$(".channels-add-help p." + v).show();
$("#add-channel-value").attr("placeholder", placeholders[v]);
});
$(".edit-checks").click(function() { $(".edit-checks").click(function() {
$("#checks-modal").modal("show"); $("#checks-modal").modal("show");
var url = $(this).attr("href"); $.ajax(this.dataset.url).done(function(data) {
$.ajax(url).done(function(data) {
$("#checks-modal .modal-content").html(data); $("#checks-modal .modal-content").html(data);
}) })
return false; return false;
}); });

View File

@ -18,114 +18,94 @@
<table class="table channels-table"> <table class="table channels-table">
{% if channels %} {% if channels %}
<tr> <tr>
<th>Type</th> <th></th>
<th>Value</th> <th class="th-name">Name, Details</th>
<th>Assigned Checks</th> <th class="th-checks">Assigned Checks</th>
<th>Status</th>
<th>Last Notification</th> <th>Last Notification</th>
<th></th> <th></th>
</tr> </tr>
{% for ch in channels %} {% for ch in channels %}
{% with n=ch.latest_notification %}
<tr class="channel-row"> <tr class="channel-row">
<td>{{ ch.get_kind_display }}</td> <td class="icon-cell">
<td class="value-cell"> <img src="{% static ch.icon_path %}"
{% if ch.kind == "email" %} class="icon"
<span class="preposition">to</span> alt="Slack icon"
{{ ch.value }} data-toggle="tooltip"
{% if not ch.email_verified %} title="Slack" />
{% if ch.latest_notification and ch.latest_notification.error %} </td>
<span class="channel-disabled"> <td>
(bounced, disabled) <div class="edit-name" data-toggle="modal" data-target="#name-{{ ch.code }}">
</span> {% if ch.name %}
{% else %} {{ ch.name }}
<span class="channel-unconfirmed"> {% else %}
(unconfirmed) <div class="unnamed">unnamed</div>
</span> {% endif %}
{% endif %} <div class="channel-details-mini">
</span> {% if ch.kind == "email" %}
{% endif %} Email to <span>{{ ch.value }}</span>
{% elif ch.kind == "pd" %} {% elif ch.kind == "pd" %}
{% if ch.pd_account %} PagerDuty account <span>{{ ch.pd_account }}</span>
<span class="preposition">account</span> {% elif ch.kind == "pagertree" %}
{{ ch.pd_account}}, PagerTree
{% endif %} {% elif ch.kind == "opsgenie" %}
<span class="preposition">service key</span> OpsGenie
{{ ch.pd_service_key }} {% elif ch.kind == "victorops" %}
{% elif ch.kind == "pagertree" %} VictorOps
<span class="preposition">URL</span> {% elif ch.kind == "po" %}
{{ ch.value }} Pushover ({{ ch.po_value|last }} priority)
{% elif ch.kind == "opsgenie" %} {% elif ch.kind == "slack" %}
<span class="preposition">API key</span> Slack
{{ ch.value }} {% if ch.slack_team %}
{% elif ch.kind == "victorops" %} team <span>{{ ch.slack_team }}</span>,
<span class="preposition">Post URL</span> channel <span>{{ ch.slack_channel }}</span>
{{ ch.value }} {% endif %}
{% elif ch.kind == "po" %} {% elif ch.kind == "webhook" %}
<span class="preposition">user key</span> Webhook
{{ ch.po_value|first }} {% elif ch.kind == "pushbullet" %}
({{ ch.po_value|last }} priority) Pushbullet
{% elif ch.kind == "slack" %} {% elif ch.kind == "discord" %}
{% if ch.slack_team %} Discord
<span class="preposition">team</span> {% elif ch.kind == "telegram" %}
{{ ch.slack_team }}, Telegram
<span class="preposition">channel</span> {% if ch.telegram_type == "group" %}
{{ ch.slack_channel }} chat <span>{{ ch.telegram_name }}</span>
{% else %} {% elif ch.telegram_type == "private" %}
{{ ch.value }} user <span>{{ ch.telegram_name }}</span>
{% endif %} {% endif %}
{% elif ch.kind == "webhook" %} {% elif ch.kind == "hipchat" %}
{% if ch.url_down %} HipChat
<span class="preposition">down</span> {{ ch.url_down }} {% elif ch.kind == "sms" %}
{% endif %} SMS to <span>{{ ch.sms_number }}</span>
{% if ch.url_up and not ch.url_down %} {% elif ch.kind == "trello" %}
<span class="preposition">up</span> {{ ch.url_up }} Trello
{% endif %} board <span>{{ ch.trello_board_list|first }}</span>,
{% if ch.url_up or ch.post_data or ch.headers %} list <span>{{ ch.trello_board_list|last }}</span>
<a href="#" {% else %}
data-toggle="modal" {{ ch.kind }}
data-target="#channel-details-{{ ch.code }}">(details)</a> {% endif %}
{% endif %} </div>
</div>
</td>
{% elif ch.kind == "pushbullet" %} <td>
<span class="preposition">API key</span> <div class="edit-checks"
{{ ch.value }} data-url="{% url 'hc-channel-checks' ch.code %}">
{% elif ch.kind == "discord" %} {{ ch.n_checks }} checks
{{ ch.discord_webhook_id }} </div>
{% elif ch.kind == "telegram" %} </td>
{% if ch.telegram_type == "group" %} <td>
<span class="preposition">chat</span> {% if ch.kind == "email" and not ch.email_verified %}
{% elif ch.telegram_type == "private" %} {% if n and n.error %}
<span class="preposition">user</span> <span class="label label-danger">Disabled</span>
{% endif %} {% else %}
{{ ch.telegram_name }} <span class="label label-default">Unconfirmed</span>
{% elif ch.kind == "hipchat" %} {% endif %}
{{ ch.hipchat_webhook_url }} {% else %}
{% elif ch.kind == "zendesk" %} Ready to deliver
{{ ch.zendesk_subdomain }}.zendesk.com {% endif %}
{% elif ch.kind == "sms" %}
{% if ch.sms_label %}
{{ ch.sms_label }} ({{ ch.sms_number }})
{% else %}
{{ ch.sms_number }}
{% endif %}
{% elif ch.kind == "trello" %}
<span class="preposition">board</span>
{{ ch.trello_board_list|first }},
<span class="preposition">list</span>
{{ ch.trello_board_list|last }}
{% else %}
{{ ch.value }}
{% endif %}
</td>
<td class="channels-num-checks">
<a
class="edit-checks"
href="{% url 'hc-channel-checks' ch.code %}">
{{ ch.n_checks }} of {{ num_checks }}
</a>
</td> </td>
<td> <td>
{% with n=ch.latest_notification %}
{% if n %} {% if n %}
{% if n.error %} {% if n.error %}
<span class="text-danger" data-toggle="tooltip" title="{{ n.error }}"> <span class="text-danger" data-toggle="tooltip" title="{{ n.error }}">
@ -140,7 +120,6 @@
{% if ch.kind == "sms" %} {% if ch.kind == "sms" %}
<p>Used {{ profile.sms_sent_this_month }} of {{ profile.sms_limit }} sends this month.</p> <p>Used {{ profile.sms_sent_this_month }} of {{ profile.sms_limit }} sends this month.</p>
{% endif %} {% endif %}
{% endwith %}
</td> </td>
<td> <td>
<button <button
@ -155,6 +134,7 @@
</td> </td>
</tr> </tr>
{% endwith %}
{% endfor %} {% endfor %}
{% endif %} {% endif %}
</table> </table>
@ -364,69 +344,72 @@
</div> </div>
{% for ch in channels %} {% for ch in channels %}
{% if ch.kind == "webhook" %} <div id="name-{{ ch.code }}" class="modal channel-modal">
<div id="channel-details-{{ ch.code }}" class="modal channel-details">
<div class="modal-dialog"> <div class="modal-dialog">
<div class="modal-content"> <form
<div class="modal-header"> action="{% url 'hc-channel-name' ch.code %}"
<button type="button" class="close" data-dismiss="modal">&times;</button> class="form-horizontal"
<h4>Integration Details</h4> method="post">
{% csrf_token %}
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal">&times;</button>
<h4 class="update-timeout-title">Integration Details</h4>
</div>
<div class="modal-body">
<div class="form-group">
<label for="update-name-input" class="col-sm-2 control-label">
Name
</label>
<div class="col-sm-10">
<input
name="name"
type="text"
value="{{ ch.name }}"
placeholder="{{ ch }}"
class="input-name form-control" />
<span class="help-block">
Give this integration a human-friendly name,
so you can easily recognize it later.
</span>
</div>
</div>
{% if ch.kind == "webhook" %}
{% if ch.url_down %}
<p><strong>URL for "down" events</strong></p>
<pre>{{ ch.url_down }}</pre>
{% endif %}
{% if ch.url_up %}
<p><strong>URL for "up" events</strong></p>
<pre>{{ ch.url_up }}</pre>
{% endif %}
{% if ch.post_data %}
<p><strong>POST data</strong></p>
<pre>{{ ch.post_data }}</pre>
{% endif %}
{% for k, v in ch.headers.items %}
<p><strong>Header <code>{{ k }}</code></strong></p>
<pre>{{ v }}</pre>
{% endfor %}
{% endif %}
</div>
<div class="modal-footer">
<button type="button" class="btn btn-default" data-dismiss="modal">Cancel</button>
<button type="submit" class="btn btn-primary">Save</button>
</div>
</div> </div>
<div class="modal-body"> </form>
<table>
<tr>
<th>Request Method</th>
<td>
{% if ch.post_data %}
POST
{% else %}
GET
{% endif %}
</td>
</tr>
<tr>
<th>URL for "down" events</th>
<td>
{% if ch.url_down %}
<code>{{ ch.url_down }}</code>
{% else %}
<span class="missing">(not set)</span>
{% endif %}
</td>
</tr>
{% if ch.url_up %}
<tr>
<th>URL for "up" events</th>
<td>
{% if ch.url_up %}
<code>{{ ch.url_up }}</code>
{% else %}
<span class="missing">(not set)</span>
{% endif %}
</td>
</tr>
{% endif %}
{% if ch.post_data %}
<tr>
<th>POST data</th>
<td><code>{{ ch.post_data }}</code></td>
</tr>
{% endif %}
{% for k, v in ch.headers.items %}
<tr>
<th>Header <code>{{ k }}</code></th>
<td><code>{{ v }}</code></td>
</tr>
{% endfor %}
</table>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-default" data-dismiss="modal">Close</button>
</div>
</div>
</div> </div>
</div> </div>
{% endif %}
{% endfor %} {% endfor %}
{% endblock %} {% endblock %}

View File

@ -12,58 +12,58 @@
<h4 class="update-timeout-title">Name and Tags</h4> <h4 class="update-timeout-title">Name and Tags</h4>
</div> </div>
<div class="modal-body"> <div class="modal-body">
<div class="form-group"> <div class="form-group">
<label for="update-name-input" class="col-sm-2 control-label"> <label for="update-name-input" class="col-sm-2 control-label">
Name Name
</label> </label>
<div class="col-sm-10"> <div class="col-sm-10">
<input <input
id="update-name-input" id="update-name-input"
name="name" name="name"
type="text" type="text"
value="{{ check.name }}" value="{{ check.name }}"
placeholder="unnamed" placeholder="unnamed"
class="input-name form-control" /> class="input-name form-control" />
<span class="help-block"> <span class="help-block">
Give this check a human-friendly name, Give this check a human-friendly name,
so you can easily recognize it later. so you can easily recognize it later.
</span> </span>
</div>
</div> </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
</label> </label>
<div class="col-sm-10"> <div class="col-sm-10">
<input <input
id="update-tags-input" id="update-tags-input"
name="tags" name="tags"
type="text" type="text"
value="{{ check.tags }}" value="{{ check.tags }}"
placeholder="production www" placeholder="production www"
class="form-control" /> class="form-control" />
<span class="help-block"> <span class="help-block">
Use tags for easy filtering and for status badges. Use tags for easy filtering and for status badges.
Separate multiple tags with spaces. Separate multiple tags with spaces.
</span> </span>
</div>
</div> </div>
</div>
<div class="form-group"> <div class="form-group">
<label for="update-desc-input" class="col-sm-2 control-label"> <label for="update-desc-input" class="col-sm-2 control-label">
Description Description
</label> </label>
<div class="col-sm-10"> <div class="col-sm-10">
<textarea <textarea
id="update-desc-input" id="update-desc-input"
class="form-control" class="form-control"
rows="5" rows="5"
name="desc">{{ check.desc }}</textarea> name="desc">{{ check.desc }}</textarea>
</div>
</div> </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>