Browse Source

ux: working on query troubleshooting

Torkel Ödegaard 8 years ago
parent
commit
f65878c21d

+ 1 - 0
package.json

@@ -68,6 +68,7 @@
     "grunt-jscs": "3.0.1",
     "grunt-sass-lint": "^0.2.2",
     "grunt-sync": "^0.6.2",
+    "json-formatter-js": "^2.2.0",
     "karma-sinon": "^1.0.5",
     "lodash": "^4.17.2",
     "mousetrap": "^1.6.0",

+ 110 - 0
public/app/core/components/jsonview/helpers.ts

@@ -0,0 +1,110 @@
+// #<{(|
+//  * 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;
+// }

+ 453 - 0
public/app/core/components/jsonview/jsonview.ts

@@ -0,0 +1,453 @@
+// import {
+//   isObject,
+//   getObjectName,
+//   getType,
+//   getValuePreview,
+//   getPreview,
+//   cssClass,
+//   createElement
+// } from './helpers';
+//
+// import './style.less';
+//
+// 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 JSONFormatterConfiguration {
+//   hoverPreviewEnabled?: boolean;
+//   hoverPreviewArrayCount?: number;
+//   hoverPreviewFieldCount?: number;
+//   animateOpen?: boolean;
+//   animateClose?: boolean;
+//   theme?: string;
+// };
+//
+// const _defaultConfig: JSONFormatterConfiguration = {
+//   hoverPreviewEnabled: false,
+//   hoverPreviewArrayCount: 100,
+//   hoverPreviewFieldCount: 5,
+//   animateOpen: true,
+//   animateClose: true,
+//   theme: null
+// };
+//
+//
+// #<{(|*
+//  * @class JSONFormatter
+//  *
+//  * JSONFormatter allows you to render JSON objects in HTML with a
+//  * **collapsible** navigation.
+// |)}>#
+// export default class JSONFormatter {
+//
+//   // 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;
+//
+//   #<{(|*
+//    * @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: JSONFormatterConfiguration = _defaultConfig, private key?: string) {
+//
+//     // Setting default values for config object
+//     if (this.config.hoverPreviewEnabled === undefined) {
+//       this.config.hoverPreviewEnabled = _defaultConfig.hoverPreviewEnabled;
+//     }
+//     if (this.config.hoverPreviewArrayCount === undefined) {
+//       this.config.hoverPreviewArrayCount = _defaultConfig.hoverPreviewArrayCount;
+//     }
+//     if (this.config.hoverPreviewFieldCount === undefined) {
+//       this.config.hoverPreviewFieldCount = _defaultConfig.hoverPreviewFieldCount;
+//     }
+//   }
+//
+//   #<{(|
+//    * 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'));
+//       }
+//     }
+//   }
+//
+//   #<{(|*
+//    * Generates inline preview
+//    *
+//    * @returns {string}
+//   |)}>#
+//   getInlinepreview() {
+//     if (this.isArray) {
+//
+//       // if array length is greater then 100 it shows "Array[101]"
+//       if (this.json.length > this.config.hoverPreviewArrayCount) {
+//         return `Array[${this.json.length}]`;
+//       } else {
+//         return `[${this.json.map(getPreview).join(', ')}]`;
+//       }
+//     } else {
+//
+//       const keys = this.keys;
+//
+//       // the first five keys (like Chrome Developer Tool)
+//       const narrowKeys = keys.slice(0, this.config.hoverPreviewFieldCount);
+//
+//       // json value schematic information
+//       const kvs = narrowKeys.map(key => `${key}:${getPreview(this.json[key])}`);
+//
+//       // if keys count greater then 5 then show ellipsis
+//       const ellipsis = keys.length >= this.config.hoverPreviewFieldCount ? '…' : '';
+//
+//       return `{${kvs.join(', ')}${ellipsis}}`;
+//     }
+//   }
+//
+//
+//   #<{(|*
+//    * Renders an HTML element and installs event listeners
+//    *
+//    * @returns {HTMLDivElement}
+//   |)}>#
+//   render(): 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');
+//
+//     // if this is an object we need a wrapper span (toggler)
+//     if (this.isObject) {
+//       togglerLink.appendChild(createElement('span', 'toggler'));
+//     }
+//
+//     // 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 = createElement('span');
+//         arrayWrapperSpan.appendChild(createElement('span', 'bracket', '['));
+//         arrayWrapperSpan.appendChild(createElement('span', 'number', (this.json.length)));
+//         arrayWrapperSpan.appendChild(createElement('span', 'bracket', ']'));
+//         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);
+//     }
+//
+//     // if hover preview is enabled, append the inline preview element
+//     if (this.isObject && this.config.hoverPreviewEnabled) {
+//       const preview = createElement('span', 'preview-text');
+//       preview.appendChild(document.createTextNode(this.getInlinepreview()));
+//       togglerLink.appendChild(preview);
+//     }
+//
+//     // 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
+//     this.element.appendChild(togglerLink);
+//     this.element.appendChild(children);
+//
+//     // 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 JSONFormatter(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 JSONFormatter(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 = '';
+//       }
+//     }
+//   }
+// }

