Implement browsing of previous query expressions (#3486)

This commit is contained in:
Lovisa Svallingson 2017-12-21 10:58:05 -07:00 committed by Julius Volz
parent 78625f85a7
commit a8ff643464
5 changed files with 273 additions and 152 deletions

File diff suppressed because one or more lines are too long

View file

@ -6,6 +6,30 @@ body {
font-size: 11px; font-size: 11px;
} }
div.query-history {
font-size: 0.8em;
padding-top: 1em;
}
div.query-history:hover {
cursor: pointer;
}
div.query-history button {
background-color: transparent;
border: none;
outline: none;
padding: 0;
}
div.query-history.is-checked {
color: #286090;
}
div.page-options {
display: flex;
}
.graph_wrapper { .graph_wrapper {
margin-top: 12px; margin-top: 12px;
} }

View file

@ -3,6 +3,9 @@ var graphTemplate;
var SECOND = 1000; var SECOND = 1000;
/**
* Graph
*/
Prometheus.Graph = function(element, options, handleChange, handleRemove) { Prometheus.Graph = function(element, options, handleChange, handleRemove) {
this.el = element; this.el = element;
this.graphHTML = null; this.graphHTML = null;
@ -61,8 +64,8 @@ Prometheus.Graph.prototype.initialize = function() {
self.expr = graphWrapper.find("textarea[name=expr]"); self.expr = graphWrapper.find("textarea[name=expr]");
self.expr.keypress(function(e) { self.expr.keypress(function(e) {
// Enter was pressed without the shift key. const enter = 13;
if (e.which == 13 && !e.shiftKey) { if (e.which == enter && !e.shiftKey) {
self.queryForm.submit(); self.queryForm.submit();
e.preventDefault(); e.preventDefault();
} }
@ -183,8 +186,14 @@ Prometheus.Graph.prototype.initialize = function() {
}); });
self.checkTimeDrift(); self.checkTimeDrift();
// initialize query history
if (!localStorage.getItem("history")) {
localStorage.setItem("history", JSON.stringify([]));
}
self.populateInsertableMetrics(); self.populateInsertableMetrics();
queryHistory.bindHistoryEvents(self);
if (self.expr.val()) { if (self.expr.val()) {
self.submitQuery(); self.submitQuery();
} }
@ -230,9 +239,10 @@ Prometheus.Graph.prototype.populateInsertableMetrics = function() {
self.showError("Error loading available metrics!"); self.showError("Error loading available metrics!");
return; return;
} }
var metrics = json.data;
for (var i = 0; i < metrics.length; i++) { pageConfig.allMetrics = json.data; // todo: do we need self.allMetrics? Or can it just live on the page
self.insertMetric[0].options.add(new Option(metrics[i], metrics[i])); for (var i = 0; i < pageConfig.allMetrics.length; i++) {
self.insertMetric[0].options.add(new Option(pageConfig.allMetrics[i], pageConfig.allMetrics[i]));
} }
self.fuzzyResult = { self.fuzzyResult = {
@ -241,41 +251,7 @@ Prometheus.Graph.prototype.populateInsertableMetrics = function() {
map: {} map: {}
} }
self.expr.typeahead({ self.initTypeahead(self);
source: metrics,
items: "all",
matcher: function(item) {
// If we have result for current query, skip
if (self.fuzzyResult.query !== this.query) {
self.fuzzyResult.query = this.query;
self.fuzzyResult.map = {};
self.fuzzyResult.result = fuzzy.filter(this.query.replace(/ /g, ''), metrics, {
pre: '<strong>',
post: '</strong>'
});
self.fuzzyResult.result.forEach(function(r) {
self.fuzzyResult.map[r.original] = r;
});
}
return item in self.fuzzyResult.map;
},
sorter: function(items) {
items.sort(function(a,b) {
var i = self.fuzzyResult.map[b].score - self.fuzzyResult.map[a].score;
return i === 0 ? a.localeCompare(b) : i;
});
return items;
},
highlighter: function (item) {
return $('<div>' + self.fuzzyResult.map[item].string + '</div>')
},
});
// This needs to happen after attaching the typeahead plugin, as it
// otherwise breaks the typeahead functionality.
self.expr.focus();
}, },
error: function() { error: function() {
self.showError("Error loading available metrics!"); self.showError("Error loading available metrics!");
@ -283,6 +259,47 @@ Prometheus.Graph.prototype.populateInsertableMetrics = function() {
}); });
}; };
Prometheus.Graph.prototype.initTypeahead = function(self) {
const historyIsChecked = $("div.query-history").hasClass("is-checked");
const source = historyIsChecked ? pageConfig.allMetrics.concat(JSON.parse(localStorage.getItem("history"))) : pageConfig.allMetrics;
self.expr.typeahead({
source,
items: "all",
matcher: function (item) {
// If we have result for current query, skip
if (self.fuzzyResult.query !== this.query) {
self.fuzzyResult.query = this.query;
self.fuzzyResult.map = {};
self.fuzzyResult.result = fuzzy.filter(this.query.replace(/ /g, ""), this.source, {
pre: "<strong>",
post: "</strong>"
});
self.fuzzyResult.result.forEach(function(r) {
self.fuzzyResult.map[r.original] = r;
});
}
return item in self.fuzzyResult.map;
},
sorter: function (items) {
items.sort(function(a,b) {
var i = self.fuzzyResult.map[b].score - self.fuzzyResult.map[a].score;
return i === 0 ? a.localeCompare(b) : i;
});
return items;
},
highlighter: function (item) {
return $("<div>" + self.fuzzyResult.map[item].string + "</div>");
},
});
// This needs to happen after attaching the typeahead plugin, as it
// otherwise breaks the typeahead functionality.
self.expr.focus();
}
Prometheus.Graph.prototype.getOptions = function() { Prometheus.Graph.prototype.getOptions = function() {
var self = this; var self = this;
var options = {}; var options = {};
@ -429,6 +446,7 @@ Prometheus.Graph.prototype.submitQuery = function() {
self.showError(json.error); self.showError(json.error);
return; return;
} }
queryHistory.handleHistory(self);
success(json.data, textStatus); success(json.data, textStatus);
}, },
error: function(xhr, resp) { error: function(xhr, resp) {
@ -807,25 +825,15 @@ Prometheus.Graph.prototype.formatKMBT = function(y) {
} }
} }
function escapeHTML(string) { /**
var entityMap = { * Page
"&": "&amp;", */
"<": "&lt;", const pageConfig = {
">": "&gt;", graphs: []
'"': '&quot;',
"'": '&#39;',
"/": '&#x2F;'
};
return String(string).replace(/[&<>"'\/]/g, function (s) {
return entityMap[s];
});
}
Prometheus.Page = function() {
this.graphs = [];
}; };
Prometheus.Page = function() {};
Prometheus.Page.prototype.init = function() { Prometheus.Page.prototype.init = function() {
var graphOptions = this.parseURL(); var graphOptions = this.parseURL();
if (graphOptions.length === 0) { if (graphOptions.length === 0) {
@ -833,7 +841,6 @@ Prometheus.Page.prototype.init = function() {
} }
graphOptions.forEach(this.addGraph, this); graphOptions.forEach(this.addGraph, this);
$("#add_graph").click(this.addGraph.bind(this, {})); $("#add_graph").click(this.addGraph.bind(this, {}));
}; };
@ -855,7 +862,9 @@ Prometheus.Page.prototype.addGraph = function(options) {
this.removeGraph.bind(this) this.removeGraph.bind(this)
); );
this.graphs.push(graph); // this.graphs.push(graph);
pageConfig.graphs.push(graph);
$(window).resize(function() { $(window).resize(function() {
graph.resizeGraph(); graph.resizeGraph();
}); });
@ -863,7 +872,7 @@ Prometheus.Page.prototype.addGraph = function(options) {
// NOTE: This needs to be kept in sync with /util/strutil/strconv.go:GraphLinkForExpression // NOTE: This needs to be kept in sync with /util/strutil/strconv.go:GraphLinkForExpression
Prometheus.Page.prototype.updateURL = function() { Prometheus.Page.prototype.updateURL = function() {
var queryString = this.graphs.map(function(graph, index) { var queryString = pageConfig.graphs.map(function(graph, index) {
var graphOptions = graph.getOptions(); var graphOptions = graph.getOptions();
var queryParamHelper = new Prometheus.Page.QueryParamHelper(); var queryParamHelper = new Prometheus.Page.QueryParamHelper();
var queryObject = queryParamHelper.generateQueryObject(graphOptions, index); var queryObject = queryParamHelper.generateQueryObject(graphOptions, index);
@ -874,7 +883,7 @@ Prometheus.Page.prototype.updateURL = function() {
}; };
Prometheus.Page.prototype.removeGraph = function(graph) { Prometheus.Page.prototype.removeGraph = function(graph) {
this.graphs = this.graphs.filter(function(g) {return g !== graph}); pageConfig.graphs = pageConfig.graphs.filter(function(g) {return g !== graph});
}; };
Prometheus.Page.QueryParamHelper = function() {}; Prometheus.Page.QueryParamHelper = function() {};
@ -950,29 +959,6 @@ Prometheus.Page.QueryParamHelper.prototype.generateQueryObject = function(graphO
return queryObject; return queryObject;
}; };
function init() {
$.ajaxSetup({
cache: false
});
$.ajax({
url: PATH_PREFIX + "/static/js/graph_template.handlebar?v=" + BUILD_VERSION,
success: function(data) {
graphTemplate = data;
Mustache.parse(data);
if (isDeprecatedGraphURL()) {
redirectToMigratedURL();
} else {
var Page = new Prometheus.Page();
Page.init();
}
}
});
}
// These two methods (isDeprecatedGraphURL and redirectToMigratedURL) // These two methods (isDeprecatedGraphURL and redirectToMigratedURL)
// are added only for backward compatibility to old query format. // are added only for backward compatibility to old query format.
function isDeprecatedGraphURL() { function isDeprecatedGraphURL() {
@ -1004,4 +990,108 @@ function redirectToMigratedURL() {
window.location = PATH_PREFIX + "/graph?" + query; window.location = PATH_PREFIX + "/graph?" + query;
} }
/**
* Query History helper functions
* **/
const queryHistory = {
bindHistoryEvents: function(graph) {
const targetEl = $('div.query-history');
const icon = $(targetEl).children('i');
targetEl.off('click');
if (JSON.parse(localStorage.getItem('enable-query-history'))) {
this.toggleOn(targetEl);
}
targetEl.on('click', function() {
if (icon.hasClass('glyphicon-unchecked')) {
queryHistory.toggleOn(targetEl);
} else if (icon.hasClass('glyphicon-check')) {
queryHistory.toggleOff(targetEl);
}
});
},
handleHistory: function(graph) {
const query = graph.expr.val();
const isSimpleMetric = pageConfig.allMetrics.indexOf(query) !== -1;
if (isSimpleMetric) {
return;
}
let parsedQueryHistory = JSON.parse(localStorage.getItem('history'));
const hasStoredQuery = parsedQueryHistory.indexOf(query) !== -1;
if (hasStoredQuery) {
parsedQueryHistory.splice(parsedQueryHistory.indexOf(query), 1);
}
parsedQueryHistory.push(query);
const queryCount = parsedQueryHistory.length;
parsedQueryHistory = parsedQueryHistory.slice(queryCount - 50, queryCount);
localStorage.setItem('history', JSON.stringify(parsedQueryHistory));
this.updateTypeaheadMetricSet(parsedQueryHistory);
},
toggleOn: function(targetEl) {
this.updateTypeaheadMetricSet(JSON.parse(localStorage.getItem('history')));
$(targetEl).children('i').removeClass('glyphicon-unchecked').addClass('glyphicon-check');
targetEl.addClass('is-checked');
localStorage.setItem('enable-query-history', true);
},
toggleOff: function(targetEl) {
this.updateTypeaheadMetricSet();
$(targetEl).children('i').removeClass('glyphicon-check').addClass('glyphicon-unchecked');
targetEl.removeClass('is-checked');
localStorage.setItem('enable-query-history', false);
},
updateTypeaheadMetricSet: function(metricSet) {
pageConfig.graphs.forEach(function(graph) {
graph.expr.data('typeahead').source = metricSet ? pageConfig.allMetrics.concat(metricSet) : pageConfig.allMetrics;
});
}
};
function escapeHTML(string) {
var entityMap = {
"&": "&amp;",
"<": "&lt;",
">": "&gt;",
'"': '&quot;',
"'": '&#39;',
"/": '&#x2F;'
};
return String(string).replace(/[&<>"'\/]/g, function (s) {
return entityMap[s];
});
}
function init() {
$.ajaxSetup({
cache: false
});
$.ajax({
url: PATH_PREFIX + "/static/js/graph/graph_template.handlebar?v=" + BUILD_VERSION,
success: function(data) {
graphTemplate = data;
Mustache.parse(data);
if (isDeprecatedGraphURL()) {
redirectToMigratedURL();
} else {
var Page = new Prometheus.Page();
Page.init();
}
}
});
}
$(init); $(init);

View file

@ -17,14 +17,19 @@
<script src="{{ pathPrefix }}/static/vendor/js/jquery.selection.js?v={{ buildVersion }}"></script> <script src="{{ pathPrefix }}/static/vendor/js/jquery.selection.js?v={{ buildVersion }}"></script>
<script src="{{ pathPrefix }}/static/vendor/js/jquery.hotkeys.js?v={{ buildVersion }}"></script> <script src="{{ pathPrefix }}/static/vendor/js/jquery.hotkeys.js?v={{ buildVersion }}"></script>
<script src="{{ pathPrefix }}/static/js/graph.js?v={{ buildVersion }}"></script> <script src="{{ pathPrefix }}/static/js/graph/index.js?v={{ buildVersion }}"></script>
<script id="graph_template" type="text/x-handlebars-template"></script> <script id="graph_template" type="text/x-handlebars-template"></script>
{{end}} {{end}}
{{define "content"}} {{define "content"}}
<div id="graph_container" class="container-fluid"> <div id="graph_container" class="container-fluid">
<div class="query-history">
<i class="glyphicon glyphicon-unchecked"></i>
<button type="button" class="search-history" title="search previous queries">Enable query history</button>
</div>
</div> </div>
<div class="container-fluid"> <div class="container-fluid">
<div><input class="btn btn-primary" type="submit" value="Add Graph" id="add_graph"></div> <div><input class="btn btn-primary" type="submit" value="Add Graph" id="add_graph"></div>
</div> </div>