Browse Source

Webhooks support PUT method.

.Webhooks can have different request bodies and headers for "up" and "events".
pull/230/head
Pēteris Caune 6 years ago
parent
commit
d054970b02
No known key found for this signature in database GPG Key ID: E28D7679E9A9EDE2
11 changed files with 482 additions and 272 deletions
  1. +2
    -0
      CHANGELOG.md
  2. +44
    -26
      hc/api/models.py
  3. +77
    -7
      hc/api/tests/test_notify.py
  4. +13
    -11
      hc/api/transports.py
  5. +42
    -31
      hc/front/forms.py
  6. +5
    -0
      hc/front/templatetags/hc_extras.py
  7. +65
    -30
      hc/front/tests/test_add_webhook.py
  8. +36
    -0
      static/css/add_webhook.css
  9. +11
    -21
      static/js/webhook.js
  10. +29
    -21
      templates/front/channels.html
  11. +158
    -125
      templates/integrations/add_webhook.html

+ 2
- 0
CHANGELOG.md View File

@ -6,6 +6,8 @@ All notable changes to this project will be documented in this file.
### Improvements ### Improvements
- Add the `prunetokenbucket` management command - Add the `prunetokenbucket` management command
- Show check counts in JSON "badges" (#251) - Show check counts in JSON "badges" (#251)
- Webhooks support PUT method (#249)
- Webhooks can have different request bodies and headers for "up" and "events" (#249)
### Bug Fixes ### Bug Fixes
- Fix badges for tags containing special characters (#240, #237) - Fix badges for tags containing special characters (#240, #237)


+ 44
- 26
hc/api/models.py View File

@ -360,44 +360,62 @@ class Channel(models.Model):
prio = int(parts[1]) prio = int(parts[1])
return PO_PRIORITIES[prio] return PO_PRIORITIES[prio]
@property
def url_down(self):
def webhook_spec(self, status):
assert self.kind == "webhook" assert self.kind == "webhook"
if not self.value.startswith("{"):
parts = self.value.split("\n")
return parts[0]
doc = json.loads(self.value)
return doc.get("url_down")
@property
def url_up(self):
assert self.kind == "webhook"
if not self.value.startswith("{"): if not self.value.startswith("{"):
parts = self.value.split("\n") parts = self.value.split("\n")
return parts[1] if len(parts) > 1 else ""
url_down = parts[0]
url_up = parts[1] if len(parts) > 1 else ""
post_data = parts[2] if len(parts) > 2 else ""
return {
"method": "POST" if post_data else "GET",
"url": url_down if status == "down" else url_up,
"body": post_data,
"headers": {},
}
doc = json.loads(self.value) doc = json.loads(self.value)
return doc.get("url_up")
if "post_data" in doc:
# Legacy "post_data" in doc -- use the legacy fields
return {
"method": "POST" if doc["post_data"] else "GET",
"url": doc["url_down"] if status == "down" else doc["url_up"],
"body": doc["post_data"],
"headers": doc["headers"],
}
if status == "down" and "method_down" in doc:
return {
"method": doc["method_down"],
"url": doc["url_down"],
"body": doc["body_down"],
"headers": doc["headers_down"],
}
elif status == "up" and "method_up" in doc:
return {
"method": doc["method_up"],
"url": doc["url_up"],
"body": doc["body_up"],
"headers": doc["headers_up"],
}
@property @property
def post_data(self):
assert self.kind == "webhook"
if not self.value.startswith("{"):
parts = self.value.split("\n")
return parts[2] if len(parts) > 2 else ""
def down_webhook_spec(self):
return self.webhook_spec("down")
doc = json.loads(self.value)
return doc.get("post_data")
@property
def up_webhook_spec(self):
return self.webhook_spec("up")
@property @property
def headers(self):
assert self.kind == "webhook"
if not self.value.startswith("{"):
return {}
def url_down(self):
return self.down_webhook_spec["url"]
doc = json.loads(self.value)
return doc.get("headers", {})
@property
def url_up(self):
return self.up_webhook_spec["url"]
@property @property
def slack_team(self): def slack_team(self):


+ 77
- 7
hc/api/tests/test_notify.py View File

@ -146,8 +146,8 @@ class NotifyTestCase(BaseTestCase):
self.assertIsInstance(kwargs["data"], bytes) self.assertIsInstance(kwargs["data"], bytes)
@patch("hc.api.transports.requests.request") @patch("hc.api.transports.requests.request")
def test_webhooks_handle_json_value(self, mock_request):
definition = {"url_down": "http://foo.com"}
def test_legacy_webhooks_handle_json_value(self, mock_request):
definition = {"url_down": "http://foo.com", "post_data": "", "headers": {}}
self._setup_data("webhook", json.dumps(definition)) self._setup_data("webhook", json.dumps(definition))
self.channel.notify(self.check) self.channel.notify(self.check)
@ -156,9 +156,24 @@ class NotifyTestCase(BaseTestCase):
"get", "http://foo.com", headers=headers, timeout=5 "get", "http://foo.com", headers=headers, timeout=5
) )
@patch("hc.api.transports.requests.request")
def test_legacy_webhooks_handle_json_up_event(self, mock_request):
definition = {"url_up": "http://bar", "post_data": "", "headers": {}}
self._setup_data("webhook", json.dumps(definition), status="up")
self.channel.notify(self.check)
headers = {"User-Agent": "healthchecks.io"}
mock_request.assert_called_with("get", "http://bar", headers=headers, timeout=5)
@patch("hc.api.transports.requests.request") @patch("hc.api.transports.requests.request")
def test_webhooks_handle_json_up_event(self, mock_request): def test_webhooks_handle_json_up_event(self, mock_request):
definition = {"url_up": "http://bar"}
definition = {
"method_up": "GET",
"url_up": "http://bar",
"body_up": "",
"headers_up": {}
}
self._setup_data("webhook", json.dumps(definition), status="up") self._setup_data("webhook", json.dumps(definition), status="up")
self.channel.notify(self.check) self.channel.notify(self.check)
@ -167,7 +182,7 @@ class NotifyTestCase(BaseTestCase):
mock_request.assert_called_with("get", "http://bar", headers=headers, timeout=5) mock_request.assert_called_with("get", "http://bar", headers=headers, timeout=5)
@patch("hc.api.transports.requests.request") @patch("hc.api.transports.requests.request")
def test_webhooks_handle_post_headers(self, mock_request):
def test_legacy_webhooks_handle_post_headers(self, mock_request):
definition = { definition = {
"url_down": "http://foo.com", "url_down": "http://foo.com",
"post_data": "data", "post_data": "data",
@ -183,9 +198,27 @@ class NotifyTestCase(BaseTestCase):
) )
@patch("hc.api.transports.requests.request") @patch("hc.api.transports.requests.request")
def test_webhooks_handle_get_headers(self, mock_request):
def test_webhooks_handle_post_headers(self, mock_request):
definition = {
"method_down": "POST",
"url_down": "http://foo.com",
"body_down": "data",
"headers_down": {"Content-Type": "application/json"},
}
self._setup_data("webhook", json.dumps(definition))
self.channel.notify(self.check)
headers = {"User-Agent": "healthchecks.io", "Content-Type": "application/json"}
mock_request.assert_called_with(
"post", "http://foo.com", data=b"data", headers=headers, timeout=5
)
@patch("hc.api.transports.requests.request")
def test_legacy_webhooks_handle_get_headers(self, mock_request):
definition = { definition = {
"url_down": "http://foo.com", "url_down": "http://foo.com",
"post_data": "",
"headers": {"Content-Type": "application/json"}, "headers": {"Content-Type": "application/json"},
} }
@ -198,9 +231,27 @@ class NotifyTestCase(BaseTestCase):
) )
@patch("hc.api.transports.requests.request") @patch("hc.api.transports.requests.request")
def test_webhooks_allow_user_agent_override(self, mock_request):
def test_webhooks_handle_get_headers(self, mock_request):
definition = { definition = {
"method_down": "GET",
"url_down": "http://foo.com", "url_down": "http://foo.com",
"body_down": "",
"headers_down": {"Content-Type": "application/json"},
}
self._setup_data("webhook", json.dumps(definition))
self.channel.notify(self.check)
headers = {"User-Agent": "healthchecks.io", "Content-Type": "application/json"}
mock_request.assert_called_with(
"get", "http://foo.com", headers=headers, timeout=5
)
@patch("hc.api.transports.requests.request")
def test_legacy_webhooks_allow_user_agent_override(self, mock_request):
definition = {
"url_down": "http://foo.com",
"post_data": "",
"headers": {"User-Agent": "My-Agent"}, "headers": {"User-Agent": "My-Agent"},
} }
@ -212,11 +263,30 @@ class NotifyTestCase(BaseTestCase):
"get", "http://foo.com", headers=headers, timeout=5 "get", "http://foo.com", headers=headers, timeout=5
) )
@patch("hc.api.transports.requests.request")
def test_webhooks_allow_user_agent_override(self, mock_request):
definition = {
"method_down": "GET",
"url_down": "http://foo.com",
"body_down": "",
"headers_down": {"User-Agent": "My-Agent"},
}
self._setup_data("webhook", json.dumps(definition))
self.channel.notify(self.check)
headers = {"User-Agent": "My-Agent"}
mock_request.assert_called_with(
"get", "http://foo.com", headers=headers, timeout=5
)
@patch("hc.api.transports.requests.request") @patch("hc.api.transports.requests.request")
def test_webhooks_support_variables_in_headers(self, mock_request): def test_webhooks_support_variables_in_headers(self, mock_request):
definition = { definition = {
"method_down": "GET",
"url_down": "http://foo.com", "url_down": "http://foo.com",
"headers": {"X-Message": "$NAME is DOWN"},
"body_down": "",
"headers_down": {"X-Message": "$NAME is DOWN"},
} }
self._setup_data("webhook", json.dumps(definition)) self._setup_data("webhook", json.dumps(definition))


