diff --git a/assets/static/__mocks__/localStorageMock.js b/assets/static/__mocks__/localStorageMock.js
new file mode 100644
index 000000000..78e61825a
--- /dev/null
+++ b/assets/static/__mocks__/localStorageMock.js
@@ -0,0 +1,21 @@
+class LocalStorageMock {
+
+ constructor() {
+ this.store = {};
+ }
+
+ getItem(key) {
+ return this.store[key] || null;
+ }
+
+ setItem(key, value) {
+ this.store[key] = value.toString();
+ }
+
+ clear() {
+ this.store = {};
+ }
+
+}
+
+module.exports = new LocalStorageMock();
diff --git a/assets/static/__mocks__/templatesMock.js b/assets/static/__mocks__/templatesMock.js
index 72aa4f95a..6aa69656f 100644
--- a/assets/static/__mocks__/templatesMock.js
+++ b/assets/static/__mocks__/templatesMock.js
@@ -9,6 +9,7 @@ function loadTemplates() {
"modal.html",
"silence.html",
"summary.html",
+ "history.html",
];
templateFiles.forEach(function(filename){
var templatePath = path.join(__dirname, "../../templates/", filename);
diff --git a/assets/static/__snapshots__/filters.test.js.snap b/assets/static/__snapshots__/filters.test.js.snap
new file mode 100644
index 000000000..91b106da6
--- /dev/null
+++ b/assets/static/__snapshots__/filters.test.js.snap
@@ -0,0 +1,390 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`appended filtes should be present in history 1`] = `
+"
+
+
+"
+`;
+
+exports[`appended filtes should be present in history 2`] = `
+"
+
+
+
+
+
+
+
+
+"
+`;
+
+exports[`appended filtes should be present in history 3`] = `
+"
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+"
+`;
+
+exports[`appended filtes should be present in history 4`] = `
+"
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+"
+`;
+
+exports[`appended filtes should be present in history 5`] = `
+"
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+"
+`;
+
+exports[`appended filtes should be present in history 6`] = `
+"
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+"
+`;
+
+exports[`default filter should be in history after setting filter to foo 1`] = `
+"
+
+
+"
+`;
diff --git a/assets/static/base.css b/assets/static/base.css
index 5721cc432..41c59024f 100644
--- a/assets/static/base.css
+++ b/assets/static/base.css
@@ -141,7 +141,7 @@ span.label-ts-span {
/* make filter input take all the space it can */
div.filterbar {
- width: 950px;
+ width: 905px;
}
div.filterbar > .input-group-addon.input-sm {
width: 10px;
@@ -186,12 +186,14 @@ div.bootstrap-tagsinput > .twitter-typeahead > input.tt-input {
/* menus uses links with icons instead of buttons, fix focus color
so the link doesn't stay colored after clicking */
+#history:focus,
#help:focus,
#settings:focus,
#refresh:focus {
color: #fff;
}
+#history:hover,
#help:hover,
#settings:hover,
#refresh:hover {
@@ -498,3 +500,10 @@ a[aria-expanded=false] .fa-chevron-down {
.alert-static-elements {
margin-bottom: 4px;
}
+
+li.history-menu > a.history-menu-item {
+ padding-left: 10px;
+ padding-right: 4px;
+ padding-top: 2px;
+ padding-bottom: 6px;
+}
diff --git a/assets/static/filters.js b/assets/static/filters.js
index 9fe8e5cea..79bac98bf 100644
--- a/assets/static/filters.js
+++ b/assets/static/filters.js
@@ -13,11 +13,16 @@ require("./bootstrap-tagsinput.less");
const autocomplete = require("./autocomplete");
const unsee = require("./unsee");
const querystring = require("./querystring");
+const templates = require("./templates");
var selectors = {
filter: "#filter",
- icon: "#filter-icon"
+ icon: "#filter-icon",
+ historyMenu: "#historyMenu"
};
+var appendsEnabled = true;
+var historyStorage;
+const historyKey = "filterHistory";
function addBadge(text) {
$.each($("span.tag"), function(i, tag) {
@@ -51,6 +56,27 @@ function addFilter(text) {
$(selectors.filter).tagsinput("add", text);
}
+function applyFilterList(filterList) {
+ // we need to add filters one by one, this would reload alerts on every
+ // add() so let's pause reloads and resume once we're done with updating
+ // filters
+ unsee.pause();
+ // disable history appends as it would record each new filter in the
+ // history
+ appendsEnabled = false;
+ $(selectors.filter).tagsinput("removeAll");
+ for (var i = 0; i < filterList.length; i++) {
+ $(selectors.filter).tagsinput("add", filterList[i]);
+ }
+ // enable everything again
+ appendsEnabled = true;
+ unsee.resume();
+}
+
+function clearFilters() {
+ $(selectors.filter).tagsinput("removeAll");
+}
+
function setUpdating() {
// visual hint that alerts are reloaded due to filter change
$(selectors.icon).removeClass("fa-search fa-pause").addClass("fa-circle-o-notch fa-spin");
@@ -64,17 +90,64 @@ function setPause() {
$(selectors.icon).removeClass("fa-circle-o-notch fa-spin fa-search").addClass("fa-pause");
}
+function renderHistory() {
+ var historicFilters = [];
+
+ const currentFilterText = getFilters().join(",");
+
+ const history = historyStorage.getItem(historyKey);
+ if (history) {
+ historicFilters = history.split("\n");
+ }
+
+ var historyMenuHTML = templates.renderTemplate("historyMenu", {
+ activeFilter: currentFilterText,
+ defaultFilter: $(selectors.filter).data("default-filter"),
+ savedFilter: Cookies.get("defaultFilter.v2"),
+ filters: historicFilters
+ });
+ $(selectors.historyMenu).html(historyMenuHTML);
+}
+
+function appendFilterToHistory(text) {
+ // require non empty text and enabled appends
+ if (!text || !appendsEnabled) return false;
+
+ // final filter list we'll save to storage
+ var filterList = new Set([ text ]);
+
+ // get current history list from storage and append it to our final list
+ // of filters, but avoid duplicates
+ const history = historyStorage.getItem(historyKey);
+ if (history) {
+ const historyArr = history.split("\n");
+ for (var i = 0; i < historyArr.length; i++) {
+ filterList.add(historyArr[i]);
+ }
+ }
+
+ // truncate the history to up to 11 elements
+ const filterListTrunc = Array.from(filterList).slice(0, 10);
+
+ historyStorage.setItem(historyKey, filterListTrunc.join("\n"));
+}
+
function setFilters() {
setUpdating();
// update location so it's easy to share it
querystring.update("q", getFilters().join(","));
+ // append filter to the history and render it
+ appendFilterToHistory(getFilters().join(","));
+ renderHistory();
+
// reload alerts
unsee.triggerReload();
}
-function init() {
+function init(historyStore) {
+ historyStorage = historyStore;
var initialFilter;
if ($(selectors.filter).data("default-used") == "false" || $(selectors.filter).data("default-used") === false) {
@@ -141,10 +214,18 @@ function init() {
$("body").css("padding-top", $(".navbar").height());
});
+ renderHistory();
+ $(selectors.historyMenu).on("click", "a.history-menu-item", function(event) {
+ var elem = $(event.target).parents("li.history-menu");
+ const filtersList = elem.find(".rawFilter").text().trim().split(",");
+ applyFilterList(filtersList);
+ });
+
}
exports.init = init;
exports.addFilter = addFilter;
+exports.clearFilters = clearFilters;
exports.setFilters = setFilters;
exports.getFilters = getFilters;
exports.addBadge = addBadge;
@@ -152,3 +233,4 @@ exports.reloadBadges = reloadBadges;
exports.updateDone = updateDone;
exports.setUpdating = setUpdating;
exports.setPause = setPause;
+exports.renderHistory = renderHistory;
diff --git a/assets/static/filters.test.js b/assets/static/filters.test.js
index 14b52a818..0fe7ad582 100644
--- a/assets/static/filters.test.js
+++ b/assets/static/filters.test.js
@@ -1,7 +1,133 @@
+const $ = window.jQuery = require("jquery");
+const LocalStorageMock = require("./__mocks__/localStorageMock");
+
test("filters addBadge()", () => {
- // mock data attr with default filter
- //document.body.innerHTML = "";
- window.jQuery = require("jquery");
const filters = require("./filters");
filters.addBadge("foo=bar");
});
+
+test("default filter should be in history after setting filter to foo", () => {
+ LocalStorageMock.clear();
+ document.body.innerHTML =
+ "" +
+ "";
+
+ const templatesMock = require("./__mocks__/templatesMock");
+ document.body.innerHTML += templatesMock.loadTemplates();
+ const templates = require("./templates");
+ templates.init();
+
+ const autocomplete = require("./autocomplete");
+ const filters = require("./filters");
+
+ autocomplete.init();
+ filters.init(LocalStorageMock);
+ filters.setFilters();
+ filters.renderHistory();
+
+ // use snapshot to check that generated HTML is what we expect
+ const historyMenu = $("#historyMenu").html().trim();
+ expect(historyMenu).toMatchSnapshot();
+
+ // we set foo, so that what should be in history
+ expect(LocalStorageMock.getItem("filterHistory")).toBe("foo");
+});
+
+test("appended filtes should be present in history", () => {
+ LocalStorageMock.clear();
+ document.body.innerHTML =
+ "" +
+ "";
+
+ const templatesMock = require("./__mocks__/templatesMock");
+ document.body.innerHTML += templatesMock.loadTemplates();
+ const templates = require("./templates");
+ templates.init();
+
+ const autocomplete = require("./autocomplete");
+ const filters = require("./filters");
+
+ autocomplete.init();
+ filters.init(LocalStorageMock);
+ filters.setFilters();
+ filters.renderHistory();
+
+ // we only used default, so there should be a single (default) entry
+ let historyMenu = $("#historyMenu").html().trim();
+ expect(historyMenu).toMatchSnapshot();
+ // and that's what history should have
+ expect(LocalStorageMock.getItem("filterHistory")).toBe("default");
+
+ // now we append more filters, so q=default becomes q=default,bar
+ filters.addFilter("bar");
+ filters.setFilters();
+ // now we got non-default filter as active, so we should have 2 entries
+ // both for default (as recent and as global default)
+ historyMenu = $("#historyMenu").html().trim();
+ expect(historyMenu).toMatchSnapshot();
+ expect(
+ LocalStorageMock.getItem("filterHistory").split("\n")
+ ).toMatchObject(
+ [ "default,bar", "default" ]
+ );
+
+ // append another filter, so we now have: q=default,bar,@state=active
+ filters.addFilter("@state=active");
+ filters.setFilters();
+ // now we should have 3 entries, 2x default + default,bar
+ historyMenu = $("#historyMenu").html().trim();
+ expect(historyMenu).toMatchSnapshot();
+ expect(
+ LocalStorageMock.getItem("filterHistory").split("\n")
+ ).toMatchObject(
+ [ "default,bar,@state=active", "default,bar", "default" ]
+ );
+
+ // clear filters, so now we have: q=
+ filters.clearFilters();
+ filters.setFilters();
+ // now we should have 4 entries, 2x default + default,bar + default,bar,@state=active
+ historyMenu = $("#historyMenu").html().trim();
+ expect(historyMenu).toMatchSnapshot();
+ expect(
+ LocalStorageMock.getItem("filterHistory").split("\n")
+ ).toMatchObject(
+ [ "default,bar,@state=active", "default,bar", "default" ]
+ );
+
+ // now add a filter back, so now we have: q=@state=active
+ filters.addFilter("@state=active");
+ filters.setFilters();
+ // we should have same filters as before
+ historyMenu = $("#historyMenu").html().trim();
+ expect(historyMenu).toMatchSnapshot();
+ expect(
+ LocalStorageMock.getItem("filterHistory").split("\n")
+ ).toMatchObject(
+ [ "@state=active", "default,bar,@state=active", "default,bar", "default" ]
+ );
+
+ // as a last test add default back to have @state=active rendered
+ filters.clearFilters();
+ filters.addFilter("default");
+ filters.setFilters();
+ // we should have same filters as before
+ historyMenu = $("#historyMenu").html().trim();
+ expect(historyMenu).toMatchSnapshot();
+ // default should move from the bottom to to top of the list
+ expect(
+ LocalStorageMock.getItem("filterHistory").split("\n")
+ ).toMatchObject(
+ [ "default", "@state=active", "default,bar,@state=active", "default,bar" ]
+ );
+});
diff --git a/assets/static/templates.js b/assets/static/templates.js
index c2e8f605a..2035ed6cb 100644
--- a/assets/static/templates.js
+++ b/assets/static/templates.js
@@ -45,7 +45,11 @@ var templates = {},
alertGroupLabels: "#alert-group-labels",
alertGroupElements: "#alert-group-elements",
alertGroupSilence: "#alert-group-silence",
- alertGroupLabelMap: "#alert-group-label-map"
+ alertGroupLabelMap: "#alert-group-label-map",
+
+ // history dropdown
+ historyMenu: "#history-menu",
+ historyMenuItem: "#history-menu-item"
};
function getConfig() {
diff --git a/assets/static/unsee.js b/assets/static/unsee.js
index 9157c25c2..49b5e1b9c 100644
--- a/assets/static/unsee.js
+++ b/assets/static/unsee.js
@@ -304,7 +304,7 @@ function setupPageVisibilityHandler() {
}
}
-function init() {
+function init(localStore) {
progress.init();
config.init({
@@ -318,7 +318,7 @@ function init() {
summary.init();
grid.init();
autocomplete.init();
- filters.init();
+ filters.init(localStore);
watchdog.init(30, 60*15); // set watchdog to 15 minutes
$(selectors.refreshButton).click(function() {
@@ -331,7 +331,7 @@ function init() {
setupPageVisibilityHandler();
}
-function onReady() {
+function onReady(localStore) {
// wrap all inits so we can handle errors
try {
// init all elements using bootstrapSwitch
@@ -347,7 +347,7 @@ function onReady() {
ui.setupModal();
silence.setupSilenceForm();
unsilence.init();
- init();
+ init(localStore);
// delay initial alert load to allow browser finish rendering
setTimeout(function() {
@@ -381,4 +381,6 @@ exports.flash = flash;
exports.parseAJAXError = parseAJAXError;
exports.onReady = onReady;
-$(document).ready(onReady);
+$(document).ready(function() {
+ onReady(window.localStorage);
+});
diff --git a/assets/static/unsee.test.js b/assets/static/unsee.test.js
index cb84b8fab..c24ceee71 100644
--- a/assets/static/unsee.test.js
+++ b/assets/static/unsee.test.js
@@ -1,6 +1,7 @@
const $ = require("jquery");
const templatesMock = require("./__mocks__/templatesMock");
const alertsMock = require("./__mocks__/alertsMock");
+const LocalStorageMock = require("./__mocks__/localStorageMock");
jest.useFakeTimers();
@@ -50,7 +51,7 @@ if (alertsServer) {
require("bootstrap/js/popover.js");
alertsServer.start();
- unsee.onReady();
+ unsee.onReady(LocalStorageMock);
unsee.triggerReload();
jest.runOnlyPendingTimers();
// we should have 2 alerts
diff --git a/assets/templates/history.html b/assets/templates/history.html
new file mode 100644
index 000000000..55c8e9943
--- /dev/null
+++ b/assets/templates/history.html
@@ -0,0 +1,34 @@
+
+
+
diff --git a/assets/templates/index.html b/assets/templates/index.html
index 5c600fe72..b2eb79523 100644
--- a/assets/templates/index.html
+++ b/assets/templates/index.html
@@ -47,6 +47,13 @@