From 5513d3c9d10ca9764e3295433f978bc87d92ba95 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Fri, 19 May 2017 13:16:05 +0200 Subject: [PATCH] feat: moved json-formatter-js into core grafana to be able to make changes --- package.json | 1 - .../core/components/json_explorer/helpers.ts | 113 +++++ .../components/json_explorer/json_explorer.ts | 454 ++++++++++++++++++ .../app/core/components/jsonview/helpers.ts | 110 ----- .../app/core/components/jsonview/jsonview.ts | 453 ----------------- public/app/core/components/response_viewer.ts | 4 +- public/app/system.conf.js | 1 - public/sass/_grafana.scss | 1 + public/sass/components/_json_explorer.scss | 128 +++++ public/test/test-main.js | 1 - tasks/options/copy.js | 1 - yarn.lock | 4 - 12 files changed, 698 insertions(+), 573 deletions(-) create mode 100644 public/app/core/components/json_explorer/helpers.ts create mode 100644 public/app/core/components/json_explorer/json_explorer.ts delete mode 100644 public/app/core/components/jsonview/helpers.ts delete mode 100644 public/app/core/components/jsonview/jsonview.ts create mode 100644 public/sass/components/_json_explorer.scss diff --git a/package.json b/package.json index 19e91a31ffe..47102820d58 100644 --- a/package.json +++ b/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", diff --git a/public/app/core/components/json_explorer/helpers.ts b/public/app/core/components/json_explorer/helpers.ts new file mode 100644 index 00000000000..2f2b80e73ea --- /dev/null +++ b/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; +} diff --git a/public/app/core/components/json_explorer/json_explorer.ts b/public/app/core/components/json_explorer/json_explorer.ts new file mode 100644 index 00000000000..3f460968b77 --- /dev/null +++ b/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 = ''; + } + } + } +} diff --git a/public/app/core/components/jsonview/helpers.ts b/public/app/core/components/jsonview/helpers.ts deleted file mode 100644 index 3ab36a19a7d..00000000000 --- a/public/app/core/components/jsonview/helpers.ts +++ /dev/null @@ -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; -// } diff --git a/public/app/core/components/jsonview/jsonview.ts b/public/app/core/components/jsonview/jsonview.ts deleted file mode 100644 index 71994ceef59..00000000000 --- a/public/app/core/components/jsonview/jsonview.ts +++ /dev/null @@ -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 = ''; -// } -// } -// } -// } diff --git a/public/app/core/components/response_viewer.ts b/public/app/core/components/response_viewer.ts index 5861e010fb3..fe332f070ec 100644 --- a/public/app/core/components/response_viewer.ts +++ b/public/app/core/components/response_viewer.ts @@ -1,7 +1,7 @@ /// 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', }); diff --git a/public/app/system.conf.js b/public/app/system.conf.js index 9354a708785..816cba4fb42 100644 --- a/public/app/system.conf.js +++ b/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: { diff --git a/public/sass/_grafana.scss b/public/sass/_grafana.scss index 6128a9c47a8..c8f82ad845a 100644 --- a/public/sass/_grafana.scss +++ b/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"; diff --git a/public/sass/components/_json_explorer.scss b/public/sass/components/_json_explorer.scss new file mode 100644 index 00000000000..3c4c19eceb7 --- /dev/null +++ b/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); +} diff --git a/public/test/test-main.js b/public/test/test-main.js index 6cdf19c5372..777e9d76afb 100644 --- a/public/test/test-main.js +++ b/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: { diff --git a/tasks/options/copy.js b/tasks/options/copy.js index 6a902c7188b..59921576da3 100644 --- a/tasks/options/copy.js +++ b/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' } diff --git a/yarn.lock b/yarn.lock index cb45e85d1fb..a9100c88e10 100644 --- a/yarn.lock +++ b/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"