Bladeren bron

Merge branch 'master' into dashboard_folders

Torkel Ödegaard 8 jaren geleden
bovenliggende
commit
e63fa9c83c
47 gewijzigde bestanden met toevoegingen van 1620 en 276 verwijderingen
  1. 1 1
      conf/sample.ini
  2. 0 1
      docker/blocks/graphite/fig
  3. 9 0
      docs/sources/http_api/snapshot.md
  4. 58 0
      public/app/core/components/collapse_box.ts
  5. 248 0
      public/app/core/components/form_dropdown/form_dropdown.ts
  6. 113 0
      public/app/core/components/json_explorer/helpers.ts
  7. 431 0
      public/app/core/components/json_explorer/json_explorer.ts
  8. 6 0
      public/app/core/core.ts
  9. 1 1
      public/app/core/directives/dash_edit_link.js
  10. 30 6
      public/app/core/directives/misc.js
  11. 2 2
      public/app/core/directives/plugin_component.ts
  12. 8 2
      public/app/core/services/backend_srv.ts
  13. 1 1
      public/app/features/dashboard/export/exporter.ts
  14. 2 0
      public/app/features/dashboard/history/history.ts
  15. 2 2
      public/app/features/dashboard/partials/shareModal.html
  16. 4 15
      public/app/features/dashboard/shareModalCtrl.js
  17. 4 0
      public/app/features/dashboard/shareSnapshotCtrl.js
  18. 1 1
      public/app/features/panel/all.js
  19. 0 113
      public/app/features/panel/metrics_ds_selector.ts
  20. 24 2
      public/app/features/panel/metrics_panel_ctrl.ts
  21. 83 0
      public/app/features/panel/metrics_tab.ts
  22. 1 1
      public/app/features/panel/panel_ctrl.ts
  23. 6 1
      public/app/features/panel/panel_editor_tab.ts
  24. 50 0
      public/app/features/panel/partials/metrics_tab.html
  25. 4 17
      public/app/features/panel/query_editor_row.ts
  26. 154 0
      public/app/features/panel/query_troubleshooter.ts
  27. 0 1
      public/app/headers/common.d.ts
  28. 1 1
      public/app/partials/login.html
  29. 0 20
      public/app/partials/metrics.html
  30. 14 7
      public/app/plugins/datasource/elasticsearch/bucket_agg.js
  31. 44 6
      public/app/plugins/datasource/elasticsearch/partials/bucket_agg.html
  32. 2 2
      public/app/plugins/datasource/elasticsearch/query_ctrl.ts
  33. 1 1
      public/app/plugins/datasource/graphite/partials/query.options.html
  34. 6 4
      public/app/plugins/datasource/influxdb/datasource.ts
  35. 1 1
      public/app/system.conf.js
  36. 57 0
      public/img/grafana_com_auth_icon.svg
  37. 57 58
      public/img/grafana_icon.svg
  38. 2 0
      public/sass/_grafana.scss
  39. 18 0
      public/sass/_variables.dark.scss
  40. 18 0
      public/sass/_variables.light.scss
  41. 46 0
      public/sass/components/_collapse_box.scss
  42. 1 0
      public/sass/components/_gf-form.scss
  43. 98 0
      public/sass/components/_json_explorer.scss
  44. 1 0
      public/sass/grafana.dark.scss
  45. 1 0
      public/sass/pages/_login.scss
  46. 1 1
      public/test/test-main.js
  47. 8 8
      yarn.lock

+ 1 - 1
conf/sample.ini

@@ -298,7 +298,7 @@
 # Use space to separate multiple modes, e.g. "console file"
 # Use space to separate multiple modes, e.g. "console file"
 ;mode = console file
 ;mode = console file
 
 
-# Either "trace", "debug", "info", "warn", "error", "critical", default is "info"
+# Either "debug", "info", "warn", "error", "critical", default is "info"
 ;level = info
 ;level = info
 
 
 # optional settings to set different levels for specific loggers. Ex filters = sqlstore:debug
 # optional settings to set different levels for specific loggers. Ex filters = sqlstore:debug

+ 0 - 1
docker/blocks/graphite/fig

@@ -4,7 +4,6 @@ graphite:
     - "8080:80"
     - "8080:80"
     - "2003:2003"
     - "2003:2003"
   volumes:
   volumes:
-    - /var/docker/gfdev/graphite:/opt/graphite/storage/whisper
     - /etc/localtime:/etc/localtime:ro
     - /etc/localtime:/etc/localtime:ro
     - /etc/timezone:/etc/timezone:ro
     - /etc/timezone:/etc/timezone:ro
 
 

+ 9 - 0
docs/sources/http_api/snapshot.md

@@ -52,6 +52,15 @@ parent = "http_api"
       "expires": 3600
       "expires": 3600
     }
     }
 
 
+JSON Body schema:
+
+- **dashboard** – Required. The complete dashboard model.
+- **name** – Optional. snapshot name
+- **expires** - Optional. When the snapshot should expire in seconds. 3600 is 1 hour, 86400 is 1 day. Default is never to expire. 
+- **external** - Optional. Save the snapshot on an external server rather than locally. Default is `false`.
+- **key** - Optional. Define the unique key. Required if **external** is `true`.
+- **deleteKey** - Optional. Unique key used to delete the snapshot. It is different from the **key** so that only the creator can delete the snapshot. Required if **external** is `true`.
+
 **Example Response**:
 **Example Response**:
 
 
     HTTP/1.1 200
     HTTP/1.1 200

+ 58 - 0
public/app/core/components/collapse_box.ts

@@ -0,0 +1,58 @@
+///<reference path="../../headers/common.d.ts" />
+
+import coreModule from 'app/core/core_module';
+
+const template = `
+<div class="collapse-box">
+  <div class="collapse-box__header">
+    <a class="collapse-box__header-title pointer" ng-click="ctrl.toggle()">
+      <span class="fa fa-fw fa-caret-right" ng-hide="ctrl.isOpen"></span>
+      <span class="fa fa-fw fa-caret-down" ng-hide="!ctrl.isOpen"></span>
+      {{ctrl.title}}
+    </a>
+    <div class="collapse-box__header-actions" ng-transclude="actions" ng-if="ctrl.isOpen"></div>
+  </div>
+  <div class="collapse-box__body" ng-transclude="body" ng-if="ctrl.isOpen">
+  </div>
+</div>
+`;
+
+export class CollapseBoxCtrl {
+  isOpen: boolean;
+  stateChanged: () => void;
+
+  /** @ngInject **/
+  constructor(private $timeout) {
+    this.isOpen = false;
+  }
+
+  toggle() {
+    this.isOpen = !this.isOpen;
+    this.$timeout(() => {
+      this.stateChanged();
+    });
+  }
+}
+
+export function collapseBox() {
+  return {
+    restrict: 'E',
+    template: template,
+    controller: CollapseBoxCtrl,
+    bindToController: true,
+    controllerAs: 'ctrl',
+    scope: {
+      "title": "@",
+      "isOpen": "=?",
+      "stateChanged": "&"
+    },
+    transclude: {
+      'actions': '?collapseBoxActions',
+      'body': 'collapseBoxBody',
+    },
+    link: function(scope, elem, attrs) {
+    }
+  };
+}
+
+coreModule.directive('collapseBox', collapseBox);

+ 248 - 0
public/app/core/components/form_dropdown/form_dropdown.ts

@@ -0,0 +1,248 @@
+///<reference path="../../../headers/common.d.ts" />
+
+import config from 'app/core/config';
+import _ from 'lodash';
+import $ from 'jquery';
+import coreModule from '../../core_module';
+
+function typeaheadMatcher(item) {
+  var str = this.query;
+  if (str[0] === '/') { str = str.substring(1); }
+  if (str[str.length - 1] === '/') { str = str.substring(0, str.length-1); }
+  return item.toLowerCase().match(str.toLowerCase());
+}
+
+export class FormDropdownCtrl {
+  inputElement: any;
+  linkElement: any;
+  model: any;
+  display: any;
+  text: any;
+  options: any;
+  cssClass: any;
+  cssClasses: any;
+  allowCustom: any;
+  labelMode: boolean;
+  linkMode: boolean;
+  cancelBlur: any;
+  onChange: any;
+  getOptions: any;
+  optionCache: any;
+  lookupText: boolean;
+
+  constructor(private $scope, $element, private $sce, private templateSrv, private $q) {
+    this.inputElement = $element.find('input').first();
+    this.linkElement = $element.find('a').first();
+    this.linkMode = true;
+    this.cancelBlur = null;
+
+    // listen to model changes
+    $scope.$watch("ctrl.model", this.modelChanged.bind(this));
+
+    if (this.labelMode) {
+      this.cssClasses = 'gf-form-label ' + this.cssClass;
+    } else {
+      this.cssClasses = 'gf-form-input gf-form-input--dropdown ' + this.cssClass;
+    }
+
+    this.inputElement.attr('data-provide', 'typeahead');
+    this.inputElement.typeahead({
+      source: this.typeaheadSource.bind(this),
+      minLength: 0,
+      items: 10000,
+      updater: this.typeaheadUpdater.bind(this),
+      matcher: typeaheadMatcher,
+    });
+
+    // modify typeahead lookup
+    // this = typeahead
+    var typeahead = this.inputElement.data('typeahead');
+    typeahead.lookup = function () {
+      this.query = this.$element.val() || '';
+      var items = this.source(this.query, $.proxy(this.process, this));
+      return items ? this.process(items) : items;
+    };
+
+    this.linkElement.keydown(evt => {
+      // trigger typeahead on down arrow or enter key
+      if (evt.keyCode === 40 || evt.keyCode === 13) {
+        this.linkElement.click();
+      }
+    });
+
+    this.inputElement.keydown(evt => {
+      if (evt.keyCode === 13) {
+        this.inputElement.blur();
+      }
+    });
+
+    this.inputElement.blur(this.inputBlur.bind(this));
+  }
+
+  getOptionsInternal(query) {
+    var result = this.getOptions({$query: query});
+    if (this.isPromiseLike(result)) {
+      return result;
+    }
+    return this.$q.when(result);
+  }
+
+  isPromiseLike(obj) {
+    return obj && (typeof obj.then === 'function');
+  }
+
+  modelChanged() {
+    if (_.isObject(this.model)) {
+      this.updateDisplay(this.model.text);
+    } else {
+      // if we have text use it
+      if (this.lookupText) {
+        this.getOptionsInternal("").then(options => {
+          var item = _.find(options, {value: this.model});
+          this.updateDisplay(item ? item.text : this.model);
+        });
+      } else {
+        this.updateDisplay(this.model);
+      }
+    }
+  }
+
+  typeaheadSource(query, callback) {
+    this.getOptionsInternal(query).then(options => {
+      this.optionCache = options;
+
+      // extract texts
+      let optionTexts = _.map(options, 'text');
+
+      // add custom values
+      if (this.allowCustom) {
+        if (_.indexOf(optionTexts, this.text) === -1) {
+          options.unshift(this.text);
+        }
+      }
+
+      callback(optionTexts);
+    });
+  }
+
+  typeaheadUpdater(text) {
+    if (text === this.text) {
+      clearTimeout(this.cancelBlur);
+      this.inputElement.focus();
+      return text;
+    }
+
+    this.inputElement.val(text);
+    this.switchToLink(true);
+    return text;
+  }
+
+  switchToLink(fromClick) {
+    if (this.linkMode && !fromClick) { return; }
+
+    clearTimeout(this.cancelBlur);
+    this.cancelBlur = null;
+    this.linkMode = true;
+    this.inputElement.hide();
+    this.linkElement.show();
+    this.updateValue(this.inputElement.val());
+  }
+
+  inputBlur() {
+    // happens long before the click event on the typeahead options
+    // need to have long delay because the blur
+    this.cancelBlur = setTimeout(this.switchToLink.bind(this), 200);
+  }
+
+  updateValue(text) {
+    if (text === '' || this.text === text) {
+      return;
+    }
+
+    this.$scope.$apply(() => {
+      var option = _.find(this.optionCache, {text: text});
+
+      if (option) {
+        if (_.isObject(this.model)) {
+          this.model = option;
+        } else {
+          this.model = option.value;
+        }
+        this.text = option.text;
+      } else if (this.allowCustom) {
+        if (_.isObject(this.model)) {
+          this.model.text = this.model.value = text;
+        } else {
+          this.model = text;
+        }
+        this.text = text;
+      }
+
+      // needs to call this after digest so
+      // property is synced with outerscope
+      this.$scope.$$postDigest(() => {
+        this.$scope.$apply(() => {
+          this.onChange({$option: option});
+        });
+      });
+
+    });
+  }
+
+  updateDisplay(text) {
+    this.text = text;
+    this.display = this.$sce.trustAsHtml(this.templateSrv.highlightVariablesAsHtml(text));
+  }
+
+  open() {
+    this.inputElement.show();
+
+    this.inputElement.css('width', (Math.max(this.linkElement.width(), 80) + 16) + 'px');
+    this.inputElement.focus();
+
+    this.linkElement.hide();
+    this.linkMode = false;
+
+    var typeahead = this.inputElement.data('typeahead');
+    if (typeahead) {
+      this.inputElement.val('');
+      typeahead.lookup();
+    }
+  }
+}
+
+const template =  `
+<input type="text"
+  data-provide="typeahead"
+  class="gf-form-input"
+  spellcheck="false"
+  style="display:none">
+</input>
+<a ng-class="ctrl.cssClasses"
+	 tabindex="1"
+	 ng-click="ctrl.open()"
+	 give-focus="ctrl.focus"
+	 ng-bind-html="ctrl.display">
+</a>
+`;
+
+export function formDropdownDirective() {
+  return {
+    restrict: 'E',
+    template: template,
+    controller: FormDropdownCtrl,
+    bindToController: true,
+    controllerAs: 'ctrl',
+    scope: {
+      model: "=",
+      getOptions: "&",
+      onChange: "&",
+      cssClass: "@",
+      allowCustom: "@",
+      labelMode: "@",
+      lookupText: "@",
+    },
+  };
+}
+
+coreModule.directive('gfFormDropdown', formDropdownDirective);

+ 113 - 0
public/app/core/components/json_explorer/helpers.ts

