Browse Source

feat: moved json-formatter-js into core grafana to be able to make changes

Torkel Ödegaard 8 năm trước cách đây
mục cha
commit
5513d3c9d1

+ 0 - 1
package.json

@@ -68,7 +68,6 @@
     "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",

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

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

@@ -0,0 +1,454 @@
+// 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';
+
+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 {
+  hoverPreviewEnabled?: boolean;
+  hoverPreviewArrayCount?: number;
+  hoverPreviewFieldCount?: number;
+  animateOpen?: boolean;
+  animateClose?: boolean;
+  theme?: string;
+}
+
+const _defaultConfig: JsonExplorerConfig = {
+  hoverPreviewEnabled: false,
+  hoverPreviewArrayCount: 100,
+  hoverPreviewFieldCount: 5,
+  animateOpen: true,
+  animateClose: true,
+  theme: null
+};
+
+
+/**
+ * @class JsonExplorer
+ *
+ * JsonExplorer allows you to render JSON objects in HTML with a
+ * **collapsible** navigation.
+*/
+export default 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;
+
+  /**
+   * @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) {
+
+    // 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 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 = '';
+      }
+    }
+  }
+}

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

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

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

@@ -1,453 +0,0 @@
-// 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 = '';
-//       }
-//     }
-//   }
-// }

+ 2 - 2
public/app/core/components/response_viewer.ts

@@ -1,7 +1,7 @@
 ///<reference path="../../headers/common.d.ts" />
 
 import coreModule from 'app/core/core_module';
-import JsonFormatter from 'json-formatter-js';
+import JsonExplorer from './json_explorer/json_explorer';
 
 
 const template = `
@@ -48,7 +48,7 @@ export function responseViewer() {
         }
 
 
-        const formatter =  new JsonFormatter(scope.response, 2, {
+        const formatter =  new JsonExplorer(scope.response, 2, {
           theme: 'dark',
         });
 

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

@@ -33,7 +33,6 @@ System.config({
     "jquery.flot.gauge": "vendor/flot/jquery.flot.gauge",
     "d3": "vendor/d3/d3.js",
     "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

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

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

@@ -0,0 +1,128 @@
+@mixin json-explorer-theme(
+  $default-color: black,
+  $string-color: green,
+  $number-color: blue,
+  $boolean-color: red,
+  $null-color: #855A00,
+  $undefined-color: rgb(202, 11, 105),
+  $function-color: #FF20ED,
+  $rotate-time: 100ms,
+  $toggler-opacity: 0.6,
+  $toggler-color: #45376F,
+  $bracket-color: blue,
+  $key-color: #00008B,
+  $url-color: blue) {
+
+  font-family: monospace;
+  &, a, a:hover {
+    color: $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: $string-color;
+    white-space: pre;
+    word-wrap: break-word;
+  }
+  .json-formatter-number { color: $number-color; }
+  .json-formatter-boolean { color: $boolean-color; }
+  .json-formatter-null { color: $null-color; }
+  .json-formatter-undefined { color: $undefined-color; }
+  .json-formatter-function { color: $function-color; }
+  .json-formatter-date { background-color: fade($default-color, 5%); }
+  .json-formatter-url {
+    text-decoration: underline;
+    color: $url-color;
+    cursor: pointer;
+  }
+
+  .json-formatter-bracket { color: $bracket-color; }
+  .json-formatter-key {
+    color: $key-color;
+    cursor: pointer;
+    padding-right: 0.2rem;
+  }
+  .json-formatter-constructor-name {
+    cursor: pointer;
+  }
+
+  .json-formatter-toggler {
+    line-height: 1.2rem;
+    font-size: 0.7rem;
+    vertical-align: middle;
+    opacity: $toggler-opacity;
+    cursor: pointer;
+    padding-right: 0.2rem;
+
+    &::after {
+      display: inline-block;
+      transition: transform $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;
+    }
+  }
+}
+
+
+.json-formatter-row {
+  @include json-explorer-theme();
+}
+
+// Dark theme
+.json-formatter-dark.json-formatter-row {
+  @include json-explorer-theme(
+    $default-color: white,
+    $string-color: #31F031,
+    $number-color: #66C2FF,
+    $boolean-color: #EC4242,
+    $null-color: #EEC97D,
+    $undefined-color: rgb(239, 143, 190),
+    $function-color: #FD48CB,
+    $rotate-time: 100ms,
+    $toggler-opacity: 0.6,
+    $toggler-color: #45376F,
+    $bracket-color: #9494FF,
+    $key-color: #23A0DB,
+    $url-color: #027BFF);
+}

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

@@ -41,7 +41,6 @@
       "jquery.flot.gauge": "vendor/flot/jquery.flot.gauge",
       "d3": "vendor/d3/d3.js",
       "jquery.flot.dashes": "vendor/flot/jquery.flot.dashes",
-      "json-formatter-js": "vendor/npm/json-formatter-js/dist/json-formatter"
     },
 
     packages: {

+ 0 - 1
tasks/options/copy.js

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

+ 0 - 4
yarn.lock

@@ -2561,10 +2561,6 @@ 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"