Merge pull request #907 from prometheus/better-autocomplete

Enable autocomplete anywhere in expression.
This commit is contained in:
Julius Volz 2015-10-15 22:16:40 +02:00
commit 5a0ce511dc
3 changed files with 307 additions and 190 deletions

File diff suppressed because one or more lines are too long

View file

@ -73,7 +73,6 @@ Prometheus.Graph.prototype.initialize = function() {
self.rangeInput = self.queryForm.find("input[name=range_input]"); self.rangeInput = self.queryForm.find("input[name=range_input]");
self.stackedBtn = self.queryForm.find(".stacked_btn"); self.stackedBtn = self.queryForm.find(".stacked_btn");
self.stacked = self.queryForm.find("input[name=stacked]"); self.stacked = self.queryForm.find("input[name=stacked]");
self.insertMetric = self.queryForm.find("select[name=insert_metric]");
self.refreshInterval = self.queryForm.find("select[name=refresh]"); self.refreshInterval = self.queryForm.find("select[name=refresh]");
self.consoleTab = graphWrapper.find(".console"); self.consoleTab = graphWrapper.find(".console");
@ -90,14 +89,6 @@ Prometheus.Graph.prototype.initialize = function() {
} }
}); });
// Return moves focus back to expr instead of submitting.
self.insertMetric.bind("keydown", "return", function(e) {
self.expr.focus();
self.expr.val(self.expr.val());
return e.preventDefault();
})
self.error = graphWrapper.find(".error").hide(); self.error = graphWrapper.find(".error").hide();
self.graphArea = graphWrapper.find(".graph_area"); self.graphArea = graphWrapper.find(".graph_area");
self.graph = self.graphArea.find(".graph"); self.graph = self.graphArea.find(".graph");
@ -160,19 +151,83 @@ Prometheus.Graph.prototype.initialize = function() {
self.queryForm.find("button[name=inc_end]").click(function() { self.increaseEnd(); }); self.queryForm.find("button[name=inc_end]").click(function() { self.increaseEnd(); });
self.queryForm.find("button[name=dec_end]").click(function() { self.decreaseEnd(); }); self.queryForm.find("button[name=dec_end]").click(function() { self.decreaseEnd(); });
self.insertMetric.change(function() { self.populateAutocompleteMetrics();
self.expr.selection("replace", {text: self.insertMetric.val(), mode: "before"});
self.expr.focus(); // refocusing
});
self.populateInsertableMetrics();
if (self.expr.val()) { if (self.expr.val()) {
self.submitQuery(); self.submitQuery();
} }
}; };
Prometheus.Graph.prototype.populateInsertableMetrics = function() { // Returns true if the character at "pos" in the expression string "str" looks
// like it could be a metric name (if it's not in a string or a label matchers
// section).
function isPotentialMetric(str, pos) {
var quote = null;
var inMatchers = false;
for (var i = 0; i < pos; i++) {
var ch = str[i];
// Skip over escaped characters (quotes or otherwise) in strings.
if (quote !== null && ch === "\\") {
i += 1;
continue;
}
// Track if we are entering or leaving a string.
switch (ch) {
case quote:
quote = null;
break;
case '"':
case "'":
quote = ch;
break;
}
// Ignore curly braces in strings.
if (quote) {
continue;
}
// Track whether we are in curly braces (label matchers).
switch (ch) {
case "{":
inMatchers = true;
break;
case "}":
inMatchers = false;
break;
}
}
return !inMatchers && quote === null;
}
// Returns the current word under the cursor position in $input.
function currentWord($input) {
var wordRE = new RegExp("[a-zA-Z0-9:_]");
var pos = $input.prop("selectionStart");
var str = $input.val();
var len = str.length;
var start = pos;
var end = pos;
while (start > 0 && str[start-1].match(wordRE)) {
start--;
}
while (end < len && str[end].match(wordRE)) {
end++;
}
return {
start: start,
end: end,
word: $input.val().substring(start, end)
};
}
Prometheus.Graph.prototype.populateAutocompleteMetrics = function() {
var self = this; var self = this;
$.ajax({ $.ajax({
method: "GET", method: "GET",
@ -183,12 +238,40 @@ 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++) { // For the typeahead autocompletion, we need to remember where to put
self.insertMetric[0].options.add(new Option(metrics[i], metrics[i])); // the cursor after inserting an autocompleted word (we want to put it
} // after that word, not at the end of the entire input string).
var afterUpdatePos = null;
self.expr.typeahead({ self.expr.typeahead({
source: metrics, // Needs to return true for autocomplete items that should be matched
// by the current input.
matcher: function(item) {
var cw = currentWord(self.expr);
if (cw.word.length !== 0 &&
item.toLowerCase().indexOf(cw.word.toLowerCase()) > -1 &&
isPotentialMetric(self.expr.val(), cw.start)) {
return true;
}
return false;
},
// Returns the entire string to which the input field should be set
// upon selecting an item from the autocomplete list.
updater: function(item) {
var str = self.expr.val();
var cw = currentWord(self.expr);
afterUpdatePos = cw.start + item.length;
return str.substring(0, cw.start) + item + str.substring(cw.end, str.length);
},
// Is called *after* the input field has been set to the string
// returned by the "updater" callback. We want to move the cursor to
// the end of the actually inserted word here.
afterSelect: function(item) {
self.expr.prop("selectionStart", afterUpdatePos);
self.expr.prop("selectionEnd", afterUpdatePos);
},
source: json.data,
items: "all" items: "all"
}); });
// This needs to happen after attaching the typeahead plugin, as it // This needs to happen after attaching the typeahead plugin, as it

View file

@ -12,9 +12,6 @@
<div class="row"> <div class="row">
<div class="col-lg-10"> <div class="col-lg-10">
<input class="btn btn-primary execute_btn" type="submit" value="Execute" name="submit"> <input class="btn btn-primary execute_btn" type="submit" value="Execute" name="submit">
<select class="form-control expression_select" name="insert_metric">
<option value="">- insert metric at cursor -</option>
</select>
</div> </div>
</div> </div>
<div class="row"> <div class="row">