@@ -0,0 +1,113 @@
+// Based on work https://github.com/mohsen1/json-formatter-js
+// Licence MIT, Copyright (c) 2015 Mohsen Azimi
+
+/*
+ * Escapes `"` charachters from string
+*/
+function escapeString(str: string): string {
+  return str.replace('"', '\"');
+}
+
+/*
+ * Determines if a value is an object
+*/
+export function isObject(value: any): boolean {
+  var type = typeof value;
+  return !!value && (type === 'object');
+}
+
+/*
+ * Gets constructor name of an object.
+ * From http://stackoverflow.com/a/332429
+ *
+*/
+export function getObjectName(object: Object): string {
+  if (object === undefined) {
+    return '';
+  }
+  if (object === null) {
+    return 'Object';
+  }
+  if (typeof object === 'object' && !object.constructor) {
+      return 'Object';
+  }
+
+  const funcNameRegex = /function ([^(]*)/;
+  const results = (funcNameRegex).exec((object).constructor.toString());
+  if (results && results.length > 1) {
+    return results[1];
+  } else {
+    return '';
+  }
+}
+
+/*
+ * Gets type of an object. Returns "null" for null objects
+*/
+export function getType(object: Object): string {
+  if (object === null) { return 'null'; }
+  return typeof object;
+}
+
+/*
+ * Generates inline preview for a JavaScript object based on a value
+*/
+export function getValuePreview (object: Object, value: string): string {
+  var type = getType(object);
+
+  if (type === 'null' || type === 'undefined') { return type; }
+
+  if (type === 'string') {
+    value = '"' + escapeString(value) + '"';
+  }
+  if (type === 'function'){
+
+    // Remove content of the function
+    return object.toString()
+        .replace(/[\r\n]/g, '')
+        .replace(/\{.*\}/, '') + '{…}';
+  }
+  return value;
+}
+
+/*
+ * Generates inline preview for a JavaScript object
+*/
+export function getPreview(object: string): string {
+  let value = '';
+  if (isObject(object)) {
+    value = getObjectName(object);
+    if (Array.isArray(object)) {
+      value += '[' + object.length + ']';
+    }
+  } else {
+    value = getValuePreview(object, object);
+  }
+  return value;
+}
+
+/*
+ * Generates a prefixed CSS class name
+*/
+export function cssClass(className: string): string {
+  return `json-formatter-${className}`;
+}
+
+/*
+  * Creates a new DOM element wiht given type and class
+  * TODO: move me to helpers
+*/
+export function createElement(type: string, className?: string, content?: Element|string): Element {
+  const el = document.createElement(type);
+  if (className) {
+    el.classList.add(cssClass(className));
+  }
+  if (content !== undefined) {
+    if (content instanceof Node) {
+      el.appendChild(content);
+    } else {
+      el.appendChild(document.createTextNode(String(content)));
+    }
+  }
+  return el;
+}

+ 431 - 0
public/app/core/components/json_explorer/json_explorer.ts

@@ -0,0 +1,431 @@
+// Based on work https://github.com/mohsen1/json-formatter-js
+// Licence MIT, Copyright (c) 2015 Mohsen Azimi
+
+import {
+  isObject,
+  getObjectName,
+  getType,
+  getValuePreview,
+  getPreview,
+  cssClass,
+  createElement
+} from './helpers';
+
+import _ from 'lodash';
+
+const DATE_STRING_REGEX = /(^\d{1,4}[\.|\\/|-]\d{1,2}[\.|\\/|-]\d{1,4})(\s*(?:0?[1-9]:[0-5]|1(?=[012])\d:[0-5])\d\s*[ap]m)?$/;
+const PARTIAL_DATE_REGEX = /\d{2}:\d{2}:\d{2} GMT-\d{4}/;
+const JSON_DATE_REGEX = /\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z/;
+
+// When toggleing, don't animated removal or addition of more than a few items
+const MAX_ANIMATED_TOGGLE_ITEMS = 10;
+
+const requestAnimationFrame = window.requestAnimationFrame || function(cb: ()=>void) { cb(); return 0; };
+
+export interface JsonExplorerConfig {
+  animateOpen?: boolean;
+  animateClose?: boolean;
+  theme?: string;
+}
+
+const _defaultConfig: JsonExplorerConfig = {
+  animateOpen: true,
+  animateClose: true,
+  theme: null
+};
+
+
+/**
+ * @class JsonExplorer
+ *
+ * JsonExplorer allows you to render JSON objects in HTML with a
+ * **collapsible** navigation.
+*/
+export class JsonExplorer {
+
+  // Hold the open state after the toggler is used
+  private _isOpen: boolean = null;
+
+  // A reference to the element that we render to
+  private element: Element;
+
+  private skipChildren = false;
+
+  /**
+   * @param {object} json The JSON object you want to render. It has to be an
+   * object or array. Do NOT pass raw JSON string.
+   *
+   * @param {number} [open=1] his number indicates up to how many levels the
+   * rendered tree should expand. Set it to `0` to make the whole tree collapsed
+   * or set it to `Infinity` to expand the tree deeply
+   *
+   * @param {object} [config=defaultConfig] -
+   *  defaultConfig = {
+   *   hoverPreviewEnabled: false,
+   *   hoverPreviewArrayCount: 100,
+   *   hoverPreviewFieldCount: 5
+   * }
+   *
+   * Available configurations:
+   *  #####Hover Preview
+   * * `hoverPreviewEnabled`:  enable preview on hover
+   * * `hoverPreviewArrayCount`: number of array items to show in preview Any
+   *    array larger than this number will be shown as `Array[XXX]` where `XXX`
+   *    is length of the array.
+   * * `hoverPreviewFieldCount`: number of object properties to show for object
+   *   preview. Any object with more properties that thin number will be
+   *   truncated.
+   *
+   * @param {string} [key=undefined] The key that this object in it's parent
+   * context
+  */
+  constructor(public json: any, private open = 1, private config: JsonExplorerConfig = _defaultConfig, private key?: string) {
+  }
+
+  /*
+   * is formatter open?
+  */
+  private get isOpen(): boolean {
+    if (this._isOpen !== null) {
+      return this._isOpen;
+    } else {
+      return this.open > 0;
+    }
+  }
+
+  /*
+   * set open state (from toggler)
+  */
+  private set isOpen(value: boolean) {
+    this._isOpen = value;
+  }
+
+  /*
+   * is this a date string?
+  */
+  private get isDate(): boolean {
+    return (this.type === 'string') &&
+      (DATE_STRING_REGEX.test(this.json) ||
+      JSON_DATE_REGEX.test(this.json) ||
+      PARTIAL_DATE_REGEX.test(this.json));
+  }
+
+  /*
+   * is this a URL string?
+  */
+  private get isUrl(): boolean {
+    return this.type === 'string' && (this.json.indexOf('http') === 0);
+  }
+
+  /*
+   * is this an array?
+  */
+  private get isArray(): boolean {
+    return Array.isArray(this.json);
+  }
+
+  /*
+   * is this an object?
+   * Note: In this context arrays are object as well
+  */
+  private get isObject(): boolean {
+    return isObject(this.json);
+  }
+
+  /*
+   * is this an empty object with no properties?
+  */
+  private get isEmptyObject(): boolean {
+    return !this.keys.length && !this.isArray;
+  }
+
+  /*
+   * is this an empty object or array?
+  */
+  private get isEmpty(): boolean {
+    return this.isEmptyObject || (this.keys && !this.keys.length && this.isArray);
+  }
+
+  /*
+   * did we recieve a key argument?
+   * This means that the formatter was called as a sub formatter of a parent formatter
+  */
+  private get hasKey(): boolean {
+    return typeof this.key !== 'undefined';
+  }
+
+  /*
+   * if this is an object, get constructor function name
+  */
+  private get constructorName(): string {
+    return getObjectName(this.json);
+  }
+
+  /*
+   * get type of this value
+   * Possible values: all JavaScript primitive types plus "array" and "null"
+  */
+  private get type(): string {
+    return getType(this.json);
+  }
+
+  /*
+   * get object keys
+   * If there is an empty key we pad it wit quotes to make it visible
+  */
+  private get keys(): string[] {
+    if (this.isObject) {
+      return Object.keys(this.json).map((key)=> key ? key : '""');
+    } else {
+      return [];
+    }
+  }
+
+  /**
+   * Toggles `isOpen` state
+   *
+  */
+  toggleOpen() {
+    this.isOpen = !this.isOpen;
+
+    if (this.element) {
+      if (this.isOpen) {
+        this.appendChildren(this.config.animateOpen);
+      } else{
+        this.removeChildren(this.config.animateClose);
+      }
+      this.element.classList.toggle(cssClass('open'));
+    }
+  }
+
+  /**
+  * Open all children up to a certain depth.
+  * Allows actions such as expand all/collapse all
+  *
+  */
+  openAtDepth(depth = 1) {
+    if (depth < 0) {
+      return;
+    }
+
+    this.open = depth;
+    this.isOpen = (depth !== 0);
+
+    if (this.element) {
+      this.removeChildren(false);
+
+      if (depth === 0) {
+        this.element.classList.remove(cssClass('open'));
+      } else {
+        this.appendChildren(this.config.animateOpen);
+        this.element.classList.add(cssClass('open'));
+      }
+    }
+  }
+
+  isNumberArray() {
+    return (this.json.length > 0 && this.json.length < 4) &&
+      (_.isNumber(this.json[0]) || _.isNumber(this.json[1]));
+  }
+
+  renderArray() {
+    const arrayWrapperSpan = createElement('span');
+    arrayWrapperSpan.appendChild(createElement('span', 'bracket', '['));
+
+    // some pretty handling of number arrays
+    if (this.isNumberArray()) {
+      this.json.forEach((val, index) => {
+        if (index > 0) {
+          arrayWrapperSpan.appendChild(createElement('span', 'array-comma', ','));
+        }
+        arrayWrapperSpan.appendChild(createElement('span', 'number', val));
+      });
+      this.skipChildren = true;
+    } else {
+      arrayWrapperSpan.appendChild(createElement('span', 'number', (this.json.length)));
+    }
+
+    arrayWrapperSpan.appendChild(createElement('span', 'bracket', ']'));
+    return arrayWrapperSpan;
+  }
+
+  /**
+   * Renders an HTML element and installs event listeners
+   *
+   * @returns {HTMLDivElement}
+   */
+  render(skipRoot = false): HTMLDivElement {
+    // construct the root element and assign it to this.element
+    this.element = createElement('div', 'row');
+
+    // construct the toggler link
+    const togglerLink = createElement('a', 'toggler-link');
+    const togglerIcon = createElement('span', 'toggler');
+
+    // if this is an object we need a wrapper span (toggler)
+    if (this.isObject) {
+      togglerLink.appendChild(togglerIcon);
+    }
+
+    // if this is child of a parent formatter we need to append the key
+    if (this.hasKey) {
+      togglerLink.appendChild(createElement('span', 'key', `${this.key}:`));
+    }
+
+    // Value for objects and arrays
+    if (this.isObject) {
+      // construct the value holder element
+      const value = createElement('span', 'value');
+
+      // we need a wrapper span for objects
+      const objectWrapperSpan = createElement('span');
+
+      // get constructor name and append it to wrapper span
+      var constructorName = createElement('span', 'constructor-name', this.constructorName);
+      objectWrapperSpan.appendChild(constructorName);
+
+      // if it's an array append the array specific elements like brackets and length
+      if (this.isArray) {
+        const arrayWrapperSpan = this.renderArray();
+        objectWrapperSpan.appendChild(arrayWrapperSpan);
+      }
+
+      // append object wrapper span to toggler link
+      value.appendChild(objectWrapperSpan);
+      togglerLink.appendChild(value);
+      // Primitive values
+    } else {
+
+      // make a value holder element
+      const value = this.isUrl ? createElement('a') : createElement('span');
+
+      // add type and other type related CSS classes
+      value.classList.add(cssClass(this.type));
+      if (this.isDate) {
+        value.classList.add(cssClass('date'));
+      }
+      if (this.isUrl) {
+        value.classList.add(cssClass('url'));
+        value.setAttribute('href', this.json);
+      }
+
+      // Append value content to value element
+      const valuePreview = getValuePreview(this.json, this.json);
+      value.appendChild(document.createTextNode(valuePreview));
+
+      // append the value element to toggler link
+      togglerLink.appendChild(value);
+    }
+
+    // construct a children element
+    const children = createElement('div', 'children');
+
+    // set CSS classes for children
+    if (this.isObject) {
+      children.classList.add(cssClass('object'));
+    }
+    if (this.isArray) {
+      children.classList.add(cssClass('array'));
+    }
+    if (this.isEmpty) {
+      children.classList.add(cssClass('empty'));
+    }
+
+    // set CSS classes for root element
+    if (this.config && this.config.theme) {
+      this.element.classList.add(cssClass(this.config.theme));
+    }
+    if (this.isOpen) {
+      this.element.classList.add(cssClass('open'));
+    }
+
+    // append toggler and children elements to root element
+    if (!skipRoot) {
+      this.element.appendChild(togglerLink);
+    }
+
+    if (!this.skipChildren) {
+      this.element.appendChild(children);
+    } else {
+      // remove togglerIcon
+      togglerLink.removeChild(togglerIcon);
+    }
+
+    // if formatter is set to be open call appendChildren
+    if (this.isObject && this.isOpen) {
+      this.appendChildren();
+    }
+
+    // add event listener for toggling
+    if (this.isObject) {
+      togglerLink.addEventListener('click', this.toggleOpen.bind(this));
+    }
+
+    return this.element as HTMLDivElement;
+  }
+
+  /**
+   * Appends all the children to children element
+   * Animated option is used when user triggers this via a click
+  */
+  appendChildren(animated = false) {
+    const children = this.element.querySelector(`div.${cssClass('children')}`);
+
+    if (!children || this.isEmpty) { return; }
+
+    if (animated) {
+      let index = 0;
+      const addAChild = ()=> {
+        const key = this.keys[index];
+        const formatter = new JsonExplorer(this.json[key], this.open - 1, this.config, key);
+        children.appendChild(formatter.render());
+
+        index += 1;
+
+        if (index < this.keys.length) {
+          if (index > MAX_ANIMATED_TOGGLE_ITEMS) {
+            addAChild();
+          } else {
+            requestAnimationFrame(addAChild);
+          }
+        }
+      };
+
+      requestAnimationFrame(addAChild);
+
+    } else {
+      this.keys.forEach(key => {
+        const formatter = new JsonExplorer(this.json[key], this.open - 1, this.config, key);
+        children.appendChild(formatter.render());
+      });
+    }
+  }
+
+  /**
+   * Removes all the children from children element
+   * Animated option is used when user triggers this via a click
+  */
+  removeChildren(animated = false) {
+    const childrenElement = this.element.querySelector(`div.${cssClass('children')}`) as HTMLDivElement;
+
+    if (animated) {
+      let childrenRemoved = 0;
+      const removeAChild = ()=> {
+        if (childrenElement && childrenElement.children.length) {
+          childrenElement.removeChild(childrenElement.children[0]);
+          childrenRemoved += 1;
+          if (childrenRemoved > MAX_ANIMATED_TOGGLE_ITEMS) {
+            removeAChild();
+          } else {
+            requestAnimationFrame(removeAChild);
+          }
+        }
+      };
+      requestAnimationFrame(removeAChild);
+    } else {
+      if (childrenElement) {
+        childrenElement.innerHTML = '';
+      }
+    }
+  }
+}

+ 6 - 0
public/app/core/core.ts

@@ -34,6 +34,7 @@ import {switchDirective} from './components/switch';
 import {dashboardSelector} from './components/dashboard_selector';
 import {dashboardSelector} from './components/dashboard_selector';
 import {queryPartEditorDirective} from './components/query_part/query_part_editor';
 import {queryPartEditorDirective} from './components/query_part/query_part_editor';
 import {WizardFlow} from './components/wizard/wizard';
 import {WizardFlow} from './components/wizard/wizard';
+import {formDropdownDirective} from './components/form_dropdown/form_dropdown';
 import 'app/core/controllers/all';
 import 'app/core/controllers/all';
 import 'app/core/services/all';
 import 'app/core/services/all';
 import 'app/core/routes/routes';
 import 'app/core/routes/routes';
@@ -45,6 +46,8 @@ import {assignModelProperties} from './utils/model_utils';
 import {contextSrv} from './services/context_srv';
 import {contextSrv} from './services/context_srv';
 import {KeybindingSrv} from './services/keybindingSrv';
 import {KeybindingSrv} from './services/keybindingSrv';
 import {helpModal} from './components/help/help';
 import {helpModal} from './components/help/help';
+import {collapseBox} from './components/collapse_box';
+import {JsonExplorer} from './components/json_explorer/json_explorer';
 import {NavModelSrv, NavModel} from './nav_model_srv';
 import {NavModelSrv, NavModel} from './nav_model_srv';
 import {userPicker} from './components/user_picker';
 import {userPicker} from './components/user_picker';
 import {userGroupPicker} from './components/user_group_picker';
 import {userGroupPicker} from './components/user_group_picker';
@@ -67,10 +70,13 @@ export {
   queryPartEditorDirective,
   queryPartEditorDirective,
   WizardFlow,
   WizardFlow,
   colors,
   colors,
+  formDropdownDirective,
   assignModelProperties,
   assignModelProperties,
   contextSrv,
   contextSrv,
   KeybindingSrv,
   KeybindingSrv,
   helpModal,
   helpModal,
+  collapseBox,
+  JsonExplorer,
   NavModelSrv,
   NavModelSrv,
   NavModel,
   NavModel,
   userPicker,
   userPicker,

+ 1 - 1
public/app/core/directives/dash_edit_link.js

@@ -35,7 +35,7 @@ function ($, angular, coreModule) {
             options.html = editViewMap[options.editview].html;
             options.html = editViewMap[options.editview].html;
           }
           }
 
 
-          if (lastEditView === options.editview) {
+          if (lastEditView && lastEditView === options.editview) {
             hideEditorPane(false);
             hideEditorPane(false);
             return;
             return;
           }
           }

+ 30 - 6
public/app/core/directives/misc.js

@@ -1,9 +1,10 @@
 define([
 define([
   'angular',
   'angular',
+  'require',
   '../core_module',
   '../core_module',
   'app/core/utils/kbn',
   'app/core/utils/kbn',
 ],
 ],
-function (angular, coreModule, kbn) {
+function (angular, require, coreModule, kbn) {
   'use strict';
   'use strict';
 
 
   coreModule.default.directive('tip', function($compile) {
   coreModule.default.directive('tip', function($compile) {
@@ -18,6 +19,29 @@ function (angular, coreModule, kbn) {
     };
     };
   });
   });
 
 
+  coreModule.default.directive('clipboardButton', function() {
+    return {
+      scope: {
+        getText: '&clipboardButton'
+      },
+      link: function(scope, elem) {
+        require(['vendor/clipboard/dist/clipboard'], function(Clipboard) {
+          scope.clipboard = new Clipboard(elem[0], {
+            text: function() {
+              return scope.getText();
+            }
+          });
+        });
+
+        scope.$on('$destroy', function() {
+          if (scope.clipboard) {
+            scope.clipboard.destroy();
+          }
+        });
+      }
+    };
+  });
+
   coreModule.default.directive('compile', function($compile) {
   coreModule.default.directive('compile', function($compile) {
     return {
     return {
       restrict: 'A',
       restrict: 'A',
@@ -77,10 +101,10 @@ function (angular, coreModule, kbn) {
           text + tip + '</label>';
           text + tip + '</label>';
 
 
         var template =
         var template =
-          '<input class="cr1" id="' + scope.$id + model + '" type="checkbox" ' +
-          '       ng-model="' + model + '"' + ngchange +
-          '       ng-checked="' + model + '"></input>' +
-          ' <label for="' + scope.$id + model + '" class="cr1"></label>';
+        '<input class="cr1" id="' + scope.$id + model + '" type="checkbox" ' +
+        '       ng-model="' + model + '"' + ngchange +
+        '       ng-checked="' + model + '"></input>' +
+        ' <label for="' + scope.$id + model + '" class="cr1"></label>';
 
 
         template = template + label;
         template = template + label;
         elem.addClass('gf-form-checkbox');
         elem.addClass('gf-form-checkbox');
@@ -105,7 +129,7 @@ function (angular, coreModule, kbn) {
         var li = '<li' + (item.submenu && item.submenu.length ? ' class="dropdown-submenu"' : '') + '>' +
         var li = '<li' + (item.submenu && item.submenu.length ? ' class="dropdown-submenu"' : '') + '>' +
           '<a tabindex="-1" ng-href="' + (item.href || '') + '"' + (item.click ? ' ng-click="' + item.click + '"' : '') +
           '<a tabindex="-1" ng-href="' + (item.href || '') + '"' + (item.click ? ' ng-click="' + item.click + '"' : '') +
           (item.target ? ' target="' + item.target + '"' : '') + (item.method ? ' data-method="' + item.method + '"' : '') +
           (item.target ? ' target="' + item.target + '"' : '') + (item.method ? ' data-method="' + item.method + '"' : '') +
-          '>' + (item.text || '') + '</a>';
+            '>' + (item.text || '') + '</a>';
 
 
         if (item.submenu && item.submenu.length) {
         if (item.submenu && item.submenu.length) {
           li += buildTemplate(item.submenu).join('\n');
           li += buildTemplate(item.submenu).join('\n');

+ 2 - 2
public/app/core/directives/plugin_component.ts

@@ -109,7 +109,7 @@ function pluginDirectiveLoader($compile, datasourceSrv, $rootScope, $q, $http, $
               baseUrl: ds.meta.baseUrl,
               baseUrl: ds.meta.baseUrl,
               name: 'query-ctrl-' + ds.meta.id,
               name: 'query-ctrl-' + ds.meta.id,
               bindings: {target: "=", panelCtrl: "=", datasource: "="},
               bindings: {target: "=", panelCtrl: "=", datasource: "="},
-              attrs: {"target": "target", "panel-ctrl": "ctrl", datasource: "datasource"},
+              attrs: {"target": "target", "panel-ctrl": "ctrl.panelCtrl", datasource: "datasource"},
               Component: dsModule.QueryCtrl
               Component: dsModule.QueryCtrl
             };
             };
           });
           });
@@ -127,7 +127,7 @@ function pluginDirectiveLoader($compile, datasourceSrv, $rootScope, $q, $http, $
               baseUrl: ds.meta.baseUrl,
               baseUrl: ds.meta.baseUrl,
               name: 'query-options-ctrl-' + ds.meta.id,
               name: 'query-options-ctrl-' + ds.meta.id,
               bindings: {panelCtrl: "="},
               bindings: {panelCtrl: "="},
-              attrs: {"panel-ctrl": "ctrl"},
+              attrs: {"panel-ctrl": "ctrl.panelCtrl"},
               Component: dsModule.QueryOptionsCtrl
               Component: dsModule.QueryOptionsCtrl
             };
             };
           });
           });

+ 8 - 2
public/app/core/services/backend_srv.ts

@@ -4,6 +4,7 @@ import angular from 'angular';
 import _ from 'lodash';
 import _ from 'lodash';
 import config from 'app/core/config';
 import config from 'app/core/config';
 import coreModule from 'app/core/core_module';
 import coreModule from 'app/core/core_module';
+import appEvents from 'app/core/app_events';
 
 
 export class BackendSrv {
 export class BackendSrv {
   inFlightRequests = {};
   inFlightRequests = {};
@@ -150,7 +151,10 @@ export class BackendSrv {
       }
       }
     }
     }
 
 
-    return this.$http(options).catch(err => {
+    return this.$http(options).then(response => {
+      appEvents.emit('ds-request-response', response);
+      return response;
+    }).catch(err => {
       if (err.status === this.HTTP_REQUEST_CANCELLED) {
       if (err.status === this.HTTP_REQUEST_CANCELLED) {
         throw {err, cancelled: true};
         throw {err, cancelled: true};
       }
       }
@@ -166,7 +170,7 @@ export class BackendSrv {
         });
         });
       }
       }
 
 
-      //populate error obj on Internal Error
+      // populate error obj on Internal Error
       if (_.isString(err.data) && err.status === 500) {
       if (_.isString(err.data) && err.status === 500) {
         err.data = {
         err.data = {
           error: err.statusText,
           error: err.statusText,
@@ -179,7 +183,9 @@ export class BackendSrv {
         err.data.message = err.data.error;
         err.data.message = err.data.error;
       }
       }
 
 
+      appEvents.emit('ds-request-error', err);
       throw err;
       throw err;
+
     }).finally(() => {
     }).finally(() => {
       // clean up
       // clean up
       if (options.requestId) {
       if (options.requestId) {

+ 1 - 1
public/app/features/dashboard/export/exporter.ts

@@ -103,7 +103,7 @@ export class DashboardExporter {
         templateizeDatasourceUsage(variable);
         templateizeDatasourceUsage(variable);
         variable.options = [];
         variable.options = [];
         variable.current = {};
         variable.current = {};
-        variable.refresh = 1;
+        variable.refresh = variable.refresh > 0 ? variable.refresh : 1;
       }
       }
     }
     }
 
 

+ 2 - 0
public/app/features/dashboard/history/history.ts

@@ -27,6 +27,7 @@ export class HistoryListCtrl {
 
 
   /** @ngInject */
   /** @ngInject */
   constructor(private $scope,
   constructor(private $scope,
+              private $route,
               private $rootScope,
               private $rootScope,
               private $location,
               private $location,
               private $window,
               private $window,
@@ -179,6 +180,7 @@ export class HistoryListCtrl {
     this.loading = true;
     this.loading = true;
     return this.historySrv.restoreDashboard(this.dashboard, version).then(response => {
     return this.historySrv.restoreDashboard(this.dashboard, version).then(response => {
       this.$location.path('dashboard/db/' + response.slug);
       this.$location.path('dashboard/db/' + response.slug);
+      this.$route.reload();
       this.$rootScope.appEvent('alert-success', ['Dashboard restored', 'Restored from version ' + version]);
       this.$rootScope.appEvent('alert-success', ['Dashboard restored', 'Restored from version ' + version]);
     }).catch(() => {
     }).catch(() => {
       this.mode = 'list';
       this.mode = 'list';

+ 2 - 2
public/app/features/dashboard/partials/shareModal.html

@@ -86,7 +86,7 @@
 							<input type="text" data-share-panel-url class="gf-form-input" ng-model="shareUrl"></input>
 							<input type="text" data-share-panel-url class="gf-form-input" ng-model="shareUrl"></input>
 						</div>
 						</div>
 						<div class="gf-form">
 						<div class="gf-form">
-							<button class="btn btn-inverse" data-clipboard-text="{{shareUrl}}" clipboard-button><i class="fa fa-clipboard"></i> Copy</button>
+							<button class="btn btn-inverse" clipboard-button="getShareUrl()"><i class="fa fa-clipboard"></i> Copy</button>
 						</div>
 						</div>
 					</div>
 					</div>
 				</div>
 				</div>
@@ -143,7 +143,7 @@
 								{{snapshotUrl}}
 								{{snapshotUrl}}
 							</a>
 							</a>
 							<br>
 							<br>
-							<button class="btn btn-inverse" data-clipboard-text="{{snapshotUrl}}" clipboard-button><i class="fa fa-clipboard"></i> Copy Link</button>
+							<button class="btn btn-inverse" clipboard-button="getSnapshotUrl()"><i class="fa fa-clipboard"></i> Copy Link</button>
 						</div>
 						</div>
 					</div>
 					</div>
 				</div>
 				</div>

+ 4 - 15
public/app/features/dashboard/shareModalCtrl.js

@@ -2,10 +2,9 @@ define(['angular',
   'lodash',
   'lodash',
   'jquery',
   'jquery',
   'moment',
   'moment',
-  'require',
   'app/core/config',
   'app/core/config',
 ],
 ],
-function (angular, _, $, moment, require, config) {
+function (angular, _, $, moment, config) {
   'use strict';
   'use strict';
 
 
   var module = angular.module('grafana.controllers');
   var module = angular.module('grafana.controllers');
@@ -89,20 +88,10 @@ function (angular, _, $, moment, require, config) {
       $scope.imageUrl += '&tz=UTC' + encodeURIComponent(moment().format("Z"));
       $scope.imageUrl += '&tz=UTC' + encodeURIComponent(moment().format("Z"));
     };
     };
 
 
-  });
-
-  module.directive('clipboardButton',function() {
-    return function(scope, elem) {
-      require(['vendor/clipboard/dist/clipboard'], function(Clipboard) {
-        scope.clipboard = new Clipboard(elem[0]);
-      });
-
-      scope.$on('$destroy', function() {
-        if (scope.clipboard) {
-          scope.clipboard.destroy();
-        }
-      });
+    $scope.getShareUrl = function() {
+      return $scope.shareUrl;
     };
     };
+
   });
   });
 
 
 });
 });

+ 4 - 0
public/app/features/dashboard/shareSnapshotCtrl.js

@@ -96,6 +96,10 @@ function (angular, _) {
       });
       });
     };
     };
 
 
