From 15c58c0f3ea575b855135977e590c6918e8ba9e5 Mon Sep 17 00:00:00 2001 From: Julius Volz Date: Mon, 20 Jul 2015 20:24:08 +0200 Subject: [PATCH] Enable autocomplete anywhere in expression. This enables metric name autocompletion for every word in an expression, not just the very first one. It would be great to also support all language keywords during autocompletion in the future. --- web/blob/static/js/graph.js | 125 ++++++++++++++++---- web/blob/static/js/graph_template.handlebar | 3 - 2 files changed, 104 insertions(+), 24 deletions(-) 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 @@
-