Merge remote-tracking branch 'origin/graphite-query-editor-enhancements'

This commit is contained in:
Torkel Ödegaard
2018-01-23 16:02:46 +01:00
20 changed files with 938 additions and 452 deletions

View File

@@ -8,7 +8,6 @@ charset = utf-8
trim_trailing_whitespace = true trim_trailing_whitespace = true
insert_final_newline = true insert_final_newline = true
max_line_length = 120 max_line_length = 120
insert_final_newline = true
[*.go] [*.go]
indent_style = tab indent_style = tab

View File

@@ -134,7 +134,7 @@
"clipboard": "^1.7.1", "clipboard": "^1.7.1",
"d3": "^4.11.0", "d3": "^4.11.0",
"d3-scale-chromatic": "^1.1.1", "d3-scale-chromatic": "^1.1.1",
"eventemitter3": "^2.0.2", "eventemitter3": "^2.0.3",
"file-saver": "^1.3.3", "file-saver": "^1.3.3",
"jquery": "^3.2.1", "jquery": "^3.2.1",
"lodash": "^4.17.4", "lodash": "^4.17.4",
@@ -153,6 +153,7 @@
"react-select": "^1.1.0", "react-select": "^1.1.0",
"react-sizeme": "^2.3.6", "react-sizeme": "^2.3.6",
"remarkable": "^1.7.1", "remarkable": "^1.7.1",
"rst2html": "github:thoward/rst2html#990cb89",
"rxjs": "^5.4.3", "rxjs": "^5.4.3",
"tether": "^1.4.0", "tether": "^1.4.0",
"tether-drop": "https://github.com/torkelo/drop", "tether-drop": "https://github.com/torkelo/drop",

View File

@@ -1,9 +1,11 @@
import _ from 'lodash'; import _ from 'lodash';
import $ from 'jquery';
import coreModule from '../../core_module'; import coreModule from '../../core_module';
function typeaheadMatcher(item) { function typeaheadMatcher(item) {
var str = this.query; var str = this.query;
if (str === '') {
return true;
}
if (str[0] === '/') { if (str[0] === '/') {
str = str.substring(1); str = str.substring(1);
} }
@@ -30,6 +32,8 @@ export class FormDropdownCtrl {
getOptions: any; getOptions: any;
optionCache: any; optionCache: any;
lookupText: boolean; lookupText: boolean;
placeholder: any;
startOpen: any;
/** @ngInject **/ /** @ngInject **/
constructor(private $scope, $element, private $sce, private templateSrv, private $q) { constructor(private $scope, $element, private $sce, private templateSrv, private $q) {
@@ -47,6 +51,10 @@ export class FormDropdownCtrl {
this.cssClasses = 'gf-form-input gf-form-input--dropdown ' + this.cssClass; this.cssClasses = 'gf-form-input gf-form-input--dropdown ' + this.cssClass;
} }
if (this.placeholder) {
this.inputElement.attr('placeholder', this.placeholder);
}
this.inputElement.attr('data-provide', 'typeahead'); this.inputElement.attr('data-provide', 'typeahead');
this.inputElement.typeahead({ this.inputElement.typeahead({
source: this.typeaheadSource.bind(this), source: this.typeaheadSource.bind(this),
@@ -61,8 +69,7 @@ export class FormDropdownCtrl {
var typeahead = this.inputElement.data('typeahead'); var typeahead = this.inputElement.data('typeahead');
typeahead.lookup = function() { typeahead.lookup = function() {
this.query = this.$element.val() || ''; this.query = this.$element.val() || '';
var items = this.source(this.query, $.proxy(this.process, this)); this.source(this.query, this.process.bind(this));
return items ? this.process(items) : items;
}; };
this.linkElement.keydown(evt => { this.linkElement.keydown(evt => {
@@ -81,6 +88,10 @@ export class FormDropdownCtrl {
}); });
this.inputElement.blur(this.inputBlur.bind(this)); this.inputElement.blur(this.inputBlur.bind(this));
if (this.startOpen) {
setTimeout(this.open.bind(this), 0);
}
} }
getOptionsInternal(query) { getOptionsInternal(query) {
@@ -121,9 +132,9 @@ export class FormDropdownCtrl {
}); });
// add custom values // add custom values
if (this.allowCustom) { if (this.allowCustom && this.text !== '') {
if (_.indexOf(optionTexts, this.text) === -1) { if (_.indexOf(optionTexts, this.text) === -1) {
options.unshift(this.text); optionTexts.unshift(this.text);
} }
} }
@@ -228,10 +239,10 @@ const template = `
style="display:none"> style="display:none">
</input> </input>
<a ng-class="ctrl.cssClasses" <a ng-class="ctrl.cssClasses"
tabindex="1" tabindex="1"
ng-click="ctrl.open()" ng-click="ctrl.open()"
give-focus="ctrl.focus" give-focus="ctrl.focus"
ng-bind-html="ctrl.display"> ng-bind-html="ctrl.display || '&nbsp;'">
</a> </a>
`; `;
@@ -250,6 +261,8 @@ export function formDropdownDirective() {
allowCustom: '@', allowCustom: '@',
labelMode: '@', labelMode: '@',
lookupText: '@', lookupText: '@',
placeholder: '@',
startOpen: '@',
}, },
}; };
} }

View File

@@ -12,7 +12,7 @@ function (_, $, coreModule) {
' class="gf-form-input input-medium tight-form-input"' + ' class="gf-form-input input-medium tight-form-input"' +
' spellcheck="false" style="display:none"></input>'; ' spellcheck="false" style="display:none"></input>';
var buttonTemplate = '<a class="gf-form-label tight-form-func dropdown-toggle"' + var buttonTemplate = '<a class="gf-form-label tight-form-func dropdown-toggle"' +
' tabindex="1" gf-dropdown="menuItems" data-toggle="dropdown"' + ' tabindex="1" gf-dropdown="menuItems" data-toggle="dropdown"' +
' data-placement="top"><i class="fa fa-plus"></i></a>'; ' data-placement="top"><i class="fa fa-plus"></i></a>';

View File

@@ -106,10 +106,6 @@ function (angular, _, coreModule) {
return new MetricSegment({fake: true, html: '<i class="fa fa-plus "></i>', type: 'plus-button', cssClass: 'query-part' }); return new MetricSegment({fake: true, html: '<i class="fa fa-plus "></i>', type: 'plus-button', cssClass: 'query-part' });
}; };
this.newSelectTagValue = function() {
return new MetricSegment({value: 'select tag value', fake: true});
};
}); });
}); });

View File

@@ -16,12 +16,12 @@
Add variable Add variable
</a> </a>
<div class="grafana-info-box"> <div class="grafana-info-box">
<h5>What does variables do?</h5> <h5>What do variables do?</h5>
<p>Variables enables more interactive and dynamic dashboards. Instead of hard-coding things like server or sensor names <p>Variables enable more interactive and dynamic dashboards. Instead of hard-coding things like server or sensor names
in your metric queries you can use variables in their place. Variables are shown as dropdown select boxes at the top of in your metric queries you can use variables in their place. Variables are shown as dropdown select boxes at the top of
the dashboard. These dropdowns make it easy to change the data being displayed in your dashboard. the dashboard. These dropdowns make it easy to change the data being displayed in your dashboard.
Checkout the Check out the
<a class="external-link" href="http://docs.grafana.org/reference/templating/" target="_blank"> <a class="external-link" href="http://docs.grafana.org/reference/templating/" target="_blank">
Templating documentation Templating documentation
</a> for more information. </a> for more information.
@@ -93,7 +93,7 @@
</div> </div>
<div class="gf-form" ng-show="ctrl.form.name.$error.pattern"> <div class="gf-form" ng-show="ctrl.form.name.$error.pattern">
<span class="gf-form-label gf-form-label--error">Template names cannot begin with '__' that's reserved for Grafanas global variables</span> <span class="gf-form-label gf-form-label--error">Template names cannot begin with '__', that's reserved for Grafana's global variables</span>
</div> </div>
<div class="gf-form-inline"> <div class="gf-form-inline">

View File

@@ -1,46 +1,37 @@
define([ define(['angular', 'lodash', 'jquery', 'rst2html', 'tether-drop'], function(angular, _, $, rst2html, Drop) {
'angular',
'lodash',
'jquery',
'./gfunc',
],
function (angular, _, $, gfunc) {
'use strict'; 'use strict';
gfunc = gfunc.default; angular.module('grafana.directives').directive('graphiteAddFunc', function($compile) {
var inputTemplate =
'<input type="text"' + ' class="gf-form-input"' + ' spellcheck="false" style="display:none"></input>';
angular var buttonTemplate =
.module('grafana.directives') '<a class="gf-form-label query-part dropdown-toggle"' +
.directive('graphiteAddFunc', function($compile) { ' tabindex="1" gf-dropdown="functionMenu" data-toggle="dropdown">' +
var inputTemplate = '<input type="text"'+ '<i class="fa fa-plus"></i></a>';
' class="gf-form-input"' +
' spellcheck="false" style="display:none"></input>';
var buttonTemplate = '<a class="gf-form-label query-part dropdown-toggle"' + return {
' tabindex="1" gf-dropdown="functionMenu" data-toggle="dropdown">' + link: function($scope, elem) {
'<i class="fa fa-plus"></i></a>'; var ctrl = $scope.ctrl;
return { var $input = $(inputTemplate);
link: function($scope, elem) { var $button = $(buttonTemplate);
var ctrl = $scope.ctrl;
var graphiteVersion = ctrl.datasource.graphiteVersion;
var categories = gfunc.getCategories(graphiteVersion);
var allFunctions = getAllFunctionNames(categories);
$scope.functionMenu = createFunctionDropDownMenu(categories); $input.appendTo(elem);
$button.appendTo(elem);
var $input = $(inputTemplate); ctrl.datasource.getFuncDefs().then(function(funcDefs) {
var $button = $(buttonTemplate); var allFunctions = _.map(funcDefs, 'name').sort();
$input.appendTo(elem);
$button.appendTo(elem); $scope.functionMenu = createFunctionDropDownMenu(funcDefs);
$input.attr('data-provide', 'typeahead'); $input.attr('data-provide', 'typeahead');
$input.typeahead({ $input.typeahead({
source: allFunctions, source: allFunctions,
minLength: 1, minLength: 1,
items: 10, items: 10,
updater: function (value) { updater: function(value) {
var funcDef = gfunc.getFuncDef(value); var funcDef = ctrl.datasource.getFuncDef(value);
if (!funcDef) { if (!funcDef) {
// try find close match // try find close match
value = value.toLowerCase(); value = value.toLowerCase();
@@ -48,7 +39,9 @@ function (angular, _, $, gfunc) {
return funcName.toLowerCase().indexOf(value) === 0; return funcName.toLowerCase().indexOf(value) === 0;
}); });
if (!funcDef) { return; } if (!funcDef) {
return;
}
} }
$scope.$apply(function() { $scope.$apply(function() {
@@ -57,7 +50,7 @@ function (angular, _, $, gfunc) {
$input.trigger('blur'); $input.trigger('blur');
return ''; return '';
} },
}); });
$button.click(function() { $button.click(function() {
@@ -82,32 +75,81 @@ function (angular, _, $, gfunc) {
}); });
$compile(elem.contents())($scope); $compile(elem.contents())($scope);
} });
};
});
function getAllFunctionNames(categories) { var drop;
return _.reduce(categories, function(list, category) { var cleanUpDrop = function() {
_.each(category, function(func) { if (drop) {
list.push(func.name); drop.destroy();
}); drop = null;
return list; }
}, []);
}
function createFunctionDropDownMenu(categories) {
return _.map(categories, function(list, category) {
var submenu = _.map(list, function(value) {
return {
text: value.name,
click: "ctrl.addFunction('" + value.name + "')",
}; };
});
return { $(elem)
text: category, .on('mouseenter', 'ul.dropdown-menu li', function() {
submenu: submenu cleanUpDrop();
};
var funcDef;
try {
funcDef = ctrl.datasource.getFuncDef($('a', this).text());
} catch (e) {
// ignore
}
if (funcDef && funcDef.description) {
var shortDesc = funcDef.description;
if (shortDesc.length > 500) {
shortDesc = shortDesc.substring(0, 497) + '...';
}
var contentElement = document.createElement('div');
contentElement.innerHTML = '<h4>' + funcDef.name + '</h4>' + rst2html(shortDesc);
drop = new Drop({
target: this,
content: contentElement,
classes: 'drop-popover',
openOn: 'always',
tetherOptions: {
attachment: 'bottom left',
targetAttachment: 'bottom right',
},
});
}
})
.on('mouseout', 'ul.dropdown-menu li', function() {
cleanUpDrop();
});
$scope.$on('$destroy', cleanUpDrop);
},
};
});
function createFunctionDropDownMenu(funcDefs) {
var categories = {};
_.forEach(funcDefs, function(funcDef) {
if (!funcDef.category) {
return;
}
if (!categories[funcDef.category]) {
categories[funcDef.category] = [];
}
categories[funcDef.category].push({
text: funcDef.name,
click: "ctrl.addFunction('" + funcDef.name + "')",
});
}); });
return _.sortBy(
_.map(categories, function(submenu, category) {
return {
text: category,
submenu: _.sortBy(submenu, 'text'),
};
}),
'text'
);
} }
}); });

View File

@@ -8,7 +8,6 @@ export class GraphiteConfigCtrl {
this.datasourceSrv = datasourceSrv; this.datasourceSrv = datasourceSrv;
this.current.jsonData = this.current.jsonData || {}; this.current.jsonData = this.current.jsonData || {};
this.current.jsonData.graphiteVersion = this.current.jsonData.graphiteVersion || '0.9'; this.current.jsonData.graphiteVersion = this.current.jsonData.graphiteVersion || '0.9';
this.autoDetectGraphiteVersion(); this.autoDetectGraphiteVersion();
} }

View File

@@ -1,6 +1,7 @@
import _ from 'lodash'; import _ from 'lodash';
import * as dateMath from 'app/core/utils/datemath'; import * as dateMath from 'app/core/utils/datemath';
import { isVersionGtOrEq, SemVersion } from 'app/core/utils/version'; import { isVersionGtOrEq, SemVersion } from 'app/core/utils/version';
import gfunc from './gfunc';
/** @ngInject */ /** @ngInject */
export function GraphiteDatasource(instanceSettings, $q, backendSrv, templateSrv) { export function GraphiteDatasource(instanceSettings, $q, backendSrv, templateSrv) {
@@ -12,6 +13,8 @@ export function GraphiteDatasource(instanceSettings, $q, backendSrv, templateSrv
this.cacheTimeout = instanceSettings.cacheTimeout; this.cacheTimeout = instanceSettings.cacheTimeout;
this.withCredentials = instanceSettings.withCredentials; this.withCredentials = instanceSettings.withCredentials;
this.render_method = instanceSettings.render_method || 'POST'; this.render_method = instanceSettings.render_method || 'POST';
this.funcDefs = null;
this.funcDefsPromise = null;
this.getQueryOptionsInfo = function() { this.getQueryOptionsInfo = function() {
return { return {
@@ -200,6 +203,35 @@ export function GraphiteDatasource(instanceSettings, $q, backendSrv, templateSrv
let options = optionalOptions || {}; let options = optionalOptions || {};
let interpolatedQuery = templateSrv.replace(query); let interpolatedQuery = templateSrv.replace(query);
// special handling for tag_values(<tag>[,<expression>]*), this is used for template variables
let matches = interpolatedQuery.match(/^tag_values\(([^,]+)((, *[^,]+)*)\)$/);
if (matches) {
const expressions = [];
const exprRegex = /, *([^,]+)/g;
let match;
while ((match = exprRegex.exec(matches[2])) !== null) {
expressions.push(match[1]);
}
options.limit = 10000;
return this.getTagValuesAutoComplete(expressions, matches[1], undefined, options);
}
// special handling for tags(<expression>[,<expression>]*), this is used for template variables
matches = interpolatedQuery.match(/^tags\(([^,]*)((, *[^,]+)*)\)$/);
if (matches) {
const expressions = [];
if (matches[1]) {
expressions.push(matches[1]);
const exprRegex = /, *([^,]+)/g;
let match;
while ((match = exprRegex.exec(matches[2])) !== null) {
expressions.push(match[1]);
}
}
options.limit = 10000;
return this.getTagsAutoComplete(expressions, undefined, options);
}
let httpOptions: any = { let httpOptions: any = {
method: 'GET', method: 'GET',
url: '/metrics/find', url: '/metrics/find',
@@ -210,7 +242,7 @@ export function GraphiteDatasource(instanceSettings, $q, backendSrv, templateSrv
requestId: options.requestId, requestId: options.requestId,
}; };
if (options && options.range) { if (options.range) {
httpOptions.params.from = this.translateTime(options.range.from, false); httpOptions.params.from = this.translateTime(options.range.from, false);
httpOptions.params.until = this.translateTime(options.range.to, true); httpOptions.params.until = this.translateTime(options.range.to, true);
} }
@@ -235,7 +267,7 @@ export function GraphiteDatasource(instanceSettings, $q, backendSrv, templateSrv
requestId: options.requestId, requestId: options.requestId,
}; };
if (options && options.range) { if (options.range) {
httpOptions.params.from = this.translateTime(options.range.from, false); httpOptions.params.from = this.translateTime(options.range.from, false);
httpOptions.params.until = this.translateTime(options.range.to, true); httpOptions.params.until = this.translateTime(options.range.to, true);
} }
@@ -255,12 +287,12 @@ export function GraphiteDatasource(instanceSettings, $q, backendSrv, templateSrv
let httpOptions: any = { let httpOptions: any = {
method: 'GET', method: 'GET',
url: '/tags/' + tag, url: '/tags/' + templateSrv.replace(tag),
// for cancellations // for cancellations
requestId: options.requestId, requestId: options.requestId,
}; };
if (options && options.range) { if (options.range) {
httpOptions.params.from = this.translateTime(options.range.from, false); httpOptions.params.from = this.translateTime(options.range.from, false);
httpOptions.params.until = this.translateTime(options.range.to, true); httpOptions.params.until = this.translateTime(options.range.to, true);
} }
@@ -279,18 +311,29 @@ export function GraphiteDatasource(instanceSettings, $q, backendSrv, templateSrv
}); });
}; };
this.getTagsAutoComplete = (expression, tagPrefix) => { this.getTagsAutoComplete = (expressions, tagPrefix, optionalOptions) => {
let options = optionalOptions || {};
let httpOptions: any = { let httpOptions: any = {
method: 'GET', method: 'GET',
url: '/tags/autoComplete/tags', url: '/tags/autoComplete/tags',
params: { params: {
expr: expression, expr: _.map(expressions, expression => templateSrv.replace(expression)),
}, },
// for cancellations
requestId: options.requestId,
}; };
if (tagPrefix) { if (tagPrefix) {
httpOptions.params.tagPrefix = tagPrefix; httpOptions.params.tagPrefix = tagPrefix;
} }
if (options.limit) {
httpOptions.params.limit = options.limit;
}
if (options.range) {
httpOptions.params.from = this.translateTime(options.range.from, false);
httpOptions.params.until = this.translateTime(options.range.to, true);
}
return this.doGraphiteRequest(httpOptions).then(results => { return this.doGraphiteRequest(httpOptions).then(results => {
if (results.data) { if (results.data) {
@@ -303,19 +346,30 @@ export function GraphiteDatasource(instanceSettings, $q, backendSrv, templateSrv
}); });
}; };
this.getTagValuesAutoComplete = (expression, tag, valuePrefix) => { this.getTagValuesAutoComplete = (expressions, tag, valuePrefix, optionalOptions) => {
let options = optionalOptions || {};
let httpOptions: any = { let httpOptions: any = {
method: 'GET', method: 'GET',
url: '/tags/autoComplete/values', url: '/tags/autoComplete/values',
params: { params: {
expr: expression, expr: _.map(expressions, expression => templateSrv.replace(expression)),
tag: tag, tag: templateSrv.replace(tag),
}, },
// for cancellations
requestId: options.requestId,
}; };
if (valuePrefix) { if (valuePrefix) {
httpOptions.params.valuePrefix = valuePrefix; httpOptions.params.valuePrefix = valuePrefix;
} }
if (options.limit) {
httpOptions.params.limit = options.limit;
}
if (options.range) {
httpOptions.params.from = this.translateTime(options.range.from, false);
httpOptions.params.until = this.translateTime(options.range.to, true);
}
return this.doGraphiteRequest(httpOptions).then(results => { return this.doGraphiteRequest(httpOptions).then(results => {
if (results.data) { if (results.data) {
@@ -328,10 +382,13 @@ export function GraphiteDatasource(instanceSettings, $q, backendSrv, templateSrv
}); });
}; };
this.getVersion = function() { this.getVersion = function(optionalOptions) {
let options = optionalOptions || {};
let httpOptions = { let httpOptions = {
method: 'GET', method: 'GET',
url: '/version/_', // Prevent last / trimming url: '/version',
requestId: options.requestId,
}; };
return this.doGraphiteRequest(httpOptions) return this.doGraphiteRequest(httpOptions)
@@ -347,6 +404,52 @@ export function GraphiteDatasource(instanceSettings, $q, backendSrv, templateSrv
}); });
}; };
this.createFuncInstance = function(funcDef, options?) {
return gfunc.createFuncInstance(funcDef, options, this.funcDefs);
};
this.getFuncDef = function(name) {
return gfunc.getFuncDef(name, this.funcDefs);
};
this.waitForFuncDefsLoaded = function() {
return this.getFuncDefs();
};
this.getFuncDefs = function() {
if (this.funcDefsPromise !== null) {
return this.funcDefsPromise;
}
if (!supportsFunctionIndex(this.graphiteVersion)) {
this.funcDefs = gfunc.getFuncDefs(this.graphiteVersion);
this.funcDefsPromise = Promise.resolve(this.funcDefs);
return this.funcDefsPromise;
}
let httpOptions = {
method: 'GET',
url: '/functions',
};
this.funcDefsPromise = this.doGraphiteRequest(httpOptions)
.then(results => {
if (results.status !== 200 || typeof results.data !== 'object') {
this.funcDefs = gfunc.getFuncDefs(this.graphiteVersion);
} else {
this.funcDefs = gfunc.parseFuncDefs(results.data);
}
return this.funcDefs;
})
.catch(err => {
console.log('Fetching graphite functions error', err);
this.funcDefs = gfunc.getFuncDefs(this.graphiteVersion);
return this.funcDefs;
});
return this.funcDefsPromise;
};
this.testDatasource = function() { this.testDatasource = function() {
return this.metricFindQuery('*').then(function() { return this.metricFindQuery('*').then(function() {
return { status: 'success', message: 'Data source is working' }; return { status: 'success', message: 'Data source is working' };
@@ -440,3 +543,7 @@ export function GraphiteDatasource(instanceSettings, $q, backendSrv, templateSrv
function supportsTags(version: string): boolean { function supportsTags(version: string): boolean {
return isVersionGtOrEq(version, '1.1'); return isVersionGtOrEq(version, '1.1');
} }
function supportsFunctionIndex(version: string): boolean {
return isVersionGtOrEq(version, '1.1');
}

View File

@@ -2,17 +2,18 @@ define([
'angular', 'angular',
'lodash', 'lodash',
'jquery', 'jquery',
'rst2html',
], ],
function (angular, _, $) { function (angular, _, $, rst2html) {
'use strict'; 'use strict';
angular angular
.module('grafana.directives') .module('grafana.directives')
.directive('graphiteFuncEditor', function($compile, templateSrv) { .directive('graphiteFuncEditor', function($compile, templateSrv, popoverSrv) {
var funcSpanTemplate = '<a ng-click="">{{func.def.name}}</a><span>(</span>'; var funcSpanTemplate = '<a ng-click="">{{func.def.name}}</a><span>(</span>';
var paramTemplate = '<input type="text" style="display:none"' + var paramTemplate = '<input type="text" style="display:none"' +
' class="input-mini tight-form-func-param"></input>'; ' class="input-small tight-form-func-param"></input>';
var funcControlsTemplate = var funcControlsTemplate =
'<div class="tight-form-func-controls">' + '<div class="tight-form-func-controls">' +
@@ -29,19 +30,20 @@ function (angular, _, $) {
var $funcControls = $(funcControlsTemplate); var $funcControls = $(funcControlsTemplate);
var ctrl = $scope.ctrl; var ctrl = $scope.ctrl;
var func = $scope.func; var func = $scope.func;
var funcDef = func.def;
var scheduledRelink = false; var scheduledRelink = false;
var paramCountAtLink = 0; var paramCountAtLink = 0;
var cancelBlur = null;
function clickFuncParam(paramIndex) { function clickFuncParam(paramIndex) {
/*jshint validthis:true */ /*jshint validthis:true */
var $link = $(this); var $link = $(this);
var $comma = $link.prev('.comma');
var $input = $link.next(); var $input = $link.next();
$input.val(func.params[paramIndex]); $input.val(func.params[paramIndex]);
$input.css('width', ($link.width() + 16) + 'px');
$comma.removeClass('last');
$link.hide(); $link.hide();
$input.show(); $input.show();
$input.focus(); $input.focus();
@@ -68,31 +70,64 @@ function (angular, _, $) {
} }
} }
function inputBlur(paramIndex) { function paramDef(index) {
if (index < func.def.params.length) {
return func.def.params[index];
}
if (_.last(func.def.params).multiple) {
return _.assign({}, _.last(func.def.params), {optional: true});
}
return {};
}
function switchToLink(inputElem, paramIndex) {
/*jshint validthis:true */ /*jshint validthis:true */
var $input = $(this); var $input = $(inputElem);
clearTimeout(cancelBlur);
cancelBlur = null;
var $link = $input.prev(); var $link = $input.prev();
var $comma = $link.prev('.comma');
var newValue = $input.val(); var newValue = $input.val();
if (newValue !== '' || func.def.params[paramIndex].optional) { // remove optional empty params
$link.html(templateSrv.highlightVariablesAsHtml(newValue)); if (newValue !== '' || paramDef(paramIndex).optional) {
func.updateParam(newValue, paramIndex);
func.updateParam($input.val(), paramIndex); $link.html(newValue ? templateSrv.highlightVariablesAsHtml(newValue) : '&nbsp;');
scheduledRelinkIfNeeded();
$scope.$apply(function() {
ctrl.targetChanged();
});
$input.hide();
$link.show();
} }
scheduledRelinkIfNeeded();
$scope.$apply(function() {
ctrl.targetChanged();
});
if ($link.hasClass('last') && newValue === '') {
$comma.addClass('last');
} else {
$link.removeClass('last');
}
$input.hide();
$link.show();
}
// this = input element
function inputBlur(paramIndex) {
/*jshint validthis:true */
var inputElem = this;
// happens long before the click event on the typeahead options
// need to have long delay because the blur
cancelBlur = setTimeout(function() {
switchToLink(inputElem, paramIndex);
}, 200);
} }
function inputKeyPress(paramIndex, e) { function inputKeyPress(paramIndex, e) {
/*jshint validthis:true */ /*jshint validthis:true */
if(e.which === 13) { if(e.which === 13) {
inputBlur.call(this, paramIndex); $(this).blur();
} }
} }
@@ -104,8 +139,8 @@ function (angular, _, $) {
function addTypeahead($input, paramIndex) { function addTypeahead($input, paramIndex) {
$input.attr('data-provide', 'typeahead'); $input.attr('data-provide', 'typeahead');
var options = funcDef.params[paramIndex].options; var options = paramDef(paramIndex).options;
if (funcDef.params[paramIndex].type === 'int') { if (paramDef(paramIndex).type === 'int') {
options = _.map(options, function(val) { return val.toString(); }); options = _.map(options, function(val) { return val.toString(); });
} }
@@ -114,9 +149,8 @@ function (angular, _, $) {
minLength: 0, minLength: 0,
items: 20, items: 20,
updater: function (value) { updater: function (value) {
setTimeout(function() { $input.val(value);
inputBlur.call($input[0], paramIndex); switchToLink($input[0], paramIndex);
}, 0);
return value; return value;
} }
}); });
@@ -148,18 +182,34 @@ function (angular, _, $) {
$funcControls.appendTo(elem); $funcControls.appendTo(elem);
$funcLink.appendTo(elem); $funcLink.appendTo(elem);
_.each(funcDef.params, function(param, index) { var defParams = _.clone(func.def.params);
if (param.optional && func.params.length <= index) { var lastParam = _.last(func.def.params);
return;
}
if (index > 0) { while (func.params.length >= defParams.length && lastParam && lastParam.multiple) {
$('<span>, </span>').appendTo(elem); defParams.push(_.assign({}, lastParam, {optional: true}));
}
_.each(defParams, function(param, index) {
if (param.optional && func.params.length < index) {
return false;
} }
var paramValue = templateSrv.highlightVariablesAsHtml(func.params[index]); var paramValue = templateSrv.highlightVariablesAsHtml(func.params[index]);
var $paramLink = $('<a ng-click="" class="graphite-func-param-link">' + paramValue + '</a>');
var last = (index >= func.params.length - 1) && param.optional && !paramValue;
if (last && param.multiple) {
paramValue = '+';
}
if (index > 0) {
$('<span class="comma' + (last ? ' last' : '') + '">, </span>').appendTo(elem);
}
var $paramLink = $(
'<a ng-click="" class="graphite-func-param-link' + (last ? ' last' : '') + '">'
+ (paramValue || '&nbsp;') + '</a>');
var $input = $(paramTemplate); var $input = $(paramTemplate);
$input.attr('placeholder', param.name);
paramCountAtLink++; paramCountAtLink++;
@@ -171,10 +221,9 @@ function (angular, _, $) {
$input.keypress(_.partial(inputKeyPress, index)); $input.keypress(_.partial(inputKeyPress, index));
$paramLink.click(_.partial(clickFuncParam, index)); $paramLink.click(_.partial(clickFuncParam, index));
if (funcDef.params[index].options) { if (param.options) {
addTypeahead($input, index); addTypeahead($input, index);
} }
}); });
$('<span>)</span>').appendTo(elem); $('<span>)</span>').appendTo(elem);
@@ -182,7 +231,7 @@ function (angular, _, $) {
$compile(elem.contents())($scope); $compile(elem.contents())($scope);
} }
function ifJustAddedFocusFistParam() { function ifJustAddedFocusFirstParam() {
if ($scope.func.added) { if ($scope.func.added) {
$scope.func.added = false; $scope.func.added = false;
setTimeout(function() { setTimeout(function() {
@@ -223,7 +272,20 @@ function (angular, _, $) {
} }
if ($target.hasClass('fa-question-circle')) { if ($target.hasClass('fa-question-circle')) {
window.open("http://graphite.readthedocs.org/en/latest/functions.html#graphite.render.functions." + funcDef.name,'_blank'); var funcDef = ctrl.datasource.getFuncDef(func.def.name);
if (funcDef && funcDef.description) {
popoverSrv.show({
element: e.target,
position: 'bottom left',
classNames: 'drop-popover drop-function-def',
template: '<div style="overflow:auto;max-height:30rem;">'
+ '<h4>' + funcDef.name + '</h4>' + rst2html(funcDef.description) + '</div>',
openOn: 'click',
});
} else {
window.open(
"http://graphite.readthedocs.org/en/latest/functions.html#graphite.render.functions." + func.def.name,'_blank');
}
return; return;
} }
}); });
@@ -233,7 +295,7 @@ function (angular, _, $) {
elem.children().remove(); elem.children().remove();
addElementsAndCompile(); addElementsAndCompile();
ifJustAddedFocusFistParam(); ifJustAddedFocusFirstParam();
registerFuncControlsToggle(); registerFuncControlsToggle();
registerFuncControlsActions(); registerFuncControlsActions();
} }

File diff suppressed because it is too large Load Diff

View File

@@ -1,8 +1,8 @@
import _ from 'lodash'; import _ from 'lodash';
import gfunc from './gfunc';
import { Parser } from './parser'; import { Parser } from './parser';
export default class GraphiteQuery { export default class GraphiteQuery {
datasource: any;
target: any; target: any;
functions: any[]; functions: any[];
segments: any[]; segments: any[];
@@ -15,7 +15,8 @@ export default class GraphiteQuery {
scopedVars: any; scopedVars: any;
/** @ngInject */ /** @ngInject */
constructor(target, templateSrv?, scopedVars?) { constructor(datasource, target, templateSrv?, scopedVars?) {
this.datasource = datasource;
this.target = target; this.target = target;
this.parseTarget(); this.parseTarget();
@@ -86,7 +87,7 @@ export default class GraphiteQuery {
switch (astNode.type) { switch (astNode.type) {
case 'function': case 'function':
var innerFunc = gfunc.createFuncInstance(astNode.name, { var innerFunc = this.datasource.createFuncInstance(astNode.name, {
withDefaultParams: false, withDefaultParams: false,
}); });
_.each(astNode.params, param => { _.each(astNode.params, param => {
@@ -133,7 +134,7 @@ export default class GraphiteQuery {
moveAliasFuncLast() { moveAliasFuncLast() {
var aliasFunc = _.find(this.functions, function(func) { var aliasFunc = _.find(this.functions, function(func) {
return func.def.name === 'alias' || func.def.name === 'aliasByNode' || func.def.name === 'aliasByMetric'; return func.def.name.startsWith('alias');
}); });
if (aliasFunc) { if (aliasFunc) {
@@ -143,7 +144,7 @@ export default class GraphiteQuery {
} }
addFunctionParameter(func, value) { addFunctionParameter(func, value) {
if (func.params.length >= func.def.params.length) { if (func.params.length >= func.def.params.length && !_.get(_.last(func.def.params), 'multiple', false)) {
throw { message: 'too many parameters for function ' + func.def.name }; throw { message: 'too many parameters for function ' + func.def.name };
} }
func.params.push(value); func.params.push(value);
@@ -208,7 +209,7 @@ export default class GraphiteQuery {
} }
splitSeriesByTagParams(func) { splitSeriesByTagParams(func) {
const tagPattern = /([^\!=~]+)([\!=~]+)([^\!=~]+)/; const tagPattern = /([^\!=~]+)(\!?=~?)(.*)/;
return _.flatten( return _.flatten(
_.map(func.params, (param: string) => { _.map(func.params, (param: string) => {
let matches = tagPattern.exec(param); let matches = tagPattern.exec(param);

View File

@@ -10,30 +10,50 @@
<label class="gf-form-label width-6 query-keyword">Series</label> <label class="gf-form-label width-6 query-keyword">Series</label>
</div> </div>
<div ng-repeat="tag in ctrl.queryModel.tags" class="gf-form"> <div ng-if="ctrl.queryModel.seriesByTagUsed" ng-repeat="tag in ctrl.queryModel.tags" class="gf-form">
<gf-form-dropdown model="tag.key" lookup-text="false" allow-custom="false" label-mode="true" css-class="query-segment-key" <gf-form-dropdown
model="tag.key"
lookup-text="false"
allow-custom="true"
label-mode="true"
placeholder="Tag key"
css-class="query-segment-key"
get-options="ctrl.getTags($index, $query)" get-options="ctrl.getTags($index, $query)"
on-change="ctrl.tagChanged(tag, $index)"> on-change="ctrl.tagChanged(tag, $index)"
</gf-form-dropdown> />
<gf-form-dropdown model="tag.operator" lookup-text="false" allow-custom="false" label-mode="true" css-class="query-segment-operator" <gf-form-dropdown
model="tag.operator"
lookup-text="false"
allow-custom="false"
label-mode="true"
css-class="query-segment-operator"
get-options="ctrl.getTagOperators()" get-options="ctrl.getTagOperators()"
on-change="ctrl.tagChanged(tag, $index)" on-change="ctrl.tagChanged(tag, $index)"
min-input-width="30"> min-input-width="30"
</gf-form-dropdown> />
<gf-form-dropdown model="tag.value" lookup-text="false" allow-custom="false" label-mode="true" css-class="query-segment-value" <gf-form-dropdown
model="tag.value"
lookup-text="false"
allow-custom="true"
label-mode="true"
css-class="query-segment-value"
placeholder="Tag value"
get-options="ctrl.getTagValues(tag, $index, $query)" get-options="ctrl.getTagValues(tag, $index, $query)"
on-change="ctrl.tagChanged(tag, $index)"> on-change="ctrl.tagChanged(tag, $index)"
</gf-form-dropdown> />
<label class="gf-form-label query-keyword" ng-if="ctrl.showDelimiter($index)">AND</label> <label class="gf-form-label query-keyword" ng-if="ctrl.showDelimiter($index)">AND</label>
</div> </div>
<div ng-repeat="segment in ctrl.segments" role="menuitem" class="gf-form"> <div ng-if="ctrl.queryModel.seriesByTagUsed" ng-repeat="segment in ctrl.addTagSegments" role="menuitem" class="gf-form">
<metric-segment segment="segment" get-options="ctrl.getAltSegments($index)" on-change="ctrl.segmentValueChanged(segment, $index)"></metric-segment> <metric-segment segment="segment" get-options="ctrl.getTagsAsSegments($query)" on-change="ctrl.addNewTag(segment)" />
</div> </div>
<div ng-if="ctrl.queryModel.seriesByTagUsed" ng-repeat="segment in ctrl.addTagSegments" role="menuitem" class="gf-form"> <div ng-if="!ctrl.queryModel.seriesByTagUsed" ng-repeat="segment in ctrl.segments" role="menuitem" class="gf-form">
<metric-segment segment="segment" get-options="ctrl.getTagsAsSegments()" on-change="ctrl.addNewTag(segment)"> <metric-segment segment="segment" get-options="ctrl.getAltSegments($index, $query)" on-change="ctrl.segmentValueChanged(segment, $index)" />
</metric-segment> </div>
<div ng-if="ctrl.paused" class="gf-form">
<a ng-click="ctrl.unpause()" class="gf-form-label query-part"><i class="fa fa-play"></i></a>
</div> </div>
<div class="gf-form gf-form--grow"> <div class="gf-form gf-form--grow">

View File

@@ -2,7 +2,6 @@ import './add_graphite_func';
import './func_editor'; import './func_editor';
import _ from 'lodash'; import _ from 'lodash';
import gfunc from './gfunc';
import GraphiteQuery from './graphite_query'; import GraphiteQuery from './graphite_query';
import { QueryCtrl } from 'app/plugins/sdk'; import { QueryCtrl } from 'app/plugins/sdk';
import appEvents from 'app/core/app_events'; import appEvents from 'app/core/app_events';
@@ -18,17 +17,19 @@ export class GraphiteQueryCtrl extends QueryCtrl {
addTagSegments: any[]; addTagSegments: any[];
removeTagValue: string; removeTagValue: string;
supportsTags: boolean; supportsTags: boolean;
paused: boolean;
/** @ngInject **/ /** @ngInject **/
constructor($scope, $injector, private uiSegmentSrv, private templateSrv) { constructor($scope, $injector, private uiSegmentSrv, private templateSrv, $timeout) {
super($scope, $injector); super($scope, $injector);
this.supportsTags = this.datasource.supportsTags; this.supportsTags = this.datasource.supportsTags;
this.paused = false;
this.target.target = this.target.target || '';
if (this.target) { this.datasource.waitForFuncDefsLoaded().then(() => {
this.target.target = this.target.target || ''; this.queryModel = new GraphiteQuery(this.datasource, this.target, templateSrv);
this.queryModel = new GraphiteQuery(this.target, templateSrv);
this.buildSegments(); this.buildSegments();
} });
this.removeTagValue = '-- remove tag --'; this.removeTagValue = '-- remove tag --';
} }
@@ -104,8 +105,11 @@ export class GraphiteQueryCtrl extends QueryCtrl {
}); });
} }
getAltSegments(index) { getAltSegments(index, prefix) {
var query = index === 0 ? '*' : this.queryModel.getSegmentPathUpTo(index) + '.*'; var query = prefix && prefix.length > 0 ? '*' + prefix + '*' : '*';
if (index > 0) {
query = this.queryModel.getSegmentPathUpTo(index) + '.' + query;
}
var options = { var options = {
range: this.panelCtrl.range, range: this.panelCtrl.range,
requestId: 'get-alt-segments', requestId: 'get-alt-segments',
@@ -121,7 +125,7 @@ export class GraphiteQueryCtrl extends QueryCtrl {
}); });
}); });
if (altSegments.length === 0) { if (index > 0 && altSegments.length === 0) {
return altSegments; return altSegments;
} }
@@ -158,7 +162,7 @@ export class GraphiteQueryCtrl extends QueryCtrl {
if (this.supportsTags && index === 0) { if (this.supportsTags && index === 0) {
this.removeTaggedEntry(altSegments); this.removeTaggedEntry(altSegments);
return this.addAltTagSegments(index, altSegments); return this.addAltTagSegments(prefix, altSegments);
} else { } else {
return altSegments; return altSegments;
} }
@@ -168,8 +172,8 @@ export class GraphiteQueryCtrl extends QueryCtrl {
}); });
} }
addAltTagSegments(index, altSegments) { addAltTagSegments(prefix, altSegments) {
return this.getTagsAsSegments().then(tagSegments => { return this.getTagsAsSegments(prefix).then(tagSegments => {
tagSegments = _.map(tagSegments, segment => { tagSegments = _.map(tagSegments, segment => {
segment.value = TAG_PREFIX + segment.value; segment.value = TAG_PREFIX + segment.value;
return segment; return segment;
@@ -192,6 +196,7 @@ export class GraphiteQueryCtrl extends QueryCtrl {
if (segment.type === 'tag') { if (segment.type === 'tag') {
let tag = removeTagPrefix(segment.value); let tag = removeTagPrefix(segment.value);
this.pause();
this.addSeriesByTagFunc(tag); this.addSeriesByTagFunc(tag);
return; return;
} }
@@ -236,13 +241,13 @@ export class GraphiteQueryCtrl extends QueryCtrl {
var oldTarget = this.queryModel.target.target; var oldTarget = this.queryModel.target.target;
this.updateModelTarget(); this.updateModelTarget();
if (this.queryModel.target !== oldTarget) { if (this.queryModel.target !== oldTarget && !this.paused) {
this.panelCtrl.refresh(); this.panelCtrl.refresh();
} }
} }
addFunction(funcDef) { addFunction(funcDef) {
var newFunc = gfunc.createFuncInstance(funcDef, { var newFunc = this.datasource.createFuncInstance(funcDef, {
withDefaultParams: true, withDefaultParams: true,
}); });
newFunc.added = true; newFunc.added = true;
@@ -268,11 +273,10 @@ export class GraphiteQueryCtrl extends QueryCtrl {
} }
addSeriesByTagFunc(tag) { addSeriesByTagFunc(tag) {
let funcDef = gfunc.getFuncDef('seriesByTag'); let newFunc = this.datasource.createFuncInstance('seriesByTag', {
let newFunc = gfunc.createFuncInstance(funcDef, {
withDefaultParams: false, withDefaultParams: false,
}); });
let tagParam = `${tag}=select tag value`; let tagParam = `${tag}=`;
newFunc.params = [tagParam]; newFunc.params = [tagParam];
this.queryModel.addFunction(newFunc); this.queryModel.addFunction(newFunc);
newFunc.added = true; newFunc.added = true;
@@ -314,9 +318,9 @@ export class GraphiteQueryCtrl extends QueryCtrl {
}); });
} }
getTagsAsSegments() { getTagsAsSegments(tagPrefix) {
let tagExpressions = this.queryModel.renderTagExpressions(); let tagExpressions = this.queryModel.renderTagExpressions();
return this.datasource.getTagsAutoComplete(tagExpressions).then(values => { return this.datasource.getTagsAutoComplete(tagExpressions, tagPrefix).then(values => {
return _.map(values, val => { return _.map(values, val => {
return this.uiSegmentSrv.newSegment({ return this.uiSegmentSrv.newSegment({
value: val.text, value: val.text,
@@ -355,7 +359,7 @@ export class GraphiteQueryCtrl extends QueryCtrl {
addNewTag(segment) { addNewTag(segment) {
let newTagKey = segment.value; let newTagKey = segment.value;
let newTag = { key: newTagKey, operator: '=', value: 'select tag value' }; let newTag = { key: newTagKey, operator: '=', value: '' };
this.queryModel.addTag(newTag); this.queryModel.addTag(newTag);
this.targetChanged(); this.targetChanged();
this.fixTagSegments(); this.fixTagSegments();
@@ -374,6 +378,15 @@ export class GraphiteQueryCtrl extends QueryCtrl {
showDelimiter(index) { showDelimiter(index) {
return index !== this.queryModel.tags.length - 1; return index !== this.queryModel.tags.length - 1;
} }
pause() {
this.paused = true;
}
unpause() {
this.paused = false;
this.panelCtrl.refresh();
}
} }
function mapToDropdownOptions(results) { function mapToDropdownOptions(results) {

View File

@@ -5,7 +5,8 @@ describe('when creating func instance from func names', function() {
var func = gfunc.createFuncInstance('sumSeries'); var func = gfunc.createFuncInstance('sumSeries');
expect(func).toBeTruthy(); expect(func).toBeTruthy();
expect(func.def.name).toEqual('sumSeries'); expect(func.def.name).toEqual('sumSeries');
expect(func.def.params.length).toEqual(5); expect(func.def.params.length).toEqual(1);
expect(func.def.params[0].multiple).toEqual(true);
expect(func.def.defaultParams.length).toEqual(1); expect(func.def.defaultParams.length).toEqual(1);
}); });
@@ -74,10 +75,10 @@ describe('when rendering func instance', function() {
}); });
}); });
describe('when requesting function categories', function() { describe('when requesting function definitions', function() {
it('should return function categories', function() { it('should return function definitions', function() {
var catIndex = gfunc.getCategories('1.0'); var funcIndex = gfunc.getFuncDefs('1.0');
expect(catIndex.Special.length).toBeGreaterThan(8); expect(Object.keys(funcIndex).length).toBeGreaterThan(8);
}); });
}); });

View File

@@ -24,6 +24,10 @@ describe('GraphiteQueryCtrl', function() {
ctx.scope = $rootScope.$new(); ctx.scope = $rootScope.$new();
ctx.target = { target: 'aliasByNode(scaleToSeconds(test.prod.*,1),2)' }; ctx.target = { target: 'aliasByNode(scaleToSeconds(test.prod.*,1),2)' };
ctx.datasource.metricFindQuery = sinon.stub().returns(ctx.$q.when([])); ctx.datasource.metricFindQuery = sinon.stub().returns(ctx.$q.when([]));
ctx.datasource.getFuncDefs = sinon.stub().returns(ctx.$q.when(gfunc.getFuncDefs('1.0')));
ctx.datasource.getFuncDef = gfunc.getFuncDef;
ctx.datasource.waitForFuncDefsLoaded = sinon.stub().returns(ctx.$q.when(null));
ctx.datasource.createFuncInstance = gfunc.createFuncInstance;
ctx.panelCtrl = { panel: {} }; ctx.panelCtrl = { panel: {} };
ctx.panelCtrl = { ctx.panelCtrl = {
panel: { panel: {
@@ -180,7 +184,21 @@ describe('GraphiteQueryCtrl', function() {
ctx.ctrl.target.target = 'scaleToSeconds(#A, 60)'; ctx.ctrl.target.target = 'scaleToSeconds(#A, 60)';
ctx.ctrl.datasource.metricFindQuery = sinon.stub().returns(ctx.$q.when([{ expandable: false }])); ctx.ctrl.datasource.metricFindQuery = sinon.stub().returns(ctx.$q.when([{ expandable: false }]));
ctx.ctrl.parseTarget(); ctx.ctrl.parseTarget();
});
it('should add function params', function() {
expect(ctx.ctrl.queryModel.segments.length).to.be(1);
expect(ctx.ctrl.queryModel.segments[0].value).to.be('#A');
expect(ctx.ctrl.queryModel.functions[0].params.length).to.be(1);
expect(ctx.ctrl.queryModel.functions[0].params[0]).to.be(60);
});
it('target should remain the same', function() {
expect(ctx.ctrl.target.target).to.be('scaleToSeconds(#A, 60)');
});
it('targetFull should include nested queries', function() {
ctx.ctrl.panelCtrl.panel.targets = [ ctx.ctrl.panelCtrl.panel.targets = [
{ {
target: 'nested.query.count', target: 'nested.query.count',
@@ -189,13 +207,9 @@ describe('GraphiteQueryCtrl', function() {
]; ];
ctx.ctrl.updateModelTarget(); ctx.ctrl.updateModelTarget();
});
it('target should remain the same', function() {
expect(ctx.ctrl.target.target).to.be('scaleToSeconds(#A, 60)'); expect(ctx.ctrl.target.target).to.be('scaleToSeconds(#A, 60)');
});
it('targetFull should include nexted queries', function() {
expect(ctx.ctrl.target.targetFull).to.be('scaleToSeconds(nested.query.count, 60)'); expect(ctx.ctrl.target.targetFull).to.be('scaleToSeconds(nested.query.count, 60)');
}); });
}); });
@@ -271,12 +285,12 @@ describe('GraphiteQueryCtrl', function() {
}); });
it('should update tags with default value', function() { it('should update tags with default value', function() {
const expected = [{ key: 'tag1', operator: '=', value: 'select tag value' }]; const expected = [{ key: 'tag1', operator: '=', value: '' }];
expect(ctx.ctrl.queryModel.tags).to.eql(expected); expect(ctx.ctrl.queryModel.tags).to.eql(expected);
}); });
it('should update target', function() { it('should update target', function() {
const expected = "seriesByTag('tag1=select tag value')"; const expected = "seriesByTag('tag1=')";
expect(ctx.ctrl.target.target).to.eql(expected); expect(ctx.ctrl.target.target).to.eql(expected);
}); });
}); });

View File

@@ -89,7 +89,8 @@
} }
} }
input[type="text"].tight-form-func-param { input[type='text'].tight-form-func-param {
font-size: 0.875rem;
background: transparent; background: transparent;
border: none; border: none;
margin: 0; margin: 0;
@@ -129,32 +130,6 @@ input[type="text"].tight-form-func-param {
} }
} }
input[type="text"].tight-form-func-param {
background: transparent;
border: none;
margin: 0;
padding: 0;
}
.tight-form-func-controls {
display: none;
text-align: center;
.fa-arrow-left {
float: left;
position: relative;
top: 2px;
}
.fa-arrow-right {
float: right;
position: relative;
top: 2px;
}
.fa-remove {
margin-left: 10px;
}
}
.query-troubleshooter { .query-troubleshooter {
font-size: $font-size-sm; font-size: $font-size-sm;
margin: $gf-form-margin; margin: $gf-form-margin;
@@ -176,3 +151,34 @@ input[type="text"].tight-form-func-param {
.query-troubleshooter__body { .query-troubleshooter__body {
padding: $spacer 0; padding: $spacer 0;
} }
.rst-text::before {
content: ' ';
}
.rst-unknown.rst-directive {
font-family: monospace;
margin-bottom: 1rem;
}
.rst-interpreted_text {
font-family: monospace;
display: inline;
}
.rst-bullet-list {
padding-left: 1.5rem;
margin-bottom: 1rem;
}
.rst-paragraph:last-child {
margin-bottom: 0;
}
.drop-element.drop-popover.drop-function-def .drop-content {
max-width: 30rem;
}
.rst-literal-block .rst-text {
display: block;
}

View File

@@ -6,4 +6,12 @@
min-width: 100px; min-width: 100px;
text-align: center; text-align: center;
} }
.last {
display: none;
}
&:hover .last {
display: inline;
}
} }

View File

@@ -19,6 +19,8 @@ module.exports = function(grunt) {
]); ]);
grunt.registerTask('precommit', [ grunt.registerTask('precommit', [
'jscs',
'jshint',
'sasslint', 'sasslint',
'exec:tslint', 'exec:tslint',
'no-only-tests' 'no-only-tests'

183
yarn.lock
View File

@@ -263,6 +263,10 @@ acorn-dynamic-import@^2.0.0:
dependencies: dependencies:
acorn "^4.0.3" acorn "^4.0.3"
acorn-es7-plugin@^1.0.12:
version "1.1.7"
resolved "https://registry.yarnpkg.com/acorn-es7-plugin/-/acorn-es7-plugin-1.1.7.tgz#f2ee1f3228a90eead1245f9ab1922eb2e71d336b"
acorn-globals@^4.0.0: acorn-globals@^4.0.0:
version "4.1.0" version "4.1.0"
resolved "https://registry.yarnpkg.com/acorn-globals/-/acorn-globals-4.1.0.tgz#ab716025dbe17c54d3ef81d32ece2b2d99fe2538" resolved "https://registry.yarnpkg.com/acorn-globals/-/acorn-globals-4.1.0.tgz#ab716025dbe17c54d3ef81d32ece2b2d99fe2538"
@@ -279,7 +283,7 @@ acorn@^3.0.4:
version "3.3.0" version "3.3.0"
resolved "https://registry.yarnpkg.com/acorn/-/acorn-3.3.0.tgz#45e37fb39e8da3f25baee3ff5369e2bb5f22017a" resolved "https://registry.yarnpkg.com/acorn/-/acorn-3.3.0.tgz#45e37fb39e8da3f25baee3ff5369e2bb5f22017a"
acorn@^4.0.3: acorn@^4.0.0, acorn@^4.0.3:
version "4.0.13" version "4.0.13"
resolved "https://registry.yarnpkg.com/acorn/-/acorn-4.0.13.tgz#105495ae5361d697bd195c825192e1ad7f253787" resolved "https://registry.yarnpkg.com/acorn/-/acorn-4.0.13.tgz#105495ae5361d697bd195c825192e1ad7f253787"
@@ -525,6 +529,10 @@ array-equal@^1.0.0:
version "1.0.0" version "1.0.0"
resolved "https://registry.yarnpkg.com/array-equal/-/array-equal-1.0.0.tgz#8c2a5ef2472fd9ea742b04c77a75093ba2757c93" resolved "https://registry.yarnpkg.com/array-equal/-/array-equal-1.0.0.tgz#8c2a5ef2472fd9ea742b04c77a75093ba2757c93"
array-filter@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/array-filter/-/array-filter-1.0.0.tgz#baf79e62e6ef4c2a4c0b831232daffec251f9d83"
array-filter@~0.0.0: array-filter@~0.0.0:
version "0.0.1" version "0.0.1"
resolved "https://registry.yarnpkg.com/array-filter/-/array-filter-0.0.1.tgz#7da8cf2e26628ed732803581fd21f67cacd2eeec" resolved "https://registry.yarnpkg.com/array-filter/-/array-filter-0.0.1.tgz#7da8cf2e26628ed732803581fd21f67cacd2eeec"
@@ -1512,6 +1520,10 @@ call-limit@~1.1.0:
version "1.1.0" version "1.1.0"
resolved "https://registry.yarnpkg.com/call-limit/-/call-limit-1.1.0.tgz#6fd61b03f3da42a2cd0ec2b60f02bd0e71991fea" resolved "https://registry.yarnpkg.com/call-limit/-/call-limit-1.1.0.tgz#6fd61b03f3da42a2cd0ec2b60f02bd0e71991fea"
call-signature@0.0.2:
version "0.0.2"
resolved "https://registry.yarnpkg.com/call-signature/-/call-signature-0.0.2.tgz#a84abc825a55ef4cb2b028bd74e205a65b9a4996"
caller-path@^0.1.0: caller-path@^0.1.0:
version "0.1.0" version "0.1.0"
resolved "https://registry.yarnpkg.com/caller-path/-/caller-path-0.1.0.tgz#94085ef63581ecd3daa92444a8fe94e82577751f" resolved "https://registry.yarnpkg.com/caller-path/-/caller-path-0.1.0.tgz#94085ef63581ecd3daa92444a8fe94e82577751f"
@@ -2072,6 +2084,10 @@ core-js@^1.0.0:
version "1.2.7" version "1.2.7"
resolved "https://registry.yarnpkg.com/core-js/-/core-js-1.2.7.tgz#652294c14651db28fa93bd2d5ff2983a4f08c636" resolved "https://registry.yarnpkg.com/core-js/-/core-js-1.2.7.tgz#652294c14651db28fa93bd2d5ff2983a4f08c636"
core-js@^2.0.0:
version "2.5.3"
resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.5.3.tgz#8acc38345824f16d8365b7c9b4259168e8ed603e"
core-js@^2.2.0, core-js@^2.4.0, core-js@^2.5.0: core-js@^2.2.0, core-js@^2.4.0, core-js@^2.5.0:
version "2.5.1" version "2.5.1"
resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.5.1.tgz#ae6874dc66937789b80754ff5428df66819ca50b" resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.5.1.tgz#ae6874dc66937789b80754ff5428df66819ca50b"
@@ -2744,6 +2760,10 @@ di@^0.0.1:
version "0.0.1" version "0.0.1"
resolved "https://registry.yarnpkg.com/di/-/di-0.0.1.tgz#806649326ceaa7caa3306d75d985ea2748ba913c" resolved "https://registry.yarnpkg.com/di/-/di-0.0.1.tgz#806649326ceaa7caa3306d75d985ea2748ba913c"
diff-match-patch@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/diff-match-patch/-/diff-match-patch-1.0.0.tgz#1cc3c83a490d67f95d91e39f6ad1f2e086b63048"
diff@3.3.1: diff@3.3.1:
version "3.3.1" version "3.3.1"
resolved "https://registry.yarnpkg.com/diff/-/diff-3.3.1.tgz#aa8567a6eed03c531fc89d3f711cd0e5259dec75" resolved "https://registry.yarnpkg.com/diff/-/diff-3.3.1.tgz#aa8567a6eed03c531fc89d3f711cd0e5259dec75"
@@ -2891,6 +2911,10 @@ each-async@^1.0.0:
onetime "^1.0.0" onetime "^1.0.0"
set-immediate-shim "^1.0.0" set-immediate-shim "^1.0.0"
eastasianwidth@^0.1.1:
version "0.1.1"
resolved "https://registry.yarnpkg.com/eastasianwidth/-/eastasianwidth-0.1.1.tgz#44d656de9da415694467335365fb3147b8572b7c"
ecc-jsbn@~0.1.1: ecc-jsbn@~0.1.1:
version "0.1.1" version "0.1.1"
resolved "https://registry.yarnpkg.com/ecc-jsbn/-/ecc-jsbn-0.1.1.tgz#0fc73a9ed5f0d53c38193398523ef7e543777505" resolved "https://registry.yarnpkg.com/ecc-jsbn/-/ecc-jsbn-0.1.1.tgz#0fc73a9ed5f0d53c38193398523ef7e543777505"
@@ -2939,6 +2963,20 @@ emojis-list@^2.0.0:
version "2.1.0" version "2.1.0"
resolved "https://registry.yarnpkg.com/emojis-list/-/emojis-list-2.1.0.tgz#4daa4d9db00f9819880c79fa457ae5b09a1fd389" resolved "https://registry.yarnpkg.com/emojis-list/-/emojis-list-2.1.0.tgz#4daa4d9db00f9819880c79fa457ae5b09a1fd389"
empower-core@^0.6.2:
version "0.6.2"
resolved "https://registry.yarnpkg.com/empower-core/-/empower-core-0.6.2.tgz#5adef566088e31fba80ba0a36df47d7094169144"
dependencies:
call-signature "0.0.2"
core-js "^2.0.0"
empower@^1.2.3:
version "1.2.3"
resolved "https://registry.yarnpkg.com/empower/-/empower-1.2.3.tgz#6f0da73447f4edd838fec5c60313a88ba5cb852b"
dependencies:
core-js "^2.0.0"
empower-core "^0.6.2"
encodeurl@~1.0.1: encodeurl@~1.0.1:
version "1.0.1" version "1.0.1"
resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.1.tgz#79e3d58655346909fe6f0f45a5de68103b294d20" resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.1.tgz#79e3d58655346909fe6f0f45a5de68103b294d20"
@@ -3266,6 +3304,12 @@ esprima@^4.0.0:
version "4.0.0" version "4.0.0"
resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.0.tgz#4499eddcd1110e0b218bacf2fa7f7f59f55ca804" resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.0.tgz#4499eddcd1110e0b218bacf2fa7f7f59f55ca804"
espurify@^1.6.0:
version "1.7.0"
resolved "https://registry.yarnpkg.com/espurify/-/espurify-1.7.0.tgz#1c5cf6cbccc32e6f639380bd4f991fab9ba9d226"
dependencies:
core-js "^2.0.0"
esrecurse@^4.1.0: esrecurse@^4.1.0:
version "4.2.0" version "4.2.0"
resolved "https://registry.yarnpkg.com/esrecurse/-/esrecurse-4.2.0.tgz#fa9568d98d3823f9a41d91e902dcab9ea6e5b163" resolved "https://registry.yarnpkg.com/esrecurse/-/esrecurse-4.2.0.tgz#fa9568d98d3823f9a41d91e902dcab9ea6e5b163"
@@ -3300,7 +3344,7 @@ eventemitter3@1.x.x:
version "1.2.0" version "1.2.0"
resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-1.2.0.tgz#1c86991d816ad1e504750e73874224ecf3bec508" resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-1.2.0.tgz#1c86991d816ad1e504750e73874224ecf3bec508"
eventemitter3@^2.0.2: eventemitter3@^2.0.3:
version "2.0.3" version "2.0.3"
resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-2.0.3.tgz#b5e1079b59fb5e1ba2771c0a993be060a58c99ba" resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-2.0.3.tgz#b5e1079b59fb5e1ba2771c0a993be060a58c99ba"
@@ -7058,7 +7102,7 @@ object-is@^1.0.1:
version "1.0.1" version "1.0.1"
resolved "https://registry.yarnpkg.com/object-is/-/object-is-1.0.1.tgz#0aa60ec9989a0b3ed795cf4d06f62cf1ad6539b6" resolved "https://registry.yarnpkg.com/object-is/-/object-is-1.0.1.tgz#0aa60ec9989a0b3ed795cf4d06f62cf1ad6539b6"
object-keys@^1.0.11, object-keys@^1.0.8: object-keys@^1.0.0, object-keys@^1.0.11, object-keys@^1.0.8:
version "1.0.11" version "1.0.11"
resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.0.11.tgz#c54601778ad560f1142ce0e01bcca8b56d13426d" resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.0.11.tgz#c54601778ad560f1142ce0e01bcca8b56d13426d"
@@ -7809,6 +7853,94 @@ postcss@^6.0.0, postcss@^6.0.1, postcss@^6.0.8:
source-map "^0.6.1" source-map "^0.6.1"
supports-color "^4.4.0" supports-color "^4.4.0"
power-assert-context-formatter@^1.0.7:
version "1.1.1"
resolved "https://registry.yarnpkg.com/power-assert-context-formatter/-/power-assert-context-formatter-1.1.1.tgz#edba352d3ed8a603114d667265acce60d689ccdf"
dependencies:
core-js "^2.0.0"
power-assert-context-traversal "^1.1.1"
power-assert-context-reducer-ast@^1.0.7:
version "1.1.2"
resolved "https://registry.yarnpkg.com/power-assert-context-reducer-ast/-/power-assert-context-reducer-ast-1.1.2.tgz#484a99e26f4973ff8832e5c5cc756702e6094174"
dependencies:
acorn "^4.0.0"
acorn-es7-plugin "^1.0.12"
core-js "^2.0.0"
espurify "^1.6.0"
estraverse "^4.2.0"
power-assert-context-traversal@^1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/power-assert-context-traversal/-/power-assert-context-traversal-1.1.1.tgz#88cabca0d13b6359f07d3d3e8afa699264577ed9"
dependencies:
core-js "^2.0.0"
estraverse "^4.1.0"
power-assert-formatter@^1.3.1:
version "1.4.1"
resolved "https://registry.yarnpkg.com/power-assert-formatter/-/power-assert-formatter-1.4.1.tgz#5dc125ed50a3dfb1dda26c19347f3bf58ec2884a"
dependencies:
core-js "^2.0.0"
power-assert-context-formatter "^1.0.7"
power-assert-context-reducer-ast "^1.0.7"
power-assert-renderer-assertion "^1.0.7"
power-assert-renderer-comparison "^1.0.7"
power-assert-renderer-diagram "^1.0.7"
power-assert-renderer-file "^1.0.7"
power-assert-renderer-assertion@^1.0.7:
version "1.1.1"
resolved "https://registry.yarnpkg.com/power-assert-renderer-assertion/-/power-assert-renderer-assertion-1.1.1.tgz#cbfc0e77e0086a8f96af3f1d8e67b9ee7e28ce98"
dependencies:
power-assert-renderer-base "^1.1.1"
power-assert-util-string-width "^1.1.1"
power-assert-renderer-base@^1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/power-assert-renderer-base/-/power-assert-renderer-base-1.1.1.tgz#96a650c6fd05ee1bc1f66b54ad61442c8b3f63eb"
power-assert-renderer-comparison@^1.0.7:
version "1.1.1"
resolved "https://registry.yarnpkg.com/power-assert-renderer-comparison/-/power-assert-renderer-comparison-1.1.1.tgz#d7439d97d85156be4e30a00f2fb5a72514ce3c08"
dependencies:
core-js "^2.0.0"
diff-match-patch "^1.0.0"
power-assert-renderer-base "^1.1.1"
stringifier "^1.3.0"
type-name "^2.0.1"
power-assert-renderer-diagram@^1.0.7:
version "1.1.2"
resolved "https://registry.yarnpkg.com/power-assert-renderer-diagram/-/power-assert-renderer-diagram-1.1.2.tgz#655f8f711935a9b6d541b86327654717c637a986"
dependencies:
core-js "^2.0.0"
power-assert-renderer-base "^1.1.1"
power-assert-util-string-width "^1.1.1"
stringifier "^1.3.0"
power-assert-renderer-file@^1.0.7:
version "1.1.1"
resolved "https://registry.yarnpkg.com/power-assert-renderer-file/-/power-assert-renderer-file-1.1.1.tgz#a37e2bbd178ccacd04e78dbb79c92fe34933c5e7"
dependencies:
power-assert-renderer-base "^1.1.1"
power-assert-util-string-width@^1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/power-assert-util-string-width/-/power-assert-util-string-width-1.1.1.tgz#be659eb7937fdd2e6c9a77268daaf64bd5b7c592"
dependencies:
eastasianwidth "^0.1.1"
power-assert@^1.2.0:
version "1.4.4"
resolved "https://registry.yarnpkg.com/power-assert/-/power-assert-1.4.4.tgz#9295ea7437196f5a601fde420f042631186d7517"
dependencies:
define-properties "^1.1.2"
empower "^1.2.3"
power-assert-formatter "^1.3.1"
universal-deep-strict-equal "^1.2.1"
xtend "^4.0.0"
prebuild-install@^2.3.0: prebuild-install@^2.3.0:
version "2.3.0" version "2.3.0"
resolved "https://registry.yarnpkg.com/prebuild-install/-/prebuild-install-2.3.0.tgz#19481247df728b854ab57b187ce234211311b485" resolved "https://registry.yarnpkg.com/prebuild-install/-/prebuild-install-2.3.0.tgz#19481247df728b854ab57b187ce234211311b485"
@@ -8651,6 +8783,15 @@ restore-cursor@^1.0.1:
exit-hook "^1.0.0" exit-hook "^1.0.0"
onetime "^1.0.0" onetime "^1.0.0"
restructured@0.0.11:
version "0.0.11"
resolved "https://registry.yarnpkg.com/restructured/-/restructured-0.0.11.tgz#f914f6b6f358b8e45d6d8ee268926cf1a783f710"
dependencies:
commander "^2.9.0"
lodash "^4.0.0"
power-assert "^1.2.0"
unist-util-map "^1.0.2"
ret@~0.1.10: ret@~0.1.10:
version "0.1.15" version "0.1.15"
resolved "https://registry.yarnpkg.com/ret/-/ret-0.1.15.tgz#b8a4825d5bdb1fc3f6f53c2bc33f81388681c7bc" resolved "https://registry.yarnpkg.com/ret/-/ret-0.1.15.tgz#b8a4825d5bdb1fc3f6f53c2bc33f81388681c7bc"
@@ -8693,6 +8834,12 @@ rst-selector-parser@^2.2.3:
lodash.flattendeep "^4.4.0" lodash.flattendeep "^4.4.0"
nearley "^2.7.10" nearley "^2.7.10"
"rst2html@github:thoward/rst2html#990cb89":
version "1.0.4"
resolved "https://codeload.github.com/thoward/rst2html/tar.gz/990cb89f2a300cdd9151790be377c4c0840df809"
dependencies:
restructured "0.0.11"
run-async@^0.1.0: run-async@^0.1.0:
version "0.1.0" version "0.1.0"
resolved "https://registry.yarnpkg.com/run-async/-/run-async-0.1.0.tgz#c8ad4a5e110661e402a7d21b530e009f25f8e389" resolved "https://registry.yarnpkg.com/run-async/-/run-async-0.1.0.tgz#c8ad4a5e110661e402a7d21b530e009f25f8e389"
@@ -9359,6 +9506,14 @@ string_decoder@~0.10.x:
version "0.10.31" version "0.10.31"
resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-0.10.31.tgz#62e203bc41766c6c28c9fc84301dab1c5310fa94" resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-0.10.31.tgz#62e203bc41766c6c28c9fc84301dab1c5310fa94"
stringifier@^1.3.0:
version "1.3.0"
resolved "https://registry.yarnpkg.com/stringifier/-/stringifier-1.3.0.tgz#def18342f6933db0f2dbfc9aa02175b448c17959"
dependencies:
core-js "^2.0.0"
traverse "^0.6.6"
type-name "^2.0.1"
stringify-object@^3.2.0: stringify-object@^3.2.0:
version "3.2.1" version "3.2.1"
resolved "https://registry.yarnpkg.com/stringify-object/-/stringify-object-3.2.1.tgz#2720c2eff940854c819f6ee252aaeb581f30624d" resolved "https://registry.yarnpkg.com/stringify-object/-/stringify-object-3.2.1.tgz#2720c2eff940854c819f6ee252aaeb581f30624d"
@@ -9713,6 +9868,10 @@ tr46@^1.0.0:
dependencies: dependencies:
punycode "^2.1.0" punycode "^2.1.0"
traverse@^0.6.6:
version "0.6.6"
resolved "https://registry.yarnpkg.com/traverse/-/traverse-0.6.6.tgz#cbdf560fd7b9af632502fed40f918c157ea97137"
trim-newlines@^1.0.0: trim-newlines@^1.0.0:
version "1.0.0" version "1.0.0"
resolved "https://registry.yarnpkg.com/trim-newlines/-/trim-newlines-1.0.0.tgz#5887966bb582a4503a41eb524f7d35011815a613" resolved "https://registry.yarnpkg.com/trim-newlines/-/trim-newlines-1.0.0.tgz#5887966bb582a4503a41eb524f7d35011815a613"
@@ -9826,6 +9985,10 @@ type-is@~1.6.15:
media-typer "0.3.0" media-typer "0.3.0"
mime-types "~2.1.15" mime-types "~2.1.15"
type-name@^2.0.1:
version "2.0.2"
resolved "https://registry.yarnpkg.com/type-name/-/type-name-2.0.2.tgz#efe7d4123d8ac52afff7f40c7e4dec5266008fb4"
typedarray@^0.0.6: typedarray@^0.0.6:
version "0.0.6" version "0.0.6"
resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777" resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777"
@@ -9952,6 +10115,20 @@ unique-string@^1.0.0:
dependencies: dependencies:
crypto-random-string "^1.0.0" crypto-random-string "^1.0.0"
unist-util-map@^1.0.2:
version "1.0.3"
resolved "https://registry.yarnpkg.com/unist-util-map/-/unist-util-map-1.0.3.tgz#26a913d7cddb3cd3e9a886d135d37a3d1f54e514"
dependencies:
object-assign "^4.0.1"
universal-deep-strict-equal@^1.2.1:
version "1.2.2"
resolved "https://registry.yarnpkg.com/universal-deep-strict-equal/-/universal-deep-strict-equal-1.2.2.tgz#0da4ac2f73cff7924c81fa4de018ca562ca2b0a7"
dependencies:
array-filter "^1.0.0"
indexof "0.0.1"
object-keys "^1.0.0"
universalify@^0.1.0: universalify@^0.1.0:
version "0.1.1" version "0.1.1"
resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.1.1.tgz#fa71badd4437af4c148841e3b3b165f9e9e590b7" resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.1.1.tgz#fa71badd4437af4c148841e3b3b165f9e9e590b7"