+    $scope.getSnapshotUrl = function() {
+      return $scope.snapshotUrl;
+    };
+
     $scope.scrubDashboard = function(dash) {
     $scope.scrubDashboard = function(dash) {
       // change title
       // change title
       dash.title = $scope.snapshot.name;
       dash.title = $scope.snapshot.name;

+ 1 - 1
public/app/features/panel/all.js

@@ -5,5 +5,5 @@ define([
   './query_ctrl',
   './query_ctrl',
   './panel_editor_tab',
   './panel_editor_tab',
   './query_editor_row',
   './query_editor_row',
-  './metrics_ds_selector',
+  './query_troubleshooter',
 ], function () {});
 ], function () {});

+ 0 - 113
public/app/features/panel/metrics_ds_selector.ts

@@ -1,113 +0,0 @@
-///<reference path="../../headers/common.d.ts" />
-
-import angular from 'angular';
-import _ from 'lodash';
-
-var module = angular.module('grafana.directives');
-
-var template = `
-<div class="gf-form-group">
-  <div class="gf-form-inline">
-    <div class="gf-form">
-      <label class="gf-form-label">
-        <i class="icon-gf icon-gf-datasources"></i>
-      </label>
-      <label class="gf-form-label">
-        Data Source
-      </label>
-
-      <metric-segment segment="ctrl.dsSegment"
-                      get-options="ctrl.getOptions(true)"
-                      on-change="ctrl.datasourceChanged()"></metric-segment>
-    </div>
-
-    <div class="gf-form gf-form--offset-1">
-      <button class="btn btn-secondary gf-form-btn" ng-click="ctrl.addDataQuery()" ng-hide="ctrl.current.meta.mixed">
-        <i class="fa fa-plus"></i>&nbsp;
-        Add query
-      </button>
-
-      <div class="dropdown" ng-if="ctrl.current.meta.mixed">
-        <metric-segment segment="ctrl.mixedDsSegment"
-                        get-options="ctrl.getOptions(false)"
-                        on-change="ctrl.mixedDatasourceChanged()"></metric-segment>
-      </div>
-    </div>
-  </div>
-</div>
-`;
-
-
-export class MetricsDsSelectorCtrl {
-  dsSegment: any;
-  mixedDsSegment: any;
-  dsName: string;
-  panelCtrl: any;
-  datasources: any[];
-  current: any;
-
-  /** @ngInject */
-  constructor(private uiSegmentSrv, datasourceSrv) {
-    this.datasources = datasourceSrv.getMetricSources();
-
-    var dsValue = this.panelCtrl.panel.datasource || null;
-
-    for (let ds of this.datasources) {
-      if (ds.value === dsValue) {
-        this.current = ds;
-      }
-    }
-
-    if (!this.current) {
-      this.current = {name: dsValue + ' not found', value: null};
-    }
-
-    this.dsSegment = uiSegmentSrv.newSegment({value: this.current.name, selectMode: true});
-    this.mixedDsSegment = uiSegmentSrv.newSegment({value: 'Add query', selectMode: true});
-  }
-
-  getOptions(includeBuiltin) {
-    return Promise.resolve(this.datasources.filter(value => {
-      return includeBuiltin || !value.meta.builtIn;
-    }).map(value => {
-      return this.uiSegmentSrv.newSegment(value.name);
-    }));
-  }
-
-  datasourceChanged() {
-    var ds = _.find(this.datasources, {name: this.dsSegment.value});
-    if (ds) {
-      this.current = ds;
-      this.panelCtrl.setDatasource(ds);
-    }
-  }
-
-  mixedDatasourceChanged() {
-    var target: any = {isNew: true};
-    var ds = _.find(this.datasources, {name: this.mixedDsSegment.value});
-    if (ds) {
-      target.datasource = ds.name;
-      this.panelCtrl.panel.targets.push(target);
-      this.mixedDsSegment.value = '';
-    }
-  }
-
-  addDataQuery() {
-    var target: any = {isNew: true};
-    this.panelCtrl.panel.targets.push(target);
-  }
-}
-
-module.directive('metricsDsSelector', function() {
-  return {
-    restrict: 'E',
-    template: template,
-    controller: MetricsDsSelectorCtrl,
-    bindToController: true,
-    controllerAs: 'ctrl',
-    transclude: true,
-    scope: {
-      panelCtrl: "="
-    }
-  };
-});

