forked from GithubBackups/healthchecks
Notification Channels WIP
This commit is contained in:
parent
63bdd841fc
commit
061fc4f6a9
@ -9,7 +9,7 @@ from django.http import HttpResponseBadRequest
|
||||
from django.shortcuts import redirect, render
|
||||
|
||||
from hc.accounts.forms import EmailForm
|
||||
from hc.api.models import Check
|
||||
from hc.api.models import Channel, Check
|
||||
from hc.lib.emails import send
|
||||
|
||||
|
||||
@ -18,6 +18,13 @@ def _make_user(email):
|
||||
user = User(username=username, email=email)
|
||||
user.save()
|
||||
|
||||
channel = Channel()
|
||||
channel.user = user
|
||||
channel.kind = "email"
|
||||
channel.value = email
|
||||
channel.email_verified = True
|
||||
channel.save()
|
||||
|
||||
return user
|
||||
|
||||
|
||||
@ -29,6 +36,9 @@ def _associate_demo_check(request, user):
|
||||
if check.user is None:
|
||||
check.user = user
|
||||
check.save()
|
||||
|
||||
check.assign_all_channels()
|
||||
|
||||
del request.session["welcome_code"]
|
||||
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
from django.contrib import admin
|
||||
|
||||
from hc.api.models import Check, Ping
|
||||
from hc.api.models import Channel, Check, Ping
|
||||
|
||||
|
||||
class OwnershipListFilter(admin.SimpleListFilter):
|
||||
@ -55,4 +55,10 @@ class PingsAdmin(admin.ModelAdmin):
|
||||
return obj.owner.name if obj.owner.name else obj.owner.code
|
||||
|
||||
def email(self, obj):
|
||||
return obj.owner.user.email if obj.owner.user else None
|
||||
return obj.owner.user.email if obj.owner.user else None
|
||||
|
||||
|
||||
@admin.register(Channel)
|
||||
class ChannelsAdmin(admin.ModelAdmin):
|
||||
list_select_related = ("user", )
|
||||
list_display = ("id", "code", "user", "kind", "value")
|
||||
|
30
hc/api/migrations/0010_channel.py
Normal file
30
hc/api/migrations/0010_channel.py
Normal file
@ -0,0 +1,30 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import models, migrations
|
||||
from django.conf import settings
|
||||
import uuid
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
('api', '0009_auto_20150801_1250'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Channel',
|
||||
fields=[
|
||||
('id', models.AutoField(primary_key=True, auto_created=True, verbose_name='ID', serialize=False)),
|
||||
('code', models.UUIDField(editable=False, default=uuid.uuid4)),
|
||||
('created', models.DateTimeField(auto_now_add=True)),
|
||||
('kind', models.CharField(choices=[('email', 'Email'), ('webhook', 'Webhook'), ('pd', 'PagerDuty')], max_length=20)),
|
||||
('value', models.CharField(max_length=200, blank=True)),
|
||||
('email_verified', models.BooleanField(default=False)),
|
||||
('checks', models.ManyToManyField(to='api.Check')),
|
||||
('user', models.ForeignKey(to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
),
|
||||
]
|
@ -13,6 +13,8 @@ from hc.lib.emails import send
|
||||
STATUSES = (("up", "Up"), ("down", "Down"), ("new", "New"))
|
||||
DEFAULT_TIMEOUT = td(days=1)
|
||||
DEFAULT_GRACE = td(hours=1)
|
||||
CHANNEL_KINDS = (("email", "Email"), ("webhook", "Webhook"),
|
||||
("pd", "PagerDuty"))
|
||||
|
||||
|
||||
class Check(models.Model):
|
||||
@ -62,6 +64,11 @@ class Check(models.Model):
|
||||
|
||||
return "down"
|
||||
|
||||
def assign_all_channels(self):
|
||||
for channel in Channel.objects.filter(user=self.user):
|
||||
channel.checks.add(self)
|
||||
channel.save()
|
||||
|
||||
|
||||
class Ping(models.Model):
|
||||
owner = models.ForeignKey(Check)
|
||||
@ -71,3 +78,13 @@ class Ping(models.Model):
|
||||
method = models.CharField(max_length=10, blank=True)
|
||||
ua = models.CharField(max_length=200, blank=True)
|
||||
body = models.TextField(blank=True)
|
||||
|
||||
|
||||
class Channel(models.Model):
|
||||
code = models.UUIDField(default=uuid.uuid4, editable=False)
|
||||
user = models.ForeignKey(User)
|
||||
created = models.DateTimeField(auto_now_add=True)
|
||||
kind = models.CharField(max_length=20, choices=CHANNEL_KINDS)
|
||||
value = models.CharField(max_length=200, blank=True)
|
||||
email_verified = models.BooleanField(default=False)
|
||||
checks = models.ManyToManyField(Check)
|
||||
|
@ -1,6 +1,15 @@
|
||||
from django import forms
|
||||
|
||||
from hc.api.models import Channel
|
||||
|
||||
|
||||
class TimeoutForm(forms.Form):
|
||||
timeout = forms.IntegerField(min_value=60, max_value=604800)
|
||||
grace = forms.IntegerField(min_value=60, max_value=604800)
|
||||
|
||||
|
||||
class AddChannelForm(forms.ModelForm):
|
||||
|
||||
class Meta:
|
||||
model = Channel
|
||||
fields = ['kind', 'value']
|
||||
|
@ -3,14 +3,17 @@ from django.conf.urls import url
|
||||
from hc.front import views
|
||||
|
||||
urlpatterns = [
|
||||
url(r'^$', views.index, name="hc-index"),
|
||||
url(r'^checks/add/$', views.add_check, name="hc-add-check"),
|
||||
url(r'^checks/([\w-]+)/name/$', views.update_name, name="hc-update-name"),
|
||||
url(r'^checks/([\w-]+)/timeout/$', views.update_timeout, name="hc-update-timeout"),
|
||||
url(r'^checks/([\w-]+)/email/$', views.email_preview),
|
||||
url(r'^checks/([\w-]+)/remove/$', views.remove, name="hc-remove-check"),
|
||||
url(r'^checks/([\w-]+)/log/$', views.log, name="hc-log"),
|
||||
url(r'^pricing/$', views.pricing, name="hc-pricing"),
|
||||
url(r'^docs/$', views.docs, name="hc-docs"),
|
||||
url(r'^about/$', views.about, name="hc-about"),
|
||||
url(r'^$', views.index, name="hc-index"),
|
||||
url(r'^checks/add/$', views.add_check, name="hc-add-check"),
|
||||
url(r'^checks/([\w-]+)/name/$', views.update_name, name="hc-update-name"),
|
||||
url(r'^checks/([\w-]+)/timeout/$', views.update_timeout, name="hc-update-timeout"),
|
||||
url(r'^checks/([\w-]+)/email/$', views.email_preview),
|
||||
url(r'^checks/([\w-]+)/remove/$', views.remove, name="hc-remove-check"),
|
||||
url(r'^checks/([\w-]+)/log/$', views.log, name="hc-log"),
|
||||
url(r'^pricing/$', views.pricing, name="hc-pricing"),
|
||||
url(r'^docs/$', views.docs, name="hc-docs"),
|
||||
url(r'^about/$', views.about, name="hc-about"),
|
||||
url(r'^channels/$', views.channels, name="hc-channels"),
|
||||
url(r'^channels/add/$', views.add_channel, name="hc-add-channel"),
|
||||
url(r'^channels/([\w-]+)/checks/$', views.channel_checks, name="hc-channel-checks"),
|
||||
]
|
||||
|
@ -7,8 +7,8 @@ from django.shortcuts import redirect, render
|
||||
from django.utils import timezone
|
||||
|
||||
from hc.api.decorators import uuid_or_400
|
||||
from hc.api.models import Check, Ping
|
||||
from hc.front.forms import TimeoutForm
|
||||
from hc.api.models import Channel, Check, Ping
|
||||
from hc.front.forms import AddChannelForm, TimeoutForm
|
||||
|
||||
|
||||
def _welcome(request):
|
||||
@ -79,6 +79,9 @@ def add_check(request):
|
||||
|
||||
check = Check(user=request.user)
|
||||
check.save()
|
||||
|
||||
check.assign_all_channels()
|
||||
|
||||
return redirect("hc-index")
|
||||
|
||||
|
||||
@ -169,3 +172,67 @@ def log(request, code):
|
||||
}
|
||||
|
||||
return render(request, "front/log.html", ctx)
|
||||
|
||||
|
||||
@login_required
|
||||
def channels(request):
|
||||
if request.method == "POST":
|
||||
code = request.POST["channel"]
|
||||
channel = Channel.objects.get(code=code)
|
||||
assert channel.user == request.user
|
||||
|
||||
channel.checks = []
|
||||
print (request.POST)
|
||||
for key in request.POST:
|
||||
if key.startswith("check-"):
|
||||
code = key[6:]
|
||||
check = Check.objects.get(code=code)
|
||||
assert check.user == request.user
|
||||
channel.checks.add(check)
|
||||
|
||||
channel.save()
|
||||
return redirect("hc-channels")
|
||||
|
||||
|
||||
channels = Channel.objects.filter(user=request.user).order_by("created")
|
||||
num_checks = Check.objects.filter(user=request.user).count()
|
||||
|
||||
ctx = {
|
||||
"channels": channels,
|
||||
"num_checks": num_checks
|
||||
|
||||
}
|
||||
return render(request, "front/channels.html", ctx)
|
||||
|
||||
|
||||
@login_required
|
||||
def add_channel(request):
|
||||
assert request.method == "POST"
|
||||
form = AddChannelForm(request.POST)
|
||||
if form.is_valid():
|
||||
channel = form.save(commit=False)
|
||||
channel.user = request.user
|
||||
channel.save()
|
||||
|
||||
checks = Check.objects.filter(user=request.user)
|
||||
channel.checks.add(*checks)
|
||||
channel.save()
|
||||
|
||||
return redirect("hc-channels")
|
||||
|
||||
|
||||
@login_required
|
||||
@uuid_or_400
|
||||
def channel_checks(request, code):
|
||||
|
||||
channel = Channel.objects.get(code=code)
|
||||
assigned = set([check.code for check in channel.checks.all()])
|
||||
checks = Check.objects.filter(user=request.user).order_by("created")
|
||||
|
||||
ctx = {
|
||||
"checks": checks,
|
||||
"assigned": assigned,
|
||||
"channel": channel
|
||||
}
|
||||
|
||||
return render(request, "front/channel_checks.html", ctx)
|
||||
|
26
static/css/channel_checks.css
Normal file
26
static/css/channel_checks.css
Normal file
@ -0,0 +1,26 @@
|
||||
.channel-checks-table tr:first-child td {
|
||||
border-top: 0;
|
||||
}
|
||||
|
||||
.channel-checks-table td:first-child, .channel-checks-table th:first-child {
|
||||
padding-left: 16px;
|
||||
}
|
||||
|
||||
.channel-checks-table .check-all-cell {
|
||||
background: #EEE;
|
||||
}
|
||||
|
||||
.channel-checks-table .check-all-cell .cbx-container {
|
||||
background: #FFF;
|
||||
}
|
||||
|
||||
.channel-checks-table input[type=checkbox]:checked + label:after {
|
||||
font-family: 'Glyphicons Halflings';
|
||||
content: "\e013";
|
||||
}
|
||||
|
||||
.channel-checks-table label:after {
|
||||
padding-left: 4px;
|
||||
padding-top: 2px;
|
||||
font-size: 9px;
|
||||
}
|
44
static/css/channels.css
Normal file
44
static/css/channels.css
Normal file
@ -0,0 +1,44 @@
|
||||
.channels-table {
|
||||
margin-top: 36px;
|
||||
}
|
||||
|
||||
table.channels-table > tbody > tr > th {
|
||||
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 {
|
||||
color: #5cb85c;
|
||||
font-weight: bold
|
||||
}
|
||||
|
||||
.word-down {
|
||||
color: #d9534f;
|
||||
font-weight: bold
|
||||
}
|
||||
|
||||
.preposition {
|
||||
color: #888;
|
||||
}
|
||||
|
||||
.channel-unconfirmed {
|
||||
font-size: small;
|
||||
}
|
||||
|
||||
.channels-help-hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.channels-table .channels-num-checks {
|
||||
padding-left: 40px;
|
||||
}
|
||||
|
@ -1,3 +1,7 @@
|
||||
#checks-table {
|
||||
margin-top: 36px;
|
||||
}
|
||||
|
||||
.my-checks-name.unnamed {
|
||||
color: #999;
|
||||
font-style: italic;
|
||||
|
38
static/js/channels.js
Normal file
38
static/js/channels.js
Normal file
@ -0,0 +1,38 @@
|
||||
$(function() {
|
||||
var placeholders = {
|
||||
email: "address@example.org",
|
||||
webhook: "http://",
|
||||
pd: "service key"
|
||||
}
|
||||
|
||||
$("#add-check-kind").change(function() {
|
||||
$(".channels-add-help p").hide();
|
||||
|
||||
var v = $("#add-check-kind").val();
|
||||
$(".channels-add-help p." + v).show();
|
||||
|
||||
$("#add-check-value").attr("placeholder", placeholders[v]);
|
||||
});
|
||||
|
||||
$(".edit-checks").click(function() {
|
||||
$("#checks-modal").modal("show");
|
||||
var url = $(this).attr("href");
|
||||
$.ajax(url).done(function(data) {
|
||||
$("#checks-modal .modal-content").html(data);
|
||||
|
||||
})
|
||||
|
||||
|
||||
return false;
|
||||
});
|
||||
|
||||
|
||||
var $cm = $("#checks-modal");
|
||||
$cm.on("click", "#toggle-all", function() {
|
||||
var value = $(this).prop("checked");
|
||||
$cm.find(".toggle").prop("checked", value);
|
||||
console.log("aaa", value);
|
||||
|
||||
});
|
||||
|
||||
});
|
@ -21,6 +21,8 @@
|
||||
<link rel="stylesheet" href="{% static 'css/my_checks_desktop.css' %}" type="text/css">
|
||||
<link rel="stylesheet" href="{% static 'css/pricing.css' %}" type="text/css">
|
||||
<link rel="stylesheet" href="{% static 'css/syntax.css' %}" type="text/css">
|
||||
<link rel="stylesheet" href="{% static 'css/channels.css' %}" type="text/css">
|
||||
<link rel="stylesheet" href="{% static 'css/channel_checks.css' %}" type="text/css">
|
||||
{% endcompress %}
|
||||
</head>
|
||||
<body class="page-{{ page }}">
|
||||
|
47
templates/front/channel_checks.html
Normal file
47
templates/front/channel_checks.html
Normal file
@ -0,0 +1,47 @@
|
||||
{% load compress humanize staticfiles hc_extras %}
|
||||
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
|
||||
<div class="modal-header">
|
||||
<button type="button" class="close" data-dismiss="modal">×</span></button>
|
||||
<h4 class="update-timeout-title">Assign Checks to Channel {{ channel.value }}</h4>
|
||||
</div>
|
||||
|
||||
<input type="hidden" name="channel" value="{{ channel.code }}" />
|
||||
<table class="table channel-checks-table">
|
||||
<tr>
|
||||
<th class="check-all-cell">
|
||||
<input
|
||||
id="toggle-all"
|
||||
type="checkbox"
|
||||
class="toggle" />
|
||||
</th>
|
||||
<th class="check-all-cell">
|
||||
Check / Uncheck All
|
||||
</th>
|
||||
</tr>
|
||||
{% for check in checks %}
|
||||
<tr>
|
||||
<td>
|
||||
<input
|
||||
type="checkbox"
|
||||
class="toggle"
|
||||
data-toggle="checkbox-x"
|
||||
{% if check.code in assigned %} checked {% endif %}
|
||||
name="check-{{ check.code }}">
|
||||
|
||||
</td>
|
||||
<td>
|
||||
{{ check.name|default:check.code }}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
|
||||
<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>
|
||||
</form>
|
||||
|
117
templates/front/channels.html
Normal file
117
templates/front/channels.html
Normal file
@ -0,0 +1,117 @@
|
||||
{% extends "base.html" %}
|
||||
{% load compress humanize staticfiles hc_extras %}
|
||||
|
||||
{% block title %}Notification Channels - healthchecks.io{% endblock %}
|
||||
|
||||
|
||||
{% block content %}
|
||||
<div class="row">
|
||||
<div class="col-sm-12">
|
||||
<h1>Notification Channels</h1>
|
||||
<table class="table channels-table">
|
||||
<tr>
|
||||
<th>Type</th>
|
||||
<th>Value</th>
|
||||
<th>Assigned Checks</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
{% for ch in channels %}
|
||||
<tr>
|
||||
<td>
|
||||
{% if ch.kind == "email" %} Email {% endif %}
|
||||
{% if ch.kind == "webhook" %} Webhook {% endif %}
|
||||
{% if ch.kind == "pd" %} PagerDuty {% endif %}
|
||||
</td>
|
||||
<td>
|
||||
<span class="preposition">
|
||||
{% if ch.kind == "email" %} to {% endif %}
|
||||
{% if ch.kind == "pd" %} service key {% endif %}
|
||||
</span>
|
||||
|
||||
{{ ch.value }}
|
||||
|
||||
{% if ch.kind == "email" and not ch.email_verified %}
|
||||
<span class="channel-unconfirmed">(unconfirmed)
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="channels-num-checks">
|
||||
<a
|
||||
class="edit-checks"
|
||||
href="{% url 'hc-channel-checks' ch.code %}">
|
||||
{{ ch.checks.count }} of {{ num_checks }}
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
<tr>
|
||||
<th colspan="2" class="channels-add-title">
|
||||
Add Notification Channel
|
||||
</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<form method="post" action="{% url 'hc-add-channel' %}">
|
||||
<td>
|
||||
<select id="add-check-kind" class="form-control" name="kind">
|
||||
<option value="email">Email</option>
|
||||
<option value="webhook">Webhook</option>
|
||||
<option value="pd">PagerDuty</option>
|
||||
</select>
|
||||
</td>
|
||||
<td class="form-inline">
|
||||
{% csrf_token %}
|
||||
<input
|
||||
id="add-check-value"
|
||||
name="value"
|
||||
class="form-control"
|
||||
type="text"
|
||||
placeholder="address@example.org" />
|
||||
<button type="submit" class="btn btn-success">Add</button>
|
||||
</td>
|
||||
<td>
|
||||
|
||||
</td>
|
||||
</form>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="3" class="channels-add-help">
|
||||
<p class="email">
|
||||
Healthchecks.io will send an email to the specified
|
||||
address when a check goes
|
||||
<span class="word-up">up</span> or <span class="word-down">down</span>.
|
||||
</p>
|
||||
<p class="channels-help-hidden webhook">
|
||||
Healthchecks.io will request the specified URL when
|
||||
a check goes
|
||||
<span class="word-down">down</span>.
|
||||
</p>
|
||||
<p class="channels-help-hidden pd">
|
||||
Healthchecks.io will create an incident on PagerDuty when
|
||||
a check goes
|
||||
<span class="word-down">down</span> and will resolve it
|
||||
when same check goes <span class="word-up">up</span>
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="checks-modal" class="modal">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
|
||||
</div>
|
||||
</form>
|
||||
</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/channels.js' %}"></script>
|
||||
{% endcompress %}
|
||||
{% endblock %}
|
Loading…
x
Reference in New Issue
Block a user