mirror of
https://github.com/grafana/grafana.git
synced 2026-08-19 01:34:54 -05:00
Transformers: configure result transformations after query(alpha) (#18740)
This commit is contained in:
committed by
Dominik Prokop
parent
205c0a58ac
commit
7d32caeac2
@@ -1,50 +0,0 @@
|
||||
import React, { PureComponent, createRef } from 'react';
|
||||
import { JsonExplorer } from 'app/core/core'; // We have made some monkey-patching of json-formatter-js so we can't switch right now
|
||||
|
||||
interface Props {
|
||||
className?: string;
|
||||
json: {};
|
||||
config?: any;
|
||||
open?: number;
|
||||
onDidRender?: (formattedJson: any) => void;
|
||||
}
|
||||
|
||||
export class JSONFormatter extends PureComponent<Props> {
|
||||
private wrapperRef = createRef<HTMLDivElement>();
|
||||
|
||||
static defaultProps = {
|
||||
open: 3,
|
||||
config: {
|
||||
animateOpen: true,
|
||||
},
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
this.renderJson();
|
||||
}
|
||||
|
||||
componentDidUpdate() {
|
||||
this.renderJson();
|
||||
}
|
||||
|
||||
renderJson = () => {
|
||||
const { json, config, open, onDidRender } = this.props;
|
||||
const wrapperEl = this.wrapperRef.current;
|
||||
const formatter = new JsonExplorer(json, open, config);
|
||||
const hasChildren: boolean = wrapperEl.hasChildNodes();
|
||||
if (hasChildren) {
|
||||
wrapperEl.replaceChild(formatter.render(), wrapperEl.lastChild);
|
||||
} else {
|
||||
wrapperEl.appendChild(formatter.render());
|
||||
}
|
||||
|
||||
if (onDidRender) {
|
||||
onDidRender(formatter.json);
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
const { className } = this.props;
|
||||
return <div className={className} ref={this.wrapperRef} />;
|
||||
}
|
||||
}
|
||||
@@ -1,119 +0,0 @@
|
||||
// Based on work https://github.com/mohsen1/json-formatter-js
|
||||
// License MIT, Copyright (c) 2015 Mohsen Azimi
|
||||
|
||||
/*
|
||||
* Escapes `"` characters from string
|
||||
*/
|
||||
function escapeString(str: string): string {
|
||||
return str.replace('"', '"');
|
||||
}
|
||||
|
||||
/*
|
||||
* Determines if a value is an object
|
||||
*/
|
||||
export function isObject(value: any): boolean {
|
||||
const 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 {
|
||||
const 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
|
||||
*/
|
||||
let value = '';
|
||||
export function getPreview(obj: object): string {
|
||||
if (isObject(obj)) {
|
||||
value = getObjectName(obj);
|
||||
if (Array.isArray(obj)) {
|
||||
value += '[' + obj.length + ']';
|
||||
}
|
||||
} else {
|
||||
value = getValuePreview(obj, obj.toString());
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
/*
|
||||
* Generates a prefixed CSS class name
|
||||
*/
|
||||
export function cssClass(className: string): string {
|
||||
return `json-formatter-${className}`;
|
||||
}
|
||||
|
||||
/*
|
||||
* Creates a new DOM element with 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;
|
||||
}
|
||||
@@ -1,429 +0,0 @@
|
||||
// Based on work https://github.com/mohsen1/json-formatter-js
|
||||
// License MIT, Copyright (c) 2015 Mohsen Azimi
|
||||
|
||||
import { isObject, getObjectName, getType, getValuePreview, cssClass, createElement } from './helpers';
|
||||
|
||||
import _ from 'lodash';
|
||||
|
||||
const DATE_STRING_REGEX = /(^\d{1,4}[\.|\\/|-]\d{1,2}[\.|\\/|-]\d{1,4})(\s*(?:0?[1-9]:[0-5]|1(?=[012])\d:[0-5])\d\s*[ap]m)?$/;
|
||||
const PARTIAL_DATE_REGEX = /\d{2}:\d{2}:\d{2} GMT-\d{4}/;
|
||||
const JSON_DATE_REGEX = /\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z/;
|
||||
|
||||
// When toggleing, don't animated removal or addition of more than a few items
|
||||
const MAX_ANIMATED_TOGGLE_ITEMS = 10;
|
||||
|
||||
const requestAnimationFrame =
|
||||
window.requestAnimationFrame ||
|
||||
((cb: () => void) => {
|
||||
cb();
|
||||
return 0;
|
||||
});
|
||||
|
||||
export interface JsonExplorerConfig {
|
||||
animateOpen?: boolean;
|
||||
animateClose?: boolean;
|
||||
theme?: string;
|
||||
}
|
||||
|
||||
const _defaultConfig: JsonExplorerConfig = {
|
||||
animateOpen: true,
|
||||
animateClose: true,
|
||||
theme: null,
|
||||
};
|
||||
|
||||
/**
|
||||
* @class JsonExplorer
|
||||
*
|
||||
* JsonExplorer allows you to render JSON objects in HTML with a
|
||||
* **collapsible** navigation.
|
||||
*/
|
||||
export class JsonExplorer {
|
||||
// Hold the open state after the toggler is used
|
||||
private _isOpen: boolean = null;
|
||||
|
||||
// A reference to the element that we render to
|
||||
private element: Element;
|
||||
|
||||
private skipChildren = false;
|
||||
|
||||
/**
|
||||
* @param {object} json The JSON object you want to render. It has to be an
|
||||
* object or array. Do NOT pass raw JSON string.
|
||||
*
|
||||
* @param {number} [open=1] his number indicates up to how many levels the
|
||||
* rendered tree should expand. Set it to `0` to make the whole tree collapsed
|
||||
* or set it to `Infinity` to expand the tree deeply
|
||||
*
|
||||
* @param {object} [config=defaultConfig] -
|
||||
* defaultConfig = {
|
||||
* hoverPreviewEnabled: false,
|
||||
* hoverPreviewArrayCount: 100,
|
||||
* hoverPreviewFieldCount: 5
|
||||
* }
|
||||
*
|
||||
* Available configurations:
|
||||
* #####Hover Preview
|
||||
* * `hoverPreviewEnabled`: enable preview on hover
|
||||
* * `hoverPreviewArrayCount`: number of array items to show in preview Any
|
||||
* array larger than this number will be shown as `Array[XXX]` where `XXX`
|
||||
* is length of the array.
|
||||
* * `hoverPreviewFieldCount`: number of object properties to show for object
|
||||
* preview. Any object with more properties that thin number will be
|
||||
* truncated.
|
||||
*
|
||||
* @param {string} [key=undefined] The key that this object in it's parent
|
||||
* context
|
||||
*/
|
||||
constructor(
|
||||
public json: any,
|
||||
private open = 1,
|
||||
private config: JsonExplorerConfig = _defaultConfig,
|
||||
private key?: string
|
||||
) {}
|
||||
|
||||
/*
|
||||
* is formatter open?
|
||||
*/
|
||||
private get isOpen(): boolean {
|
||||
if (this._isOpen !== null) {
|
||||
return this._isOpen;
|
||||
} else {
|
||||
return this.open > 0;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* set open state (from toggler)
|
||||
*/
|
||||
private set isOpen(value: boolean) {
|
||||
this._isOpen = value;
|
||||
}
|
||||
|
||||
/*
|
||||
* is this a date string?
|
||||
*/
|
||||
private get isDate(): boolean {
|
||||
return (
|
||||
this.type === 'string' &&
|
||||
(DATE_STRING_REGEX.test(this.json) || JSON_DATE_REGEX.test(this.json) || PARTIAL_DATE_REGEX.test(this.json))
|
||||
);
|
||||
}
|
||||
|
||||
/*
|
||||
* is this a URL string?
|
||||
*/
|
||||
private get isUrl(): boolean {
|
||||
return this.type === 'string' && this.json.indexOf('http') === 0;
|
||||
}
|
||||
|
||||
/*
|
||||
* is this an array?
|
||||
*/
|
||||
private get isArray(): boolean {
|
||||
return Array.isArray(this.json);
|
||||
}
|
||||
|
||||
/*
|
||||
* is this an object?
|
||||
* Note: In this context arrays are object as well
|
||||
*/
|
||||
private get isObject(): boolean {
|
||||
return isObject(this.json);
|
||||
}
|
||||
|
||||
/*
|
||||
* is this an empty object with no properties?
|
||||
*/
|
||||
private get isEmptyObject(): boolean {
|
||||
return !this.keys.length && !this.isArray;
|
||||
}
|
||||
|
||||
/*
|
||||
* is this an empty object or array?
|
||||
*/
|
||||
private get isEmpty(): boolean {
|
||||
return this.isEmptyObject || (this.keys && !this.keys.length && this.isArray);
|
||||
}
|
||||
|
||||
/*
|
||||
* did we receive a key argument?
|
||||
* This means that the formatter was called as a sub formatter of a parent formatter
|
||||
*/
|
||||
private get hasKey(): boolean {
|
||||
return typeof this.key !== 'undefined';
|
||||
}
|
||||
|
||||
/*
|
||||
* if this is an object, get constructor function name
|
||||
*/
|
||||
private get constructorName(): string {
|
||||
return getObjectName(this.json);
|
||||
}
|
||||
|
||||
/*
|
||||
* get type of this value
|
||||
* Possible values: all JavaScript primitive types plus "array" and "null"
|
||||
*/
|
||||
private get type(): string {
|
||||
return getType(this.json);
|
||||
}
|
||||
|
||||
/*
|
||||
* get object keys
|
||||
* If there is an empty key we pad it wit quotes to make it visible
|
||||
*/
|
||||
private get keys(): string[] {
|
||||
if (this.isObject) {
|
||||
return Object.keys(this.json).map(key => (key ? key : '""'));
|
||||
} else {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggles `isOpen` state
|
||||
*
|
||||
*/
|
||||
toggleOpen() {
|
||||
this.isOpen = !this.isOpen;
|
||||
|
||||
if (this.element) {
|
||||
if (this.isOpen) {
|
||||
this.appendChildren(this.config.animateOpen);
|
||||
} else {
|
||||
this.removeChildren(this.config.animateClose);
|
||||
}
|
||||
this.element.classList.toggle(cssClass('open'));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Open all children up to a certain depth.
|
||||
* Allows actions such as expand all/collapse all
|
||||
*
|
||||
*/
|
||||
openAtDepth(depth = 1) {
|
||||
if (depth < 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.open = depth;
|
||||
this.isOpen = depth !== 0;
|
||||
|
||||
if (this.element) {
|
||||
this.removeChildren(false);
|
||||
|
||||
if (depth === 0) {
|
||||
this.element.classList.remove(cssClass('open'));
|
||||
} else {
|
||||
this.appendChildren(this.config.animateOpen);
|
||||
this.element.classList.add(cssClass('open'));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
isNumberArray() {
|
||||
return this.json.length > 0 && this.json.length < 4 && (_.isNumber(this.json[0]) || _.isNumber(this.json[1]));
|
||||
}
|
||||
|
||||
renderArray() {
|
||||
const arrayWrapperSpan = createElement('span');
|
||||
arrayWrapperSpan.appendChild(createElement('span', 'bracket', '['));
|
||||
|
||||
// some pretty handling of number arrays
|
||||
if (this.isNumberArray()) {
|
||||
this.json.forEach((val: any, index: number) => {
|
||||
if (index > 0) {
|
||||
arrayWrapperSpan.appendChild(createElement('span', 'array-comma', ','));
|
||||
}
|
||||
arrayWrapperSpan.appendChild(createElement('span', 'number', val));
|
||||
});
|
||||
this.skipChildren = true;
|
||||
} else {
|
||||
arrayWrapperSpan.appendChild(createElement('span', 'number', this.json.length));
|
||||
}
|
||||
|
||||
arrayWrapperSpan.appendChild(createElement('span', 'bracket', ']'));
|
||||
return arrayWrapperSpan;
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders an HTML element and installs event listeners
|
||||
*
|
||||
* @returns {HTMLDivElement}
|
||||
*/
|
||||
render(skipRoot = false): HTMLDivElement {
|
||||
// construct the root element and assign it to this.element
|
||||
this.element = createElement('div', 'row');
|
||||
|
||||
// construct the toggler link
|
||||
const togglerLink = createElement('a', 'toggler-link');
|
||||
const togglerIcon = createElement('span', 'toggler');
|
||||
|
||||
// if this is an object we need a wrapper span (toggler)
|
||||
if (this.isObject) {
|
||||
togglerLink.appendChild(togglerIcon);
|
||||
}
|
||||
|
||||
// if this is child of a parent formatter we need to append the key
|
||||
if (this.hasKey) {
|
||||
togglerLink.appendChild(createElement('span', 'key', `${this.key}:`));
|
||||
}
|
||||
|
||||
// Value for objects and arrays
|
||||
if (this.isObject) {
|
||||
// construct the value holder element
|
||||
const value = createElement('span', 'value');
|
||||
|
||||
// we need a wrapper span for objects
|
||||
const objectWrapperSpan = createElement('span');
|
||||
|
||||
// get constructor name and append it to wrapper span
|
||||
const constructorName = createElement('span', 'constructor-name', this.constructorName);
|
||||
objectWrapperSpan.appendChild(constructorName);
|
||||
|
||||
// if it's an array append the array specific elements like brackets and length
|
||||
if (this.isArray) {
|
||||
const arrayWrapperSpan = this.renderArray();
|
||||
objectWrapperSpan.appendChild(arrayWrapperSpan);
|
||||
}
|
||||
|
||||
// append object wrapper span to toggler link
|
||||
value.appendChild(objectWrapperSpan);
|
||||
togglerLink.appendChild(value);
|
||||
// Primitive values
|
||||
} else {
|
||||
// make a value holder element
|
||||
const value = this.isUrl ? createElement('a') : createElement('span');
|
||||
|
||||
// add type and other type related CSS classes
|
||||
value.classList.add(cssClass(this.type));
|
||||
if (this.isDate) {
|
||||
value.classList.add(cssClass('date'));
|
||||
}
|
||||
if (this.isUrl) {
|
||||
value.classList.add(cssClass('url'));
|
||||
value.setAttribute('href', this.json);
|
||||
}
|
||||
|
||||
// Append value content to value element
|
||||
const valuePreview = getValuePreview(this.json, this.json);
|
||||
value.appendChild(document.createTextNode(valuePreview));
|
||||
|
||||
// append the value element to toggler link
|
||||
togglerLink.appendChild(value);
|
||||
}
|
||||
|
||||
// construct a children element
|
||||
const children = createElement('div', 'children');
|
||||
|
||||
// set CSS classes for children
|
||||
if (this.isObject) {
|
||||
children.classList.add(cssClass('object'));
|
||||
}
|
||||
if (this.isArray) {
|
||||
children.classList.add(cssClass('array'));
|
||||
}
|
||||
if (this.isEmpty) {
|
||||
children.classList.add(cssClass('empty'));
|
||||
}
|
||||
|
||||
// set CSS classes for root element
|
||||
if (this.config && this.config.theme) {
|
||||
this.element.classList.add(cssClass(this.config.theme));
|
||||
}
|
||||
if (this.isOpen) {
|
||||
this.element.classList.add(cssClass('open'));
|
||||
}
|
||||
|
||||
// append toggler and children elements to root element
|
||||
if (!skipRoot) {
|
||||
this.element.appendChild(togglerLink);
|
||||
}
|
||||
|
||||
if (!this.skipChildren) {
|
||||
this.element.appendChild(children);
|
||||
} else {
|
||||
// remove togglerIcon
|
||||
togglerLink.removeChild(togglerIcon);
|
||||
}
|
||||
|
||||
// if formatter is set to be open call appendChildren
|
||||
if (this.isObject && this.isOpen) {
|
||||
this.appendChildren();
|
||||
}
|
||||
|
||||
// add event listener for toggling
|
||||
if (this.isObject) {
|
||||
togglerLink.addEventListener('click', this.toggleOpen.bind(this));
|
||||
}
|
||||
|
||||
return this.element as HTMLDivElement;
|
||||
}
|
||||
|
||||
/**
|
||||
* Appends all the children to children element
|
||||
* Animated option is used when user triggers this via a click
|
||||
*/
|
||||
appendChildren(animated = false) {
|
||||
const children = this.element.querySelector(`div.${cssClass('children')}`);
|
||||
|
||||
if (!children || this.isEmpty) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (animated) {
|
||||
let index = 0;
|
||||
const addAChild = () => {
|
||||
const key = this.keys[index];
|
||||
const formatter = new JsonExplorer(this.json[key], this.open - 1, this.config, key);
|
||||
children.appendChild(formatter.render());
|
||||
|
||||
index += 1;
|
||||
|
||||
if (index < this.keys.length) {
|
||||
if (index > MAX_ANIMATED_TOGGLE_ITEMS) {
|
||||
addAChild();
|
||||
} else {
|
||||
requestAnimationFrame(addAChild);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
requestAnimationFrame(addAChild);
|
||||
} else {
|
||||
this.keys.forEach(key => {
|
||||
const formatter = new JsonExplorer(this.json[key], this.open - 1, this.config, key);
|
||||
children.appendChild(formatter.render());
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes all the children from children element
|
||||
* Animated option is used when user triggers this via a click
|
||||
*/
|
||||
removeChildren(animated = false) {
|
||||
const childrenElement = this.element.querySelector(`div.${cssClass('children')}`) as HTMLDivElement;
|
||||
|
||||
if (animated) {
|
||||
let childrenRemoved = 0;
|
||||
const removeAChild = () => {
|
||||
if (childrenElement && childrenElement.children.length) {
|
||||
childrenElement.removeChild(childrenElement.children[0]);
|
||||
childrenRemoved += 1;
|
||||
if (childrenRemoved > MAX_ANIMATED_TOGGLE_ITEMS) {
|
||||
removeAChild();
|
||||
} else {
|
||||
requestAnimationFrame(removeAChild);
|
||||
}
|
||||
}
|
||||
};
|
||||
requestAnimationFrame(removeAChild);
|
||||
} else {
|
||||
if (childrenElement) {
|
||||
childrenElement.innerHTML = '';
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import coreModule from 'app/core/core_module';
|
||||
import { JsonExplorer } from '../json_explorer/json_explorer';
|
||||
import { JsonExplorer } from '@grafana/ui';
|
||||
|
||||
coreModule.directive('jsonTree', [
|
||||
function jsonTreeDirective() {
|
||||
|
||||
Reference in New Issue
Block a user