+ 24 - 2
public/app/features/panel/metrics_panel_ctrl.ts

@@ -1,5 +1,6 @@
 ///<reference path="../../headers/common.d.ts" />
 ///<reference path="../../headers/common.d.ts" />
 
 
+import angular from 'angular';
 import config from 'app/core/config';
 import config from 'app/core/config';
 import $ from 'jquery';
 import $ from 'jquery';
 import _ from 'lodash';
 import _ from 'lodash';
@@ -10,6 +11,7 @@ import * as rangeUtil from 'app/core/utils/rangeutil';
 import * as dateMath from 'app/core/utils/datemath';
 import * as dateMath from 'app/core/utils/datemath';
 
 
 import {Subject} from 'vendor/npm/rxjs/Subject';
 import {Subject} from 'vendor/npm/rxjs/Subject';
+import {metricsTabDirective} from './metrics_tab';
 
 
 class MetricsPanelCtrl extends PanelCtrl {
 class MetricsPanelCtrl extends PanelCtrl {
   scope: any;
   scope: any;
@@ -32,6 +34,7 @@ class MetricsPanelCtrl extends PanelCtrl {
   dataStream: any;
   dataStream: any;
   dataSubscription: any;
   dataSubscription: any;
   dataList: any;
   dataList: any;
+  nextRefId: string;
 
 
   constructor($scope, $injector) {
   constructor($scope, $injector) {
     super($scope, $injector);
     super($scope, $injector);
@@ -61,7 +64,7 @@ class MetricsPanelCtrl extends PanelCtrl {
   }
   }
 
 
   private onInitMetricsPanelEditMode() {
   private onInitMetricsPanelEditMode() {
-    this.addEditorTab('Metrics', 'public/app/partials/metrics.html');
+    this.addEditorTab('Metrics', metricsTabDirective);
     this.addEditorTab('Time range', 'public/app/features/panel/partials/panelTime.html');
     this.addEditorTab('Time range', 'public/app/features/panel/partials/panelTime.html');
   }
   }
 
 
@@ -256,7 +259,7 @@ class MetricsPanelCtrl extends PanelCtrl {
       result = {data: []};
       result = {data: []};
     }
     }
 
 
-    return this.events.emit('data-received', result.data);
+    this.events.emit('data-received', result.data);
   }
   }
 
 
   handleDataStream(stream) {
   handleDataStream(stream) {
@@ -306,6 +309,25 @@ class MetricsPanelCtrl extends PanelCtrl {
     this.datasource = null;
     this.datasource = null;
     this.refresh();
     this.refresh();
   }
   }
+
+  addQuery(target) {
+    target.refId = this.dashboard.getNextQueryLetter(this.panel);
+
+    this.panel.targets.push(target);
+    this.nextRefId = this.dashboard.getNextQueryLetter(this.panel);
+  }
+
+  removeQuery(target) {
+    var index = _.indexOf(this.panel.targets, target);
+    this.panel.targets.splice(index, 1);
+    this.nextRefId = this.dashboard.getNextQueryLetter(this.panel);
+    this.refresh();
+  }
+
+  moveQuery(target, direction) {
+    var index = _.indexOf(this.panel.targets, target);
+    _.move(this.panel.targets, index, index + direction);
+  }
 }
 }
 
 
 export {MetricsPanelCtrl};
 export {MetricsPanelCtrl};

+ 83 - 0
public/app/features/panel/metrics_tab.ts

@@ -0,0 +1,83 @@
+///<reference path="../../headers/common.d.ts" />
+
+import _ from 'lodash';
+//import {coreModule} from 'app/core/core';
+import {DashboardModel} from '../dashboard/model';
+
+export class MetricsTabCtrl {
+  dsName: string;
+  panel: any;
+  panelCtrl: any;
+  datasources: any[];
+  current: any;
+  nextRefId: string;
+  dashboard: DashboardModel;
+  panelDsValue: any;
+  addQueryDropdown: any;
+
+  /** @ngInject */
+  constructor($scope, private uiSegmentSrv, private datasourceSrv) {
+    this.panelCtrl = $scope.ctrl;
+    $scope.ctrl = this;
+
+    this.panel = this.panelCtrl.panel;
+    this.dashboard = this.panelCtrl.dashboard;
+    this.datasources = datasourceSrv.getMetricSources();
+    this.panelDsValue = this.panelCtrl.panel.datasource || null;
+
+    for (let ds of this.datasources) {
+      if (ds.value === this.panelDsValue) {
+        this.current = ds;
+      }
+    }
+
+    this.addQueryDropdown = {text: 'Add Query', value: null, fake: true};
+
+    // update next ref id
+    this.panelCtrl.nextRefId = this.dashboard.getNextQueryLetter(this.panel);
+  }
+
+  getOptions(includeBuiltin) {
+    return Promise.resolve(this.datasources.filter(value => {
+      return includeBuiltin || !value.meta.builtIn;
+    }).map(ds => {
+      return {value: ds.value, text: ds.name, datasource: ds};
+    }));
+  }
+
+  datasourceChanged(option) {
+    if (!option) {
+      return;
+    }
+
+    this.current = option.datasource;
+    this.panelCtrl.setDatasource(option.datasource);
+  }
+
+  addMixedQuery(option) {
+    if (!option) {
+      return;
+    }
+
+    var target: any = {isNew: true};
+    this.panelCtrl.addQuery({isNew: true, datasource: option.datasource.name});
+    this.addQueryDropdown = {text: 'Add Query', value: null, fake: true};
+  }
+
+  addQuery() {
+    this.panelCtrl.addQuery({isNew: true});
+  }
+}
+
+/** @ngInject **/
+export function metricsTabDirective() {
+  'use strict';
+  return {
+    restrict: 'E',
+    scope: true,
+    templateUrl: 'public/app/features/panel/partials/metrics_tab.html',
+    controller: MetricsTabCtrl,
+  };
+}
+
+//coreModule.directive('metricsTab', metricsTabDirective);

+ 1 - 1
public/app/features/panel/panel_ctrl.ts

@@ -75,7 +75,7 @@ export class PanelCtrl {
   }
   }
 
 
   refresh() {
   refresh() {
-    this.events.emit('refresh', null);
+   this.events.emit('refresh', null);
   }
   }
 
 
   publishAppEvent(evtName, evt) {
   publishAppEvent(evtName, evt) {

+ 6 - 1
public/app/features/panel/panel_editor_tab.ts

@@ -16,10 +16,15 @@ function panelEditorTab(dynamicDirectiveSrv) {
     directive: scope => {
     directive: scope => {
       var pluginId = scope.ctrl.pluginId;
       var pluginId = scope.ctrl.pluginId;
       var tabIndex = scope.index;
       var tabIndex = scope.index;
+      // create a wrapper for directiveFn
+      // required for metrics tab directive
+      // that is the same for many panels but
+      // given different names in this function
+      var fn = () => scope.editorTab.directiveFn();
 
 
       return Promise.resolve({
       return Promise.resolve({
         name: `panel-editor-tab-${pluginId}${tabIndex}`,
         name: `panel-editor-tab-${pluginId}${tabIndex}`,
-        fn: scope.editorTab.directiveFn,
+        fn: fn,
       });
       });
     }
     }
   });
   });

+ 50 - 0
public/app/features/panel/partials/metrics_tab.html

@@ -0,0 +1,50 @@
+<div class="query-editor-rows gf-form-group">
+  <div ng-repeat="target in ctrl.panel.targets" ng-class="{'gf-form-disabled': target.hide}">
+    <rebuild-on-change property="ctrl.panel.datasource || target.datasource" show-null="true">
+      <plugin-component type="query-ctrl">
+      </plugin-component>
+    </rebuild-on-change>
+	</div>
+
+	<div class="gf-form-query">
+		<div class="gf-form gf-form-query-letter-cell">
+			<label class="gf-form-label">
+				<span class="gf-form-query-letter-cell-carret">
+					<i class="fa fa-caret-down"></i>
+				</span>
+				<span class="gf-form-query-letter-cell-letter">{{ctrl.panelCtrl.nextRefId}}</span>
+			</label>
+      <button class="btn btn-secondary gf-form-btn" ng-click="ctrl.addQuery()" ng-hide="ctrl.current.meta.mixed">
+        Add Query
+      </button>
+
+      <div class="dropdown" ng-if="ctrl.current.meta.mixed">
+        <gf-form-dropdown model="ctrl.addQueryDropdown"
+                          get-options="ctrl.getOptions(false)"
+                          on-change="ctrl.addMixedQuery($option)">
+        </gf-form-dropdown>
+      </div>
+    </div>
+  </div>
+</div>
+
+<!-- <query&#45;troubleshooter panel&#45;ctrl="ctrl.panelCtrl"></query&#45;troubleshooter> -->
+
+<div class="gf-form-group">
+  <div class="gf-form-inline">
+    <div class="gf-form">
+      <label class="gf-form-label">Panel Data Source</label>
+      <gf-form-dropdown model="ctrl.panelDsValue"
+                        lookup-text="true"
+                        get-options="ctrl.getOptions(true)"
+                        on-change="ctrl.datasourceChanged($option)">
+      </gf-form-dropdown>
+    </div>
+  </div>
+</div>
+
+<rebuild-on-change property="ctrl.panel.datasource" show-null="true">
+  <plugin-component type="query-options-ctrl">
+  </plugin-component>
+</rebuild-on-change>
+</div>

+ 4 - 17
public/app/features/panel/query_editor_row.ts

@@ -21,7 +21,7 @@ export class QueryRowCtrl {
     this.panel = this.panelCtrl.panel;
     this.panel = this.panelCtrl.panel;
 
 
     if (!this.target.refId) {
     if (!this.target.refId) {
-      this.target.refId = this.getNextQueryLetter();
+      this.target.refId = this.panelCtrl.dashboard.getNextQueryLetter(this.panel);
     }
     }
 
 
     this.toggleCollapse(true);
     this.toggleCollapse(true);
@@ -40,16 +40,6 @@ export class QueryRowCtrl {
     this.panelCtrl.refresh();
     this.panelCtrl.refresh();
   }
   }
 
 
-  getNextQueryLetter() {
-    var letters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
-
-    return _.find(letters, refId => {
-      return _.every(this.panel.targets, function(other) {
-        return other.refId !== refId;
-      });
-    });
-  }
-
   toggleCollapse(init) {
   toggleCollapse(init) {
     if (!this.canCollapse) {
     if (!this.canCollapse) {
       return;
       return;
@@ -87,19 +77,16 @@ export class QueryRowCtrl {
       delete this.panelCtrl.__collapsedQueryCache[this.target.refId];
       delete this.panelCtrl.__collapsedQueryCache[this.target.refId];
     }
     }
 
 
-    this.panel.targets = _.without(this.panel.targets, this.target);
-    this.panelCtrl.refresh();
+    this.panelCtrl.removeQuery(this.target);
   }
   }
 
 
   duplicateQuery() {
   duplicateQuery() {
     var clone = angular.copy(this.target);
     var clone = angular.copy(this.target);
-    clone.refId = this.getNextQueryLetter();
-    this.panel.targets.push(clone);
+    this.panelCtrl.addQuery(clone);
   }
   }
 
 
   moveQuery(direction) {
   moveQuery(direction) {
-    var index = _.indexOf(this.panel.targets, this.target);
-    _.move(this.panel.targets, index, index + direction);
+    this.panelCtrl.moveQuery(this.target, direction);
   }
   }
 }
 }
 
 

+ 154 - 0
public/app/features/panel/query_troubleshooter.ts

@@ -0,0 +1,154 @@
+///<reference path="../../headers/common.d.ts" />
+
+import _ from 'lodash';
+import appEvents  from 'app/core/app_events';
+import {coreModule, JsonExplorer} from 'app/core/core';
+
+const template = `
+<collapse-box title="Query Troubleshooter" is-open="ctrl.isOpen" state-changed="ctrl.stateChanged()"
+              ng-class="{'collapse-box--error': ctrl.hasError}">
+  <collapse-box-actions>
+    <a class="pointer" ng-click="ctrl.toggleExpand()" ng-hide="ctrl.allNodesExpanded">
+      <i class="fa fa-plus-square-o"></i> Expand All
+    </a>
+    <a class="pointer" ng-click="ctrl.toggleExpand()" ng-show="ctrl.allNodesExpanded">
+      <i class="fa fa-minus-square-o"></i> Collapse All
+    </a>
+    <a class="pointer" clipboard-button="ctrl.getClipboardText()"><i class="fa fa-clipboard"></i> Copy to Clipboard</a>
+  </collapse-box-actions>
+  <collapse-box-body>
+    <i class="fa fa-spinner fa-spin" ng-show="ctrl.isLoading"></i>
+    <div class="query-troubleshooter-json"></div>
+  </collapse-box-body>
+</collapse-box>
+`;
+
+export class QueryTroubleshooterCtrl {
+  isOpen: any;
+  isLoading: boolean;
+  showResponse: boolean;
+  panelCtrl: any;
+  renderJsonExplorer: (data) => void;
+  onRequestErrorEventListener: any;
+  onRequestResponseEventListener: any;
+  hasError: boolean;
+  allNodesExpanded: boolean;
+  jsonExplorer: JsonExplorer;
+
+  /** @ngInject **/
+  constructor($scope, private $timeout) {
+    this.onRequestErrorEventListener = this.onRequestError.bind(this);
+    this.onRequestResponseEventListener = this.onRequestResponse.bind(this);
+
+    appEvents.on('ds-request-response', this.onRequestResponseEventListener);
+    appEvents.on('ds-request-error', this.onRequestErrorEventListener);
+    $scope.$on('$destroy',  this.removeEventsListeners.bind(this));
+  }
+
+  removeEventsListeners() {
+    appEvents.off('ds-request-response', this.onRequestResponseEventListener);
+    appEvents.off('ds-request-error', this.onRequestErrorEventListener);
+  }
+
+  onRequestError(err) {
+    this.isOpen = true;
+    this.hasError = true;
+    this.onRequestResponse(err);
+  }
+
+  stateChanged() {
+    if (this.isOpen) {
+      this.panelCtrl.refresh();
+      this.isLoading = true;
+    }
+  }
+
+  getClipboardText() {
+    if (this.jsonExplorer) {
+      return JSON.stringify(this.jsonExplorer.json, null, 2);
+    }
+  }
+
+  onRequestResponse(data) {
+    // ignore if closed
+    if (!this.isOpen) {
+      return;
+    }
+
+    this.isLoading = false;
+    data = _.cloneDeep(data);
+
+    if (data.headers) {
+      delete data.headers;
+    }
+
+    if (data.config) {
+      data.request = data.config;
+      delete data.config;
+      delete data.request.transformRequest;
+      delete data.request.transformResponse;
+      delete data.request.paramSerializer;
+      delete data.request.jsonpCallbackParam;
+      delete data.request.headers;
+      delete data.request.requestId;
+      delete data.request.inspect;
+      delete data.request.retry;
+      delete data.request.timeout;
+    }
+
+    if (data.data) {
+      data.response = data.data;
+
+      if (data.status === 200) {
+        // if we are in error state, assume we automatically opened
+        // and auto close it again
+        if (this.hasError) {
+          this.hasError = false;
+          this.isOpen = false;
+        }
+      }
+
+      delete data.data;
+      delete data.status;
+      delete data.statusText;
+      delete data.$$config;
+    }
+
+    this.$timeout(_.partial(this.renderJsonExplorer, data));
+  }
+
+  toggleExpand(depth) {
+    if (this.jsonExplorer) {
+      this.allNodesExpanded = !this.allNodesExpanded;
+      this.jsonExplorer.openAtDepth(this.allNodesExpanded ? 20 : 1);
+    }
+  }
+}
+
+export function queryTroubleshooter() {
+  return {
+    restrict: 'E',
+    template: template,
+    controller: QueryTroubleshooterCtrl,
+    bindToController: true,
+    controllerAs: 'ctrl',
+    scope: {
+      panelCtrl: "="
+    },
+    link: function(scope, elem, attrs, ctrl) {
+
+      ctrl.renderJsonExplorer = function(data) {
+        var jsonElem = elem.find('.query-troubleshooter-json');
+
+        ctrl.jsonExplorer =  new JsonExplorer(data, 3, {
+          animateOpen: true,
+        });
+
+        const html = ctrl.jsonExplorer.render(true);
+        jsonElem.html(html);
+      };
+    }
+  };
+}
+
+coreModule.directive('queryTroubleshooter', queryTroubleshooter);

