0
0
Fork 0
mirror of https://github.com/healthchecks/healthchecks.git synced 2025-03-15 04:34:49 +00:00

Webhooks support PUT method.

.Webhooks can have different request bodies and headers for "up" and "events".
This commit is contained in:
Pēteris Caune 2019-05-28 14:25:29 +03:00
parent 8f6726d1ee
commit d054970b02
No known key found for this signature in database
GPG key ID: E28D7679E9A9EDE2
11 changed files with 486 additions and 276 deletions

View file

@ -6,6 +6,8 @@ All notable changes to this project will be documented in this file.
### Improvements
- Add the `prunetokenbucket` management command
- 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
- Fix badges for tags containing special characters (#240, #237)

View file

@ -360,44 +360,62 @@ class Channel(models.Model):
prio = int(parts[1])
return PO_PRIORITIES[prio]
@property
def url_down(self):
def webhook_spec(self, status):
assert self.kind == "webhook"
if not self.value.startswith("{"):
parts = self.value.split("\n")
return parts[0]
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)
return doc.get("url_down")
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
def down_webhook_spec(self):
return self.webhook_spec("down")
@property
def up_webhook_spec(self):
return self.webhook_spec("up")
@property
def url_down(self):
return self.down_webhook_spec["url"]
@property
def url_up(self):
assert self.kind == "webhook"
if not self.value.startswith("{"):
parts = self.value.split("\n")
return parts[1] if len(parts) > 1 else ""
doc = json.loads(self.value)
return doc.get("url_up")
@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 ""
doc = json.loads(self.value)
return doc.get("post_data")
@property
def headers(self):
assert self.kind == "webhook"
if not self.value.startswith("{"):
return {}
doc = json.loads(self.value)
return doc.get("headers", {})
return self.up_webhook_spec["url"]
@property
def slack_team(self):

View file

@ -146,8 +146,8 @@ class NotifyTestCase(BaseTestCase):
self.assertIsInstance(kwargs["data"], bytes)
@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.channel.notify(self.check)
@ -157,8 +157,8 @@ class NotifyTestCase(BaseTestCase):
)
@patch("hc.api.transports.requests.request")
def test_webhooks_handle_json_up_event(self, mock_request):
definition = {"url_up": "http://bar"}
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)
@ -167,7 +167,22 @@ class NotifyTestCase(BaseTestCase):
mock_request.assert_called_with("get", "http://bar", headers=headers, timeout=5)
@patch("hc.api.transports.requests.request")
def test_webhooks_handle_post_headers(self, mock_request):
def test_webhooks_handle_json_up_event(self, mock_request):
definition = {
"method_up": "GET",
"url_up": "http://bar",
"body_up": "",
"headers_up": {}
}
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")
def test_legacy_webhooks_handle_post_headers(self, mock_request):
definition = {
"url_down": "http://foo.com",
"post_data": "data",
@ -183,9 +198,27 @@ class NotifyTestCase(BaseTestCase):
)
@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 = {
"url_down": "http://foo.com",
"post_data": "",
"headers": {"Content-Type": "application/json"},
}
@ -198,9 +231,27 @@ class NotifyTestCase(BaseTestCase):
)
@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 = {
"method_down": "GET",
"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"},
}
@ -212,11 +263,30 @@ class NotifyTestCase(BaseTestCase):
"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")
def test_webhooks_support_variables_in_headers(self, mock_request):
definition = {
"method_down": "GET",
"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))

View file

@ -178,22 +178,24 @@ class Webhook(HttpTransport):
return False
def notify(self, check):
url = self.channel.url_down
if check.status == "up":
url = self.channel.url_up
spec = self.channel.webhook_spec(check.status)
assert spec["url"]
assert url
url = self.prepare(url, check, urlencode=True)
url = self.prepare(spec["url"], check, urlencode=True)
headers = {}
for key, value in self.channel.headers.items():
for key, value in spec["headers"].items():
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)
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):

View file

@ -1,10 +1,11 @@
from datetime import timedelta as td
import json
import re
from urllib.parse import quote, urlencode
from django import forms
from django.forms import URLField
from django.conf import settings
from django.core.exceptions import ValidationError
from django.core.validators import RegexValidator
from hc.front.validators import (
CronExpressionValidator,
@ -14,6 +15,37 @@ from hc.front.validators import (
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):
name = forms.CharField(max_length=100, 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()])
_valid_header_name = re.compile(r"\A[^:\s][^:\r\n]*\Z").match
METHODS = ("GET", "POST", "PUT")
class AddWebhookForm(forms.Form):
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()]
)
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(
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):
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(

View file

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

View file

@ -8,24 +8,32 @@ class AddWebhookTestCase(BaseTestCase):
def test_instructions_work(self):
self.client.login(username="alice@example.org", password="password")
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):
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="alice@example.org", password="password")
r = self.client.post(self.url, form)
self.assertRedirects(r, "/integrations/")
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.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):
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
# should work.
@ -34,10 +42,8 @@ class AddWebhookTestCase(BaseTestCase):
c = Channel.objects.get()
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):
urls = [
@ -52,7 +58,12 @@ class AddWebhookTestCase(BaseTestCase):
self.client.login(username="alice@example.org", password="password")
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)
self.assertContains(r, "Enter a valid URL.", msg_prefix=url)
@ -60,35 +71,41 @@ class AddWebhookTestCase(BaseTestCase):
self.assertEqual(Channel.objects.count(), 0)
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="alice@example.org", password="password")
self.client.post(self.url, form)
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="alice@example.org", password="password")
r = self.client.post(self.url, form)
self.assertRedirects(r, "/integrations/")
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):
form = {
"method_down": "GET",
"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="alice@example.org", password="password")
@ -96,16 +113,34 @@ class AddWebhookTestCase(BaseTestCase):
self.assertRedirects(r, "/integrations/")
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="alice@example.org", password="password")
form = {
"method_down": "GET",
"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)
self.assertContains(r, "Please use valid HTTP header names.")
self.assertContains(r, """invalid-headers""")
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="alice@example.org", 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"})

View file

@ -2,3 +2,39 @@
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
}

View file

@ -1,25 +1,15 @@
$(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");
});

View file

@ -398,30 +398,38 @@
</div>
{% 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>
{% 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 %}
{% if spec.headers %}
<p>Request Headers</p>
<pre>{{ spec.headers|format_headers }}</pre>
{% endif %}
{% endif %}
{% endwith %}
{% 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>

View file

@ -6,168 +6,201 @@
{% block content %}
<div class="row">
<div class="col-sm-12">
<h1>Webhook</h1>
<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>Executes an HTTP request to your specified URL when a check
goes up or down.</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>
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>
<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">
<form id="add-webhook-form" method="post">
{% 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 %}
<div class="help-block">
{{ form.url_down.errors|join:"" }}
</div>
{% endif %}
</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"
name="url_up"
placeholder="http://..."
value="{{ form.url_up.value|default:"" }}">
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"
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 %}
<div class="help-block">
{{ form.url_up.errors|join:"" }}
</div>
{% endif %}
</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"
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 %}
<div class="help-block">
{{ form.post_data.errors|join:"" }}
</div>
{% endif %}
</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 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>
{% if form.invalid_header_names %}
<div class="text-danger">
Please use valid HTTP header names.
</div>
{% endif %}
</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>
</div>
</div>
</form>
</div>
</div>
</form>
<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>
</div>
<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>
{% endblock %}
{% block scripts %}
{% compress js %}
<script src="{% static 'js/jquery-2.1.4.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>
{% endcompress %}
{% endblock %}