+ 63 - 0
public/app/core/components/response_viewer.ts

@@ -0,0 +1,63 @@
+///<reference path="../../headers/common.d.ts" />
+
+import coreModule from 'app/core/core_module';
+import JsonFormatter from 'json-formatter-js';
+
+
+const template = `
+<div class="response-viewer">
+  <div class="response-viewer-json"></div>
+</div>
+`;
+
+export function responseViewer() {
+  return {
+    restrict: 'E',
+    template: template,
+    scope: {response: "="},
+    link: function(scope, elem) {
+      var jsonElem = elem.find('.response-viewer-json');
+
+      scope.$watch("response", newVal => {
+        if (!newVal) {
+          elem.empty();
+          return;
+        }
+
+        if (scope.response.headers) {
+          delete scope.response.headers;
+        }
+
+        if (scope.response.data) {
+          scope.response.response = scope.response.data;
+          delete scope.response.data;
+        }
+
+        if (scope.response.config) {
+          scope.response.request = scope.response.config;
+          delete scope.response.config;
+          delete scope.response.request.transformRequest;
+          delete scope.response.request.transformResponse;
+          delete scope.response.request.paramSerializer;
+          delete scope.response.request.jsonpCallbackParam;
+          delete scope.response.request.headers;
+          delete scope.response.request.requestId;
+          delete scope.response.request.inspect;
+          delete scope.response.request.retry;
+          delete scope.response.request.timeout;
+        }
+
+
+        const formatter =  new JsonFormatter(scope.response, 2, {
+          theme: 'dark',
+        });
+
+        const html = formatter.render();
+        jsonElem.html(html);
+      });
+
+    }
+  };
+}
+
+coreModule.directive('responseViewer', responseViewer);

+ 2 - 1
public/app/core/core.ts

@@ -45,7 +45,7 @@ import {assignModelProperties} from './utils/model_utils';
 import {contextSrv} from './services/context_srv';
 import {KeybindingSrv} from './services/keybindingSrv';
 import {helpModal} from './components/help/help';