+ 0 - 1
public/app/headers/common.d.ts

@@ -72,4 +72,3 @@ declare module 'd3' {
   var d3: any;
   var d3: any;
   export default d3;
   export default d3;
 }
 }
-

+ 1 - 1
public/app/partials/login.html

@@ -60,7 +60,7 @@
 						GitHub
 						GitHub
 					</a>
 					</a>
 					<a class="btn btn-large btn-grafana-com" href="login/grafana_com" target="_self" ng-if="oauth.grafana_com">
 					<a class="btn btn-large btn-grafana-com" href="login/grafana_com" target="_self" ng-if="oauth.grafana_com">
-						<img src="public/img/grafana_icon.svg"></img>
+						<img src="public/img/grafana_com_auth_icon.svg"></img>
 						<span>Grafana.com</span>
 						<span>Grafana.com</span>
 					</a>
 					</a>
 					<a class="btn btn-large btn-generic-oauth" href="login/generic_oauth" target="_self" ng-if="oauth.generic_oauth">
 					<a class="btn btn-large btn-generic-oauth" href="login/generic_oauth" target="_self" ng-if="oauth.generic_oauth">

+ 0 - 20
public/app/partials/metrics.html

@@ -1,20 +0,0 @@
-
-<div class="query-editor-rows gf-form-group">
-  <div ng-repeat="target in ctrl.panel.targets" ng-class="{'gf-form-disabled': target.hide}">
-    <rebuild-on-change property="ctrl.panel.datasource || target.datasource" show-null="true">
-      <plugin-component type="query-ctrl">
-      </plugin-component>
-    </rebuild-on-change>
-  </div>
-</div>
-
-<metrics-ds-selector panel-ctrl="ctrl"></metrics-ds-selector>
-
-<div class="gf-form-group">
-  <rebuild-on-change property="ctrl.panel.datasource" show-null="true">
-    <plugin-component type="query-options-ctrl">
-    </plugin-component>
-  </rebuild-on-change>
-</div>
-
-<div class="clearfix"></div>

+ 14 - 7
public/app/plugins/datasource/elasticsearch/bucket_agg.js

