about summary refs log tree commit diff
path: root/bundles/prometheus/files/console_assets/prom_console.js
diff options
context:
space:
mode:
Diffstat (limited to 'bundles/prometheus/files/console_assets/prom_console.js')
-rw-r--r--bundles/prometheus/files/console_assets/prom_console.js687
1 files changed, 687 insertions, 0 deletions
diff --git a/bundles/prometheus/files/console_assets/prom_console.js b/bundles/prometheus/files/console_assets/prom_console.js
new file mode 100644
index 0000000..cb3501c
--- /dev/null
+++ b/bundles/prometheus/files/console_assets/prom_console.js
@@ -0,0 +1,687 @@
+/*
+ * Functions to make it easier to write prometheus consoles, such
+ * as graphs.
+ *
+ */
+PromConsole = {};
+
+PromConsole.NumberFormatter = {};
+PromConsole.NumberFormatter.prefixesBig = ["k", "M", "G", "T", "P", "E", "Z", "Y"];
+PromConsole.NumberFormatter.prefixesBig1024 = ["ki", "Mi", "Gi", "Ti", "Pi", "Ei", "Zi", "Yi"];
+PromConsole.NumberFormatter.prefixesSmall = ["m", "u", "n", "p", "f", "a", "z", "y"];
+
+PromConsole._stripTrailingZero = function(x) {
+  if (x.indexOf("e") == -1) {
+    // It's not safe to strip if it's scientific notation.
+    return x.replace(/\.?0*$/, '');
+  }
+  return x;
+};
+
+// Humanize a number.
+PromConsole.NumberFormatter.humanize = function(x) {
+  if (x === null) {
+    return "null";
+  }
+  var ret = PromConsole.NumberFormatter._humanize(
+    x, PromConsole.NumberFormatter.prefixesBig,
+    PromConsole.NumberFormatter.prefixesSmall, 1000);
+  x = ret[0];
+  var prefix = ret[1];
+  if (Math.abs(x) < 1) {
+    return x.toExponential(3) + prefix;
+  }
+  return PromConsole._stripTrailingZero(x.toFixed(3)) + prefix;
+};
+
+// Humanize a number, don't use milli/micro/etc. prefixes.
+PromConsole.NumberFormatter.humanizeNoSmallPrefix = function(x) {
+  if (x === null) {
+    return "null";
+  }
+  if (Math.abs(x) < 1) {
+    return PromConsole._stripTrailingZero(x.toPrecision(3));
+  }
+  var ret = PromConsole.NumberFormatter._humanize(
+    x, PromConsole.NumberFormatter.prefixesBig,
+    [], 1000);
+  x = ret[0];
+  var prefix = ret[1];
+  return PromConsole._stripTrailingZero(x.toFixed(3)) + prefix;
+};
+
+// Humanize a number with 1024 as the base, rather than 1000.
+PromConsole.NumberFormatter.humanize1024 = function(x) {
+  if (x === null) {
+    return "null";
+  }
+  var ret = PromConsole.NumberFormatter._humanize(
+    x, PromConsole.NumberFormatter.prefixesBig1024,
+    [], 1024);
+  x = ret[0];
+  var prefix = ret[1];
+  if (Math.abs(x) < 1) {
+    return x.toExponential(3) + prefix;
+  }
+  return PromConsole._stripTrailingZero(x.toFixed(3)) + prefix;
+};
+
+// Humanize a number, returning an exact representation.
+PromConsole.NumberFormatter.humanizeExact = function(x) {
+  if (x === null) {
+    return "null";
+  }
+  var ret = PromConsole.NumberFormatter._humanize(
+    x, PromConsole.NumberFormatter.prefixesBig,
+    PromConsole.NumberFormatter.prefixesSmall, 1000);
+  return ret[0] + ret[1];
+};
+
+PromConsole.NumberFormatter._humanize = function(x, prefixesBig, prefixesSmall, factor) {
+  var prefix = "";
+  if (x === 0) {
+    /* Do nothing. */
+  } else if (Math.abs(x) >= 1) {
+    for (var i = 0; i < prefixesBig.length && Math.abs(x) >= factor; ++i) {
+      x /= factor;
+      prefix = prefixesBig[i];
+    }
+  } else {
+    for (var i = 0; i < prefixesSmall.length && Math.abs(x) < 1; ++i) {
+      x *= factor;
+      prefix = prefixesSmall[i];
+    }
+  }
+  return [x, prefix];
+};
+
+
+PromConsole.TimeControl = function() {
+  document.getElementById("prom_graph_duration_shrink").onclick = this.decreaseDuration.bind(this);
+  document.getElementById("prom_graph_duration_grow").onclick = this.increaseDuration.bind(this);
+  document.getElementById("prom_graph_time_back").onclick = this.decreaseEnd.bind(this);
+  document.getElementById("prom_graph_time_forward").onclick = this.increaseEnd.bind(this);
+  document.getElementById("prom_graph_refresh_button").onclick = this.refresh.bind(this);
+  this.durationElement = document.getElementById("prom_graph_duration");
+  this.endElement = document.getElementById("prom_graph_time_end");
+  this.durationElement.oninput = this.dispatch.bind(this);
+  this.endElement.oninput = this.dispatch.bind(this);
+  this.endElement.oninput = this.dispatch.bind(this);
+  this.refreshValueElement = document.getElementById("prom_graph_refresh_button_value");
+
+  var refreshList = document.getElementById("prom_graph_refresh_intervals");
+  var refreshIntervals = ["Off", "1m", "5m", "15m", "1h"];
+  for (var i = 0; i < refreshIntervals.length; ++i) {
+    var li = document.createElement("li");
+    li.onclick = this.setRefresh.bind(this, refreshIntervals[i]);
+    li.textContent = refreshIntervals[i];
+    refreshList.appendChild(li);
+  }
+
+  this.durationElement.value = PromConsole.TimeControl.prototype.getHumanDuration(
+    PromConsole.TimeControl._initialValues.duration);
+  if (!PromConsole.TimeControl._initialValues.endTimeNow) {
+    this.endElement.value = PromConsole.TimeControl.prototype.getHumanDate(
+      new Date(PromConsole.TimeControl._initialValues.endTime * 1000));
+  }
+};
+
+PromConsole.TimeControl.timeFactors = {
+  "y": 60 * 60 * 24 * 365,
+  "w": 60 * 60 * 24 * 7,
+  "d": 60 * 60 * 24,
+  "h": 60 * 60,
+  "m": 60,
+  "s": 1
+};
+
+PromConsole.TimeControl.stepValues = [
+  "10s", "1m", "5m", "15m", "30m", "1h", "2h", "6h", "12h", "1d", "2d",
+  "1w", "2w", "4w", "8w", "1y", "2y"
+];
+
+PromConsole.TimeControl.prototype._setHash = function() {
+  var duration = this.parseDuration(this.durationElement.value);
+  var endTime = this.getEndDate() / 1000;
+  window.location.hash = "#pctc" + encodeURIComponent(JSON.stringify(
+    { duration: duration, endTime: endTime }));
+};
+
+PromConsole.TimeControl._initialValues = function() {
+  var hash = window.location.hash;
+  var values;
+  if (hash.indexOf('#pctc') === 0) {
+    values = JSON.parse(decodeURIComponent(hash.substring(5)));
+  } else {
+    values = { duration: 3600, endTime: 0 };
+  }
+  if (values.endTime == 0) {
+    values.endTime = new Date().getTime() / 1000;
+    values.endTimeNow = true;
+  }
+  return values;
+}();
+
+PromConsole.TimeControl.prototype.parseDuration = function(durationText) {
+  var durationRE = new RegExp("^([0-9]+)([ywdhms]?)$");
+  var matches = durationText.match(durationRE);
+  if (!matches) { return 3600; }
+  var value = parseInt(matches[1]);
+  var unit = matches[2] || 's';
+  return value * PromConsole.TimeControl.timeFactors[unit];
+};
+
+PromConsole.TimeControl.prototype.getHumanDuration = function(duration) {
+  var units = [];
+  for (var key in PromConsole.TimeControl.timeFactors) {
+    units.push([PromConsole.TimeControl.timeFactors[key], key]);
+  }
+  units.sort(function(a, b) { return b[0] - a[0]; });
+  for (var i = 0; i < units.length; ++i) {
+    if (duration % units[i][0] === 0) {
+      return (duration / units[i][0]) + units[i][1];
+    }
+  }
+  return duration;
+};
+
+PromConsole.TimeControl.prototype.increaseDuration = function() {
+  var durationSeconds = this.parseDuration(this.durationElement.value);
+  for (var i = 0; i < PromConsole.TimeControl.stepValues.length; i++) {
+    if (durationSeconds < this.parseDuration(PromConsole.TimeControl.stepValues[i])) {
+      this.setDuration(PromConsole.TimeControl.stepValues[i]);
+      this.dispatch();
+      return;
+    }
+  }
+};
+
+PromConsole.TimeControl.prototype.decreaseDuration = function() {
+  var durationSeconds = this.parseDuration(this.durationElement.value);
+  for (var i = PromConsole.TimeControl.stepValues.length - 1; i >= 0; i--) {
+    if (durationSeconds > this.parseDuration(PromConsole.TimeControl.stepValues[i])) {
+      this.setDuration(PromConsole.TimeControl.stepValues[i]);
+      this.dispatch();
+      return;
+    }
+  }
+};
+
+PromConsole.TimeControl.prototype.setDuration = function(duration) {
+  this.durationElement.value = duration;
+  this._setHash();
+};
+
+PromConsole.TimeControl.prototype.getEndDate = function() {
+  if (this.endElement.value === '') {
+    return null;
+  }
+  var dateParts = this.endElement.value.split(/[- :]/)
+  return Date.UTC(dateParts[0], dateParts[1] - 1, dateParts[2], dateParts[3], dateParts[4]);
+};
+
+PromConsole.TimeControl.prototype.getOrSetEndDate = function() {
+  var date = this.getEndDate();
+  if (date) {
+    return date;
+  }
+  date = new Date();
+  this.setEndDate(date);
+  return date.getTime();
+};
+
+PromConsole.TimeControl.prototype.getHumanDate = function(date) {
+  var hours = date.getUTCHours() < 10 ? '0' + date.getUTCHours() : date.getUTCHours();
+  var minutes = date.getUTCMinutes() < 10 ? '0' + date.getUTCMinutes() : date.getUTCMinutes();
+  return date.getUTCFullYear() + "-" + (date.getUTCMonth() + 1) + "-" + date.getUTCDate() + " " +
+    hours + ":" + minutes;
+};
+
+PromConsole.TimeControl.prototype.setEndDate = function(date) {
+  this.setRefresh("Off");
+  this.endElement.value = this.getHumanDate(date);
+  this._setHash();
+};
+
+PromConsole.TimeControl.prototype.increaseEnd = function() {
+  // Increase duration 25% range & convert ms to s.
+  this.setEndDate(new Date(this.getOrSetEndDate() + this.parseDuration(this.durationElement.value) * 1000 / 4));
+  this.dispatch();
+};
+
+PromConsole.TimeControl.prototype.decreaseEnd = function() {
+  this.setEndDate(new Date(this.getOrSetEndDate() - this.parseDuration(this.durationElement.value) * 1000 / 4));
+  this.dispatch();
+};
+
+PromConsole.TimeControl.prototype.refresh = function() {
+  this.endElement.value = '';
+  this._setHash();
+  this.dispatch();
+};
+
+PromConsole.TimeControl.prototype.dispatch = function() {
+  var durationSeconds = this.parseDuration(this.durationElement.value);
+  var end = this.getEndDate();
+  if (end === null) {
+    end = new Date().getTime();
+  }
+  for (var i = 0; i < PromConsole._graph_registry.length; i++) {
+    var graph = PromConsole._graph_registry[i];
+    graph.params.duration = durationSeconds;
+    graph.params.endTime = end / 1000;
+    graph.dispatch();
+  }
+};
+
+PromConsole.TimeControl.prototype._refreshInterval = null;
+
+PromConsole.TimeControl.prototype.setRefresh = function(duration) {
+  if (this._refreshInterval !== null) {
+    window.clearInterval(this._refreshInterval);
+    this._refreshInterval = null;
+  }
+  if (duration != "Off") {
+    if (this.endElement.value !== '') {
+      this.refresh();
+    }
+    var durationSeconds = this.parseDuration(duration);
+    this._refreshInterval = window.setInterval(this.dispatch.bind(this), durationSeconds * 1000);
+  }
+  this.refreshValueElement.textContent = duration;
+};
+
+// List of all graphs, used by time controls.
+PromConsole._graph_registry = [];
+
+PromConsole.graphDefaults = {
+  expr: null,     // Expression to graph. Can be a list of strings.
+  node: null,     // DOM node to place graph under.
+  // How long the graph is over, in seconds.
+  duration: PromConsole.TimeControl._initialValues.duration,
+  // The unixtime the graph ends at.
+  endTime: PromConsole.TimeControl._initialValues.endTime,
+  width: null,    // Height of the graph div, excluding titles and legends.
+  // Defaults to auto-detection.
+  height: 200,    // Height of the graph div, excluding titles and legends.
+  min: "auto",    // Minimum Y-axis value, defaults to lowest data value.
+  max: undefined, // Maximum Y-axis value, defaults to highest data value.
+  renderer: 'line',  // Type of graphs, options are 'line' and 'area'.
+  name: null,     // What to call plots, defaults to trying to do
+  // something reasonable.
+  // If a string, it'll use that. [[ label ]] will be substituted.
+  // If a function it'll be called with a map of keys to values,
+  // and should return the name to use.
+  // Can be a list of strings/functions, each element
+  // will be applied to the plots from the corresponding
+  // element of the expr list.
+  xTitle: "Time",     // The title of the x axis.
+  yUnits: "",     // The units of the y axis.
+  yTitle: "",     // The title of the y axis.
+  // Number formatter for y axis.
+  yAxisFormatter: PromConsole.NumberFormatter.humanize,
+  // Number of ticks (horizontal lines) for the y axis
+  yAxisTicks: null,
+  // Number formatter for y values hover detail.
+  yHoverFormatter: PromConsole.NumberFormatter.humanizeExact,
+  // Color scheme to be used by the plots. Can be either a list of hex color
+  // codes or one of the color scheme names supported by Rickshaw.
+  colorScheme: null,
+};
+
+PromConsole.Graph = function(params) {
+  for (var k in PromConsole.graphDefaults) {
+    if (!(k in params)) {
+      params[k] = PromConsole.graphDefaults[k];
+    }
+  }
+  if (typeof params.expr == "string") {
+    params.expr = [params.expr];
+  }
+  if (typeof params.name == "string" || typeof params.name == "function") {
+    var name = [];
+    for (var i = 0; i < params.expr.length; i++) {
+      name.push(params.name);
+    }
+    params.name = name;
+  }
+
+  this.params = params;
+  this.rendered_data = null;
+  // Keep a reference so that further updates (e.g. annotations) can be made
+  // by the user in their templates.
+  this.rickshawGraph = null;
+  PromConsole._graph_registry.push(this);
+
+  /*
+   * Table layout:
+   * | yTitle | Graph  |
+   * |        | xTitle |
+   * | /graph | Legend |
+   */
+  var table = document.createElement("table");
+  table.className = "prom_graph_table";
+  params.node.appendChild(table);
+  var tr = document.createElement("tr");
+  table.appendChild(tr);
+  var yTitleTd = document.createElement("td");
+  tr.appendChild(yTitleTd);
+  var yTitleDiv = document.createElement("td");
+  yTitleTd.appendChild(yTitleDiv);
+  yTitleDiv.className = "prom_graph_ytitle";
+  yTitleDiv.textContent = params.yTitle + (params.yUnits ? " (" + params.yUnits.trim() + ")" : "");
+
+  this.graphTd = document.createElement("td");
+  tr.appendChild(this.graphTd);
+  this.graphTd.className = "rickshaw_graph";
+  this.graphTd.width = params.width;
+  this.graphTd.height = params.height;
+
+  tr = document.createElement("tr");
+  table.appendChild(tr);
+  tr.appendChild(document.createElement("td"));
+  var xTitleTd = document.createElement("td");
+  tr.appendChild(xTitleTd);
+  xTitleTd.className = "prom_graph_xtitle";
+  xTitleTd.textContent = params.xTitle;
+
+  tr = document.createElement("tr");
+  table.appendChild(tr);
+  var graphLinkTd = document.createElement("td");
+  tr.appendChild(graphLinkTd);
+  var graphLinkA = document.createElement("a");
+  graphLinkTd.appendChild(graphLinkA);
+  graphLinkA.className = "prom_graph_link";
+  graphLinkA.textContent = "+";
+  graphLinkA.href = PromConsole._graphsToSlashGraphURL(params.expr);
+  var legendTd = document.createElement("td");
+  tr.appendChild(legendTd);
+  this.legendDiv = document.createElement("div");
+  legendTd.width = params.width;
+  legendTd.appendChild(this.legendDiv);
+
+  window.addEventListener('resize', function() {
+    if (this.rendered_data !== null) {
+      this._render(this.rendered_data);
+    }
+  }.bind(this));
+
+  this.dispatch();
+};
+
+PromConsole.Graph.prototype._parseValue = function(value) {
+  var val = parseFloat(value);
+  if (isNaN(val)) {
+    // "+Inf", "-Inf", "+Inf" will be parsed into NaN by parseFloat(). The
+    // can't be graphed, so show them as gaps (null).
+    return null;
+  }
+  return val;
+};
+
+PromConsole.Graph.prototype._escapeHTML = function(string) {
+  var entityMap = {
+    "&": "&amp;",
+    "<": "&lt;",
+    ">": "&gt;",
+    '"': '&quot;',
+    "'": '&#39;',
+    "/": '&#x2F;'
+  };
+
+  return string.replace(/[&<>"'\/]/g, function(s) {
+    return entityMap[s];
+  });
+};
+
+PromConsole.Graph.prototype._render = function(data) {
+  var self = this;
+  var palette = new Rickshaw.Color.Palette({ scheme: this.params.colorScheme });
+  var series = [];
+
+  // This will be used on resize.
+  this.rendered_data = data;
+
+
+  var nameFuncs = [];
+  if (this.params.name === null) {
+    var chooser = PromConsole._chooseNameFunction(data);
+    for (var i = 0; i < this.params.expr.length; i++) {
+      nameFuncs.push(chooser);
+    }
+  } else {
+    for (var i = 0; i < this.params.name.length; i++) {
+      if (typeof this.params.name[i] == "string") {
+        nameFuncs.push(function(i, metric) {
+          return PromConsole._interpolateName(this.params.name[i], metric);
+        }.bind(this, i));
+      } else {
+        nameFuncs.push(this.params.name[i]);
+      }
+    }
+  }
+
+  // Get the data into the right format.
+  var seriesLen = 0;
+
+  for (var e = 0; e < data.length; e++) {
+    for (var i = 0; i < data[e].data.result.length; i++) {
+      series[seriesLen] = {
+        data: data[e].data.result[i].values.map(function(s) { return { x: s[0], y: self._parseValue(s[1]) }; }),
+        color: palette.color(),
+        name: self._escapeHTML(nameFuncs[e](data[e].data.result[i].metric)),
+      };
+      // Insert nulls for all missing steps.
+      var newSeries = [];
+      var pos = 0;
+      var start = self.params.endTime - self.params.duration;
+      var step = Math.floor(self.params.duration / this.graphTd.offsetWidth * 1000) / 1000;
+      for (var t = start; t <= self.params.endTime; t += step) {
+        // Allow for floating point inaccuracy.
+        if (series[seriesLen].data.length > pos && series[seriesLen].data[pos].x < t + step / 100) {
+          newSeries.push(series[seriesLen].data[pos]);
+          pos++;
+        } else {
+          newSeries.push({ x: t, y: null });
+        }
+      }
+      series[seriesLen].data = newSeries;
+      seriesLen++;
+    }
+  }
+  this._clearGraph();
+  if (!series.length) {
+    var errorText = document.createElement("div");
+    errorText.className = 'prom_graph_error';
+    errorText.textContent = 'No timeseries returned';
+    this.graphTd.appendChild(errorText);
+    return;
+  }
+  // Render.
+  var graph = new Rickshaw.Graph({
+    interpolation: "linear",
+    width: this.graphTd.offsetWidth,
+    height: this.params.height,
+    element: this.graphTd,
+    renderer: this.params.renderer,
+    max: this.params.max,
+    min: this.params.min,
+    series: series
+  });
+  var hoverDetail = new Rickshaw.Graph.HoverDetail({
+    graph: graph,
+    onRender: function() {
+      var xLabel = this.element.getElementsByClassName("x_label")[0];
+      var item = this.element.getElementsByClassName("item")[0];
+      if (xLabel.offsetWidth + xLabel.offsetLeft + this.element.offsetLeft > graph.element.offsetWidth ||
+        item.offsetWidth + item.offsetLeft + this.element.offsetLeft > graph.element.offsetWidth) {
+        xLabel.classList.add("prom_graph_hover_flipped");
+        item.classList.add("prom_graph_hover_flipped");
+      } else {
+        xLabel.classList.remove("prom_graph_hover_flipped");
+        item.classList.remove("prom_graph_hover_flipped");
+      }
+    },
+    yFormatter: function(y) {
+      if (y === null) {
+        return "";
+      }
+      return this.params.yHoverFormatter(y) + this.params.yUnits;
+    }.bind(this)
+  });
+  var yAxis = new Rickshaw.Graph.Axis.Y({
+    graph: graph,
+    tickFormat: this.params.yAxisFormatter,
+    ticks: this.params.yAxisTicks
+  });
+  var xAxis = new Rickshaw.Graph.Axis.Time({
+    graph: graph,
+  });
+  var legend = new Rickshaw.Graph.Legend({
+    graph: graph,
+    element: this.legendDiv
+  });
+  xAxis.render();
+  yAxis.render();
+  graph.render();
+
+  this.rickshawGraph = graph;
+};
+
+PromConsole.Graph.prototype._clearGraph = function() {
+  while (this.graphTd.lastChild) {
+    this.graphTd.removeChild(this.graphTd.lastChild);
+  }
+  while (this.legendDiv.lastChild) {
+    this.legendDiv.removeChild(this.legendDiv.lastChild);
+  }
+  this.rickshawGraph = null;
+};
+
+PromConsole.Graph.prototype._xhrs = [];
+
+PromConsole.Graph.prototype.buildQueryUrl = function(expr) {
+  var p = this.params;
+  return PATH_PREFIX + "/api/v1/query_range?query=" +
+    encodeURIComponent(expr) +
+    "&step=" + Math.floor(p.duration / this.graphTd.offsetWidth * 1000) / 1000 +
+    "&start=" + (p.endTime - p.duration) + "&end=" + p.endTime;
+};
+
+PromConsole.Graph.prototype.dispatch = function() {
+  for (var j = 0; j < this._xhrs.length; j++) {
+    this._xhrs[j].abort();
+  }
+  var all_data = new Array(this.params.expr.length);
+  this._xhrs = new Array(this.params.expr.length);
+  var pending_requests = this.params.expr.length;
+  for (var i = 0; i < this.params.expr.length; ++i) {
+    var endTime = this.params.endTime;
+    var url = this.buildQueryUrl(this.params.expr[i]);
+    var xhr = new XMLHttpRequest();
+    xhr.open('get', url, true);
+    xhr.responseType = 'json';
+    xhr.onerror = function(xhr, i) {
+      this._clearGraph();
+      var errorText = document.createElement("div");
+      errorText.className = 'prom_graph_error';
+      errorText.textContent = 'Error loading data';
+      this.graphTd.appendChild(errorText);
+      console.log('Error loading data for ' + this.params.expr[i]);
+      pending_requests = 0;
+      // onabort gets any aborts.
+      for (var j = 0; j < pending_requests; j++) {
+        this._xhrs[j].abort();
+      }
+    }.bind(this, xhr, i);
+    xhr.onload = function(xhr, i) {
+      if (pending_requests === 0) {
+        // Got an error before this success.
+        return;
+      }
+      var data = xhr.response;
+      if (typeof data !== "object") {
+        data = JSON.parse(xhr.responseText);
+      }
+      pending_requests -= 1;
+      all_data[i] = data;
+      if (pending_requests === 0) {
+        this._xhrs = [];
+        this._render(all_data);
+      }
+    }.bind(this, xhr, i);
+    xhr.send();
+    this._xhrs[i] = xhr;
+  }
+
+  var loadingImg = document.createElement("img");
+  loadingImg.src = PATH_PREFIX + '/user/ajax-loader.gif';
+  loadingImg.alt = 'Loading...';
+  loadingImg.className = 'prom_graph_loading';
+  this.graphTd.appendChild(loadingImg);
+};
+
+// Substitute the value of 'label' for [[ label ]].
+PromConsole._interpolateName = function(name, metric) {
+  var re = /(.*?)\[\[\s*(\w+)+\s*\]\](.*?)/g;
+  var result = '';
+  while (match = re.exec(name)) {
+    result = result + match[1] + metric[match[2]] + match[3];
+  }
+  if (!result) {
+    return name;
+  }
+  return result;
+};
+
+// Given the data returned by the API, return an appropriate function
+// to return plot names.
+PromConsole._chooseNameFunction = function(data) {
+  // By default, use the full metric name.
+  var nameFunc = function(metric) {
+    var name = metric.__name__ + "{";
+    for (var label in metric) {
+      if (label.substring(0, 2) == "__") {
+        continue;
+      }
+      name += label + "='" + metric[label] + "',";
+    }
+    return name + "}";
+  };
+
+  // If only one label varies, use that value.
+  var labelValues = {};
+  for (var e = 0; e < data.length; e++) {
+    for (var i = 0; i < data[e].data.result.length; i++) {
+      for (var label in data[e].data.result[i].metric) {
+        if (!(label in labelValues)) {
+          labelValues[label] = {};
+        }
+        labelValues[label][data[e].data.result[i].metric[label]] = 1;
+      }
+    }
+  }
+
+  var multiValueLabels = [];
+  for (var label in labelValues) {
+    if (Object.keys(labelValues[label]).length > 1) {
+      multiValueLabels.push(label);
+    }
+  }
+  if (multiValueLabels.length == 1) {
+    nameFunc = function(metric) {
+      return metric[multiValueLabels[0]];
+    };
+  }
+  return nameFunc;
+};
+
+// Given a list of expressions, produce the /graph url for them.
+PromConsole._graphsToSlashGraphURL = function(exprs) {
+  var data = [];
+  for (var i = 0; i < exprs.length; ++i) {
+    data.push({ 'expr': exprs[i], 'tab': 0 });
+  }
+  return PATH_PREFIX + '/graph#' + encodeURIComponent(JSON.stringify(data));
+
+};