mirror of
https://github.com/healthchecks/healthchecks.git
synced 2025-01-11 03:18:33 +00:00
* MVP for notification groups. * Addressed review comments. * Push notification group to the front. * Updated icons. * Reduce code duplication. * Show groups at the top. * Add label to group forms. * Add checkboxes for integration selection. * CSS for checkboxes. * Added tests for group notify.
This commit is contained in:
parent
24bad5af52
commit
7057f6d3a5
18 changed files with 294 additions and 21 deletions
hc
static
css
fonts
img/integrations
js
templates
|
@ -45,6 +45,7 @@ TRANSPORTS: dict[str, tuple[str, type[transports.Transport]]] = {
|
|||
"discord": ("Discord", transports.Discord),
|
||||
"email": ("Email", transports.Email),
|
||||
"gotify": ("Gotify", transports.Gotify),
|
||||
"group": ("Group", transports.Group),
|
||||
"hipchat": ("HipChat", transports.RemovedTransport),
|
||||
"linenotify": ("LINE Notify", transports.LineNotify),
|
||||
"matrix": ("Matrix", transports.Matrix),
|
||||
|
@ -793,7 +794,15 @@ class Channel(models.Model):
|
|||
return {"id": str(self.code), "name": self.name, "kind": self.kind}
|
||||
|
||||
def is_editable(self) -> bool:
|
||||
return self.kind in ("email", "webhook", "sms", "signal", "whatsapp", "ntfy")
|
||||
return self.kind in (
|
||||
"email",
|
||||
"webhook",
|
||||
"sms",
|
||||
"signal",
|
||||
"whatsapp",
|
||||
"ntfy",
|
||||
"group",
|
||||
)
|
||||
|
||||
def assign_all_checks(self) -> None:
|
||||
checks = Check.objects.filter(project=self.project)
|
||||
|
@ -1043,6 +1052,13 @@ class Channel(models.Model):
|
|||
gotify_url = json_property("gotify", "url")
|
||||
gotify_token = json_property("gotify", "token")
|
||||
|
||||
@property
|
||||
def group_channels(self) -> QuerySet[Channel]:
|
||||
assert self.kind == "group"
|
||||
return Channel.objects.filter(
|
||||
project=self.project, code__in=self.value.split(",")
|
||||
)
|
||||
|
||||
ntfy_topic = json_property("ntfy", "topic")
|
||||
ntfy_url = json_property("ntfy", "url")
|
||||
ntfy_priority = json_property("ntfy", "priority")
|
||||
|
|
72
hc/api/tests/test_notify_group.py
Normal file
72
hc/api/tests/test_notify_group.py
Normal file
|
@ -0,0 +1,72 @@
|
|||
# coding: utf-8
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from datetime import timedelta as td
|
||||
from unittest.mock import Mock, patch
|
||||
|
||||
from django.utils.timezone import now
|
||||
from django.test.utils import override_settings
|
||||
|
||||
from hc.api.models import Channel, Check, Notification
|
||||
from hc.test import BaseTestCase
|
||||
|
||||
|
||||
class NotifyGroupTestCase(BaseTestCase):
|
||||
def setUp(self) -> None:
|
||||
super().setUp()
|
||||
|
||||
self.check = Check(project=self.project)
|
||||
self.check.name = "Foobar"
|
||||
self.check.status = "down"
|
||||
self.check.last_ping = now() - td(minutes=61)
|
||||
self.check.save()
|
||||
|
||||
self.channel_shell = Channel(project=self.project)
|
||||
self.channel_shell.kind = "shell"
|
||||
self.channel_shell.value = json.dumps({"cmd_down": "", "cmd_up": ""})
|
||||
self.channel_shell.save()
|
||||
|
||||
self.channel_zulip = Channel(project=self.project)
|
||||
self.channel_zulip.kind = "zulip"
|
||||
self.channel_zulip.value = json.dumps(
|
||||
{
|
||||
"bot_email": "bot@example.org",
|
||||
"api_key": "fake-key",
|
||||
"mtype": "stream",
|
||||
"to": "general",
|
||||
}
|
||||
)
|
||||
self.channel_zulip.save()
|
||||
self.channel_zulip.checks.add(self.check)
|
||||
|
||||
self.channel = Channel(project=self.project)
|
||||
self.channel.kind = "group"
|
||||
self.channel.value = ",".join(
|
||||
[str(self.channel_shell.code), str(self.channel_zulip.code)]
|
||||
)
|
||||
self.channel.save()
|
||||
self.channel.checks.add(self.check)
|
||||
|
||||
def test_it_invalid_group_ids(self) -> None:
|
||||
self.channel.value = (
|
||||
"bda20a83-409c-4b2c-8e9b-589d408cd57b,40500bf8-0f37-4bb3-970c-9fe64b7ef39d"
|
||||
)
|
||||
self.channel.save()
|
||||
|
||||
self.channel.notify(self.check)
|
||||
|
||||
assert Notification.objects.count() == 1
|
||||
assert self.channel.last_error == ""
|
||||
|
||||
@patch("hc.api.transports.curl.request")
|
||||
@patch("hc.api.transports.os.system")
|
||||
@override_settings(SHELL_ENABLED=True)
|
||||
def test_it_group(self, mock_system: Mock, mock_post: Mock) -> None:
|
||||
mock_system.return_value = 123
|
||||
mock_post.return_value.status_code = 200
|
||||
self.channel.notify(self.check)
|
||||
|
||||
self.channel.refresh_from_db()
|
||||
assert self.channel.last_error == "1 out of 2 notifications failed"
|
|
@ -1214,6 +1214,20 @@ class Gotify(HttpTransport):
|
|||
self.post(url, json=payload)
|
||||
|
||||
|
||||
class Group(Transport):
|
||||
def notify(self, check: Check, notification: Notification | None = None) -> None:
|
||||
channels = self.channel.group_channels
|
||||
error_count = 0
|
||||
for channel in channels:
|
||||
error = channel.notify(check)
|
||||
if error:
|
||||
error_count += 1
|
||||
if error_count:
|
||||
raise TransportError(
|
||||
f"{error_count} out of {len(channels)} notifications failed"
|
||||
)
|
||||
|
||||
|
||||
class Ntfy(HttpTransport):
|
||||
def priority(self, check: Check) -> int:
|
||||
if check.status == "up":
|
||||
|
|
|
@ -10,7 +10,10 @@ from urllib.parse import quote, urlencode
|
|||
from django import forms
|
||||
from django.conf import settings
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.utils.html import format_html
|
||||
from django.utils.safestring import mark_safe
|
||||
|
||||
from hc.api.models import Channel
|
||||
from hc.front.validators import (
|
||||
CronExpressionValidator,
|
||||
TimezoneValidator,
|
||||
|
@ -335,6 +338,27 @@ class AddGotifyForm(forms.Form):
|
|||
return json.dumps(dict(self.cleaned_data), sort_keys=True)
|
||||
|
||||
|
||||
class GroupForm(forms.Form):
|
||||
def __init__(self, *args, **kwargs):
|
||||
project = kwargs.pop("project")
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
self.fields["channels"].choices = (
|
||||
(
|
||||
c.code,
|
||||
format_html('<span class="ic-{}"></span> {}', mark_safe(c.kind), c),
|
||||
)
|
||||
for c in Channel.objects.filter(project=project).exclude(kind="group")
|
||||
)
|
||||
|
||||
error_css_class = "has-error"
|
||||
label = forms.CharField(max_length=100, required=False)
|
||||
channels = forms.MultipleChoiceField(widget=forms.CheckboxSelectMultiple)
|
||||
|
||||
def get_value(self) -> str:
|
||||
return ",".join(self.cleaned_data["channels"])
|
||||
|
||||
|
||||
class NtfyForm(forms.Form):
|
||||
error_css_class = "has-error"
|
||||
topic = forms.CharField(max_length=50)
|
||||
|
|
|
@ -63,6 +63,7 @@ project_urls = [
|
|||
path("add_discord/", views.add_discord, name="hc-add-discord"),
|
||||
path("add_email/", views.add_email, name="hc-add-email"),
|
||||
path("add_gotify/", views.add_gotify, name="hc-add-gotify"),
|
||||
path("add_group/", views.add_group, name="hc-add-group"),
|
||||
path("add_linenotify/", views.add_linenotify, name="hc-add-linenotify"),
|
||||
path("add_matrix/", views.add_matrix, name="hc-add-matrix"),
|
||||
path("add_mattermost/", views.add_mattermost, name="hc-add-mattermost"),
|
||||
|
|
|
@ -24,7 +24,7 @@ from django.contrib.auth.decorators import login_required
|
|||
from django.contrib.auth.models import User
|
||||
from django.core import signing
|
||||
from django.core.exceptions import PermissionDenied
|
||||
from django.db.models import Count, F, QuerySet
|
||||
from django.db.models import Case, Count, F, QuerySet, When
|
||||
from django.http import (
|
||||
Http404,
|
||||
HttpRequest,
|
||||
|
@ -900,7 +900,12 @@ def details(request: AuthenticatedHttpRequest, code: UUID) -> HttpResponse:
|
|||
check.project.show_slugs = request.GET["urls"] == "slug"
|
||||
check.project.save()
|
||||
|
||||
channels = list(check.project.channel_set.order_by("created"))
|
||||
all_channels = check.project.channel_set.order_by("created")
|
||||
regular_channels = []
|
||||
group_channels = []
|
||||
for channel in all_channels:
|
||||
channels = group_channels if channel.kind == "group" else regular_channels
|
||||
channels.append(channel)
|
||||
|
||||
all_tags = set()
|
||||
q = Check.objects.filter(project=check.project).exclude(tags="")
|
||||
|
@ -912,7 +917,8 @@ def details(request: AuthenticatedHttpRequest, code: UUID) -> HttpResponse:
|
|||
"project": check.project,
|
||||
"check": check,
|
||||
"rw": rw,
|
||||
"channels": channels,
|
||||
"channels": regular_channels,
|
||||
"group_channels": group_channels,
|
||||
"enabled_channels": list(check.channel_set.all()),
|
||||
"timezones": all_timezones,
|
||||
"downtimes": check.downtimes(3, request.profile.tz),
|
||||
|
@ -1096,8 +1102,10 @@ def channels(request: AuthenticatedHttpRequest, code: UUID) -> HttpResponse:
|
|||
return redirect("hc-channels", project.code)
|
||||
|
||||
channels = Channel.objects.filter(project=project)
|
||||
channels = channels.order_by("created")
|
||||
channels = channels.annotate(n_checks=Count("checks"))
|
||||
channels = channels.annotate(
|
||||
is_group=Case(When(kind="group", then=0), default=1), n_checks=Count("checks")
|
||||
)
|
||||
channels = channels.order_by("is_group", "created")
|
||||
|
||||
ctx = {
|
||||
"page": "channels",
|
||||
|
@ -1312,16 +1320,18 @@ def edit_channel(request: AuthenticatedHttpRequest, code: UUID) -> HttpResponse:
|
|||
channel = _get_rw_channel_for_user(request, code)
|
||||
if channel.kind == "email":
|
||||
return email_form(request, channel)
|
||||
if channel.kind == "webhook":
|
||||
elif channel.kind == "webhook":
|
||||
return webhook_form(request, channel)
|
||||
if channel.kind == "sms":
|
||||
elif channel.kind == "sms":
|
||||
return sms_form(request, channel)
|
||||
if channel.kind == "signal":
|
||||
elif channel.kind == "signal":
|
||||
return signal_form(request, channel)
|
||||
if channel.kind == "whatsapp":
|
||||
elif channel.kind == "whatsapp":
|
||||
return whatsapp_form(request, channel)
|
||||
if channel.kind == "ntfy":
|
||||
elif channel.kind == "ntfy":
|
||||
return ntfy_form(request, channel)
|
||||
elif channel.kind == "group":
|
||||
return group_form(request, channel)
|
||||
|
||||
return HttpResponseBadRequest()
|
||||
|
||||
|
@ -2458,6 +2468,36 @@ def add_gotify(request: AuthenticatedHttpRequest, code: UUID) -> HttpResponse:
|
|||
return render(request, "integrations/add_gotify.html", ctx)
|
||||
|
||||
|
||||
def group_form(request: HttpRequest, channel: Channel) -> HttpResponse:
|
||||
adding = channel._state.adding
|
||||
if request.method == "POST":
|
||||
form = forms.GroupForm(request.POST, project=channel.project)
|
||||
if form.is_valid():
|
||||
channel.name = form.cleaned_data["label"]
|
||||
channel.value = form.get_value()
|
||||
channel.save()
|
||||
|
||||
if adding:
|
||||
channel.assign_all_checks()
|
||||
return redirect("hc-channels", channel.project.code)
|
||||
elif adding:
|
||||
form = forms.GroupForm(project=channel.project)
|
||||
else:
|
||||
# Filter out unavailable channsl
|
||||
channels = list(channel.group_channels.values_list("code", flat=True))
|
||||
form = forms.GroupForm({"channels": channels}, project=channel.project)
|
||||
|
||||
ctx = {"page": "channels", "project": channel.project, "form": form}
|
||||
return render(request, "integrations/group_form.html", ctx)
|
||||
|
||||
|
||||
@login_required
|
||||
def add_group(request: AuthenticatedHttpRequest, code: UUID) -> HttpResponse:
|
||||
project = _get_rw_project_for_user(request, code)
|
||||
channel = Channel(project=project, kind="group")
|
||||
return group_form(request, channel)
|
||||
|
||||
|
||||
def ntfy_form(request: HttpRequest, channel: Channel) -> HttpResponse:
|
||||
adding = channel._state.adding
|
||||
if request.method == "POST":
|
||||
|
|
|
@ -247,4 +247,20 @@ body.dark img.kind-matrix {
|
|||
|
||||
img.kind-ntfy {
|
||||
filter: drop-shadow(1px 1px 1px rgba(0,0,0,0.2));
|
||||
}
|
||||
|
||||
input[name="channels"] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
label[for^="id_channels"] {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
input[name="channels"]:not(:checked) ~ span:before {
|
||||
color: var(--channel-off-inside-color);
|
||||
}
|
||||
|
||||
input[name="channels"]:not(:checked) ~ span:after {
|
||||
color: var(--channel-off-color);
|
||||
}
|
|
@ -60,20 +60,20 @@
|
|||
color: #999;
|
||||
}
|
||||
|
||||
#details-integrations th {
|
||||
.details-integrations th {
|
||||
width: 50px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
#details-integrations .label {
|
||||
.details-integrations .label {
|
||||
background: #777777;
|
||||
}
|
||||
|
||||
#details-integrations .on .label {
|
||||
.details-integrations .on .label {
|
||||
background: #22bc66;
|
||||
}
|
||||
|
||||
#details-integrations.rw tr:hover th, #details-integrations.rw tr:hover td {
|
||||
.details-integrations.rw tr:hover th, .details-integrations.rw tr:hover td {
|
||||
cursor: pointer;
|
||||
background-color: var(--table-bg-hover);
|
||||
}
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
@font-face {
|
||||
font-family: 'icomoon';
|
||||
src:
|
||||
url('../fonts/icomoon.ttf?omvtqt') format('truetype'),
|
||||
url('../fonts/icomoon.woff?omvtqt') format('woff'),
|
||||
url('../fonts/icomoon.svg?omvtqt#icomoon') format('svg');
|
||||
url('../fonts/icomoon.ttf?fw1vj7') format('truetype'),
|
||||
url('../fonts/icomoon.woff?fw1vj7') format('woff'),
|
||||
url('../fonts/icomoon.svg?fw1vj7#icomoon') format('svg');
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
font-display: block;
|
||||
|
@ -24,6 +24,10 @@
|
|||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
.ic-group:after {
|
||||
content: "\e932";
|
||||
color: #f0ad4e;
|
||||
}
|
||||
.ic-rocketchat:before {
|
||||
content: "\e930";
|
||||
color: #f5455c;
|
||||
|
|
|
@ -69,4 +69,5 @@
|
|||
<glyph unicode="" d="M803.157 338.987l-66.347-22.229 23.040-68.693c9.301-27.733-5.76-57.813-33.536-67.157-6.4-1.92-12.16-2.859-18.56-2.688-21.803 0.64-41.643 14.421-49.28 36.224l-23.040 68.565-136.96-45.781 22.997-68.608c9.003-27.819-5.76-57.941-33.877-67.2-6.4-2.048-12.16-2.859-18.56-2.731-21.76 0.64-41.643 14.507-49.323 36.267l-22.997 69.077-66.603-22.357c-6.4-1.92-12.16-2.603-18.56-2.603-21.803 0.683-41.643 14.72-49.28 36.48-9.6 28.16 5.76 58.197 33.28 67.2l66.56 22.4-42.112 131.755-66.176-22.4c-6.016-2.048-12.16-2.816-18.261-2.731-21.12 0.683-41.6 14.421-48.683 36.181-8.917 27.733 5.76 57.771 33.963 67.157l66.56 22.187-23.040 68.48c-8.96 27.904 5.803 57.984 33.963 67.2 28.117 9.387 58.155-5.803 67.157-33.408l22.997-68.608 136.32 45.824-23.040 68.48c-8.917 27.52 5.76 57.6 33.664 67.157 27.819 9.003 57.984-5.76 67.2-33.749l23.040-69.163 66.347 21.76c27.776 9.344 57.856-5.76 67.2-33.237 9.301-27.904-5.76-57.984-33.451-67.2l-66.432-22.357 44.16-131.669 66.176 22.016c27.819 9.003 57.941-5.76 67.2-33.92 9.387-28.16-5.76-58.24-33.237-67.157zM981.12 567.51c-105.6 351.701-258.091 433.92-609.963 328.277-351.701-105.557-433.92-257.963-328.277-609.963 105.6-351.787 257.963-433.92 609.963-328.277 351.787 105.6 433.92 257.963 328.277 609.963zM421.504 469.547l44.16-131.627 136.747 45.824-44.16 131.157-136.747-46.080z" />
|
||||
<glyph unicode="" glyph-name="rocketchat" d="M977.361 602.351c-27.77 43.124-66.694 81.3-115.625 113.501-94.54 62.115-218.758 96.335-349.775 96.335-43.776 0-86.9-3.808-128.765-11.343-25.972 25.007-56.34 47.5-88.503 65.274-119.328 59.492-224.486 37.341-277.619 18.234-17.456-6.281-22.842-28.358-9.931-41.679 37.471-38.671 99.466-115.098 84.226-184.599-59.245-60.48-91.369-133.408-91.369-209.341 0-77.379 32.124-150.307 91.369-210.787 15.241-69.501-46.755-145.967-84.226-184.637-12.873-13.283-7.525-35.36 9.931-41.641 53.134-19.107 158.291-41.297 277.657 18.193 32.163 17.775 62.531 40.271 88.503 65.277 41.865-7.537 84.992-11.343 128.765-11.343 131.055 0 255.276 34.179 349.775 96.297 48.931 32.201 87.855 70.339 115.625 113.501 30.941 48.032 46.601 99.759 46.601 153.655-0.038 55.343-15.7 107.028-46.639 155.101zM506.615 173.431c-56.649 0-110.659 7.308-159.895 20.515l-35.983-34.598c-19.558-18.801-42.476-35.817-66.388-49.213-31.665-15.491-62.95-23.98-93.888-26.528 1.757 3.156 3.36 6.353 5.079 9.553 36.058 66.225 45.798 125.754 29.184 178.545-58.979 46.321-94.348 105.623-94.348 170.214 0 148.25 186.365 268.451 416.239 268.451 229.871 0 416.276-120.201 416.276-268.451 0-148.288-186.365-268.489-416.276-268.489zM307.491 558.929c-33.882 0-61.347-27.252-61.347-60.861s27.465-60.861 61.347-60.861c33.879 0 61.344 27.252 61.344 60.861s-27.465 60.861-61.344 60.861zM504.934 558.929c-33.879 0-61.344-27.252-61.344-60.861s27.465-60.861 61.344-60.861c33.882 0 61.347 27.252 61.347 60.861s-27.465 60.861-61.347 60.861zM702.415 558.929c-33.882 0-61.347-27.252-61.347-60.861s27.465-60.861 61.347-60.861c33.879 0 61.344 27.252 61.344 60.861s-27.465 60.861-61.344 60.861z" />
|
||||
<glyph unicode="" d="M896 938.667h-768c-70.699 0-128-57.301-128-128v-768c0-70.656 57.301-128 128-128h768c70.656 0 128 57.344 128 128v768c0 70.699-57.344 128-128 128zM445.44 162.987c0-33.92-27.52-61.44-61.44-61.44h-189.44c-33.92 0-61.44 27.563-61.44 61.44v581.12c0 33.92 27.52 61.44 61.44 61.44h189.44c33.92 0 61.44-27.52 61.44-61.44v-581.12zM890.88 418.987c0-33.877-27.52-61.44-61.44-61.44h-189.44c-33.92 0-61.44 27.563-61.44 61.44v325.12c0 33.92 27.563 61.44 61.44 61.44h189.44c33.92 0 61.44-27.52 61.44-61.44v-325.12z" />
|
||||
<glyph unicode="" glyph-name="group" d="M56.396 786.262c-28.625 0-51.828-23.207-51.831-51.831v-439.072l126.261 310.141c8.136 19.99 24.97 32.656 43.407 32.656h646.498v21.728c0 28.625-23.206 51.828-51.831 51.831h-369.431l-50.13 56.963c-9.843 11.18-24.020 17.586-38.916 17.584zM820.731 390.123l-115.611-274.87c-8.248-19.613-24.914-31.967-43.123-31.968h-622.301c5.246-1.788 10.849-2.804 16.7-2.804h712.504c28.625 0 51.828 23.206 51.831 51.831zM247.475 572.639c-19.758 0-37.798-11.232-46.517-28.963l-191.079-388.484c-16.937-34.445 8.133-74.71 46.517-74.71h713.789c19.514 0 37.374 10.959 46.213 28.356l197.416 388.484c17.52 34.481-7.527 75.31-46.203 75.318z" />
|
||||
</font></defs></svg>
|
Before (image error) Size: 52 KiB After (image error) Size: 53 KiB |
Binary file not shown.
Binary file not shown.
BIN
static/img/integrations/group.png
Normal file
BIN
static/img/integrations/group.png
Normal file
Binary file not shown.
After (image error) Size: 3.6 KiB |
|
@ -62,7 +62,7 @@ $(function () {
|
|||
}, 300);
|
||||
});
|
||||
|
||||
$("#details-integrations.rw tr").click(function() {
|
||||
$(".details-integrations.rw tr").click(function() {
|
||||
var isOn = $(this).toggleClass("on").hasClass("on");
|
||||
$(".label", this).text(isOn ? "ON" : "OFF");
|
||||
|
||||
|
|
|
@ -258,6 +258,13 @@
|
|||
<a href="{% url 'hc-add-gotify' project.code %}" class="btn btn-primary">Add Integration</a>
|
||||
</li>
|
||||
|
||||
<li>
|
||||
<img src="{% static 'img/integrations/group.png' %}" class="icon" alt="Group icon" />
|
||||
<h2>Group</h2>
|
||||
<p> Deliver to multiple integrations at once.</p>
|
||||
<a href="{% url 'hc-add-group' project.code %}" class="btn btn-primary">Add Integration</a>
|
||||
</li>
|
||||
|
||||
{% if enable_linenotify %}
|
||||
<li>
|
||||
<img src="{% static 'img/integrations/linenotify.png' %}"
|
||||
|
|
|
@ -207,9 +207,27 @@
|
|||
</div>
|
||||
|
||||
<div class="details-block">
|
||||
{% if group_channels %}
|
||||
<h2>Notification Groups</h2>
|
||||
<table class="details-integrations table {% if rw %}rw{% endif %}">
|
||||
{% for channel in group_channels %}
|
||||
<tr data-url="{% url 'hc-switch-channel' check.code channel.code %}" {% if channel in enabled_channels %}class="on"{% endif %}>
|
||||
<th>
|
||||
<span class="label">
|
||||
{% if channel in enabled_channels %}ON{% else %}OFF{% endif %}
|
||||
</span>
|
||||
</th>
|
||||
<td>
|
||||
<span class="ic-{{ channel.kind }}"></span>
|
||||
{{ channel }}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
{% endif %}
|
||||
<h2>Notification Methods</h2>
|
||||
{% if channels %}
|
||||
<table id="details-integrations" class="table {% if rw %}rw{% endif %}">
|
||||
<table class="details-integrations table {% if rw %}rw{% endif %}">
|
||||
{% for channel in channels %}
|
||||
<tr data-url="{% url 'hc-switch-channel' check.code channel.code %}" {% if channel in enabled_channels %}class="on"{% endif %}>
|
||||
<th>
|
||||
|
|
|
@ -82,7 +82,7 @@
|
|||
</div>
|
||||
</div>
|
||||
<div class="form-group {{ form.token.css_classes }}">
|
||||
<label for="url" class="col-sm-2 control-label">Application Token</label>
|
||||
<label for="token" class="col-sm-2 control-label">Application Token</label>
|
||||
<div class="col-sm-10">
|
||||
<input
|
||||
id="token"
|
||||
|
|
60
templates/integrations/group_form.html
Normal file
60
templates/integrations/group_form.html
Normal file
|
@ -0,0 +1,60 @@
|
|||
{% extends "base_project.html" %}
|
||||
{% load humanize static hc_extras %}
|
||||
|
||||
{% block title %}Group Integration for {{ site_name }}{% endblock %}
|
||||
|
||||
|
||||
{% block content %}
|
||||
<div class="row">
|
||||
<div class="col-sm-12">
|
||||
<h1>Group</h1>
|
||||
<p>Dispatch a notification to multiple integrations at once.</p>
|
||||
|
||||
<h2>Integration Settings</h2>
|
||||
|
||||
<form method="post" class="form-horizontal">
|
||||
{% csrf_token %}
|
||||
|
||||
<div class="form-group {{ form.label.css_classes }}">
|
||||
<label for="id_label" class="col-sm-2 control-label">Label</label>
|
||||
<div class="col-sm-6">
|
||||
<input
|
||||
id="id_label"
|
||||
type="text"
|
||||
class="form-control"
|
||||
name="label"
|
||||
placeholder="VIP group"
|
||||
value="{{ form.label.value|default:"" }}">
|
||||
|
||||
{% if form.label.errors %}
|
||||
<div class="help-block">
|
||||
{{ form.label.errors|join:"" }}
|
||||
</div>
|
||||
{% else %}
|
||||
<span class="help-block">
|
||||
Optional. If you add multiple groups,
|
||||
the labels will help you tell them apart.
|
||||
</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group {{ form.channels.css_classes }}">
|
||||
<label for="channels" class="col-sm-2 control-label">Integrations</label>
|
||||
<div class="col-sm-10">
|
||||
{{ form.channels }}
|
||||
|
||||
{% if form.channels.errors %}
|
||||
<div class="help-block">{{ form.channels.errors|join:"" }}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<div class="col-sm-offset-2 col-sm-10">
|
||||
<button type="submit" class="btn btn-primary">Save Integration</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
Loading…
Reference in a new issue