+ 13
- 11
hc/api/transports.py View File

@ -178,22 +178,24 @@ class Webhook(HttpTransport):
return False return False
def notify(self, check): def notify(self, check):
url = self.channel.url_down
if check.status == "up":
url = self.channel.url_up
assert url
spec = self.channel.webhook_spec(check.status)
assert spec["url"]
url = self.prepare(url, check, urlencode=True)
url = self.prepare(spec["url"], check, urlencode=True)
headers = {} headers = {}
for key, value in self.channel.headers.items():
for key, value in spec["headers"].items():
headers[key] = self.prepare(value, check) headers[key] = self.prepare(value, check)
if self.channel.post_data:
payload = self.prepare(self.channel.post_data, check)
return self.post(url, data=payload.encode(), headers=headers)
else:
body = spec["body"]
if body:
body = self.prepare(body, check)
if spec["method"] == "GET":
return self.get(url, headers=headers) return self.get(url, headers=headers)
elif spec["method"] == "POST":
return self.post(url, data=body.encode(), headers=headers)
elif spec["method"] == "PUT":
return self.put(url, data=body.encode(), headers=headers)
class Slack(HttpTransport): class Slack(HttpTransport):


+ 42
- 31
hc/front/forms.py View File

@ -1,10 +1,11 @@
from datetime import timedelta as td from datetime import timedelta as td
import json import json
import re
from urllib.parse import quote, urlencode from urllib.parse import quote, urlencode
from django import forms from django import forms
from django.forms import URLField
from django.conf import settings from django.conf import settings
from django.core.exceptions import ValidationError
from django.core.validators import RegexValidator from django.core.validators import RegexValidator
from hc.front.validators import ( from hc.front.validators import (
CronExpressionValidator, CronExpressionValidator,
@ -14,6 +15,37 @@ from hc.front.validators import (
import requests import requests
class HeadersField(forms.Field):
message = """Use "Header-Name: value" pairs, one per line."""
def to_python(self, value):
if not value:
return {}
headers = {}
for line in value.split("\n"):
if not line.strip():
continue
if ":" not in value:
raise ValidationError(self.message)
n, v = line.split(":", maxsplit=1)
n, v = n.strip(), v.strip()
if not n or not v:
raise ValidationError(message=self.message)
headers[n] = v
return headers
def validate(self, value):
super().validate(value)
for k, v in value.items():
if len(k) > 1000 or len(v) > 1000:
raise ValidationError("Value too long")
class NameTagsForm(forms.Form): class NameTagsForm(forms.Form):
name = forms.CharField(max_length=100, required=False) name = forms.CharField(max_length=100, required=False)
tags = forms.CharField(max_length=500, required=False) tags = forms.CharField(max_length=500, required=False)
@ -68,49 +100,28 @@ class AddUrlForm(forms.Form):
value = forms.URLField(max_length=1000, validators=[WebhookValidator()]) value = forms.URLField(max_length=1000, validators=[WebhookValidator()])
_valid_header_name = re.compile(r"\A[^:\s][^:\r\n]*\Z").match
METHODS = ("GET", "POST", "PUT")
class AddWebhookForm(forms.Form): class AddWebhookForm(forms.Form):
error_css_class = "has-error" error_css_class = "has-error"
url_down = forms.URLField(
method_down = forms.ChoiceField(initial="GET", choices=zip(METHODS, METHODS))
body_down = forms.CharField(max_length=1000, required=False)
headers_down = HeadersField(required=False)
url_down = URLField(
max_length=1000, required=False, validators=[WebhookValidator()] max_length=1000, required=False, validators=[WebhookValidator()]
) )
method_up = forms.ChoiceField(initial="GET", choices=zip(METHODS, METHODS))
body_up = forms.CharField(max_length=1000, required=False)
headers_up = HeadersField(required=False)
url_up = forms.URLField( url_up = forms.URLField(
max_length=1000, required=False, validators=[WebhookValidator()] max_length=1000, required=False, validators=[WebhookValidator()]
) )
post_data = forms.CharField(max_length=1000, required=False)
def __init__(self, *args, **kwargs):
super(AddWebhookForm, self).__init__(*args, **kwargs)
self.invalid_header_names = set()
self.headers = {}
if "header_key[]" in self.data and "header_value[]" in self.data:
keys = self.data.getlist("header_key[]")
values = self.data.getlist("header_value[]")
for key, value in zip(keys, values):
if not key:
continue
if not _valid_header_name(key):
self.invalid_header_names.add(key)
self.headers[key] = value
def clean(self):
if self.invalid_header_names:
raise forms.ValidationError("Invalid header names")
return self.cleaned_data
def get_value(self): def get_value(self):
val = dict(self.cleaned_data)
val["headers"] = self.headers
return json.dumps(val, sort_keys=True)
return json.dumps(dict(self.cleaned_data), sort_keys=True)
phone_validator = RegexValidator( phone_validator = RegexValidator(


+ 5
- 0
hc/front/templatetags/hc_extras.py View File

@ -123,3 +123,8 @@ def fix_asterisks(s):
""" Prepend asterisks with "Combining Grapheme Joiner" characters. """ """ Prepend asterisks with "Combining Grapheme Joiner" characters. """
return s.replace("*", "\u034f*") return s.replace("*", "\u034f*")
@register.filter
def format_headers(headers):
return "\n".join("%s: %s" % (k, v) for k, v in headers.items())

+ 65
- 30
hc/front/tests/test_add_webhook.py View File

@ -8,24 +8,32 @@ class AddWebhookTestCase(BaseTestCase):
def test_instructions_work(self): def test_instructions_work(self):
self.client.login(username="[email protected]", password="password") self.client.login(username="[email protected]", password="password")
r = self.client.get(self.url) r = self.client.get(self.url)
self.assertContains(r, "Runs a HTTP GET or HTTP POST")
self.assertContains(r, "Executes an HTTP request")
def test_it_adds_two_webhook_urls_and_redirects(self): def test_it_adds_two_webhook_urls_and_redirects(self):
form = {"url_down": "http://foo.com", "url_up": "https://bar.com"}
form = {
"method_down": "GET",
"url_down": "http://foo.com",
"method_up": "GET",
"url_up": "https://bar.com",
}
self.client.login(username="[email protected]", password="password") self.client.login(username="[email protected]", password="password")
r = self.client.post(self.url, form) r = self.client.post(self.url, form)
self.assertRedirects(r, "/integrations/") self.assertRedirects(r, "/integrations/")
c = Channel.objects.get() c = Channel.objects.get()
self.assertEqual(
c.value,
'{"headers": {}, "post_data": "", "url_down": "http://foo.com", "url_up": "https://bar.com"}',
)
self.assertEqual(c.project, self.project) self.assertEqual(c.project, self.project)
self.assertEqual(c.down_webhook_spec["url"], "http://foo.com")
self.assertEqual(c.up_webhook_spec["url"], "https://bar.com")
def test_it_adds_webhook_using_team_access(self): def test_it_adds_webhook_using_team_access(self):
form = {"url_down": "http://foo.com", "url_up": "https://bar.com"}
form = {
"method_down": "GET",
"url_down": "http://foo.com",
"method_up": "GET",
"url_up": "https://bar.com",
}
# 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.
@ -34,10 +42,8 @@ class AddWebhookTestCase(BaseTestCase):
c = Channel.objects.get() c = Channel.objects.get()
self.assertEqual(c.project, self.project) self.assertEqual(c.project, self.project)
self.assertEqual(
c.value,
'{"headers": {}, "post_data": "", "url_down": "http://foo.com", "url_up": "https://bar.com"}',
)
self.assertEqual(c.down_webhook_spec["url"], "http://foo.com")
self.assertEqual(c.up_webhook_spec["url"], "https://bar.com")
def test_it_rejects_bad_urls(self): def test_it_rejects_bad_urls(self):
urls = [ urls = [
@ -52,7 +58,12 @@ class AddWebhookTestCase(BaseTestCase):
self.client.login(username="[email protected]", password="password") self.client.login(username="[email protected]", password="password")
for url in urls: for url in urls:
form = {"url_down": url, "url_up": ""}
form = {
"method_down": "GET",
"url_down": url,
"method_up": "GET",
"url_up": "",
}
r = self.client.post(self.url, form) r = self.client.post(self.url, form)
self.assertContains(r, "Enter a valid URL.", msg_prefix=url) self.assertContains(r, "Enter a valid URL.", msg_prefix=url)
@ -60,35 +71,41 @@ class AddWebhookTestCase(BaseTestCase):
self.assertEqual(Channel.objects.count(), 0) self.assertEqual(Channel.objects.count(), 0)
def test_it_handles_empty_down_url(self): def test_it_handles_empty_down_url(self):
form = {"url_down": "", "url_up": "http://foo.com"}
form = {
"method_down": "GET",
"url_down": "",
"method_up": "GET",
"url_up": "http://foo.com",
}
self.client.login(username="[email protected]", password="password") self.client.login(username="[email protected]", password="password")
self.client.post(self.url, form) self.client.post(self.url, form)
c = Channel.objects.get() c = Channel.objects.get()
self.assertEqual(
c.value,
'{"headers": {}, "post_data": "", "url_down": "", "url_up": "http://foo.com"}',
)
self.assertEqual(c.down_webhook_spec["url"], "")
self.assertEqual(c.up_webhook_spec["url"], "http://foo.com")
def test_it_adds_post_data(self):
form = {"url_down": "http://foo.com", "post_data": "hello"}
def test_it_adds_request_body(self):
form = {
"method_down": "POST",
"url_down": "http://foo.com",
"body_down": "hello",
"method_up": "GET",
}
self.client.login(username="[email protected]", password="password") self.client.login(username="[email protected]", password="password")
r = self.client.post(self.url, form) r = self.client.post(self.url, form)
self.assertRedirects(r, "/integrations/") self.assertRedirects(r, "/integrations/")
c = Channel.objects.get() c = Channel.objects.get()
self.assertEqual(
c.value,
'{"headers": {}, "post_data": "hello", "url_down": "http://foo.com", "url_up": ""}',
)
self.assertEqual(c.down_webhook_spec["body"], "hello")
def test_it_adds_headers(self): def test_it_adds_headers(self):
form = { form = {
"method_down": "GET",
"url_down": "http://foo.com", "url_down": "http://foo.com",
"header_key[]": ["test", "test2"],
"header_value[]": ["123", "abc"],
"headers_down": "test:123\ntest2:abc",
"method_up": "GET",
} }
self.client.login(username="[email protected]", password="password") self.client.login(username="[email protected]", password="password")
@ -96,16 +113,34 @@ class AddWebhookTestCase(BaseTestCase):
self.assertRedirects(r, "/integrations/") self.assertRedirects(r, "/integrations/")
c = Channel.objects.get() c = Channel.objects.get()
self.assertEqual(c.headers, {"test": "123", "test2": "abc"})
self.assertEqual(
c.down_webhook_spec["headers"], {"test": "123", "test2": "abc"}
)
def test_it_rejects_bad_header_names(self):
def test_it_rejects_bad_headers(self):
self.client.login(username="[email protected]", password="password") self.client.login(username="[email protected]", password="password")
form = { form = {
"method_down": "GET",
"url_down": "http://example.org", "url_down": "http://example.org",
"header_key[]": ["ill:egal"],
"header_value[]": ["123"],
"headers_down": "invalid-headers",
"method_up": "GET",
} }
r = self.client.post(self.url, form) r = self.client.post(self.url, form)
self.assertContains(r, "Please use valid HTTP header names.")
self.assertContains(r, """invalid-headers""")
self.assertEqual(Channel.objects.count(), 0) self.assertEqual(Channel.objects.count(), 0)
def test_it_strips_headers(self):
form = {
"method_down": "GET",
"url_down": "http://foo.com",
"headers_down": " test : 123 ",
"method_up": "GET",
}
self.client.login(username="[email protected]", password="password")
r = self.client.post(self.url, form)
self.assertRedirects(r, "/integrations/")
c = Channel.objects.get()
self.assertEqual(c.down_webhook_spec["headers"], {"test": "123"})

+ 36
- 0
static/css/add_webhook.css View File

@ -2,3 +2,39 @@
border-color: #a94442; border-color: #a94442;
} }
#add-webhook-form div.bootstrap-select {
width: 100px;
}
.method-url-group {
display: flex;
}
.method-url-group > div.dropdown button {
border-right: 0;
border-top-right-radius: 0;
border-bottom-right-radius: 0;
}
.method-url-group input {
z-index: 1;
border-top-left-radius: 0;
border-bottom-left-radius: 0;
}
#webhook-variables tr:first-child th, #webhook-variables tr:first-child td {
border-top: 0;
}
#webhook-variables th {
white-space: nowrap;
}
.label-down {
color: #d9534f;
}
.label-up {
color: #5cb85c
}

+ 11
- 21
static/js/webhook.js View File

@ -1,25 +1,15 @@
$(function() { $(function() {
function haveBlankHeaderForm() {
return $("#webhook-headers .webhook-header").filter(function() {
var key = $(".key", this).val();
var value = $(".value", this).val();
return !key && !value;
}).length;
}
$("#method-down").change(function() {
var method = this.value;
$("#body-down-group").toggle(method != "GET");
});
function ensureBlankHeaderForm() {
if (!haveBlankHeaderForm()) {
var tmpl = $("#header-template").html();
$("#webhook-headers").append(tmpl);
}
}
$("#method-up").change(function() {
var method = this.value;
$("#body-up-group").toggle(method != "GET");
});
$("#webhook-headers").on("click", "button", function(e) {
e.preventDefault();
$(this).closest(".webhook-header").remove();
ensureBlankHeaderForm();
})
$("#webhook-headers").on("keyup", "input", ensureBlankHeaderForm);
ensureBlankHeaderForm();
// On page load, check if we need to show "request body" fields
$("#method-down").trigger("change");
$("#method-up").trigger("change");
}); });

+ 29
- 21
templates/front/channels.html View File

@ -398,30 +398,38 @@
</div> </div>
{% if ch.kind == "webhook" %} {% if ch.kind == "webhook" %}
{% with ch.down_webhook_spec as spec %}
{% if spec.url %}
<p><strong>Execute on "down" events:</strong></p>
<pre>{{ spec.method }} {{ spec.url }}</pre>
{% if spec.body %}
<p>Request Body</p>
<pre>{{ spec.body }}</pre>
{% endif %}
{% 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 %}
{% if spec.headers %}
<p>Request Headers</p>
<pre>{{ spec.headers|format_headers }}</pre>
{% endif %}
{% endif %}
{% endwith %}
{% with ch.up_webhook_spec as spec %}
{% if spec.url %}
<p><strong>Execute on "up" events:</strong></p>
<pre>{{ spec.method }} {{ spec.url }}</pre>
{% if spec.body %}
<p>Request Body</p>
<pre>{{ spec.body }}</pre>
{% endif %}
{% for k, v in ch.headers.items %}
<p><strong>Header <code>{{ k }}</code></strong></p>
<pre>{{ v }}</pre>
{% endfor %}
{% if spec.headers %}
<p>Request Headers</p>
<pre>{{ spec.headers|format_headers }}</pre>
{% endif %}
{% endif %}
{% endwith %}
{% endif %} {% endif %}
</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>


+ 158
- 125
templates/integrations/add_webhook.html View File

@ -6,168 +6,201 @@
{% block content %} {% block content %}
<div class="row"> <div class="row">
<div class="col-sm-12">
<h1>Webhook</h1>
<p>Runs a HTTP GET or HTTP POST to your specified URL when a check
goes up or down. Uses GET by default, and uses POST if you specify
any POST data.</p>
<p>You can use the following variables in webhook URLs:</p>
<table class="table webhook-variables">
<tr>
<th class="variable-column">Variable</th>
<td>Will be replaced with…</td>
</tr>
<tr>
<th><code>$CODE</code></th>
<td>The UUID code of the check</td>
</tr>
<tr>
<th><code>$NAME</code></th>
<td>Name of the check</td>
</tr>
<tr>
<th><code>$NOW</code></th>
<td>
Current UTC time in ISO8601 format.
Example: "{{ now }}"
</td>
</tr>
<tr>
<th><code>$STATUS</code></th>
<td>Check's current status ("up" or "down")</td>
</tr>
<tr>
<th><code>$TAG1, $TAG2, …</code></th>
<td>Value of the first tag, the second tag, …</td>
</tr>
</table>
<p>For example, a callback URL using variables might look like so:
<pre>http://requestb.in/1hhct291?message=<strong>$NAME</strong>:<strong>$STATUS</strong></pre>
<p>
After encoding and replacing the variables, {% site_name %} would then call:
</p>
<pre>http://requestb.in/1hhct291?message=<strong>My%20Check</strong>:<strong>down</strong></pre>
<h2>Integration Settings</h2>
<form method="post" class="form-horizontal">
<div class="col-sm-12">
<h1>Webhook</h1>
<p>Executes an HTTP request to your specified URL when a check
goes up or down.</p>
<p>
You can use placeholders <strong>$NAME</strong>, <strong>$STATUS</strong> and others in webhook URLs,
request body and header values
<a href="#" data-toggle="modal" data-target="#reference-modal">(quick reference)</a>.
</p>
<form id="add-webhook-form" method="post">
{% csrf_token %} {% csrf_token %}
<input type="hidden" name="kind" value="webhook" />
<div class="form-group {{ form.url_down.css_classes }}">
<label class="col-sm-2 control-label">URL for "down" events</label>
<div class="col-sm-10">
<input
type="text"
class="form-control"
name="url_down"
placeholder="http://..."
value="{{ form.url_down.value|default:"" }}">
<div class="row">
<div class="col-sm-6">
<h2>Execute when a check goes <strong class="label-down">down</strong></h2>
<br />
<div class="form-group {{ form.url_down.css_classes }}">
<label>URL</label>
<div class="method-url-group">
<select id="method-down" name="method_down" class="selectpicker">
<option{% if form.method_down.value == "GET" %} selected{% endif %}>GET</option>
<option{% if form.method_down.value == "POST" %} selected{% endif %}>POST</option>
<option{% if form.method_down.value == "PUT" %} selected{% endif %}>PUT</option>
</select>
<input
name="url_down"
value="{{ form.url_down.value|default:"" }}"
type="text"
class="form-control"
placeholder="https://..." />
</div>
{% if form.url_down.errors %} {% if form.url_down.errors %}
<div class="help-block"> <div class="help-block">
{{ form.url_down.errors|join:"" }} {{ form.url_down.errors|join:"" }}
</div> </div>
{% endif %} {% endif %}
</div> </div>
</div>
<div class="form-group {{ form.url_up.css_classes }}">
<label class="col-sm-2 control-label">URL for "up" events</label>
<div class="col-sm-10">
<input
type="text"
<div id="body-down-group" class="form-group {{ form.body_down.css_classes }}" style="display: none">
<label class="control-label">Request Body</label>
<textarea
class="form-control"
rows="3"
name="body_down"
placeholder='{"status": "$STATUS"}'>{{ form.body_down.value|default:"" }}</textarea>
{% if form.body_down.errors %}
<div class="help-block">
{{ form.body_down.errors|join:"" }}
</div>
{% endif %}
</div>
<div class="form-group {{ form.headers_down.css_classes }}">
<label class="control-label">Request Headers</label>
<textarea
class="form-control" class="form-control"
name="url_up"
placeholder="http://..."
value="{{ form.url_up.value|default:"" }}">
rows="3"
name="headers_down"
placeholder="X-Sample-Header: $NAME has gone down">{{ form.headers_down.value|default:"" }}</textarea>
<div class="help-block">
{% if form.headers_down.errors %}
{{ form.headers_down.errors|join:"" }}
{% else %}
Optional "Header-Name: value" pairs, one pair per line.
{% endif %}
</div>
</div>
</div>
<div class="col-sm-6">
<h2>Execute when a check goes <strong class="label-up">up</strong></h2>
<br />
<div class="form-group {{ form.url_up.css_classes }}">
<label>URL</label>
<div class="method-url-group">
<select id="method-up" name="method_up" class="selectpicker">
<option{% if form.method_up.value == "GET" %} selected{% endif %}>GET</option>
<option{% if form.method_up.value == "POST" %} selected{% endif %}>POST</option>
<option{% if form.method_up.value == "PUT" %} selected{% endif %}>PUT</option>
</select>
<input
name="url_up"
value="{{ form.url_up.value|default:"" }}"
type="text"
class="form-control"
placeholder="https://..." />
</div>
{% if form.url_up.errors %} {% if form.url_up.errors %}
<div class="help-block"> <div class="help-block">
{{ form.url_up.errors|join:"" }} {{ form.url_up.errors|join:"" }}
</div> </div>
{% endif %} {% endif %}
</div> </div>
</div>
<div class="form-group {{ form.post_data.css_classes }}">
<label class="col-sm-2 control-label">POST data</label>
<div class="col-sm-10">
<input
type="text"
<div id="body-up-group" class="form-group {{ form.body_up.css_classes }}" style="display: none">
<label class="control-label">Request Body</label>
<textarea
class="form-control" class="form-control"
name="post_data"
placeholder='{"status": "$STATUS"}'
value="{{ form.post_data.value|default:"" }}">
rows="3"
name="body_up"
placeholder='{"status": "$STATUS"}'>{{ form.body_up.value|default:"" }}</textarea>
{% if form.post_data.errors %} {% if form.post_data.errors %}
<div class="help-block"> <div class="help-block">
{{ form.post_data.errors|join:"" }} {{ form.post_data.errors|join:"" }}
</div> </div>
{% endif %} {% endif %}
</div> </div>
</div>
<div class="form-group">
<label class="col-sm-2 control-label">Request Headers</label>
<div class="col-xs-12 col-sm-10">
<div id="webhook-headers">
{% for k, v in form.headers.items %}
<div class="form-inline webhook-header">
<input
type="text"
class="form-control key {% if k in form.invalid_header_names %}error{% endif %}"
name="header_key[]"
placeholder="Content-Type"
value="{{ k }}" />
<input
type="text"
class="form-control value"
name="header_value[]"
placeholder="application/json"
value="{{ v }}" />
<button class="btn btn-default" type="button">
<span class="icon-delete"></span>
</button>
</div>
{% endfor %}
</div>
{% if form.invalid_header_names %}
<div class="text-danger">
Please use valid HTTP header names.
<div class="form-group {{ form.headers_up.css_classes }}">
<label class="control-label">Request Headers</label>
<textarea
class="form-control"
rows="3"
name="headers_up"
placeholder="X-Sample-Header: $NAME is back up">{{ form.headers_up.value|default:"" }}</textarea>
<div class="help-block">
{% if form.headers_up.errors %}
{{ form.headers_up.errors|join:"" }}
{% else %}
Optional "Header-Name: value" pairs, one pair per line.
{% endif %}
</div> </div>
{% endif %}
</div> </div>
</div> </div>
<div class="form-group">
<div class="col-sm-offset-2 col-sm-10">
</div>
<div class="form-group" class="clearfix">
<br>
<br>
<div class="text-right">
<button type="submit" class="btn btn-primary">Save Integration</button> <button type="submit" class="btn btn-primary">Save Integration</button>
</div> </div>
</div> </div>
</form>
</div>
</form>
</div>
</div> </div>
<div id="header-template" class="hide">
<div class="form-inline webhook-header">
<input
type="text"
class="form-control key"
name="header_key[]"
placeholder="Content-Type" />
<input
type="text"
class="form-control value"
name="header_value[]"
placeholder="application/json" />
<button class="btn btn-default" type="button">
<span class="icon-delete"></span>
</button>
<div id="reference-modal" class="modal">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h4>Supported Placeholders</h4>
</div>
<div class="modal-body">
<p>
You can use the below placeholders in webhook URL, request body
and header values. {% site_name %} will replace the placeholders
with the correct values.
</p>
<table id="webhook-variables" class="table modal-body">
<tr>
<th><code>$CODE</code></th>
<td>The UUID code of the check</td>
</tr>
<tr>
<th><code>$NAME</code></th>
<td>Name of the check</td>
</tr>
<tr>
<th><code>$NOW</code></th>
<td>
Current UTC time in ISO8601 format.
Example: "{{ now }}"
</td>
</tr>
<tr>
<th><code>$STATUS</code></th>
<td>Check's current status ("up" or "down")</td>
</tr>
<tr>
<th><code>$TAG1, $TAG2, …</code></th>
<td>Value of the first tag, the second tag, …</td>
</tr>
</table>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-default" data-dismiss="modal">Got It!</button>
</div>
</div>
</div> </div>
</div> </div>
{% endblock %} {% endblock %}
{% block scripts %} {% block scripts %}
{% compress js %} {% compress js %}
<script src="{% static 'js/jquery-2.1.4.min.js' %}"></script> <script src="{% static 'js/jquery-2.1.4.min.js' %}"></script>
<script src="{% static 'js/bootstrap.min.js' %}"></script> <script src="{% static 'js/bootstrap.min.js' %}"></script>
<script src="{% static 'js/bootstrap-select.min.js' %}"></script>
<script src="{% static 'js/webhook.js' %}"></script> <script src="{% static 'js/webhook.js' %}"></script>
{% endcompress %} {% endcompress %}
{% endblock %} {% endblock %}

Loading…
Cancel
Save