From 86ce3d5e45e823b4763ca1350b1a45f4bc558eb9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Wed, 17 May 2017 17:02:50 +0200 Subject: [PATCH 01/23] feat: in app query request & response troubleshooting --- public/app/core/services/backend_srv.ts | 8 ++- .../app/features/panel/metrics_ds_selector.ts | 53 +++++++++++++++++-- .../app/features/panel/metrics_panel_ctrl.ts | 2 +- 3 files changed, 57 insertions(+), 6 deletions(-) diff --git a/public/app/core/services/backend_srv.ts b/public/app/core/services/backend_srv.ts index 16edc364340..85cc63d057f 100644 --- a/public/app/core/services/backend_srv.ts +++ b/public/app/core/services/backend_srv.ts @@ -4,6 +4,7 @@ import angular from 'angular'; import _ from 'lodash'; import config from 'app/core/config'; import coreModule from 'app/core/core_module'; +import appEvents from 'app/core/app_events'; export class BackendSrv { inFlightRequests = {}; @@ -150,7 +151,10 @@ export class BackendSrv { } } - return this.$http(options).catch(err => { + return this.$http(options).then(response => { + appEvents.emit('ds-request-response', response); + return response; + }).catch(err => { if (err.status === this.HTTP_REQUEST_CANCELLED) { throw {err, cancelled: true}; } @@ -179,7 +183,9 @@ export class BackendSrv { err.data.message = err.data.error; } + appEvents.emit('ds-request-error', err); throw err; + }).finally(() => { // clean up if (options.requestId) { diff --git a/public/app/features/panel/metrics_ds_selector.ts b/public/app/features/panel/metrics_ds_selector.ts index 7c4bc0ef495..67619837e30 100644 --- a/public/app/features/panel/metrics_ds_selector.ts +++ b/public/app/features/panel/metrics_ds_selector.ts @@ -2,10 +2,24 @@ import angular from 'angular'; import _ from 'lodash'; +import appEvents from 'app/core/app_events'; var module = angular.module('grafana.directives'); var template = ` + +
+
+
{{ctrl.lastError}}
+
+
+ +
+
+
{{ctrl.lastResponse}}
+
+
+
@@ -22,9 +36,9 @@ var template = `
-
+ +
+ +
+
`; @@ -45,9 +67,12 @@ export class MetricsDsSelectorCtrl { panelCtrl: any; datasources: any[]; current: any; + lastResponse: any; + lastError: any; + showResponse: boolean; /** @ngInject */ - constructor(private uiSegmentSrv, datasourceSrv) { + constructor($scope, private uiSegmentSrv, datasourceSrv) { this.datasources = datasourceSrv.getMetricSources(); var dsValue = this.panelCtrl.panel.datasource || null; @@ -63,7 +88,25 @@ export class MetricsDsSelectorCtrl { } this.dsSegment = uiSegmentSrv.newSegment({value: this.current.name, selectMode: true}); - this.mixedDsSegment = uiSegmentSrv.newSegment({value: 'Add query', selectMode: true}); + this.mixedDsSegment = uiSegmentSrv.newSegment({value: 'Add Query', selectMode: true}); + + appEvents.on('ds-request-response', this.onRequestResponse.bind(this), $scope); + appEvents.on('ds-request-error', this.onRequestError.bind(this), $scope); + } + + onRequestResponse(data) { + console.log(data); + this.lastResponse = JSON.stringify(data, null, 2); + this.lastError = null; + } + + toggleShowResponse() { + this.showResponse = !this.showResponse; + } + + onRequestError(err) { + console.log(err); + this.lastError = JSON.stringify(err, null, 2); } getOptions(includeBuiltin) { @@ -79,6 +122,8 @@ export class MetricsDsSelectorCtrl { if (ds) { this.current = ds; this.panelCtrl.setDatasource(ds); + this.lastError = null; + this.lastResponse = null; } } diff --git a/public/app/features/panel/metrics_panel_ctrl.ts b/public/app/features/panel/metrics_panel_ctrl.ts index af9d06d8742..000705d74d5 100644 --- a/public/app/features/panel/metrics_panel_ctrl.ts +++ b/public/app/features/panel/metrics_panel_ctrl.ts @@ -256,7 +256,7 @@ class MetricsPanelCtrl extends PanelCtrl { result = {data: []}; } - return this.events.emit('data-received', result.data); + this.events.emit('data-received', result.data); } handleDataStream(stream) { From 78dbb4dc13975cd9c6c1813f3be2597d258b74c5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Thu, 18 May 2017 14:19:39 +0200 Subject: [PATCH 02/23] config: removed trace level from config comment --- conf/sample.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/conf/sample.ini b/conf/sample.ini index 84328e7d537..766dd8075f4 100644 --- a/conf/sample.ini +++ b/conf/sample.ini @@ -298,7 +298,7 @@ # Use space to separate multiple modes, e.g. "console file" ;mode = console file -# Either "trace", "debug", "info", "warn", "error", "critical", default is "info" +# Either "debug", "info", "warn", "error", "critical", default is "info" ;level = info # optional settings to set different levels for specific loggers. Ex filters = sqlstore:debug From f65878c21d5ce92068cd58ec0d041965f6a713b5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Thu, 18 May 2017 17:04:31 +0200 Subject: [PATCH 03/23] ux: working on query troubleshooting --- package.json | 1 + .../app/core/components/jsonview/helpers.ts | 110 +++++ .../app/core/components/jsonview/jsonview.ts | 453 ++++++++++++++++++ public/app/core/components/response_viewer.ts | 63 +++ public/app/core/core.ts | 3 +- .../app/features/panel/metrics_ds_selector.ts | 31 +- public/app/headers/common.d.ts | 5 + public/app/system.conf.js | 3 +- public/sass/_grafana.scss | 1 + public/sass/components/_response_viewer.scss | 6 + public/test/test-main.js | 3 +- tasks/options/copy.js | 1 + yarn.lock | 20 +- 13 files changed, 669 insertions(+), 31 deletions(-) create mode 100644 public/app/core/components/jsonview/helpers.ts create mode 100644 public/app/core/components/jsonview/jsonview.ts create mode 100644 public/app/core/components/response_viewer.ts create mode 100644 public/sass/components/_response_viewer.scss diff --git a/package.json b/package.json index 47102820d58..19e91a31ffe 100644 --- a/package.json +++ b/package.json @@ -68,6 +68,7 @@ "grunt-jscs": "3.0.1", "grunt-sass-lint": "^0.2.2", "grunt-sync": "^0.6.2", + "json-formatter-js": "^2.2.0", "karma-sinon": "^1.0.5", "lodash": "^4.17.2", "mousetrap": "^1.6.0", diff --git a/public/app/core/components/jsonview/helpers.ts b/public/app/core/components/jsonview/helpers.ts new file mode 100644 index 00000000000..3ab36a19a7d --- /dev/null +++ b/public/app/core/components/jsonview/helpers.ts @@ -0,0 +1,110 @@ +// #<{(| +// * Escapes `"` charachters from string +// |)}># +// function escapeString(str: string): string { +// return str.replace('"', '\"'); +// } +// +// #<{(| +// * Determines if a value is an object +// |)}># +// export function isObject(value: any): boolean { +// var type = typeof value; +// return !!value && (type === 'object'); +// } +// +// #<{(| +// * Gets constructor name of an object. +// * From http://stackoverflow.com/a/332429 +// * +// |)}># +// export function getObjectName(object: Object): string { +// if (object === undefined) { +// return ''; +// } +// if (object === null) { +// return 'Object'; +// } +// if (typeof object === 'object' && !object.constructor) { +// return 'Object'; +// } +// +// const funcNameRegex = /function ([^(]*)/; +// const results = (funcNameRegex).exec((object).constructor.toString()); +// if (results && results.length > 1) { +// return results[1]; +// } else { +// return ''; +// } +// } +// +// #<{(| +// * Gets type of an object. Returns "null" for null objects +// |)}># +// export function getType(object: Object): string { +// if (object === null) { return 'null'; } +// return typeof object; +// } +// +// #<{(| +// * Generates inline preview for a JavaScript object based on a value +// |)}># +// export function getValuePreview (object: Object, value: string): string { +// var type = getType(object); +// +// if (type === 'null' || type === 'undefined') { return type; } +// +// if (type === 'string') { +// value = '"' + escapeString(value) + '"'; +// } +// if (type === 'function'){ +// +// // Remove content of the function +// return object.toString() +// .replace(/[\r\n]/g, '') +// .replace(/\{.*\}/, '') + '{…}'; +// } +// return value; +// } +// +// #<{(| +// * Generates inline preview for a JavaScript object +// |)}># +// export function getPreview(object: string): string { +// let value = ''; +// if (isObject(object)) { +// value = getObjectName(object); +// if (Array.isArray(object)) { +// value += '[' + object.length + ']'; +// } +// } else { +// value = getValuePreview(object, object); +// } +// return value; +// } +// +// #<{(| +// * Generates a prefixed CSS class name +// |)}># +// export function cssClass(className: string): string { +// return `json-formatter-${className}`; +// } +// +// #<{(| +// * Creates a new DOM element wiht given type and class +// * TODO: move me to helpers +// |)}># +// export function createElement(type: string, className?: string, content?: Element|string): Element { +// const el = document.createElement(type); +// if (className) { +// el.classList.add(cssClass(className)); +// } +// if (content !== undefined) { +// if (content instanceof Node) { +// el.appendChild(content); +// } else { +// el.appendChild(document.createTextNode(String(content))); +// } +// } +// return el; +// } diff --git a/public/app/core/components/jsonview/jsonview.ts b/public/app/core/components/jsonview/jsonview.ts new file mode 100644 index 00000000000..71994ceef59 --- /dev/null +++ b/public/app/core/components/jsonview/jsonview.ts @@ -0,0 +1,453 @@ +// import { +// isObject, +// getObjectName, +// getType, +// getValuePreview, +// getPreview, +// cssClass, +// createElement +// } from './helpers'; +// +// import './style.less'; +// +// const DATE_STRING_REGEX = /(^\d{1,4}[\.|\\/|-]\d{1,2}[\.|\\/|-]\d{1,4})(\s*(?:0?[1-9]:[0-5]|1(?=[012])\d:[0-5])\d\s*[ap]m)?$/; +// const PARTIAL_DATE_REGEX = /\d{2}:\d{2}:\d{2} GMT-\d{4}/; +// const JSON_DATE_REGEX = /\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z/; +// +// // When toggleing, don't animated removal or addition of more than a few items +// const MAX_ANIMATED_TOGGLE_ITEMS = 10; +// +// const requestAnimationFrame = window.requestAnimationFrame || function(cb: ()=>void) { cb(); return 0; }; +// +// export interface JSONFormatterConfiguration { +// hoverPreviewEnabled?: boolean; +// hoverPreviewArrayCount?: number; +// hoverPreviewFieldCount?: number; +// animateOpen?: boolean; +// animateClose?: boolean; +// theme?: string; +// }; +// +// const _defaultConfig: JSONFormatterConfiguration = { +// hoverPreviewEnabled: false, +// hoverPreviewArrayCount: 100, +// hoverPreviewFieldCount: 5, +// animateOpen: true, +// animateClose: true, +// theme: null +// }; +// +// +// #<{(|* +// * @class JSONFormatter +// * +// * JSONFormatter allows you to render JSON objects in HTML with a +// * **collapsible** navigation. +// |)}># +// export default class JSONFormatter { +// +// // Hold the open state after the toggler is used +// private _isOpen: boolean = null; +// +// // A reference to the element that we render to +// private element: Element; +// +// #<{(|* +// * @param {object} json The JSON object you want to render. It has to be an +// * object or array. Do NOT pass raw JSON string. +// * +// * @param {number} [open=1] his number indicates up to how many levels the +// * rendered tree should expand. Set it to `0` to make the whole tree collapsed +// * or set it to `Infinity` to expand the tree deeply +// * +// * @param {object} [config=defaultConfig] - +// * defaultConfig = { +// * hoverPreviewEnabled: false, +// * hoverPreviewArrayCount: 100, +// * hoverPreviewFieldCount: 5 +// * } +// * +// * Available configurations: +// * #####Hover Preview +// * * `hoverPreviewEnabled`: enable preview on hover +// * * `hoverPreviewArrayCount`: number of array items to show in preview Any +// * array larger than this number will be shown as `Array[XXX]` where `XXX` +// * is length of the array. +// * * `hoverPreviewFieldCount`: number of object properties to show for object +// * preview. Any object with more properties that thin number will be +// * truncated. +// * +// * @param {string} [key=undefined] The key that this object in it's parent +// * context +// |)}># +// constructor(public json: any, private open = 1, private config: JSONFormatterConfiguration = _defaultConfig, private key?: string) { +// +// // Setting default values for config object +// if (this.config.hoverPreviewEnabled === undefined) { +// this.config.hoverPreviewEnabled = _defaultConfig.hoverPreviewEnabled; +// } +// if (this.config.hoverPreviewArrayCount === undefined) { +// this.config.hoverPreviewArrayCount = _defaultConfig.hoverPreviewArrayCount; +// } +// if (this.config.hoverPreviewFieldCount === undefined) { +// this.config.hoverPreviewFieldCount = _defaultConfig.hoverPreviewFieldCount; +// } +// } +// +// #<{(| +// * is formatter open? +// |)}># +// private get isOpen(): boolean { +// if (this._isOpen !== null) { +// return this._isOpen; +// } else { +// return this.open > 0; +// } +// } +// +// #<{(| +// * set open state (from toggler) +// |)}># +// private set isOpen(value: boolean) { +// this._isOpen = value; +// } +// +// #<{(| +// * is this a date string? +// |)}># +// private get isDate(): boolean { +// return (this.type === 'string') && +// (DATE_STRING_REGEX.test(this.json) || +// JSON_DATE_REGEX.test(this.json) || +// PARTIAL_DATE_REGEX.test(this.json)); +// } +// +// #<{(| +// * is this a URL string? +// |)}># +// private get isUrl(): boolean { +// return this.type === 'string' && (this.json.indexOf('http') === 0); +// } +// +// #<{(| +// * is this an array? +// |)}># +// private get isArray(): boolean { +// return Array.isArray(this.json); +// } +// +// #<{(| +// * is this an object? +// * Note: In this context arrays are object as well +// |)}># +// private get isObject(): boolean { +// return isObject(this.json); +// } +// +// #<{(| +// * is this an empty object with no properties? +// |)}># +// private get isEmptyObject(): boolean { +// return !this.keys.length && !this.isArray; +// } +// +// #<{(| +// * is this an empty object or array? +// |)}># +// private get isEmpty(): boolean { +// return this.isEmptyObject || (this.keys && !this.keys.length && this.isArray); +// } +// +// #<{(| +// * did we recieve a key argument? +// * This means that the formatter was called as a sub formatter of a parent formatter +// |)}># +// private get hasKey(): boolean { +// return typeof this.key !== 'undefined'; +// } +// +// #<{(| +// * if this is an object, get constructor function name +// |)}># +// private get constructorName(): string { +// return getObjectName(this.json); +// } +// +// #<{(| +// * get type of this value +// * Possible values: all JavaScript primitive types plus "array" and "null" +// |)}># +// private get type(): string { +// return getType(this.json); +// } +// +// #<{(| +// * get object keys +// * If there is an empty key we pad it wit quotes to make it visible +// |)}># +// private get keys(): string[] { +// if (this.isObject) { +// return Object.keys(this.json).map((key)=> key ? key : '""'); +// } else { +// return []; +// } +// } +// +// #<{(|* +// * Toggles `isOpen` state +// * +// |)}># +// toggleOpen() { +// this.isOpen = !this.isOpen; +// +// if (this.element) { +// if (this.isOpen) { +// this.appendChildren(this.config.animateOpen); +// } else{ +// this.removeChildren(this.config.animateClose); +// } +// this.element.classList.toggle(cssClass('open')); +// } +// } +// +// #<{(|* +// * Open all children up to a certain depth. +// * Allows actions such as expand all/collapse all +// * +// |)}># +// openAtDepth(depth = 1) { +// if (depth < 0) { +// return; +// } +// +// this.open = depth; +// this.isOpen = (depth !== 0); +// +// if (this.element) { +// this.removeChildren(false); +// +// if (depth === 0) { +// this.element.classList.remove(cssClass('open')); +// } else { +// this.appendChildren(this.config.animateOpen); +// this.element.classList.add(cssClass('open')); +// } +// } +// } +// +// #<{(|* +// * Generates inline preview +// * +// * @returns {string} +// |)}># +// getInlinepreview() { +// if (this.isArray) { +// +// // if array length is greater then 100 it shows "Array[101]" +// if (this.json.length > this.config.hoverPreviewArrayCount) { +// return `Array[${this.json.length}]`; +// } else { +// return `[${this.json.map(getPreview).join(', ')}]`; +// } +// } else { +// +// const keys = this.keys; +// +// // the first five keys (like Chrome Developer Tool) +// const narrowKeys = keys.slice(0, this.config.hoverPreviewFieldCount); +// +// // json value schematic information +// const kvs = narrowKeys.map(key => `${key}:${getPreview(this.json[key])}`); +// +// // if keys count greater then 5 then show ellipsis +// const ellipsis = keys.length >= this.config.hoverPreviewFieldCount ? '…' : ''; +// +// return `{${kvs.join(', ')}${ellipsis}}`; +// } +// } +// +// +// #<{(|* +// * Renders an HTML element and installs event listeners +// * +// * @returns {HTMLDivElement} +// |)}># +// render(): HTMLDivElement { +// +// // construct the root element and assign it to this.element +// this.element = createElement('div', 'row'); +// +// // construct the toggler link +// const togglerLink = createElement('a', 'toggler-link'); +// +// // if this is an object we need a wrapper span (toggler) +// if (this.isObject) { +// togglerLink.appendChild(createElement('span', 'toggler')); +// } +// +// // if this is child of a parent formatter we need to append the key +// if (this.hasKey) { +// togglerLink.appendChild(createElement('span', 'key', `${this.key}:`)); +// } +// +// // Value for objects and arrays +// if (this.isObject) { +// +// // construct the value holder element +// const value = createElement('span', 'value'); +// +// // we need a wrapper span for objects +// const objectWrapperSpan = createElement('span'); +// +// // get constructor name and append it to wrapper span +// var constructorName = createElement('span', 'constructor-name', this.constructorName); +// objectWrapperSpan.appendChild(constructorName); +// +// // if it's an array append the array specific elements like brackets and length +// if (this.isArray) { +// const arrayWrapperSpan = createElement('span'); +// arrayWrapperSpan.appendChild(createElement('span', 'bracket', '[')); +// arrayWrapperSpan.appendChild(createElement('span', 'number', (this.json.length))); +// arrayWrapperSpan.appendChild(createElement('span', 'bracket', ']')); +// objectWrapperSpan.appendChild(arrayWrapperSpan); +// } +// +// // append object wrapper span to toggler link +// value.appendChild(objectWrapperSpan); +// togglerLink.appendChild(value); +// +// // Primitive values +// } else { +// +// // make a value holder element +// const value = this.isUrl ? createElement('a') : createElement('span'); +// +// // add type and other type related CSS classes +// value.classList.add(cssClass(this.type)); +// if (this.isDate) { +// value.classList.add(cssClass('date')); +// } +// if (this.isUrl) { +// value.classList.add(cssClass('url')); +// value.setAttribute('href', this.json); +// } +// +// // Append value content to value element +// const valuePreview = getValuePreview(this.json, this.json); +// value.appendChild(document.createTextNode(valuePreview)); +// +// // append the value element to toggler link +// togglerLink.appendChild(value); +// } +// +// // if hover preview is enabled, append the inline preview element +// if (this.isObject && this.config.hoverPreviewEnabled) { +// const preview = createElement('span', 'preview-text'); +// preview.appendChild(document.createTextNode(this.getInlinepreview())); +// togglerLink.appendChild(preview); +// } +// +// // construct a children element +// const children = createElement('div', 'children'); +// +// // set CSS classes for children +// if (this.isObject) { +// children.classList.add(cssClass('object')); +// } +// if (this.isArray) { +// children.classList.add(cssClass('array')); +// } +// if (this.isEmpty) { +// children.classList.add(cssClass('empty')); +// } +// +// // set CSS classes for root element +// if (this.config && this.config.theme) { +// this.element.classList.add(cssClass(this.config.theme)); +// } +// if (this.isOpen) { +// this.element.classList.add(cssClass('open')); +// } +// +// // append toggler and children elements to root element +// this.element.appendChild(togglerLink); +// this.element.appendChild(children); +// +// // if formatter is set to be open call appendChildren +// if (this.isObject && this.isOpen) { +// this.appendChildren(); +// } +// +// // add event listener for toggling +// if (this.isObject) { +// togglerLink.addEventListener('click', this.toggleOpen.bind(this)); +// } +// +// return this.element as HTMLDivElement; +// } +// +// #<{(|* +// * Appends all the children to children element +// * Animated option is used when user triggers this via a click +// |)}># +// appendChildren(animated = false) { +// const children = this.element.querySelector(`div.${cssClass('children')}`); +// +// if (!children || this.isEmpty) { return; } +// +// if (animated) { +// let index = 0; +// const addAChild = ()=> { +// const key = this.keys[index]; +// const formatter = new JSONFormatter(this.json[key], this.open - 1, this.config, key); +// children.appendChild(formatter.render()); +// +// index += 1; +// +// if (index < this.keys.length) { +// if (index > MAX_ANIMATED_TOGGLE_ITEMS) { +// addAChild(); +// } else { +// requestAnimationFrame(addAChild); +// } +// } +// }; +// +// requestAnimationFrame(addAChild); +// +// } else { +// this.keys.forEach(key => { +// const formatter = new JSONFormatter(this.json[key], this.open - 1, this.config, key); +// children.appendChild(formatter.render()); +// }); +// } +// } +// +// #<{(|* +// * Removes all the children from children element +// * Animated option is used when user triggers this via a click +// |)}># +// removeChildren(animated = false) { +// const childrenElement = this.element.querySelector(`div.${cssClass('children')}`) as HTMLDivElement; +// +// if (animated) { +// let childrenRemoved = 0; +// const removeAChild = ()=> { +// if (childrenElement && childrenElement.children.length) { +// childrenElement.removeChild(childrenElement.children[0]); +// childrenRemoved += 1; +// if (childrenRemoved > MAX_ANIMATED_TOGGLE_ITEMS) { +// removeAChild(); +// } else { +// requestAnimationFrame(removeAChild); +// } +// } +// }; +// requestAnimationFrame(removeAChild); +// } else { +// if (childrenElement) { +// childrenElement.innerHTML = ''; +// } +// } +// } +// } diff --git a/public/app/core/components/response_viewer.ts b/public/app/core/components/response_viewer.ts new file mode 100644 index 00000000000..5861e010fb3 --- /dev/null +++ b/public/app/core/components/response_viewer.ts @@ -0,0 +1,63 @@ +/// + +import coreModule from 'app/core/core_module'; +import JsonFormatter from 'json-formatter-js'; + + +const template = ` +
+
+
+`; + +export function responseViewer() { + return { + restrict: 'E', + template: template, + scope: {response: "="}, + link: function(scope, elem) { + var jsonElem = elem.find('.response-viewer-json'); + + scope.$watch("response", newVal => { + if (!newVal) { + elem.empty(); + return; + } + + if (scope.response.headers) { + delete scope.response.headers; + } + + if (scope.response.data) { + scope.response.response = scope.response.data; + delete scope.response.data; + } + + if (scope.response.config) { + scope.response.request = scope.response.config; + delete scope.response.config; + delete scope.response.request.transformRequest; + delete scope.response.request.transformResponse; + delete scope.response.request.paramSerializer; + delete scope.response.request.jsonpCallbackParam; + delete scope.response.request.headers; + delete scope.response.request.requestId; + delete scope.response.request.inspect; + delete scope.response.request.retry; + delete scope.response.request.timeout; + } + + + const formatter = new JsonFormatter(scope.response, 2, { + theme: 'dark', + }); + + const html = formatter.render(); + jsonElem.html(html); + }); + + } + }; +} + +coreModule.directive('responseViewer', responseViewer); diff --git a/public/app/core/core.ts b/public/app/core/core.ts index 4aa2e7eb64a..4c5fa429677 100644 --- a/public/app/core/core.ts +++ b/public/app/core/core.ts @@ -45,7 +45,7 @@ import {assignModelProperties} from './utils/model_utils'; import {contextSrv} from './services/context_srv'; import {KeybindingSrv} from './services/keybindingSrv'; import {helpModal} from './components/help/help'; - +import {responseViewer} from './components/response_viewer'; export { arrayJoin, @@ -69,4 +69,5 @@ export { contextSrv, KeybindingSrv, helpModal, + responseViewer, }; diff --git a/public/app/features/panel/metrics_ds_selector.ts b/public/app/features/panel/metrics_ds_selector.ts index 67619837e30..6b7fa8da6f5 100644 --- a/public/app/features/panel/metrics_ds_selector.ts +++ b/public/app/features/panel/metrics_ds_selector.ts @@ -8,16 +8,8 @@ var module = angular.module('grafana.directives'); var template = ` -
-
-
{{ctrl.lastError}}
-
-
-
-
-
{{ctrl.lastResponse}}
-
+
@@ -49,9 +41,9 @@ var template = `
-
@@ -68,7 +60,7 @@ export class MetricsDsSelectorCtrl { datasources: any[]; current: any; lastResponse: any; - lastError: any; + responseData: any; showResponse: boolean; /** @ngInject */ @@ -95,9 +87,8 @@ export class MetricsDsSelectorCtrl { } onRequestResponse(data) { - console.log(data); - this.lastResponse = JSON.stringify(data, null, 2); - this.lastError = null; + this.responseData = data; + this.showResponse = true; } toggleShowResponse() { @@ -105,8 +96,9 @@ export class MetricsDsSelectorCtrl { } onRequestError(err) { - console.log(err); - this.lastError = JSON.stringify(err, null, 2); + this.responseData = err; + this.responseData.isError = true; + this.showResponse = true; } getOptions(includeBuiltin) { @@ -122,8 +114,7 @@ export class MetricsDsSelectorCtrl { if (ds) { this.current = ds; this.panelCtrl.setDatasource(ds); - this.lastError = null; - this.lastResponse = null; + this.responseData = null; } } diff --git a/public/app/headers/common.d.ts b/public/app/headers/common.d.ts index 9ea5e96654d..b1d064b8036 100644 --- a/public/app/headers/common.d.ts +++ b/public/app/headers/common.d.ts @@ -72,3 +72,8 @@ declare module 'd3' { var d3: any; export default d3; } + +declare module 'json-formatter-js' { + var JSONFormatter: any; + export default JSONFormatter; +} diff --git a/public/app/system.conf.js b/public/app/system.conf.js index ae8e93a726a..9354a708785 100644 --- a/public/app/system.conf.js +++ b/public/app/system.conf.js @@ -32,7 +32,8 @@ System.config({ "jquery.flot.fillbelow": "vendor/flot/jquery.flot.fillbelow", "jquery.flot.gauge": "vendor/flot/jquery.flot.gauge", "d3": "vendor/d3/d3.js", - "jquery.flot.dashes": "vendor/flot/jquery.flot.dashes" + "jquery.flot.dashes": "vendor/flot/jquery.flot.dashes", + "json-formatter-js": "vendor/npm/json-formatter-js/dist/json-formatter" }, packages: { diff --git a/public/sass/_grafana.scss b/public/sass/_grafana.scss index 6266bd9c98f..6128a9c47a8 100644 --- a/public/sass/_grafana.scss +++ b/public/sass/_grafana.scss @@ -75,6 +75,7 @@ @import "components/jsontree"; @import "components/edit_sidemenu.scss"; @import "components/row.scss"; +@import "components/response_viewer.scss"; // PAGES @import "pages/login"; diff --git a/public/sass/components/_response_viewer.scss b/public/sass/components/_response_viewer.scss new file mode 100644 index 00000000000..f554f98e382 --- /dev/null +++ b/public/sass/components/_response_viewer.scss @@ -0,0 +1,6 @@ +.response-viewer { + background: $card-background; + box-shadow: $card-shadow; + padding: 1rem; + border-radius: 4px; +} diff --git a/public/test/test-main.js b/public/test/test-main.js index 50553b7c8b3..6cdf19c5372 100644 --- a/public/test/test-main.js +++ b/public/test/test-main.js @@ -40,7 +40,8 @@ "jquery.flot.fillbelow": "vendor/flot/jquery.flot.fillbelow", "jquery.flot.gauge": "vendor/flot/jquery.flot.gauge", "d3": "vendor/d3/d3.js", - "jquery.flot.dashes": "vendor/flot/jquery.flot.dashes" + "jquery.flot.dashes": "vendor/flot/jquery.flot.dashes", + "json-formatter-js": "vendor/npm/json-formatter-js/dist/json-formatter" }, packages: { diff --git a/tasks/options/copy.js b/tasks/options/copy.js index 59921576da3..6a902c7188b 100644 --- a/tasks/options/copy.js +++ b/tasks/options/copy.js @@ -34,6 +34,7 @@ module.exports = function(config) { 'remarkable/dist/*', 'virtual-scroll/**/*', 'mousetrap/**/*', + 'json-formatter-js/dist/*.js', ], dest: '<%= srcDir %>/vendor/npm' } diff --git a/yarn.lock b/yarn.lock index 74cad3c2407..cb45e85d1fb 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1663,9 +1663,9 @@ glob@7.0.5: once "^1.3.0" path-is-absolute "^1.0.0" -glob@^7.0.0, glob@^7.0.3, glob@^7.0.5, glob@~7.0.0: - version "7.0.6" - resolved "https://registry.yarnpkg.com/glob/-/glob-7.0.6.tgz#211bafaf49e525b8cd93260d14ab136152b3f57a" +glob@^7.0.0, glob@^7.1.1, glob@~7.1.1: + version "7.1.1" + resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.1.tgz#805211df04faaf1c63a3600306cdf5ade50b2ec8" dependencies: fs.realpath "^1.0.0" inflight "^1.0.4" @@ -1674,9 +1674,9 @@ glob@^7.0.0, glob@^7.0.3, glob@^7.0.5, glob@~7.0.0: once "^1.3.0" path-is-absolute "^1.0.0" -glob@^7.1.1, glob@~7.1.1: - version "7.1.1" - resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.1.tgz#805211df04faaf1c63a3600306cdf5ade50b2ec8" +glob@^7.0.3, glob@^7.0.5, glob@~7.0.0: + version "7.0.6" + resolved "https://registry.yarnpkg.com/glob/-/glob-7.0.6.tgz#211bafaf49e525b8cd93260d14ab136152b3f57a" dependencies: fs.realpath "^1.0.0" inflight "^1.0.4" @@ -2561,6 +2561,10 @@ jshint@~2.9.4: shelljs "0.3.x" strip-json-comments "1.0.x" +json-formatter-js@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/json-formatter-js/-/json-formatter-js-2.2.0.tgz#1ed987223ef2f1d945304597faae78b580a8212b" + json-schema@0.2.3: version "0.2.3" resolved "https://registry.yarnpkg.com/json-schema/-/json-schema-0.2.3.tgz#b480c892e59a2f05954ce727bd3f2a4e882f9e13" @@ -3807,11 +3811,11 @@ resolve-pkg@^0.1.0: dependencies: resolve-from "^2.0.0" -resolve@1.1.x, resolve@^1.1.6, resolve@~1.1.0: +resolve@1.1.x, resolve@~1.1.0: version "1.1.7" resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.1.7.tgz#203114d82ad2c5ed9e8e0411b3932875e889e97b" -resolve@^1.3.2: +resolve@^1.1.6, resolve@^1.3.2: version "1.3.3" resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.3.3.tgz#655907c3469a8680dc2de3a275a8fdd69691f0e5" dependencies: From f7a6c9a1e66123d69b8075254f3f0d8f93e8aeae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Thu, 18 May 2017 17:05:15 +0200 Subject: [PATCH 04/23] ux: minor change --- public/app/features/panel/metrics_ds_selector.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/public/app/features/panel/metrics_ds_selector.ts b/public/app/features/panel/metrics_ds_selector.ts index 6b7fa8da6f5..2a9137e6aa5 100644 --- a/public/app/features/panel/metrics_ds_selector.ts +++ b/public/app/features/panel/metrics_ds_selector.ts @@ -88,7 +88,6 @@ export class MetricsDsSelectorCtrl { onRequestResponse(data) { this.responseData = data; - this.showResponse = true; } toggleShowResponse() { 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 05/23] 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" From 6ad1a396a543ff488093f38f3f743f2f8ebc768d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Fri, 19 May 2017 16:00:01 +0200 Subject: [PATCH 06/23] feat: query troubleshooter --- public/app/core/components/collapse_box.ts | 58 ++++++++++ .../components/json_explorer/json_explorer.ts | 8 +- public/app/core/components/response_viewer.ts | 63 ---------- public/app/core/core.ts | 6 +- public/app/features/panel/all.js | 1 + .../app/features/panel/metrics_ds_selector.ts | 27 ----- .../features/panel/query_troubleshooter.ts | 108 ++++++++++++++++++ public/app/partials/metrics.html | 4 +- public/sass/_grafana.scss | 2 +- public/sass/components/_collapse_box.scss | 29 +++++ public/sass/components/_json_explorer.scss | 2 +- public/sass/components/_response_viewer.scss | 6 - 12 files changed, 210 insertions(+), 104 deletions(-) create mode 100644 public/app/core/components/collapse_box.ts delete mode 100644 public/app/core/components/response_viewer.ts create mode 100644 public/app/features/panel/query_troubleshooter.ts create mode 100644 public/sass/components/_collapse_box.scss delete mode 100644 public/sass/components/_response_viewer.scss diff --git a/public/app/core/components/collapse_box.ts b/public/app/core/components/collapse_box.ts new file mode 100644 index 00000000000..05fac27904a --- /dev/null +++ b/public/app/core/components/collapse_box.ts @@ -0,0 +1,58 @@ +/// + +import coreModule from 'app/core/core_module'; + +const template = ` + +`; + +export class CollapseBoxCtrl { + isOpen: boolean; + onOpen: () => void; + + /** @ngInject **/ + constructor() { + this.isOpen = false; + } + + toggle() { + this.isOpen = !this.isOpen; + if (this.isOpen) { + this.onOpen(); + } + } +} + +export function collapseBox() { + return { + restrict: 'E', + template: template, + controller: CollapseBoxCtrl, + bindToController: true, + controllerAs: 'ctrl', + scope: { + "title": "@", + "isOpen": "=?", + "onOpen": "&" + }, + transclude: { + 'actions': '?collapseBoxActions', + 'body': 'collapseBoxBody', + }, + link: function(scope, elem, attrs) { + } + }; +} + +coreModule.directive('collapseBox', collapseBox); diff --git a/public/app/core/components/json_explorer/json_explorer.ts b/public/app/core/components/json_explorer/json_explorer.ts index 3f460968b77..4b57268c663 100644 --- a/public/app/core/components/json_explorer/json_explorer.ts +++ b/public/app/core/components/json_explorer/json_explorer.ts @@ -45,7 +45,7 @@ const _defaultConfig: JsonExplorerConfig = { * JsonExplorer allows you to render JSON objects in HTML with a * **collapsible** navigation. */ -export default class JsonExplorer { +export class JsonExplorer { // Hold the open state after the toggler is used private _isOpen: boolean = null; @@ -273,7 +273,7 @@ export default class JsonExplorer { * * @returns {HTMLDivElement} */ - render(): HTMLDivElement { + render(skipRoot = false): HTMLDivElement { // construct the root element and assign it to this.element this.element = createElement('div', 'row'); @@ -371,7 +371,9 @@ export default class JsonExplorer { } // append toggler and children elements to root element - this.element.appendChild(togglerLink); + if (!skipRoot) { + this.element.appendChild(togglerLink); + } this.element.appendChild(children); // if formatter is set to be open call appendChildren diff --git a/public/app/core/components/response_viewer.ts b/public/app/core/components/response_viewer.ts deleted file mode 100644 index fe332f070ec..00000000000 --- a/public/app/core/components/response_viewer.ts +++ /dev/null @@ -1,63 +0,0 @@ -/// - -import coreModule from 'app/core/core_module'; -import JsonExplorer from './json_explorer/json_explorer'; - - -const template = ` -
-
-
-`; - -export function responseViewer() { - return { - restrict: 'E', - template: template, - scope: {response: "="}, - link: function(scope, elem) { - var jsonElem = elem.find('.response-viewer-json'); - - scope.$watch("response", newVal => { - if (!newVal) { - elem.empty(); - return; - } - - if (scope.response.headers) { - delete scope.response.headers; - } - - if (scope.response.data) { - scope.response.response = scope.response.data; - delete scope.response.data; - } - - if (scope.response.config) { - scope.response.request = scope.response.config; - delete scope.response.config; - delete scope.response.request.transformRequest; - delete scope.response.request.transformResponse; - delete scope.response.request.paramSerializer; - delete scope.response.request.jsonpCallbackParam; - delete scope.response.request.headers; - delete scope.response.request.requestId; - delete scope.response.request.inspect; - delete scope.response.request.retry; - delete scope.response.request.timeout; - } - - - const formatter = new JsonExplorer(scope.response, 2, { - theme: 'dark', - }); - - const html = formatter.render(); - jsonElem.html(html); - }); - - } - }; -} - -coreModule.directive('responseViewer', responseViewer); diff --git a/public/app/core/core.ts b/public/app/core/core.ts index 4c5fa429677..f6b5046c3fc 100644 --- a/public/app/core/core.ts +++ b/public/app/core/core.ts @@ -45,7 +45,8 @@ import {assignModelProperties} from './utils/model_utils'; import {contextSrv} from './services/context_srv'; import {KeybindingSrv} from './services/keybindingSrv'; import {helpModal} from './components/help/help'; -import {responseViewer} from './components/response_viewer'; +import {collapseBox} from './components/collapse_box'; +import {JsonExplorer} from './components/json_explorer/json_explorer'; export { arrayJoin, @@ -69,5 +70,6 @@ export { contextSrv, KeybindingSrv, helpModal, - responseViewer, + collapseBox, + JsonExplorer, }; diff --git a/public/app/features/panel/all.js b/public/app/features/panel/all.js index 2f978e65345..b4afba4da1b 100644 --- a/public/app/features/panel/all.js +++ b/public/app/features/panel/all.js @@ -6,4 +6,5 @@ define([ './panel_editor_tab', './query_editor_row', './metrics_ds_selector', + './query_troubleshooter', ], function () {}); diff --git a/public/app/features/panel/metrics_ds_selector.ts b/public/app/features/panel/metrics_ds_selector.ts index 2a9137e6aa5..523268953a0 100644 --- a/public/app/features/panel/metrics_ds_selector.ts +++ b/public/app/features/panel/metrics_ds_selector.ts @@ -8,10 +8,6 @@ var module = angular.module('grafana.directives'); var template = ` -
- -
-
@@ -40,13 +36,6 @@ var template = `
-
- -
-
`; @@ -81,24 +70,8 @@ export class MetricsDsSelectorCtrl { this.dsSegment = uiSegmentSrv.newSegment({value: this.current.name, selectMode: true}); this.mixedDsSegment = uiSegmentSrv.newSegment({value: 'Add Query', selectMode: true}); - - appEvents.on('ds-request-response', this.onRequestResponse.bind(this), $scope); - appEvents.on('ds-request-error', this.onRequestError.bind(this), $scope); } - onRequestResponse(data) { - this.responseData = data; - } - - toggleShowResponse() { - this.showResponse = !this.showResponse; - } - - onRequestError(err) { - this.responseData = err; - this.responseData.isError = true; - this.showResponse = true; - } getOptions(includeBuiltin) { return Promise.resolve(this.datasources.filter(value => { diff --git a/public/app/features/panel/query_troubleshooter.ts b/public/app/features/panel/query_troubleshooter.ts new file mode 100644 index 00000000000..0c9dabb3ac1 --- /dev/null +++ b/public/app/features/panel/query_troubleshooter.ts @@ -0,0 +1,108 @@ +/// + +import _ from 'lodash'; +import appEvents from 'app/core/app_events'; +import {coreModule, JsonExplorer} from 'app/core/core'; + +const template = ` + + + Copy to clipboard + + +
+
+
+`; + +export class QueryTroubleshooterCtrl { + responseData: any; + showResponse: boolean; + panelCtrl: any; + renderJsonExplorer: (data) => void; + + /** @ngInject **/ + constructor($scope, private $timeout) { + appEvents.on('ds-request-response', this.onRequestResponse.bind(this), $scope); + appEvents.on('ds-request-error', this.onRequestError.bind(this), $scope); + } + + onRequestResponse(data) { + this.responseData = data; + } + + toggleShowResponse() { + this.showResponse = !this.showResponse; + } + + onRequestError(err) { + this.responseData = err; + this.responseData.isError = true; + this.showResponse = true; + } + + onOpen() { + if (!this.responseData) { + console.log('no data'); + return; + } + + var data = this.responseData; + if (data.headers) { + delete data.headers; + } + + if (data.config) { + data.request = data.config; + delete data.config; + delete data.request.transformRequest; + delete data.request.transformResponse; + delete data.request.paramSerializer; + delete data.request.jsonpCallbackParam; + delete data.request.headers; + delete data.request.requestId; + delete data.request.inspect; + delete data.request.retry; + delete data.request.timeout; + } + + if (data.data) { + data.response = data.data; + + delete data.data; + delete data.status; + delete data.statusText; + delete data.$$config; + } + + this.$timeout(_.partial(this.renderJsonExplorer, data), 10); + } +} + +export function queryTroubleshooter() { + return { + restrict: 'E', + template: template, + controller: QueryTroubleshooterCtrl, + bindToController: true, + controllerAs: 'ctrl', + scope: { + panelCtrl: "=" + }, + link: function(scope, elem, attrs, ctrl) { + + ctrl.renderJsonExplorer = function(data) { + var jsonElem = elem.find('.query-troubleshooter-json'); + + const formatter = new JsonExplorer(data, 2, { + theme: 'dark', + }); + + const html = formatter.render(true); + jsonElem.html(html); + }; + } + }; +} + +coreModule.directive('queryTroubleshooter', queryTroubleshooter); diff --git a/public/app/partials/metrics.html b/public/app/partials/metrics.html index 39c471eabd1..d3c6e6bcfc4 100644 --- a/public/app/partials/metrics.html +++ b/public/app/partials/metrics.html @@ -1,4 +1,6 @@ + +
@@ -8,7 +10,7 @@
- +
diff --git a/public/sass/_grafana.scss b/public/sass/_grafana.scss index c8f82ad845a..63ff5c6fe02 100644 --- a/public/sass/_grafana.scss +++ b/public/sass/_grafana.scss @@ -75,8 +75,8 @@ @import "components/jsontree"; @import "components/edit_sidemenu.scss"; @import "components/row.scss"; -@import "components/response_viewer.scss"; @import "components/json_explorer.scss"; +@import "components/collapse_box.scss"; // PAGES @import "pages/login"; diff --git a/public/sass/components/_collapse_box.scss b/public/sass/components/_collapse_box.scss new file mode 100644 index 00000000000..61abebaf634 --- /dev/null +++ b/public/sass/components/_collapse_box.scss @@ -0,0 +1,29 @@ +.collapse-box { + margin-bottom: $spacer; +} + +.collapse-box__header { + display: flex; + flex-direction: row; + padding: $input-padding-y $input-padding-x; + margin-right: $gf-form-margin; + background-color: $input-bg; + font-size: $font-size-sm; + margin-right: $gf-form-margin; + + border: $input-btn-border-width solid transparent; + @include border-radius($label-border-radius-sm); +} + +.collapse-box__header-title { + flex-grow: 1; +} + +.collapse-box__body { + padding: $input-padding-y*2 $input-padding-x; + background-color: $input-label-bg; + display: block; + margin-right: $gf-form-margin; + border: $input-btn-border-width solid transparent; + @include border-radius($label-border-radius-sm); +} diff --git a/public/sass/components/_json_explorer.scss b/public/sass/components/_json_explorer.scss index 3c4c19eceb7..d372c332176 100644 --- a/public/sass/components/_json_explorer.scss +++ b/public/sass/components/_json_explorer.scss @@ -36,7 +36,7 @@ .json-formatter-string { color: $string-color; - white-space: pre; + white-space: normal; word-wrap: break-word; } .json-formatter-number { color: $number-color; } diff --git a/public/sass/components/_response_viewer.scss b/public/sass/components/_response_viewer.scss deleted file mode 100644 index f554f98e382..00000000000 --- a/public/sass/components/_response_viewer.scss +++ /dev/null @@ -1,6 +0,0 @@ -.response-viewer { - background: $card-background; - box-shadow: $card-shadow; - padding: 1rem; - border-radius: 4px; -} From 5fcb966297948cb9f35f24a4ac140c136bb3fe96 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Fri, 19 May 2017 17:35:36 +0200 Subject: [PATCH 07/23] feat: query troubleshooter progress --- public/app/core/components/collapse_box.ts | 14 +++--- .../features/panel/query_troubleshooter.ts | 50 +++++++++++-------- .../plugins/datasource/influxdb/datasource.ts | 10 ++-- public/sass/components/_collapse_box.scss | 8 +++ 4 files changed, 51 insertions(+), 31 deletions(-) diff --git a/public/app/core/components/collapse_box.ts b/public/app/core/components/collapse_box.ts index 05fac27904a..7fc234cb583 100644 --- a/public/app/core/components/collapse_box.ts +++ b/public/app/core/components/collapse_box.ts @@ -10,7 +10,7 @@ const template = ` {{ctrl.title}} -
+
@@ -19,18 +19,18 @@ const template = ` export class CollapseBoxCtrl { isOpen: boolean; - onOpen: () => void; + stateChanged: () => void; /** @ngInject **/ - constructor() { + constructor(private $timeout) { this.isOpen = false; } toggle() { this.isOpen = !this.isOpen; - if (this.isOpen) { - this.onOpen(); - } + this.$timeout(() => { + this.stateChanged(); + }); } } @@ -44,7 +44,7 @@ export function collapseBox() { scope: { "title": "@", "isOpen": "=?", - "onOpen": "&" + "stateChanged": "&" }, transclude: { 'actions': '?collapseBoxActions', diff --git a/public/app/features/panel/query_troubleshooter.ts b/public/app/features/panel/query_troubleshooter.ts index 0c9dabb3ac1..7d5e4dd0e9c 100644 --- a/public/app/features/panel/query_troubleshooter.ts +++ b/public/app/features/panel/query_troubleshooter.ts @@ -5,7 +5,8 @@ import appEvents from 'app/core/app_events'; import {coreModule, JsonExplorer} from 'app/core/core'; const template = ` - + Copy to clipboard @@ -16,38 +17,47 @@ const template = ` `; export class QueryTroubleshooterCtrl { - responseData: any; + isOpen: any; showResponse: boolean; panelCtrl: any; renderJsonExplorer: (data) => void; + onRequestErrorEventListener: any; + onRequestResponseEventListener: any; + hasError: boolean; /** @ngInject **/ constructor($scope, private $timeout) { - appEvents.on('ds-request-response', this.onRequestResponse.bind(this), $scope); - appEvents.on('ds-request-error', this.onRequestError.bind(this), $scope); + this.onRequestErrorEventListener = this.onRequestError.bind(this); + this.onRequestResponseEventListener = this.onRequestResponse.bind(this); + + appEvents.on('ds-request-error', this.onRequestErrorEventListener); + $scope.$on('$destroy', this.removeEventsListeners.bind(this)); } - onRequestResponse(data) { - this.responseData = data; - } - - toggleShowResponse() { - this.showResponse = !this.showResponse; + removeEventsListeners() { + appEvents.off('ds-request-response', this.onRequestResponseEventListener); + appEvents.off('ds-request-error', this.onRequestErrorEventListener); } onRequestError(err) { - this.responseData = err; - this.responseData.isError = true; - this.showResponse = true; + this.isOpen = true; + this.hasError = true; + this.onRequestResponse(err); } - onOpen() { - if (!this.responseData) { - console.log('no data'); - return; + stateChanged() { + console.log(this.isOpen); + if (this.isOpen) { + appEvents.on('ds-request-response', this.onRequestResponseEventListener); + this.panelCtrl.refresh(); + } else { + this.hasError = false; } + } + + onRequestResponse(data) { + data = _.cloneDeep(data); - var data = this.responseData; if (data.headers) { delete data.headers; } @@ -75,7 +85,7 @@ export class QueryTroubleshooterCtrl { delete data.$$config; } - this.$timeout(_.partial(this.renderJsonExplorer, data), 10); + this.$timeout(_.partial(this.renderJsonExplorer, data)); } } @@ -94,7 +104,7 @@ export function queryTroubleshooter() { ctrl.renderJsonExplorer = function(data) { var jsonElem = elem.find('.query-troubleshooter-json'); - const formatter = new JsonExplorer(data, 2, { + const formatter = new JsonExplorer(data, 3, { theme: 'dark', }); diff --git a/public/app/plugins/datasource/influxdb/datasource.ts b/public/app/plugins/datasource/influxdb/datasource.ts index 6af6a849e95..6bfcaf45fef 100644 --- a/public/app/plugins/datasource/influxdb/datasource.ts +++ b/public/app/plugins/datasource/influxdb/datasource.ts @@ -213,10 +213,12 @@ export default class InfluxDatasource { var currentUrl = self.urls.shift(); self.urls.push(currentUrl); - var params: any = { - u: self.username, - p: self.password, - }; + var params: any = {}; + + if (self.username) { + params.username = self.username; + params.password = self.password; + } if (self.database) { params.db = self.database; diff --git a/public/sass/components/_collapse_box.scss b/public/sass/components/_collapse_box.scss index 61abebaf634..96835ba9db9 100644 --- a/public/sass/components/_collapse_box.scss +++ b/public/sass/components/_collapse_box.scss @@ -1,5 +1,12 @@ .collapse-box { margin-bottom: $spacer; + + &--error { + .collapse-box__header { + background-color: $red; + color: $white; + } + } } .collapse-box__header { @@ -27,3 +34,4 @@ border: $input-btn-border-width solid transparent; @include border-radius($label-border-radius-sm); } + From 5909f9ef924371a7cf2e423e433ee991c4152725 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Fri, 19 May 2017 21:32:23 +0200 Subject: [PATCH 08/23] feat: more work on metrics tab reworkings --- docker/blocks/graphite/fig | 1 - .../app/core/directives/plugin_component.ts | 4 +- public/app/features/panel/all.js | 1 - .../app/features/panel/metrics_panel_ctrl.ts | 3 +- ...{metrics_ds_selector.ts => metrics_tab.ts} | 83 ++++++------------- public/app/partials/metrics.html | 58 +++++++++++-- 6 files changed, 80 insertions(+), 70 deletions(-) rename public/app/features/panel/{metrics_ds_selector.ts => metrics_tab.ts} (50%) diff --git a/docker/blocks/graphite/fig b/docker/blocks/graphite/fig index 60acb8c1131..b7e030e388e 100644 --- a/docker/blocks/graphite/fig +++ b/docker/blocks/graphite/fig @@ -4,7 +4,6 @@ graphite: - "8080:80" - "2003:2003" volumes: - - /var/docker/gfdev/graphite:/opt/graphite/storage/whisper - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro diff --git a/public/app/core/directives/plugin_component.ts b/public/app/core/directives/plugin_component.ts index 4c098f60a4c..3c797aede3e 100644 --- a/public/app/core/directives/plugin_component.ts +++ b/public/app/core/directives/plugin_component.ts @@ -109,7 +109,7 @@ function pluginDirectiveLoader($compile, datasourceSrv, $rootScope, $q, $http, $ baseUrl: ds.meta.baseUrl, name: 'query-ctrl-' + ds.meta.id, bindings: {target: "=", panelCtrl: "=", datasource: "="}, - attrs: {"target": "target", "panel-ctrl": "ctrl", datasource: "datasource"}, + attrs: {"target": "target", "panel-ctrl": "ctrl.panelCtrl", datasource: "datasource"}, Component: dsModule.QueryCtrl }; }); @@ -127,7 +127,7 @@ function pluginDirectiveLoader($compile, datasourceSrv, $rootScope, $q, $http, $ baseUrl: ds.meta.baseUrl, name: 'query-options-ctrl-' + ds.meta.id, bindings: {panelCtrl: "="}, - attrs: {"panel-ctrl": "ctrl"}, + attrs: {"panel-ctrl": "ctrl.panelCtrl"}, Component: dsModule.QueryOptionsCtrl }; }); diff --git a/public/app/features/panel/all.js b/public/app/features/panel/all.js index b4afba4da1b..cba296643ef 100644 --- a/public/app/features/panel/all.js +++ b/public/app/features/panel/all.js @@ -5,6 +5,5 @@ define([ './query_ctrl', './panel_editor_tab', './query_editor_row', - './metrics_ds_selector', './query_troubleshooter', ], function () {}); diff --git a/public/app/features/panel/metrics_panel_ctrl.ts b/public/app/features/panel/metrics_panel_ctrl.ts index 000705d74d5..ee1b45b4abf 100644 --- a/public/app/features/panel/metrics_panel_ctrl.ts +++ b/public/app/features/panel/metrics_panel_ctrl.ts @@ -10,6 +10,7 @@ import * as rangeUtil from 'app/core/utils/rangeutil'; import * as dateMath from 'app/core/utils/datemath'; import {Subject} from 'vendor/npm/rxjs/Subject'; +import {metricsTabDirective} from './metrics_tab'; class MetricsPanelCtrl extends PanelCtrl { scope: any; @@ -61,7 +62,7 @@ class MetricsPanelCtrl extends PanelCtrl { } private onInitMetricsPanelEditMode() { - this.addEditorTab('Metrics', 'public/app/partials/metrics.html'); + this.addEditorTab('Metrics', metricsTabDirective); this.addEditorTab('Time range', 'public/app/features/panel/partials/panelTime.html'); } diff --git a/public/app/features/panel/metrics_ds_selector.ts b/public/app/features/panel/metrics_tab.ts similarity index 50% rename from public/app/features/panel/metrics_ds_selector.ts rename to public/app/features/panel/metrics_tab.ts index 523268953a0..4fe96be052b 100644 --- a/public/app/features/panel/metrics_ds_selector.ts +++ b/public/app/features/panel/metrics_tab.ts @@ -1,59 +1,26 @@ /// -import angular from 'angular'; import _ from 'lodash'; -import appEvents from 'app/core/app_events'; +import {DashboardModel} from '../dashboard/model'; -var module = angular.module('grafana.directives'); - -var template = ` - -
-
-
- - - - -
- -
- - - -
- -
-
-`; - - -export class MetricsDsSelectorCtrl { +export class MetricsTabCtrl { dsSegment: any; mixedDsSegment: any; dsName: string; + panel: any; panelCtrl: any; datasources: any[]; current: any; - lastResponse: any; - responseData: any; - showResponse: boolean; + nextRefId: string; + dashboard: DashboardModel; /** @ngInject */ constructor($scope, private uiSegmentSrv, datasourceSrv) { + this.panelCtrl = $scope.ctrl; + $scope.ctrl = this; + + this.panel = this.panelCtrl.panel; + this.dashboard = this.panelCtrl.dashboard; this.datasources = datasourceSrv.getMetricSources(); var dsValue = this.panelCtrl.panel.datasource || null; @@ -70,9 +37,9 @@ export class MetricsDsSelectorCtrl { this.dsSegment = uiSegmentSrv.newSegment({value: this.current.name, selectMode: true}); this.mixedDsSegment = uiSegmentSrv.newSegment({value: 'Add Query', selectMode: true}); + this.nextRefId = this.getNextQueryLetter(); } - getOptions(includeBuiltin) { return Promise.resolve(this.datasources.filter(value => { return includeBuiltin || !value.meta.builtIn; @@ -86,7 +53,6 @@ export class MetricsDsSelectorCtrl { if (ds) { this.current = ds; this.panelCtrl.setDatasource(ds); - this.responseData = null; } } @@ -100,22 +66,27 @@ export class MetricsDsSelectorCtrl { } } + getNextQueryLetter() { + return this.dashboard.getNextQueryLetter(this.panel); + } + addDataQuery() { - var target: any = {isNew: true}; + var target: any = { + isNew: true, + refId: this.getNextQueryLetter() + }; this.panelCtrl.panel.targets.push(target); + this.nextRefId = this.getNextQueryLetter(); } } -module.directive('metricsDsSelector', function() { +/** @ngInject **/ +export function metricsTabDirective() { + 'use strict'; return { restrict: 'E', - template: template, - controller: MetricsDsSelectorCtrl, - bindToController: true, - controllerAs: 'ctrl', - transclude: true, - scope: { - panelCtrl: "=" - } + scope: true, + templateUrl: 'public/app/partials/metrics.html', + controller: MetricsTabCtrl, }; -}); +} diff --git a/public/app/partials/metrics.html b/public/app/partials/metrics.html index d3c6e6bcfc4..131c85996ae 100644 --- a/public/app/partials/metrics.html +++ b/public/app/partials/metrics.html @@ -1,5 +1,21 @@ - +
+
+
+ + + + + +
+
+
@@ -7,16 +23,40 @@ -
+
+ +
+
+ + + + +
+
- + -
- - - - -
+ + + +
+ + + + +
+ +
+
From 912301fe24ee3c24405434ed96bae108523e7d28 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Sat, 20 May 2017 10:14:41 +0200 Subject: [PATCH 09/23] query: more work on metrics tab changes --- .../app/features/panel/metrics_panel_ctrl.ts | 21 +++++++++++++++++++ public/app/features/panel/metrics_tab.ts | 21 +++++++------------ public/app/features/panel/panel_ctrl.ts | 12 ++++++++++- public/app/features/panel/query_editor_row.ts | 21 ++++--------------- public/app/partials/metrics.html | 8 ++----- public/sass/components/_gf-form.scss | 1 + public/sass/components/edit_sidemenu.scss | 1 - 7 files changed, 46 insertions(+), 39 deletions(-) diff --git a/public/app/features/panel/metrics_panel_ctrl.ts b/public/app/features/panel/metrics_panel_ctrl.ts index ee1b45b4abf..e3b4e9c77cf 100644 --- a/public/app/features/panel/metrics_panel_ctrl.ts +++ b/public/app/features/panel/metrics_panel_ctrl.ts @@ -1,5 +1,6 @@ /// +import angular from 'angular'; import config from 'app/core/config'; import $ from 'jquery'; import _ from 'lodash'; @@ -33,6 +34,7 @@ class MetricsPanelCtrl extends PanelCtrl { dataStream: any; dataSubscription: any; dataList: any; + nextRefId: string; constructor($scope, $injector) { super($scope, $injector); @@ -307,6 +309,25 @@ class MetricsPanelCtrl extends PanelCtrl { this.datasource = null; this.refresh(); } + + addQuery(target) { + target.refId = this.dashboard.getNextQueryLetter(this.panel); + + this.panel.targets.push(target); + this.nextRefId = this.dashboard.getNextQueryLetter(this.panel); + } + + removeQuery(target) { + var index = _.indexOf(this.panel.targets, target); + this.panel.targets.splice(index, 1); + this.nextRefId = this.dashboard.getNextQueryLetter(this.panel); + this.refresh(); + } + + moveQuery(target, direction) { + var index = _.indexOf(this.panel.targets, target); + _.move(this.panel.targets, index, index + direction); + } } export {MetricsPanelCtrl}; diff --git a/public/app/features/panel/metrics_tab.ts b/public/app/features/panel/metrics_tab.ts index 4fe96be052b..375c20b0ae0 100644 --- a/public/app/features/panel/metrics_tab.ts +++ b/public/app/features/panel/metrics_tab.ts @@ -36,8 +36,10 @@ export class MetricsTabCtrl { } this.dsSegment = uiSegmentSrv.newSegment({value: this.current.name, selectMode: true}); - this.mixedDsSegment = uiSegmentSrv.newSegment({value: 'Add Query', selectMode: true}); - this.nextRefId = this.getNextQueryLetter(); + this.mixedDsSegment = uiSegmentSrv.newSegment({value: 'Add Query', selectMode: true, fake: true}); + + // update next ref id + this.panelCtrl.nextRefId = this.dashboard.getNextQueryLetter(this.panel); } getOptions(includeBuiltin) { @@ -61,22 +63,13 @@ export class MetricsTabCtrl { var ds = _.find(this.datasources, {name: this.mixedDsSegment.value}); if (ds) { target.datasource = ds.name; - this.panelCtrl.panel.targets.push(target); + this.panelCtrl.addDataQuery(target); this.mixedDsSegment.value = ''; } } - getNextQueryLetter() { - return this.dashboard.getNextQueryLetter(this.panel); - } - - addDataQuery() { - var target: any = { - isNew: true, - refId: this.getNextQueryLetter() - }; - this.panelCtrl.panel.targets.push(target); - this.nextRefId = this.getNextQueryLetter(); + addQuery() { + this.panelCtrl.addQuery({isNew: true}); } } diff --git a/public/app/features/panel/panel_ctrl.ts b/public/app/features/panel/panel_ctrl.ts index 8de78291baa..a0645af43de 100644 --- a/public/app/features/panel/panel_ctrl.ts +++ b/public/app/features/panel/panel_ctrl.ts @@ -76,8 +76,18 @@ export class PanelCtrl { profiler.renderingCompleted(this.panel.id, this.timing); } + shouldSkipRefresh() { + // some scenarios we should never ignore refresh + if (this.fullscreen || this.dashboard.meta.soloMode || this.dashboard.snapshot) { + return false; + } + + return !this.isPanelVisible(); + } + refresh() { - if (!this.isPanelVisible() && !this.dashboard.meta.soloMode && !this.dashboard.snapshot) { + // somet + if (this.shouldSkipRefresh()) { this.skippedLastRefresh = true; return; } diff --git a/public/app/features/panel/query_editor_row.ts b/public/app/features/panel/query_editor_row.ts index 8249d84774f..1241d45db69 100644 --- a/public/app/features/panel/query_editor_row.ts +++ b/public/app/features/panel/query_editor_row.ts @@ -21,7 +21,7 @@ export class QueryRowCtrl { this.panel = this.panelCtrl.panel; if (!this.target.refId) { - this.target.refId = this.getNextQueryLetter(); + this.target.refId = this.panelCtrl.dashboard.getNextQueryLetter(this.panel); } this.toggleCollapse(true); @@ -40,16 +40,6 @@ export class QueryRowCtrl { this.panelCtrl.refresh(); } - getNextQueryLetter() { - var letters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'; - - return _.find(letters, refId => { - return _.every(this.panel.targets, function(other) { - return other.refId !== refId; - }); - }); - } - toggleCollapse(init) { if (!this.canCollapse) { return; @@ -87,19 +77,16 @@ export class QueryRowCtrl { delete this.panelCtrl.__collapsedQueryCache[this.target.refId]; } - this.panel.targets = _.without(this.panel.targets, this.target); - this.panelCtrl.refresh(); + this.panelCtrl.removeQuery(this.target); } duplicateQuery() { var clone = angular.copy(this.target); - clone.refId = this.getNextQueryLetter(); - this.panel.targets.push(clone); + this.panelCtrl.addQuery(clone); } moveQuery(direction) { - var index = _.indexOf(this.panel.targets, this.target); - _.move(this.panel.targets, index, index + direction); + this.panelCtrl.moveQuery(this.target, direction); } } diff --git a/public/app/partials/metrics.html b/public/app/partials/metrics.html index 131c85996ae..46f690bd775 100644 --- a/public/app/partials/metrics.html +++ b/public/app/partials/metrics.html @@ -2,10 +2,6 @@
- - @@ -31,9 +27,9 @@ - {{ctrl.nextRefId}} + {{ctrl.panelCtrl.nextRefId}} - diff --git a/public/sass/components/_gf-form.scss b/public/sass/components/_gf-form.scss index 9c88435fcc2..313f4ced3ed 100644 --- a/public/sass/components/_gf-form.scss +++ b/public/sass/components/_gf-form.scss @@ -118,6 +118,7 @@ $gf-form-margin: 0.25rem; padding: $input-padding-y $input-padding-x; margin-right: $gf-form-margin; font-size: $font-size-base; + margin-right: $gf-form-margin; line-height: $input-line-height; color: $input-color; background-color: $input-bg; diff --git a/public/sass/components/edit_sidemenu.scss b/public/sass/components/edit_sidemenu.scss index 5da2f6a21ce..e84ae2c1914 100644 --- a/public/sass/components/edit_sidemenu.scss +++ b/public/sass/components/edit_sidemenu.scss @@ -10,7 +10,6 @@ } .edit-sidemenu-aside { - min-width: 6rem; margin-right: $spacer*2; } From b8aa6a8e4725bbec7a8c75a67a61b0cc0131092d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Sat, 20 May 2017 10:48:47 +0200 Subject: [PATCH 10/23] query: more work on metrics tab changes --- public/app/core/directives/misc.js | 17 +++++++++++++- .../app/features/dashboard/shareModalCtrl.js | 17 +------------- .../features/panel/query_troubleshooter.ts | 22 +++++++++++++++---- public/sass/components/_collapse_box.scss | 7 ++++++ 4 files changed, 42 insertions(+), 21 deletions(-) diff --git a/public/app/core/directives/misc.js b/public/app/core/directives/misc.js index 8e1791af46c..ff005e76b64 100644 --- a/public/app/core/directives/misc.js +++ b/public/app/core/directives/misc.js @@ -1,9 +1,10 @@ define([ 'angular', + 'require', '../core_module', 'app/core/utils/kbn', ], -function (angular, coreModule, kbn) { +function (angular, require, coreModule, kbn) { 'use strict'; coreModule.default.directive('tip', function($compile) { @@ -18,6 +19,20 @@ function (angular, coreModule, kbn) { }; }); + coreModule.default.directive('clipboardButton',function() { + return function(scope, elem) { + require(['vendor/clipboard/dist/clipboard'], function(Clipboard) { + scope.clipboard = new Clipboard(elem[0]); + }); + + scope.$on('$destroy', function() { + if (scope.clipboard) { + scope.clipboard.destroy(); + } + }); + }; + }); + coreModule.default.directive('watchChange', function() { return { scope: { onchange: '&watchChange' }, diff --git a/public/app/features/dashboard/shareModalCtrl.js b/public/app/features/dashboard/shareModalCtrl.js index 6f3cb320bef..10f6a62fc3e 100644 --- a/public/app/features/dashboard/shareModalCtrl.js +++ b/public/app/features/dashboard/shareModalCtrl.js @@ -2,10 +2,9 @@ define(['angular', 'lodash', 'jquery', 'moment', - 'require', 'app/core/config', ], -function (angular, _, $, moment, require, config) { +function (angular, _, $, moment, config) { 'use strict'; var module = angular.module('grafana.controllers'); @@ -91,18 +90,4 @@ function (angular, _, $, moment, require, config) { }); - module.directive('clipboardButton',function() { - return function(scope, elem) { - require(['vendor/clipboard/dist/clipboard'], function(Clipboard) { - scope.clipboard = new Clipboard(elem[0]); - }); - - scope.$on('$destroy', function() { - if (scope.clipboard) { - scope.clipboard.destroy(); - } - }); - }; - }); - }); diff --git a/public/app/features/panel/query_troubleshooter.ts b/public/app/features/panel/query_troubleshooter.ts index 7d5e4dd0e9c..08388eb1b5e 100644 --- a/public/app/features/panel/query_troubleshooter.ts +++ b/public/app/features/panel/query_troubleshooter.ts @@ -8,7 +8,13 @@ const template = ` - Copy to clipboard + + Expand All + + + Collapse All + + Copy to Clipboard
@@ -24,6 +30,8 @@ export class QueryTroubleshooterCtrl { onRequestErrorEventListener: any; onRequestResponseEventListener: any; hasError: boolean; + allNodesExpanded: boolean; + jsonExplorer: JsonExplorer; /** @ngInject **/ constructor($scope, private $timeout) { @@ -46,7 +54,6 @@ export class QueryTroubleshooterCtrl { } stateChanged() { - console.log(this.isOpen); if (this.isOpen) { appEvents.on('ds-request-response', this.onRequestResponseEventListener); this.panelCtrl.refresh(); @@ -87,6 +94,13 @@ export class QueryTroubleshooterCtrl { this.$timeout(_.partial(this.renderJsonExplorer, data)); } + + toggleExpand(depth) { + if (this.jsonExplorer) { + this.allNodesExpanded = !this.allNodesExpanded; + this.jsonExplorer.openAtDepth(this.allNodesExpanded ? 20 : 1); + } + } } export function queryTroubleshooter() { @@ -104,11 +118,11 @@ export function queryTroubleshooter() { ctrl.renderJsonExplorer = function(data) { var jsonElem = elem.find('.query-troubleshooter-json'); - const formatter = new JsonExplorer(data, 3, { + ctrl.jsonExplorer = new JsonExplorer(data, 3, { theme: 'dark', }); - const html = formatter.render(true); + const html = ctrl.jsonExplorer.render(true); jsonElem.html(html); }; } diff --git a/public/sass/components/_collapse_box.scss b/public/sass/components/_collapse_box.scss index 96835ba9db9..38dbb3d8ca6 100644 --- a/public/sass/components/_collapse_box.scss +++ b/public/sass/components/_collapse_box.scss @@ -35,3 +35,10 @@ @include border-radius($label-border-radius-sm); } +.collapse-box__header-actions { + display: flex; + flex-direction: row; + a { + margin-left: $spacer; + } +} From 5e090b84ec5bed4a60cd466c96c6ee4a5c979171 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Sat, 20 May 2017 14:52:28 +0200 Subject: [PATCH 11/23] feat: Copy to clipboard now works in query troubleshooter --- public/app/core/directives/misc.js | 39 ++++++++++++------- .../dashboard/partials/shareModal.html | 4 +- .../app/features/dashboard/shareModalCtrl.js | 4 ++ .../features/dashboard/shareSnapshotCtrl.js | 4 ++ .../features/panel/query_troubleshooter.ts | 8 +++- 5 files changed, 41 insertions(+), 18 deletions(-) diff --git a/public/app/core/directives/misc.js b/public/app/core/directives/misc.js index ff005e76b64..82076eebb7f 100644 --- a/public/app/core/directives/misc.js +++ b/public/app/core/directives/misc.js @@ -19,17 +19,26 @@ function (angular, require, coreModule, kbn) { }; }); - coreModule.default.directive('clipboardButton',function() { - return function(scope, elem) { - require(['vendor/clipboard/dist/clipboard'], function(Clipboard) { - scope.clipboard = new Clipboard(elem[0]); - }); + coreModule.default.directive('clipboardButton', function() { + return { + scope: { + getText: '&clipboardButton' + }, + link: function(scope, elem) { + require(['vendor/clipboard/dist/clipboard'], function(Clipboard) { + scope.clipboard = new Clipboard(elem[0], { + text: function() { + return scope.getText(); + } + }); + }); - scope.$on('$destroy', function() { - if (scope.clipboard) { - scope.clipboard.destroy(); - } - }); + scope.$on('$destroy', function() { + if (scope.clipboard) { + scope.clipboard.destroy(); + } + }); + } }; }); @@ -78,10 +87,10 @@ function (angular, require, coreModule, kbn) { text + tip + ''; var template = - '' + - ' '; + '' + + ' '; template = template + label; elem.addClass('gf-form-checkbox'); @@ -106,7 +115,7 @@ function (angular, require, coreModule, kbn) { var li = '' + '' + (item.text || '') + ''; + '>' + (item.text || '') + ''; if (item.submenu && item.submenu.length) { li += buildTemplate(item.submenu).join('\n'); diff --git a/public/app/features/dashboard/partials/shareModal.html b/public/app/features/dashboard/partials/shareModal.html index f9bf5ccf4ad..382baec1aba 100644 --- a/public/app/features/dashboard/partials/shareModal.html +++ b/public/app/features/dashboard/partials/shareModal.html @@ -86,7 +86,7 @@
- +
@@ -143,7 +143,7 @@ {{snapshotUrl}}
- + diff --git a/public/app/features/dashboard/shareModalCtrl.js b/public/app/features/dashboard/shareModalCtrl.js index 10f6a62fc3e..211b0efbac9 100644 --- a/public/app/features/dashboard/shareModalCtrl.js +++ b/public/app/features/dashboard/shareModalCtrl.js @@ -88,6 +88,10 @@ function (angular, _, $, moment, config) { $scope.imageUrl += '&tz=UTC' + encodeURIComponent(moment().format("Z")); }; + $scope.getShareUrl = function() { + return $scope.shareUrl; + }; + }); }); diff --git a/public/app/features/dashboard/shareSnapshotCtrl.js b/public/app/features/dashboard/shareSnapshotCtrl.js index 846dc0f7ed7..a30caf7f1c9 100644 --- a/public/app/features/dashboard/shareSnapshotCtrl.js +++ b/public/app/features/dashboard/shareSnapshotCtrl.js @@ -96,6 +96,10 @@ function (angular, _) { }); }; + $scope.getSnapshotUrl = function() { + return $scope.snapshotUrl; + }; + $scope.scrubDashboard = function(dash) { // change title dash.title = $scope.snapshot.name; diff --git a/public/app/features/panel/query_troubleshooter.ts b/public/app/features/panel/query_troubleshooter.ts index 08388eb1b5e..658d3bce003 100644 --- a/public/app/features/panel/query_troubleshooter.ts +++ b/public/app/features/panel/query_troubleshooter.ts @@ -14,7 +14,7 @@ const template = ` Collapse All - Copy to Clipboard + Copy to Clipboard
@@ -62,6 +62,12 @@ export class QueryTroubleshooterCtrl { } } + getClipboardText() { + if (this.jsonExplorer) { + return JSON.stringify(this.jsonExplorer.json, null, 2); + } + } + onRequestResponse(data) { data = _.cloneDeep(data); From 499e01d8327fea2636e1519713ba7658b3bd0ef4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Sat, 20 May 2017 18:21:41 +0200 Subject: [PATCH 12/23] feat: metrics tab reworking --- public/app/features/panel/metrics_tab.ts | 5 +- public/app/features/panel/panel_editor_tab.ts | 7 +- .../app/features/panel/partials/metrics.html | 1 + .../panel/partials/metrics_tab.html} | 1 - .../features/panel/query_troubleshooter.ts | 8 +-- .../graphite/partials/query.options.html | 2 +- public/sass/_variables.dark.scss | 18 ++++++ public/sass/_variables.light.scss | 20 ++++++ public/sass/components/_collapse_box.scss | 24 +++---- public/sass/components/_json_explorer.scss | 64 +++++-------------- public/sass/grafana.dark.scss | 1 + 11 files changed, 81 insertions(+), 70 deletions(-) create mode 100644 public/app/features/panel/partials/metrics.html rename public/app/{partials/metrics.html => features/panel/partials/metrics_tab.html} (99%) diff --git a/public/app/features/panel/metrics_tab.ts b/public/app/features/panel/metrics_tab.ts index 375c20b0ae0..8f5e3dfdded 100644 --- a/public/app/features/panel/metrics_tab.ts +++ b/public/app/features/panel/metrics_tab.ts @@ -1,6 +1,7 @@ /// import _ from 'lodash'; +//import {coreModule} from 'app/core/core'; import {DashboardModel} from '../dashboard/model'; export class MetricsTabCtrl { @@ -79,7 +80,9 @@ export function metricsTabDirective() { return { restrict: 'E', scope: true, - templateUrl: 'public/app/partials/metrics.html', + templateUrl: 'public/app/features/panel/partials/metrics_tab.html', controller: MetricsTabCtrl, }; } + +//coreModule.directive('metricsTab', metricsTabDirective); diff --git a/public/app/features/panel/panel_editor_tab.ts b/public/app/features/panel/panel_editor_tab.ts index f6c11306678..9bbb979bcc0 100644 --- a/public/app/features/panel/panel_editor_tab.ts +++ b/public/app/features/panel/panel_editor_tab.ts @@ -16,10 +16,15 @@ function panelEditorTab(dynamicDirectiveSrv) { directive: scope => { var pluginId = scope.ctrl.pluginId; var tabIndex = scope.index; + // create a wrapper for directiveFn + // required for metrics tab directive + // that is the same for many panels but + // given different names in this function + var fn = () => scope.editorTab.directiveFn(); return Promise.resolve({ name: `panel-editor-tab-${pluginId}${tabIndex}`, - fn: scope.editorTab.directiveFn, + fn: fn, }); } }); diff --git a/public/app/features/panel/partials/metrics.html b/public/app/features/panel/partials/metrics.html new file mode 100644 index 00000000000..5d44948df4a --- /dev/null +++ b/public/app/features/panel/partials/metrics.html @@ -0,0 +1 @@ + diff --git a/public/app/partials/metrics.html b/public/app/features/panel/partials/metrics_tab.html similarity index 99% rename from public/app/partials/metrics.html rename to public/app/features/panel/partials/metrics_tab.html index 46f690bd775..9ecfd769f7a 100644 --- a/public/app/partials/metrics.html +++ b/public/app/features/panel/partials/metrics_tab.html @@ -1,4 +1,3 @@ -
diff --git a/public/app/features/panel/query_troubleshooter.ts b/public/app/features/panel/query_troubleshooter.ts index 658d3bce003..bbbe0e70935 100644 --- a/public/app/features/panel/query_troubleshooter.ts +++ b/public/app/features/panel/query_troubleshooter.ts @@ -9,10 +9,10 @@ const template = ` ng-class="{'collapse-box--error': ctrl.hasError}"> - Expand All + Expand All - Collapse All + Collapse All Copy to Clipboard @@ -124,9 +124,7 @@ export function queryTroubleshooter() { ctrl.renderJsonExplorer = function(data) { var jsonElem = elem.find('.query-troubleshooter-json'); - ctrl.jsonExplorer = new JsonExplorer(data, 3, { - theme: 'dark', - }); + ctrl.jsonExplorer = new JsonExplorer(data, 3, { }); const html = ctrl.jsonExplorer.render(true); jsonElem.html(html); diff --git a/public/app/plugins/datasource/graphite/partials/query.options.html b/public/app/plugins/datasource/graphite/partials/query.options.html index 05aecf4b44a..22c7d8c0be8 100644 --- a/public/app/plugins/datasource/graphite/partials/query.options.html +++ b/public/app/plugins/datasource/graphite/partials/query.options.html @@ -1,4 +1,4 @@ -
+
diff --git a/public/sass/_variables.dark.scss b/public/sass/_variables.dark.scss index 7e7865b6b88..c6bee53b14b 100644 --- a/public/sass/_variables.dark.scss +++ b/public/sass/_variables.dark.scss @@ -280,3 +280,21 @@ $card-shadow: -1px -1px 0 0 hsla(0, 0%, 100%, .1), 1px 1px 0 0 rgba(0, 0, 0, .3) $footer-link-color: $gray-1; $footer-link-hover: $gray-4; +// collapse box +$collapse-box-body-border: $dark-5; +$collapse-box-body-error-border: $red; + +// json-explorer +$json-explorer-default-color: white; +$json-explorer-string-color: #31F031; +$json-explorer-number-color: #66C2FF; +$json-explorer-boolean-color: #EC4242; +$json-explorer-null-color: #EEC97D; +$json-explorer-undefined-color: rgb(239, 143, 190); +$json-explorer-function-color: #FD48CB; +$json-explorer-rotate-time: 100ms; +$json-explorer-toggler-opacity: 0.6; +$json-explorer-toggler-color: #45376F; +$json-explorer-bracket-color: #9494FF; +$json-explorer-key-color: #23A0DB; +$json-explorer-url-color: #027BFF; diff --git a/public/sass/_variables.light.scss b/public/sass/_variables.light.scss index a10f94afc1b..de23ae512f1 100644 --- a/public/sass/_variables.light.scss +++ b/public/sass/_variables.light.scss @@ -303,3 +303,23 @@ $card-shadow: -1px -1px 0 0 hsla(0, 0%, 100%, .1), 1px 1px 0 0 rgba(0, 0, 0, .1) // footer $footer-link-color: $gray-3; $footer-link-hover: $dark-5; + +// collapse box +$collapse-box-body-border: $gray-4; +$collapse-box-body-error-border: $red; + +// json explorer +$json-explorer-default-color: black; +$json-explorer-string-color: green; +$json-explorer-number-color: blue; +$json-explorer-boolean-color: red; +$json-explorer-null-color: #855A00; +$json-explorer-undefined-color: rgb(202, 11, 105); +$json-explorer-function-color: #FF20ED; +$json-explorer-rotate-time: 100ms; +$json-explorer-toggler-opacity: 0.6; +$json-explorer-toggler-color: #45376F; +$json-explorer-bracket-color: blue; +$json-explorer-key-color: #00008B; +$json-explorer-url-color: blue; + diff --git a/public/sass/components/_collapse_box.scss b/public/sass/components/_collapse_box.scss index 38dbb3d8ca6..a8a5db46abe 100644 --- a/public/sass/components/_collapse_box.scss +++ b/public/sass/components/_collapse_box.scss @@ -1,12 +1,5 @@ .collapse-box { margin-bottom: $spacer; - - &--error { - .collapse-box__header { - background-color: $red; - color: $white; - } - } } .collapse-box__header { @@ -14,12 +7,15 @@ flex-direction: row; padding: $input-padding-y $input-padding-x; margin-right: $gf-form-margin; - background-color: $input-bg; + background-color: $input-label-bg; font-size: $font-size-sm; margin-right: $gf-form-margin; - - border: $input-btn-border-width solid transparent; + border: $input-btn-border-width solid $collapse-box-body-border; @include border-radius($label-border-radius-sm); + + &--error { + border-color: $collapse-box-body-error-border; + } } .collapse-box__header-title { @@ -28,11 +24,15 @@ .collapse-box__body { padding: $input-padding-y*2 $input-padding-x; - background-color: $input-label-bg; display: block; margin-right: $gf-form-margin; - border: $input-btn-border-width solid transparent; + border: $input-btn-border-width solid $collapse-box-body-border; + border-top: none; @include border-radius($label-border-radius-sm); + + &--error { + border-color: $collapse-box-body-error-border; + } } .collapse-box__header-actions { diff --git a/public/sass/components/_json_explorer.scss b/public/sass/components/_json_explorer.scss index d372c332176..b6a2f089db6 100644 --- a/public/sass/components/_json_explorer.scss +++ b/public/sass/components/_json_explorer.scss @@ -1,21 +1,9 @@ -@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) { +.json-formatter-row { font-family: monospace; + &, a, a:hover { - color: $default-color; + color: $json-explorer-default-color; text-decoration: none; } @@ -35,25 +23,25 @@ } .json-formatter-string { - color: $string-color; + color: $json-explorer-string-color; white-space: normal; 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-number { color: $json-explorer-number-color; } + .json-formatter-boolean { color: $json-explorer-boolean-color; } + .json-formatter-null { color: $json-explorer-null-color; } + .json-formatter-undefined { color: $json-explorer-undefined-color; } + .json-formatter-function { color: $json-explorer-function-color; } + .json-formatter-date { background-color: fade($json-explorer-default-color, 5%); } .json-formatter-url { text-decoration: underline; - color: $url-color; + color: $json-explorer-url-color; cursor: pointer; } - .json-formatter-bracket { color: $bracket-color; } + .json-formatter-bracket { color: $json-explorer-bracket-color; } .json-formatter-key { - color: $key-color; + color: $json-explorer-key-color; cursor: pointer; padding-right: 0.2rem; } @@ -65,13 +53,13 @@ line-height: 1.2rem; font-size: 0.7rem; vertical-align: middle; - opacity: $toggler-opacity; + opacity: $json-explorer-toggler-opacity; cursor: pointer; padding-right: 0.2rem; &::after { display: inline-block; - transition: transform $rotate-time ease-in; + transition: transform $json-explorer-rotate-time ease-in; content: "►"; } } @@ -104,25 +92,3 @@ } } - -.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/sass/grafana.dark.scss b/public/sass/grafana.dark.scss index 53193d213e6..858d6ace336 100644 --- a/public/sass/grafana.dark.scss +++ b/public/sass/grafana.dark.scss @@ -1,3 +1,4 @@ @import "variables"; @import "variables.dark"; @import "grafana"; + From 4a2c405ac0001be46f98a31624a83f74ac5c12ba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Sat, 20 May 2017 18:35:34 +0200 Subject: [PATCH 13/23] feat: metris tab, moved data source selector --- .../app/features/panel/partials/metrics.html | 1 - .../features/panel/partials/metrics_tab.html | 33 ++++++++----------- 2 files changed, 14 insertions(+), 20 deletions(-) delete mode 100644 public/app/features/panel/partials/metrics.html diff --git a/public/app/features/panel/partials/metrics.html b/public/app/features/panel/partials/metrics.html deleted file mode 100644 index 5d44948df4a..00000000000 --- a/public/app/features/panel/partials/metrics.html +++ /dev/null @@ -1 +0,0 @@ - diff --git a/public/app/features/panel/partials/metrics_tab.html b/public/app/features/panel/partials/metrics_tab.html index 9ecfd769f7a..5cffbc19666 100644 --- a/public/app/features/panel/partials/metrics_tab.html +++ b/public/app/features/panel/partials/metrics_tab.html @@ -1,17 +1,3 @@ -
-
-
- - - -
-
-
-
@@ -41,16 +27,25 @@ - +
- - - - +
+
+ + +
+
+ + + + +
From a8673a2e330bf9c33a7c071f6d7289ce94eb7f8b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Sat, 20 May 2017 22:41:34 +0200 Subject: [PATCH 14/23] feat: metrics tab --- .../features/panel/partials/metrics_tab.html | 2 +- .../app/features/panel/query_troubleshooter.ts | 15 +++++++++++++-- public/sass/components/_collapse_box.scss | 18 ++++++++++-------- 3 files changed, 24 insertions(+), 11 deletions(-) diff --git a/public/app/features/panel/partials/metrics_tab.html b/public/app/features/panel/partials/metrics_tab.html index 5cffbc19666..b9e704bbcca 100644 --- a/public/app/features/panel/partials/metrics_tab.html +++ b/public/app/features/panel/partials/metrics_tab.html @@ -27,7 +27,7 @@ - +
diff --git a/public/app/features/panel/query_troubleshooter.ts b/public/app/features/panel/query_troubleshooter.ts index bbbe0e70935..68c2bf35c10 100644 --- a/public/app/features/panel/query_troubleshooter.ts +++ b/public/app/features/panel/query_troubleshooter.ts @@ -17,6 +17,7 @@ const template = ` Copy to Clipboard +
@@ -24,6 +25,7 @@ const template = ` export class QueryTroubleshooterCtrl { isOpen: any; + isLoading: boolean; showResponse: boolean; panelCtrl: any; renderJsonExplorer: (data) => void; @@ -57,8 +59,7 @@ export class QueryTroubleshooterCtrl { if (this.isOpen) { appEvents.on('ds-request-response', this.onRequestResponseEventListener); this.panelCtrl.refresh(); - } else { - this.hasError = false; + this.isLoading = true; } } @@ -69,6 +70,7 @@ export class QueryTroubleshooterCtrl { } onRequestResponse(data) { + this.isLoading = false; data = _.cloneDeep(data); if (data.headers) { @@ -92,6 +94,15 @@ export class QueryTroubleshooterCtrl { if (data.data) { data.response = data.data; + if (data.status === 200) { + // if we are in error state, assume we automatically opened + // and auto close it again + if (this.hasError) { + this.hasError = false; + this.isOpen = false; + } + } + delete data.data; delete data.status; delete data.statusText; diff --git a/public/sass/components/_collapse_box.scss b/public/sass/components/_collapse_box.scss index a8a5db46abe..86658cfcfec 100644 --- a/public/sass/components/_collapse_box.scss +++ b/public/sass/components/_collapse_box.scss @@ -1,5 +1,15 @@ .collapse-box { margin-bottom: $spacer; + + &--error { + .collapse-box__header { + border-color: $collapse-box-body-error-border; + } + .collapse-box__body { + border-color: $collapse-box-body-error-border; + } + } + } .collapse-box__header { @@ -12,10 +22,6 @@ margin-right: $gf-form-margin; border: $input-btn-border-width solid $collapse-box-body-border; @include border-radius($label-border-radius-sm); - - &--error { - border-color: $collapse-box-body-error-border; - } } .collapse-box__header-title { @@ -29,10 +35,6 @@ border: $input-btn-border-width solid $collapse-box-body-border; border-top: none; @include border-radius($label-border-radius-sm); - - &--error { - border-color: $collapse-box-body-error-border; - } } .collapse-box__header-actions { From d840645dd742054c8b4bf6146220576e1feb96be Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Sat, 20 May 2017 22:55:36 +0200 Subject: [PATCH 15/23] feat: metrics tab, minor change --- public/app/core/services/backend_srv.ts | 2 +- public/app/features/panel/query_troubleshooter.ts | 11 +++++++++-- public/sass/_variables.dark.scss | 6 +++--- 3 files changed, 13 insertions(+), 6 deletions(-) diff --git a/public/app/core/services/backend_srv.ts b/public/app/core/services/backend_srv.ts index 85cc63d057f..5965793d738 100644 --- a/public/app/core/services/backend_srv.ts +++ b/public/app/core/services/backend_srv.ts @@ -170,7 +170,7 @@ export class BackendSrv { }); } - //populate error obj on Internal Error + // populate error obj on Internal Error if (_.isString(err.data) && err.status === 500) { err.data = { error: err.statusText, diff --git a/public/app/features/panel/query_troubleshooter.ts b/public/app/features/panel/query_troubleshooter.ts index 68c2bf35c10..84ae2045dc7 100644 --- a/public/app/features/panel/query_troubleshooter.ts +++ b/public/app/features/panel/query_troubleshooter.ts @@ -40,6 +40,7 @@ export class QueryTroubleshooterCtrl { this.onRequestErrorEventListener = this.onRequestError.bind(this); this.onRequestResponseEventListener = this.onRequestResponse.bind(this); + appEvents.on('ds-request-response', this.onRequestResponseEventListener); appEvents.on('ds-request-error', this.onRequestErrorEventListener); $scope.$on('$destroy', this.removeEventsListeners.bind(this)); } @@ -57,7 +58,6 @@ export class QueryTroubleshooterCtrl { stateChanged() { if (this.isOpen) { - appEvents.on('ds-request-response', this.onRequestResponseEventListener); this.panelCtrl.refresh(); this.isLoading = true; } @@ -70,6 +70,11 @@ export class QueryTroubleshooterCtrl { } onRequestResponse(data) { + // ignore if closed + if (!this.isOpen) { + return; + } + this.isLoading = false; data = _.cloneDeep(data); @@ -135,7 +140,9 @@ export function queryTroubleshooter() { ctrl.renderJsonExplorer = function(data) { var jsonElem = elem.find('.query-troubleshooter-json'); - ctrl.jsonExplorer = new JsonExplorer(data, 3, { }); + ctrl.jsonExplorer = new JsonExplorer(data, 3, { + animateOpen: true, + }); const html = ctrl.jsonExplorer.render(true); jsonElem.html(html); diff --git a/public/sass/_variables.dark.scss b/public/sass/_variables.dark.scss index c6bee53b14b..30b769c3cda 100644 --- a/public/sass/_variables.dark.scss +++ b/public/sass/_variables.dark.scss @@ -286,9 +286,9 @@ $collapse-box-body-error-border: $red; // json-explorer $json-explorer-default-color: white; -$json-explorer-string-color: #31F031; -$json-explorer-number-color: #66C2FF; -$json-explorer-boolean-color: #EC4242; +$json-explorer-string-color: #23d662; +$json-explorer-number-color: $variable; +$json-explorer-boolean-color: $variable; $json-explorer-null-color: #EEC97D; $json-explorer-undefined-color: rgb(239, 143, 190); $json-explorer-function-color: #FD48CB; From 7cb64662513f1470f3149b51de07729336f2a6f4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Sat, 20 May 2017 23:43:59 +0200 Subject: [PATCH 16/23] feat: query troubleshooter, improving json explorer --- .../components/json_explorer/json_explorer.ts | 103 +++++++----------- public/sass/_variables.dark.scss | 2 +- public/sass/components/_json_explorer.scss | 4 + 3 files changed, 44 insertions(+), 65 deletions(-) diff --git a/public/app/core/components/json_explorer/json_explorer.ts b/public/app/core/components/json_explorer/json_explorer.ts index 4b57268c663..c2810f23b54 100644 --- a/public/app/core/components/json_explorer/json_explorer.ts +++ b/public/app/core/components/json_explorer/json_explorer.ts @@ -11,6 +11,8 @@ import { 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/; @@ -21,18 +23,12 @@ 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 @@ -53,6 +49,8 @@ export class JsonExplorer { // 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. @@ -82,17 +80,6 @@ export class JsonExplorer { * 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; - } } /* @@ -236,54 +223,48 @@ export class JsonExplorer { } } - /** - * 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}}`; - } + isNumberArray() { + return (this.json.length > 0 && this.json.length < 4) && + (_.isNumber(this.json[0]) || _.isNumber(this.json[1])); } + renderArray() { + const arrayWrapperSpan = createElement('span'); + arrayWrapperSpan.appendChild(createElement('span', 'bracket', '[')); + + // some pretty handling of number arrays + if (this.isNumberArray()) { + this.json.forEach((val, index) => { + if (index > 0) { + arrayWrapperSpan.appendChild(createElement('span', 'array-comma', ',')); + } + arrayWrapperSpan.appendChild(createElement('span', 'number', val)); + }); + this.skipChildren = true; + } else { + arrayWrapperSpan.appendChild(createElement('span', 'number', (this.json.length))); + } + + arrayWrapperSpan.appendChild(createElement('span', 'bracket', ']')); + return arrayWrapperSpan; + } /** * Renders an HTML element and installs event listeners * * @returns {HTMLDivElement} - */ + */ render(skipRoot = false): HTMLDivElement { - // construct the root element and assign it to this.element this.element = createElement('div', 'row'); // construct the toggler link const togglerLink = createElement('a', 'toggler-link'); + const togglerIcon = createElement('span', 'toggler'); // if this is an object we need a wrapper span (toggler) if (this.isObject) { - togglerLink.appendChild(createElement('span', 'toggler')); + togglerLink.appendChild(togglerIcon); } // if this is child of a parent formatter we need to append the key @@ -293,7 +274,6 @@ export class JsonExplorer { // Value for objects and arrays if (this.isObject) { - // construct the value holder element const value = createElement('span', 'value'); @@ -306,18 +286,14 @@ export class JsonExplorer { // 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', ']')); + const arrayWrapperSpan = this.renderArray(); objectWrapperSpan.appendChild(arrayWrapperSpan); } // append object wrapper span to toggler link value.appendChild(objectWrapperSpan); togglerLink.appendChild(value); - - // Primitive values + // Primitive values } else { // make a value holder element @@ -341,13 +317,6 @@ export class JsonExplorer { 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'); @@ -374,7 +343,13 @@ export class JsonExplorer { if (!skipRoot) { this.element.appendChild(togglerLink); } - this.element.appendChild(children); + + 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) { diff --git a/public/sass/_variables.dark.scss b/public/sass/_variables.dark.scss index 30b769c3cda..2a9d5256252 100644 --- a/public/sass/_variables.dark.scss +++ b/public/sass/_variables.dark.scss @@ -285,7 +285,7 @@ $collapse-box-body-border: $dark-5; $collapse-box-body-error-border: $red; // json-explorer -$json-explorer-default-color: white; +$json-explorer-default-color: $text-color; $json-explorer-string-color: #23d662; $json-explorer-number-color: $variable; $json-explorer-boolean-color: $variable; diff --git a/public/sass/components/_json_explorer.scss b/public/sass/components/_json_explorer.scss index b6a2f089db6..37c610b2002 100644 --- a/public/sass/components/_json_explorer.scss +++ b/public/sass/components/_json_explorer.scss @@ -44,11 +44,15 @@ color: $json-explorer-key-color; cursor: pointer; padding-right: 0.2rem; + margin-right: 4px; } + .json-formatter-constructor-name { cursor: pointer; } + .json-formatter-array-comma { margin-right: 4px; } + .json-formatter-toggler { line-height: 1.2rem; font-size: 0.7rem; From 6992b484bc461cc0d2c02bafb621e1a283ce02db Mon Sep 17 00:00:00 2001 From: Daniel Lee Date: Fri, 9 Jun 2017 14:27:27 +0200 Subject: [PATCH 17/23] docs: improvements to building the docs readme Explains that you need to clone the grafana repo if you have not done that already. --- docs/README.md | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/docs/README.md b/docs/README.md index 7aeab15888b..ff5ef6a4131 100644 --- a/docs/README.md +++ b/docs/README.md @@ -18,11 +18,28 @@ make docs-build **Build the Documentation**: Now that the docker image has been prepared we can build the -grafana docs and start a docs server. Switch your working directory back to the directory this file +grafana docs and start a docs server. + +If you have not cloned the Grafana repository already then: + +``` +cd .. +git clone https://github.com/grafana/grafana +``` + +Switch your working directory to the directory this file (README.md) is in. +``` +cd grafana/docs +``` + An AWS config file is required to build the docs Docker image and to publish the site to AWS. If you are building locally only and do not have any AWS credentials for docs.grafana.org then create an empty file named `awsconfig` in the current directory. +``` +touch awsconfig +``` + Then run (possibly with ``sudo``): ``` From d10d897d6546f45a36f338ec9aca276d061fc0f7 Mon Sep 17 00:00:00 2001 From: Martin Molnar Date: Mon, 12 Jun 2017 15:11:00 +0200 Subject: [PATCH 18/23] fix: component name of plugin page contains 'undefined' (#8590) --- public/app/core/directives/plugin_component.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/app/core/directives/plugin_component.ts b/public/app/core/directives/plugin_component.ts index 4c098f60a4c..22c83f9b557 100644 --- a/public/app/core/directives/plugin_component.ts +++ b/public/app/core/directives/plugin_component.ts @@ -181,7 +181,7 @@ function pluginDirectiveLoader($compile, datasourceSrv, $rootScope, $q, $http, $ return System.import(appModel.module).then(function(appModule) { return { baseUrl: appModel.baseUrl, - name: 'app-page-' + appModel.appId + '-' + scope.ctrl.page.slug, + name: 'app-page-' + appModel.id + '-' + scope.ctrl.page.slug, bindings: {appModel: "="}, attrs: {"app-model": "ctrl.appModel"}, Component: appModule[scope.ctrl.page.component], From 192c447c2ce05367dbe1b66e68b5c451bdbb9abe Mon Sep 17 00:00:00 2001 From: Trent White Date: Mon, 12 Jun 2017 11:17:00 -0400 Subject: [PATCH 19/23] create new auth icon for grafana.com so it doesn't share the same file as the main logo (#8581) --- public/app/partials/login.html | 2 +- public/img/grafana_com_auth_icon.svg | 57 +++++++++++++ public/img/grafana_icon.svg | 115 +++++++++++++-------------- public/sass/pages/_login.scss | 1 + 4 files changed, 116 insertions(+), 59 deletions(-) create mode 100644 public/img/grafana_com_auth_icon.svg diff --git a/public/app/partials/login.html b/public/app/partials/login.html index 38517951cea..fe19d34f8d5 100644 --- a/public/app/partials/login.html +++ b/public/app/partials/login.html @@ -60,7 +60,7 @@ GitHub - + Grafana.com diff --git a/public/img/grafana_com_auth_icon.svg b/public/img/grafana_com_auth_icon.svg new file mode 100644 index 00000000000..72702223dc7 --- /dev/null +++ b/public/img/grafana_com_auth_icon.svg @@ -0,0 +1,57 @@ + + + + + + + + + + + + diff --git a/public/img/grafana_icon.svg b/public/img/grafana_icon.svg index 8616d369046..72702223dc7 100644 --- a/public/img/grafana_icon.svg +++ b/public/img/grafana_icon.svg @@ -1,58 +1,57 @@ - - - - - - - - - - - - - - + + + + + + + + + + + + diff --git a/public/sass/pages/_login.scss b/public/sass/pages/_login.scss index 67218f4dc71..2b3426b45b5 100644 --- a/public/sass/pages/_login.scss +++ b/public/sass/pages/_login.scss @@ -119,6 +119,7 @@ img { width: 19px; + vertical-align: sub; } } } From 7d642546b3fcf8ac8876fa46ee42d394670b832b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Tue, 13 Jun 2017 13:21:22 -0400 Subject: [PATCH 20/23] fix: restore dashboard history version did not reload route correctly when slug did not change --- public/app/features/dashboard/history/history.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/public/app/features/dashboard/history/history.ts b/public/app/features/dashboard/history/history.ts index 10ab3f3fa28..f199d84ffb4 100644 --- a/public/app/features/dashboard/history/history.ts +++ b/public/app/features/dashboard/history/history.ts @@ -27,6 +27,7 @@ export class HistoryListCtrl { /** @ngInject */ constructor(private $scope, + private $route, private $rootScope, private $location, private $window, @@ -179,6 +180,7 @@ export class HistoryListCtrl { this.loading = true; return this.historySrv.restoreDashboard(this.dashboard, version).then(response => { this.$location.path('dashboard/db/' + response.slug); + this.$route.reload(); this.$rootScope.appEvent('alert-success', ['Dashboard restored', 'Restored from version ' + version]); }).catch(() => { this.mode = 'list'; From 1f92e589e816f50fc3b5e3a4a60e87521bdb96cf Mon Sep 17 00:00:00 2001 From: Daniel Lee Date: Tue, 13 Jun 2017 22:42:56 +0200 Subject: [PATCH 21/23] exporter: query template var keeps refresh value.. on export if the value is not set to never. Otherwise the template variable will not be populated with any values."" --- public/app/features/dashboard/export/exporter.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/app/features/dashboard/export/exporter.ts b/public/app/features/dashboard/export/exporter.ts index 0a0b4cbdaab..d76e782905b 100644 --- a/public/app/features/dashboard/export/exporter.ts +++ b/public/app/features/dashboard/export/exporter.ts @@ -103,7 +103,7 @@ export class DashboardExporter { templateizeDatasourceUsage(variable); variable.options = []; variable.current = {}; - variable.refresh = 1; + variable.refresh = variable.refresh > 0 ? variable.refresh : 1; } } From cb720d8eafc1f55ebef8c9575bc687335126246c Mon Sep 17 00:00:00 2001 From: Daniel Lee Date: Tue, 13 Jun 2017 23:17:14 +0200 Subject: [PATCH 22/23] docs: add body options for snapshot api --- docs/sources/http_api/snapshot.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/docs/sources/http_api/snapshot.md b/docs/sources/http_api/snapshot.md index f0b8304cd24..d466d01e051 100644 --- a/docs/sources/http_api/snapshot.md +++ b/docs/sources/http_api/snapshot.md @@ -52,6 +52,15 @@ parent = "http_api" "expires": 3600 } +JSON Body schema: + +- **dashboard** – Required. The complete dashboard model. +- **name** – Optional. snapshot name +- **expires** - Optional. When the snapshot should expire in seconds. 3600 is 1 hour, 86400 is 1 day. Default is never to expire. +- **external** - Optional. Save the snapshot on an external server rather than locally. Default is `false`. +- **key** - Optional. Define the unique key. Required if **external** is `true`. +- **deleteKey** - Optional. Unique key used to delete the snapshot. It is different from the **key** so that only the creator can delete the snapshot. Required if **external** is `true`. + **Example Response**: HTTP/1.1 200 From c771dd4bd27a22c319a69575dd2a1a0548aceedd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Tue, 13 Jun 2017 18:31:43 -0400 Subject: [PATCH 23/23] ux: metrics tab add query feature --- public/app/features/panel/metrics_tab.ts | 10 +++- .../features/panel/partials/metrics_tab.html | 55 ++++++++----------- 2 files changed, 32 insertions(+), 33 deletions(-) diff --git a/public/app/features/panel/metrics_tab.ts b/public/app/features/panel/metrics_tab.ts index 8f5e3dfdded..f2d99738040 100644 --- a/public/app/features/panel/metrics_tab.ts +++ b/public/app/features/panel/metrics_tab.ts @@ -62,11 +62,17 @@ export class MetricsTabCtrl { mixedDatasourceChanged() { var target: any = {isNew: true}; var ds = _.find(this.datasources, {name: this.mixedDsSegment.value}); + if (ds) { target.datasource = ds.name; - this.panelCtrl.addDataQuery(target); - this.mixedDsSegment.value = ''; + this.panelCtrl.addQuery(target); } + + // metric segments are really bad, requires hacks to update + const segment = this.uiSegmentSrv.newSegment({value: 'Add Query', selectMode: true, fake: true}); + this.mixedDsSegment.value = segment.value; + this.mixedDsSegment.html = segment.html; + this.mixedDsSegment.text = segment.text; } addQuery() { diff --git a/public/app/features/panel/partials/metrics_tab.html b/public/app/features/panel/partials/metrics_tab.html index b9e704bbcca..a020a2edbd1 100644 --- a/public/app/features/panel/partials/metrics_tab.html +++ b/public/app/features/panel/partials/metrics_tab.html @@ -14,39 +14,32 @@ {{ctrl.panelCtrl.nextRefId}} - + - -
-
+ +
+
- + - - +
+
+
+ + +
+
+
-
-
-
- - -
-
-
- - - - - - -
-
- -
+ + + + +