Merge pull request #70 from cloudflare/silence-multiselect

Allow selecting multiple label values when creating silence.
This commit is contained in:
Łukasz Mierzwa
2017-04-23 09:12:55 -07:00
committed by GitHub
13 changed files with 307 additions and 71 deletions

View File

@@ -166,6 +166,9 @@ 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
# multiselect widget for bootstrap3, used for silence form
assets: bootstrap-select/1.12.2/js/bootstrap-select.min.js
assets: bootstrap-select/1.12.2/css/bootstrap-select.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

View File

@@ -14,6 +14,17 @@ to alert data, therefore safe to be accessed by wider audience.
Alertmanager's API isn't stable yet and can change between releases.
unsee currently supports Alertmanager `0.4` and `0.5`.
## Security
The unsee process doesn't send any API request to the Alertmanager that could
modify alerts or silence state, but it does provide a web interface that allows
a user to send such requests directly to the Alertmanager API.
If you wish to deploy unsee as a read-only tool please ensure that:
* the unsee process is able to connect to the Alertmanager API
* read-only users are able to connect to the unsee web interface
* read-only users are NOT able to connect to the Alertmanager API
## Metrics
unsee process metrics are accessible under `/metrics` path by default.

View File

@@ -388,3 +388,29 @@ span.alert-group-link > a {
.silence-result-icon {
font-size: 12em;
}
.silence-label-select {
background-color: inherit;
}
.bootstrap-select > button {
padding: 0;
}
.silence-label-select > .bs-caret {
margin-left: -6px;
}
.silence-label-select:hover, .silence-label-picker:hover,
.silence-label-select:active, .silence-label-picker:active,
.silence-label-select:focus, .silence-label-picker:focus {
color: inherit;
}
.bootstrap-select > .dropdown-menu > .dropdown-menu > li > a > .label {
margin-right: 20px;
}
a[aria-expanded=true] .fa-chevron-right {
display: none;
}
a[aria-expanded=false] .fa-chevron-down {
display: none;
}

File diff suppressed because one or more lines are too long

View File

@@ -5,4 +5,5 @@
0.8.0-bootstrap-tagsinput.css
0.8.0-bootstrap-tagsinput-typeahead.css
4.17.47-bootstrap-datetimepicker.min.css
1.12.2-bootstrap-select.min.css
0.1.2-loaders.min.css

File diff suppressed because one or more lines are too long

View File

@@ -7,6 +7,7 @@
0.8.0-bootstrap-tagsinput.min.js
1.1.1-typeahead.bundle.min.js
4.17.47-bootstrap-datetimepicker.min.js
1.12.2-bootstrap-select.min.js
0.1.2-loaders.css.min.js
2.1.3-js.cookie.min.js
1.8.3-underscore-min.js

File diff suppressed because one or more lines are too long

View File

@@ -23,6 +23,8 @@ var Templates = (function(params) {
silenceForm: '#silence-form',
silenceFormSuccess: '#silence-form-success',
silenceFormError: '#silence-form-error',
silenceFormFatal: '#silence-form-fatal',
silenceFormLoading: '#silence-form-loading',
// label button
buttonLabel: '#label-button-filter',

View File

@@ -82,41 +82,131 @@ var UI = (function(params) {
}
silenceFormData = function() {
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;
}
});
$.each($("#newSilenceForm .selectpicker"), function(i, elem) {
var label_key = $(elem).data('label-key');
var values = $(elem).selectpicker('val');
if (values && values.length > 0) {
var pval;
isRegex = false;
if (values.length > 1) {
pval = "(" + values.join("|") + ")";
isRegex = true;
} else {
pval = values[0];
}
payload["matchers"].push({
name: label_key,
value: pval,
isRegex: isRegex
});
}
});
return payload;
}
silenceFormJSONRender = function() {
var d = "curl " + $("#silenceModal").data("silence-api")
+ "\n -X POST --data "
+ JSON.stringify(silenceFormData(), undefined, 2);
$("#silenceJSONBlob").html(d);
}
// 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
})
Templates.Render("silenceFormLoading", {})
);
$('.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'
var elem = $(event.relatedTarget);
var elemLabels = {};
$.each(elem.data("labels").split(","), function(i, l) {
elemLabels[l.split("=")[0]] = l.split("=")[1];
});
$.ajax({
url: 'alerts.json?q=alertname=' + elem.data('alertname'),
error: function(xhr, textStatus, errorThrown) {
var err = xhr.responseText || errorThrown || textStatus;
modal.find(".modal-body").html(
Templates.Render("silenceFormFatal", {error: err})
);
},
minDate: moment().subtract(1, 'minutes'),
sideBySide: true
success: function(data) {
var modal = $("#silenceModal");
var labels = {};
$.each(data.groups, function(i, group) {
$.each(group.alerts, function(j, alert) {
$.each(alert.labels, function(label_key, label_val) {
if (labels[label_key] == undefined) {
labels[label_key] = {};
}
if (labels[label_key][label_val] == undefined) {
labels[label_key][label_val] = {
key: label_key,
value: label_val,
attrs: Alerts.GetLabelAttrs(label_key, label_val),
selected: elemLabels[label_key] == label_val
}
}
});
});
});
modal.find(".modal-body").html(
Templates.Render("silenceForm", {labels: labels})
);
$.each($(".selectpicker"), function(i, elem) {
$(elem).selectpicker({
iconBase: 'fa',
tickIcon: 'fa-check',
width: 'fit',
selectAllText: '<i class="fa fa-check-square-o"></i>',
deselectAllText: '<i class="fa fa-square-o"></i>',
noneSelectedText: '<span class="label label-list label-default">' + $(this).data('label-key') + ": none</span>",
multipleSeparator: ' ',
selectedTextFormat: 'count > 1',
countSelectedText: function (numSelected, numTotal) {
return '<span class="label label-list label-warning">'
+ $(elem).data('label-key') + ": " + numSelected + " values selected</span>";
}
});
});
$('.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
});
}
});
});
@@ -125,34 +215,14 @@ var UI = (function(params) {
modal.find(".modal-body").children().remove();
Unsee.WaitForNextReload();
});
modal.on('show.bs.collapse, dp.change', function (e) {
silenceFormJSONRender();
});
modal.on('change', function (e) {
silenceFormJSONRender();
});
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
})
}
}
});
payload = silenceFormData();
if (payload["matchers"].length == 0) {
var errContent = Templates.Render("silenceFormError", {error: "Select at least on label"});
$("#newSilenceAlert").html(errContent).removeClass("hidden");

View File

@@ -90,6 +90,7 @@
<span class="label label-list label-success cursor-pointer"
type="button"
data-labels="<%= labels.join(',') %>"
data-alertname="<%= alert.labels.alertname %>"
data-toggle="modal"
data-target="#silenceModal">
<i class="fa fa-bell-slash" title="Silence this alert" data-toggle="tooltip" data-placement="top" />

View File

@@ -2,17 +2,25 @@
<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) { %>
<% _.each(Alerts.SortMapByKey(labels), function(label) { %>
<div>
<label>
<input type="checkbox"
checked="checked"
name="<%= label.key %>=<%= label.value %>">
<span class="<%= label.attrs.class %>" style="<%= label.attrs.style %>">
<%- label.attrs.text %>
</span>
</label>
</div>
<select class="selectpicker silence-label-picker"
data-label-key="<%= label.key %>"
data-style="silence-label-select"
<% if (Object.keys(label.value).length > 10) { %>data-live-search="true"<% } %>
<% if (Object.keys(label.value).length > 1) { %>data-actions-box="true"<% } %>
multiple>
<% _.each(Alerts.SortMapByKey(label.value), function(label_val) { %>
<option <% if (label_val.value.selected) { %>selected="selected"<% } %>
value="<%= label_val.key %>"
data-content="<span class='<%= label_val.value.attrs.class %>' style='<%= label_val.value.attrs.style %>'><%- label_val.value.attrs.text %></span>">
<%- label_val.value.attrs.text %>
</option>
<% }) %>
</select>
</label>
</div>
<% }) %>
<hr class="separator">
@@ -80,8 +88,18 @@
<hr class="separator">
<a class="text-muted" data-toggle="collapse" href="#silenceJSON" aria-expanded="false" aria-controls="silenceJSON">
<i class="fa fa-chevron-right"></i>
<i class="fa fa-chevron-down"></i>
</a>
<div class="collapse" id="silenceJSON">
<p class="text-muted">
</p>
<pre id="silenceJSONBlob"></pre>
</div>
<div class="text-center">
<button id="silenceSubmit" type="submit" class="btn btn-default">Create</button>
<button id="silenceSubmit" type="submit" class="btn btn-success">Create</button>
</div>
</form>
</script>
@@ -95,7 +113,25 @@
</p>
</script>
<script type="application/json" id="silence-form-fatal">
<div class="silence-result-icon text-center text-danger">
<i class="fa fa-exclamation-circle"></i>
</div>
<p class="text-center">
New silence form rendering failed.
</p>
<p class="text-center">
<%- error %>
</p>
</script>
<script type="application/json" id="silence-form-error">
<i class="fa fa-exclamation-circle"></i>
<%- error %>
</script>
<script type="application/json" id="silence-form-loading">
<div class="silence-result-icon text-center text-muted">
<i class="fa fa-refresh fa-spin"></i>
</div>
</script>

File diff suppressed because one or more lines are too long