diff --git a/web/blob/static/js/graph.js b/web/blob/static/js/graph.js index c054c03c01..d1e66b8c48 100644 --- a/web/blob/static/js/graph.js +++ b/web/blob/static/js/graph.js @@ -73,7 +73,6 @@ Prometheus.Graph.prototype.initialize = function() { self.rangeInput = self.queryForm.find("input[name=range_input]"); self.stackedBtn = self.queryForm.find(".stacked_btn"); 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.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.graphArea = graphWrapper.find(".graph_area"); 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=dec_end]").click(function() { self.decreaseEnd(); }); - self.insertMetric.change(function() { - self.expr.selection("replace", {text: self.insertMetric.val(), mode: "before"}); - self.expr.focus(); // refocusing - }); - - self.populateInsertableMetrics(); + self.populateAutocompleteMetrics(); if (self.expr.val()) { 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; $.ajax({ method: "GET", @@ -183,12 +238,40 @@ Prometheus.Graph.prototype.populateInsertableMetrics = function() { self.showError("Error loading available metrics!") return; } - var metrics = json.data; - for (var i = 0; i < metrics.length; i++) { - self.insertMetric[0].options.add(new Option(metrics[i], metrics[i])); - } + + // For the typeahead autocompletion, we need to remember where to put + // 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({ - 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" }); // This needs to happen after attaching the typeahead plugin, as it diff --git a/web/blob/static/js/graph_template.handlebar b/web/blob/static/js/graph_template.handlebar index 7dfbd8d03e..da71dca4ce 100644 --- a/web/blob/static/js/graph_template.handlebar +++ b/web/blob/static/js/graph_template.handlebar @@ -12,9 +12,6 @@