瀏覽代碼

Merge branch 'master' into new-sidemenu

Torkel Ödegaard 8 年之前
父節點
當前提交
3e8937e53b
共有 43 個文件被更改,包括 1334 次插入262 次删除
  1. 1 1
      conf/sample.ini
  2. 0 1
      docker/blocks/graphite/fig
  3. 18 1
      docs/README.md
  4. 9 0
      docs/sources/http_api/snapshot.md
  5. 58 0
      public/app/core/components/collapse_box.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. 4 0
      public/app/core/core.ts
  9. 30 6
      public/app/core/directives/misc.js
  10. 3 3
      public/app/core/directives/plugin_component.ts
  11. 8 2
      public/app/core/services/backend_srv.ts
  12. 1 1
      public/app/features/dashboard/export/exporter.ts
  13. 2 0
      public/app/features/dashboard/history/history.ts
  14. 2 2
      public/app/features/dashboard/partials/shareModal.html
  15. 4 15
      public/app/features/dashboard/shareModalCtrl.js
  16. 4 0
      public/app/features/dashboard/shareSnapshotCtrl.js
  17. 1 1
      public/app/features/panel/all.js
  18. 0 113
      public/app/features/panel/metrics_ds_selector.ts
  19. 24 2
      public/app/features/panel/metrics_panel_ctrl.ts
  20. 94 0
      public/app/features/panel/metrics_tab.ts
  21. 1 1
      public/app/features/panel/panel_ctrl.ts
  22. 6 1
      public/app/features/panel/panel_editor_tab.ts
  23. 45 0
      public/app/features/panel/partials/metrics_tab.html
  24. 4 17
      public/app/features/panel/query_editor_row.ts
  25. 154 0
      public/app/features/panel/query_troubleshooter.ts
  26. 0 1
      public/app/headers/common.d.ts
  27. 1 1
      public/app/partials/login.html
  28. 0 20
      public/app/partials/metrics.html
  29. 1 1
      public/app/plugins/datasource/graphite/partials/query.options.html
  30. 6 4
      public/app/plugins/datasource/influxdb/datasource.ts
  31. 1 1
      public/app/system.conf.js
  32. 57 0
      public/img/grafana_com_auth_icon.svg
  33. 57 58
      public/img/grafana_icon.svg
  34. 2 0
      public/sass/_grafana.scss
  35. 18 0
      public/sass/_variables.dark.scss
  36. 18 0
      public/sass/_variables.light.scss
  37. 46 0
      public/sass/components/_collapse_box.scss
  38. 1 0
      public/sass/components/_gf-form.scss
  39. 98 0
      public/sass/components/_json_explorer.scss
  40. 1 0
      public/sass/grafana.dark.scss
  41. 1 0
      public/sass/pages/_login.scss
  42. 1 1
      public/test/test-main.js
  43. 8 8
      yarn.lock

+ 1 - 1
conf/sample.ini

@@ -298,7 +298,7 @@
 # Use space to separate multiple modes, e.g. "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
 
 # 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"
     - "2003:2003"
   volumes:
-    - /var/docker/gfdev/graphite:/opt/graphite/storage/whisper
     - /etc/localtime:/etc/localtime:ro
     - /etc/timezone:/etc/timezone:ro
 

+ 18 - 1
docs/README.md

@@ -18,11 +18,28 @@ make docs-build
 **Build the Documentation**:
 
 Now that the docker image has been prepared we can build the
-grafana docs and start a docs server. Switch your working directory back to the directory this file
+grafana docs and start a docs server. 
+
+If you have not cloned the Grafana repository already then:
+
+```
+cd ..
+git clone https://github.com/grafana/grafana
+```
+
+Switch your working directory to the directory this file
 (README.md) is in.
 
+```
+cd grafana/docs
+```
+
 An AWS config file is required to build the docs Docker image and to publish the site to AWS. If you are building locally only and do not have any AWS credentials for docs.grafana.org then create an empty file named `awsconfig` in the current directory.
 
+```
+touch awsconfig
+```
+
 Then run (possibly with ``sudo``):
 
 ```

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

@@ -52,6 +52,15 @@ parent = "http_api"
       "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**:
 
     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);

+ 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 = '';
+      }
+    }
+  }
+}

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

@@ -45,6 +45,8 @@ import {assignModelProperties} from './utils/model_utils';
 import {contextSrv} from './services/context_srv';
 import {KeybindingSrv} from './services/keybindingSrv';
 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';
 
 
@@ -70,6 +72,8 @@ export {
   contextSrv,
   KeybindingSrv,
   helpModal,
+  collapseBox,
+  JsonExplorer,
   NavModelSrv,
   NavModel,
 };

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

@@ -1,9 +1,10 @@
 define([
   'angular',
+  'require',
   '../core_module',
   'app/core/utils/kbn',
 ],
-function (angular, coreModule, kbn) {
+function (angular, require, coreModule, kbn) {
   'use strict';
 
   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) {
     return {
       restrict: 'A',
@@ -77,10 +101,10 @@ function (angular, coreModule, kbn) {
           text + tip + '</label>';
 
         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;
         elem.addClass('gf-form-checkbox');
@@ -105,7 +129,7 @@ function (angular, coreModule, kbn) {
         var li = '<li' + (item.submenu && item.submenu.length ? ' class="dropdown-submenu"' : '') + '>' +
           '<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.text || '') + '</a>';
+            '>' + (item.text || '') + '</a>';
 
         if (item.submenu && item.submenu.length) {
           li += buildTemplate(item.submenu).join('\n');

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

@@ -109,7 +109,7 @@ function pluginDirectiveLoader($compile, datasourceSrv, $rootScope, $q, $http, $
               baseUrl: ds.meta.baseUrl,
               name: 'query-ctrl-' + ds.meta.id,
               bindings: {target: "=", panelCtrl: "=", datasource: "="},
-              attrs: {"target": "target", "panel-ctrl": "ctrl", datasource: "datasource"},
+              attrs: {"target": "target", "panel-ctrl": "ctrl.panelCtrl", datasource: "datasource"},
               Component: dsModule.QueryCtrl
             };
           });
@@ -127,7 +127,7 @@ function pluginDirectiveLoader($compile, datasourceSrv, $rootScope, $q, $http, $
               baseUrl: ds.meta.baseUrl,
               name: 'query-options-ctrl-' + ds.meta.id,
               bindings: {panelCtrl: "="},
-              attrs: {"panel-ctrl": "ctrl"},
+              attrs: {"panel-ctrl": "ctrl.panelCtrl"},
               Component: dsModule.QueryOptionsCtrl
             };
           });
@@ -181,7 +181,7 @@ function pluginDirectiveLoader($compile, datasourceSrv, $rootScope, $q, $http, $
         return System.import(appModel.module).then(function(appModule) {
           return {
             baseUrl: appModel.baseUrl,
-            name: 'app-page-' + appModel.appId + '-' + scope.ctrl.page.slug,
+            name: 'app-page-' + appModel.id + '-' + scope.ctrl.page.slug,
             bindings: {appModel: "="},
             attrs: {"app-model": "ctrl.appModel"},
             Component: appModule[scope.ctrl.page.component],

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

@@ -4,6 +4,7 @@ import angular from 'angular';
 import _ from 'lodash';
 import config from 'app/core/config';
 import coreModule from 'app/core/core_module';
+import appEvents from 'app/core/app_events';
 
 export class BackendSrv {
   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) {
         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) {
         err.data = {
           error: err.statusText,
@@ -179,7 +183,9 @@ export class BackendSrv {
         err.data.message = err.data.error;
       }
 
+      appEvents.emit('ds-request-error', err);
       throw err;
+
     }).finally(() => {
       // clean up
       if (options.requestId) {

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

@@ -103,7 +103,7 @@ export class DashboardExporter {
         templateizeDatasourceUsage(variable);
         variable.options = [];
         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 */
   constructor(private $scope,
+              private $route,
               private $rootScope,
               private $location,
               private $window,
@@ -179,6 +180,7 @@ export class HistoryListCtrl {
     this.loading = true;
     return this.historySrv.restoreDashboard(this.dashboard, version).then(response => {
       this.$location.path('dashboard/db/' + response.slug);
+      this.$route.reload();
       this.$rootScope.appEvent('alert-success', ['Dashboard restored', 'Restored from version ' + version]);
     }).catch(() => {
       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>
 						</div>
 						<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>
@@ -143,7 +143,7 @@
 								{{snapshotUrl}}
 							</a>
 							<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>

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

@@ -2,10 +2,9 @@ define(['angular',
   'lodash',
   'jquery',
   'moment',
-  'require',
   'app/core/config',
 ],
-function (angular, _, $, moment, require, config) {
+function (angular, _, $, moment, config) {
   'use strict';
 
   var module = angular.module('grafana.controllers');
@@ -89,20 +88,10 @@ function (angular, _, $, moment, require, config) {
       $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) {
       // change title
       dash.title = $scope.snapshot.name;

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

@@ -5,5 +5,5 @@ define([
   './query_ctrl',
   './panel_editor_tab',
   './query_editor_row',
-  './metrics_ds_selector',
+  './query_troubleshooter',
 ], 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" />
 
+import angular from 'angular';
 import config from 'app/core/config';
 import $ from 'jquery';
 import _ from 'lodash';
@@ -10,6 +11,7 @@ import * as rangeUtil from 'app/core/utils/rangeutil';
 import * as dateMath from 'app/core/utils/datemath';
 
 import {Subject} from 'vendor/npm/rxjs/Subject';
+import {metricsTabDirective} from './metrics_tab';
 
 class MetricsPanelCtrl extends PanelCtrl {
   scope: any;
@@ -32,6 +34,7 @@ class MetricsPanelCtrl extends PanelCtrl {
   dataStream: any;
   dataSubscription: any;
   dataList: any;
+  nextRefId: string;
 
   constructor($scope, $injector) {
     super($scope, $injector);
@@ -61,7 +64,7 @@ class MetricsPanelCtrl extends PanelCtrl {
   }
 
   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');
   }
 
@@ -256,7 +259,7 @@ class MetricsPanelCtrl extends PanelCtrl {
       result = {data: []};
     }
 
-    return this.events.emit('data-received', result.data);
+    this.events.emit('data-received', result.data);
   }
 
   handleDataStream(stream) {
@@ -306,6 +309,25 @@ class MetricsPanelCtrl extends PanelCtrl {
     this.datasource = null;
     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};

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

@@ -0,0 +1,94 @@
+///<reference path="../../headers/common.d.ts" />
+
+import _ from 'lodash';
+//import {coreModule} from 'app/core/core';
+import {DashboardModel} from '../dashboard/model';
+
+export class MetricsTabCtrl {
+  dsSegment: any;
+  mixedDsSegment: any;
+  dsName: string;
+  panel: any;
+  panelCtrl: any;
+  datasources: any[];
+  current: any;
+  nextRefId: string;
+  dashboard: DashboardModel;
+
+  /** @ngInject */
+  constructor($scope, private uiSegmentSrv, datasourceSrv) {
+    this.panelCtrl = $scope.ctrl;
+    $scope.ctrl = this;
+
+    this.panel = this.panelCtrl.panel;
+    this.dashboard = this.panelCtrl.dashboard;
+    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, 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(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.addQuery(target);
+    }
+
+    // metric segments are really bad, requires hacks to update
+    const segment = this.uiSegmentSrv.newSegment({value: 'Add Query', selectMode: true, fake: true});
+    this.mixedDsSegment.value = segment.value;
+    this.mixedDsSegment.html = segment.html;
+    this.mixedDsSegment.text = segment.text;
+  }
+
+  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() {
-    this.events.emit('refresh', null);
+   this.events.emit('refresh', null);
   }
 
   publishAppEvent(evtName, evt) {

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

@@ -16,10 +16,15 @@ function panelEditorTab(dynamicDirectiveSrv) {
     directive: scope => {
       var pluginId = scope.ctrl.pluginId;
       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({
         name: `panel-editor-tab-${pluginId}${tabIndex}`,
-        fn: scope.editorTab.directiveFn,
+        fn: fn,
       });
     }
   });

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

@@ -0,0 +1,45 @@
+<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">
+        <metric-segment segment="ctrl.mixedDsSegment" get-options="ctrl.getOptions(false)" on-change="ctrl.mixedDatasourceChanged()"></metric-segment>
+      </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>
+      <metric-segment segment="ctrl.dsSegment" get-options="ctrl.getOptions(true)" on-change="ctrl.datasourceChanged()"></metric-segment>
+    </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;
 
     if (!this.target.refId) {
-      this.target.refId = this.getNextQueryLetter();
+      this.target.refId = this.panelCtrl.dashboard.getNextQueryLetter(this.panel);
     }
 
     this.toggleCollapse(true);
@@ -40,16 +40,6 @@ export class QueryRowCtrl {
     this.panelCtrl.refresh();
   }
 
-  getNextQueryLetter() {
-    var letters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
-
-    return _.find(letters, refId => {
-      return _.every(this.panel.targets, function(other) {
-        return other.refId !== refId;
-      });
-    });
-  }
-
   toggleCollapse(init) {
     if (!this.canCollapse) {
       return;
@@ -87,19 +77,16 @@ export class QueryRowCtrl {
       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() {
     var clone = angular.copy(this.target);
-    clone.refId = this.getNextQueryLetter();
-    this.panel.targets.push(clone);
+    this.panelCtrl.addQuery(clone);
   }
 
   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;
   export default d3;
 }
-

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

@@ -60,7 +60,7 @@
 						GitHub
 					</a>
 					<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>
 					</a>
 					<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>

+ 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 max-width-15">
 			<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();
     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) {
       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.gauge": "vendor/flot/jquery.flot.gauge",
     "d3": "vendor/d3/d3.js",
-    "jquery.flot.dashes": "vendor/flot/jquery.flot.dashes"
+    "jquery.flot.dashes": "vendor/flot/jquery.flot.dashes",
   },
 
   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

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

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

@@ -291,6 +291,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-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
 // -------------------------

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

@@ -313,6 +313,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-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
 // -------------------------

+ 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;
   margin-right: $gf-form-margin;
   font-size: $font-size-base;
+  margin-right: $gf-form-margin;
   line-height: $input-line-height;
   color: $input-color;
   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.dark";
 @import "grafana";
+

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

@@ -119,6 +119,7 @@
 
     img {
       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.gauge": "vendor/flot/jquery.flot.gauge",
       "d3": "vendor/d3/d3.js",
-      "jquery.flot.dashes": "vendor/flot/jquery.flot.dashes"
+      "jquery.flot.dashes": "vendor/flot/jquery.flot.dashes",
     },
 
     packages: {

+ 8 - 8
yarn.lock

@@ -1663,9 +1663,9 @@ glob@7.0.5:
     once "^1.3.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:
     fs.realpath "^1.0.0"
     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"
     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:
     fs.realpath "^1.0.0"
     inflight "^1.0.4"
@@ -3807,11 +3807,11 @@ resolve-pkg@^0.1.0:
   dependencies:
     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"
   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"
   resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.3.3.tgz#655907c3469a8680dc2de3a275a8fdd69691f0e5"
   dependencies: