Merge remote-tracking branch 'upstream/external-plugins' into externalPlugin

Conflicts:
	public/views/index.html
This commit is contained in:
woodsaj 2015-12-03 12:47:58 +08:00
commit fd392a2422
63 changed files with 1876 additions and 641 deletions

View File

@ -6,6 +6,8 @@
### Enhancements
* **CloudWatch**: Support for multiple AWS Credentials, closes [#3053](https://github.com/grafana/grafana/issues/3053), [#3080](https://github.com/grafana/grafana/issues/3080)
* **Elasticsearch**: Support for dynamic daily indices for annotations, closes [#3061](https://github.com/grafana/grafana/issues/3061)
* **Graph Panel**: Option to hide series with all zeroes from legend and tooltip, closes [#1381](https://github.com/grafana/grafana/issues/1381), [#3336](https://github.com/grafana/grafana/issues/3336)
### Bug Fixes
* **cloudwatch**: fix for handling of period for long time ranges, fixes [#3086](https://github.com/grafana/grafana/issues/3086)

View File

@ -75,7 +75,7 @@ the latest master builds [here](http://grafana.org/download/builds)
### Dependencies
- Go 1.4
- Go 1.5
- NodeJS
### Get Code
@ -85,11 +85,12 @@ go get github.com/grafana/grafana
```
### Building the backend
Replace X.Y.Z by actual version number.
```
cd $GOPATH/src/github.com/grafana/grafana
go run build.go setup (only needed once to install godep)
godep restore (will pull down all golang lib dependencies in your current GOPATH)
go build .
godep go run build.go build
```
### Building frontend assets
@ -112,7 +113,7 @@ bra run
### Running
```
./grafana
./bin/grafana-server
```
Open grafana in your browser (default http://localhost:3000) and login with admin user (default user/pass = admin/admin).
@ -128,6 +129,7 @@ You only need to add the options you want to override. Config files are applied
## Create a pull request
Before or after you create a pull request, sign the [contributor license agreement](http://grafana.org/docs/contributing/cla.html).
## Contribute
If you have any idea for an improvement or found a bug do not hesitate to open an issue.
And if you have time clone this repo and submit a pull request and help me make Grafana

View File

@ -328,9 +328,9 @@ func build(pkg string, tags []string) {
func ldflags() string {
var b bytes.Buffer
b.WriteString("-w")
b.WriteString(fmt.Sprintf(" -X main.version %s", version))
b.WriteString(fmt.Sprintf(" -X main.commit %s", getGitSha()))
b.WriteString(fmt.Sprintf(" -X main.buildstamp %d", buildStamp()))
b.WriteString(fmt.Sprintf(" -X main.version=%s", version))
b.WriteString(fmt.Sprintf(" -X main.commit=%s", getGitSha()))
b.WriteString(fmt.Sprintf(" -X main.buildstamp=%d", buildStamp()))
return b.String()
}

View File

@ -122,7 +122,7 @@ To configure Grafana add a configuration file named `custom.ini` to the
`conf` folder and override any of the settings defined in
`conf/defaults.ini`.
Start Grafana by executing `./grafana web`. The `grafana` binary needs
Start Grafana by executing `./grafana-server web`. The `grafana-server` binary needs
the working directory to be the root install directory (where the binary
and the `public` folder is located).

View File

@ -1,5 +1,5 @@
----
page_title: Dashboard JSON
page_title: Dashboard JSON
page_description: Dashboard JSON Reference
page_keywords: grafana, dashboard, json, documentation
---
@ -363,7 +363,7 @@ Usage of the fields is explained below:
],
"query": "tag_values(cpu.utilization.average,env)",
"refresh": false,
"refresh_on_load": false,
"refresh": false,
"type": "query"
},
{
@ -390,7 +390,7 @@ Usage of the fields is explained below:
}
],
"query": "tag_values(cpu.utilization.average,app)",
"refresh_on_load": false,
"refresh": false,
"regex": "",
"type": "query"
}
@ -413,7 +413,7 @@ Usage of the above mentioned fields in the templating section is explained below
| **name** | name of variable |
| **options** | array of variable text/value pairs available for selection on dashboard |
| **query** | datasource query used to fetch values for a variable |
| **refresh_on_load** | TODO |
| **refresh** | TODO |
| **regex** | TODO |
| **type** | type of variable, i.e. `custom`, `query` or `interval` |

View File

@ -142,10 +142,10 @@ Will return the dashboard given the dashboard slug. Slug is the url friendly ver
"rows": [
{
}
]
],
"schemaVersion": 6,
"version": 0
},
}
}
### Delete dashboard
@ -787,7 +787,7 @@ Update Organisation, fields *Adress 1*, *Adress 2*, *City* are not implemented y
"id": 2,
"name": "User",
"login": "user",
"email": "user@mygraf.com"
"email": "user@mygraf.com",
"isAdmin": false
}
]
@ -1046,7 +1046,7 @@ Deletes the starring of the given Dashboard for the actual user.
"timezone":"browser",
"title":"Home",
"version":5
}
},
"expires": 3600
}
@ -1091,34 +1091,33 @@ Keys:
"canStar":false,
"slug":"",
"expires":"2200-13-32T25:23:23+02:00",
"created":"2200-13-32T28:24:23+02:00"},
{
"dashboard": {
"editable":false,
"hideControls":true,
"nav":[
{
"enable":false,
"type":"timepicker"
}
],
"rows": [
"created":"2200-13-32T28:24:23+02:00"
},
"dashboard": {
"editable":false,
"hideControls":true,
"nav":[
{
"enable":false,
"type":"timepicker"
}
],
"style":"dark",
"tags":[],
"templating":{
"list":[
]
},
"time":{
},
"timezone":"browser",
"title":"Home",
"version":5
],
"rows": [
{
}
],
"style":"dark",
"tags":[],
"templating":{
"list":[
]
},
"time":{
},
"timezone":"browser",
"title":"Home",
"version":5
}
}
@ -1181,11 +1180,10 @@ Keys:
"pluginType":"datasource",
"serviceName":"Grafana",
"type":"grafanasearch"
}
}
}
}
defaultDatasource: "Grafana"
},
"defaultDatasource": "Grafana"
}
## Login

10
main.go
View File

@ -2,6 +2,7 @@ package main
import (
"flag"
"fmt"
"io/ioutil"
"os"
"os/signal"
@ -27,6 +28,7 @@ import (
var version = "master"
var commit = "NA"
var buildstamp string
var build_date string
var configFile = flag.String("config", "", "path to config file")
var homePath = flag.String("homepath", "", "path to grafana install/home path, defaults to working directory")
@ -38,6 +40,14 @@ func init() {
}
func main() {
v := flag.Bool("v", false, "prints current version and exits")
flag.Parse()
if *v {
fmt.Printf("Version %s (commit: %s)\n", version, commit)
os.Exit(0)
}
buildstampInt64, _ := strconv.ParseInt(buildstamp, 10, 64)
setting.BuildVersion = version

View File

@ -21,7 +21,7 @@
"grunt-contrib-connect": "~0.5.0",
"grunt-contrib-copy": "~0.5.0",
"grunt-contrib-cssmin": "~0.6.1",
"grunt-contrib-htmlmin": "~0.1.3",
"grunt-contrib-htmlmin": "~0.6.0",
"grunt-contrib-jshint": "~0.10.0",
"grunt-contrib-less": "~0.7.0",
"grunt-contrib-requirejs": "~0.4.4",

View File

@ -9,6 +9,7 @@ import (
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/credentials"
"github.com/aws/aws-sdk-go/aws/credentials/ec2rolecreds"
"github.com/aws/aws-sdk-go/aws/ec2metadata"
"github.com/aws/aws-sdk-go/aws/session"
"github.com/aws/aws-sdk-go/service/cloudwatch"
"github.com/aws/aws-sdk-go/service/ec2"
@ -40,11 +41,12 @@ func init() {
}
func handleGetMetricStatistics(req *cwRequest, c *middleware.Context) {
sess := session.New()
creds := credentials.NewChainCredentials(
[]credentials.Provider{
&credentials.EnvProvider{},
&credentials.SharedCredentialsProvider{Filename: "", Profile: req.DataSource.Database},
&ec2rolecreds.EC2RoleProvider{ExpiryWindow: 5 * time.Minute},
&ec2rolecreds.EC2RoleProvider{Client: ec2metadata.New(sess), ExpiryWindow: 5 * time.Minute},
})
cfg := &aws.Config{
@ -87,11 +89,12 @@ func handleGetMetricStatistics(req *cwRequest, c *middleware.Context) {
}
func handleListMetrics(req *cwRequest, c *middleware.Context) {
sess := session.New()
creds := credentials.NewChainCredentials(
[]credentials.Provider{
&credentials.EnvProvider{},
&credentials.SharedCredentialsProvider{Filename: "", Profile: req.DataSource.Database},
&ec2rolecreds.EC2RoleProvider{ExpiryWindow: 5 * time.Minute},
&ec2rolecreds.EC2RoleProvider{Client: ec2metadata.New(sess), ExpiryWindow: 5 * time.Minute},
})
cfg := &aws.Config{
@ -126,8 +129,17 @@ func handleListMetrics(req *cwRequest, c *middleware.Context) {
}
func handleDescribeInstances(req *cwRequest, c *middleware.Context) {
sess := session.New()
creds := credentials.NewChainCredentials(
[]credentials.Provider{
&credentials.EnvProvider{},
&credentials.SharedCredentialsProvider{Filename: "", Profile: req.DataSource.Database},
&ec2rolecreds.EC2RoleProvider{Client: ec2metadata.New(sess), ExpiryWindow: 5 * time.Minute},
})
cfg := &aws.Config{
Region: aws.String(req.Region),
Region: aws.String(req.Region),
Credentials: creds,
}
svc := ec2.New(session.New(cfg), cfg)

View File

@ -57,6 +57,8 @@ func GetDashboard(c *middleware.Context) {
CanStar: c.IsSignedIn,
CanSave: c.OrgRole == m.ROLE_ADMIN || c.OrgRole == m.ROLE_EDITOR,
CanEdit: canEditDashboard(c.OrgRole),
Created: dash.Created,
Updated: dash.Updated,
},
}

View File

@ -40,6 +40,7 @@ type DashboardMeta struct {
Slug string `json:"slug"`
Expires time.Time `json:"expires"`
Created time.Time `json:"created"`
Updated time.Time `json:"updated"`
}
type DashboardFullWithMeta struct {

View File

@ -20,13 +20,13 @@ function (_, $, coreModule) {
getOptions: "&",
onChange: "&",
},
link: function($scope, elem) {
var $input = $(inputTemplate);
var $button = $(buttonTemplate);
var segment = $scope.segment;
var options = null;
var cancelBlur = null;
var linkMode = true;
$input.appendTo(elem);
$button.appendTo(elem);
@ -55,19 +55,21 @@ function (_, $, coreModule) {
});
};
$scope.switchToLink = function(now) {
if (now === true || cancelBlur) {
clearTimeout(cancelBlur);
cancelBlur = null;
$input.hide();
$button.show();
$scope.updateVariableValue($input.val());
}
else {
// need to have long delay because the blur
// happens long before the click event on the typeahead options
cancelBlur = setTimeout($scope.switchToLink, 100);
}
$scope.switchToLink = function() {
if (linkMode) { return; }
clearTimeout(cancelBlur);
cancelBlur = null;
linkMode = true;
$input.hide();
$button.show();
$scope.updateVariableValue($input.val());
};
$scope.inputBlur = function() {
// happens long before the click event on the typeahead options
// need to have long delay because the blur
cancelBlur = setTimeout($scope.switchToLink, 100);
};
$scope.source = function(query, callback) {
@ -98,7 +100,7 @@ function (_, $, coreModule) {
}
$input.val(value);
$scope.switchToLink(true);
$scope.switchToLink();
return value;
};
@ -139,6 +141,8 @@ function (_, $, coreModule) {
$input.show();
$input.focus();
linkMode = false;
var typeahead = $input.data('typeahead');
if (typeahead) {
$input.val('');
@ -146,7 +150,7 @@ function (_, $, coreModule) {
}
});
$input.blur($scope.switchToLink);
$input.blur($scope.inputBlur);
$compile(elem.contents())($scope);
}

View File

@ -7,6 +7,7 @@ function (angular, _, coreModule) {
'use strict';
coreModule.service('uiSegmentSrv', function($sce, templateSrv) {
var self = this;
function MetricSegment(options) {
if (options === '*' || options.value === '*') {
@ -74,6 +75,24 @@ function (angular, _, coreModule) {
});
};
this.transformToSegments = function(addTemplateVars, variableTypeFilter) {
return function(results) {
var segments = _.map(results, function(segment) {
return self.newSegment({ value: segment.text, expandable: segment.expandable });
});
if (addTemplateVars) {
_.each(templateSrv.variables, function(variable) {
if (variableTypeFilter === void 0 || variableTypeFilter === variable.type) {
segments.unshift(self.newSegment({ type: 'template', value: '$' + variable.name, expandable: true }));
}
});
}
return segments;
};
};
this.newSelectMetric = function() {
return new MetricSegment({value: 'select metric', fake: true});
};

View File

@ -15,7 +15,6 @@ function (_) {
appSubUrl: ""
};
var settings = _.extend({}, defaults, options);
return settings;
return _.extend({}, defaults, options);
};
});

View File

@ -12,7 +12,7 @@ define([], function() {
if (def !== void 0 && !this.exists(key)) {
return def;
}
return window.localStorage[key] === 'true' ? true : false;
return window.localStorage[key] === 'true';
},
exists: function(key) {
return window.localStorage[key] !== void 0;

View File

@ -28,6 +28,7 @@ class TimeSeries {
stats: any;
legend: boolean;
allIsNull: boolean;
allIsZero: boolean;
decimals: number;
scaledDecimals: number;
@ -96,6 +97,7 @@ class TimeSeries {
this.stats.avg = null;
this.stats.current = null;
this.allIsNull = true;
this.allIsZero = true;
var ignoreNulls = fillStyle === 'connected';
var nullAsZero = fillStyle === 'null as zero';
@ -130,6 +132,10 @@ class TimeSeries {
}
}
if (currentValue != 0) {
this.allIsZero = false;
}
result.push([currentTime, currentValue]);
}

View File

@ -0,0 +1,39 @@
// Copyright (c) 2014, Hugh Kennedy
// Based on code from https://github.com/hughsk/flat/blob/master/index.js
//
function flatten(target, opts): any {
opts = opts || {};
var delimiter = opts.delimiter || '.';
var maxDepth = opts.maxDepth || 3;
var currentDepth = 1;
var output = {};
function step(object, prev) {
Object.keys(object).forEach(function(key) {
var value = object[key];
var isarray = opts.safe && Array.isArray(value);
var type = Object.prototype.toString.call(value);
var isobject = type === "[object Object]";
var newKey = prev ? prev + delimiter + key : key;
if (!opts.maxDepth) {
maxDepth = currentDepth + 1;
}
if (!isarray && isobject && Object.keys(value).length && currentDepth < maxDepth) {
++currentDepth;
return step(value, newKey);
}
output[newKey] = value;
});
}
step(target, null);
return output;
}
export = flatten;

View File

@ -341,6 +341,8 @@ function($, _) {
// Currencies
kbn.valueFormats.currencyUSD = kbn.formatBuilders.currency('$');
kbn.valueFormats.currencyGBP = kbn.formatBuilders.currency('£');
kbn.valueFormats.currencyEUR = kbn.formatBuilders.currency('€');
kbn.valueFormats.currencyJPY = kbn.formatBuilders.currency('¥');
// Data
kbn.valueFormats.bits = kbn.formatBuilders.binarySIPrefix('b');
@ -430,7 +432,7 @@ function($, _) {
kbn.valueFormats.s = function(size, decimals, scaledDecimals) {
if (size === null) { return ""; }
if (Math.abs(size) < 600) {
if (Math.abs(size) < 60) {
return kbn.toFixed(size, decimals) + " s";
}
// Less than 1 hour, devide in minutes
@ -487,6 +489,57 @@ function($, _) {
}
};
kbn.valueFormats.m = function(size, decimals, scaledDecimals) {
if (size === null) { return ""; }
if (Math.abs(size) < 60) {
return kbn.toFixed(size, decimals) + " min";
}
else if (Math.abs(size) < 1440) {
return kbn.toFixedScaled(size / 60, decimals, scaledDecimals, 2, " hour");
}
else if (Math.abs(size) < 10080) {
return kbn.toFixedScaled(size / 1440, decimals, scaledDecimals, 3, " day");
}
else if (Math.abs(size) < 604800) {
return kbn.toFixedScaled(size / 10080, decimals, scaledDecimals, 4, " week");
}
else {
return kbn.toFixedScaled(size / 5.25948e5, decimals, scaledDecimals, 5, " year");
}
};
kbn.valueFormats.h = function(size, decimals, scaledDecimals) {
if (size === null) { return ""; }
if (Math.abs(size) < 24) {
return kbn.toFixed(size, decimals) + " hour";
}
else if (Math.abs(size) < 168) {
return kbn.toFixedScaled(size / 24, decimals, scaledDecimals, 2, " day");
}
else if (Math.abs(size) < 8760) {
return kbn.toFixedScaled(size / 168, decimals, scaledDecimals, 3, " week");
}
else {
return kbn.toFixedScaled(size / 8760, decimals, scaledDecimals, 4, " year");
}
};
kbn.valueFormats.d = function(size, decimals, scaledDecimals) {
if (size === null) { return ""; }
if (Math.abs(size) < 7) {
return kbn.toFixed(size, decimals) + " day";
}
else if (Math.abs(size) < 365) {
return kbn.toFixedScaled(size / 7, decimals, scaledDecimals, 2, " week");
}
else {
return kbn.toFixedScaled(size / 365, decimals, scaledDecimals, 3, " year");
}
};
///// FORMAT MENU /////
kbn.getUnitFormats = function() {
@ -508,6 +561,8 @@ function($, _) {
submenu: [
{text: 'Dollars ($)', value: 'currencyUSD'},
{text: 'Pounds (£)', value: 'currencyGBP'},
{text: 'Euro (€)', value: 'currencyEUR'},
{text: 'Yen (¥)', value: 'currencyJPY'},
]
},
{
@ -518,6 +573,9 @@ function($, _) {
{text: 'microseconds (µs)', value: 'µs' },
{text: 'milliseconds (ms)', value: 'ms' },
{text: 'seconds (s)', value: 's' },
{text: 'minutes (m)', value: 'm' },
{text: 'hours (h)', value: 'h' },
{text: 'days (d)', value: 'd' },
]
},
{

View File

@ -17,6 +17,7 @@
<th></th>
</tr>
<tr ng-repeat="org in orgs">
<td>{{org.id}}</td>
<td>{{org.name}}</td>
<td style="width: 1%">
<a href="admin/orgs/edit/{{org.id}}" class="btn btn-inverse btn-small">

View File

@ -26,7 +26,7 @@ function (angular, $, _, moment) {
this.tags = data.tags || [];
this.style = data.style || "dark";
this.timezone = data.timezone || 'browser';
this.editable = data.editable === false ? false : true;
this.editable = data.editable !== false;
this.hideControls = data.hideControls || false;
this.sharedCrosshair = data.sharedCrosshair || false;
this.rows = data.rows || [];
@ -48,10 +48,10 @@ function (angular, $, _, moment) {
p._initMeta = function(meta) {
meta = meta || {};
meta.canShare = meta.canShare === false ? false : true;
meta.canSave = meta.canSave === false ? false : true;
meta.canStar = meta.canStar === false ? false : true;
meta.canEdit = meta.canEdit === false ? false : true;
meta.canShare = meta.canShare !== false;
meta.canSave = meta.canSave !== false;
meta.canStar = meta.canStar !== false;
meta.canEdit = meta.canEdit !== false;
if (!this.editable) {
meta.canEdit = false;
@ -151,7 +151,6 @@ function (angular, $, _, moment) {
result.panel = panel;
result.row = row;
result.index = index;
return;
}
});
});
@ -230,9 +229,9 @@ function (angular, $, _, moment) {
var i, j, k;
var oldVersion = this.schemaVersion;
var panelUpgrades = [];
this.schemaVersion = 7;
this.schemaVersion = 8;
if (oldVersion === 7) {
if (oldVersion === 8) {
return;
}
@ -343,6 +342,49 @@ function (angular, $, _, moment) {
});
}
if (oldVersion < 8) {
panelUpgrades.push(function(panel) {
_.each(panel.targets, function(target) {
// update old influxdb query schema
if (target.fields && target.tags && target.groupBy) {
if (target.rawQuery) {
delete target.fields;
delete target.fill;
} else {
target.select = _.map(target.fields, function(field) {
var parts = [];
parts.push({type: 'field', params: [field.name]});
parts.push({type: field.func, params: []});
if (field.mathExpr) {
parts.push({type: 'math', params: [field.mathExpr]});
}
if (field.asExpr) {
parts.push({type: 'alias', params: [field.asExpr]});
}
return parts;
});
delete target.fields;
_.each(target.groupBy, function(part) {
if (part.type === 'time' && part.interval) {
part.params = [part.interval];
delete part.interval;
}
if (part.type === 'tag' && part.key) {
part.params = [part.key];
delete part.key;
}
});
if (target.fill) {
target.groupBy.push({type: 'fill', params: [target.fill]});
delete target.fill;
}
}
}
});
});
}
if (panelUpgrades.length === 0) {
return;
}

View File

@ -25,7 +25,7 @@
<td>
<button class="btn btn-inverse pull-right" ng-click="import(dash.name)">
Load
</a>
</button>
</td>
</tr>
</table>

View File

@ -90,11 +90,11 @@ define([
timer.cancel(this.refresh_timer);
};
this.setTime = function(time) {
this.setTime = function(time, enableRefresh) {
_.extend(this.time, time);
// disable refresh if we have an absolute time
if (moment.isMoment(time.to)) {
// disable refresh if zoom in or zoom out
if (!enableRefresh && moment.isMoment(time.to)) {
this.old_refresh = this.dashboard.refresh || this.old_refresh;
this.setAutoRefresh(false);
}

View File

@ -115,7 +115,7 @@ export class TimePickerCtrl {
this.timeSrv.setAutoRefresh(this.refresh.value);
}
this.timeSrv.setTime(this.timeRaw);
this.timeSrv.setTime(this.timeRaw, true);
this.$rootScope.appEvent('hide-dash-editor');
}

View File

@ -122,11 +122,7 @@ function(angular, _) {
var currentJson = angular.toJson(current);
var originalJson = angular.toJson(original);
if (currentJson !== originalJson) {
return true;
}
return false;
return currentJson !== originalJson;
};
p.open_modal = function() {

View File

@ -52,7 +52,7 @@ function (angular, _) {
if (link.asDropdown) {
template += '<ul class="dropdown-menu" role="menu">' +
'<li ng-repeat="dash in link.searchHits"><a href="{{dash.url}}"><i class="fa fa-th-large"></i> {{dash.title}}</a></li>' +
'</ul';
'</ul>';
}
elem.html(template);

View File

@ -45,7 +45,7 @@
{{invite.email}}
<span ng-show="invite.name" style="padding-left: 20px"> {{invite.name}}</span>
<span class="pull-right">
<button class="btn btn-inverse btn-mini " data-clipboard-text="{{invite.url}}" clipboard-button ng-click="copyInviteToClipboard($event)"
<button class="btn btn-inverse btn-mini " data-clipboard-text="{{invite.url}}" clipboard-button ng-click="copyInviteToClipboard($event)">
<i class="fa fa-clipboard"></i> Copy Invite
</button>
&nbsp;

View File

@ -64,8 +64,7 @@ function (angular, $, _) {
}
function getExtendedMenu($scope) {
var menu = angular.copy($scope.panelMeta.extendedMenu);
return menu;
return angular.copy($scope.panelMeta.extendedMenu);
}
return {

View File

@ -12,7 +12,7 @@ function (angular, _) {
var replacementDefaults = {
type: 'query',
datasource: null,
refresh_on_load: false,
refresh: false,
name: '',
options: [],
includeAll: false,

View File

@ -45,17 +45,6 @@ function (angular, _, kbn) {
};
this.setVariableFromUrl = function(variable, urlValue) {
if (variable.refresh) {
var self = this;
//refresh the list of options before setting the value
return this.updateOptions(variable).then(function() {
var option = _.findWhere(variable.options, { text: urlValue });
option = option || { text: urlValue, value: urlValue };
self.updateAutoInterval(variable);
return self.setVariableValue(variable, option);
});
}
var option = _.findWhere(variable.options, { text: urlValue });
option = option || { text: urlValue, value: urlValue };

View File

@ -73,7 +73,7 @@
<ul class="tight-form-list" role="menu">
<li class="tight-form-item query-keyword tight-form-align" style="width: 100px">
Alias
<tip>{{metric}} {{stat}} {{namespace}} {{region}} {{<dimension name>}}</tip>
<tip>{{metric}} {{stat}} {{namespace}} {{region}} {{DIMENSION_NAME}}</tip>
</li>
<li>
<input type="text" class="input-xlarge tight-form-input" ng-model="target.alias" spellcheck='false' ng-model-onblur ng-change="refreshMetricData()">

View File

@ -15,7 +15,6 @@ function (angular, _, queryDef) {
$scope.bucketAggTypes = queryDef.bucketAggTypes;
$scope.orderOptions = queryDef.orderOptions;
$scope.sizeOptions = queryDef.sizeOptions;
$scope.intervalOptions = queryDef.intervalOptions;
$rootScope.onAppEvent('elastic-query-updated', function() {
$scope.validateModel();
@ -128,6 +127,10 @@ function (angular, _, queryDef) {
}
};
$scope.getIntervalOptions = function() {
return $q.when(uiSegmentSrv.transformToSegments(true, 'interval')(queryDef.intervalOptions));
};
$scope.addBucketAgg = function() {
// if last is date histogram add it before
var lastBucket = bucketAggs[bucketAggs.length - 1];

View File

@ -41,7 +41,7 @@
Interval
</li>
<li>
<metric-segment-model property="agg.settings.interval" options="intervalOptions" on-change="onChangeInternal()" css-class="last" custom="true"></metric-segment-model>
<metric-segment-model property="agg.settings.interval" get-options="getIntervalOptions()" on-change="onChangeInternal()" css-class="last" custom="true"></metric-segment-model>
</li>
</ul>
<div class="clearfix"></div>

View File

@ -1,13 +1,12 @@
define([
'angular',
'lodash',
],
function (angular, _) {
function (angular) {
'use strict';
var module = angular.module('grafana.controllers');
module.controller('ElasticQueryCtrl', function($scope, $timeout, uiSegmentSrv, templateSrv) {
module.controller('ElasticQueryCtrl', function($scope, $timeout, uiSegmentSrv) {
$scope.init = function() {
var target = $scope.target;
@ -21,7 +20,7 @@ function (angular, _) {
$scope.getFields = function(type) {
var jsonStr = angular.toJson({find: 'fields', type: type});
return $scope.datasource.metricFindQuery(jsonStr)
.then($scope.transformToSegments(false))
.then(uiSegmentSrv.transformToSegments(false))
.then(null, $scope.handleQueryError);
};
@ -35,21 +34,6 @@ function (angular, _) {
$scope.appEvent('elastic-query-updated');
};
$scope.transformToSegments = function(addTemplateVars) {
return function(results) {
var segments = _.map(results, function(segment) {
return uiSegmentSrv.newSegment({ value: segment.text, expandable: segment.expandable });
});
if (addTemplateVars) {
_.each(templateSrv.variables, function(variable) {
segments.unshift(uiSegmentSrv.newSegment({ type: 'template', value: '$' + variable.name, expandable: true }));
});
}
return segments;
};
};
$scope.handleQueryError = function(err) {
$scope.parserError = err.message || 'Failed to issue metric query';
return [];

View File

@ -3,11 +3,11 @@ define([
'lodash',
'app/core/utils/datemath',
'./influx_series',
'./query_builder',
'./influx_query',
'./directives',
'./query_ctrl',
],
function (angular, _, dateMath, InfluxSeries, InfluxQueryBuilder) {
function (angular, _, dateMath, InfluxSeries, InfluxQuery) {
'use strict';
var module = angular.module('grafana.services');
@ -41,8 +41,8 @@ function (angular, _, dateMath, InfluxSeries, InfluxQueryBuilder) {
queryTargets.push(target);
// build query
var queryBuilder = new InfluxQueryBuilder(target);
var query = queryBuilder.build();
var queryModel = new InfluxQuery(target);
var query = queryModel.render();
query = query.replace(/\$interval/g, (target.interval || options.interval));
return query;

View File

@ -0,0 +1,214 @@
///<reference path="../../../headers/common.d.ts" />
import _ = require('lodash');
import queryPart = require('./query_part');
class InfluxQuery {
target: any;
selectModels: any[];
groupByParts: any;
queryBuilder: any;
constructor(target) {
this.target = target;
target.tags = target.tags || [];
target.groupBy = target.groupBy || [
{type: 'time', params: ['$interval']},
{type: 'fill', params: ['null']},
];
target.select = target.select || [[
{type: 'field', params: ['value']},
{type: 'mean', params: []},
]];
this.updateProjection();
}
updateProjection() {
this.selectModels = _.map(this.target.select, function(parts: any) {
return _.map(parts, queryPart.create);
});
this.groupByParts = _.map(this.target.groupBy, queryPart.create);
}
updatePersistedParts() {
this.target.select = _.map(this.selectModels, function(selectParts) {
return _.map(selectParts, function(part: any) {
return {type: part.def.type, params: part.params};
});
});
}
hasGroupByTime() {
return _.find(this.target.groupBy, (g: any) => g.type === 'time');
}
hasFill() {
return _.find(this.target.groupBy, (g: any) => g.type === 'fill');
}
addGroupBy(value) {
var stringParts = value.match(/^(\w+)\((.*)\)$/);
var typePart = stringParts[1];
var arg = stringParts[2];
var partModel = queryPart.create({type: typePart, params: [arg]});
var partCount = this.target.groupBy.length;
if (partCount === 0) {
this.target.groupBy.push(partModel.part);
} else if (typePart === 'time') {
this.target.groupBy.splice(0, 0, partModel.part);
} else if (typePart === 'tag') {
if (this.target.groupBy[partCount-1].type === 'fill') {
this.target.groupBy.splice(partCount-1, 0, partModel.part);
} else {
this.target.groupBy.push(partModel.part);
}
} else {
this.target.groupBy.push(partModel.part);
}
this.updateProjection();
}
removeGroupByPart(part, index) {
var categories = queryPart.getCategories();
if (part.def.type === 'time') {
// remove fill
this.target.groupBy = _.filter(this.target.groupBy, (g: any) => g.type !== 'fill');
// remove aggregations
this.target.select = _.map(this.target.select, (s: any) => {
return _.filter(s, (part: any) => {
var partModel = queryPart.create(part);
if (partModel.def.category === categories.Aggregations) {
return false;
}
if (partModel.def.category === categories.Selectors) {
return false;
}
return true;
});
});
}
this.target.groupBy.splice(index, 1);
this.updateProjection();
}
removeSelect(index: number) {
this.target.select.splice(index, 1);
this.updateProjection();
}
removeSelectPart(selectParts, part) {
// if we remove the field remove the whole statement
if (part.def.type === 'field') {
if (this.selectModels.length > 1) {
var modelsIndex = _.indexOf(this.selectModels, selectParts);
this.selectModels.splice(modelsIndex, 1);
}
} else {
var partIndex = _.indexOf(selectParts, part);
selectParts.splice(partIndex, 1);
}
this.updatePersistedParts();
}
addSelectPart(selectParts, type) {
var partModel = queryPart.create({type: type});
partModel.def.addStrategy(selectParts, partModel, this);
this.updatePersistedParts();
}
private renderTagCondition(tag, index) {
var str = "";
var operator = tag.operator;
var value = tag.value;
if (index > 0) {
str = (tag.condition || 'AND') + ' ';
}
if (!operator) {
if (/^\/.*\/$/.test(tag.value)) {
operator = '=~';
} else {
operator = '=';
}
}
// quote value unless regex
if (operator !== '=~' && operator !== '!~') {
value = "'" + value + "'";
}
return str + '"' + tag.key + '" ' + operator + ' ' + value;
}
render() {
var target = this.target;
if (target.rawQuery) {
return target.query;
}
if (!target.measurement) {
throw "Metric measurement is missing";
}
var query = 'SELECT ';
var i, y;
for (i = 0; i < this.selectModels.length; i++) {
let parts = this.selectModels[i];
var selectText = "";
for (y = 0; y < parts.length; y++) {
let part = parts[y];
selectText = part.render(selectText);
}
if (i > 0) {
query += ', ';
}
query += selectText;
}
var measurement = target.measurement;
if (!measurement.match('^/.*/') && !measurement.match(/^merge\(.*\)/)) {
measurement = '"' + measurement+ '"';
}
query += ' FROM ' + measurement + ' WHERE ';
var conditions = _.map(target.tags, (tag, index) => {
return this.renderTagCondition(tag, index);
});
query += conditions.join(' ');
query += (conditions.length > 0 ? ' AND ' : '') + '$timeFilter';
var groupBySection = "";
for (i = 0; i < this.groupByParts.length; i++) {
var part = this.groupByParts[i];
if (i > 0) {
// for some reason fill has no seperator
groupBySection += part.def.type === 'fill' ? ' ' : ', ';
}
groupBySection += part.render('');
}
if (groupBySection.length) {
query += ' GROUP BY ' + groupBySection;
}
if (target.fill) {
query += ' fill(' + target.fill + ')';
}
target.query = query;
return query;
}
}
export = InfluxQuery;

View File

@ -1,4 +1,4 @@
<div class="tight-form-container-no-item-borders">
<div class="">
<div class="tight-form">
<ul class="tight-form-list pull-right">
<li ng-show="parserError" class="tight-form-item">
@ -48,98 +48,47 @@
<li>
<metric-segment segment="measurementSegment" get-options="getMeasurements()" on-change="measurementChanged()"></metric-segment>
</li>
<li class="tight-form-item query-keyword" style="padding-left: 15px; padding-right: 15px;">
WHERE
</li>
<li ng-repeat="segment in tagSegments">
<metric-segment segment="segment" get-options="getTagsOrValues(segment, $index)" on-change="tagSegmentUpdated(segment, $index)"></metric-segment>
</li>
</ul>
<div class="clearfix"></div>
<div style="padding: 10px" ng-if="target.rawQuery">
<textarea ng-model="target.query" rows="8" spellcheck="false" style="width: 100%; box-sizing: border-box;" ng-blur="get_data()"></textarea>
</div>
</div>
<div ng-hide="target.rawQuery">
<div class="tight-form">
<ul class="tight-form-list">
<li class="tight-form-item query-keyword tight-form-align" style="width: 75px;">
WHERE
</li>
<li ng-repeat="segment in tagSegments">
<metric-segment segment="segment" get-options="getTagsOrValues(segment, $index)" on-change="tagSegmentUpdated(segment, $index)"></metric-segment>
</li>
</ul>
<div class="clearfix"></div>
</div>
<div class="tight-form" ng-repeat="field in target.fields">
<div class="tight-form" ng-repeat="selectParts in queryModel.selectModels">
<ul class="tight-form-list">
<li class="tight-form-item query-keyword tight-form-align" style="width: 75px;">
<span ng-show="$index === 0">SELECT</span>
</li>
<li>
<metric-segment-model property="field.func" get-options="getFunctions()" on-change="get_data()" css-class="tight-form-item-xlarge"></metric-segment>
<li ng-repeat="part in selectParts">
<influx-query-part-editor part="part" class="tight-form-item tight-form-func" remove-action="removeSelectPart(selectParts, part)" part-updated="selectPartUpdated(selectParts, part)" get-options="getPartOptions(part)"></influx-query-part-editor>
</li>
<li>
<metric-segment-model property="field.name" get-options="getFields()" on-change="get_data()" css-class="tight-form-item-large"></metric-segment>
</li>
<li>
<input type="text" class="tight-form-clear-input text-center" style="width: 70px;" ng-model="field.mathExpr" spellcheck='false' placeholder="math expr" ng-blur="get_data()">
</li>
<li class="tight-form-item query-keyword">
AS
</li>
<li>
<input type="text" class="tight-form-clear-input" style="width: 180px;" ng-model="field.asExpr" spellcheck='false' placeholder="as expr" ng-blur="get_data()">
</li>
</ul>
<ul class="tight-form-list pull-right">
<li class="tight-form-item last" ng-show="$index === 0">
<a class="pointer" ng-click="addSelect()"><i class="fa fa-plus"></i></a>
</li>
<li class="tight-form-item last" ng-show="target.fields.length > 1">
<a class="pointer" ng-click="removeSelect($index)"><i class="fa fa-minus"></i></a>
<li class="dropdown" dropdown-typeahead="selectMenu" dropdown-typeahead-on-select="addSelectPart(selectParts, $item, $subItem)">
</li>
</ul>
<div class="clearfix"></div>
</div>
<div class="tight-form" ng-repeat="groupBy in target.groupBy">
<div class="tight-form">
<ul class="tight-form-list">
<li class="tight-form-item query-keyword tight-form-align" style="width: 75px;">
<span ng-show="$index === 0">GROUP BY</span>
</li>
<li ng-if="groupBy.type === 'time'">
<span class="tight-form-item">time</span>
<metric-segment-model property="groupBy.interval" get-options="getGroupByTimeIntervals()" on-change="get_data()">
</metric-segment>
<li ng-repeat="part in queryModel.groupByParts">
<influx-query-part-editor part="part" class="tight-form-item tight-form-func" remove-action="removeGroupByPart(part, $index)" part-updated="get_data();" get-options="getPartOptions(part)"></influx-query-part-editor>
</li>
<li class="dropdown" ng-if="groupBy.type === 'time'">
<a class="tight-form-item pointer" data-toggle="dropdown" bs-tooltip="'Insert missing values, important when stacking'" data-placement="right">
<span ng-show="target.fill">
fill ({{target.fill}})
</span>
<span ng-show="!target.fill">
no fill
</span>
</a>
<ul class="dropdown-menu">
<li><a ng-click="setFill('')">no fill</a></li>
<li><a ng-click="setFill('0')">fill (0)</a></li>
<li><a ng-click="setFill('null')">fill (null)</a></li>
<li><a ng-click="setFill('none')">fill (none)</a></li>
<li><a ng-click="setFill('previous')">fill (previous)</a></li>
</ul>
</li>
<li ng-if="groupBy.type === 'tag'">
<metric-segment-model property="groupBy.key" get-options="getTagOptions()" on-change="get_data()"></metric-segment>
</li>
</ul>
<ul class="tight-form-list pull-right">
<li class="tight-form-item last" ng-show="$index === 0">
<a class="pointer" ng-click="addGroupBy()"><i class="fa fa-plus"></i></a>
</li>
<li class="tight-form-item last" ng-show="$index > 0">
<a class="pointer" ng-click="removeGroupBy($index)"><i class="fa fa-minus"></i></a>
<li>
<metric-segment segment="groupBySegment" get-options="getGroupByOptions()" on-change="groupByAction(part, $index)"></metric-segment>
</li>
</ul>
<div class="clearfix"></div>

View File

@ -0,0 +1,5 @@
<div class="tight-form-func-controls">
<span class="pointer fa fa-remove" ng-click="removeActionInternal()" ></span>
</div>
<a ng-click="toggleControls()">{{part.def.type}}</a><span>(</span><span class="query-part-parameters"></span><span>)</span>

View File

@ -4,8 +4,9 @@ define([
function (_) {
'use strict';
function InfluxQueryBuilder(target) {
function InfluxQueryBuilder(target, queryModel) {
this.target = target;
this.model = queryModel;
if (target.groupByTags) {
target.groupBy = [{type: 'time', interval: 'auto'}];
@ -92,78 +93,5 @@ function (_) {
return query;
};
p._getGroupByTimeInterval = function(interval) {
if (interval === 'auto') {
return '$interval';
}
return interval;
};
p._buildQuery = function() {
var target = this.target;
if (!target.measurement) {
throw "Metric measurement is missing";
}
if (!target.fields) {
target.fields = [{name: 'value', func: target.function || 'mean'}];
}
var query = 'SELECT ';
var i;
for (i = 0; i < target.fields.length; i++) {
var field = target.fields[i];
if (i > 0) {
query += ', ';
}
query += field.func + '("' + field.name + '")';
if (field.mathExpr) {
query += field.mathExpr;
}
if (field.asExpr) {
query += ' AS "' + field.asExpr + '"';
} else {
query += ' AS "' + field.name + '"';
}
}
var measurement = target.measurement;
if (!measurement.match('^/.*/') && !measurement.match(/^merge\(.*\)/)) {
measurement = '"' + measurement+ '"';
}
query += ' FROM ' + measurement + ' WHERE ';
var conditions = _.map(target.tags, function(tag, index) {
return renderTagCondition(tag, index);
});
query += conditions.join(' ');
query += (conditions.length > 0 ? ' AND ' : '') + '$timeFilter';
query += ' GROUP BY';
for (i = 0; i < target.groupBy.length; i++) {
var group = target.groupBy[i];
if (group.type === 'time') {
query += ' time(' + this._getGroupByTimeInterval(group.interval) + ')';
} else {
query += ', "' + group.key + '"';
}
}
if (target.fill) {
query += ' fill(' + target.fill + ')';
}
target.query = query;
return query;
};
p._modifyRawQuery = function () {
var query = this.target.query.replace(";", "");
return query;
};
return InfluxQueryBuilder;
});

View File

@ -2,32 +2,33 @@ define([
'angular',
'lodash',
'./query_builder',
'./influx_query',
'./query_part',
'./query_part_editor',
],
function (angular, _, InfluxQueryBuilder) {
function (angular, _, InfluxQueryBuilder, InfluxQuery, queryPart) {
'use strict';
var module = angular.module('grafana.controllers');
module.controller('InfluxQueryCtrl', function($scope, $timeout, $sce, templateSrv, $q, uiSegmentSrv) {
module.controller('InfluxQueryCtrl', function($scope, templateSrv, $q, uiSegmentSrv) {
$scope.init = function() {
if (!$scope.target) { return; }
var target = $scope.target;
target.tags = target.tags || [];
target.groupBy = target.groupBy || [{type: 'time', interval: 'auto'}];
target.fields = target.fields || [{name: 'value', func: target.function || 'mean'}];
$scope.target = $scope.target;
$scope.queryModel = new InfluxQuery($scope.target);
$scope.queryBuilder = new InfluxQueryBuilder($scope.target);
$scope.groupBySegment = uiSegmentSrv.newPlusButton();
$scope.queryBuilder = new InfluxQueryBuilder(target);
if (!target.measurement) {
if (!$scope.target.measurement) {
$scope.measurementSegment = uiSegmentSrv.newSelectMeasurement();
} else {
$scope.measurementSegment = uiSegmentSrv.newSegment(target.measurement);
$scope.measurementSegment = uiSegmentSrv.newSegment($scope.target.measurement);
}
$scope.tagSegments = [];
_.each(target.tags, function(tag) {
_.each($scope.target.tags, function(tag) {
if (!tag.operator) {
if (/^\/.*\/$/.test(tag.value)) {
tag.operator = "=~";
@ -46,9 +47,69 @@ function (angular, _, InfluxQueryBuilder) {
});
$scope.fixTagSegments();
$scope.buildSelectMenu();
$scope.removeTagFilterSegment = uiSegmentSrv.newSegment({fake: true, value: '-- remove tag filter --'});
};
$scope.buildSelectMenu = function() {
var categories = queryPart.getCategories();
$scope.selectMenu = _.reduce(categories, function(memo, cat, key) {
var menu = {text: key};
menu.submenu = _.map(cat, function(item) {
return {text: item.type, value: item.type};
});
memo.push(menu);
return memo;
}, []);
};
$scope.getGroupByOptions = function() {
var query = $scope.queryBuilder.buildExploreQuery('TAG_KEYS');
return $scope.datasource.metricFindQuery(query)
.then(function(tags) {
var options = [];
if (!$scope.queryModel.hasFill()) {
options.push(uiSegmentSrv.newSegment({value: 'fill(null)'}));
}
if (!$scope.queryModel.hasGroupByTime()) {
options.push(uiSegmentSrv.newSegment({value: 'time($interval)'}));
}
_.each(tags, function(tag) {
options.push(uiSegmentSrv.newSegment({value: 'tag(' + tag.text + ')'}));
});
return options;
})
.then(null, $scope.handleQueryError);
};
$scope.groupByAction = function() {
$scope.queryModel.addGroupBy($scope.groupBySegment.value);
var plusButton = uiSegmentSrv.newPlusButton();
$scope.groupBySegment.value = plusButton.value;
$scope.groupBySegment.html = plusButton.html;
$scope.get_data();
};
$scope.removeGroupByPart = function(part, index) {
$scope.queryModel.removeGroupByPart(part, index);
$scope.get_data();
};
$scope.addSelectPart = function(selectParts, cat, subitem) {
$scope.queryModel.addSelectPart(selectParts, subitem.value);
$scope.get_data();
};
$scope.removeSelectPart = function(selectParts, part) {
$scope.queryModel.removeSelectPart(selectParts, part);
$scope.get_data();
};
$scope.selectPartUpdated = function() {
$scope.get_data();
};
$scope.fixTagSegments = function() {
var count = $scope.tagSegments.length;
var lastSegment = $scope.tagSegments[Math.max(count-1, 0)];
@ -58,38 +119,9 @@ function (angular, _, InfluxQueryBuilder) {
}
};
$scope.addGroupBy = function() {
$scope.target.groupBy.push({type: 'tag', key: "select tag"});
};
$scope.removeGroupBy = function(index) {
$scope.target.groupBy.splice(index, 1);
$scope.get_data();
};
$scope.addSelect = function() {
$scope.target.fields.push({name: "select field", func: 'mean'});
};
$scope.removeSelect = function(index) {
$scope.target.fields.splice(index, 1);
$scope.get_data();
};
$scope.changeFunction = function(func) {
$scope.target.function = func;
$scope.$parent.get_data();
};
$scope.measurementChanged = function() {
$scope.target.measurement = $scope.measurementSegment.value;
$scope.$parent.get_data();
};
$scope.getFields = function() {
var fieldsQuery = $scope.queryBuilder.buildExploreQuery('FIELDS');
return $scope.datasource.metricFindQuery(fieldsQuery)
.then($scope.transformToSegments(false), $scope.handleQueryError);
$scope.get_data();
};
$scope.toggleQueryMode = function () {
@ -102,20 +134,17 @@ function (angular, _, InfluxQueryBuilder) {
.then($scope.transformToSegments(true), $scope.handleQueryError);
};
$scope.getFunctions = function () {
var functionList = ['count', 'mean', 'sum', 'min', 'max', 'mode', 'distinct', 'median',
'stddev', 'first', 'last'
];
return $q.when(_.map(functionList, function(func) {
return uiSegmentSrv.newSegment(func);
}));
};
$scope.getGroupByTimeIntervals = function () {
var times = ['auto', '1s', '10s', '1m', '2m', '5m', '10m', '30m', '1h', '1d'];
return $q.when(_.map(times, function(func) {
return uiSegmentSrv.newSegment(func);
}));
$scope.getPartOptions = function(part) {
if (part.def.type === 'field') {
var fieldsQuery = $scope.queryBuilder.buildExploreQuery('FIELDS');
return $scope.datasource.metricFindQuery(fieldsQuery)
.then($scope.transformToSegments(true), $scope.handleQueryError);
}
if (part.def.type === 'tag') {
var tagsQuery = $scope.queryBuilder.buildExploreQuery('TAG_KEYS');
return $scope.datasource.metricFindQuery(tagsQuery)
.then($scope.transformToSegments(true), $scope.handleQueryError);
}
};
$scope.handleQueryError = function(err) {
@ -179,25 +208,8 @@ function (angular, _, InfluxQueryBuilder) {
.then(null, $scope.handleQueryError);
};
$scope.addField = function() {
$scope.target.fields.push({name: $scope.addFieldSegment.value, func: 'mean'});
_.extend($scope.addFieldSegment, uiSegmentSrv.newPlusButton());
};
$scope.fieldChanged = function(field) {
if (field.name === '-- remove from select --') {
$scope.target.fields = _.without($scope.target.fields, field);
}
$scope.get_data();
};
$scope.getTagOptions = function() {
var query = $scope.queryBuilder.buildExploreQuery('TAG_KEYS');
return $scope.datasource.metricFindQuery(query)
.then($scope.transformToSegments(false))
.then(null, $scope.handleQueryError);
};
};
$scope.setFill = function(fill) {
$scope.target.fill = fill;

View File

@ -0,0 +1,432 @@
///<reference path="../../../headers/common.d.ts" />
import _ = require('lodash');
var index = [];
var categories = {
Aggregations: [],
Selectors: [],
Transformations: [],
Math: [],
Aliasing: [],
Fields: [],
};
var groupByTimeFunctions = [];
class QueryPartDef {
type: string;
params: any[];
defaultParams: any[];
renderer: any;
category: any;
addStrategy: any;
constructor(options: any) {
this.type = options.type;
this.params = options.params;
this.defaultParams = options.defaultParams;
this.renderer = options.renderer;
this.category = options.category;
this.addStrategy = options.addStrategy;
}
static register(options: any) {
index[options.type] = new QueryPartDef(options);
options.category.push(index[options.type]);
}
}
function functionRenderer(part, innerExpr) {
var str = part.def.type + '(';
var parameters = _.map(part.params, (value, index) => {
var paramType = part.def.params[index];
if (paramType.type === 'time') {
if (value === 'auto') {
value = '$interval';
}
}
if (paramType.quote === 'single') {
return "'" + value + "'";
} else if (paramType.quote === 'double') {
return '"' + value + '"';
}
return value;
});
if (innerExpr) {
parameters.unshift(innerExpr);
}
return str + parameters.join(', ') + ')';
}
function aliasRenderer(part, innerExpr) {
return innerExpr + ' AS ' + '"' + part.params[0] + '"';
}
function suffixRenderer(part, innerExpr) {
return innerExpr + ' ' + part.params[0];
}
function identityRenderer(part, innerExpr) {
return part.params[0];
}
function quotedIdentityRenderer(part, innerExpr) {
return '"' + part.params[0] + '"';
}
function fieldRenderer(part, innerExpr) {
if (part.params[0] === '*') {
return '*';
}
return '"' + part.params[0] + '"';
}
function replaceAggregationAddStrategy(selectParts, partModel) {
// look for existing aggregation
for (var i = 0; i < selectParts.length; i++) {
var part = selectParts[i];
if (part.def.category === categories.Aggregations) {
selectParts[i] = partModel;
return;
}
if (part.def.category === categories.Selectors) {
selectParts[i] = partModel;
return;
}
}
selectParts.splice(1, 0, partModel);
}
function addTransformationStrategy(selectParts, partModel) {
var i;
// look for index to add transformation
for (i = 0; i < selectParts.length; i++) {
var part = selectParts[i];
if (part.def.category === categories.Math || part.def.category === categories.Aliasing) {
break;
}
}
selectParts.splice(i, 0, partModel);
}
function addMathStrategy(selectParts, partModel) {
var partCount = selectParts.length;
if (partCount > 0) {
// if last is math, replace it
if (selectParts[partCount-1].def.type === 'math') {
selectParts[partCount-1] = partModel;
return;
}
// if next to last is math, replace it
if (selectParts[partCount-2].def.type === 'math') {
selectParts[partCount-2] = partModel;
return;
}
// if last is alias add it before
else if (selectParts[partCount-1].def.type === 'alias') {
selectParts.splice(partCount-1, 0, partModel);
return;
}
}
selectParts.push(partModel);
}
function addAliasStrategy(selectParts, partModel) {
var partCount = selectParts.length;
if (partCount > 0) {
// if last is alias, replace it
if (selectParts[partCount-1].def.type === 'alias') {
selectParts[partCount-1] = partModel;
return;
}
}
selectParts.push(partModel);
}
function addFieldStrategy(selectParts, partModel, query) {
// copy all parts
var parts = _.map(selectParts, function(part: any) {
return new QueryPart({type: part.def.type, params: _.clone(part.params)});
});
query.selectModels.push(parts);
}
QueryPartDef.register({
type: 'field',
addStrategy: addFieldStrategy,
category: categories.Fields,
params: [{type: 'field', dynamicLookup: true}],
defaultParams: ['value'],
renderer: fieldRenderer,
});
// Aggregations
QueryPartDef.register({
type: 'count',
addStrategy: replaceAggregationAddStrategy,
category: categories.Aggregations,
params: [],
defaultParams: [],
renderer: functionRenderer,
});
QueryPartDef.register({
type: 'distinct',
addStrategy: replaceAggregationAddStrategy,
category: categories.Aggregations,
params: [],
defaultParams: [],
renderer: functionRenderer,
});
QueryPartDef.register({
type: 'integral',
addStrategy: replaceAggregationAddStrategy,
category: categories.Aggregations,
params: [],
defaultParams: [],
renderer: functionRenderer,
});
QueryPartDef.register({
type: 'mean',
addStrategy: replaceAggregationAddStrategy,
category: categories.Aggregations,
params: [],
defaultParams: [],
renderer: functionRenderer,
});
QueryPartDef.register({
type: 'median',
addStrategy: replaceAggregationAddStrategy,
category: categories.Aggregations,
params: [],
defaultParams: [],
renderer: functionRenderer,
});
QueryPartDef.register({
type: 'sum',
addStrategy: replaceAggregationAddStrategy,
category: categories.Aggregations,
params: [],
defaultParams: [],
renderer: functionRenderer,
});
// transformations
QueryPartDef.register({
type: 'derivative',
addStrategy: addTransformationStrategy,
category: categories.Transformations,
params: [{ name: "duration", type: "interval", options: ['1s', '10s', '1m', '5min', '10m', '15m', '1h']}],
defaultParams: ['10s'],
renderer: functionRenderer,
});
QueryPartDef.register({
type: 'non_negative_derivative',
addStrategy: addTransformationStrategy,
category: categories.Transformations,
params: [{ name: "duration", type: "interval", options: ['1s', '10s', '1m', '5min', '10m', '15m', '1h']}],
defaultParams: ['10s'],
renderer: functionRenderer,
});
QueryPartDef.register({
type: 'stddev',
addStrategy: addTransformationStrategy,
category: categories.Transformations,
params: [],
defaultParams: [],
renderer: functionRenderer,
});
QueryPartDef.register({
type: 'time',
category: groupByTimeFunctions,
params: [{ name: "interval", type: "time", options: ['auto', '1s', '10s', '1m', '5m', '10m', '15m', '1h'] }],
defaultParams: ['auto'],
renderer: functionRenderer,
});
QueryPartDef.register({
type: 'fill',
category: groupByTimeFunctions,
params: [{ name: "fill", type: "string", options: ['none', 'null', '0', 'previous'] }],
defaultParams: ['null'],
renderer: functionRenderer,
});
// Selectors
QueryPartDef.register({
type: 'bottom',
addStrategy: replaceAggregationAddStrategy,
category: categories.Selectors,
params: [{name: 'count', type: 'int'}],
defaultParams: [3],
renderer: functionRenderer,
});
QueryPartDef.register({
type: 'first',
addStrategy: replaceAggregationAddStrategy,
category: categories.Selectors,
params: [],
defaultParams: [],
renderer: functionRenderer,
});
QueryPartDef.register({
type: 'last',
addStrategy: replaceAggregationAddStrategy,
category: categories.Selectors,
params: [],
defaultParams: [],
renderer: functionRenderer,
});
QueryPartDef.register({
type: 'max',
addStrategy: replaceAggregationAddStrategy,
category: categories.Selectors,
params: [],
defaultParams: [],
renderer: functionRenderer,
});
QueryPartDef.register({
type: 'min',
addStrategy: replaceAggregationAddStrategy,
category: categories.Selectors,
params: [],
defaultParams: [],
renderer: functionRenderer,
});
QueryPartDef.register({
type: 'percentile',
addStrategy: replaceAggregationAddStrategy,
category: categories.Selectors,
params: [{name: 'nth', type: 'int'}],
defaultParams: [95],
renderer: functionRenderer,
});
QueryPartDef.register({
type: 'top',
addStrategy: replaceAggregationAddStrategy,
category: categories.Selectors,
params: [{name: 'count', type: 'int'}],
defaultParams: [3],
renderer: functionRenderer,
});
QueryPartDef.register({
type: 'tag',
category: groupByTimeFunctions,
params: [{name: 'tag', type: 'string', dynamicLookup: true}],
defaultParams: ['tag'],
renderer: fieldRenderer,
});
QueryPartDef.register({
type: 'math',
addStrategy: addMathStrategy,
category: categories.Math,
params: [{ name: "expr", type: "string"}],
defaultParams: [' / 100'],
renderer: suffixRenderer,
});
QueryPartDef.register({
type: 'alias',
addStrategy: addAliasStrategy,
category: categories.Aliasing,
params: [{ name: "name", type: "string", quote: 'double'}],
defaultParams: ['alias'],
renderMode: 'suffix',
renderer: aliasRenderer,
});
class QueryPart {
part: any;
def: QueryPartDef;
params: any[];
text: string;
constructor(part: any) {
this.part = part;
this.def = index[part.type];
if (!this.def) {
throw {message: 'Could not find query part ' + part.type};
}
part.params = part.params || _.clone(this.def.defaultParams);
this.params = part.params;
this.updateText();
}
render(innerExpr: string) {
return this.def.renderer(this, innerExpr);
}
hasMultipleParamsInString (strValue, index) {
if (strValue.indexOf(',') === -1) {
return false;
}
return this.def.params[index + 1] && this.def.params[index + 1].optional;
}
updateParam (strValue, index) {
// handle optional parameters
// if string contains ',' and next param is optional, split and update both
if (this.hasMultipleParamsInString(strValue, index)) {
_.each(strValue.split(','), function(partVal: string, idx) {
this.updateParam(partVal.trim(), idx);
}, this);
return;
}
if (strValue === '' && this.def.params[index].optional) {
this.params.splice(index, 1);
}
else {
this.params[index] = strValue;
}
this.part.params = this.params;
this.updateText();
}
updateText() {
if (this.params.length === 0) {
this.text = this.def.type + '()';
return;
}
var text = this.def.type + '(';
text += this.params.join(', ');
text += ')';
this.text = text;
}
}
export = {
create: function(part): any {
return new QueryPart(part);
},
getCategories: function() {
return categories;
}
};

View File

@ -0,0 +1,178 @@
define([
'angular',
'lodash',
'jquery',
],
function (angular, _, $) {
'use strict';
angular
.module('grafana.directives')
.directive('influxQueryPartEditor', function($compile, templateSrv) {
var paramTemplate = '<input type="text" style="display:none"' +
' class="input-mini tight-form-func-param"></input>';
return {
restrict: 'E',
templateUrl: 'app/plugins/datasource/influxdb/partials/query_part.html',
scope: {
part: "=",
removeAction: "&",
partUpdated: "&",
getOptions: "&",
},
link: function postLink($scope, elem) {
var part = $scope.part;
var partDef = part.def;
var $paramsContainer = elem.find('.query-part-parameters');
var $controlsContainer = elem.find('.tight-form-func-controls');
function clickFuncParam(paramIndex) {
/*jshint validthis:true */
var $link = $(this);
var $input = $link.next();
$input.val(part.params[paramIndex]);
$input.css('width', ($link.width() + 16) + 'px');
$link.hide();
$input.show();
$input.focus();
$input.select();
var typeahead = $input.data('typeahead');
if (typeahead) {
$input.val('');
typeahead.lookup();
}
}
function inputBlur(paramIndex) {
/*jshint validthis:true */
var $input = $(this);
var $link = $input.prev();
var newValue = $input.val();
if (newValue !== '' || part.def.params[paramIndex].optional) {
$link.html(templateSrv.highlightVariablesAsHtml(newValue));
part.updateParam($input.val(), paramIndex);
$scope.$apply($scope.partUpdated);
}
$input.hide();
$link.show();
}
function inputKeyPress(paramIndex, e) {
/*jshint validthis:true */
if(e.which === 13) {
inputBlur.call(this, paramIndex);
}
}
function inputKeyDown() {
/*jshint validthis:true */
this.style.width = (3 + this.value.length) * 8 + 'px';
}
function addTypeahead($input, param, paramIndex) {
if (!param.options && !param.dynamicLookup) {
return;
}
var typeaheadSource = function (query, callback) {
if (param.options) { return param.options; }
$scope.$apply(function() {
$scope.getOptions().then(function(result) {
var dynamicOptions = _.map(result, function(op) { return op.value; });
callback(dynamicOptions);
});
});
};
$input.attr('data-provide', 'typeahead');
var options = param.options;
if (param.type === 'int') {
options = _.map(options, function(val) { return val.toString(); });
}
$input.typeahead({
source: typeaheadSource,
minLength: 0,
items: 1000,
updater: function (value) {
setTimeout(function() {
inputBlur.call($input[0], paramIndex);
}, 0);
return value;
}
});
var typeahead = $input.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;
};
}
$scope.toggleControls = function() {
var targetDiv = elem.closest('.tight-form');
if (elem.hasClass('show-function-controls')) {
elem.removeClass('show-function-controls');
targetDiv.removeClass('has-open-function');
$controlsContainer.hide();
return;
}
elem.addClass('show-function-controls');
targetDiv.addClass('has-open-function');
$controlsContainer.show();
};
$scope.removeActionInternal = function() {
$scope.toggleControls();
$scope.removeAction();
};
function addElementsAndCompile() {
_.each(partDef.params, function(param, index) {
if (param.optional && part.params.length <= index) {
return;
}
if (index > 0) {
$('<span>, </span>').appendTo($paramsContainer);
}
var paramValue = templateSrv.highlightVariablesAsHtml(part.params[index]);
var $paramLink = $('<a class="graphite-func-param-link pointer">' + paramValue + '</a>');
var $input = $(paramTemplate);
$paramLink.appendTo($paramsContainer);
$input.appendTo($paramsContainer);
$input.blur(_.partial(inputBlur, index));
$input.keyup(inputKeyDown);
$input.keypress(_.partial(inputKeyPress, index));
$paramLink.click(_.partial(clickFuncParam, index));
addTypeahead($input, param, index);
});
}
function relink() {
$paramsContainer.empty();
addElementsAndCompile();
}
relink();
}
};
});
});

View File

@ -0,0 +1,216 @@
import {describe, beforeEach, it, sinon, expect} from 'test/lib/common';
import InfluxQuery = require('../influx_query');
describe('InfluxQuery', function() {
describe('render series with mesurement only', function() {
it('should generate correct query', function() {
var query = new InfluxQuery({
measurement: 'cpu',
});
var queryText = query.render();
expect(queryText).to.be('SELECT mean("value") FROM "cpu" WHERE $timeFilter GROUP BY time($interval) fill(null)');
});
});
describe('render series with math and alias', function() {
it('should generate correct query', function() {
var query = new InfluxQuery({
measurement: 'cpu',
select: [
[
{type: 'field', params: ['value']},
{type: 'mean', params: []},
{type: 'math', params: ['/100']},
{type: 'alias', params: ['text']},
]
]
});
var queryText = query.render();
expect(queryText).to.be('SELECT mean("value") /100 AS "text" FROM "cpu" WHERE $timeFilter GROUP BY time($interval) fill(null)');
});
});
describe('series with single tag only', function() {
it('should generate correct query', function() {
var query = new InfluxQuery({
measurement: 'cpu',
groupBy: [{type: 'time', params: ['auto']}],
tags: [{key: 'hostname', value: 'server1'}]
});
var queryText = query.render();
expect(queryText).to.be('SELECT mean("value") FROM "cpu" WHERE "hostname" = \'server1\' AND $timeFilter'
+ ' GROUP BY time($interval)');
});
it('should switch regex operator with tag value is regex', function() {
var query = new InfluxQuery({
measurement: 'cpu',
groupBy: [{type: 'time', params: ['auto']}],
tags: [{key: 'app', value: '/e.*/'}]
});
var queryText = query.render();
expect(queryText).to.be('SELECT mean("value") FROM "cpu" WHERE "app" =~ /e.*/ AND $timeFilter GROUP BY time($interval)');
});
});
describe('series with multiple tags only', function() {
it('should generate correct query', function() {
var query = new InfluxQuery({
measurement: 'cpu',
groupBy: [{type: 'time', params: ['auto']}],
tags: [{key: 'hostname', value: 'server1'}, {key: 'app', value: 'email', condition: "AND"}]
});
var queryText = query.render();
expect(queryText).to.be('SELECT mean("value") FROM "cpu" WHERE "hostname" = \'server1\' AND "app" = \'email\' AND ' +
'$timeFilter GROUP BY time($interval)');
});
});
describe('series with tags OR condition', function() {
it('should generate correct query', function() {
var query = new InfluxQuery({
measurement: 'cpu',
groupBy: [{type: 'time', params: ['auto']}],
tags: [{key: 'hostname', value: 'server1'}, {key: 'hostname', value: 'server2', condition: "OR"}]
});
var queryText = query.render();
expect(queryText).to.be('SELECT mean("value") FROM "cpu" WHERE "hostname" = \'server1\' OR "hostname" = \'server2\' AND ' +
'$timeFilter GROUP BY time($interval)');
});
});
describe('series with groupByTag', function() {
it('should generate correct query', function() {
var query = new InfluxQuery({
measurement: 'cpu',
tags: [],
groupBy: [{type: 'time', interval: 'auto'}, {type: 'tag', params: ['host']}],
});
var queryText = query.render();
expect(queryText).to.be('SELECT mean("value") FROM "cpu" WHERE $timeFilter ' +
'GROUP BY time($interval), "host"');
});
});
describe('render series without group by', function() {
it('should generate correct query', function() {
var query = new InfluxQuery({
measurement: 'cpu',
select: [[{type: 'field', params: ['value']}]],
groupBy: [],
});
var queryText = query.render();
expect(queryText).to.be('SELECT "value" FROM "cpu" WHERE $timeFilter');
});
});
describe('render series without group by and fill', function() {
it('should generate correct query', function() {
var query = new InfluxQuery({
measurement: 'cpu',
select: [[{type: 'field', params: ['value']}]],
groupBy: [{type: 'time'}, {type: 'fill', params: ['0']}],
});
var queryText = query.render();
expect(queryText).to.be('SELECT "value" FROM "cpu" WHERE $timeFilter GROUP BY time($interval) fill(0)');
});
});
describe('when adding group by part', function() {
it('should add tag before fill', function() {
var query = new InfluxQuery({
measurement: 'cpu',
groupBy: [{type: 'time'}, {type: 'fill'}]
});
query.addGroupBy('tag(host)');
expect(query.target.groupBy.length).to.be(3);
expect(query.target.groupBy[1].type).to.be('tag');
expect(query.target.groupBy[1].params[0]).to.be('host');
expect(query.target.groupBy[2].type).to.be('fill');
});
it('should add tag last if no fill', function() {
var query = new InfluxQuery({
measurement: 'cpu',
groupBy: []
});
query.addGroupBy('tag(host)');
expect(query.target.groupBy.length).to.be(1);
expect(query.target.groupBy[0].type).to.be('tag');
});
});
describe('when adding select part', function() {
it('should add mean after after field', function() {
var query = new InfluxQuery({
measurement: 'cpu',
select: [[{type: 'field', params: ['value']}]]
});
query.addSelectPart(query.selectModels[0], 'mean');
expect(query.target.select[0].length).to.be(2);
expect(query.target.select[0][1].type).to.be('mean');
});
it('should replace sum by mean', function() {
var query = new InfluxQuery({
measurement: 'cpu',
select: [[{type: 'field', params: ['value']}, {type: 'mean'}]]
});
query.addSelectPart(query.selectModels[0], 'sum');
expect(query.target.select[0].length).to.be(2);
expect(query.target.select[0][1].type).to.be('sum');
});
it('should add math before alias', function() {
var query = new InfluxQuery({
measurement: 'cpu',
select: [[{type: 'field', params: ['value']}, {type: 'mean'}, {type: 'alias'}]]
});
query.addSelectPart(query.selectModels[0], 'math');
expect(query.target.select[0].length).to.be(4);
expect(query.target.select[0][2].type).to.be('math');
});
it('should add math last', function() {
var query = new InfluxQuery({
measurement: 'cpu',
select: [[{type: 'field', params: ['value']}, {type: 'mean'}]]
});
query.addSelectPart(query.selectModels[0], 'math');
expect(query.target.select[0].length).to.be(3);
expect(query.target.select[0][2].type).to.be('math');
});
it('should replace math', function() {
var query = new InfluxQuery({
measurement: 'cpu',
select: [[{type: 'field', params: ['value']}, {type: 'mean'}, {type: 'math'}]]
});
query.addSelectPart(query.selectModels[0], 'math');
expect(query.target.select[0].length).to.be(3);
expect(query.target.select[0][2].type).to.be('math');
});
});
});

View File

@ -6,116 +6,6 @@ declare var InfluxQueryBuilder: any;
describe('InfluxQueryBuilder', function() {
describe('series with mesurement only', function() {
it('should generate correct query', function() {
var builder = new InfluxQueryBuilder({
measurement: 'cpu',
groupBy: [{type: 'time', interval: 'auto'}]
});
var query = builder.build();
expect(query).to.be('SELECT mean("value") AS "value" FROM "cpu" WHERE $timeFilter GROUP BY time($interval)');
});
});
describe('series with math expr and as expr', function() {
it('should generate correct query', function() {
var builder = new InfluxQueryBuilder({
measurement: 'cpu',
fields: [{name: 'test', func: 'max', mathExpr: '*2', asExpr: 'new_name'}],
groupBy: [{type: 'time', interval: 'auto'}]
});
var query = builder.build();
expect(query).to.be('SELECT max("test")*2 AS "new_name" FROM "cpu" WHERE $timeFilter GROUP BY time($interval)');
});
});
describe('series with single tag only', function() {
it('should generate correct query', function() {
var builder = new InfluxQueryBuilder({
measurement: 'cpu',
groupBy: [{type: 'time', interval: 'auto'}],
tags: [{key: 'hostname', value: 'server1'}]
});
var query = builder.build();
expect(query).to.be('SELECT mean("value") AS "value" FROM "cpu" WHERE "hostname" = \'server1\' AND $timeFilter'
+ ' GROUP BY time($interval)');
});
it('should switch regex operator with tag value is regex', function() {
var builder = new InfluxQueryBuilder({
measurement: 'cpu',
groupBy: [{type: 'time', interval: 'auto'}],
tags: [{key: 'app', value: '/e.*/'}]
});
var query = builder.build();
expect(query).to.be('SELECT mean("value") AS "value" FROM "cpu" WHERE "app" =~ /e.*/ AND $timeFilter GROUP BY time($interval)');
});
});
describe('series with multiple fields', function() {
it('should generate correct query', function() {
var builder = new InfluxQueryBuilder({
measurement: 'cpu',
tags: [],
groupBy: [{type: 'time', interval: 'auto'}],
fields: [{ name: 'tx_in', func: 'sum' }, { name: 'tx_out', func: 'mean' }]
});
var query = builder.build();
expect(query).to.be('SELECT sum("tx_in") AS "tx_in", mean("tx_out") AS "tx_out" ' +
'FROM "cpu" WHERE $timeFilter GROUP BY time($interval)');
});
});
describe('series with multiple tags only', function() {
it('should generate correct query', function() {
var builder = new InfluxQueryBuilder({
measurement: 'cpu',
groupBy: [{type: 'time', interval: 'auto'}],
tags: [{key: 'hostname', value: 'server1'}, {key: 'app', value: 'email', condition: "AND"}]
});
var query = builder.build();
expect(query).to.be('SELECT mean("value") AS "value" FROM "cpu" WHERE "hostname" = \'server1\' AND "app" = \'email\' AND ' +
'$timeFilter GROUP BY time($interval)');
});
});
describe('series with tags OR condition', function() {
it('should generate correct query', function() {
var builder = new InfluxQueryBuilder({
measurement: 'cpu',
groupBy: [{type: 'time', interval: 'auto'}],
tags: [{key: 'hostname', value: 'server1'}, {key: 'hostname', value: 'server2', condition: "OR"}]
});
var query = builder.build();
expect(query).to.be('SELECT mean("value") AS "value" FROM "cpu" WHERE "hostname" = \'server1\' OR "hostname" = \'server2\' AND ' +
'$timeFilter GROUP BY time($interval)');
});
});
describe('series with groupByTag', function() {
it('should generate correct query', function() {
var builder = new InfluxQueryBuilder({
measurement: 'cpu',
tags: [],
groupBy: [{type: 'time', interval: 'auto'}, {type: 'tag', key: 'host'}],
});
var query = builder.build();
expect(query).to.be('SELECT mean("value") AS "value" FROM "cpu" WHERE $timeFilter ' +
'GROUP BY time($interval), "host"');
});
});
describe('when building explore queries', function() {
it('should only have measurement condition in tag keys query given query with measurement', function() {
@ -126,8 +16,7 @@ describe('InfluxQueryBuilder', function() {
it('should handle regex measurement in tag keys query', function() {
var builder = new InfluxQueryBuilder({
measurement: '/.*/',
tags: []
measurement: '/.*/', tags: []
});
var query = builder.buildExploreQuery('TAG_KEYS');
expect(query).to.be('SHOW TAG KEYS FROM /.*/');
@ -170,7 +59,10 @@ describe('InfluxQueryBuilder', function() {
});
it('should switch to regex operator in tag condition', function() {
var builder = new InfluxQueryBuilder({measurement: 'cpu', tags: [{key: 'host', value: '/server.*/'}]});
var builder = new InfluxQueryBuilder({
measurement: 'cpu',
tags: [{key: 'host', value: '/server.*/'}]
});
var query = builder.buildExploreQuery('TAG_VALUES', 'app');
expect(query).to.be('SHOW TAG VALUES FROM "cpu" WITH KEY = "app" WHERE "host" =~ /server.*/');
});

View File

@ -0,0 +1,41 @@
import {describe, beforeEach, it, sinon, expect} from 'test/lib/common';
import queryPart = require('../query_part');
describe('InfluxQueryPart', () => {
describe('series with mesurement only', () => {
it('should handle nested function parts', () => {
var part = queryPart.create({
type: 'derivative',
params: ['10s'],
});
expect(part.text).to.be('derivative(10s)');
expect(part.render('mean(value)')).to.be('derivative(mean(value), 10s)');
});
it('should handle suffirx parts', () => {
var part = queryPart.create({
type: 'math',
params: ['/ 100'],
});
expect(part.text).to.be('math(/ 100)');
expect(part.render('mean(value)')).to.be('mean(value) / 100');
});
it('should handle alias parts', () => {
var part = queryPart.create({
type: 'alias',
params: ['test'],
});
expect(part.text).to.be('alias(test)');
expect(part.render('mean(value)')).to.be('mean(value) AS "test"');
});
});
});

View File

@ -111,11 +111,9 @@ function (angular, _, moment, dateMath) {
var url = '/api/v1/label/__name__/values';
return this._request('GET', url).then(function(result) {
var suggestData = _.filter(result.data.data, function(metricName) {
return metricName.indexOf(query) !== 1;
return _.filter(result.data.data, function (metricName) {
return metricName.indexOf(query) !== 1;
});
return suggestData;
});
};

View File

@ -6,7 +6,7 @@
<li class="tight-form-item" style="width: 80px">
Left Y
</li>
<li class="tight-form-item">
<li class="tight-form-item" style="width: 40px">
Unit
</li>
<li class="dropdown" style="width: 140px;"
@ -14,22 +14,6 @@
dropdown-typeahead="unitFormats"
dropdown-typeahead-on-select="setUnitFormat(0, $subItem)">
</li>
<li class="tight-form-item">
&nbsp;&nbsp; Grid Max
</li>
<li>
<input type="number" class="input-small tight-form-input" placeholder="auto"
empty-to-null ng-model="panel.grid.leftMax"
ng-change="render()" ng-model-onblur>
</li>
<li class="tight-form-item">
Min
</li>
<li>
<input type="number" class="input-small tight-form-input" placeholder="auto"
empty-to-null ng-model="panel.grid.leftMin"
ng-change="render()" ng-model-onblur>
</li>
<li class="tight-form-item">
Scale type
</li>
@ -46,12 +30,36 @@
</ul>
<div class="clearfix"></div>
</div>
<div class="tight-form last">
<div class="tight-form">
<ul class="tight-form-list">
<li class="tight-form-item" style="width: 80px">
<i class="fa fa-remove invisible"></i>
</li>
<li class="tight-form-item">
Y-Max
</li>
<li>
<input type="number" class="input-small tight-form-input" placeholder="auto"
empty-to-null ng-model="panel.grid.leftMax"
ng-change="render()" ng-model-onblur>
</li>
<li class="tight-form-item" style="width: 115px; text-align: right;">
Y-Min
</li>
<li>
<input type="number" class="input-small tight-form-input" placeholder="auto" style="width: 113px;"
empty-to-null ng-model="panel.grid.leftMin"
ng-change="render()" ng-model-onblur>
</li>
</ul>
<div class="clearfix"></div>
</div>
<div class="tight-form">
<ul class="tight-form-list">
<li class="tight-form-item" style="width: 80px">
Right Y
</li>
<li class="tight-form-item">
<li class="tight-form-item" style="width: 40px">
Unit
</li>
<li class="dropdown" style="width: 140px"
@ -59,22 +67,6 @@
dropdown-typeahead="unitFormats"
dropdown-typeahead-on-select="setUnitFormat(1, $subItem)">
</li>
<li class="tight-form-item">
&nbsp;&nbsp; Grid Max
</li>
<li>
<input type="number" class="input-small tight-form-input" placeholder="auto"
empty-to-null ng-model="panel.grid.rightMax"
ng-change="render()" ng-model-onblur>
</li>
<li class="tight-form-item">
Min
</li>
<li>
<input type="number" class="input-small tight-form-input" placeholder="auto"
empty-to-null ng-model="panel.grid.rightMin"
ng-change="render()" ng-model-onblur>
</li>
<li class="tight-form-item">
Scale type
</li>
@ -91,6 +83,31 @@
</ul>
<div class="clearfix"></div>
</div>
<div class="tight-form last">
<ul class="tight-form-list">
<li class="tight-form-item" style="width: 80px">
<i class="fa fa-remove invisible"></i>
</li>
<li class="tight-form-item">
Y-Max
</li>
<li>
<input type="number" class="input-small tight-form-input" placeholder="auto"
empty-to-null ng-model="panel.grid.rightMax"
ng-change="render()" ng-model-onblur>
</li>
<li class="tight-form-item" style="width: 115px; text-align: right;">
Y-Min
</li>
<li>
<input type="number" class="input-small tight-form-input" placeholder="auto" style="width: 113px;"
empty-to-null ng-model="panel.grid.rightMin"
ng-change="render()" ng-model-onblur>
</li>
</ul>
<div class="clearfix"></div>
</div>
</div>
<div class="section" style="margin-bottom: 20px">
@ -150,9 +167,9 @@
<div class="editor-row">
<div class="section">
<div class="tight-form last">
<div class="tight-form">
<ul class="tight-form-list">
<li class="tight-form-item" style="width: 110px">
<li class="tight-form-item" style="width: 80px">
Legend
</li>
<li class="tight-form-item">
@ -164,18 +181,28 @@
<li class="tight-form-item">
<editor-checkbox text="Right side" model="panel.legend.rightSide" change="render()"></editor-checkbox>
</li>
</ul>
<div class="clearfix"></div>
</div>
<div class="tight-form">
<ul class="tight-form-list">
<li class="tight-form-item" style="width: 80px">
Hide series
</li>
<li class="tight-form-item">
<editor-checkbox text="With only nulls" model="panel.legend.hideEmpty" change="render()"></editor-checkbox>
</li>
<li class="tight-form-item last">
<editor-checkbox text="Hide empty" model="panel.legend.hideEmpty" change="render()"></editor-checkbox>
<editor-checkbox text="With only zeroes" model="panel.legend.hideZero" change="render()"></editor-checkbox>
</li>
</ul>
<div class="clearfix"></div>
</div>
</div>
<div class="section">
<div class="tight-form">
<div class="tight-form last">
<ul class="tight-form-list">
<li class="tight-form-item" style="width: 105px">
Legend values
<li class="tight-form-item" style="width: 80px">
Values
</li>
<li class="tight-form-item">
<editor-checkbox text="Min" model="panel.legend.min" change="legendValuesOptionChanged()"></editor-checkbox>
@ -189,16 +216,11 @@
<li class="tight-form-item">
<editor-checkbox text="Current" model="panel.legend.current" change="legendValuesOptionChanged()"></editor-checkbox>
</li>
<li class="tight-form-item last">
<li class="tight-form-item">
<editor-checkbox text="Total" model="panel.legend.total" change="legendValuesOptionChanged()"></editor-checkbox>
</li>
</ul>
<div class="clearfix"></div>
</div>
<div class="tight-form last">
<ul class="tight-form-list">
<li class="tight-form-item" style="width: 105px">
<strong>Decimals</strong>
<li class="tight-form-item">
Decimals
</li>
<li style="width: 105px">
<input type="number" class="input-small tight-form-input" placeholder="auto" bs-tooltip="'Override automatic decimal precision for legend and tooltips'" data-placement="right"
@ -207,7 +229,5 @@
</ul>
<div class="clearfix"></div>
</div>
</div>
</div>

View File

@ -52,6 +52,11 @@ function ($) {
continue;
}
if (!series.data.length || (scope.panel.legend.hideZero && series.allIsZero)) {
results.push({ hidden: true });
continue;
}
hoverIndex = this.findHoverIndexFromData(pos.x, series);
results.time = series.data[hoverIndex][0];

View File

@ -137,6 +137,10 @@ function (angular, _, $) {
if (!series.legend) {
continue;
}
// ignore zero series
if (panel.legend.hideZero && series.allIsZero) {
continue;
}
var html = '<div class="graph-legend-series';
if (series.yaxis === 2) { html += ' pull-right'; }

View File

@ -60,7 +60,6 @@ export class TablePanelCtrl {
}
_.defaults($scope.panel, panelDefaults);
panelSrv.init($scope);
};

View File

@ -16,7 +16,7 @@
</ul>
<div class="clearfix"></div>
</div>
<div class="tight-form" ng-if="showColumnOptions">
<div class="tight-form">
<ul class="tight-form-list">
<li class="tight-form-item" style="width: 140px">
Columns
@ -27,7 +27,8 @@
{{column.text}}
</span>
</li>
<li class="dropdown" dropdown-typeahead="columnsMenu" dropdown-typeahead-on-select="addColumn($item, $subItem)">
<li>
<metric-segment segment="addColumnSegment" get-options="getColumnOptions()" on-change="addColumn()"></metric-segment>
</li>
</ul>
<div class="clearfix"></div>
@ -158,7 +159,7 @@
</div>
<button class="btn btn-inverse" style="margin-top: 20px" ng-click="addColumnStyle()">
Add style display rule
Add column style rule
</button>
</div>

View File

@ -8,93 +8,101 @@ import moment = require('moment');
import {transformers} from './transformers';
export function tablePanelEditor() {
export class TablePanelEditorCtrl {
/** @ngInject */
constructor($scope, $q, uiSegmentSrv) {
$scope.transformers = transformers;
$scope.unitFormats = kbn.getUnitFormats();
$scope.colorModes = [
{text: 'Disabled', value: null},
{text: 'Cell', value: 'cell'},
{text: 'Value', value: 'value'},
{text: 'Row', value: 'row'},
];
$scope.columnTypes = [
{text: 'Number', value: 'number'},
{text: 'String', value: 'string'},
{text: 'Date', value: 'date'},
];
$scope.fontSizes = ['80%', '90%', '100%', '110%', '120%', '130%', '150%', '160%', '180%', '200%', '220%', '250%'];
$scope.dateFormats = [
{text: 'YYYY-MM-DD HH:mm:ss', value: 'YYYY-MM-DD HH:mm:ss'},
{text: 'MM/DD/YY h:mm:ss a', value: 'MM/DD/YY h:mm:ss a'},
{text: 'MMMM D, YYYY LT', value: 'MMMM D, YYYY LT'},
];
$scope.addColumnSegment = uiSegmentSrv.newPlusButton();
$scope.getColumnOptions = function() {
if (!$scope.dataRaw) {
return $q.when([]);
}
var columns = transformers[$scope.panel.transform].getColumns($scope.dataRaw);
var segments = _.map(columns, (c: any) => uiSegmentSrv.newSegment({value: c.text}));
return $q.when(segments);
};
$scope.addColumn = function() {
$scope.panel.columns.push({text: $scope.addColumnSegment.value, value: $scope.addColumnSegment.value});
$scope.render();
var plusButton = uiSegmentSrv.newPlusButton();
$scope.addColumnSegment.html = plusButton.html;
};
$scope.transformChanged = function() {
$scope.panel.columns = [];
$scope.render();
};
$scope.removeColumn = function(column) {
$scope.panel.columns = _.without($scope.panel.columns, column);
$scope.render();
};
$scope.setUnitFormat = function(column, subItem) {
column.unit = subItem.value;
$scope.render();
};
$scope.addColumnStyle = function() {
var columnStyleDefaults = {
unit: 'short',
type: 'number',
decimals: 2,
colors: ["rgba(245, 54, 54, 0.9)", "rgba(237, 129, 40, 0.89)", "rgba(50, 172, 45, 0.97)"],
colorMode: null,
pattern: '/.*/',
dateFormat: 'YYYY-MM-DD HH:mm:ss',
thresholds: [],
};
$scope.panel.styles.push(angular.copy(columnStyleDefaults));
};
$scope.removeColumnStyle = function(style) {
$scope.panel.styles = _.without($scope.panel.styles, style);
};
$scope.getColumnNames = function() {
if (!$scope.table) {
return [];
}
return _.map($scope.table.columns, function(col: any) {
return col.text;
});
};
}
}
export function tablePanelEditor($q, uiSegmentSrv) {
'use strict';
return {
restrict: 'E',
scope: true,
templateUrl: 'app/plugins/panels/table/editor.html',
link: function(scope, elem) {
scope.transformers = transformers;
scope.unitFormats = kbn.getUnitFormats();
scope.colorModes = [
{text: 'Disabled', value: null},
{text: 'Cell', value: 'cell'},
{text: 'Value', value: 'value'},
{text: 'Row', value: 'row'},
];
scope.columnTypes = [
{text: 'Number', value: 'number'},
{text: 'String', value: 'string'},
{text: 'Date', value: 'date'},
];
scope.fontSizes = ['80%', '90%', '100%', '110%', '120%', '130%', '150%', '160%', '180%', '200%', '220%', '250%'];
scope.dateFormats = [
{text: 'YYYY-MM-DD HH:mm:ss', value: 'YYYY-MM-DD HH:mm:ss'},
{text: 'MM/DD/YY h:mm:ss a', value: 'MM/DD/YY h:mm:ss a'},
{text: 'MMMM D, YYYY LT', value: 'MMMM D, YYYY LT'},
];
scope.updateColumnsMenu = function(data) {
scope.columnsMenu = transformers[scope.panel.transform].getColumns(data);
scope.showColumnOptions = true;
};
scope.$on('render', function(event, table, rawData) {
scope.updateColumnsMenu(rawData);
});
scope.addColumn = function(menuItem) {
scope.panel.columns.push({text: menuItem.text, value: menuItem.value});
scope.render();
};
scope.transformChanged = function() {
scope.panel.columns = [];
scope.updateColumnsMenu();
scope.render();
};
scope.removeColumn = function(column) {
scope.panel.columns = _.without(scope.panel.columns, column);
scope.render();
};
scope.setUnitFormat = function(column, subItem) {
column.unit = subItem.value;
scope.render();
};
scope.addColumnStyle = function() {
var columnStyleDefaults = {
unit: 'short',
type: 'number',
decimals: 2,
colors: ["rgba(245, 54, 54, 0.9)", "rgba(237, 129, 40, 0.89)", "rgba(50, 172, 45, 0.97)"],
colorMode: null,
pattern: '/.*/',
dateFormat: 'YYYY-MM-DD HH:mm:ss',
thresholds: [],
};
scope.panel.styles.push(angular.copy(columnStyleDefaults));
};
scope.removeColumnStyle = function(style) {
scope.panel.styles = _.without(scope.panel.styles, style);
};
scope.getColumnNames = function() {
if (!scope.table) {
return [];
}
return _.map(scope.table.columns, function(col: any) {
return col.text;
});
};
scope.updateColumnsMenu(scope.dataRaw);
}
templateUrl: 'app/panels/table/editor.html',
controller: TablePanelEditorCtrl,
};
}

View File

@ -19,6 +19,7 @@ export function tablePanel() {
link: function(scope, elem) {
var data;
var panel = scope.panel;
var pageCount = 0;
var formaters = [];
function getTableHeight() {
@ -26,8 +27,11 @@ export function tablePanel() {
if (_.isString(panelHeight)) {
panelHeight = parseInt(panelHeight.replace('px', ''), 10);
}
if (pageCount > 1) {
panelHeight -= 28;
}
return (panelHeight - 40) + 'px';
return (panelHeight - 60) + 'px';
}
function appendTableRows(tbodyElem) {
@ -46,7 +50,7 @@ export function tablePanel() {
footerElem.empty();
var pageSize = panel.pageSize || 100;
var pageCount = Math.ceil(data.rows.length / pageSize);
pageCount = Math.ceil(data.rows.length / pageSize);
if (pageCount === 1) {
return;
}
@ -73,12 +77,10 @@ export function tablePanel() {
appendTableRows(tbodyElem);
rootElem.css({
'max-height': panel.scroll ? getTableHeight() : ''
});
container.css({'font-size': panel.fontSize});
appendPaginationControls(footerElem);
rootElem.css({'max-height': panel.scroll ? getTableHeight() : '' });
}
elem.on('click', '.table-panel-page-link', switchPage);

View File

@ -1,6 +1,7 @@
import {describe, beforeEach, it, sinon, expect} from 'test/lib/common';
import {TableModel} from '../table_model';
import {transformers} from '../transformers';
describe('when transforming time series table', () => {
var table;
@ -100,7 +101,11 @@ describe('when transforming time series table', () => {
describe('JSON Data', () => {
var panel = {
transform: 'json',
columns: [{text: 'Timestamp', value: 'timestamp'}, {text: 'Message', value: 'message'}]
columns: [
{text: 'Timestamp', value: 'timestamp'},
{text: 'Message', value: 'message'},
{text: 'nested.level2', value: 'nested.level2'},
]
};
var rawData = [
{
@ -108,26 +113,42 @@ describe('when transforming time series table', () => {
datapoints: [
{
timestamp: 'time',
message: 'message'
message: 'message',
nested: {
level2: 'level2-value'
}
}
]
}
];
beforeEach(() => {
table = TableModel.transform(rawData, panel);
describe('getColumns', function() {
it('should return nested properties', function() {
var columns = transformers['json'].getColumns(rawData);
expect(columns[0].text).to.be('timestamp');
expect(columns[1].text).to.be('message');
expect(columns[2].text).to.be('nested.level2');
});
});
it ('should return 2 columns', () => {
expect(table.columns.length).to.be(2);
expect(table.columns[0].text).to.be('Timestamp');
expect(table.columns[1].text).to.be('Message');
});
describe('transform', function() {
beforeEach(() => {
table = TableModel.transform(rawData, panel);
});
it ('should return 2 rows', () => {
expect(table.rows.length).to.be(1);
expect(table.rows[0][0]).to.be('time');
expect(table.rows[0][1]).to.be('message');
it ('should return 2 columns', () => {
expect(table.columns.length).to.be(3);
expect(table.columns[0].text).to.be('Timestamp');
expect(table.columns[1].text).to.be('Message');
expect(table.columns[2].text).to.be('nested.level2');
});
it ('should return 2 rows', () => {
expect(table.rows.length).to.be(1);
expect(table.rows[0][0]).to.be('time');
expect(table.rows[0][1]).to.be('message');
expect(table.rows[0][2]).to.be('level2-value');
});
});
});

View File

@ -2,6 +2,7 @@
import moment = require('moment');
import _ = require('lodash');
import flatten = require('app/core/utils/flatten');
import TimeSeries = require('app/core/time_series');
var transformers = {};
@ -149,9 +150,12 @@ transformers['json'] = {
continue;
}
for (var y = 0; y < series.datapoints.length; y++) {
// only look at 100 docs
var maxDocs = Math.min(series.datapoints.length, 100);
for (var y = 0; y < maxDocs; y++) {
var doc = series.datapoints[y];
for (var propName in doc) {
var flattened = flatten(doc, null);
for (var propName in flattened) {
names[propName] = true;
}
}
@ -177,13 +181,16 @@ transformers['json'] = {
for (y = 0; y < series.datapoints.length; y++) {
var dp = series.datapoints[y];
var values = [];
for (z = 0; z < panel.columns.length; z++) {
values.push(dp[panel.columns[z].value]);
}
if (values.length === 0) {
if (_.isObject(dp) && panel.columns.length > 0) {
var flattened = flatten(dp, null);
for (z = 0; z < panel.columns.length; z++) {
values.push(flattened[panel.columns[z].value]);
}
} else {
values.push(JSON.stringify(dp));
}
model.rows.push(values);
}
}

View File

@ -241,7 +241,7 @@
{
"type": "query",
"datasource": null,
"refresh_on_load": false,
"refresh": false,
"name": "metric",
"options": [],
"includeAll": true,

View File

@ -0,0 +1,24 @@
import {describe, beforeEach, it, sinon, expect} from 'test/lib/common'
import flatten = require('app/core/utils/flatten')
describe("flatten", () => {
it('should return flatten object', () => {
var flattened = flatten({
level1: 'level1-value',
deeper: {
level2: 'level2-value',
deeper: {
level3: 'level3-value'
}
}
}, null);
expect(flattened['level1']).to.be('level1-value');
expect(flattened['deeper.level2']).to.be('level2-value');
expect(flattened['deeper.deeper.level3']).to.be('level3-value');
});
});

View File

@ -68,6 +68,27 @@ define([
describeValueFormat('wps', 789000000, 1000000, -1, '789M wps');
describeValueFormat('iops', 11000000000, 1000000000, -1, '11B iops');
describeValueFormat('s', 24, 1, 0, '24 s');
describeValueFormat('s', 246, 1, 0, '4.1 min');
describeValueFormat('s', 24567, 100, 0, '6.82 hour');
describeValueFormat('s', 24567890, 10000, 0, '40.62 week');
describeValueFormat('s', 24567890000, 1000000, 0, '778.53 year');
describeValueFormat('m', 24, 1, 0, '24 min');
describeValueFormat('m', 246, 10, 0, '4.1 hour');
describeValueFormat('m', 6545, 10, 0, '4.55 day');
describeValueFormat('m', 24567, 100, 0, '2.44 week');
describeValueFormat('m', 24567892, 10000, 0, '46.7 year');
describeValueFormat('h', 21, 1, 0, '21 hour');
describeValueFormat('h', 145, 1, 0, '6.04 day');
describeValueFormat('h', 1234, 100, 0, '7.3 week');
describeValueFormat('h', 9458, 1000, 0, '1.08 year');
describeValueFormat('d', 3, 1, 0, '3 day');
describeValueFormat('d', 245, 100, 0, '35 week');
describeValueFormat('d', 2456, 10, 0, '6.73 year');
describe('kbn.toFixed and negative decimals', function() {
it('should treat as zero decimals', function() {
var str = kbn.toFixed(186.123, -2);

View File

@ -204,7 +204,7 @@ define([
});
it('dashboard schema version should be set to latest', function() {
expect(model.schemaVersion).to.be(7);
expect(model.schemaVersion).to.be(8);
});
});
@ -248,5 +248,90 @@ define([
expect(clone.meta).to.be(undefined);
});
});
describe('when loading dashboard with old influxdb query schema', function() {
var model;
var target;
beforeEach(function() {
model = _dashboardSrv.create({
rows: [{
panels: [{
type: 'graph',
targets: [{
"alias": "$tag_datacenter $tag_source $col",
"column": "value",
"measurement": "logins.count",
"fields": [
{
"func": "mean",
"name": "value",
"mathExpr": "*2",
"asExpr": "value"
},
{
"name": "one-minute",
"func": "mean",
"mathExpr": "*3",
"asExpr": "one-minute"
}
],
"tags": [],
"fill": "previous",
"function": "mean",
"groupBy": [
{
"interval": "auto",
"type": "time"
},
{
"key": "source",
"type": "tag"
},
{
"type": "tag",
"key": "datacenter"
}
],
}]
}]
}]
});
target = model.rows[0].panels[0].targets[0];
});
it('should update query schema', function() {
expect(target.fields).to.be(undefined);
expect(target.select.length).to.be(2);
expect(target.select[0].length).to.be(4);
expect(target.select[0][0].type).to.be('field');
expect(target.select[0][1].type).to.be('mean');
expect(target.select[0][2].type).to.be('math');
expect(target.select[0][3].type).to.be('alias');
});
});
describe('when creating dashboard model with missing list for annoations or templating', function() {
var model;
beforeEach(function() {
model = _dashboardSrv.create({
annotations: {
enable: true,
},
templating: {
enable: true
}
});
});
it('should add empty list', function() {
expect(model.annotations.list.length).to.be(0);
expect(model.templating.list.length).to.be(0);
});
});
});
});

View File

@ -78,13 +78,20 @@ define([
});
describe('setTime', function() {
it('should return disable refresh for absolute times', function() {
it('should return disable refresh if refresh is disabled for any range', function() {
_dashboard.refresh = false;
ctx.service.setTime({from: '2011-01-01', to: '2015-01-01' });
expect(_dashboard.refresh).to.be(false);
});
it('should restore refresh for absolute time range', function() {
_dashboard.refresh = '30s';
ctx.service.setTime({from: '2011-01-01', to: '2015-01-01' });
expect(_dashboard.refresh).to.be('30s');
});
it('should restore refresh after relative time range is set', function() {
_dashboard.refresh = '10s';
ctx.service.setTime({from: moment([2011,1,1]), to: moment([2015,1,1])});

View File

@ -855,7 +855,7 @@ var _DoLists = function(text) {
// Turn double returns into triple returns, so that we can make a
// paragraph for the last item in a list, if necessary:
list = list.replace(/\n{2,}/g,"\n\n\n");;
list = list.replace(/\n{2,}/g,"\n\n\n");
var result = _ProcessListItems(list);
// Trim any trailing whitespace, to put the closing `</$list_type>`
@ -875,7 +875,7 @@ var _DoLists = function(text) {
var list_type = (m3.search(/[*+-]/g)>-1) ? "ul" : "ol";
// Turn double returns into triple returns, so that we can make a
// paragraph for the last item in a list, if necessary:
var list = list.replace(/\n{2,}/g,"\n\n\n");;
list = list.replace(/\n{2,}/g,"\n\n\n");
var result = _ProcessListItems(list);
result = runup + "<"+list_type+">\n" + result + "</"+list_type+">\n";
return result;
@ -1451,4 +1451,4 @@ if (typeof define === 'function' && define.amd) {
define(function() {
return Showdown;
});
}
}

View File

@ -9,12 +9,12 @@
<title>Grafana</title>
[[if .User.LightTheme]]
<link rel="stylesheet" href="[[.AppSubUrl]]/css/grafana.light.min.css">
<link rel="stylesheet" href="[[.AppSubUrl]]/public/css/grafana.light.min.css">
[[ range $css := .PluginCss ]]
<link rel="stylesheet" href="[[$.AppSubUrl]]/[[ $css.Light ]]">
[[ end ]]
[[else]]
<link rel="stylesheet" href="[[.AppSubUrl]]/css/grafana.dark.min.css">
<link rel="stylesheet" href="[[.AppSubUrl]]/public/css/grafana.dark.min.css">
[[ range $css := .PluginCss ]]
<link rel="stylesheet" href="[[$.AppSubUrl]]/[[ $css.Dark ]]">
[[ end ]]
@ -22,10 +22,10 @@
<link rel="icon" type="image/png" href="[[.AppSubUrl]]/img/fav32.png">
<link rel="icon" type="image/png" href="[[.AppSubUrl]]/public/img/fav32.png">
<base href="[[.AppSubUrl]]/" />
<!-- build:js [[.AppSubUrl]]/app/app.js -->
<!-- build:js [[.AppSubUrl]]/public/app/app.js -->
<script src="[[.AppSubUrl]]/public/vendor/requirejs/require.js"></script>
<script src="[[.AppSubUrl]]/public/app/require_config.js"></script>
<!-- endbuild -->

View File

@ -13,7 +13,7 @@ module.exports = function(grunt) {
'karma:test',
'phantomjs',
'css',
'htmlmin:build',
// 'htmlmin:build',
'ngtemplates',
'cssmin:build',
'ngAnnotate:build',
@ -34,8 +34,8 @@ module.exports = function(grunt) {
for(var key in summary){
if(summary.hasOwnProperty(key)){
var orig = key.replace(root, root+'/[[.AppSubUrl]]');
var revved = summary[key].replace(root, root+'/[[.AppSubUrl]]');
var orig = key.replace(root, root+'/[[.AppSubUrl]]/public');
var revved = summary[key].replace(root, root+'/[[.AppSubUrl]]/public');
fixed[orig] = revved;
}
}

View File

@ -27,7 +27,7 @@ module.exports = function(config) {
js: {
src: [
'<%= tempDir %>/vendor/requirejs/require.js',
'<%= tempDir %>/app/components/require.config.js',
'<%= tempDir %>/app/require_config.js',
'<%= tempDir %>/app/app.js',
],
dest: '<%= genDir %>/app/app.js'