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/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/form_dropdown/form_dropdown.ts b/public/app/core/components/form_dropdown/form_dropdown.ts
new file mode 100644
index 00000000000..72a101388b7
--- /dev/null
+++ b/public/app/core/components/form_dropdown/form_dropdown.ts
@@ -0,0 +1,248 @@
+///
+
+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;
+ model: any;
+ display: any;
+ text: any;
+ options: any;
+ cssClass: any;
+ cssClasses: any;
+ allowCustom: any;
+ labelMode: boolean;
+ linkMode: boolean;
+ cancelBlur: any;
+ onChange: any;
+ getOptions: any;
+ optionCache: any;
+ lookupText: boolean;
+
+ 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;
+ this.cancelBlur = null;
+
+ // 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),
+ 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.keydown(evt => {
+ if (evt.keyCode === 13) {
+ this.inputElement.blur();
+ }
+ });
+
+ this.inputElement.blur(this.inputBlur.bind(this));
+ }
+
+ 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.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.getOptionsInternal(query).then(options => {
+ this.optionCache = options;
+
+ // extract texts
+ let optionTexts = _.map(options, 'text');
+
+ // add custom values
+ if (this.allowCustom) {
+ if (_.indexOf(optionTexts, this.text) === -1) {
+ options.unshift(this.text);
+ }
+ }
+
+ callback(optionTexts);
+ });
+ }
+
+ 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.optionCache, {text: text});
+
+ if (option) {
+ if (_.isObject(this.model)) {
+ this.model = option;
+ } else {
+ this.model = option.value;
+ }
+ this.text = option.text;
+ } else if (this.allowCustom) {
+ 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({$option: option});
+ });
+ });
+
+ });
+ }
+
+ 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();
+ }
+ }
+}
+
+const template = `
+
+
+
+
+`;
+
+export function formDropdownDirective() {
+ return {
+ restrict: 'E',
+ template: template,
+ controller: FormDropdownCtrl,
+ bindToController: true,
+ controllerAs: 'ctrl',
+ scope: {
+ model: "=",
+ getOptions: "&",
+ onChange: "&",
+ cssClass: "@",
+ allowCustom: "@",
+ labelMode: "@",
+ lookupText: "@",
+ },
+ };
+}
+
+coreModule.directive('gfFormDropdown', formDropdownDirective);
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 eef62b510be..92db42be8c4 100644
--- a/public/app/core/core.ts
+++ b/public/app/core/core.ts
@@ -34,6 +34,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';
@@ -45,6 +46,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';
import {userPicker} from './components/user_picker';
import {userGroupPicker} from './components/user_group_picker';
@@ -67,10 +70,13 @@ export {
queryPartEditorDirective,
WizardFlow,
colors,
+ formDropdownDirective,
assignModelProperties,
contextSrv,
KeybindingSrv,
helpModal,
+ collapseBox,
+ JsonExplorer,
NavModelSrv,
NavModel,
userPicker,
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/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 = '