mirror of
https://github.com/prymitive/karma
synced 2026-05-05 03:16:51 +00:00
Merge pull request #70 from cloudflare/silence-multiselect
Allow selecting multiple label values when creating silence.
This commit is contained in:
3
Makefile
3
Makefile
@@ -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
|
||||
|
||||
11
README.md
11
README.md
@@ -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.
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
6
assets/static/managed/css/1.12.2-bootstrap-select.min.css
vendored
Normal file
6
assets/static/managed/css/1.12.2-bootstrap-select.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
@@ -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
|
||||
|
||||
9
assets/static/managed/js/1.12.2-bootstrap-select.min.js
vendored
Normal file
9
assets/static/managed/js/1.12.2-bootstrap-select.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
@@ -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
|
||||
|
||||
1
assets/static/managed/js/bootstrap-select.js.map
Normal file
1
assets/static/managed/js/bootstrap-select.js.map
Normal file
File diff suppressed because one or more lines are too long
@@ -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',
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user