@@ -26,13 +26,21 @@ function (angular, _, queryDef) {
     var bucketAggs = $scope.target.bucketAggs;
     var bucketAggs = $scope.target.bucketAggs;
 
 
     $scope.orderByOptions = [];
     $scope.orderByOptions = [];
-    $scope.bucketAggTypes = queryDef.bucketAggTypes;
-    $scope.orderOptions = queryDef.orderOptions;
-    $scope.sizeOptions = queryDef.sizeOptions;
+
+    $scope.getBucketAggTypes = function() {
+      return queryDef.bucketAggTypes;
+    };
+
+    $scope.getOrderOptions = function() {
+      return queryDef.orderOptions;
+    };
+
+    $scope.getSizeOptions = function() {
+      return queryDef.sizeOptions;
+    };
 
 
     $rootScope.onAppEvent('elastic-query-updated', function() {
     $rootScope.onAppEvent('elastic-query-updated', function() {
       $scope.validateModel();
       $scope.validateModel();
-      $scope.updateOrderByOptions();
     }, $scope);
     }, $scope);
 
 
     $scope.init = function() {
     $scope.init = function() {
@@ -166,11 +174,10 @@ function (angular, _, queryDef) {
 
 
     $scope.toggleOptions = function() {
     $scope.toggleOptions = function() {
       $scope.showOptions = !$scope.showOptions;
       $scope.showOptions = !$scope.showOptions;
-      $scope.updateOrderByOptions();
     };
     };
 
 
-    $scope.updateOrderByOptions = function() {
-      $scope.orderByOptions = queryDef.getOrderByOptions($scope.target);
+    $scope.getOrderByOptions = function() {
+      return queryDef.getOrderByOptions($scope.target);
     };
     };
 
 
     $scope.getFieldsInternal = function() {
     $scope.getFieldsInternal = function() {

+ 44 - 6
public/app/plugins/datasource/elasticsearch/partials/bucket_agg.html

@@ -5,8 +5,22 @@
 			<span ng-hide="isFirst">Then by</span>
 			<span ng-hide="isFirst">Then by</span>
 		</label>
 		</label>
 
 
-		<metric-segment-model property="agg.type" options="bucketAggTypes" on-change="onTypeChanged()" custom="false" css-class="width-10"></metric-segment-model>
-		<metric-segment-model ng-if="agg.field" property="agg.field" get-options="getFieldsInternal()" on-change="onChange()" css-class="width-12"></metric-segment-model>
+		<gf-form-dropdown model="agg.type"
+											lookup-text="true"
+											get-options="getBucketAggTypes()"
+											on-change="onTypeChanged()"
+											allow-custom="false"
+											label-mode="true"
+											css-class="width-10">
+		</gf-form-dropdown>
+		<gf-form-dropdown ng-if="agg.field"
+											model="agg.field"
+											get-options="getFieldsInternal()"
+											on-change="onChange()"
+											allow-custom="false"
+											label-mode="true"
+											css-class="width-12">
+		</gf-form-dropdown>
 	</div>
 	</div>
 
 
 	<div class="gf-form gf-form--grow">
 	<div class="gf-form gf-form--grow">
@@ -33,7 +47,13 @@
 	<div ng-if="agg.type === 'date_histogram'">
 	<div ng-if="agg.type === 'date_histogram'">
 		<div class="gf-form offset-width-7">
 		<div class="gf-form offset-width-7">
 			<label class="gf-form-label width-10">Interval</label>
 			<label class="gf-form-label width-10">Interval</label>
-			<metric-segment-model property="agg.settings.interval" get-options="getIntervalOptions()" on-change="onChangeInternal()" css-class="width-12" custom="true"></metric-segment-model>
+			<gf-form-dropdown model="agg.settings.interval"
+												get-options="getIntervalOptions()"
+												on-change="onChangeInternal()"
+												allow-custom="true"
+												label-mode="true"
+												css-class="width-12">
+			</gf-form-dropdown>
 		</div>
 		</div>
 
 
 		<div class="gf-form offset-width-7">
 		<div class="gf-form offset-width-7">
@@ -66,11 +86,23 @@
 	<div ng-if="agg.type === 'terms'">
 	<div ng-if="agg.type === 'terms'">
 		<div class="gf-form offset-width-7">
 		<div class="gf-form offset-width-7">
 			<label class="gf-form-label width-10">Order</label>
 			<label class="gf-form-label width-10">Order</label>
-			<metric-segment-model property="agg.settings.order" options="orderOptions" on-change="onChangeInternal()" css-class="width-12"></metric-segment-model>
+			<gf-form-dropdown model="agg.settings.order"
+											  lookup-text="true"
+												get-options="getOrderOptions()"
+												on-change="onChangeInternal()"
+												label-mode="true"
+												css-class="width-12">
+			</gf-form-dropdown>
 		</div>
 		</div>
 		<div class="gf-form offset-width-7">
 		<div class="gf-form offset-width-7">
 			<label class="gf-form-label width-10">Size</label>
 			<label class="gf-form-label width-10">Size</label>
-			<metric-segment-model property="agg.settings.size" options="sizeOptions" on-change="onChangeInternal()" css-class="width-12"></metric-segment-model>
+			<gf-form-dropdown model="agg.settings.size"
+											  lookup-text="true"
+												get-options="getSizeOptions()"
+												on-change="onChangeInternal()"
+												label-mode="true"
+												css-class="width-12">
+			</gf-form-dropdown>
 		</div>
 		</div>
 		<div class="gf-form offset-width-7">
 		<div class="gf-form offset-width-7">
 			<label class="gf-form-label width-10">Min Doc Count</label>
 			<label class="gf-form-label width-10">Min Doc Count</label>
@@ -78,7 +110,13 @@
 		</div>
 		</div>
 		<div class="gf-form offset-width-7">
 		<div class="gf-form offset-width-7">
 			<label class="gf-form-label width-10">Order By</label>
 			<label class="gf-form-label width-10">Order By</label>
-			<metric-segment-model property="agg.settings.orderBy" options="orderByOptions" on-change="onChangeInternal()" css-class="width-12"></metric-segment-model>
+			<gf-form-dropdown model="agg.settings.orderBy"
+											  lookup-text="true"
+												get-options="getOrderByOptions()"
+												on-change="onChangeInternal()"
+												label-mode="true"
+												css-class="width-12">
+			</gf-form-dropdown>
 		</div>
 		</div>
 		<div class="gf-form offset-width-7">
 		<div class="gf-form offset-width-7">
 			<label class="gf-form-label width-10">
 			<label class="gf-form-label width-10">

+ 2 - 2
public/app/plugins/datasource/elasticsearch/query_ctrl.ts

@@ -31,11 +31,11 @@ export class ElasticQueryCtrl extends QueryCtrl {
 
 
   queryUpdated() {
   queryUpdated() {
     var newJson = angular.toJson(this.datasource.queryBuilder.build(this.target), true);
     var newJson = angular.toJson(this.datasource.queryBuilder.build(this.target), true);
-    if (newJson !== this.rawQueryOld) {
-      this.rawQueryOld = newJson;
+    if (this.rawQueryOld && newJson !== this.rawQueryOld) {
       this.refresh();
       this.refresh();
     }
     }
 
 
+    this.rawQueryOld = newJson;
     this.$rootScope.appEvent('elastic-query-updated');
     this.$rootScope.appEvent('elastic-query-updated');
   }
   }
 
 

+ 1 - 1
public/app/plugins/datasource/graphite/partials/query.options.html

@@ -1,4 +1,4 @@
-<section class="grafana-metric-options gf-form-group">
+<section class="gf-form-group">
 	<div class="gf-form-inline">
 	<div class="gf-form-inline">
 		<div class="gf-form max-width-15">
 		<div class="gf-form max-width-15">
 			<span class="gf-form-label width-8">
 			<span class="gf-form-label width-8">

+ 6 - 4
public/app/plugins/datasource/influxdb/datasource.ts

@@ -210,10 +210,12 @@ export default class InfluxDatasource {
     var currentUrl = self.urls.shift();
     var currentUrl = self.urls.shift();
     self.urls.push(currentUrl);
     self.urls.push(currentUrl);
 
 
-    var params: any = {
-      u: self.username,
-      p: self.password,
-    };
+    var params: any = {};
+
+    if (self.username) {
+      params.username =  self.username;
+      params.password =  self.password;
+    }
 
 
     if (self.database) {
     if (self.database) {
       params.db = self.database;
       params.db = self.database;

+ 1 - 1
public/app/system.conf.js

@@ -32,7 +32,7 @@ System.config({
     "jquery.flot.fillbelow": "vendor/flot/jquery.flot.fillbelow",
     "jquery.flot.fillbelow": "vendor/flot/jquery.flot.fillbelow",
     "jquery.flot.gauge": "vendor/flot/jquery.flot.gauge",
     "jquery.flot.gauge": "vendor/flot/jquery.flot.gauge",
     "d3": "vendor/d3/d3.js",
     "d3": "vendor/d3/d3.js",
-    "jquery.flot.dashes": "vendor/flot/jquery.flot.dashes"
+    "jquery.flot.dashes": "vendor/flot/jquery.flot.dashes",
   },
   },
 
 
   packages: {
   packages: {

+ 57 - 0
public/img/grafana_com_auth_icon.svg

@@ -0,0 +1,57 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 20.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0)  -->
+<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+	 width="351px" height="365px" viewBox="0 0 351 365" style="enable-background:new 0 0 351 365;" xml:space="preserve">
+<style type="text/css">
+	.st0{fill:url(#SVGID_1_);}
+</style>
+<g id="Layer_1_1_">
+</g>
+<linearGradient id="SVGID_1_" gradientUnits="userSpaceOnUse" x1="175.5" y1="445.4948" x2="175.5" y2="114.0346">
+	<stop  offset="0" style="stop-color:#FFF100"/>
+	<stop  offset="1" style="stop-color:#F05A28"/>
+</linearGradient>
+<path class="st0" d="M342,161.2c-0.6-6.1-1.6-13.1-3.6-20.9c-2-7.7-5-16.2-9.4-25c-4.4-8.8-10.1-17.9-17.5-26.8
+	c-2.9-3.5-6.1-6.9-9.5-10.2c5.1-20.3-6.2-37.9-6.2-37.9c-19.5-1.2-31.9,6.1-36.5,9.4c-0.8-0.3-1.5-0.7-2.3-1
+	c-3.3-1.3-6.7-2.6-10.3-3.7c-3.5-1.1-7.1-2.1-10.8-3c-3.7-0.9-7.4-1.6-11.2-2.2c-0.7-0.1-1.3-0.2-2-0.3
+	c-8.5-27.2-32.9-38.6-32.9-38.6c-27.3,17.3-32.4,41.5-32.4,41.5s-0.1,0.5-0.3,1.4c-1.5,0.4-3,0.9-4.5,1.3c-2.1,0.6-4.2,1.4-6.2,2.2
+	c-2.1,0.8-4.1,1.6-6.2,2.5c-4.1,1.8-8.2,3.8-12.2,6c-3.9,2.2-7.7,4.6-11.4,7.1c-0.5-0.2-1-0.4-1-0.4c-37.8-14.4-71.3,2.9-71.3,2.9
+	c-3.1,40.2,15.1,65.5,18.7,70.1c-0.9,2.5-1.7,5-2.5,7.5c-2.8,9.1-4.9,18.4-6.2,28.1c-0.2,1.4-0.4,2.8-0.5,4.2
+	C18.8,192.7,8.5,228,8.5,228c29.1,33.5,63.1,35.6,63.1,35.6c0,0,0.1-0.1,0.1-0.1c4.3,7.7,9.3,15,14.9,21.9c2.4,2.9,4.8,5.6,7.4,8.3
+	c-10.6,30.4,1.5,55.6,1.5,55.6c32.4,1.2,53.7-14.2,58.2-17.7c3.2,1.1,6.5,2.1,9.8,2.9c10,2.6,20.2,4.1,30.4,4.5
+	c2.5,0.1,5.1,0.2,7.6,0.1l1.2,0l0.8,0l1.6,0l1.6-0.1l0,0.1c15.3,21.8,42.1,24.9,42.1,24.9c19.1-20.1,20.2-40.1,20.2-44.4l0,0
+	c0,0,0-0.1,0-0.3c0-0.4,0-0.6,0-0.6l0,0c0-0.3,0-0.6,0-0.9c4-2.8,7.8-5.8,11.4-9.1c7.6-6.9,14.3-14.8,19.9-23.3
+	c0.5-0.8,1-1.6,1.5-2.4c21.6,1.2,36.9-13.4,36.9-13.4c-3.6-22.5-16.4-33.5-19.1-35.6l0,0c0,0-0.1-0.1-0.3-0.2
+	c-0.2-0.1-0.2-0.2-0.2-0.2c0,0,0,0,0,0c-0.1-0.1-0.3-0.2-0.5-0.3c0.1-1.4,0.2-2.7,0.3-4.1c0.2-2.4,0.2-4.9,0.2-7.3l0-1.8l0-0.9
+	l0-0.5c0-0.6,0-0.4,0-0.6l-0.1-1.5l-0.1-2c0-0.7-0.1-1.3-0.2-1.9c-0.1-0.6-0.1-1.3-0.2-1.9l-0.2-1.9l-0.3-1.9
+	c-0.4-2.5-0.8-4.9-1.4-7.4c-2.3-9.7-6.1-18.9-11-27.2c-5-8.3-11.2-15.6-18.3-21.8c-7-6.2-14.9-11.2-23.1-14.9
+	c-8.3-3.7-16.9-6.1-25.5-7.2c-4.3-0.6-8.6-0.8-12.9-0.7l-1.6,0l-0.4,0c-0.1,0-0.6,0-0.5,0l-0.7,0l-1.6,0.1c-0.6,0-1.2,0.1-1.7,0.1
+	c-2.2,0.2-4.4,0.5-6.5,0.9c-8.6,1.6-16.7,4.7-23.8,9c-7.1,4.3-13.3,9.6-18.3,15.6c-5,6-8.9,12.7-11.6,19.6c-2.7,6.9-4.2,14.1-4.6,21
+	c-0.1,1.7-0.1,3.5-0.1,5.2c0,0.4,0,0.9,0,1.3l0.1,1.4c0.1,0.8,0.1,1.7,0.2,2.5c0.3,3.5,1,6.9,1.9,10.1c1.9,6.5,4.9,12.4,8.6,17.4
+	c3.7,5,8.2,9.1,12.9,12.4c4.7,3.2,9.8,5.5,14.8,7c5,1.5,10,2.1,14.7,2.1c0.6,0,1.2,0,1.7,0c0.3,0,0.6,0,0.9,0c0.3,0,0.6,0,0.9-0.1
+	c0.5,0,1-0.1,1.5-0.1c0.1,0,0.3,0,0.4-0.1l0.5-0.1c0.3,0,0.6-0.1,0.9-0.1c0.6-0.1,1.1-0.2,1.7-0.3c0.6-0.1,1.1-0.2,1.6-0.4
+	c1.1-0.2,2.1-0.6,3.1-0.9c2-0.7,4-1.5,5.7-2.4c1.8-0.9,3.4-2,5-3c0.4-0.3,0.9-0.6,1.3-1c1.6-1.3,1.9-3.7,0.6-5.3
+	c-1.1-1.4-3.1-1.8-4.7-0.9c-0.4,0.2-0.8,0.4-1.2,0.6c-1.4,0.7-2.8,1.3-4.3,1.8c-1.5,0.5-3.1,0.9-4.7,1.2c-0.8,0.1-1.6,0.2-2.5,0.3
+	c-0.4,0-0.8,0.1-1.3,0.1c-0.4,0-0.9,0-1.2,0c-0.4,0-0.8,0-1.2,0c-0.5,0-1,0-1.5-0.1c0,0-0.3,0-0.1,0l-0.2,0l-0.3,0
+	c-0.2,0-0.5,0-0.7-0.1c-0.5-0.1-0.9-0.1-1.4-0.2c-3.7-0.5-7.4-1.6-10.9-3.2c-3.6-1.6-7-3.8-10.1-6.6c-3.1-2.8-5.8-6.1-7.9-9.9
+	c-2.1-3.8-3.6-8-4.3-12.4c-0.3-2.2-0.5-4.5-0.4-6.7c0-0.6,0.1-1.2,0.1-1.8c0,0.2,0-0.1,0-0.1l0-0.2l0-0.5c0-0.3,0.1-0.6,0.1-0.9
+	c0.1-1.2,0.3-2.4,0.5-3.6c1.7-9.6,6.5-19,13.9-26.1c1.9-1.8,3.9-3.4,6-4.9c2.1-1.5,4.4-2.8,6.8-3.9c2.4-1.1,4.8-2,7.4-2.7
+	c2.5-0.7,5.1-1.1,7.8-1.4c1.3-0.1,2.6-0.2,4-0.2c0.4,0,0.6,0,0.9,0l1.1,0l0.7,0c0.3,0,0,0,0.1,0l0.3,0l1.1,0.1
+	c2.9,0.2,5.7,0.6,8.5,1.3c5.6,1.2,11.1,3.3,16.2,6.1c10.2,5.7,18.9,14.5,24.2,25.1c2.7,5.3,4.6,11,5.5,16.9c0.2,1.5,0.4,3,0.5,4.5
+	l0.1,1.1l0.1,1.1c0,0.4,0,0.8,0,1.1c0,0.4,0,0.8,0,1.1l0,1l0,1.1c0,0.7-0.1,1.9-0.1,2.6c-0.1,1.6-0.3,3.3-0.5,4.9
+	c-0.2,1.6-0.5,3.2-0.8,4.8c-0.3,1.6-0.7,3.2-1.1,4.7c-0.8,3.1-1.8,6.2-3,9.3c-2.4,6-5.6,11.8-9.4,17.1
+	c-7.7,10.6-18.2,19.2-30.2,24.7c-6,2.7-12.3,4.7-18.8,5.7c-3.2,0.6-6.5,0.9-9.8,1l-0.6,0l-0.5,0l-1.1,0l-1.6,0l-0.8,0
+	c0.4,0-0.1,0-0.1,0l-0.3,0c-1.8,0-3.5-0.1-5.3-0.3c-7-0.5-13.9-1.8-20.7-3.7c-6.7-1.9-13.2-4.6-19.4-7.8
+	c-12.3-6.6-23.4-15.6-32-26.5c-4.3-5.4-8.1-11.3-11.2-17.4c-3.1-6.1-5.6-12.6-7.4-19.1c-1.8-6.6-2.9-13.3-3.4-20.1l-0.1-1.3l0-0.3
+	l0-0.3l0-0.6l0-1.1l0-0.3l0-0.4l0-0.8l0-1.6l0-0.3c0,0,0,0.1,0-0.1l0-0.6c0-0.8,0-1.7,0-2.5c0.1-3.3,0.4-6.8,0.8-10.2
+	c0.4-3.4,1-6.9,1.7-10.3c0.7-3.4,1.5-6.8,2.5-10.2c1.9-6.7,4.3-13.2,7.1-19.3c5.7-12.2,13.1-23.1,22-31.8c2.2-2.2,4.5-4.2,6.9-6.2
+	c2.4-1.9,4.9-3.7,7.5-5.4c2.5-1.7,5.2-3.2,7.9-4.6c1.3-0.7,2.7-1.4,4.1-2c0.7-0.3,1.4-0.6,2.1-0.9c0.7-0.3,1.4-0.6,2.1-0.9
+	c2.8-1.2,5.7-2.2,8.7-3.1c0.7-0.2,1.5-0.4,2.2-0.7c0.7-0.2,1.5-0.4,2.2-0.6c1.5-0.4,3-0.8,4.5-1.1c0.7-0.2,1.5-0.3,2.3-0.5
+	c0.8-0.2,1.5-0.3,2.3-0.5c0.8-0.1,1.5-0.3,2.3-0.4l1.1-0.2l1.2-0.2c0.8-0.1,1.5-0.2,2.3-0.3c0.9-0.1,1.7-0.2,2.6-0.3
+	c0.7-0.1,1.9-0.2,2.6-0.3c0.5-0.1,1.1-0.1,1.6-0.2l1.1-0.1l0.5-0.1l0.6,0c0.9-0.1,1.7-0.1,2.6-0.2l1.3-0.1c0,0,0.5,0,0.1,0l0.3,0
+	l0.6,0c0.7,0,1.5-0.1,2.2-0.1c2.9-0.1,5.9-0.1,8.8,0c5.8,0.2,11.5,0.9,17,1.9c11.1,2.1,21.5,5.6,31,10.3
+	c9.5,4.6,17.9,10.3,25.3,16.5c0.5,0.4,0.9,0.8,1.4,1.2c0.4,0.4,0.9,0.8,1.3,1.2c0.9,0.8,1.7,1.6,2.6,2.4c0.9,0.8,1.7,1.6,2.5,2.4
+	c0.8,0.8,1.6,1.6,2.4,2.5c3.1,3.3,6,6.6,8.6,10c5.2,6.7,9.4,13.5,12.7,19.9c0.2,0.4,0.4,0.8,0.6,1.2c0.2,0.4,0.4,0.8,0.6,1.2
+	c0.4,0.8,0.8,1.6,1.1,2.4c0.4,0.8,0.7,1.5,1.1,2.3c0.3,0.8,0.7,1.5,1,2.3c1.2,3,2.4,5.9,3.3,8.6c1.5,4.4,2.6,8.3,3.5,11.7
+	c0.3,1.4,1.6,2.3,3,2.1c1.5-0.1,2.6-1.3,2.6-2.8C342.6,170.4,342.5,166.1,342,161.2z"/>
+</svg>

+ 57 - 58
public/img/grafana_icon.svg

@@ -1,58 +1,57 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!-- Generator: Adobe Illustrator 19.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0)  -->
-<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" width="351px"
-	 height="365px" viewBox="0 0 351 365" style="enable-background:new 0 0 351 365;" xml:space="preserve">
-<style type="text/css">
-	.st0{fill:url(#SVGID_1_);}
-</style>
-<g id="Layer_1">
-</g>
-<g id="Layer_2">
-	<linearGradient id="SVGID_1_" gradientUnits="userSpaceOnUse" x1="170.963" y1="439.9414" x2="170.963" y2="106.0154">
-		<stop  offset="0" style="stop-color:#FFF200"/>
-		<stop  offset="1" style="stop-color:#F15A29"/>
-	</linearGradient>
-	<path class="st0" d="M228.8,239.8c-1.1-1.4-3.1-1.8-4.7-0.9c-0.4,0.2-0.8,0.4-1.2,0.6c-1.4,0.7-2.8,1.3-4.3,1.8
-		c-1.5,0.5-3.1,0.9-4.7,1.2c-0.8,0.1-1.6,0.2-2.5,0.3c-0.4,0-0.8,0.1-1.3,0.1c-0.4,0-0.9,0-1.2,0c-0.4,0-0.8,0-1.2,0
-		c-0.5,0-1,0-1.5-0.1c0,0-0.3,0-0.1,0l-0.2,0l-0.3,0c-0.2,0-0.5,0-0.7-0.1c-0.5-0.1-0.9-0.1-1.4-0.2c-3.7-0.5-7.4-1.6-10.9-3.2
-		c-3.6-1.6-7-3.8-10.1-6.6c-3.1-2.8-5.8-6.1-7.9-9.9c-2.1-3.8-3.6-8-4.3-12.4c-0.3-2.2-0.5-4.5-0.4-6.7c0-0.6,0.1-1.2,0.1-1.8
-		c0,0.2,0-0.1,0-0.1l0-0.2l0-0.5c0-0.3,0.1-0.6,0.1-0.9c0.1-1.2,0.3-2.4,0.5-3.6c1.7-9.6,6.5-19,13.9-26.1c1.9-1.8,3.9-3.4,6-4.9
-		c2.1-1.5,4.4-2.8,6.8-3.9c2.4-1.1,4.8-2,7.4-2.7c2.5-0.7,5.1-1.1,7.8-1.4c1.3-0.1,2.6-0.2,4-0.2c0.4,0,0.6,0,0.9,0l1.1,0l0.7,0
-		c0.3,0,0,0,0.1,0l0.3,0l1.1,0.1c2.9,0.2,5.7,0.6,8.5,1.3c5.6,1.2,11.1,3.3,16.2,6.1c10.2,5.7,18.9,14.5,24.2,25.1
-		c2.7,5.3,4.6,11,5.5,16.9c0.2,1.5,0.4,3,0.5,4.5l0.1,1.1l0.1,1.1c0,0.4,0,0.8,0,1.1c0,0.4,0,0.8,0,1.1l0,1l0,1.1
-		c0,0.7-0.1,1.9-0.1,2.6c-0.1,1.6-0.3,3.3-0.5,4.9c-0.2,1.6-0.5,3.2-0.8,4.8c-0.3,1.6-0.7,3.2-1.1,4.7c-0.8,3.1-1.8,6.2-3,9.3
-		c-2.4,6-5.6,11.8-9.4,17.1c-7.7,10.6-18.2,19.2-30.1,24.7c-6,2.7-12.3,4.7-18.8,5.7c-3.2,0.6-6.5,0.9-9.8,1l-0.6,0l-0.5,0l-1.1,0
-		l-1.6,0l-0.8,0c0.4,0-0.1,0-0.1,0l-0.3,0c-1.8,0-3.5-0.1-5.3-0.3c-7-0.5-13.9-1.8-20.7-3.7c-6.7-1.9-13.2-4.6-19.4-7.8
-		c-12.3-6.6-23.3-15.6-32-26.5c-4.3-5.4-8.1-11.3-11.2-17.4c-3.1-6.1-5.6-12.6-7.4-19.1c-1.8-6.6-2.9-13.3-3.4-20.1l-0.1-1.3l0-0.3
-		l0-0.3l0-0.6l0-1.1l0-0.3l0-0.4l0-0.8l0-1.6l0-0.3c0,0,0,0.1,0-0.1l0-0.6c0-0.8,0-1.7,0-2.5c0.1-3.3,0.4-6.8,0.8-10.2
-		c0.4-3.4,1-6.9,1.7-10.3c0.7-3.4,1.5-6.8,2.5-10.2c1.9-6.7,4.3-13.2,7.1-19.3c5.7-12.2,13.1-23.1,22-31.8c2.2-2.2,4.5-4.2,6.9-6.2
-		c2.4-1.9,4.9-3.7,7.5-5.4c2.5-1.7,5.2-3.2,7.9-4.6c1.3-0.7,2.7-1.4,4.1-2c0.7-0.3,1.4-0.6,2.1-0.9c0.7-0.3,1.4-0.6,2.1-0.9
-		c2.8-1.2,5.7-2.2,8.7-3.1c0.7-0.2,1.5-0.4,2.2-0.7c0.7-0.2,1.5-0.4,2.2-0.6c1.5-0.4,3-0.8,4.5-1.1c0.7-0.2,1.5-0.3,2.3-0.5
-		c0.8-0.2,1.5-0.3,2.3-0.5c0.8-0.1,1.5-0.3,2.3-0.4l1.1-0.2l1.1-0.2c0.8-0.1,1.5-0.2,2.3-0.3c0.9-0.1,1.7-0.2,2.6-0.3
-		c0.7-0.1,1.9-0.2,2.6-0.3c0.5-0.1,1.1-0.1,1.6-0.2l1.1-0.1l0.5-0.1l0.6,0c0.9-0.1,1.7-0.1,2.6-0.2l1.3-0.1c0,0,0.5,0,0.1,0l0.3,0
-		l0.6,0c0.7,0,1.5-0.1,2.2-0.1c2.9-0.1,5.9-0.1,8.8,0c5.8,0.2,11.5,0.9,17,1.9c11.1,2.1,21.5,5.6,30.9,10.3
-		c9.5,4.6,17.9,10.3,25.3,16.5c0.5,0.4,0.9,0.8,1.4,1.2c0.4,0.4,0.9,0.8,1.3,1.2c0.9,0.8,1.7,1.6,2.6,2.4c0.9,0.8,1.7,1.6,2.5,2.4
-		c0.8,0.8,1.6,1.6,2.4,2.5c3.1,3.3,6,6.6,8.6,10c5.2,6.7,9.4,13.5,12.7,19.9c0.2,0.4,0.4,0.8,0.6,1.2c0.2,0.4,0.4,0.8,0.6,1.2
-		c0.4,0.8,0.8,1.6,1.1,2.3c0.4,0.8,0.7,1.5,1.1,2.3c0.3,0.8,0.7,1.5,1,2.3c1.2,3,2.4,5.8,3.3,8.6c1.5,4.4,2.6,8.3,3.5,11.7
-		c0.3,1.4,1.6,2.3,3,2.1l0,0c1.5-0.1,2.6-1.3,2.6-2.8c0.1-3.7,0-8-0.4-12.9c-0.6-6.1-1.6-13.1-3.6-20.9c-2-7.7-5-16.2-9.4-25
-		c-4.4-8.8-10.1-17.9-17.5-26.8c-2.9-3.5-6.1-6.9-9.5-10.2c5.1-20.3-6.2-37.9-6.2-37.9c-19.5-1.2-31.9,6.1-36.5,9.4
-		c-0.8-0.3-1.5-0.7-2.3-1c-3.3-1.3-6.7-2.6-10.2-3.7c-3.5-1.1-7.1-2.1-10.8-3c-3.7-0.9-7.4-1.6-11.2-2.2c-0.7-0.1-1.3-0.2-2-0.3
-		C209.6,12.4,185.2,1,185.2,1c-27.3,17.3-32.4,41.5-32.4,41.5c0.2,0.4,0.5,0.8,0.7,1.1c-1.8,0.5-3.7,1.1-5.5,1.6
-		c-2.1,0.6-4.2,1.4-6.2,2.2c-2.1,0.8-4.1,1.6-6.2,2.5c-4.1,1.8-8.2,3.8-12.2,6c-4,2.2-7.9,4.7-11.7,7.4c-0.1,0.1-0.2,0.2-0.4,0.3
-		c-0.2-0.6-0.3-0.9-0.3-0.9c-37.7-14.4-71.3,2.9-71.3,2.9c-3.1,40.2,15.1,65.4,18.7,70c-0.9,2.5-1.7,5-2.5,7.5
-		c-2.8,9.1-4.9,18.4-6.2,28.1c-0.2,1.4-0.4,2.8-0.5,4.2C14.3,192.6,4,227.9,4,227.9c29.1,33.5,63,35.5,63,35.5c0,0,0.1-0.1,0.1-0.1
-		c4.3,7.7,9.3,15,14.9,21.9c2.4,2.9,4.8,5.6,7.4,8.3c-10.6,30.3,1.5,55.6,1.5,55.6c32.4,1.2,53.7-14.2,58.2-17.7
-		c3.2,1.1,6.5,2.1,9.8,2.9c10,2.6,20.2,4.1,30.3,4.5c2.5,0.1,5.1,0.2,7.6,0.1l1.2,0l0.8,0l1.6,0l1.6-0.1c0,0,0,0.1,0,0.1
-		c15.3,21.8,42.1,24.9,42.1,24.9c21.6-22.7,20.2-45.3,20.2-45.3c-0.2-0.2-0.4-0.3-0.6-0.5c4.2-2.9,8.2-6.1,12-9.5
-		c7.6-6.9,14.3-14.8,19.9-23.3c0.5-0.8,1-1.6,1.5-2.4c21.6,1.2,36.9-13.4,36.9-13.4c-4-25.2-19.6-36-19.6-36c-0.2,0-0.3,0.1-0.5,0.1
-		c0.2-1.5,0.3-3,0.4-4.5c0.2-2.4,0.2-4.9,0.2-7.3l0-1.8l0-0.9l0-0.5c0-0.6,0-0.4,0-0.6l-0.1-1.5l-0.1-2c0-0.7-0.1-1.3-0.2-1.9
-		c-0.1-0.6-0.1-1.3-0.2-1.9l-0.2-1.9l-0.3-1.9c-0.4-2.5-0.8-4.9-1.4-7.4c-2.3-9.7-6.1-18.9-11-27.2c-5-8.3-11.2-15.6-18.2-21.8
-		c-7-6.2-14.9-11.2-23.1-14.9c-8.2-3.7-16.9-6.1-25.5-7.2c-4.3-0.6-8.6-0.8-12.9-0.7l-1.6,0l-0.4,0c-0.1,0-0.6,0-0.5,0l-0.7,0
-		l-1.6,0.1c-0.6,0-1.2,0.1-1.7,0.1c-2.2,0.2-4.4,0.5-6.5,0.9c-8.6,1.6-16.6,4.7-23.8,9c-7.1,4.3-13.3,9.6-18.3,15.6
-		c-5,6-8.9,12.7-11.6,19.6c-2.7,6.9-4.2,14.1-4.6,21c-0.1,1.7-0.1,3.5-0.1,5.2c0,0.4,0,0.9,0,1.3l0.1,1.4c0.1,0.8,0.1,1.7,0.2,2.5
-		c0.3,3.5,1,6.9,1.9,10.1c1.9,6.5,4.9,12.4,8.6,17.4c3.7,5,8.2,9.1,12.9,12.4c4.7,3.2,9.8,5.5,14.8,7c5,1.5,10,2.1,14.7,2.1
-		c0.6,0,1.2,0,1.7,0c0.3,0,0.6,0,0.9,0c0.3,0,0.6,0,0.9-0.1c0.5,0,1-0.1,1.5-0.1c0.1,0,0.3,0,0.4-0.1l0.5-0.1c0.3,0,0.6-0.1,0.9-0.1
-		c0.6-0.1,1.1-0.2,1.7-0.3c0.6-0.1,1.1-0.2,1.6-0.4c1.1-0.2,2.1-0.6,3.1-0.9c2-0.7,3.9-1.5,5.7-2.4c1.8-0.9,3.4-2,5-3
-		c0.4-0.3,0.9-0.6,1.3-1C229.9,243.8,230.1,241.4,228.8,239.8L228.8,239.8z"/>
-</g>
-</svg>
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 20.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0)  -->
+<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+	 width="351px" height="365px" viewBox="0 0 351 365" style="enable-background:new 0 0 351 365;" xml:space="preserve">
+<style type="text/css">
+	.st0{fill:url(#SVGID_1_);}
+</style>
+<g id="Layer_1_1_">
+</g>
+<linearGradient id="SVGID_1_" gradientUnits="userSpaceOnUse" x1="175.5" y1="445.4948" x2="175.5" y2="114.0346">
+	<stop  offset="0" style="stop-color:#FFF100"/>
+	<stop  offset="1" style="stop-color:#F05A28"/>
+</linearGradient>
+<path class="st0" d="M342,161.2c-0.6-6.1-1.6-13.1-3.6-20.9c-2-7.7-5-16.2-9.4-25c-4.4-8.8-10.1-17.9-17.5-26.8
+	c-2.9-3.5-6.1-6.9-9.5-10.2c5.1-20.3-6.2-37.9-6.2-37.9c-19.5-1.2-31.9,6.1-36.5,9.4c-0.8-0.3-1.5-0.7-2.3-1
+	c-3.3-1.3-6.7-2.6-10.3-3.7c-3.5-1.1-7.1-2.1-10.8-3c-3.7-0.9-7.4-1.6-11.2-2.2c-0.7-0.1-1.3-0.2-2-0.3
+	c-8.5-27.2-32.9-38.6-32.9-38.6c-27.3,17.3-32.4,41.5-32.4,41.5s-0.1,0.5-0.3,1.4c-1.5,0.4-3,0.9-4.5,1.3c-2.1,0.6-4.2,1.4-6.2,2.2
+	c-2.1,0.8-4.1,1.6-6.2,2.5c-4.1,1.8-8.2,3.8-12.2,6c-3.9,2.2-7.7,4.6-11.4,7.1c-0.5-0.2-1-0.4-1-0.4c-37.8-14.4-71.3,2.9-71.3,2.9
+	c-3.1,40.2,15.1,65.5,18.7,70.1c-0.9,2.5-1.7,5-2.5,7.5c-2.8,9.1-4.9,18.4-6.2,28.1c-0.2,1.4-0.4,2.8-0.5,4.2
+	C18.8,192.7,8.5,228,8.5,228c29.1,33.5,63.1,35.6,63.1,35.6c0,0,0.1-0.1,0.1-0.1c4.3,7.7,9.3,15,14.9,21.9c2.4,2.9,4.8,5.6,7.4,8.3
+	c-10.6,30.4,1.5,55.6,1.5,55.6c32.4,1.2,53.7-14.2,58.2-17.7c3.2,1.1,6.5,2.1,9.8,2.9c10,2.6,20.2,4.1,30.4,4.5
+	c2.5,0.1,5.1,0.2,7.6,0.1l1.2,0l0.8,0l1.6,0l1.6-0.1l0,0.1c15.3,21.8,42.1,24.9,42.1,24.9c19.1-20.1,20.2-40.1,20.2-44.4l0,0
+	c0,0,0-0.1,0-0.3c0-0.4,0-0.6,0-0.6l0,0c0-0.3,0-0.6,0-0.9c4-2.8,7.8-5.8,11.4-9.1c7.6-6.9,14.3-14.8,19.9-23.3
+	c0.5-0.8,1-1.6,1.5-2.4c21.6,1.2,36.9-13.4,36.9-13.4c-3.6-22.5-16.4-33.5-19.1-35.6l0,0c0,0-0.1-0.1-0.3-0.2
+	c-0.2-0.1-0.2-0.2-0.2-0.2c0,0,0,0,0,0c-0.1-0.1-0.3-0.2-0.5-0.3c0.1-1.4,0.2-2.7,0.3-4.1c0.2-2.4,0.2-4.9,0.2-7.3l0-1.8l0-0.9
+	l0-0.5c0-0.6,0-0.4,0-0.6l-0.1-1.5l-0.1-2c0-0.7-0.1-1.3-0.2-1.9c-0.1-0.6-0.1-1.3-0.2-1.9l-0.2-1.9l-0.3-1.9
+	c-0.4-2.5-0.8-4.9-1.4-7.4c-2.3-9.7-6.1-18.9-11-27.2c-5-8.3-11.2-15.6-18.3-21.8c-7-6.2-14.9-11.2-23.1-14.9
+	c-8.3-3.7-16.9-6.1-25.5-7.2c-4.3-0.6-8.6-0.8-12.9-0.7l-1.6,0l-0.4,0c-0.1,0-0.6,0-0.5,0l-0.7,0l-1.6,0.1c-0.6,0-1.2,0.1-1.7,0.1
+	c-2.2,0.2-4.4,0.5-6.5,0.9c-8.6,1.6-16.7,4.7-23.8,9c-7.1,4.3-13.3,9.6-18.3,15.6c-5,6-8.9,12.7-11.6,19.6c-2.7,6.9-4.2,14.1-4.6,21
+	c-0.1,1.7-0.1,3.5-0.1,5.2c0,0.4,0,0.9,0,1.3l0.1,1.4c0.1,0.8,0.1,1.7,0.2,2.5c0.3,3.5,1,6.9,1.9,10.1c1.9,6.5,4.9,12.4,8.6,17.4
+	c3.7,5,8.2,9.1,12.9,12.4c4.7,3.2,9.8,5.5,14.8,7c5,1.5,10,2.1,14.7,2.1c0.6,0,1.2,0,1.7,0c0.3,0,0.6,0,0.9,0c0.3,0,0.6,0,0.9-0.1
+	c0.5,0,1-0.1,1.5-0.1c0.1,0,0.3,0,0.4-0.1l0.5-0.1c0.3,0,0.6-0.1,0.9-0.1c0.6-0.1,1.1-0.2,1.7-0.3c0.6-0.1,1.1-0.2,1.6-0.4
+	c1.1-0.2,2.1-0.6,3.1-0.9c2-0.7,4-1.5,5.7-2.4c1.8-0.9,3.4-2,5-3c0.4-0.3,0.9-0.6,1.3-1c1.6-1.3,1.9-3.7,0.6-5.3
+	c-1.1-1.4-3.1-1.8-4.7-0.9c-0.4,0.2-0.8,0.4-1.2,0.6c-1.4,0.7-2.8,1.3-4.3,1.8c-1.5,0.5-3.1,0.9-4.7,1.2c-0.8,0.1-1.6,0.2-2.5,0.3
+	c-0.4,0-0.8,0.1-1.3,0.1c-0.4,0-0.9,0-1.2,0c-0.4,0-0.8,0-1.2,0c-0.5,0-1,0-1.5-0.1c0,0-0.3,0-0.1,0l-0.2,0l-0.3,0
+	c-0.2,0-0.5,0-0.7-0.1c-0.5-0.1-0.9-0.1-1.4-0.2c-3.7-0.5-7.4-1.6-10.9-3.2c-3.6-1.6-7-3.8-10.1-6.6c-3.1-2.8-5.8-6.1-7.9-9.9
+	c-2.1-3.8-3.6-8-4.3-12.4c-0.3-2.2-0.5-4.5-0.4-6.7c0-0.6,0.1-1.2,0.1-1.8c0,0.2,0-0.1,0-0.1l0-0.2l0-0.5c0-0.3,0.1-0.6,0.1-0.9
+	c0.1-1.2,0.3-2.4,0.5-3.6c1.7-9.6,6.5-19,13.9-26.1c1.9-1.8,3.9-3.4,6-4.9c2.1-1.5,4.4-2.8,6.8-3.9c2.4-1.1,4.8-2,7.4-2.7
+	c2.5-0.7,5.1-1.1,7.8-1.4c1.3-0.1,2.6-0.2,4-0.2c0.4,0,0.6,0,0.9,0l1.1,0l0.7,0c0.3,0,0,0,0.1,0l0.3,0l1.1,0.1
+	c2.9,0.2,5.7,0.6,8.5,1.3c5.6,1.2,11.1,3.3,16.2,6.1c10.2,5.7,18.9,14.5,24.2,25.1c2.7,5.3,4.6,11,5.5,16.9c0.2,1.5,0.4,3,0.5,4.5
+	l0.1,1.1l0.1,1.1c0,0.4,0,0.8,0,1.1c0,0.4,0,0.8,0,1.1l0,1l0,1.1c0,0.7-0.1,1.9-0.1,2.6c-0.1,1.6-0.3,3.3-0.5,4.9
+	c-0.2,1.6-0.5,3.2-0.8,4.8c-0.3,1.6-0.7,3.2-1.1,4.7c-0.8,3.1-1.8,6.2-3,9.3c-2.4,6-5.6,11.8-9.4,17.1
+	c-7.7,10.6-18.2,19.2-30.2,24.7c-6,2.7-12.3,4.7-18.8,5.7c-3.2,0.6-6.5,0.9-9.8,1l-0.6,0l-0.5,0l-1.1,0l-1.6,0l-0.8,0
+	c0.4,0-0.1,0-0.1,0l-0.3,0c-1.8,0-3.5-0.1-5.3-0.3c-7-0.5-13.9-1.8-20.7-3.7c-6.7-1.9-13.2-4.6-19.4-7.8
+	c-12.3-6.6-23.4-15.6-32-26.5c-4.3-5.4-8.1-11.3-11.2-17.4c-3.1-6.1-5.6-12.6-7.4-19.1c-1.8-6.6-2.9-13.3-3.4-20.1l-0.1-1.3l0-0.3
+	l0-0.3l0-0.6l0-1.1l0-0.3l0-0.4l0-0.8l0-1.6l0-0.3c0,0,0,0.1,0-0.1l0-0.6c0-0.8,0-1.7,0-2.5c0.1-3.3,0.4-6.8,0.8-10.2
+	c0.4-3.4,1-6.9,1.7-10.3c0.7-3.4,1.5-6.8,2.5-10.2c1.9-6.7,4.3-13.2,7.1-19.3c5.7-12.2,13.1-23.1,22-31.8c2.2-2.2,4.5-4.2,6.9-6.2
+	c2.4-1.9,4.9-3.7,7.5-5.4c2.5-1.7,5.2-3.2,7.9-4.6c1.3-0.7,2.7-1.4,4.1-2c0.7-0.3,1.4-0.6,2.1-0.9c0.7-0.3,1.4-0.6,2.1-0.9
+	c2.8-1.2,5.7-2.2,8.7-3.1c0.7-0.2,1.5-0.4,2.2-0.7c0.7-0.2,1.5-0.4,2.2-0.6c1.5-0.4,3-0.8,4.5-1.1c0.7-0.2,1.5-0.3,2.3-0.5
+	c0.8-0.2,1.5-0.3,2.3-0.5c0.8-0.1,1.5-0.3,2.3-0.4l1.1-0.2l1.2-0.2c0.8-0.1,1.5-0.2,2.3-0.3c0.9-0.1,1.7-0.2,2.6-0.3
+	c0.7-0.1,1.9-0.2,2.6-0.3c0.5-0.1,1.1-0.1,1.6-0.2l1.1-0.1l0.5-0.1l0.6,0c0.9-0.1,1.7-0.1,2.6-0.2l1.3-0.1c0,0,0.5,0,0.1,0l0.3,0
+	l0.6,0c0.7,0,1.5-0.1,2.2-0.1c2.9-0.1,5.9-0.1,8.8,0c5.8,0.2,11.5,0.9,17,1.9c11.1,2.1,21.5,5.6,31,10.3
+	c9.5,4.6,17.9,10.3,25.3,16.5c0.5,0.4,0.9,0.8,1.4,1.2c0.4,0.4,0.9,0.8,1.3,1.2c0.9,0.8,1.7,1.6,2.6,2.4c0.9,0.8,1.7,1.6,2.5,2.4
+	c0.8,0.8,1.6,1.6,2.4,2.5c3.1,3.3,6,6.6,8.6,10c5.2,6.7,9.4,13.5,12.7,19.9c0.2,0.4,0.4,0.8,0.6,1.2c0.2,0.4,0.4,0.8,0.6,1.2
+	c0.4,0.8,0.8,1.6,1.1,2.4c0.4,0.8,0.7,1.5,1.1,2.3c0.3,0.8,0.7,1.5,1,2.3c1.2,3,2.4,5.9,3.3,8.6c1.5,4.4,2.6,8.3,3.5,11.7
+	c0.3,1.4,1.6,2.3,3,2.1c1.5-0.1,2.6-1.3,2.6-2.8C342.6,170.4,342.5,166.1,342,161.2z"/>
+</svg>

+ 2 - 0
public/sass/_grafana.scss

@@ -76,6 +76,8 @@
 @import "components/jsontree";
 @import "components/jsontree";
 @import "components/edit_sidemenu.scss";
 @import "components/edit_sidemenu.scss";
 @import "components/row.scss";
 @import "components/row.scss";
+@import "components/json_explorer.scss";
+@import "components/collapse_box.scss";
 
 
 // PAGES
 // PAGES
 @import "pages/login";
 @import "pages/login";

+ 18 - 0
public/sass/_variables.dark.scss

@@ -280,6 +280,24 @@ $card-shadow: -1px -1px 0 0 hsla(0, 0%, 100%, .1), 1px 1px 0 0 rgba(0, 0, 0, .3)
 $footer-link-color:   $gray-1;
 $footer-link-color:   $gray-1;
 $footer-link-hover:   $gray-4;
 $footer-link-hover:   $gray-4;
 
 
+// collapse box
+$collapse-box-body-border: $dark-5;
+$collapse-box-body-error-border: $red;
+
+// json-explorer
+$json-explorer-default-color: $text-color;
+$json-explorer-string-color: #23d662;
+$json-explorer-number-color: $variable;
+$json-explorer-boolean-color: $variable;
+$json-explorer-null-color: #EEC97D;
+$json-explorer-undefined-color: rgb(239, 143, 190);
+$json-explorer-function-color: #FD48CB;
+$json-explorer-rotate-time: 100ms;
+$json-explorer-toggler-opacity: 0.6;
+$json-explorer-toggler-color: #45376F;
+$json-explorer-bracket-color: #9494FF;
+$json-explorer-key-color: #23A0DB;
+$json-explorer-url-color: #027BFF;
 
 
 // Changelog and diff
 // Changelog and diff
 // -------------------------
 // -------------------------

+ 18 - 0
public/sass/_variables.light.scss

@@ -304,6 +304,24 @@ $card-shadow: -1px -1px 0 0 hsla(0, 0%, 100%, .1), 1px 1px 0 0 rgba(0, 0, 0, .1)
 $footer-link-color:   $gray-3;
 $footer-link-color:   $gray-3;
 $footer-link-hover:   $dark-5;
 $footer-link-hover:   $dark-5;
 
 
+// collapse box
+$collapse-box-body-border: $gray-4;
+$collapse-box-body-error-border: $red;
+
+// json explorer
+$json-explorer-default-color: black;
+$json-explorer-string-color: green;
+$json-explorer-number-color: blue;
+$json-explorer-boolean-color: red;
+$json-explorer-null-color: #855A00;
+$json-explorer-undefined-color: rgb(202, 11, 105);
+$json-explorer-function-color: #FF20ED;
+$json-explorer-rotate-time: 100ms;
+$json-explorer-toggler-opacity: 0.6;
+$json-explorer-toggler-color: #45376F;
+$json-explorer-bracket-color: blue;
+$json-explorer-key-color: #00008B;
+$json-explorer-url-color: blue;
 
 
 // Changelog and diff
 // Changelog and diff
 // -------------------------
 // -------------------------

+ 46 - 0
public/sass/components/_collapse_box.scss

@@ -0,0 +1,46 @@
+.collapse-box {
+  margin-bottom: $spacer;
+
+  &--error {
+    .collapse-box__header {
+      border-color: $collapse-box-body-error-border;
+    }
+    .collapse-box__body {
+      border-color: $collapse-box-body-error-border;
+    }
+  }
+
+}
+
+.collapse-box__header {
+  display: flex;
+  flex-direction: row;
+  padding: $input-padding-y $input-padding-x;
+  margin-right: $gf-form-margin;
+  background-color: $input-label-bg;
+  font-size: $font-size-sm;
+  margin-right: $gf-form-margin;
+  border: $input-btn-border-width solid $collapse-box-body-border;
+  @include border-radius($label-border-radius-sm);
+}
+
+.collapse-box__header-title {
+  flex-grow: 1;
+}
+
+.collapse-box__body {
+  padding: $input-padding-y*2 $input-padding-x;
+  display: block;
+  margin-right: $gf-form-margin;
+  border: $input-btn-border-width solid $collapse-box-body-border;
+  border-top: none;
+  @include border-radius($label-border-radius-sm);
+}
+
+.collapse-box__header-actions {
+  display: flex;
+  flex-direction: row;
+  a {
+    margin-left: $spacer;
+  }
+}

+ 1 - 0
public/sass/components/_gf-form.scss

@@ -103,6 +103,7 @@ $gf-form-margin: 0.25rem;
   padding: $input-padding-y $input-padding-x;
   padding: $input-padding-y $input-padding-x;
   margin-right: $gf-form-margin;
   margin-right: $gf-form-margin;
   font-size: $font-size-base;
   font-size: $font-size-base;
+  margin-right: $gf-form-margin;
   line-height: $input-line-height;
   line-height: $input-line-height;
   color: $input-color;
   color: $input-color;
   background-color: $input-bg;
   background-color: $input-bg;

+ 98 - 0
public/sass/components/_json_explorer.scss

@@ -0,0 +1,98 @@
+
+.json-formatter-row {
+  font-family: monospace;
+
+  &, a, a:hover {
+    color: $json-explorer-default-color;
+    text-decoration: none;
+  }
+
+  .json-formatter-row {
+    margin-left: 1rem;
+  }
+
+  .json-formatter-children {
+    &.json-formatter-empty {
+      opacity: 0.5;
+      margin-left: 1rem;
+
+      &::after { display: none; }
+      &.json-formatter-object::after { content: "No properties"; }
+      &.json-formatter-array::after { content: "[]"; }
+    }
+  }
+
+  .json-formatter-string {
+    color: $json-explorer-string-color;
+    white-space: normal;
+    word-wrap: break-word;
+  }
+  .json-formatter-number { color: $json-explorer-number-color; }
+  .json-formatter-boolean { color: $json-explorer-boolean-color; }
+  .json-formatter-null { color: $json-explorer-null-color; }
+  .json-formatter-undefined { color: $json-explorer-undefined-color; }
+  .json-formatter-function { color: $json-explorer-function-color; }
+  .json-formatter-date { background-color: fade($json-explorer-default-color, 5%); }
+  .json-formatter-url {
+    text-decoration: underline;
+    color: $json-explorer-url-color;
+    cursor: pointer;
+  }
+
+  .json-formatter-bracket { color: $json-explorer-bracket-color; }
+  .json-formatter-key {
+    color: $json-explorer-key-color;
+    cursor: pointer;
+    padding-right: 0.2rem;
+    margin-right: 4px;
+  }
+
+  .json-formatter-constructor-name {
+    cursor: pointer;
+  }
+
+  .json-formatter-array-comma { margin-right: 4px; }
+
+  .json-formatter-toggler {
+    line-height: 1.2rem;
+    font-size: 0.7rem;
+    vertical-align: middle;
+    opacity: $json-explorer-toggler-opacity;
+    cursor: pointer;
+    padding-right: 0.2rem;
+
+    &::after {
+      display: inline-block;
+      transition: transform $json-explorer-rotate-time ease-in;
+      content: "►";
+    }
+  }
+
+  // Inline preview on hover (optional)
+  > a > .json-formatter-preview-text {
+    opacity: 0;
+    transition: opacity .15s ease-in;
+    font-style: italic;
+  }
+
+  &:hover > a > .json-formatter-preview-text {
+    opacity: 0.6;
+  }
+
+  // Open state
+  &.json-formatter-open {
+    > .json-formatter-toggler-link .json-formatter-toggler::after{
+      transform: rotate(90deg);
+    }
+    > .json-formatter-children::after {
+      display: inline-block;
+    }
+    > a > .json-formatter-preview-text {
+      display: none;
+    }
+    &.json-formatter-empty::after {
+      display: block;
+    }
+  }
+}
+

+ 1 - 0
public/sass/grafana.dark.scss

@@ -1,3 +1,4 @@
 @import "variables";
 @import "variables";
 @import "variables.dark";
 @import "variables.dark";
 @import "grafana";
 @import "grafana";
+

+ 1 - 0
public/sass/pages/_login.scss

@@ -119,6 +119,7 @@
 
 
     img {
     img {
       width: 19px;
       width: 19px;
+      vertical-align: sub;
     }
     }
   }
   }
 }
 }

+ 1 - 1
public/test/test-main.js

@@ -40,7 +40,7 @@
       "jquery.flot.fillbelow": "vendor/flot/jquery.flot.fillbelow",
       "jquery.flot.fillbelow": "vendor/flot/jquery.flot.fillbelow",
       "jquery.flot.gauge": "vendor/flot/jquery.flot.gauge",
       "jquery.flot.gauge": "vendor/flot/jquery.flot.gauge",
       "d3": "vendor/d3/d3.js",
       "d3": "vendor/d3/d3.js",
-      "jquery.flot.dashes": "vendor/flot/jquery.flot.dashes"
+      "jquery.flot.dashes": "vendor/flot/jquery.flot.dashes",
     },
     },
 
 
     packages: {
     packages: {

+ 8 - 8
yarn.lock

@@ -1663,9 +1663,9 @@ glob@7.0.5:
     once "^1.3.0"
     once "^1.3.0"
     path-is-absolute "^1.0.0"
     path-is-absolute "^1.0.0"
 
 
-glob@^7.0.0, glob@^7.0.3, glob@^7.0.5, glob@~7.0.0:
-  version "7.0.6"
-  resolved "https://registry.yarnpkg.com/glob/-/glob-7.0.6.tgz#211bafaf49e525b8cd93260d14ab136152b3f57a"
+glob@^7.0.0, glob@^7.1.1, glob@~7.1.1:
+  version "7.1.1"
+  resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.1.tgz#805211df04faaf1c63a3600306cdf5ade50b2ec8"
   dependencies:
   dependencies:
     fs.realpath "^1.0.0"
     fs.realpath "^1.0.0"
     inflight "^1.0.4"
     inflight "^1.0.4"
@@ -1674,9 +1674,9 @@ glob@^7.0.0, glob@^7.0.3, glob@^7.0.5, glob@~7.0.0:
     once "^1.3.0"
     once "^1.3.0"
     path-is-absolute "^1.0.0"
     path-is-absolute "^1.0.0"
 
 
-glob@^7.1.1, glob@~7.1.1:
-  version "7.1.1"
-  resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.1.tgz#805211df04faaf1c63a3600306cdf5ade50b2ec8"
+glob@^7.0.3, glob@^7.0.5, glob@~7.0.0:
+  version "7.0.6"
+  resolved "https://registry.yarnpkg.com/glob/-/glob-7.0.6.tgz#211bafaf49e525b8cd93260d14ab136152b3f57a"
   dependencies:
   dependencies:
     fs.realpath "^1.0.0"
     fs.realpath "^1.0.0"
     inflight "^1.0.4"
     inflight "^1.0.4"
@@ -3807,11 +3807,11 @@ resolve-pkg@^0.1.0:
   dependencies:
   dependencies:
     resolve-from "^2.0.0"
     resolve-from "^2.0.0"
 
 
-resolve@1.1.x, resolve@^1.1.6, resolve@~1.1.0:
+resolve@1.1.x, resolve@~1.1.0:
   version "1.1.7"
   version "1.1.7"
   resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.1.7.tgz#203114d82ad2c5ed9e8e0411b3932875e889e97b"
   resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.1.7.tgz#203114d82ad2c5ed9e8e0411b3932875e889e97b"
 
 
-resolve@^1.3.2:
+resolve@^1.1.6, resolve@^1.3.2:
   version "1.3.3"
   version "1.3.3"
   resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.3.3.tgz#655907c3469a8680dc2de3a275a8fdd69691f0e5"
   resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.3.3.tgz#655907c3469a8680dc2de3a275a8fdd69691f0e5"
   dependencies:
   dependencies: