Merge branch 'master' into external-plugins

Conflicts:
	public/app/plugins/panels/table/editor.ts
	public/views/index.html
This commit is contained in:
Torkel Ödegaard 2015-12-02 18:30:48 +01:00
commit ee0e4d2b69
63 changed files with 1876 additions and 641 deletions

View File

@ -6,6 +6,8 @@
### Enhancements ### Enhancements
* **CloudWatch**: Support for multiple AWS Credentials, closes [#3053](https://github.com/grafana/grafana/issues/3053), [#3080](https://github.com/grafana/grafana/issues/3080) * **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) * **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 ### Bug Fixes
* **cloudwatch**: fix for handling of period for long time ranges, fixes [#3086](https://github.com/grafana/grafana/issues/3086) * **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 ### Dependencies
- Go 1.4 - Go 1.5
- NodeJS - NodeJS
### Get Code ### Get Code
@ -85,11 +85,12 @@ go get github.com/grafana/grafana
``` ```
### Building the backend ### Building the backend
Replace X.Y.Z by actual version number.
``` ```
cd $GOPATH/src/github.com/grafana/grafana cd $GOPATH/src/github.com/grafana/grafana
go run build.go setup (only needed once to install godep) go run build.go setup (only needed once to install godep)
godep restore (will pull down all golang lib dependencies in your current GOPATH) godep restore (will pull down all golang lib dependencies in your current GOPATH)
go build . godep go run build.go build
``` ```
### Building frontend assets ### Building frontend assets
@ -112,7 +113,7 @@ bra run
### Running ### Running
``` ```
./grafana ./bin/grafana-server
``` ```
Open grafana in your browser (default http://localhost:3000) and login with admin user (default user/pass = admin/admin). 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 ## Create a pull request
Before or after you create a pull request, sign the [contributor license agreement](http://grafana.org/docs/contributing/cla.html). Before or after you create a pull request, sign the [contributor license agreement](http://grafana.org/docs/contributing/cla.html).
## Contribute ## Contribute
If you have any idea for an improvement or found a bug do not hesitate to open an issue. 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 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 { func ldflags() string {
var b bytes.Buffer var b bytes.Buffer
b.WriteString("-w") b.WriteString("-w")
b.WriteString(fmt.Sprintf(" -X main.version %s", version)) b.WriteString(fmt.Sprintf(" -X main.version=%s", version))
b.WriteString(fmt.Sprintf(" -X main.commit %s", getGitSha())) b.WriteString(fmt.Sprintf(" -X main.commit=%s", getGitSha()))
b.WriteString(fmt.Sprintf(" -X main.buildstamp %d", buildStamp())) b.WriteString(fmt.Sprintf(" -X main.buildstamp=%d", buildStamp()))
return b.String() 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` folder and override any of the settings defined in
`conf/defaults.ini`. `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 the working directory to be the root install directory (where the binary
and the `public` folder is located). 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_description: Dashboard JSON Reference
page_keywords: grafana, dashboard, json, documentation page_keywords: grafana, dashboard, json, documentation
--- ---
@ -363,7 +363,7 @@ Usage of the fields is explained below:
], ],
"query": "tag_values(cpu.utilization.average,env)", "query": "tag_values(cpu.utilization.average,env)",
"refresh": false, "refresh": false,
"refresh_on_load": false, "refresh": false,
"type": "query" "type": "query"
}, },
{ {
@ -390,7 +390,7 @@ Usage of the fields is explained below:
} }
], ],
"query": "tag_values(cpu.utilization.average,app)", "query": "tag_values(cpu.utilization.average,app)",
"refresh_on_load": false, "refresh": false,
"regex": "", "regex": "",
"type": "query" "type": "query"
} }
@ -413,7 +413,7 @@ Usage of the above mentioned fields in the templating section is explained below
| **name** | name of variable | | **name** | name of variable |
| **options** | array of variable text/value pairs available for selection on dashboard | | **options** | array of variable text/value pairs available for selection on dashboard |
| **query** | datasource query used to fetch values for a variable | | **query** | datasource query used to fetch values for a variable |
| **refresh_on_load** | TODO | | **refresh** | TODO |
| **regex** | TODO | | **regex** | TODO |
| **type** | type of variable, i.e. `custom`, `query` or `interval` | | **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": [ "rows": [
{ {
} }
] ],
"schemaVersion": 6, "schemaVersion": 6,
"version": 0 "version": 0
}, }
} }
### Delete dashboard ### Delete dashboard
@ -787,7 +787,7 @@ Update Organisation, fields *Adress 1*, *Adress 2*, *City* are not implemented y
"id": 2, "id": 2,
"name": "User", "name": "User",
"login": "user", "login": "user",
"email": "user@mygraf.com" "email": "user@mygraf.com",
"isAdmin": false "isAdmin": false
} }
] ]
@ -1046,7 +1046,7 @@ Deletes the starring of the given Dashboard for the actual user.
"timezone":"browser", "timezone":"browser",
"title":"Home", "title":"Home",
"version":5 "version":5
} },
"expires": 3600 "expires": 3600
} }
@ -1091,34 +1091,33 @@ Keys:
"canStar":false, "canStar":false,
"slug":"", "slug":"",
"expires":"2200-13-32T25:23:23+02:00", "expires":"2200-13-32T25:23:23+02:00",
"created":"2200-13-32T28:24:23+02:00"}, "created":"2200-13-32T28:24:23+02:00"
},
{ "dashboard": {
"dashboard": { "editable":false,
"editable":false, "hideControls":true,
"hideControls":true, "nav":[
"nav":[
{
"enable":false,
"type":"timepicker"
}
],
"rows": [
{ {
"enable":false,
"type":"timepicker"
} }
], ],
"style":"dark", "rows": [
"tags":[], {
"templating":{
"list":[ }
] ],
}, "style":"dark",
"time":{ "tags":[],
}, "templating":{
"timezone":"browser", "list":[
"title":"Home", ]
"version":5 },
"time":{
},
"timezone":"browser",
"title":"Home",
"version":5
} }
} }
@ -1181,11 +1180,10 @@ Keys:
"pluginType":"datasource", "pluginType":"datasource",
"serviceName":"Grafana", "serviceName":"Grafana",
"type":"grafanasearch" "type":"grafanasearch"
}
} }
} },
} "defaultDatasource": "Grafana"
defaultDatasource: "Grafana"
} }
## Login ## Login

10
main.go
View File

@ -2,6 +2,7 @@ package main
import ( import (
"flag" "flag"
"fmt"
"io/ioutil" "io/ioutil"
"os" "os"
"os/signal" "os/signal"
@ -27,6 +28,7 @@ import (
var version = "master" var version = "master"
var commit = "NA" var commit = "NA"
var buildstamp string var buildstamp string
var build_date string
var configFile = flag.String("config", "", "path to config file") var configFile = flag.String("config", "", "path to config file")
var homePath = flag.String("homepath", "", "path to grafana install/home path, defaults to working directory") var homePath = flag.String("homepath", "", "path to grafana install/home path, defaults to working directory")
@ -38,6 +40,14 @@ func init() {
} }
func main() { 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) buildstampInt64, _ := strconv.ParseInt(buildstamp, 10, 64)
setting.BuildVersion = version setting.BuildVersion = version

View File

@ -21,7 +21,7 @@
"grunt-contrib-connect": "~0.5.0", "grunt-contrib-connect": "~0.5.0",
"grunt-contrib-copy": "~0.5.0", "grunt-contrib-copy": "~0.5.0",
"grunt-contrib-cssmin": "~0.6.1", "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-jshint": "~0.10.0",
"grunt-contrib-less": "~0.7.0", "grunt-contrib-less": "~0.7.0",
"grunt-contrib-requirejs": "~0.4.4", "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"
"github.com/aws/aws-sdk-go/aws/credentials" "github.com/aws/aws-sdk-go/aws/credentials"
"github.com/aws/aws-sdk-go/aws/credentials/ec2rolecreds" "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/aws/session"
"github.com/aws/aws-sdk-go/service/cloudwatch" "github.com/aws/aws-sdk-go/service/cloudwatch"
"github.com/aws/aws-sdk-go/service/ec2" "github.com/aws/aws-sdk-go/service/ec2"
@ -40,11 +41,12 @@ func init() {
} }
func handleGetMetricStatistics(req *cwRequest, c *middleware.Context) { func handleGetMetricStatistics(req *cwRequest, c *middleware.Context) {
sess := session.New()
creds := credentials.NewChainCredentials( creds := credentials.NewChainCredentials(
[]credentials.Provider{ []credentials.Provider{
&credentials.EnvProvider{}, &credentials.EnvProvider{},
&credentials.SharedCredentialsProvider{Filename: "", Profile: req.DataSource.Database}, &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{ cfg := &aws.Config{
@ -87,11 +89,12 @@ func handleGetMetricStatistics(req *cwRequest, c *middleware.Context) {
} }
func handleListMetrics(req *cwRequest, c *middleware.Context) { func handleListMetrics(req *cwRequest, c *middleware.Context) {
sess := session.New()
creds := credentials.NewChainCredentials( creds := credentials.NewChainCredentials(
[]credentials.Provider{ []credentials.Provider{
&credentials.EnvProvider{}, &credentials.EnvProvider{},
&credentials.SharedCredentialsProvider{Filename: "", Profile: req.DataSource.Database}, &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{ cfg := &aws.Config{
@ -126,8 +129,17 @@ func handleListMetrics(req *cwRequest, c *middleware.Context) {
} }
func handleDescribeInstances(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{ cfg := &aws.Config{
Region: aws.String(req.Region), Region: aws.String(req.Region),
Credentials: creds,
} }
svc := ec2.New(session.New(cfg), cfg) svc := ec2.New(session.New(cfg), cfg)

View File

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

View File

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

View File

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

View File

@ -7,6 +7,7 @@ function (angular, _, coreModule) {
'use strict'; 'use strict';
coreModule.service('uiSegmentSrv', function($sce, templateSrv) { coreModule.service('uiSegmentSrv', function($sce, templateSrv) {
var self = this;
function MetricSegment(options) { function MetricSegment(options) {
if (options === '*' || options.value === '*') { 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() { this.newSelectMetric = function() {
return new MetricSegment({value: 'select metric', fake: true}); return new MetricSegment({value: 'select metric', fake: true});
}; };

View File

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

View File

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

View File

@ -28,6 +28,7 @@ class TimeSeries {
stats: any; stats: any;
legend: boolean; legend: boolean;
allIsNull: boolean; allIsNull: boolean;
allIsZero: boolean;
decimals: number; decimals: number;
scaledDecimals: number; scaledDecimals: number;
@ -96,6 +97,7 @@ class TimeSeries {
this.stats.avg = null; this.stats.avg = null;
this.stats.current = null; this.stats.current = null;
this.allIsNull = true; this.allIsNull = true;
this.allIsZero = true;
var ignoreNulls = fillStyle === 'connected'; var ignoreNulls = fillStyle === 'connected';
var nullAsZero = fillStyle === 'null as zero'; var nullAsZero = fillStyle === 'null as zero';
@ -130,6 +132,10 @@ class TimeSeries {
} }
} }
if (currentValue != 0) {
this.allIsZero = false;
}
result.push([currentTime, currentValue]); 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 // Currencies
kbn.valueFormats.currencyUSD = kbn.formatBuilders.currency('$'); kbn.valueFormats.currencyUSD = kbn.formatBuilders.currency('$');
kbn.valueFormats.currencyGBP = kbn.formatBuilders.currency('£'); kbn.valueFormats.currencyGBP = kbn.formatBuilders.currency('£');
kbn.valueFormats.currencyEUR = kbn.formatBuilders.currency('€');
kbn.valueFormats.currencyJPY = kbn.formatBuilders.currency('¥');
// Data // Data
kbn.valueFormats.bits = kbn.formatBuilders.binarySIPrefix('b'); kbn.valueFormats.bits = kbn.formatBuilders.binarySIPrefix('b');
@ -430,7 +432,7 @@ function($, _) {
kbn.valueFormats.s = function(size, decimals, scaledDecimals) { kbn.valueFormats.s = function(size, decimals, scaledDecimals) {
if (size === null) { return ""; } if (size === null) { return ""; }
if (Math.abs(size) < 600) { if (Math.abs(size) < 60) {
return kbn.toFixed(size, decimals) + " s"; return kbn.toFixed(size, decimals) + " s";
} }
// Less than 1 hour, devide in minutes // 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 ///// ///// FORMAT MENU /////
kbn.getUnitFormats = function() { kbn.getUnitFormats = function() {
@ -508,6 +561,8 @@ function($, _) {
submenu: [ submenu: [
{text: 'Dollars ($)', value: 'currencyUSD'}, {text: 'Dollars ($)', value: 'currencyUSD'},
{text: 'Pounds (£)', value: 'currencyGBP'}, {text: 'Pounds (£)', value: 'currencyGBP'},
{text: 'Euro (€)', value: 'currencyEUR'},
{text: 'Yen (¥)', value: 'currencyJPY'},
] ]
}, },
{ {
@ -518,6 +573,9 @@ function($, _) {
{text: 'microseconds (µs)', value: 'µs' }, {text: 'microseconds (µs)', value: 'µs' },
{text: 'milliseconds (ms)', value: 'ms' }, {text: 'milliseconds (ms)', value: 'ms' },
{text: 'seconds (s)', value: 's' }, {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> <th></th>
</tr> </tr>
<tr ng-repeat="org in orgs"> <tr ng-repeat="org in orgs">
<td>{{org.id}}</td>
<td>{{org.name}}</td> <td>{{org.name}}</td>
<td style="width: 1%"> <td style="width: 1%">
<a href="admin/orgs/edit/{{org.id}}" class="btn btn-inverse btn-small"> <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.tags = data.tags || [];
this.style = data.style || "dark"; this.style = data.style || "dark";
this.timezone = data.timezone || 'browser'; this.timezone = data.timezone || 'browser';
this.editable = data.editable === false ? false : true; this.editable = data.editable !== false;
this.hideControls = data.hideControls || false; this.hideControls = data.hideControls || false;
this.sharedCrosshair = data.sharedCrosshair || false; this.sharedCrosshair = data.sharedCrosshair || false;
this.rows = data.rows || []; this.rows = data.rows || [];
@ -48,10 +48,10 @@ function (angular, $, _, moment) {
p._initMeta = function(meta) { p._initMeta = function(meta) {
meta = meta || {}; meta = meta || {};
meta.canShare = meta.canShare === false ? false : true; meta.canShare = meta.canShare !== false;
meta.canSave = meta.canSave === false ? false : true; meta.canSave = meta.canSave !== false;
meta.canStar = meta.canStar === false ? false : true; meta.canStar = meta.canStar !== false;
meta.canEdit = meta.canEdit === false ? false : true; meta.canEdit = meta.canEdit !== false;
if (!this.editable) { if (!this.editable) {
meta.canEdit = false; meta.canEdit = false;
@ -151,7 +151,6 @@ function (angular, $, _, moment) {
result.panel = panel; result.panel = panel;
result.row = row; result.row = row;
result.index = index; result.index = index;
return;
} }
}); });
}); });
@ -230,9 +229,9 @@ function (angular, $, _, moment) {
var i, j, k; var i, j, k;
var oldVersion = this.schemaVersion; var oldVersion = this.schemaVersion;
var panelUpgrades = []; var panelUpgrades = [];
this.schemaVersion = 7; this.schemaVersion = 8;
if (oldVersion === 7) { if (oldVersion === 8) {
return; 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) { if (panelUpgrades.length === 0) {
return; return;
} }

View File

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

View File

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

View File

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

View File

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

View File

@ -52,7 +52,7 @@ function (angular, _) {
if (link.asDropdown) { if (link.asDropdown) {
template += '<ul class="dropdown-menu" role="menu">' + 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>' + '<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); elem.html(template);

View File

@ -45,7 +45,7 @@
{{invite.email}} {{invite.email}}
<span ng-show="invite.name" style="padding-left: 20px"> {{invite.name}}</span> <span ng-show="invite.name" style="padding-left: 20px"> {{invite.name}}</span>
<span class="pull-right"> <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 <i class="fa fa-clipboard"></i> Copy Invite
</button> </button>
&nbsp; &nbsp;

View File

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

View File

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

View File

@ -45,17 +45,6 @@ function (angular, _, kbn) {
}; };
this.setVariableFromUrl = function(variable, urlValue) { 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 }); var option = _.findWhere(variable.options, { text: urlValue });
option = option || { text: urlValue, value: urlValue }; option = option || { text: urlValue, value: urlValue };

View File

@ -73,7 +73,7 @@
<ul class="tight-form-list" role="menu"> <ul class="tight-form-list" role="menu">
<li class="tight-form-item query-keyword tight-form-align" style="width: 100px"> <li class="tight-form-item query-keyword tight-form-align" style="width: 100px">
Alias Alias
<tip>{{metric}} {{stat}} {{namespace}} {{region}} {{<dimension name>}}</tip> <tip>{{metric}} {{stat}} {{namespace}} {{region}} {{DIMENSION_NAME}}</tip>
</li> </li>
<li> <li>
<input type="text" class="input-xlarge tight-form-input" ng-model="target.alias" spellcheck='false' ng-model-onblur ng-change="refreshMetricData()"> <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.bucketAggTypes = queryDef.bucketAggTypes;
$scope.orderOptions = queryDef.orderOptions; $scope.orderOptions = queryDef.orderOptions;
$scope.sizeOptions = queryDef.sizeOptions; $scope.sizeOptions = queryDef.sizeOptions;
$scope.intervalOptions = queryDef.intervalOptions;
$rootScope.onAppEvent('elastic-query-updated', function() { $rootScope.onAppEvent('elastic-query-updated', function() {
$scope.validateModel(); $scope.validateModel();
@ -128,6 +127,10 @@ function (angular, _, queryDef) {
} }
}; };
$scope.getIntervalOptions = function() {
return $q.when(uiSegmentSrv.transformToSegments(true, 'interval')(queryDef.intervalOptions));
};
$scope.addBucketAgg = function() { $scope.addBucketAgg = function() {
// if last is date histogram add it before // if last is date histogram add it before
var lastBucket = bucketAggs[bucketAggs.length - 1]; var lastBucket = bucketAggs[bucketAggs.length - 1];

View File

@ -41,7 +41,7 @@
Interval Interval
</li> </li>
<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> </li>
</ul> </ul>
<div class="clearfix"></div> <div class="clearfix"></div>

View File

@ -1,13 +1,12 @@
define([ define([
'angular', 'angular',
'lodash',
], ],
function (angular, _) { function (angular) {
'use strict'; 'use strict';
var module = angular.module('grafana.controllers'); var module = angular.module('grafana.controllers');
module.controller('ElasticQueryCtrl', function($scope, $timeout, uiSegmentSrv, templateSrv) { module.controller('ElasticQueryCtrl', function($scope, $timeout, uiSegmentSrv) {
$scope.init = function() { $scope.init = function() {
var target = $scope.target; var target = $scope.target;
@ -21,7 +20,7 @@ function (angular, _) {
$scope.getFields = function(type) { $scope.getFields = function(type) {
var jsonStr = angular.toJson({find: 'fields', type: type}); var jsonStr = angular.toJson({find: 'fields', type: type});
return $scope.datasource.metricFindQuery(jsonStr) return $scope.datasource.metricFindQuery(jsonStr)
.then($scope.transformToSegments(false)) .then(uiSegmentSrv.transformToSegments(false))
.then(null, $scope.handleQueryError); .then(null, $scope.handleQueryError);
}; };
@ -35,21 +34,6 @@ function (angular, _) {
$scope.appEvent('elastic-query-updated'); $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.handleQueryError = function(err) {
$scope.parserError = err.message || 'Failed to issue metric query'; $scope.parserError = err.message || 'Failed to issue metric query';
return []; return [];

View File

@ -3,11 +3,11 @@ define([
'lodash', 'lodash',
'app/core/utils/datemath', 'app/core/utils/datemath',
'./influx_series', './influx_series',
'./query_builder', './influx_query',
'./directives', './directives',
'./query_ctrl', './query_ctrl',
], ],
function (angular, _, dateMath, InfluxSeries, InfluxQueryBuilder) { function (angular, _, dateMath, InfluxSeries, InfluxQuery) {
'use strict'; 'use strict';
var module = angular.module('grafana.services'); var module = angular.module('grafana.services');
@ -41,8 +41,8 @@ function (angular, _, dateMath, InfluxSeries, InfluxQueryBuilder) {
queryTargets.push(target); queryTargets.push(target);
// build query // build query
var queryBuilder = new InfluxQueryBuilder(target); var queryModel = new InfluxQuery(target);
var query = queryBuilder.build(); var query = queryModel.render();
query = query.replace(/\$interval/g, (target.interval || options.interval)); query = query.replace(/\$interval/g, (target.interval || options.interval));
return query; 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"> <div class="tight-form">
<ul class="tight-form-list pull-right"> <ul class="tight-form-list pull-right">
<li ng-show="parserError" class="tight-form-item"> <li ng-show="parserError" class="tight-form-item">
@ -48,98 +48,47 @@
<li> <li>
<metric-segment segment="measurementSegment" get-options="getMeasurements()" on-change="measurementChanged()"></metric-segment> <metric-segment segment="measurementSegment" get-options="getMeasurements()" on-change="measurementChanged()"></metric-segment>
</li> </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> </ul>
<div class="clearfix"></div> <div class="clearfix"></div>
<div style="padding: 10px" ng-if="target.rawQuery"> <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> <textarea ng-model="target.query" rows="8" spellcheck="false" style="width: 100%; box-sizing: border-box;" ng-blur="get_data()"></textarea>
</div> </div>
</div> </div>
<div ng-hide="target.rawQuery"> <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"> <ul class="tight-form-list">
<li class="tight-form-item query-keyword tight-form-align" style="width: 75px;"> <li class="tight-form-item query-keyword tight-form-align" style="width: 75px;">
<span ng-show="$index === 0">SELECT</span> <span ng-show="$index === 0">SELECT</span>
</li> </li>
<li> <li ng-repeat="part in selectParts">
<metric-segment-model property="field.func" get-options="getFunctions()" on-change="get_data()" css-class="tight-form-item-xlarge"></metric-segment> <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>
<li> <li class="dropdown" dropdown-typeahead="selectMenu" dropdown-typeahead-on-select="addSelectPart(selectParts, $item, $subItem)">
<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> </li>
</ul> </ul>
<div class="clearfix"></div> <div class="clearfix"></div>
</div> </div>
<div class="tight-form" ng-repeat="groupBy in target.groupBy"> <div class="tight-form">
<ul class="tight-form-list"> <ul class="tight-form-list">
<li class="tight-form-item query-keyword tight-form-align" style="width: 75px;"> <li class="tight-form-item query-keyword tight-form-align" style="width: 75px;">
<span ng-show="$index === 0">GROUP BY</span> <span ng-show="$index === 0">GROUP BY</span>
</li> </li>
<li ng-if="groupBy.type === 'time'"> <li ng-repeat="part in queryModel.groupByParts">
<span class="tight-form-item">time</span> <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>
<metric-segment-model property="groupBy.interval" get-options="getGroupByTimeIntervals()" on-change="get_data()">
</metric-segment>
</li> </li>
<li class="dropdown" ng-if="groupBy.type === 'time'"> <li>
<a class="tight-form-item pointer" data-toggle="dropdown" bs-tooltip="'Insert missing values, important when stacking'" data-placement="right"> <metric-segment segment="groupBySegment" get-options="getGroupByOptions()" on-change="groupByAction(part, $index)"></metric-segment>
<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> </li>
</ul> </ul>
<div class="clearfix"></div> <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 (_) { function (_) {
'use strict'; 'use strict';
function InfluxQueryBuilder(target) { function InfluxQueryBuilder(target, queryModel) {
this.target = target; this.target = target;
this.model = queryModel;
if (target.groupByTags) { if (target.groupByTags) {
target.groupBy = [{type: 'time', interval: 'auto'}]; target.groupBy = [{type: 'time', interval: 'auto'}];
@ -92,78 +93,5 @@ function (_) {
return query; 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; return InfluxQueryBuilder;
}); });

View File

@ -2,32 +2,33 @@ define([
'angular', 'angular',
'lodash', 'lodash',
'./query_builder', './query_builder',
'./influx_query',
'./query_part',
'./query_part_editor',
], ],
function (angular, _, InfluxQueryBuilder) { function (angular, _, InfluxQueryBuilder, InfluxQuery, queryPart) {
'use strict'; 'use strict';
var module = angular.module('grafana.controllers'); 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() { $scope.init = function() {
if (!$scope.target) { return; } if (!$scope.target) { return; }
var target = $scope.target; $scope.target = $scope.target;
target.tags = target.tags || []; $scope.queryModel = new InfluxQuery($scope.target);
target.groupBy = target.groupBy || [{type: 'time', interval: 'auto'}]; $scope.queryBuilder = new InfluxQueryBuilder($scope.target);
target.fields = target.fields || [{name: 'value', func: target.function || 'mean'}]; $scope.groupBySegment = uiSegmentSrv.newPlusButton();
$scope.queryBuilder = new InfluxQueryBuilder(target); if (!$scope.target.measurement) {
if (!target.measurement) {
$scope.measurementSegment = uiSegmentSrv.newSelectMeasurement(); $scope.measurementSegment = uiSegmentSrv.newSelectMeasurement();
} else { } else {
$scope.measurementSegment = uiSegmentSrv.newSegment(target.measurement); $scope.measurementSegment = uiSegmentSrv.newSegment($scope.target.measurement);
} }
$scope.tagSegments = []; $scope.tagSegments = [];
_.each(target.tags, function(tag) { _.each($scope.target.tags, function(tag) {
if (!tag.operator) { if (!tag.operator) {
if (/^\/.*\/$/.test(tag.value)) { if (/^\/.*\/$/.test(tag.value)) {
tag.operator = "=~"; tag.operator = "=~";
@ -46,9 +47,69 @@ function (angular, _, InfluxQueryBuilder) {
}); });
$scope.fixTagSegments(); $scope.fixTagSegments();
$scope.buildSelectMenu();
$scope.removeTagFilterSegment = uiSegmentSrv.newSegment({fake: true, value: '-- remove tag filter --'}); $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() { $scope.fixTagSegments = function() {
var count = $scope.tagSegments.length; var count = $scope.tagSegments.length;
var lastSegment = $scope.tagSegments[Math.max(count-1, 0)]; 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.measurementChanged = function() {
$scope.target.measurement = $scope.measurementSegment.value; $scope.target.measurement = $scope.measurementSegment.value;
$scope.$parent.get_data(); $scope.get_data();
};
$scope.getFields = function() {
var fieldsQuery = $scope.queryBuilder.buildExploreQuery('FIELDS');
return $scope.datasource.metricFindQuery(fieldsQuery)
.then($scope.transformToSegments(false), $scope.handleQueryError);
}; };
$scope.toggleQueryMode = function () { $scope.toggleQueryMode = function () {
@ -102,20 +134,17 @@ function (angular, _, InfluxQueryBuilder) {
.then($scope.transformToSegments(true), $scope.handleQueryError); .then($scope.transformToSegments(true), $scope.handleQueryError);
}; };
$scope.getFunctions = function () { $scope.getPartOptions = function(part) {
var functionList = ['count', 'mean', 'sum', 'min', 'max', 'mode', 'distinct', 'median', if (part.def.type === 'field') {
'stddev', 'first', 'last' var fieldsQuery = $scope.queryBuilder.buildExploreQuery('FIELDS');
]; return $scope.datasource.metricFindQuery(fieldsQuery)
return $q.when(_.map(functionList, function(func) { .then($scope.transformToSegments(true), $scope.handleQueryError);
return uiSegmentSrv.newSegment(func); }
})); if (part.def.type === 'tag') {
}; var tagsQuery = $scope.queryBuilder.buildExploreQuery('TAG_KEYS');
return $scope.datasource.metricFindQuery(tagsQuery)
$scope.getGroupByTimeIntervals = function () { .then($scope.transformToSegments(true), $scope.handleQueryError);
var times = ['auto', '1s', '10s', '1m', '2m', '5m', '10m', '30m', '1h', '1d']; }
return $q.when(_.map(times, function(func) {
return uiSegmentSrv.newSegment(func);
}));
}; };
$scope.handleQueryError = function(err) { $scope.handleQueryError = function(err) {
@ -179,25 +208,8 @@ function (angular, _, InfluxQueryBuilder) {
.then(null, $scope.handleQueryError); .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() { $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.setFill = function(fill) {
$scope.target.fill = 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('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() { describe('when building explore queries', function() {
it('should only have measurement condition in tag keys query given query with measurement', 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() { it('should handle regex measurement in tag keys query', function() {
var builder = new InfluxQueryBuilder({ var builder = new InfluxQueryBuilder({
measurement: '/.*/', measurement: '/.*/', tags: []
tags: []
}); });
var query = builder.buildExploreQuery('TAG_KEYS'); var query = builder.buildExploreQuery('TAG_KEYS');
expect(query).to.be('SHOW TAG KEYS FROM /.*/'); 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() { 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'); var query = builder.buildExploreQuery('TAG_VALUES', 'app');
expect(query).to.be('SHOW TAG VALUES FROM "cpu" WITH KEY = "app" WHERE "host" =~ /server.*/'); 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'; var url = '/api/v1/label/__name__/values';
return this._request('GET', url).then(function(result) { return this._request('GET', url).then(function(result) {
var suggestData = _.filter(result.data.data, function(metricName) { return _.filter(result.data.data, function (metricName) {
return metricName.indexOf(query) !== 1; return metricName.indexOf(query) !== 1;
}); });
return suggestData;
}); });
}; };

View File

@ -6,7 +6,7 @@
<li class="tight-form-item" style="width: 80px"> <li class="tight-form-item" style="width: 80px">
Left Y Left Y
</li> </li>
<li class="tight-form-item"> <li class="tight-form-item" style="width: 40px">
Unit Unit
</li> </li>
<li class="dropdown" style="width: 140px;" <li class="dropdown" style="width: 140px;"
@ -14,22 +14,6 @@
dropdown-typeahead="unitFormats" dropdown-typeahead="unitFormats"
dropdown-typeahead-on-select="setUnitFormat(0, $subItem)"> dropdown-typeahead-on-select="setUnitFormat(0, $subItem)">
</li> </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"> <li class="tight-form-item">
Scale type Scale type
</li> </li>
@ -46,12 +30,36 @@
</ul> </ul>
<div class="clearfix"></div> <div class="clearfix"></div>
</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"> <ul class="tight-form-list">
<li class="tight-form-item" style="width: 80px"> <li class="tight-form-item" style="width: 80px">
Right Y Right Y
</li> </li>
<li class="tight-form-item"> <li class="tight-form-item" style="width: 40px">
Unit Unit
</li> </li>
<li class="dropdown" style="width: 140px" <li class="dropdown" style="width: 140px"
@ -59,22 +67,6 @@
dropdown-typeahead="unitFormats" dropdown-typeahead="unitFormats"
dropdown-typeahead-on-select="setUnitFormat(1, $subItem)"> dropdown-typeahead-on-select="setUnitFormat(1, $subItem)">
</li> </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"> <li class="tight-form-item">
Scale type Scale type
</li> </li>
@ -91,6 +83,31 @@
</ul> </ul>
<div class="clearfix"></div> <div class="clearfix"></div>
</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>
<div class="section" style="margin-bottom: 20px"> <div class="section" style="margin-bottom: 20px">
@ -150,9 +167,9 @@
<div class="editor-row"> <div class="editor-row">
<div class="section"> <div class="section">
<div class="tight-form last"> <div class="tight-form">
<ul class="tight-form-list"> <ul class="tight-form-list">
<li class="tight-form-item" style="width: 110px"> <li class="tight-form-item" style="width: 80px">
Legend Legend
</li> </li>
<li class="tight-form-item"> <li class="tight-form-item">
@ -164,18 +181,28 @@
<li class="tight-form-item"> <li class="tight-form-item">
<editor-checkbox text="Right side" model="panel.legend.rightSide" change="render()"></editor-checkbox> <editor-checkbox text="Right side" model="panel.legend.rightSide" change="render()"></editor-checkbox>
</li> </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"> <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> </li>
</ul> </ul>
<div class="clearfix"></div> <div class="clearfix"></div>
</div> </div>
</div>
<div class="section"> <div class="tight-form last">
<div class="tight-form">
<ul class="tight-form-list"> <ul class="tight-form-list">
<li class="tight-form-item" style="width: 105px"> <li class="tight-form-item" style="width: 80px">
Legend values Values
</li> </li>
<li class="tight-form-item"> <li class="tight-form-item">
<editor-checkbox text="Min" model="panel.legend.min" change="legendValuesOptionChanged()"></editor-checkbox> <editor-checkbox text="Min" model="panel.legend.min" change="legendValuesOptionChanged()"></editor-checkbox>
@ -189,16 +216,11 @@
<li class="tight-form-item"> <li class="tight-form-item">
<editor-checkbox text="Current" model="panel.legend.current" change="legendValuesOptionChanged()"></editor-checkbox> <editor-checkbox text="Current" model="panel.legend.current" change="legendValuesOptionChanged()"></editor-checkbox>
</li> </li>
<li class="tight-form-item last"> <li class="tight-form-item">
<editor-checkbox text="Total" model="panel.legend.total" change="legendValuesOptionChanged()"></editor-checkbox> <editor-checkbox text="Total" model="panel.legend.total" change="legendValuesOptionChanged()"></editor-checkbox>
</li> </li>
</ul> <li class="tight-form-item">
<div class="clearfix"></div> Decimals
</div>
<div class="tight-form last">
<ul class="tight-form-list">
<li class="tight-form-item" style="width: 105px">
<strong>Decimals</strong>
</li> </li>
<li style="width: 105px"> <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" <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> </ul>
<div class="clearfix"></div> <div class="clearfix"></div>
</div> </div>
</div> </div>
</div> </div>

View File

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

View File

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

View File

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

View File

@ -16,7 +16,7 @@
</ul> </ul>
<div class="clearfix"></div> <div class="clearfix"></div>
</div> </div>
<div class="tight-form" ng-if="showColumnOptions"> <div class="tight-form">
<ul class="tight-form-list"> <ul class="tight-form-list">
<li class="tight-form-item" style="width: 140px"> <li class="tight-form-item" style="width: 140px">
Columns Columns
@ -27,7 +27,8 @@
{{column.text}} {{column.text}}
</span> </span>
</li> </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> </li>
</ul> </ul>
<div class="clearfix"></div> <div class="clearfix"></div>
@ -158,7 +159,7 @@
</div> </div>
<button class="btn btn-inverse" style="margin-top: 20px" ng-click="addColumnStyle()"> <button class="btn btn-inverse" style="margin-top: 20px" ng-click="addColumnStyle()">
Add style display rule Add column style rule
</button> </button>
</div> </div>

View File

@ -8,93 +8,101 @@ import moment = require('moment');
import {transformers} from './transformers'; 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'; 'use strict';
return { return {
restrict: 'E', restrict: 'E',
scope: true, scope: true,
templateUrl: 'app/plugins/panels/table/editor.html', templateUrl: 'app/panels/table/editor.html',
link: function(scope, elem) { controller: TablePanelEditorCtrl,
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);
}
}; };
} }

View File

@ -19,6 +19,7 @@ export function tablePanel() {
link: function(scope, elem) { link: function(scope, elem) {
var data; var data;
var panel = scope.panel; var panel = scope.panel;
var pageCount = 0;
var formaters = []; var formaters = [];
function getTableHeight() { function getTableHeight() {
@ -26,8 +27,11 @@ export function tablePanel() {
if (_.isString(panelHeight)) { if (_.isString(panelHeight)) {
panelHeight = parseInt(panelHeight.replace('px', ''), 10); panelHeight = parseInt(panelHeight.replace('px', ''), 10);
} }
if (pageCount > 1) {
panelHeight -= 28;
}
return (panelHeight - 40) + 'px'; return (panelHeight - 60) + 'px';
} }
function appendTableRows(tbodyElem) { function appendTableRows(tbodyElem) {
@ -46,7 +50,7 @@ export function tablePanel() {
footerElem.empty(); footerElem.empty();
var pageSize = panel.pageSize || 100; var pageSize = panel.pageSize || 100;
var pageCount = Math.ceil(data.rows.length / pageSize); pageCount = Math.ceil(data.rows.length / pageSize);
if (pageCount === 1) { if (pageCount === 1) {
return; return;
} }
@ -73,12 +77,10 @@ export function tablePanel() {
appendTableRows(tbodyElem); appendTableRows(tbodyElem);
rootElem.css({
'max-height': panel.scroll ? getTableHeight() : ''
});
container.css({'font-size': panel.fontSize}); container.css({'font-size': panel.fontSize});
appendPaginationControls(footerElem); appendPaginationControls(footerElem);
rootElem.css({'max-height': panel.scroll ? getTableHeight() : '' });
} }
elem.on('click', '.table-panel-page-link', switchPage); 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 {describe, beforeEach, it, sinon, expect} from 'test/lib/common';
import {TableModel} from '../table_model'; import {TableModel} from '../table_model';
import {transformers} from '../transformers';
describe('when transforming time series table', () => { describe('when transforming time series table', () => {
var table; var table;
@ -100,7 +101,11 @@ describe('when transforming time series table', () => {
describe('JSON Data', () => { describe('JSON Data', () => {
var panel = { var panel = {
transform: 'json', 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 = [ var rawData = [
{ {
@ -108,26 +113,42 @@ describe('when transforming time series table', () => {
datapoints: [ datapoints: [
{ {
timestamp: 'time', timestamp: 'time',
message: 'message' message: 'message',
nested: {
level2: 'level2-value'
}
} }
] ]
} }
]; ];
beforeEach(() => { describe('getColumns', function() {
table = TableModel.transform(rawData, panel); 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', () => { describe('transform', function() {
expect(table.columns.length).to.be(2); beforeEach(() => {
expect(table.columns[0].text).to.be('Timestamp'); table = TableModel.transform(rawData, panel);
expect(table.columns[1].text).to.be('Message'); });
});
it ('should return 2 rows', () => { it ('should return 2 columns', () => {
expect(table.rows.length).to.be(1); expect(table.columns.length).to.be(3);
expect(table.rows[0][0]).to.be('time'); expect(table.columns[0].text).to.be('Timestamp');
expect(table.rows[0][1]).to.be('message'); 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 moment = require('moment');
import _ = require('lodash'); import _ = require('lodash');
import flatten = require('app/core/utils/flatten');
import TimeSeries = require('app/core/time_series'); import TimeSeries = require('app/core/time_series');
var transformers = {}; var transformers = {};
@ -149,9 +150,12 @@ transformers['json'] = {
continue; 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]; var doc = series.datapoints[y];
for (var propName in doc) { var flattened = flatten(doc, null);
for (var propName in flattened) {
names[propName] = true; names[propName] = true;
} }
} }
@ -177,13 +181,16 @@ transformers['json'] = {
for (y = 0; y < series.datapoints.length; y++) { for (y = 0; y < series.datapoints.length; y++) {
var dp = series.datapoints[y]; var dp = series.datapoints[y];
var values = []; 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)); values.push(JSON.stringify(dp));
} }
model.rows.push(values); model.rows.push(values);
} }
} }

View File

@ -241,7 +241,7 @@
{ {
"type": "query", "type": "query",
"datasource": null, "datasource": null,
"refresh_on_load": false, "refresh": false,
"name": "metric", "name": "metric",
"options": [], "options": [],
"includeAll": true, "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('wps', 789000000, 1000000, -1, '789M wps');
describeValueFormat('iops', 11000000000, 1000000000, -1, '11B iops'); 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() { describe('kbn.toFixed and negative decimals', function() {
it('should treat as zero decimals', function() { it('should treat as zero decimals', function() {
var str = kbn.toFixed(186.123, -2); var str = kbn.toFixed(186.123, -2);

View File

@ -204,7 +204,7 @@ define([
}); });
it('dashboard schema version should be set to latest', function() { 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); 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() { 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; _dashboard.refresh = false;
ctx.service.setTime({from: '2011-01-01', to: '2015-01-01' }); ctx.service.setTime({from: '2011-01-01', to: '2015-01-01' });
expect(_dashboard.refresh).to.be(false); 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() { it('should restore refresh after relative time range is set', function() {
_dashboard.refresh = '10s'; _dashboard.refresh = '10s';
ctx.service.setTime({from: moment([2011,1,1]), to: moment([2015,1,1])}); 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 // Turn double returns into triple returns, so that we can make a
// paragraph for the last item in a list, if necessary: // 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); var result = _ProcessListItems(list);
// Trim any trailing whitespace, to put the closing `</$list_type>` // 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"; var list_type = (m3.search(/[*+-]/g)>-1) ? "ul" : "ol";
// Turn double returns into triple returns, so that we can make a // Turn double returns into triple returns, so that we can make a
// paragraph for the last item in a list, if necessary: // 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); var result = _ProcessListItems(list);
result = runup + "<"+list_type+">\n" + result + "</"+list_type+">\n"; result = runup + "<"+list_type+">\n" + result + "</"+list_type+">\n";
return result; return result;
@ -1451,4 +1451,4 @@ if (typeof define === 'function' && define.amd) {
define(function() { define(function() {
return Showdown; return Showdown;
}); });
} }

View File

@ -9,19 +9,19 @@
<title>Grafana</title> <title>Grafana</title>
[[if .User.LightTheme]] [[if .User.LightTheme]]
<link rel="stylesheet" href="[[.AppSubUrl]]/css/grafana.light.min.css"> <link rel="stylesheet" href="[[.AppSubUrl]]/public/css/grafana.light.min.css">
[[else]] [[else]]
<link rel="stylesheet" href="[[.AppSubUrl]]/css/grafana.dark.min.css"> <link rel="stylesheet" href="[[.AppSubUrl]]/public/css/grafana.dark.min.css">
[[end]] [[end]]
[[ range $css := .PluginCss ]] [[ range $css := .PluginCss ]]
<link rel="stylesheet" href="[[$.AppSubUrl]]/[[ $css ]]"> <link rel="stylesheet" href="[[$.AppSubUrl]]/[[ $css ]]">
[[ end ]] [[ end ]]
<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]]/" /> <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/vendor/requirejs/require.js"></script>
<script src="[[.AppSubUrl]]/public/app/require_config.js"></script> <script src="[[.AppSubUrl]]/public/app/require_config.js"></script>
<!-- endbuild --> <!-- endbuild -->

View File

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

View File

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