Add support for creating silences directly from unsee UI

It adds a button next to the @silenced filter that shows a modal window with a silence form. API request is send via AJAX call from user browser.
This commit is contained in:
Łukasz Mierzwa
2017-04-20 17:01:52 -07:00
parent ed03c0f234
commit d5916c81cf
13 changed files with 365 additions and 28 deletions
+3
View File
@@ -161,6 +161,9 @@ assets: bootstrap-tagsinput/0.8.0/bootstrap-tagsinput.min.js
assets: bootstrap-tagsinput/0.8.0/bootstrap-tagsinput.css
assets: corejs-typeahead/1.1.1/typeahead.bundle.min.js
assets: bootstrap-tagsinput/0.8.0/bootstrap-tagsinput-typeahead.css
# datepicker widget for bootstrap3
assets: bootstrap-datetimepicker/4.17.47/js/bootstrap-datetimepicker.min.js
assets: bootstrap-datetimepicker/4.17.47/css/bootstrap-datetimepicker.min.css
# loaders.css, for animated spinners
assets: loaders.css/0.1.2/loaders.css.min.js
assets: loaders.css/0.1.2/loaders.min.css
+8
View File
@@ -131,6 +131,10 @@ span.label-ts-span {
cursor: pointer;
}
.cursor-pointer {
cursor: pointer;
}
/* make filter input take all the space it can */
.filterbar {
width: 950px;
@@ -380,3 +384,7 @@ span.alert-group-link > a {
color: #fff;
opacity: 0;
}
.silence-result-icon {
font-size: 12em;
}
File diff suppressed because one or more lines are too long
+1
View File
@@ -4,4 +4,5 @@
0.2.0-nprogress.min.css
0.8.0-bootstrap-tagsinput.css
0.8.0-bootstrap-tagsinput-typeahead.css
4.17.47-bootstrap-datetimepicker.min.css
0.1.2-loaders.min.css
File diff suppressed because one or more lines are too long
+1
View File
@@ -6,6 +6,7 @@
0.2.0-nprogress.min.js
0.8.0-bootstrap-tagsinput.min.js
1.1.1-typeahead.bundle.min.js
4.17.47-bootstrap-datetimepicker.min.js
0.1.2-loaders.css.min.js
2.1.3-js.cookie.min.js
1.8.3-underscore-min.js
+4
View File
@@ -19,6 +19,10 @@ var Templates = (function(params) {
modalTitle: '#modal-title',
modalBody: '#modal-body',
// modal popup with silence form
silenceForm: '#silence-form',
silenceFormSuccess: '#silence-form-success',
// label button
buttonLabel: '#label-button-filter',
+140 -21
View File
@@ -4,49 +4,49 @@ var UI = (function(params) {
// when user click on any alert label modal popup with a list of possible
// filter will show, this function is used to setup that modal
setupModal = function() {
$('#labelModal').on('show.bs.modal', function(event) {
$("#labelModal").on("show.bs.modal", function(event) {
Unsee.Pause();
var modal = $(this);
var label = $(event.relatedTarget);
var label_key = label.data('label-key');
var label_val = label.data('label-val');
var label_key = label.data("label-key");
var label_val = label.data("label-val");
var attrs = Alerts.GetLabelAttrs(label_key, label_val);
var counter = Summary.Get(label_key, label_val);
modal.find('.modal-title').html(
Templates.Render('modalTitle', {
modal.find(".modal-title").html(
Templates.Render("modalTitle", {
attrs: attrs,
counter: counter
})
);
var hints = Autocomplete.GenerateHints(label_key, label_val);
modal.find('.modal-body').html(
Templates.Render('modalBody', {hints: hints})
modal.find(".modal-body").html(
Templates.Render("modalBody", {hints: hints})
);
modal.on('click', '.modal-button-filter', function(elem) {
var filter = $(elem.target).data('filter-append-value');
$('#labelModal').modal('hide');
modal.on("click", ".modal-button-filter", function(elem) {
var filter = $(elem.target).data("filter-append-value");
$("#labelModal").modal("hide");
Filters.AddFilter(filter);
});
});
$('#labelModal').on('hidden.bs.modal', function(event) {
$("#labelModal").on("hidden.bs.modal", function(event) {
var modal = $(this);
modal.find('.modal-title').children().remove();
modal.find('.modal-body').children().remove();
modal.find(".modal-title").children().remove();
modal.find(".modal-body").children().remove();
Unsee.WaitForNextReload();
});
}
// each alert group have a link generated for it, but we hide it until
// user hovers over that group so it doesn't trash the UI
// user hovers over that group so it doesn"t trash the UI
setupGroupLinkHover = function(elem) {
$(elem).on('mouseenter', function() {
$(this).find('.alert-group-link > a').finish().animate({
$(elem).on("mouseenter", function() {
$(this).find(".alert-group-link > a").finish().animate({
opacity: 100
}, 200);
});
$(elem).on('mouseleave', function() {
$(this).find('.alert-group-link > a').finish().animate({
$(elem).on("mouseleave", function() {
$(this).find(".alert-group-link > a").finish().animate({
opacity: 0
}, 200);
});
@@ -56,15 +56,15 @@ var UI = (function(params) {
// find all elements inside alert group panel that will use tooltips
// and setup those
setupGroupTooltips = function(groupElem) {
$.each(groupElem.find('[data-toggle=tooltip]'), function(i, elem) {
$.each(groupElem.find("[data-toggle=tooltip]"), function(i, elem) {
$(elem).tooltip({
animation: false, // slows down tooltip removal
delay: {
show: 500,
hide: 0
},
title: $(elem).attr('title') || $(elem).data('ts-title'),
trigger: 'hover'
title: $(elem).attr("title") || $(elem).data("ts-title"),
trigger: "hover"
});
});
}
@@ -78,6 +78,125 @@ var UI = (function(params) {
init = function() {
setupModal();
setupSilenceForm();
}
// modal form for creating new silences
setupSilenceForm = function() {
var modal = $("#silenceModal");
modal.on("show.bs.modal", function(event) {
Unsee.Pause();
var modal = $(this);
var elem = $(event.relatedTarget);
var labels = [];
$.each(elem.data("labels").split(","), function(i, l) {
labels.push({
key: l.split("=")[0],
value: l.split("=")[1],
attrs: Alerts.GetLabelAttrs(l.split("=")[0], l.split("=")[1])
});
});
modal.find(".modal-body").html(
Templates.Render("silenceForm", {
labels: labels
})
);
$('.datetime-picker').datetimepicker({
format: "YYYY-MM-DD HH:mm",
icons: {
time: 'fa fa-clock-o',
date: 'fa fa-calendar',
up: 'fa fa-chevron-up',
down: 'fa fa-chevron-down',
previous: 'fa fa-chevron-left',
next: 'fa fa-chevron-right',
today: 'fa fa-asterisk',
clear: 'fa fa-undo',
close: 'fa fa-close'
},
minDate: moment().subtract(1, 'minutes'),
sideBySide: true
});
});
modal.on("hidden.bs.modal", function(event) {
var modal = $(this);
modal.find(".modal-body").children().remove();
Unsee.WaitForNextReload();
});
modal.submit(function(event) {
var values = $("#newSilenceForm").serializeArray();
var payload = {
matchers: [],
startsAt: "",
endsAt: "",
createdBy: "",
comment: ""
};
$.each(values, function(i, elem){
switch (elem.name) {
case "comment": case "createdBy":
payload[elem.name] = elem.value;
break;
case "startsAt": case "endsAt":
payload[elem.name] = moment(elem.value);
break;
default:
if (elem.value == "on") {
payload["matchers"].push({
name: elem.name.split("=")[0],
value: elem.name.split("=")[1],
isRegex: false
})
}
}
});
if (payload["matchers"].length == 0) {
$("#newSilenceAlert").html("Select at least on label").removeClass("hidden");
return false;
}
var url = modal.data("silence-api");
$.ajax({
type: "POST",
url: url,
data: JSON.stringify(payload),
error: function(xhr, textStatus, errorThrown) {
// default to whatever error text we can get
var err = xhr.responseText || errorThrown || textStatus;
if (xhr.responseText) {
// if we have a reponse text try to decode it as JSON
// it should be error from Alertmanager (it we were able to connect)
try {
var j = JSON.parse(xhr.responseText);
if (j["error"] != undefined) {
err = j["error"];
}
} catch (err) {
// can't parse json, do nothing
}
}
$("#newSilenceAlert").html(err).removeClass("hidden");
},
success: function(data, textStatus, xhr) {
if (data["status"] == "success") {
$("#newSilenceAlert").addClass("hidden");
$('#newSilenceForm').html(Templates.Render("silenceFormSuccess", {
silenceID: data["data"]["silenceId"]
}));
} else {
var err = "Invalid response from Alertmanager API: " + JSON.stringify(data);
$("#newSilenceAlert").html(err).removeClass("hidden");
}
},
dataType: "json"
});
event.preventDefault();
});
}
+11
View File
@@ -83,6 +83,17 @@
<%= Templates.Render('buttonLabel', {elem: 'span', elemClass: 'label label-list label-success', label: {key: '@silenced', value: 'true', text: '@silenced: true'}}) %>
<% } else { %>
<%= Templates.Render('buttonLabel', {elem: 'span', elemClass: 'label label-list label-danger', label: {key: '@silenced', value: 'false', text: '@silenced: false'}}) %>
<% var labels = [] %>
<% _.each(Alerts.SortMapByKey(alert.labels), function(label) { %>
<% labels.push(label.key + '=' + label.value) %>
<% }) %>
<span class="label label-list label-success cursor-pointer"
type="button"
data-labels="<%= labels.join(',') %>"
data-toggle="modal"
data-target="#silenceModal">
<i class="fa fa-bell-slash" title="Silence this alert" data-toggle="tooltip" data-placement="top" />
</span>
<% } %>
<a class="label label-list label-default label-age label-ts"
data-toggle="tooltip"
+20
View File
@@ -160,6 +160,25 @@
</div>
</div>
<div class="modal fade" id="silenceModal" tabindex="-1" role="dialog" data-silence-api="{{ .SilencesApi }}">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header text-center">
<div class="modal-title">
<button class="close" type="button" data-dismiss="modal">
<i class="fa fa-close"></i>
</button>
<h4 class="modal-title">
<i class="fa fa-bell-slash"></i>
New silence
</h4>
</div>
</div>
<div class="modal-body"></div>
</div>
</div>
</div>
</body>
</html>
@@ -167,3 +186,4 @@
{{ template "templates/summary.html" }}
{{ template "templates/errors.html" }}
{{ template "templates/modal.html" }}
{{ template "templates/silence.html" }}
+87
View File
@@ -0,0 +1,87 @@
<script type="application/json" id="silence-form">
<div id="newSilenceAlert" class="alert alert-danger hidden" role="alert"></div>
<form id="newSilenceForm">
<label class="control-label">Labels to match</label>
<% _.each(labels, function(label) { %>
<div class="checkbox label-checkbox">
<label>
<input type="checkbox"
checked="checked"
name="<%= label.key %>=<%= label.value %>">
<div class="label-list label <%= label.attrs.class %>" style="<%= label.attrs.style %>">
<%- label.attrs.text %>
</div>
</label>
</div>
<% }) %>
<hr class="separator">
<label class="control-label">Duration</label>
<div class="row">
<div class="col-sm-6">
<div class="input-group date">
<input type="datetime"
class="form-control datetime-picker"
id="startsAt"
value="<%= moment().format("YYYY-MM-DD HH:mm:ss") %>"
name="startsAt"
required>
<span class="input-group-addon">
<i class="fa fa-calendar"></i>
</span>
</div>
</div>
<div class="col-sm-6">
<div class="input-group date">
<input type="datetime"
class="form-control datetime-picker"
id="endsAt"
value="<%= moment().add(4, 'hours').format("YYYY-MM-DD HH:mm:ss") %>"
name="endsAt"
required>
<span class="input-group-addon">
<i class="fa fa-calendar"></i>
</span>
</div>
</div>
</div>
<hr class="separator">
<div class="form-group">
<label for="createdBy" class="control-label">Email</label>
<input type="email"
class="form-control"
id="createdBy"
placeholder="Email"
name="createdBy"
required>
</div>
<div class="form-group">
<label for="comment" class="control-label">Comment</label>
<input type="text"
class="form-control"
id="comment"
placeholder="Comment"
name="comment"
required>
</div>
<hr class="separator">
<div class="text-center">
<button id="silenceSubmit" type="submit" class="btn btn-default">Create</button>
</div>
</form>
</script>
<script type="application/json" id="silence-form-success">
<div class="silence-result-icon text-center text-success">
<i class="fa fa-check-circle"></i>
</div>
<p class="text-center">
Silence ID: <span class="text-success"><%= silenceID %></span>
</p>
</script>
+76 -7
View File
File diff suppressed because one or more lines are too long
+7
View File
@@ -14,6 +14,7 @@ import (
"github.com/cloudflare/unsee/config"
"github.com/cloudflare/unsee/models"
"github.com/cloudflare/unsee/store"
"github.com/cloudflare/unsee/transport"
log "github.com/Sirupsen/logrus"
"github.com/gin-gonic/gin"
@@ -51,6 +52,11 @@ func index(c *gin.Context) {
defaultUsed = false
}
silencesAPI, err := transport.JoinURL(config.Config.AlertmanagerURI, "api/v1/silences")
if err != nil {
log.Errorf("Can't generate silences API URL: %s", err)
}
c.HTML(http.StatusOK, "templates/index.html", gin.H{
"Version": version,
"SentryDSN": config.Config.SentryPublicDSN,
@@ -62,6 +68,7 @@ func index(c *gin.Context) {
"DefaultUsed": defaultUsed,
"StaticColorLabels": strings.Join(config.Config.ColorLabelsStatic, " "),
"WebPrefix": config.Config.WebPrefix,
"SilencesApi": silencesAPI,
})
log.Infof("[%s] %s %s took %s", c.ClientIP(), c.Request.Method, c.Request.RequestURI, time.Since(start))