diff --git a/conf/sample.ini b/conf/sample.ini index 65ada5b9468..b863f26bdb1 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 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/docs/README.md b/docs/README.md index 7aeab15888b..ff5ef6a4131 100644 --- a/docs/README.md +++ b/docs/README.md @@ -18,11 +18,28 @@ make docs-build **Build the Documentation**: Now that the docker image has been prepared we can build the -grafana docs and start a docs server. Switch your working directory back to the directory this file +grafana docs and start a docs server. + +If you have not cloned the Grafana repository already then: + +``` +cd .. +git clone https://github.com/grafana/grafana +``` + +Switch your working directory to the directory this file (README.md) is in. +``` +cd grafana/docs +``` + An AWS config file is required to build the docs Docker image and to publish the site to AWS. If you are building locally only and do not have any AWS credentials for docs.grafana.org then create an empty file named `awsconfig` in the current directory. +``` +touch awsconfig +``` + Then run (possibly with ``sudo``): ``` 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 diff --git a/public/app/core/components/collapse_box.ts b/public/app/core/components/collapse_box.ts new file mode 100644 index 00000000000..7fc234cb583 --- /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; + stateChanged: () => void; + + /** @ngInject **/ + constructor(private $timeout) { + this.isOpen = false; + } + + toggle() { + this.isOpen = !this.isOpen; + this.$timeout(() => { + this.stateChanged(); + }); + } +} + +export function collapseBox() { + return { + restrict: 'E', + template: template, + controller: CollapseBoxCtrl, + bindToController: true, + controllerAs: 'ctrl', + scope: { + "title": "@", + "isOpen": "=?", + "stateChanged": "&" + }, + transclude: { + 'actions': '?collapseBoxActions', + 'body': 'collapseBoxBody', + }, + link: function(scope, elem, attrs) { + } + }; +} + +coreModule.directive('collapseBox', collapseBox); 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..c2810f23b54 --- /dev/null +++ b/public/app/core/components/json_explorer/json_explorer.ts @@ -0,0 +1,431 @@ +// 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'; + +import _ from 'lodash'; + +const DATE_STRING_REGEX = /(^\d{1,4}[\.|\\/|-]\d{1,2}[\.|\\/|-]\d{1,4})(\s*(?:0?[1-9]:[0-5]|1(?=[012])\d:[0-5])\d\s*[ap]m)?$/; +const PARTIAL_DATE_REGEX = /\d{2}:\d{2}:\d{2} GMT-\d{4}/; +const JSON_DATE_REGEX = /\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z/; + +// When toggleing, don't animated removal or addition of more than a few items +const MAX_ANIMATED_TOGGLE_ITEMS = 10; + +const requestAnimationFrame = window.requestAnimationFrame || function(cb: ()=>void) { cb(); return 0; }; + +export interface JsonExplorerConfig { + animateOpen?: boolean; + animateClose?: boolean; + theme?: string; +} + +const _defaultConfig: JsonExplorerConfig = { + animateOpen: true, + animateClose: true, + theme: null +}; + + +/** + * @class JsonExplorer + * + * JsonExplorer allows you to render JSON objects in HTML with a + * **collapsible** navigation. +*/ +export class JsonExplorer { + + // Hold the open state after the toggler is used + private _isOpen: boolean = null; + + // A reference to the element that we render to + private element: Element; + + private skipChildren = false; + + /** + * @param {object} json The JSON object you want to render. It has to be an + * object or array. Do NOT pass raw JSON string. + * + * @param {number} [open=1] his number indicates up to how many levels the + * rendered tree should expand. Set it to `0` to make the whole tree collapsed + * or set it to `Infinity` to expand the tree deeply + * + * @param {object} [config=defaultConfig] - + * defaultConfig = { + * hoverPreviewEnabled: false, + * hoverPreviewArrayCount: 100, + * hoverPreviewFieldCount: 5 + * } + * + * Available configurations: + * #####Hover Preview + * * `hoverPreviewEnabled`: enable preview on hover + * * `hoverPreviewArrayCount`: number of array items to show in preview Any + * array larger than this number will be shown as `Array[XXX]` where `XXX` + * is length of the array. + * * `hoverPreviewFieldCount`: number of object properties to show for object + * preview. Any object with more properties that thin number will be + * truncated. + * + * @param {string} [key=undefined] The key that this object in it's parent + * context + */ + constructor(public json: any, private open = 1, private config: JsonExplorerConfig = _defaultConfig, private key?: string) { + } + + /* + * is formatter open? + */ + private get isOpen(): boolean { + if (this._isOpen !== null) { + return this._isOpen; + } else { + return this.open > 0; + } + } + + /* + * set open state (from toggler) + */ + private set isOpen(value: boolean) { + this._isOpen = value; + } + + /* + * is this a date string? + */ + private get isDate(): boolean { + return (this.type === 'string') && + (DATE_STRING_REGEX.test(this.json) || + JSON_DATE_REGEX.test(this.json) || + PARTIAL_DATE_REGEX.test(this.json)); + } + + /* + * is this a URL string? + */ + private get isUrl(): boolean { + return this.type === 'string' && (this.json.indexOf('http') === 0); + } + + /* + * is this an array? + */ + private get isArray(): boolean { + return Array.isArray(this.json); + } + + /* + * is this an object? + * Note: In this context arrays are object as well + */ + private get isObject(): boolean { + return isObject(this.json); + } + + /* + * is this an empty object with no properties? + */ + private get isEmptyObject(): boolean { + return !this.keys.length && !this.isArray; + } + + /* + * is this an empty object or array? + */ + private get isEmpty(): boolean { + return this.isEmptyObject || (this.keys && !this.keys.length && this.isArray); + } + + /* + * did we 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')); + } + } + } + + 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(togglerIcon); + } + + // if this is child of a parent formatter we need to append the key + if (this.hasKey) { + togglerLink.appendChild(createElement('span', 'key', `${this.key}:`)); + } + + // Value for objects and arrays + if (this.isObject) { + // construct the value holder element + const value = createElement('span', 'value'); + + // we need a wrapper span for objects + const objectWrapperSpan = createElement('span'); + + // get constructor name and append it to wrapper span + 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 = this.renderArray(); + objectWrapperSpan.appendChild(arrayWrapperSpan); + } + + // append object wrapper span to toggler link + value.appendChild(objectWrapperSpan); + togglerLink.appendChild(value); + // Primitive values + } else { + + // make a value holder element + const value = this.isUrl ? createElement('a') : createElement('span'); + + // add type and other type related CSS classes + value.classList.add(cssClass(this.type)); + if (this.isDate) { + value.classList.add(cssClass('date')); + } + if (this.isUrl) { + value.classList.add(cssClass('url')); + value.setAttribute('href', this.json); + } + + // Append value content to value element + const valuePreview = getValuePreview(this.json, this.json); + value.appendChild(document.createTextNode(valuePreview)); + + // append the value element to toggler link + togglerLink.appendChild(value); + } + + // construct a children element + const children = createElement('div', 'children'); + + // set CSS classes for children + if (this.isObject) { + children.classList.add(cssClass('object')); + } + if (this.isArray) { + children.classList.add(cssClass('array')); + } + if (this.isEmpty) { + children.classList.add(cssClass('empty')); + } + + // set CSS classes for root element + if (this.config && this.config.theme) { + this.element.classList.add(cssClass(this.config.theme)); + } + if (this.isOpen) { + this.element.classList.add(cssClass('open')); + } + + // append toggler and children elements to root element + if (!skipRoot) { + this.element.appendChild(togglerLink); + } + + if (!this.skipChildren) { + this.element.appendChild(children); + } else { + // remove togglerIcon + togglerLink.removeChild(togglerIcon); + } + + // if formatter is set to be open call appendChildren + if (this.isObject && this.isOpen) { + this.appendChildren(); + } + + // add event listener for toggling + if (this.isObject) { + togglerLink.addEventListener('click', this.toggleOpen.bind(this)); + } + + return this.element as HTMLDivElement; + } + + /** + * Appends all the children to children element + * Animated option is used when user triggers this via a click + */ + appendChildren(animated = false) { + const children = this.element.querySelector(`div.${cssClass('children')}`); + + if (!children || this.isEmpty) { return; } + + if (animated) { + let index = 0; + const addAChild = ()=> { + const key = this.keys[index]; + const formatter = new JsonExplorer(this.json[key], this.open - 1, this.config, key); + children.appendChild(formatter.render()); + + index += 1; + + if (index < this.keys.length) { + if (index > MAX_ANIMATED_TOGGLE_ITEMS) { + addAChild(); + } else { + requestAnimationFrame(addAChild); + } + } + }; + + requestAnimationFrame(addAChild); + + } else { + this.keys.forEach(key => { + const formatter = new JsonExplorer(this.json[key], this.open - 1, this.config, key); + children.appendChild(formatter.render()); + }); + } + } + + /** + * Removes all the children from children element + * Animated option is used when user triggers this via a click + */ + removeChildren(animated = false) { + const childrenElement = this.element.querySelector(`div.${cssClass('children')}`) as HTMLDivElement; + + if (animated) { + let childrenRemoved = 0; + const removeAChild = ()=> { + if (childrenElement && childrenElement.children.length) { + childrenElement.removeChild(childrenElement.children[0]); + childrenRemoved += 1; + if (childrenRemoved > MAX_ANIMATED_TOGGLE_ITEMS) { + removeAChild(); + } else { + requestAnimationFrame(removeAChild); + } + } + }; + requestAnimationFrame(removeAChild); + } else { + if (childrenElement) { + childrenElement.innerHTML = ''; + } + } + } +} diff --git a/public/app/core/core.ts b/public/app/core/core.ts index 4ecb4318bf7..ede1e819c1a 100644 --- a/public/app/core/core.ts +++ b/public/app/core/core.ts @@ -45,6 +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 {collapseBox} from './components/collapse_box'; +import {JsonExplorer} from './components/json_explorer/json_explorer'; import {NavModelSrv, NavModel} from './nav_model_srv'; @@ -70,6 +72,8 @@ export { contextSrv, KeybindingSrv, helpModal, + collapseBox, + JsonExplorer, NavModelSrv, NavModel, }; diff --git a/public/app/core/directives/misc.js b/public/app/core/directives/misc.js index 8f6f1fc52a7..f3667c42c13 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,29 @@ function (angular, coreModule, kbn) { }; }); + 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(); + } + }); + } + }; + }); + coreModule.default.directive('compile', function($compile) { return { restrict: 'A', @@ -77,10 +101,10 @@ function (angular, coreModule, kbn) { text + tip + ''; var template = - '' + - ' '; + '' + + ' '; template = template + label; elem.addClass('gf-form-checkbox'); @@ -105,7 +129,7 @@ function (angular, 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/core/directives/plugin_component.ts b/public/app/core/directives/plugin_component.ts index 4c098f60a4c..d0f21501e7e 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 }; }); @@ -181,7 +181,7 @@ function pluginDirectiveLoader($compile, datasourceSrv, $rootScope, $q, $http, $ return System.import(appModel.module).then(function(appModule) { return { baseUrl: appModel.baseUrl, - name: 'app-page-' + appModel.appId + '-' + scope.ctrl.page.slug, + name: 'app-page-' + appModel.id + '-' + scope.ctrl.page.slug, bindings: {appModel: "="}, attrs: {"app-model": "ctrl.appModel"}, Component: appModule[scope.ctrl.page.component], diff --git a/public/app/core/services/backend_srv.ts b/public/app/core/services/backend_srv.ts index cc1a8644a51..f4c32ab82b1 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}; } @@ -166,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, @@ -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/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; } } 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'; 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 6f3cb320bef..211b0efbac9 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'); @@ -89,20 +88,10 @@ function (angular, _, $, moment, require, config) { $scope.imageUrl += '&tz=UTC' + encodeURIComponent(moment().format("Z")); }; - }); - - 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(); - } - }); + $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/all.js b/public/app/features/panel/all.js index 2f978e65345..cba296643ef 100644 --- a/public/app/features/panel/all.js +++ b/public/app/features/panel/all.js @@ -5,5 +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_ds_selector.ts b/public/app/features/panel/metrics_ds_selector.ts deleted file mode 100644 index 7c4bc0ef495..00000000000 --- a/public/app/features/panel/metrics_ds_selector.ts +++ /dev/null @@ -1,113 +0,0 @@ -/// - -import angular from 'angular'; -import _ from 'lodash'; - -var module = angular.module('grafana.directives'); - -var template = ` -
-
-
- - - - -
- -
- - - -
-
-
-`; - - -export class MetricsDsSelectorCtrl { - dsSegment: any; - mixedDsSegment: any; - dsName: string; - panelCtrl: any; - datasources: any[]; - current: any; - - /** @ngInject */ - constructor(private uiSegmentSrv, datasourceSrv) { - this.datasources = datasourceSrv.getMetricSources(); - - var dsValue = this.panelCtrl.panel.datasource || null; - - for (let ds of this.datasources) { - if (ds.value === dsValue) { - 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}); - } - - getOptions(includeBuiltin) { - return Promise.resolve(this.datasources.filter(value => { - return includeBuiltin || !value.meta.builtIn; - }).map(value => { - return this.uiSegmentSrv.newSegment(value.name); - })); - } - - datasourceChanged() { - var ds = _.find(this.datasources, {name: this.dsSegment.value}); - if (ds) { - this.current = ds; - this.panelCtrl.setDatasource(ds); - } - } - - mixedDatasourceChanged() { - var target: any = {isNew: true}; - var ds = _.find(this.datasources, {name: this.mixedDsSegment.value}); - if (ds) { - target.datasource = ds.name; - this.panelCtrl.panel.targets.push(target); - this.mixedDsSegment.value = ''; - } - } - - addDataQuery() { - var target: any = {isNew: true}; - this.panelCtrl.panel.targets.push(target); - } -} - -module.directive('metricsDsSelector', function() { - return { - restrict: 'E', - template: template, - controller: MetricsDsSelectorCtrl, - bindToController: true, - controllerAs: 'ctrl', - transclude: true, - scope: { - panelCtrl: "=" - } - }; -}); diff --git a/public/app/features/panel/metrics_panel_ctrl.ts b/public/app/features/panel/metrics_panel_ctrl.ts index af9d06d8742..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'; @@ -10,6 +11,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; @@ -32,6 +34,7 @@ class MetricsPanelCtrl extends PanelCtrl { dataStream: any; dataSubscription: any; dataList: any; + nextRefId: string; constructor($scope, $injector) { super($scope, $injector); @@ -61,7 +64,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'); } @@ -256,7 +259,7 @@ class MetricsPanelCtrl extends PanelCtrl { result = {data: []}; } - return this.events.emit('data-received', result.data); + this.events.emit('data-received', result.data); } handleDataStream(stream) { @@ -306,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 new file mode 100644 index 00000000000..f2d99738040 --- /dev/null +++ b/public/app/features/panel/metrics_tab.ts @@ -0,0 +1,94 @@ +/// + +import _ from 'lodash'; +//import {coreModule} from 'app/core/core'; +import {DashboardModel} from '../dashboard/model'; + +export class MetricsTabCtrl { + dsSegment: any; + mixedDsSegment: any; + dsName: string; + panel: any; + panelCtrl: any; + datasources: any[]; + current: any; + 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; + + for (let ds of this.datasources) { + if (ds.value === dsValue) { + 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}); + + // update next ref id + this.panelCtrl.nextRefId = this.dashboard.getNextQueryLetter(this.panel); + } + + getOptions(includeBuiltin) { + return Promise.resolve(this.datasources.filter(value => { + return includeBuiltin || !value.meta.builtIn; + }).map(value => { + return this.uiSegmentSrv.newSegment(value.name); + })); + } + + datasourceChanged() { + var ds = _.find(this.datasources, {name: this.dsSegment.value}); + if (ds) { + this.current = ds; + this.panelCtrl.setDatasource(ds); + } + } + + 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); + } + + // 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() { + this.panelCtrl.addQuery({isNew: true}); + } +} + +/** @ngInject **/ +export function metricsTabDirective() { + 'use strict'; + return { + restrict: 'E', + scope: true, + templateUrl: 'public/app/features/panel/partials/metrics_tab.html', + controller: MetricsTabCtrl, + }; +} + +//coreModule.directive('metricsTab', metricsTabDirective); diff --git a/public/app/features/panel/panel_ctrl.ts b/public/app/features/panel/panel_ctrl.ts index 83c79f4123b..5ed2b3abffb 100644 --- a/public/app/features/panel/panel_ctrl.ts +++ b/public/app/features/panel/panel_ctrl.ts @@ -75,7 +75,7 @@ export class PanelCtrl { } refresh() { - this.events.emit('refresh', null); + this.events.emit('refresh', null); } publishAppEvent(evtName, evt) { 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_tab.html b/public/app/features/panel/partials/metrics_tab.html new file mode 100644 index 00000000000..a020a2edbd1 --- /dev/null +++ b/public/app/features/panel/partials/metrics_tab.html @@ -0,0 +1,45 @@ +
+
+ + + + +
+ +
+
+ + + + +
+
+
+ + + +
+
+
+ + +
+
+
+ + + + + + 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/features/panel/query_troubleshooter.ts b/public/app/features/panel/query_troubleshooter.ts new file mode 100644 index 00000000000..84ae2045dc7 --- /dev/null +++ b/public/app/features/panel/query_troubleshooter.ts @@ -0,0 +1,154 @@ +/// + +import _ from 'lodash'; +import appEvents from 'app/core/app_events'; +import {coreModule, JsonExplorer} from 'app/core/core'; + +const template = ` + + + + Expand All + + + Collapse All + + Copy to Clipboard + + + +
+
+
+`; + +export class QueryTroubleshooterCtrl { + isOpen: any; + isLoading: boolean; + showResponse: boolean; + panelCtrl: any; + renderJsonExplorer: (data) => void; + onRequestErrorEventListener: any; + onRequestResponseEventListener: any; + hasError: boolean; + allNodesExpanded: boolean; + jsonExplorer: JsonExplorer; + + /** @ngInject **/ + constructor($scope, private $timeout) { + 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)); + } + + removeEventsListeners() { + appEvents.off('ds-request-response', this.onRequestResponseEventListener); + appEvents.off('ds-request-error', this.onRequestErrorEventListener); + } + + onRequestError(err) { + this.isOpen = true; + this.hasError = true; + this.onRequestResponse(err); + } + + stateChanged() { + if (this.isOpen) { + this.panelCtrl.refresh(); + this.isLoading = true; + } + } + + getClipboardText() { + if (this.jsonExplorer) { + return JSON.stringify(this.jsonExplorer.json, null, 2); + } + } + + onRequestResponse(data) { + // ignore if closed + if (!this.isOpen) { + return; + } + + this.isLoading = false; + data = _.cloneDeep(data); + + 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; + + 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; + delete data.$$config; + } + + 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() { + 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'); + + ctrl.jsonExplorer = new JsonExplorer(data, 3, { + animateOpen: true, + }); + + const html = ctrl.jsonExplorer.render(true); + jsonElem.html(html); + }; + } + }; +} + +coreModule.directive('queryTroubleshooter', queryTroubleshooter); diff --git a/public/app/headers/common.d.ts b/public/app/headers/common.d.ts index 314258be21e..9ea5e96654d 100644 --- a/public/app/headers/common.d.ts +++ b/public/app/headers/common.d.ts @@ -72,4 +72,3 @@ declare module 'd3' { var d3: any; export default d3; } - 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/app/partials/metrics.html b/public/app/partials/metrics.html deleted file mode 100644 index 39c471eabd1..00000000000 --- a/public/app/partials/metrics.html +++ /dev/null @@ -1,20 +0,0 @@ - -
-
- - - - -
-
- - - -
- - - - -
- -
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/app/plugins/datasource/influxdb/datasource.ts b/public/app/plugins/datasource/influxdb/datasource.ts index 0cbdcabcf10..09a9505bb49 100644 --- a/public/app/plugins/datasource/influxdb/datasource.ts +++ b/public/app/plugins/datasource/influxdb/datasource.ts @@ -210,10 +210,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/app/system.conf.js b/public/app/system.conf.js index ae8e93a726a..816cba4fb42 100644 --- a/public/app/system.conf.js +++ b/public/app/system.conf.js @@ -32,7 +32,7 @@ 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", }, packages: { 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/_grafana.scss b/public/sass/_grafana.scss index cecb9f80525..c6155da946c 100644 --- a/public/sass/_grafana.scss +++ b/public/sass/_grafana.scss @@ -75,6 +75,8 @@ @import "components/jsontree"; @import "components/edit_sidemenu.scss"; @import "components/row.scss"; +@import "components/json_explorer.scss"; +@import "components/collapse_box.scss"; // PAGES @import "pages/login"; diff --git a/public/sass/_variables.dark.scss b/public/sass/_variables.dark.scss index 81dfffbd32b..437bd89efa9 100644 --- a/public/sass/_variables.dark.scss +++ b/public/sass/_variables.dark.scss @@ -291,6 +291,24 @@ $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: $text-color; +$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; +$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; // Changelog and diff // ------------------------- diff --git a/public/sass/_variables.light.scss b/public/sass/_variables.light.scss index 3787305b80d..ecd5837aea7 100644 --- a/public/sass/_variables.light.scss +++ b/public/sass/_variables.light.scss @@ -313,6 +313,24 @@ $card-shadow: -1px -1px 0 0 hsla(0, 0%, 100%, .1), 1px 1px 0 0 rgba(0, 0, 0, .1) $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; // Changelog and diff // ------------------------- diff --git a/public/sass/components/_collapse_box.scss b/public/sass/components/_collapse_box.scss new file mode 100644 index 00000000000..86658cfcfec --- /dev/null +++ b/public/sass/components/_collapse_box.scss @@ -0,0 +1,46 @@ +.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 { + display: flex; + flex-direction: row; + padding: $input-padding-y $input-padding-x; + margin-right: $gf-form-margin; + background-color: $input-label-bg; + font-size: $font-size-sm; + margin-right: $gf-form-margin; + border: $input-btn-border-width solid $collapse-box-body-border; + @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; + display: block; + margin-right: $gf-form-margin; + border: $input-btn-border-width solid $collapse-box-body-border; + border-top: none; + @include border-radius($label-border-radius-sm); +} + +.collapse-box__header-actions { + display: flex; + flex-direction: row; + a { + margin-left: $spacer; + } +} diff --git a/public/sass/components/_gf-form.scss b/public/sass/components/_gf-form.scss index 7ff07b6e887..53d52541611 100644 --- a/public/sass/components/_gf-form.scss +++ b/public/sass/components/_gf-form.scss @@ -103,6 +103,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/_json_explorer.scss b/public/sass/components/_json_explorer.scss new file mode 100644 index 00000000000..37c610b2002 --- /dev/null +++ b/public/sass/components/_json_explorer.scss @@ -0,0 +1,98 @@ + +.json-formatter-row { + font-family: monospace; + + &, a, a:hover { + color: $json-explorer-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: $json-explorer-string-color; + white-space: normal; + word-wrap: break-word; + } + .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: $json-explorer-url-color; + cursor: pointer; + } + + .json-formatter-bracket { color: $json-explorer-bracket-color; } + .json-formatter-key { + 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; + vertical-align: middle; + opacity: $json-explorer-toggler-opacity; + cursor: pointer; + padding-right: 0.2rem; + + &::after { + display: inline-block; + transition: transform $json-explorer-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; + } + } +} + 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"; + 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; } } } diff --git a/public/test/test-main.js b/public/test/test-main.js index 50553b7c8b3..777e9d76afb 100644 --- a/public/test/test-main.js +++ b/public/test/test-main.js @@ -40,7 +40,7 @@ "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", }, packages: { diff --git a/yarn.lock b/yarn.lock index 74cad3c2407..a9100c88e10 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" @@ -3807,11 +3807,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: