Merge pull request #158 from cloudflare/history

Save filters usage history to local storage and provide a dropdown me…
This commit is contained in:
Łukasz Mierzwa
2017-08-14 11:44:26 -07:00
committed by GitHub
11 changed files with 691 additions and 13 deletions

View File

@@ -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();

View File

@@ -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);

View File

@@ -0,0 +1,390 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`appended filtes should be present in history 1`] = `
"<li role=\\"separator\\" class=\\"divider\\"></li>
<li class=\\"history-menu\\">
<a class=\\"cursor-pointer history-menu-item\\">
<span class=\\"rawFilter hidden\\">
default
</span>
<i class=\\"fa fa-home\\"></i>
<span class=\\"label-list label label-info\\">
default
</span>
</a>
</li>"
`;
exports[`appended filtes should be present in history 2`] = `
"<li class=\\"history-menu\\">
<a class=\\"cursor-pointer history-menu-item\\">
<span class=\\"rawFilter hidden\\">
default
</span>
<i class=\\"fa fa-search\\"></i>
<span class=\\"label-list label label-info\\">
default
</span>
</a>
</li>
<li role=\\"separator\\" class=\\"divider\\"></li>
<li class=\\"history-menu\\">
<a class=\\"cursor-pointer history-menu-item\\">
<span class=\\"rawFilter hidden\\">
default
</span>
<i class=\\"fa fa-home\\"></i>
<span class=\\"label-list label label-info\\">
default
</span>
</a>
</li>"
`;
exports[`appended filtes should be present in history 3`] = `
"<li class=\\"history-menu\\">
<a class=\\"cursor-pointer history-menu-item\\">
<span class=\\"rawFilter hidden\\">
default,bar
</span>
<i class=\\"fa fa-search\\"></i>
<span class=\\"label-list label label-info\\">
default
</span>
<span class=\\"label-list label label-info\\">
bar
</span>
</a>
</li>
<li class=\\"history-menu\\">
<a class=\\"cursor-pointer history-menu-item\\">
<span class=\\"rawFilter hidden\\">
default
</span>
<i class=\\"fa fa-search\\"></i>
<span class=\\"label-list label label-info\\">
default
</span>
</a>
</li>
<li role=\\"separator\\" class=\\"divider\\"></li>
<li class=\\"history-menu\\">
<a class=\\"cursor-pointer history-menu-item\\">
<span class=\\"rawFilter hidden\\">
default
</span>
<i class=\\"fa fa-home\\"></i>
<span class=\\"label-list label label-info\\">
default
</span>
</a>
</li>"
`;
exports[`appended filtes should be present in history 4`] = `
"<li class=\\"history-menu\\">
<a class=\\"cursor-pointer history-menu-item\\">
<span class=\\"rawFilter hidden\\">
default,bar,@state=active
</span>
<i class=\\"fa fa-search\\"></i>
<span class=\\"label-list label label-info\\">
default
</span>
<span class=\\"label-list label label-info\\">
bar
</span>
<span class=\\"label-list label label-info\\">
@state=active
</span>
</a>
</li>
<li class=\\"history-menu\\">
<a class=\\"cursor-pointer history-menu-item\\">
<span class=\\"rawFilter hidden\\">
default,bar
</span>
<i class=\\"fa fa-search\\"></i>
<span class=\\"label-list label label-info\\">
default
</span>
<span class=\\"label-list label label-info\\">
bar
</span>
</a>
</li>
<li class=\\"history-menu\\">
<a class=\\"cursor-pointer history-menu-item\\">
<span class=\\"rawFilter hidden\\">
default
</span>
<i class=\\"fa fa-search\\"></i>
<span class=\\"label-list label label-info\\">
default
</span>
</a>
</li>
<li role=\\"separator\\" class=\\"divider\\"></li>
<li class=\\"history-menu\\">
<a class=\\"cursor-pointer history-menu-item\\">
<span class=\\"rawFilter hidden\\">
default
</span>
<i class=\\"fa fa-home\\"></i>
<span class=\\"label-list label label-info\\">
default
</span>
</a>
</li>"
`;
exports[`appended filtes should be present in history 5`] = `
"<li class=\\"history-menu\\">
<a class=\\"cursor-pointer history-menu-item\\">
<span class=\\"rawFilter hidden\\">
default,bar,@state=active
</span>
<i class=\\"fa fa-search\\"></i>
<span class=\\"label-list label label-info\\">
default
</span>
<span class=\\"label-list label label-info\\">
bar
</span>
<span class=\\"label-list label label-info\\">
@state=active
</span>
</a>
</li>
<li class=\\"history-menu\\">
<a class=\\"cursor-pointer history-menu-item\\">
<span class=\\"rawFilter hidden\\">
default,bar
</span>
<i class=\\"fa fa-search\\"></i>
<span class=\\"label-list label label-info\\">
default
</span>
<span class=\\"label-list label label-info\\">
bar
</span>
</a>
</li>
<li class=\\"history-menu\\">
<a class=\\"cursor-pointer history-menu-item\\">
<span class=\\"rawFilter hidden\\">
default
</span>
<i class=\\"fa fa-search\\"></i>
<span class=\\"label-list label label-info\\">
default
</span>
</a>
</li>
<li role=\\"separator\\" class=\\"divider\\"></li>
<li class=\\"history-menu\\">
<a class=\\"cursor-pointer history-menu-item\\">
<span class=\\"rawFilter hidden\\">
default
</span>
<i class=\\"fa fa-home\\"></i>
<span class=\\"label-list label label-info\\">
default
</span>
</a>
</li>"
`;
exports[`appended filtes should be present in history 6`] = `
"<li class=\\"history-menu\\">
<a class=\\"cursor-pointer history-menu-item\\">
<span class=\\"rawFilter hidden\\">
@state=active
</span>
<i class=\\"fa fa-search\\"></i>
<span class=\\"label-list label label-info\\">
@state=active
</span>
</a>
</li>
<li class=\\"history-menu\\">
<a class=\\"cursor-pointer history-menu-item\\">
<span class=\\"rawFilter hidden\\">
default,bar,@state=active
</span>
<i class=\\"fa fa-search\\"></i>
<span class=\\"label-list label label-info\\">
default
</span>
<span class=\\"label-list label label-info\\">
bar
</span>
<span class=\\"label-list label label-info\\">
@state=active
</span>
</a>
</li>
<li class=\\"history-menu\\">
<a class=\\"cursor-pointer history-menu-item\\">
<span class=\\"rawFilter hidden\\">
default,bar
</span>
<i class=\\"fa fa-search\\"></i>
<span class=\\"label-list label label-info\\">
default
</span>
<span class=\\"label-list label label-info\\">
bar
</span>
</a>
</li>
<li role=\\"separator\\" class=\\"divider\\"></li>
<li class=\\"history-menu\\">
<a class=\\"cursor-pointer history-menu-item\\">
<span class=\\"rawFilter hidden\\">
default
</span>
<i class=\\"fa fa-home\\"></i>
<span class=\\"label-list label label-info\\">
default
</span>
</a>
</li>"
`;
exports[`default filter should be in history after setting filter to foo 1`] = `
"<li role=\\"separator\\" class=\\"divider\\"></li>
<li class=\\"history-menu\\">
<a class=\\"cursor-pointer history-menu-item\\">
<span class=\\"rawFilter hidden\\">
default
</span>
<i class=\\"fa fa-home\\"></i>
<span class=\\"label-list label label-info\\">
default
</span>
</a>
</li>"
`;

View File

@@ -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;
}

View File

@@ -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;

View File

@@ -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 = "<div id='filter' data-default-filter='foo=bar'></div>";
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 =
"<div class='input-group filterbar'>" +
" <div class='input-group-addon input-sm'>" +
" <i class='fa fa-search' id='filter-icon'></i>" +
" </div>" +
" <input id='filter' type='text' value='foo' data-default-used='false' data-default-filter='default'>" +
"</div>" +
"<div id='historyMenu'></div>";
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 =
"<div class='input-group filterbar'>" +
" <div class='input-group-addon input-sm'>" +
" <i class='fa fa-search' id='filter-icon'></i>" +
" </div>" +
" <input id='filter' type='text' value='default' data-default-used='true' data-default-filter='default'>" +
"</div>" +
"<div id='historyMenu'></div>";
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" ]
);
});

View File

@@ -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() {

View File

@@ -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);
});

View File

@@ -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

View File

@@ -0,0 +1,34 @@
<script type="application/json" id="history-menu">
<% if (filters.length) { %>
<% _.each(filters, function(filter) { %>
<% if (filter !== activeFilter) { %>
<%= renderTemplate("historyMenuItem", {filter: filter, icon: "fa fa-search"}) %>
<% } %>
<% }) %>
<% } %>
<% if (defaultFilter || savedFilter) { %>
<li role="separator" class="divider"></li>
<% if (defaultFilter) { %>
<%= renderTemplate("historyMenuItem", {filter: defaultFilter, icon: "fa fa-home"}) %>
<% } %>
<% if (savedFilter) { %>
<%= renderTemplate("historyMenuItem", {filter: savedFilter, icon: "fa fa-floppy-o"}) %>
<% } %>
<% } %>
</script>
<script type="application/json" id="history-menu-item">
<li class="history-menu">
<a class="cursor-pointer history-menu-item">
<span class="rawFilter hidden">
<%- filter %>
</span>
<i class="<%- icon %>"></i>
<% _.each(filter.split(","), function(filterItem) { %>
<span class="label-list label label-info">
<%- filterItem %>
</span>
<% }) %>
</a>
</li>
</script>

View File

@@ -47,6 +47,13 @@
</div>
</form>
<ul class="nav navbar-nav navbar-right">
<li class="dropdown">
<a href="#" id="history" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false"
title="Filter history" data-toggle="tooltip" data-placement="auto">
<i id="historyList" class="fa fa-history"></i>
</a>
<ul class="dropdown-menu" id="historyMenu"></ul>
</li>
<li>
<a href="{{ .WebPrefix }}help" id="help" role="button" title="Filter documentation" data-toggle="tooltip" data-placement="auto">
<i class="fa fa-question-circle"></i>
@@ -177,3 +184,4 @@
{{ template "templates/errors.html" }}
{{ template "templates/modal.html" }}
{{ template "templates/silence.html" }}
{{ template "templates/history.html" }}