-
+import {responseViewer} from './components/response_viewer';
 
 export {
   arrayJoin,
@@ -69,4 +69,5 @@ export {
   contextSrv,
   KeybindingSrv,
   helpModal,
+  responseViewer,
 };

+ 11 - 20
public/app/features/panel/metrics_ds_selector.ts

@@ -8,16 +8,8 @@ var module = angular.module('grafana.directives');
 
 var template = `
 
-<div class="gf-form-group" ng-if="ctrl.lastError">
-  <div class="gf-form">
-    <pre class="gf-form-pre alert alert-error">{{ctrl.lastError}}</pre>
-  </div>
-</div>
-
 <div class="gf-form-group" ng-if="ctrl.showResponse">
-  <div class="gf-form">
-    <pre class="gf-form-pre alert alert-info">{{ctrl.lastResponse}}</pre>
-  </div>
+  <response-viewer response="ctrl.responseData" />
 </div>
 
 <div class="gf-form-group">
@@ -49,9 +41,9 @@ var template = `
     </div>
 
     <div class="gf-form gf-form--offset-1">
-      <button class="btn btn-secondary gf-form-btn" ng-click="ctrl.toggleShowResponse()" ng-show="ctrl.lastResponse">
-        <i class="fa fa-info"></i>&nbsp;
-        Show Response
+      <button class="btn btn-inverse gf-form-btn" ng-click="ctrl.toggleShowResponse()" ng-show="ctrl.responseData">
+        <i class="fa fa-binoculars"></i>&nbsp;
+        Request & Response
       </button>
     </div>
 
@@ -68,7 +60,7 @@ export class MetricsDsSelectorCtrl {
   datasources: any[];
   current: any;
   lastResponse: any;
-  lastError: any;
+  responseData: any;
   showResponse: boolean;
 
   /** @ngInject */
@@ -95,9 +87,8 @@ export class MetricsDsSelectorCtrl {
   }
 
   onRequestResponse(data) {
-    console.log(data);
-    this.lastResponse = JSON.stringify(data, null, 2);
-    this.lastError = null;
+    this.responseData = data;
+    this.showResponse = true;
   }
 
   toggleShowResponse() {
@@ -105,8 +96,9 @@ export class MetricsDsSelectorCtrl {
   }
 
   onRequestError(err) {
-    console.log(err);
-    this.lastError = JSON.stringify(err, null, 2);
+    this.responseData = err;
+    this.responseData.isError = true;
+    this.showResponse = true;
   }
 
   getOptions(includeBuiltin) {
@@ -122,8 +114,7 @@ export class MetricsDsSelectorCtrl {
     if (ds) {
       this.current = ds;
       this.panelCtrl.setDatasource(ds);
-      this.lastError = null;
-      this.lastResponse = null;
+      this.responseData = null;
     }
   }
 

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

@@ -72,3 +72,8 @@ declare module 'd3' {
   var d3: any;
   export default d3;
 }
+
+declare module 'json-formatter-js' {
+  var JSONFormatter: any;
+  export default JSONFormatter;
+}

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

@@ -32,7 +32,8 @@ 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",
+    "json-formatter-js": "vendor/npm/json-formatter-js/dist/json-formatter"
   },
 
   packages: {

+ 1 - 0
public/sass/_grafana.scss

@@ -75,6 +75,7 @@
 @import "components/jsontree";
 @import "components/edit_sidemenu.scss";
 @import "components/row.scss";
+@import "components/response_viewer.scss";
 
 // PAGES
 @import "pages/login";

+ 6 - 0
public/sass/components/_response_viewer.scss

@@ -0,0 +1,6 @@
+.response-viewer {
+  background: $card-background;
+  box-shadow: $card-shadow;
+  padding: 1rem;
+  border-radius: 4px;
+}

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

@@ -40,7 +40,8 @@
       "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",
+      "json-formatter-js": "vendor/npm/json-formatter-js/dist/json-formatter"
     },
 
     packages: {

+ 1 - 0
tasks/options/copy.js

@@ -34,6 +34,7 @@ module.exports = function(config) {
         'remarkable/dist/*',
         'virtual-scroll/**/*',
         'mousetrap/**/*',
+        'json-formatter-js/dist/*.js',
       ],
       dest: '<%= srcDir %>/vendor/npm'
     }

+ 12 - 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"
@@ -2561,6 +2561,10 @@ jshint@~2.9.4:
     shelljs "0.3.x"
     strip-json-comments "1.0.x"
 
+json-formatter-js@^2.2.0:
+  version "2.2.0"
+  resolved "https://registry.yarnpkg.com/json-formatter-js/-/json-formatter-js-2.2.0.tgz#1ed987223ef2f1d945304597faae78b580a8212b"
+
 json-schema@0.2.3:
   version "0.2.3"
   resolved "https://registry.yarnpkg.com/json-schema/-/json-schema-0.2.3.tgz#b480c892e59a2f05954ce727bd3f2a4e882f9e13"
@@ -3807,11 +3811,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: