From b65642564a311f07b572ce2e1d63e4b83cb20903 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Sat, 8 Oct 2016 10:06:47 +0200 Subject: [PATCH 01/26] poc for new metric segment --- .../form_dropdown/form_dropdown.html | 13 ++ .../components/form_dropdown/form_dropdown.ts | 182 ++++++++++++++++++ public/app/core/core.ts | 2 + public/app/core/directives/metric_segment.js | 2 - .../elasticsearch/partials/bucket_agg.html | 3 +- .../elasticsearch/partials/metric_agg.html | 6 +- 6 files changed, 202 insertions(+), 6 deletions(-) create mode 100644 public/app/core/components/form_dropdown/form_dropdown.html create mode 100644 public/app/core/components/form_dropdown/form_dropdown.ts diff --git a/public/app/core/components/form_dropdown/form_dropdown.html b/public/app/core/components/form_dropdown/form_dropdown.html new file mode 100644 index 00000000000..34737eab27e --- /dev/null +++ b/public/app/core/components/form_dropdown/form_dropdown.html @@ -0,0 +1,13 @@ + + + + diff --git a/public/app/core/components/form_dropdown/form_dropdown.ts b/public/app/core/components/form_dropdown/form_dropdown.ts new file mode 100644 index 00000000000..fd363d3b75e --- /dev/null +++ b/public/app/core/components/form_dropdown/form_dropdown.ts @@ -0,0 +1,182 @@ +/// + +import config from 'app/core/config'; +import _ from 'lodash'; +import $ from 'jquery'; +import coreModule from '../../core_module'; + +function typeaheadMatcher(item) { + var str = this.query; + if (str[0] === '/') { str = str.substring(1); } + if (str[str.length - 1] === '/') { str = str.substring(0, str.length-1); } + return item.toLowerCase().match(str.toLowerCase()); +} + +export class FormDropdownCtrl { + inputElement: any; + linkElement: any; + value: any; + text: any; + display: any; + options: any; + cssClass: any; + allowCustom: any; + linkMode: boolean; + cancelBlur: any; + onChange: any; + + constructor(private $scope, $element, private $sce, private templateSrv) { + this.inputElement = $element.find('input').first(); + this.linkElement = $element.find('a').first(); + this.linkMode = true; + this.cancelBlur = null; + + if (this.options) { + var item = _.find(this.options, {value: this.value}); + this.updateDisplay(item ? item.text : this.value); + } + + this.inputElement.attr('data-provide', 'typeahead'); + this.inputElement.typeahead({ + source: this.typeaheadSource.bind(this), + minLength: 0, + items: 10000, + updater: this.typeaheadUpdater.bind(this), + matcher: typeaheadMatcher, + }); + + // modify typeahead lookup + // this = typeahead + var typeahead = this.inputElement.data('typeahead'); + typeahead.lookup = function () { + this.query = this.$element.val() || ''; + var items = this.source(this.query, $.proxy(this.process, this)); + return items ? this.process(items) : items; + }; + + this.linkElement.keydown(evt => { + // trigger typeahead on down arrow or enter key + if (evt.keyCode === 40 || evt.keyCode === 13) { + this.linkElement.click(); + } + }); + + this.inputElement.blur(this.inputBlur.bind(this)); + } + + typeaheadSource(query, callback) { + if (this.options) { + var typeaheadOptions = _.map(this.options, 'text'); + + // add current custom value + if (this.allowCustom) { + if (_.indexOf(typeaheadOptions, this.text) === -1) { + typeaheadOptions.unshift(this.text); + } + } + + callback(typeaheadOptions); + } + } + + typeaheadUpdater(text) { + if (text === this.text) { + clearTimeout(this.cancelBlur); + this.inputElement.focus(); + return text; + } + + this.inputElement.val(text); + this.switchToLink(true); + return text; + } + + switchToLink(fromClick) { + if (this.linkMode && !fromClick) { return; } + + clearTimeout(this.cancelBlur); + this.cancelBlur = null; + this.linkMode = true; + this.inputElement.hide(); + this.linkElement.show(); + this.updateValue(this.inputElement.val()); + } + + inputBlur() { + // happens long before the click event on the typeahead options + // need to have long delay because the blur + this.cancelBlur = setTimeout(this.switchToLink.bind(this), 200); + } + + updateValue(text) { + if (text === '' || this.text === text) { + return; + } + + this.$scope.$apply(() => { + var option = _.find(this.options, {text: text}); + + if (option) { + this.value = option.value; + this.updateDisplay(option.text); + } else if (this.allowCustom) { + this.value = text; + this.updateDisplay(text); + } + + // needs to call this after digest so + // property is synced with outerscope + this.$scope.$$postDigest(() => { + this.$scope.$apply(() => { + this.onChange(); + }); + }); + + }); + } + + updateDisplay(text) { + this.text = text; + this.display = this.$sce.trustAsHtml(this.templateSrv.highlightVariablesAsHtml(text)); + } + + open() { + this.inputElement.show(); + + this.inputElement.css('width', (Math.max(this.linkElement.width(), 80) + 16) + 'px'); + this.inputElement.focus(); + + this.linkElement.hide(); + this.linkMode = false; + + var typeahead = this.inputElement.data('typeahead'); + if (typeahead) { + this.inputElement.val(''); + typeahead.lookup(); + } + } +} + + + +export function formDropdownDirective() { + return { + restrict: 'E', + templateUrl: 'public/app/core/components/form_dropdown/form_dropdown.html', + controller: FormDropdownCtrl, + bindToController: true, + controllerAs: 'ctrl', + scope: { + value: "=", + options: "=", + getOptions: "&", + onChange: "&", + cssClass: "@", + allowCustom: "@", + }, + link: function() { + } + }; +} + +coreModule.directive('gfFormDropdown', formDropdownDirective); diff --git a/public/app/core/core.ts b/public/app/core/core.ts index d44cbf4dbfb..ea55c22be8f 100644 --- a/public/app/core/core.ts +++ b/public/app/core/core.ts @@ -35,6 +35,7 @@ import {switchDirective} from './components/switch'; import {dashboardSelector} from './components/dashboard_selector'; import {queryPartEditorDirective} from './components/query_part/query_part_editor'; import {WizardFlow} from './components/wizard/wizard'; +import {formDropdownDirective} from './components/form_dropdown/form_dropdown'; import 'app/core/controllers/all'; import 'app/core/services/all'; import 'app/core/routes/routes'; @@ -62,4 +63,5 @@ export { queryPartEditorDirective, WizardFlow, colors, + formDropdownDirective, }; diff --git a/public/app/core/directives/metric_segment.js b/public/app/core/directives/metric_segment.js index 2001073ed80..62805161155 100644 --- a/public/app/core/directives/metric_segment.js +++ b/public/app/core/directives/metric_segment.js @@ -143,7 +143,6 @@ function (_, $, coreModule) { $input.focus(); linkMode = false; - var typeahead = $input.data('typeahead'); if (typeahead) { $input.val(''); @@ -152,7 +151,6 @@ function (_, $, coreModule) { }); $input.blur($scope.inputBlur); - $compile(elem.contents())($scope); } }; diff --git a/public/app/plugins/datasource/elasticsearch/partials/bucket_agg.html b/public/app/plugins/datasource/elasticsearch/partials/bucket_agg.html index 36e914d06e0..2b674a9fdf9 100644 --- a/public/app/plugins/datasource/elasticsearch/partials/bucket_agg.html +++ b/public/app/plugins/datasource/elasticsearch/partials/bucket_agg.html @@ -5,7 +5,8 @@ Then by - + + diff --git a/public/app/plugins/datasource/elasticsearch/partials/metric_agg.html b/public/app/plugins/datasource/elasticsearch/partials/metric_agg.html index faa12b5693d..472cfca37fa 100644 --- a/public/app/plugins/datasource/elasticsearch/partials/metric_agg.html +++ b/public/app/plugins/datasource/elasticsearch/partials/metric_agg.html @@ -11,9 +11,9 @@
- - - + + +
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 02/26] 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 03/26] 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 04/26] 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 05/26] 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 06/26] 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 07/26] 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 08/26] 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 09/26] 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 10/26] 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 11/26] 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 12/26] 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 13/26] 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 14/26] 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 15/26] 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 16/26] 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 17/26] 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 192c447c2ce05367dbe1b66e68b5c451bdbb9abe Mon Sep 17 00:00:00 2001 From: Trent White Date: Mon, 12 Jun 2017 11:17:00 -0400 Subject: [PATCH 18/26] 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 19/26] 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 20/26] 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 21/26] 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 22/26] 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}} - + - -
-
+ +
+
- + - - +
+
+
+ + +
+
+
-
-
-
- - -
-
-
- - - - - - -
-
- -
+ + + + +
From 6a95df403ac967f1b24133209e36ebe313ce2d1e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Wed, 14 Jun 2017 19:42:45 -0400 Subject: [PATCH 23/26] refacoring: more work on metric segment replacement --- .../form_dropdown/form_dropdown.html | 13 --- .../components/form_dropdown/form_dropdown.ts | 90 ++++++++++++++----- public/app/core/directives/dash_edit_link.js | 2 +- public/app/core/directives/metric_segment.js | 2 + public/app/features/panel/metrics_tab.ts | 51 +++++------ .../features/panel/partials/metrics_tab.html | 15 ++-- 6 files changed, 102 insertions(+), 71 deletions(-) delete mode 100644 public/app/core/components/form_dropdown/form_dropdown.html diff --git a/public/app/core/components/form_dropdown/form_dropdown.html b/public/app/core/components/form_dropdown/form_dropdown.html deleted file mode 100644 index 34737eab27e..00000000000 --- a/public/app/core/components/form_dropdown/form_dropdown.html +++ /dev/null @@ -1,13 +0,0 @@ - - - - diff --git a/public/app/core/components/form_dropdown/form_dropdown.ts b/public/app/core/components/form_dropdown/form_dropdown.ts index fd363d3b75e..eb8be1e0945 100644 --- a/public/app/core/components/form_dropdown/form_dropdown.ts +++ b/public/app/core/components/form_dropdown/form_dropdown.ts @@ -15,15 +15,17 @@ function typeaheadMatcher(item) { export class FormDropdownCtrl { inputElement: any; linkElement: any; - value: any; - text: any; + model: any; display: any; + text: any; options: any; cssClass: any; allowCustom: any; linkMode: boolean; cancelBlur: any; onChange: any; + getOptions: any; + optionCache: any; constructor(private $scope, $element, private $sce, private templateSrv) { this.inputElement = $element.find('input').first(); @@ -31,11 +33,15 @@ export class FormDropdownCtrl { this.linkMode = true; this.cancelBlur = null; - if (this.options) { - var item = _.find(this.options, {value: this.value}); - this.updateDisplay(item ? item.text : this.value); + if (!this.getOptions) { + this.getOptions = () => { + return Promise.resolve(this.options); + }; } + // listen to model changes + $scope.$watch("ctrl.model", this.modelChanged.bind(this)); + this.inputElement.attr('data-provide', 'typeahead'); this.inputElement.typeahead({ source: this.typeaheadSource.bind(this), @@ -64,19 +70,40 @@ export class FormDropdownCtrl { this.inputElement.blur(this.inputBlur.bind(this)); } - typeaheadSource(query, callback) { - if (this.options) { - var typeaheadOptions = _.map(this.options, 'text'); + modelChanged(newVal) { + if (_.isObject(this.model)) { + this.updateDisplay(this.model.text); + } else { - // add current custom value + // if we have text use it + if (this.text) { + this.updateDisplay(this.text); + } else { + // otherwise we need to do initial lookup, usually happens first time + this.getOptions().then(options => { + var item = _.find(options, {value: this.model}); + this.updateDisplay(item ? item.text : this.model); + }); + } + } + } + + typeaheadSource(query, callback) { + this.getOptions({$query: query}).then(options => { + this.optionCache = options; + + // extract texts + let optionTexts = _.map(options, 'text'); + + // add custom values if (this.allowCustom) { - if (_.indexOf(typeaheadOptions, this.text) === -1) { - typeaheadOptions.unshift(this.text); + if (_.indexOf(optionTexts, this.text) === -1) { + options.unshift(this.text); } } - callback(typeaheadOptions); - } + callback(optionTexts); + }); } typeaheadUpdater(text) { @@ -114,21 +141,29 @@ export class FormDropdownCtrl { } this.$scope.$apply(() => { - var option = _.find(this.options, {text: text}); + var option = _.find(this.optionCache, {text: text}); if (option) { - this.value = option.value; - this.updateDisplay(option.text); + if (_.isObject(this.model)) { + this.model = option; + } else { + this.model = option.value; + } + this.text = option.text; } else if (this.allowCustom) { - this.value = text; - this.updateDisplay(text); + if (_.isObject(this.model)) { + this.model.text = this.model.value = text; + } else { + this.model = text; + } + this.text = text; } // needs to call this after digest so // property is synced with outerscope this.$scope.$$postDigest(() => { this.$scope.$apply(() => { - this.onChange(); + this.onChange({$option: option}); }); }); @@ -157,17 +192,30 @@ export class FormDropdownCtrl { } } +const template = ` + + +`; export function formDropdownDirective() { return { restrict: 'E', - templateUrl: 'public/app/core/components/form_dropdown/form_dropdown.html', + template: template, controller: FormDropdownCtrl, bindToController: true, controllerAs: 'ctrl', scope: { - value: "=", + model: "=", options: "=", getOptions: "&", onChange: "&", diff --git a/public/app/core/directives/dash_edit_link.js b/public/app/core/directives/dash_edit_link.js index 3e4bdd4c5c7..a4c1ad53b3c 100644 --- a/public/app/core/directives/dash_edit_link.js +++ b/public/app/core/directives/dash_edit_link.js @@ -35,7 +35,7 @@ function ($, angular, coreModule) { options.html = editViewMap[options.editview].html; } - if (lastEditView === options.editview) { + if (lastEditView && lastEditView === options.editview) { hideEditorPane(false); return; } diff --git a/public/app/core/directives/metric_segment.js b/public/app/core/directives/metric_segment.js index 0605d54a815..2e9442c15a0 100644 --- a/public/app/core/directives/metric_segment.js +++ b/public/app/core/directives/metric_segment.js @@ -143,6 +143,7 @@ function (_, $, coreModule) { $input.focus(); linkMode = false; + var typeahead = $input.data('typeahead'); if (typeahead) { $input.val(''); @@ -151,6 +152,7 @@ function (_, $, coreModule) { }); $input.blur($scope.inputBlur); + $compile(elem.contents())($scope); } }; diff --git a/public/app/features/panel/metrics_tab.ts b/public/app/features/panel/metrics_tab.ts index f2d99738040..c03c4d83bc2 100644 --- a/public/app/features/panel/metrics_tab.ts +++ b/public/app/features/panel/metrics_tab.ts @@ -5,8 +5,6 @@ import _ from 'lodash'; import {DashboardModel} from '../dashboard/model'; export class MetricsTabCtrl { - dsSegment: any; - mixedDsSegment: any; dsName: string; panel: any; panelCtrl: any; @@ -14,30 +12,26 @@ export class MetricsTabCtrl { current: any; nextRefId: string; dashboard: DashboardModel; + panelDsValue: any; + addQueryDropdown: any; /** @ngInject */ - constructor($scope, private uiSegmentSrv, datasourceSrv) { + constructor($scope, private uiSegmentSrv, private 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; + this.panelDsValue = this.panelCtrl.panel.datasource || null; for (let ds of this.datasources) { - if (ds.value === dsValue) { + if (ds.value === this.panelDsValue) { this.current = ds; } } - if (!this.current) { - this.current = {name: dsValue + ' not found', value: null}; - } - - this.dsSegment = uiSegmentSrv.newSegment({value: this.current.name, selectMode: true}); - this.mixedDsSegment = uiSegmentSrv.newSegment({value: 'Add Query', selectMode: true, fake: true}); + this.addQueryDropdown = {text: 'Add Query', value: null, fake: true}; // update next ref id this.panelCtrl.nextRefId = this.dashboard.getNextQueryLetter(this.panel); @@ -46,33 +40,28 @@ export class MetricsTabCtrl { getOptions(includeBuiltin) { return Promise.resolve(this.datasources.filter(value => { return includeBuiltin || !value.meta.builtIn; - }).map(value => { - return this.uiSegmentSrv.newSegment(value.name); + }).map(ds => { + return {value: ds.value, text: ds.name, datasource: ds}; })); } - datasourceChanged() { - var ds = _.find(this.datasources, {name: this.dsSegment.value}); - if (ds) { - this.current = ds; - this.panelCtrl.setDatasource(ds); + datasourceChanged(option) { + if (!option) { + return; } + + this.current = option.datasource; + this.panelCtrl.setDatasource(option.datasource); } - mixedDatasourceChanged() { - var target: any = {isNew: true}; - var ds = _.find(this.datasources, {name: this.mixedDsSegment.value}); - - if (ds) { - target.datasource = ds.name; - this.panelCtrl.addQuery(target); + addMixedQuery(option) { + if (!option) { + return; } - // 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; + var target: any = {isNew: true}; + this.panelCtrl.addQuery({isNew: true, datasource: option.datasource.name}); + this.addQueryDropdown = {text: 'Add Query', value: null, fake: true}; } addQuery() { diff --git a/public/app/features/panel/partials/metrics_tab.html b/public/app/features/panel/partials/metrics_tab.html index a020a2edbd1..32f43f1d639 100644 --- a/public/app/features/panel/partials/metrics_tab.html +++ b/public/app/features/panel/partials/metrics_tab.html @@ -19,7 +19,10 @@
@@ -30,10 +33,12 @@
- - + + +
From 5f3b5fdcb25c8d3c35b098d990520b1f1dc93d11 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Thu, 15 Jun 2017 12:21:12 -0400 Subject: [PATCH 24/26] updated --- .../core/components/form_dropdown/form_dropdown.ts | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/public/app/core/components/form_dropdown/form_dropdown.ts b/public/app/core/components/form_dropdown/form_dropdown.ts index eb8be1e0945..cb7390ead9a 100644 --- a/public/app/core/components/form_dropdown/form_dropdown.ts +++ b/public/app/core/components/form_dropdown/form_dropdown.ts @@ -194,17 +194,19 @@ export class FormDropdownCtrl { const template = ` + data-provide="typeahead" + class="gf-form-input" + spellcheck="false" + style="display:none"> + + ng-bind-html="ctrl.display"> + `; export function formDropdownDirective() { @@ -221,6 +223,7 @@ export function formDropdownDirective() { onChange: "&", cssClass: "@", allowCustom: "@", + selectMode: "@", }, link: function() { } From 76c4bfe2682a926ee479783abfe3ba4571e973ff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Thu, 15 Jun 2017 14:03:26 -0400 Subject: [PATCH 25/26] ux: new metric segment is starting to work --- .../components/form_dropdown/form_dropdown.ts | 21 +++++++++---------- .../features/panel/partials/metrics_tab.html | 3 +-- 2 files changed, 11 insertions(+), 13 deletions(-) diff --git a/public/app/core/components/form_dropdown/form_dropdown.ts b/public/app/core/components/form_dropdown/form_dropdown.ts index cb7390ead9a..50017225720 100644 --- a/public/app/core/components/form_dropdown/form_dropdown.ts +++ b/public/app/core/components/form_dropdown/form_dropdown.ts @@ -20,7 +20,9 @@ export class FormDropdownCtrl { text: any; options: any; cssClass: any; + cssClasses: any; allowCustom: any; + labelMode: boolean; linkMode: boolean; cancelBlur: any; onChange: any; @@ -33,15 +35,15 @@ export class FormDropdownCtrl { this.linkMode = true; this.cancelBlur = null; - if (!this.getOptions) { - this.getOptions = () => { - return Promise.resolve(this.options); - }; - } - // listen to model changes $scope.$watch("ctrl.model", this.modelChanged.bind(this)); + if (this.labelMode) { + this.cssClasses = 'gf-form-label ' + this.cssClass; + } else { + this.cssClasses = 'gf-form-input gf-form-input--dropdown ' + this.cssClass; + } + this.inputElement.attr('data-provide', 'typeahead'); this.inputElement.typeahead({ source: this.typeaheadSource.bind(this), @@ -199,9 +201,7 @@ const template = ` spellcheck="false" style="display:none"> - -Panel Data Source + on-change="ctrl.datasourceChanged($option)"> From 840099bec0b58f464f3605c30c2f072a96f4d994 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Thu, 15 Jun 2017 15:56:24 -0400 Subject: [PATCH 26/26] refactor: metric segment remake --- .../components/form_dropdown/form_dropdown.ts | 38 ++++++++++---- .../features/panel/partials/metrics_tab.html | 1 + .../datasource/elasticsearch/bucket_agg.js | 21 +++++--- .../elasticsearch/partials/bucket_agg.html | 51 ++++++++++++++++--- .../elasticsearch/partials/metric_agg.html | 6 +-- .../datasource/elasticsearch/query_ctrl.ts | 4 +- 6 files changed, 91 insertions(+), 30 deletions(-) diff --git a/public/app/core/components/form_dropdown/form_dropdown.ts b/public/app/core/components/form_dropdown/form_dropdown.ts index 50017225720..72a101388b7 100644 --- a/public/app/core/components/form_dropdown/form_dropdown.ts +++ b/public/app/core/components/form_dropdown/form_dropdown.ts @@ -28,8 +28,9 @@ export class FormDropdownCtrl { onChange: any; getOptions: any; optionCache: any; + lookupText: boolean; - constructor(private $scope, $element, private $sce, private templateSrv) { + constructor(private $scope, $element, private $sce, private templateSrv, private $q) { this.inputElement = $element.find('input').first(); this.linkElement = $element.find('a').first(); this.linkMode = true; @@ -69,29 +70,45 @@ export class FormDropdownCtrl { } }); + this.inputElement.keydown(evt => { + if (evt.keyCode === 13) { + this.inputElement.blur(); + } + }); + this.inputElement.blur(this.inputBlur.bind(this)); } - modelChanged(newVal) { + getOptionsInternal(query) { + var result = this.getOptions({$query: query}); + if (this.isPromiseLike(result)) { + return result; + } + return this.$q.when(result); + } + + isPromiseLike(obj) { + return obj && (typeof obj.then === 'function'); + } + + modelChanged() { if (_.isObject(this.model)) { this.updateDisplay(this.model.text); } else { - // if we have text use it - if (this.text) { - this.updateDisplay(this.text); - } else { - // otherwise we need to do initial lookup, usually happens first time - this.getOptions().then(options => { + if (this.lookupText) { + this.getOptionsInternal("").then(options => { var item = _.find(options, {value: this.model}); this.updateDisplay(item ? item.text : this.model); }); + } else { + this.updateDisplay(this.model); } } } typeaheadSource(query, callback) { - this.getOptions({$query: query}).then(options => { + this.getOptionsInternal(query).then(options => { this.optionCache = options; // extract texts @@ -223,9 +240,8 @@ export function formDropdownDirective() { cssClass: "@", allowCustom: "@", labelMode: "@", + lookupText: "@", }, - link: function() { - } }; } diff --git a/public/app/features/panel/partials/metrics_tab.html b/public/app/features/panel/partials/metrics_tab.html index d8d8816d63b..bc0bcf7c6b2 100644 --- a/public/app/features/panel/partials/metrics_tab.html +++ b/public/app/features/panel/partials/metrics_tab.html @@ -35,6 +35,7 @@
diff --git a/public/app/plugins/datasource/elasticsearch/bucket_agg.js b/public/app/plugins/datasource/elasticsearch/bucket_agg.js index b2cfc819579..5adaed173af 100644 --- a/public/app/plugins/datasource/elasticsearch/bucket_agg.js +++ b/public/app/plugins/datasource/elasticsearch/bucket_agg.js @@ -26,13 +26,21 @@ function (angular, _, queryDef) { var bucketAggs = $scope.target.bucketAggs; $scope.orderByOptions = []; - $scope.bucketAggTypes = queryDef.bucketAggTypes; - $scope.orderOptions = queryDef.orderOptions; - $scope.sizeOptions = queryDef.sizeOptions; + + $scope.getBucketAggTypes = function() { + return queryDef.bucketAggTypes; + }; + + $scope.getOrderOptions = function() { + return queryDef.orderOptions; + }; + + $scope.getSizeOptions = function() { + return queryDef.sizeOptions; + }; $rootScope.onAppEvent('elastic-query-updated', function() { $scope.validateModel(); - $scope.updateOrderByOptions(); }, $scope); $scope.init = function() { @@ -166,11 +174,10 @@ function (angular, _, queryDef) { $scope.toggleOptions = function() { $scope.showOptions = !$scope.showOptions; - $scope.updateOrderByOptions(); }; - $scope.updateOrderByOptions = function() { - $scope.orderByOptions = queryDef.getOrderByOptions($scope.target); + $scope.getOrderByOptions = function() { + return queryDef.getOrderByOptions($scope.target); }; $scope.getFieldsInternal = function() { diff --git a/public/app/plugins/datasource/elasticsearch/partials/bucket_agg.html b/public/app/plugins/datasource/elasticsearch/partials/bucket_agg.html index f6c86db0d1e..a180f97c994 100644 --- a/public/app/plugins/datasource/elasticsearch/partials/bucket_agg.html +++ b/public/app/plugins/datasource/elasticsearch/partials/bucket_agg.html @@ -5,9 +5,22 @@ Then by - - - + + + +
@@ -34,7 +47,13 @@
- + +
@@ -67,11 +86,23 @@
- + +
- + +
@@ -79,7 +110,13 @@
- + +
- - - + + +
diff --git a/public/app/plugins/datasource/elasticsearch/query_ctrl.ts b/public/app/plugins/datasource/elasticsearch/query_ctrl.ts index 009befb37a4..ef6f1c5cdc7 100644 --- a/public/app/plugins/datasource/elasticsearch/query_ctrl.ts +++ b/public/app/plugins/datasource/elasticsearch/query_ctrl.ts @@ -31,11 +31,11 @@ export class ElasticQueryCtrl extends QueryCtrl { queryUpdated() { var newJson = angular.toJson(this.datasource.queryBuilder.build(this.target), true); - if (newJson !== this.rawQueryOld) { - this.rawQueryOld = newJson; + if (this.rawQueryOld && newJson !== this.rawQueryOld) { this.refresh(); } + this.rawQueryOld = newJson; this.$rootScope.appEvent('elastic-query-updated'); }