mirror of
https://github.com/prymitive/karma
synced 2026-08-23 11:56:20 +00:00
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:
@@ -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
|
||||
|
||||
@@ -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
@@ -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
@@ -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
|
||||
|
||||
@@ -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
@@ -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();
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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" }}
|
||||
|
||||
@@ -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
File diff suppressed because one or more lines are too long
@@ -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))
|
||||
|
||||
Reference in New Issue
Block a user