Files
karma/assets/static/unsee.js
Łukasz Mierzwa c6288d27a2 Show silence ID in the UI
This adds silence ID to the list of rendered silence elements, clicking on it will allow to copy it to the clipboard. Fixes #203
2018-01-03 13:15:19 -08:00

399 lines
13 KiB
JavaScript

"use strict";
const $ = window.$ = window.jQuery = require("jquery");
const Clipboard = require("clipboard");
const moment = require("moment");
const Raven = require("raven-js");
require("bootstrap-loader");
require("font-awesome-webpack");
require("bootstrap-select");
require("bootstrap-select/dist/css/bootstrap-select.css");
require("loaders.css/loaders.css");
require("loaders.css/loaders.css.js");
require("eonasdan-bootstrap-datetimepicker/src/js/bootstrap-datetimepicker.js");
require("eonasdan-bootstrap-datetimepicker/src/less/bootstrap-datetimepicker-build.less");
require("./favicon.ico");
require("./base.css");
const alerts = require("./alerts");
const autocomplete = require("./autocomplete");
const colors = require("./colors");
const config = require("./config");
const counter = require("./counter");
const grid = require("./grid");
const filters = require("./filters");
const progress = require("./progress");
const silence = require("./silence");
const summary = require("./summary");
const templates = require("./templates");
const ui = require("./ui");
const unsilence = require("./unsilence");
const watchdog = require("./watchdog");
var timer = false;
var version = false;
var refreshInterval = 15;
var hiddenAt = false;
var selectors = {
refreshButton: "#refresh",
errors: "#errors",
instanceErrors: "#instance-errors",
clickToCopy: ".click-to-copy"
};
function parseAJAXError(xhr, textStatus) {
// default to textStatus, it's usually just "error" string
var err = textStatus;
if (xhr.readyState === 0) {
// ajax() completed but request wasn't send
err = "Connection to the remote endpoint failed";
} else if (xhr.responseJSON && xhr.responseJSON.error) {
// there's response JSON and an error key in it
err = xhr.responseJSON.error;
} else if (xhr.responseText) {
// else check response as a string
err = xhr.responseText;
}
return err;
}
function getRefreshRate() {
return refreshInterval;
}
function setRefreshRate(seconds) {
var rate = parseInt(seconds);
if (isNaN(rate) || rate === null) {
// if passed rate is incorrect use select value
rate = config.getOption("refresh").Get();
if (isNaN(rate) || rate === null) {
// if that's also borked use default 15
rate = 15;
}
}
// don't allow setting refresh rate lower than 1s
if (rate < 1) {
rate = 1;
}
refreshInterval = rate;
progress.resetTimer();
}
function needsUpgrade(responseVersion) {
if (version === false) {
version = responseVersion;
return false;
}
return version != responseVersion;
}
function updateIsReady() {
progress.complete();
$(selectors.refreshButton).prop("disabled", true);
counter.hide();
}
function updateCompleted() {
counter.show();
filters.updateDone();
progress.complete();
$(selectors.refreshButton).prop("disabled", false);
// hack for fixing padding since input can grow and change height
$("body").css("padding-top", $(".navbar").height());
}
function renderError(template, context) {
counter.markError();
grid.clear();
grid.hide();
$(selectors.errors).html(templates.renderTemplate(template, context));
$(selectors.errors).show();
counter.markUnknown();
summary.update({});
document.title = "(◕ O ◕)";
updateCompleted();
}
function resume() {
if (config.getOption("autorefresh").Get()) {
filters.updateDone();
} else {
filters.setPause();
return false;
}
progress.start();
if (timer !== false) {
clearInterval(timer);
}
/* eslint-disable no-use-before-define */
// FIXME circular dependency resume -> triggerReload -> resume -> ...
timer = setTimeout(triggerReload, getRefreshRate() * 1000);
}
function handleError(err) {
Raven.captureException(err);
if (window.console) {
console.error(err.stack);
}
renderError("internalError", {
name: err.name,
message: err.message,
raw: err
});
setTimeout(function() {
resume();
}, 500);
}
function upgrade() {
renderError("reloadNeeded", {});
setTimeout(function() {
location.reload();
}, 3000);
}
function triggerReload() {
updateIsReady();
$.ajax({
url: "alerts.json?q=" + filters.getFilters().join(","),
success: function(resp) {
counter.markSuccess();
if (needsUpgrade(resp.version)) {
upgrade();
} else {
if (resp.upstreams.counters.total === 0) {
// no upstream to use fail hard
counter.markUnknown();
$(selectors.instanceErrors).html("");
renderError("updateError", {
error: "Fatal error",
messages: [ "No working Alertmanager server found" ],
lastTs: watchdog.getLastUpdate()
});
resume();
} else if (resp.upstreams.counters.healthy > 0 ) {
// we have some healthy upstreams, check for failed ones
if (resp.upstreams.counters.failed > 0) {
var instances = [];
resp.upstreams.instances.sort(function(a, b){
if(a.name < b.name) return -1;
if(a.name > b.name) return 1;
return 0;
});
$.each(resp.upstreams.instances, function(i, instance){
if (instance.error !== "") {
instances.push(instance);
}
});
$(selectors.instanceErrors).html(
templates.renderTemplate("instanceError", {
instances: instances
})
);
} else {
$(selectors.instanceErrors).html("");
}
// update_alerts() is cpu heavy so it will block browser from applying css changes
// inject tiny delay between addClass() above and update_alerts() so that the browser
// have a chance to reflect those updates
setTimeout(function() {
try {
summary.update({});
filters.reloadBadges(resp.filters);
colors.update(resp.colors);
alerts.updateAlerts(resp);
updateCompleted();
watchdog.pong(moment(resp.timestamp));
resume();
if (!watchdog.isFatal()) {
$(selectors.errors).html("");
$(selectors.errors).hide("");
}
} catch (err) {
counter.markUnknown();
handleError(err);
resume();
}
}, 50);
} else {
// we have upstreams but none is working, fail hard
counter.markUnknown();
$(selectors.instanceErrors).html("");
var failedInstances = [];
$.each(resp.upstreams.instances, function(i, instance) {
if (instance.error !== "") {
failedInstances.push(instance);
}
});
renderError("configError", {
instances: failedInstances
});
resume();
}
}
},
error: function(xhr, textStatus) {
counter.markUnknown();
$(selectors.instanceErrors).html("");
// if fatal error was already triggered we have error message
// so don't add new one
if (!watchdog.isFatal()) {
var err = parseAJAXError(xhr, textStatus);
renderError("updateError", {
error: "Backend error",
messages: [ err ],
lastTs: watchdog.getLastUpdate()
});
}
resume();
}
});
}
function pause() {
progress.pause();
filters.setPause();
if (timer !== false) {
clearInterval(timer);
timer = false;
}
}
function flash() {
var bg = $("#flash").css("background-color");
$("#flash").css("display", "block").animate({
backgroundColor: "#fff"
}, 300, function() {
$(this).animate({
backgroundColor: bg
}, 100).css("display", "none");
});
}
// when user switches to a different tab but keeps unsee tab open in the background
// some browsers (like Chrome) will try to apply some forms of throttling for the JS
// code, to ensure that there are no visual artifacts (like state alerts not removed from the page)
// redraw all alerts if we detect that the user switches from a different tab to unsee
function setupPageVisibilityHandler() {
// based on https://developer.mozilla.org/en-US/docs/Web/API/Page_Visibility_API
if (typeof document.hidden !== "undefined" && typeof document.addEventListener !== "undefined") {
document.addEventListener("visibilitychange", function() {
if (document.hidden) {
// when tab is hidden set a timestamp of that event
hiddenAt = moment().utc().unix();
} else {
// when user switches back check if we have a timestamp
// and if autorefresh is enable
if (hiddenAt && config.getOption("autorefresh").Get()) {
// get the diff to see how long tab was hidden
var diff = moment().utc().unix() - hiddenAt;
if (diff > refreshInterval) {
// if it was hidden for more than one refresh cycle
// then manually refresh alerts to ensure everything
// is up to date
triggerReload();
}
}
hiddenAt = false;
}
}, false);
}
}
function init(localStore) {
progress.init();
config.init({
CopySelector: "#copy-settings-with-filter",
SaveSelector: "#save-default-filter",
ResetSelector: "#reset-settings"
});
config.loadFromCookies();
counter.init();
summary.init();
grid.init();
autocomplete.init();
filters.init(localStore);
watchdog.init(30, 60*15); // set watchdog to 15 minutes
$(selectors.refreshButton).click(function() {
if (!$(selectors.refreshButton).prop("disabled")) {
triggerReload();
}
return false;
});
setupPageVisibilityHandler();
}
function onReady(localStore) {
// wrap all inits so we can handle errors
try {
// init all elements using bootstrapSwitch
$(".toggle").bootstrapSwitch();
// enable tooltips, #settings is a dropdown so it already uses different data-toggle
$("[data-toggle='tooltip'], #settings, #history").tooltip({
trigger: "hover"
});
var clipboard = new Clipboard(selectors.clickToCopy);
clipboard.on("success", function(e) {
// flash element after copy
$(e.trigger).finish().fadeOut(100).fadeIn(300);
// hide tooltip after flash
$(e.trigger).tooltip("hide");
// reset focus
e.clearSelection();
});
colors.init($("#alerts").data("static-color-labels").split(" "));
templates.init();
ui.setupModal();
silence.setupSilenceForm();
unsilence.init();
init(localStore);
// delay initial alert load to allow browser finish rendering
setTimeout(function() {
filters.setFilters();
}, 100);
} catch (error) {
Raven.captureException(error);
if (window.console) {
console.error("Error: " + error.stack);
}
// templates might not be loaded yet, make some html manually
$("#errors").html(
"<div class='jumbotron'>" +
"<h1 class='text-center'>" +
"Internal error <i class='fa fa-exclamation-circle text-danger'/>" +
"</h1>" +
"<div class='text-center'><p>" +
error.message +
"</p></div></div>"
).show();
}
}
exports.init = init;
exports.pause = pause;
exports.resume = resume;
exports.triggerReload = triggerReload;
exports.getRefreshRate = getRefreshRate;
exports.setRefreshRate = setRefreshRate;
exports.flash = flash;
exports.parseAJAXError = parseAJAXError;
exports.onReady = onReady;
$(document).ready(function() {
onReady(window.localStorage);
});