mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Merge remote-tracking branch 'upstream/external-plugins' into externalPlugin
Conflicts: public/views/index.html
This commit is contained in:
commit
fd392a2422
@ -6,6 +6,8 @@
|
||||
### Enhancements
|
||||
* **CloudWatch**: Support for multiple AWS Credentials, closes [#3053](https://github.com/grafana/grafana/issues/3053), [#3080](https://github.com/grafana/grafana/issues/3080)
|
||||
* **Elasticsearch**: Support for dynamic daily indices for annotations, closes [#3061](https://github.com/grafana/grafana/issues/3061)
|
||||
* **Graph Panel**: Option to hide series with all zeroes from legend and tooltip, closes [#1381](https://github.com/grafana/grafana/issues/1381), [#3336](https://github.com/grafana/grafana/issues/3336)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
* **cloudwatch**: fix for handling of period for long time ranges, fixes [#3086](https://github.com/grafana/grafana/issues/3086)
|
||||
|
@ -75,7 +75,7 @@ the latest master builds [here](http://grafana.org/download/builds)
|
||||
|
||||
### Dependencies
|
||||
|
||||
- Go 1.4
|
||||
- Go 1.5
|
||||
- NodeJS
|
||||
|
||||
### Get Code
|
||||
@ -85,11 +85,12 @@ go get github.com/grafana/grafana
|
||||
```
|
||||
|
||||
### Building the backend
|
||||
Replace X.Y.Z by actual version number.
|
||||
```
|
||||
cd $GOPATH/src/github.com/grafana/grafana
|
||||
go run build.go setup (only needed once to install godep)
|
||||
godep restore (will pull down all golang lib dependencies in your current GOPATH)
|
||||
go build .
|
||||
godep go run build.go build
|
||||
```
|
||||
|
||||
### Building frontend assets
|
||||
@ -112,7 +113,7 @@ bra run
|
||||
|
||||
### Running
|
||||
```
|
||||
./grafana
|
||||
./bin/grafana-server
|
||||
```
|
||||
|
||||
Open grafana in your browser (default http://localhost:3000) and login with admin user (default user/pass = admin/admin).
|
||||
@ -128,6 +129,7 @@ You only need to add the options you want to override. Config files are applied
|
||||
|
||||
## Create a pull request
|
||||
Before or after you create a pull request, sign the [contributor license agreement](http://grafana.org/docs/contributing/cla.html).
|
||||
|
||||
## Contribute
|
||||
If you have any idea for an improvement or found a bug do not hesitate to open an issue.
|
||||
And if you have time clone this repo and submit a pull request and help me make Grafana
|
||||
|
6
build.go
6
build.go
@ -328,9 +328,9 @@ func build(pkg string, tags []string) {
|
||||
func ldflags() string {
|
||||
var b bytes.Buffer
|
||||
b.WriteString("-w")
|
||||
b.WriteString(fmt.Sprintf(" -X main.version %s", version))
|
||||
b.WriteString(fmt.Sprintf(" -X main.commit %s", getGitSha()))
|
||||
b.WriteString(fmt.Sprintf(" -X main.buildstamp %d", buildStamp()))
|
||||
b.WriteString(fmt.Sprintf(" -X main.version=%s", version))
|
||||
b.WriteString(fmt.Sprintf(" -X main.commit=%s", getGitSha()))
|
||||
b.WriteString(fmt.Sprintf(" -X main.buildstamp=%d", buildStamp()))
|
||||
return b.String()
|
||||
}
|
||||
|
||||
|
@ -122,7 +122,7 @@ To configure Grafana add a configuration file named `custom.ini` to the
|
||||
`conf` folder and override any of the settings defined in
|
||||
`conf/defaults.ini`.
|
||||
|
||||
Start Grafana by executing `./grafana web`. The `grafana` binary needs
|
||||
Start Grafana by executing `./grafana-server web`. The `grafana-server` binary needs
|
||||
the working directory to be the root install directory (where the binary
|
||||
and the `public` folder is located).
|
||||
|
||||
|
@ -1,5 +1,5 @@
|
||||
----
|
||||
page_title: Dashboard JSON
|
||||
page_title: Dashboard JSON
|
||||
page_description: Dashboard JSON Reference
|
||||
page_keywords: grafana, dashboard, json, documentation
|
||||
---
|
||||
@ -363,7 +363,7 @@ Usage of the fields is explained below:
|
||||
],
|
||||
"query": "tag_values(cpu.utilization.average,env)",
|
||||
"refresh": false,
|
||||
"refresh_on_load": false,
|
||||
"refresh": false,
|
||||
"type": "query"
|
||||
},
|
||||
{
|
||||
@ -390,7 +390,7 @@ Usage of the fields is explained below:
|
||||
}
|
||||
],
|
||||
"query": "tag_values(cpu.utilization.average,app)",
|
||||
"refresh_on_load": false,
|
||||
"refresh": false,
|
||||
"regex": "",
|
||||
"type": "query"
|
||||
}
|
||||
@ -413,7 +413,7 @@ Usage of the above mentioned fields in the templating section is explained below
|
||||
| **name** | name of variable |
|
||||
| **options** | array of variable text/value pairs available for selection on dashboard |
|
||||
| **query** | datasource query used to fetch values for a variable |
|
||||
| **refresh_on_load** | TODO |
|
||||
| **refresh** | TODO |
|
||||
| **regex** | TODO |
|
||||
| **type** | type of variable, i.e. `custom`, `query` or `interval` |
|
||||
|
||||
|
@ -142,10 +142,10 @@ Will return the dashboard given the dashboard slug. Slug is the url friendly ver
|
||||
"rows": [
|
||||
{
|
||||
}
|
||||
]
|
||||
],
|
||||
"schemaVersion": 6,
|
||||
"version": 0
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
### Delete dashboard
|
||||
@ -787,7 +787,7 @@ Update Organisation, fields *Adress 1*, *Adress 2*, *City* are not implemented y
|
||||
"id": 2,
|
||||
"name": "User",
|
||||
"login": "user",
|
||||
"email": "user@mygraf.com"
|
||||
"email": "user@mygraf.com",
|
||||
"isAdmin": false
|
||||
}
|
||||
]
|
||||
@ -1046,7 +1046,7 @@ Deletes the starring of the given Dashboard for the actual user.
|
||||
"timezone":"browser",
|
||||
"title":"Home",
|
||||
"version":5
|
||||
}
|
||||
},
|
||||
"expires": 3600
|
||||
}
|
||||
|
||||
@ -1091,34 +1091,33 @@ Keys:
|
||||
"canStar":false,
|
||||
"slug":"",
|
||||
"expires":"2200-13-32T25:23:23+02:00",
|
||||
"created":"2200-13-32T28:24:23+02:00"},
|
||||
|
||||
{
|
||||
"dashboard": {
|
||||
"editable":false,
|
||||
"hideControls":true,
|
||||
"nav":[
|
||||
{
|
||||
"enable":false,
|
||||
"type":"timepicker"
|
||||
}
|
||||
],
|
||||
"rows": [
|
||||
"created":"2200-13-32T28:24:23+02:00"
|
||||
},
|
||||
"dashboard": {
|
||||
"editable":false,
|
||||
"hideControls":true,
|
||||
"nav":[
|
||||
{
|
||||
|
||||
"enable":false,
|
||||
"type":"timepicker"
|
||||
}
|
||||
],
|
||||
"style":"dark",
|
||||
"tags":[],
|
||||
"templating":{
|
||||
"list":[
|
||||
]
|
||||
},
|
||||
"time":{
|
||||
},
|
||||
"timezone":"browser",
|
||||
"title":"Home",
|
||||
"version":5
|
||||
],
|
||||
"rows": [
|
||||
{
|
||||
|
||||
}
|
||||
],
|
||||
"style":"dark",
|
||||
"tags":[],
|
||||
"templating":{
|
||||
"list":[
|
||||
]
|
||||
},
|
||||
"time":{
|
||||
},
|
||||
"timezone":"browser",
|
||||
"title":"Home",
|
||||
"version":5
|
||||
}
|
||||
}
|
||||
|
||||
@ -1181,11 +1180,10 @@ Keys:
|
||||
"pluginType":"datasource",
|
||||
"serviceName":"Grafana",
|
||||
"type":"grafanasearch"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
defaultDatasource: "Grafana"
|
||||
},
|
||||
"defaultDatasource": "Grafana"
|
||||
}
|
||||
|
||||
## Login
|
||||
|
10
main.go
10
main.go
@ -2,6 +2,7 @@ package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"os/signal"
|
||||
@ -27,6 +28,7 @@ import (
|
||||
var version = "master"
|
||||
var commit = "NA"
|
||||
var buildstamp string
|
||||
var build_date string
|
||||
|
||||
var configFile = flag.String("config", "", "path to config file")
|
||||
var homePath = flag.String("homepath", "", "path to grafana install/home path, defaults to working directory")
|
||||
@ -38,6 +40,14 @@ func init() {
|
||||
}
|
||||
|
||||
func main() {
|
||||
|
||||
v := flag.Bool("v", false, "prints current version and exits")
|
||||
flag.Parse()
|
||||
if *v {
|
||||
fmt.Printf("Version %s (commit: %s)\n", version, commit)
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
buildstampInt64, _ := strconv.ParseInt(buildstamp, 10, 64)
|
||||
|
||||
setting.BuildVersion = version
|
||||
|
@ -21,7 +21,7 @@
|
||||
"grunt-contrib-connect": "~0.5.0",
|
||||
"grunt-contrib-copy": "~0.5.0",
|
||||
"grunt-contrib-cssmin": "~0.6.1",
|
||||
"grunt-contrib-htmlmin": "~0.1.3",
|
||||
"grunt-contrib-htmlmin": "~0.6.0",
|
||||
"grunt-contrib-jshint": "~0.10.0",
|
||||
"grunt-contrib-less": "~0.7.0",
|
||||
"grunt-contrib-requirejs": "~0.4.4",
|
||||
|
@ -9,6 +9,7 @@ import (
|
||||
"github.com/aws/aws-sdk-go/aws"
|
||||
"github.com/aws/aws-sdk-go/aws/credentials"
|
||||
"github.com/aws/aws-sdk-go/aws/credentials/ec2rolecreds"
|
||||
"github.com/aws/aws-sdk-go/aws/ec2metadata"
|
||||
"github.com/aws/aws-sdk-go/aws/session"
|
||||
"github.com/aws/aws-sdk-go/service/cloudwatch"
|
||||
"github.com/aws/aws-sdk-go/service/ec2"
|
||||
@ -40,11 +41,12 @@ func init() {
|
||||
}
|
||||
|
||||
func handleGetMetricStatistics(req *cwRequest, c *middleware.Context) {
|
||||
sess := session.New()
|
||||
creds := credentials.NewChainCredentials(
|
||||
[]credentials.Provider{
|
||||
&credentials.EnvProvider{},
|
||||
&credentials.SharedCredentialsProvider{Filename: "", Profile: req.DataSource.Database},
|
||||
&ec2rolecreds.EC2RoleProvider{ExpiryWindow: 5 * time.Minute},
|
||||
&ec2rolecreds.EC2RoleProvider{Client: ec2metadata.New(sess), ExpiryWindow: 5 * time.Minute},
|
||||
})
|
||||
|
||||
cfg := &aws.Config{
|
||||
@ -87,11 +89,12 @@ func handleGetMetricStatistics(req *cwRequest, c *middleware.Context) {
|
||||
}
|
||||
|
||||
func handleListMetrics(req *cwRequest, c *middleware.Context) {
|
||||
sess := session.New()
|
||||
creds := credentials.NewChainCredentials(
|
||||
[]credentials.Provider{
|
||||
&credentials.EnvProvider{},
|
||||
&credentials.SharedCredentialsProvider{Filename: "", Profile: req.DataSource.Database},
|
||||
&ec2rolecreds.EC2RoleProvider{ExpiryWindow: 5 * time.Minute},
|
||||
&ec2rolecreds.EC2RoleProvider{Client: ec2metadata.New(sess), ExpiryWindow: 5 * time.Minute},
|
||||
})
|
||||
|
||||
cfg := &aws.Config{
|
||||
@ -126,8 +129,17 @@ func handleListMetrics(req *cwRequest, c *middleware.Context) {
|
||||
}
|
||||
|
||||
func handleDescribeInstances(req *cwRequest, c *middleware.Context) {
|
||||
sess := session.New()
|
||||
creds := credentials.NewChainCredentials(
|
||||
[]credentials.Provider{
|
||||
&credentials.EnvProvider{},
|
||||
&credentials.SharedCredentialsProvider{Filename: "", Profile: req.DataSource.Database},
|
||||
&ec2rolecreds.EC2RoleProvider{Client: ec2metadata.New(sess), ExpiryWindow: 5 * time.Minute},
|
||||
})
|
||||
|
||||
cfg := &aws.Config{
|
||||
Region: aws.String(req.Region),
|
||||
Region: aws.String(req.Region),
|
||||
Credentials: creds,
|
||||
}
|
||||
|
||||
svc := ec2.New(session.New(cfg), cfg)
|
||||
|
@ -57,6 +57,8 @@ func GetDashboard(c *middleware.Context) {
|
||||
CanStar: c.IsSignedIn,
|
||||
CanSave: c.OrgRole == m.ROLE_ADMIN || c.OrgRole == m.ROLE_EDITOR,
|
||||
CanEdit: canEditDashboard(c.OrgRole),
|
||||
Created: dash.Created,
|
||||
Updated: dash.Updated,
|
||||
},
|
||||
}
|
||||
|
||||
|
@ -40,6 +40,7 @@ type DashboardMeta struct {
|
||||
Slug string `json:"slug"`
|
||||
Expires time.Time `json:"expires"`
|
||||
Created time.Time `json:"created"`
|
||||
Updated time.Time `json:"updated"`
|
||||
}
|
||||
|
||||
type DashboardFullWithMeta struct {
|
||||
|
@ -20,13 +20,13 @@ function (_, $, coreModule) {
|
||||
getOptions: "&",
|
||||
onChange: "&",
|
||||
},
|
||||
|
||||
link: function($scope, elem) {
|
||||
var $input = $(inputTemplate);
|
||||
var $button = $(buttonTemplate);
|
||||
var segment = $scope.segment;
|
||||
var options = null;
|
||||
var cancelBlur = null;
|
||||
var linkMode = true;
|
||||
|
||||
$input.appendTo(elem);
|
||||
$button.appendTo(elem);
|
||||
@ -55,19 +55,21 @@ function (_, $, coreModule) {
|
||||
});
|
||||
};
|
||||
|
||||
$scope.switchToLink = function(now) {
|
||||
if (now === true || cancelBlur) {
|
||||
clearTimeout(cancelBlur);
|
||||
cancelBlur = null;
|
||||
$input.hide();
|
||||
$button.show();
|
||||
$scope.updateVariableValue($input.val());
|
||||
}
|
||||
else {
|
||||
// need to have long delay because the blur
|
||||
// happens long before the click event on the typeahead options
|
||||
cancelBlur = setTimeout($scope.switchToLink, 100);
|
||||
}
|
||||
$scope.switchToLink = function() {
|
||||
if (linkMode) { return; }
|
||||
|
||||
clearTimeout(cancelBlur);
|
||||
cancelBlur = null;
|
||||
linkMode = true;
|
||||
$input.hide();
|
||||
$button.show();
|
||||
$scope.updateVariableValue($input.val());
|
||||
};
|
||||
|
||||
$scope.inputBlur = function() {
|
||||
// happens long before the click event on the typeahead options
|
||||
// need to have long delay because the blur
|
||||
cancelBlur = setTimeout($scope.switchToLink, 100);
|
||||
};
|
||||
|
||||
$scope.source = function(query, callback) {
|
||||
@ -98,7 +100,7 @@ function (_, $, coreModule) {
|
||||
}
|
||||
|
||||
$input.val(value);
|
||||
$scope.switchToLink(true);
|
||||
$scope.switchToLink();
|
||||
|
||||
return value;
|
||||
};
|
||||
@ -139,6 +141,8 @@ function (_, $, coreModule) {
|
||||
$input.show();
|
||||
$input.focus();
|
||||
|
||||
linkMode = false;
|
||||
|
||||
var typeahead = $input.data('typeahead');
|
||||
if (typeahead) {
|
||||
$input.val('');
|
||||
@ -146,7 +150,7 @@ function (_, $, coreModule) {
|
||||
}
|
||||
});
|
||||
|
||||
$input.blur($scope.switchToLink);
|
||||
$input.blur($scope.inputBlur);
|
||||
|
||||
$compile(elem.contents())($scope);
|
||||
}
|
||||
|
@ -7,6 +7,7 @@ function (angular, _, coreModule) {
|
||||
'use strict';
|
||||
|
||||
coreModule.service('uiSegmentSrv', function($sce, templateSrv) {
|
||||
var self = this;
|
||||
|
||||
function MetricSegment(options) {
|
||||
if (options === '*' || options.value === '*') {
|
||||
@ -74,6 +75,24 @@ function (angular, _, coreModule) {
|
||||
});
|
||||
};
|
||||
|
||||
this.transformToSegments = function(addTemplateVars, variableTypeFilter) {
|
||||
return function(results) {
|
||||
var segments = _.map(results, function(segment) {
|
||||
return self.newSegment({ value: segment.text, expandable: segment.expandable });
|
||||
});
|
||||
|
||||
if (addTemplateVars) {
|
||||
_.each(templateSrv.variables, function(variable) {
|
||||
if (variableTypeFilter === void 0 || variableTypeFilter === variable.type) {
|
||||
segments.unshift(self.newSegment({ type: 'template', value: '$' + variable.name, expandable: true }));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return segments;
|
||||
};
|
||||
};
|
||||
|
||||
this.newSelectMetric = function() {
|
||||
return new MetricSegment({value: 'select metric', fake: true});
|
||||
};
|
||||
|
@ -15,7 +15,6 @@ function (_) {
|
||||
appSubUrl: ""
|
||||
};
|
||||
|
||||
var settings = _.extend({}, defaults, options);
|
||||
return settings;
|
||||
return _.extend({}, defaults, options);
|
||||
};
|
||||
});
|
||||
|
@ -12,7 +12,7 @@ define([], function() {
|
||||
if (def !== void 0 && !this.exists(key)) {
|
||||
return def;
|
||||
}
|
||||
return window.localStorage[key] === 'true' ? true : false;
|
||||
return window.localStorage[key] === 'true';
|
||||
},
|
||||
exists: function(key) {
|
||||
return window.localStorage[key] !== void 0;
|
||||
|
@ -28,6 +28,7 @@ class TimeSeries {
|
||||
stats: any;
|
||||
legend: boolean;
|
||||
allIsNull: boolean;
|
||||
allIsZero: boolean;
|
||||
decimals: number;
|
||||
scaledDecimals: number;
|
||||
|
||||
@ -96,6 +97,7 @@ class TimeSeries {
|
||||
this.stats.avg = null;
|
||||
this.stats.current = null;
|
||||
this.allIsNull = true;
|
||||
this.allIsZero = true;
|
||||
|
||||
var ignoreNulls = fillStyle === 'connected';
|
||||
var nullAsZero = fillStyle === 'null as zero';
|
||||
@ -130,6 +132,10 @@ class TimeSeries {
|
||||
}
|
||||
}
|
||||
|
||||
if (currentValue != 0) {
|
||||
this.allIsZero = false;
|
||||
}
|
||||
|
||||
result.push([currentTime, currentValue]);
|
||||
}
|
||||
|
||||
|
39
public/app/core/utils/flatten.ts
Normal file
39
public/app/core/utils/flatten.ts
Normal 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;
|
@ -341,6 +341,8 @@ function($, _) {
|
||||
// Currencies
|
||||
kbn.valueFormats.currencyUSD = kbn.formatBuilders.currency('$');
|
||||
kbn.valueFormats.currencyGBP = kbn.formatBuilders.currency('£');
|
||||
kbn.valueFormats.currencyEUR = kbn.formatBuilders.currency('€');
|
||||
kbn.valueFormats.currencyJPY = kbn.formatBuilders.currency('¥');
|
||||
|
||||
// Data
|
||||
kbn.valueFormats.bits = kbn.formatBuilders.binarySIPrefix('b');
|
||||
@ -430,7 +432,7 @@ function($, _) {
|
||||
kbn.valueFormats.s = function(size, decimals, scaledDecimals) {
|
||||
if (size === null) { return ""; }
|
||||
|
||||
if (Math.abs(size) < 600) {
|
||||
if (Math.abs(size) < 60) {
|
||||
return kbn.toFixed(size, decimals) + " s";
|
||||
}
|
||||
// Less than 1 hour, devide in minutes
|
||||
@ -487,6 +489,57 @@ function($, _) {
|
||||
}
|
||||
};
|
||||
|
||||
kbn.valueFormats.m = function(size, decimals, scaledDecimals) {
|
||||
if (size === null) { return ""; }
|
||||
|
||||
if (Math.abs(size) < 60) {
|
||||
return kbn.toFixed(size, decimals) + " min";
|
||||
}
|
||||
else if (Math.abs(size) < 1440) {
|
||||
return kbn.toFixedScaled(size / 60, decimals, scaledDecimals, 2, " hour");
|
||||
}
|
||||
else if (Math.abs(size) < 10080) {
|
||||
return kbn.toFixedScaled(size / 1440, decimals, scaledDecimals, 3, " day");
|
||||
}
|
||||
else if (Math.abs(size) < 604800) {
|
||||
return kbn.toFixedScaled(size / 10080, decimals, scaledDecimals, 4, " week");
|
||||
}
|
||||
else {
|
||||
return kbn.toFixedScaled(size / 5.25948e5, decimals, scaledDecimals, 5, " year");
|
||||
}
|
||||
};
|
||||
|
||||
kbn.valueFormats.h = function(size, decimals, scaledDecimals) {
|
||||
if (size === null) { return ""; }
|
||||
|
||||
if (Math.abs(size) < 24) {
|
||||
return kbn.toFixed(size, decimals) + " hour";
|
||||
}
|
||||
else if (Math.abs(size) < 168) {
|
||||
return kbn.toFixedScaled(size / 24, decimals, scaledDecimals, 2, " day");
|
||||
}
|
||||
else if (Math.abs(size) < 8760) {
|
||||
return kbn.toFixedScaled(size / 168, decimals, scaledDecimals, 3, " week");
|
||||
}
|
||||
else {
|
||||
return kbn.toFixedScaled(size / 8760, decimals, scaledDecimals, 4, " year");
|
||||
}
|
||||
};
|
||||
|
||||
kbn.valueFormats.d = function(size, decimals, scaledDecimals) {
|
||||
if (size === null) { return ""; }
|
||||
|
||||
if (Math.abs(size) < 7) {
|
||||
return kbn.toFixed(size, decimals) + " day";
|
||||
}
|
||||
else if (Math.abs(size) < 365) {
|
||||
return kbn.toFixedScaled(size / 7, decimals, scaledDecimals, 2, " week");
|
||||
}
|
||||
else {
|
||||
return kbn.toFixedScaled(size / 365, decimals, scaledDecimals, 3, " year");
|
||||
}
|
||||
};
|
||||
|
||||
///// FORMAT MENU /////
|
||||
|
||||
kbn.getUnitFormats = function() {
|
||||
@ -508,6 +561,8 @@ function($, _) {
|
||||
submenu: [
|
||||
{text: 'Dollars ($)', value: 'currencyUSD'},
|
||||
{text: 'Pounds (£)', value: 'currencyGBP'},
|
||||
{text: 'Euro (€)', value: 'currencyEUR'},
|
||||
{text: 'Yen (¥)', value: 'currencyJPY'},
|
||||
]
|
||||
},
|
||||
{
|
||||
@ -518,6 +573,9 @@ function($, _) {
|
||||
{text: 'microseconds (µs)', value: 'µs' },
|
||||
{text: 'milliseconds (ms)', value: 'ms' },
|
||||
{text: 'seconds (s)', value: 's' },
|
||||
{text: 'minutes (m)', value: 'm' },
|
||||
{text: 'hours (h)', value: 'h' },
|
||||
{text: 'days (d)', value: 'd' },
|
||||
]
|
||||
},
|
||||
{
|
||||
|
@ -17,6 +17,7 @@
|
||||
<th></th>
|
||||
</tr>
|
||||
<tr ng-repeat="org in orgs">
|
||||
<td>{{org.id}}</td>
|
||||
<td>{{org.name}}</td>
|
||||
<td style="width: 1%">
|
||||
<a href="admin/orgs/edit/{{org.id}}" class="btn btn-inverse btn-small">
|
||||
|
@ -26,7 +26,7 @@ function (angular, $, _, moment) {
|
||||
this.tags = data.tags || [];
|
||||
this.style = data.style || "dark";
|
||||
this.timezone = data.timezone || 'browser';
|
||||
this.editable = data.editable === false ? false : true;
|
||||
this.editable = data.editable !== false;
|
||||
this.hideControls = data.hideControls || false;
|
||||
this.sharedCrosshair = data.sharedCrosshair || false;
|
||||
this.rows = data.rows || [];
|
||||
@ -48,10 +48,10 @@ function (angular, $, _, moment) {
|
||||
p._initMeta = function(meta) {
|
||||
meta = meta || {};
|
||||
|
||||
meta.canShare = meta.canShare === false ? false : true;
|
||||
meta.canSave = meta.canSave === false ? false : true;
|
||||
meta.canStar = meta.canStar === false ? false : true;
|
||||
meta.canEdit = meta.canEdit === false ? false : true;
|
||||
meta.canShare = meta.canShare !== false;
|
||||
meta.canSave = meta.canSave !== false;
|
||||
meta.canStar = meta.canStar !== false;
|
||||
meta.canEdit = meta.canEdit !== false;
|
||||
|
||||
if (!this.editable) {
|
||||
meta.canEdit = false;
|
||||
@ -151,7 +151,6 @@ function (angular, $, _, moment) {
|
||||
result.panel = panel;
|
||||
result.row = row;
|
||||
result.index = index;
|
||||
return;
|
||||
}
|
||||
});
|
||||
});
|
||||
@ -230,9 +229,9 @@ function (angular, $, _, moment) {
|
||||
var i, j, k;
|
||||
var oldVersion = this.schemaVersion;
|
||||
var panelUpgrades = [];
|
||||
this.schemaVersion = 7;
|
||||
this.schemaVersion = 8;
|
||||
|
||||
if (oldVersion === 7) {
|
||||
if (oldVersion === 8) {
|
||||
return;
|
||||
}
|
||||
|
||||
@ -343,6 +342,49 @@ function (angular, $, _, moment) {
|
||||
});
|
||||
}
|
||||
|
||||
if (oldVersion < 8) {
|
||||
panelUpgrades.push(function(panel) {
|
||||
_.each(panel.targets, function(target) {
|
||||
// update old influxdb query schema
|
||||
if (target.fields && target.tags && target.groupBy) {
|
||||
if (target.rawQuery) {
|
||||
delete target.fields;
|
||||
delete target.fill;
|
||||
} else {
|
||||
target.select = _.map(target.fields, function(field) {
|
||||
var parts = [];
|
||||
parts.push({type: 'field', params: [field.name]});
|
||||
parts.push({type: field.func, params: []});
|
||||
if (field.mathExpr) {
|
||||
parts.push({type: 'math', params: [field.mathExpr]});
|
||||
}
|
||||
if (field.asExpr) {
|
||||
parts.push({type: 'alias', params: [field.asExpr]});
|
||||
}
|
||||
return parts;
|
||||
});
|
||||
delete target.fields;
|
||||
_.each(target.groupBy, function(part) {
|
||||
if (part.type === 'time' && part.interval) {
|
||||
part.params = [part.interval];
|
||||
delete part.interval;
|
||||
}
|
||||
if (part.type === 'tag' && part.key) {
|
||||
part.params = [part.key];
|
||||
delete part.key;
|
||||
}
|
||||
});
|
||||
|
||||
if (target.fill) {
|
||||
target.groupBy.push({type: 'fill', params: [target.fill]});
|
||||
delete target.fill;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
if (panelUpgrades.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
@ -25,7 +25,7 @@
|
||||
<td>
|
||||
<button class="btn btn-inverse pull-right" ng-click="import(dash.name)">
|
||||
Load
|
||||
</a>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
@ -90,11 +90,11 @@ define([
|
||||
timer.cancel(this.refresh_timer);
|
||||
};
|
||||
|
||||
this.setTime = function(time) {
|
||||
this.setTime = function(time, enableRefresh) {
|
||||
_.extend(this.time, time);
|
||||
|
||||
// disable refresh if we have an absolute time
|
||||
if (moment.isMoment(time.to)) {
|
||||
// disable refresh if zoom in or zoom out
|
||||
if (!enableRefresh && moment.isMoment(time.to)) {
|
||||
this.old_refresh = this.dashboard.refresh || this.old_refresh;
|
||||
this.setAutoRefresh(false);
|
||||
}
|
||||
|
@ -115,7 +115,7 @@ export class TimePickerCtrl {
|
||||
this.timeSrv.setAutoRefresh(this.refresh.value);
|
||||
}
|
||||
|
||||
this.timeSrv.setTime(this.timeRaw);
|
||||
this.timeSrv.setTime(this.timeRaw, true);
|
||||
this.$rootScope.appEvent('hide-dash-editor');
|
||||
}
|
||||
|
||||
|
@ -122,11 +122,7 @@ function(angular, _) {
|
||||
var currentJson = angular.toJson(current);
|
||||
var originalJson = angular.toJson(original);
|
||||
|
||||
if (currentJson !== originalJson) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
return currentJson !== originalJson;
|
||||
};
|
||||
|
||||
p.open_modal = function() {
|
||||
|
@ -52,7 +52,7 @@ function (angular, _) {
|
||||
if (link.asDropdown) {
|
||||
template += '<ul class="dropdown-menu" role="menu">' +
|
||||
'<li ng-repeat="dash in link.searchHits"><a href="{{dash.url}}"><i class="fa fa-th-large"></i> {{dash.title}}</a></li>' +
|
||||
'</ul';
|
||||
'</ul>';
|
||||
}
|
||||
|
||||
elem.html(template);
|
||||
|
@ -45,7 +45,7 @@
|
||||
{{invite.email}}
|
||||
<span ng-show="invite.name" style="padding-left: 20px"> {{invite.name}}</span>
|
||||
<span class="pull-right">
|
||||
<button class="btn btn-inverse btn-mini " data-clipboard-text="{{invite.url}}" clipboard-button ng-click="copyInviteToClipboard($event)"
|
||||
<button class="btn btn-inverse btn-mini " data-clipboard-text="{{invite.url}}" clipboard-button ng-click="copyInviteToClipboard($event)">
|
||||
<i class="fa fa-clipboard"></i> Copy Invite
|
||||
</button>
|
||||
|
||||
|
@ -64,8 +64,7 @@ function (angular, $, _) {
|
||||
}
|
||||
|
||||
function getExtendedMenu($scope) {
|
||||
var menu = angular.copy($scope.panelMeta.extendedMenu);
|
||||
return menu;
|
||||
return angular.copy($scope.panelMeta.extendedMenu);
|
||||
}
|
||||
|
||||
return {
|
||||
|
@ -12,7 +12,7 @@ function (angular, _) {
|
||||
var replacementDefaults = {
|
||||
type: 'query',
|
||||
datasource: null,
|
||||
refresh_on_load: false,
|
||||
refresh: false,
|
||||
name: '',
|
||||
options: [],
|
||||
includeAll: false,
|
||||
|
@ -45,17 +45,6 @@ function (angular, _, kbn) {
|
||||
};
|
||||
|
||||
this.setVariableFromUrl = function(variable, urlValue) {
|
||||
if (variable.refresh) {
|
||||
var self = this;
|
||||
//refresh the list of options before setting the value
|
||||
return this.updateOptions(variable).then(function() {
|
||||
var option = _.findWhere(variable.options, { text: urlValue });
|
||||
option = option || { text: urlValue, value: urlValue };
|
||||
|
||||
self.updateAutoInterval(variable);
|
||||
return self.setVariableValue(variable, option);
|
||||
});
|
||||
}
|
||||
var option = _.findWhere(variable.options, { text: urlValue });
|
||||
option = option || { text: urlValue, value: urlValue };
|
||||
|
||||
|
@ -73,7 +73,7 @@
|
||||
<ul class="tight-form-list" role="menu">
|
||||
<li class="tight-form-item query-keyword tight-form-align" style="width: 100px">
|
||||
Alias
|
||||
<tip>{{metric}} {{stat}} {{namespace}} {{region}} {{<dimension name>}}</tip>
|
||||
<tip>{{metric}} {{stat}} {{namespace}} {{region}} {{DIMENSION_NAME}}</tip>
|
||||
</li>
|
||||
<li>
|
||||
<input type="text" class="input-xlarge tight-form-input" ng-model="target.alias" spellcheck='false' ng-model-onblur ng-change="refreshMetricData()">
|
||||
|
@ -15,7 +15,6 @@ function (angular, _, queryDef) {
|
||||
$scope.bucketAggTypes = queryDef.bucketAggTypes;
|
||||
$scope.orderOptions = queryDef.orderOptions;
|
||||
$scope.sizeOptions = queryDef.sizeOptions;
|
||||
$scope.intervalOptions = queryDef.intervalOptions;
|
||||
|
||||
$rootScope.onAppEvent('elastic-query-updated', function() {
|
||||
$scope.validateModel();
|
||||
@ -128,6 +127,10 @@ function (angular, _, queryDef) {
|
||||
}
|
||||
};
|
||||
|
||||
$scope.getIntervalOptions = function() {
|
||||
return $q.when(uiSegmentSrv.transformToSegments(true, 'interval')(queryDef.intervalOptions));
|
||||
};
|
||||
|
||||
$scope.addBucketAgg = function() {
|
||||
// if last is date histogram add it before
|
||||
var lastBucket = bucketAggs[bucketAggs.length - 1];
|
||||
|
@ -41,7 +41,7 @@
|
||||
Interval
|
||||
</li>
|
||||
<li>
|
||||
<metric-segment-model property="agg.settings.interval" options="intervalOptions" on-change="onChangeInternal()" css-class="last" custom="true"></metric-segment-model>
|
||||
<metric-segment-model property="agg.settings.interval" get-options="getIntervalOptions()" on-change="onChangeInternal()" css-class="last" custom="true"></metric-segment-model>
|
||||
</li>
|
||||
</ul>
|
||||
<div class="clearfix"></div>
|
||||
|
@ -1,13 +1,12 @@
|
||||
define([
|
||||
'angular',
|
||||
'lodash',
|
||||
],
|
||||
function (angular, _) {
|
||||
function (angular) {
|
||||
'use strict';
|
||||
|
||||
var module = angular.module('grafana.controllers');
|
||||
|
||||
module.controller('ElasticQueryCtrl', function($scope, $timeout, uiSegmentSrv, templateSrv) {
|
||||
module.controller('ElasticQueryCtrl', function($scope, $timeout, uiSegmentSrv) {
|
||||
|
||||
$scope.init = function() {
|
||||
var target = $scope.target;
|
||||
@ -21,7 +20,7 @@ function (angular, _) {
|
||||
$scope.getFields = function(type) {
|
||||
var jsonStr = angular.toJson({find: 'fields', type: type});
|
||||
return $scope.datasource.metricFindQuery(jsonStr)
|
||||
.then($scope.transformToSegments(false))
|
||||
.then(uiSegmentSrv.transformToSegments(false))
|
||||
.then(null, $scope.handleQueryError);
|
||||
};
|
||||
|
||||
@ -35,21 +34,6 @@ function (angular, _) {
|
||||
$scope.appEvent('elastic-query-updated');
|
||||
};
|
||||
|
||||
$scope.transformToSegments = function(addTemplateVars) {
|
||||
return function(results) {
|
||||
var segments = _.map(results, function(segment) {
|
||||
return uiSegmentSrv.newSegment({ value: segment.text, expandable: segment.expandable });
|
||||
});
|
||||
|
||||
if (addTemplateVars) {
|
||||
_.each(templateSrv.variables, function(variable) {
|
||||
segments.unshift(uiSegmentSrv.newSegment({ type: 'template', value: '$' + variable.name, expandable: true }));
|
||||
});
|
||||
}
|
||||
return segments;
|
||||
};
|
||||
};
|
||||
|
||||
$scope.handleQueryError = function(err) {
|
||||
$scope.parserError = err.message || 'Failed to issue metric query';
|
||||
return [];
|
||||
|
@ -3,11 +3,11 @@ define([
|
||||
'lodash',
|
||||
'app/core/utils/datemath',
|
||||
'./influx_series',
|
||||
'./query_builder',
|
||||
'./influx_query',
|
||||
'./directives',
|
||||
'./query_ctrl',
|
||||
],
|
||||
function (angular, _, dateMath, InfluxSeries, InfluxQueryBuilder) {
|
||||
function (angular, _, dateMath, InfluxSeries, InfluxQuery) {
|
||||
'use strict';
|
||||
|
||||
var module = angular.module('grafana.services');
|
||||
@ -41,8 +41,8 @@ function (angular, _, dateMath, InfluxSeries, InfluxQueryBuilder) {
|
||||
queryTargets.push(target);
|
||||
|
||||
// build query
|
||||
var queryBuilder = new InfluxQueryBuilder(target);
|
||||
var query = queryBuilder.build();
|
||||
var queryModel = new InfluxQuery(target);
|
||||
var query = queryModel.render();
|
||||
query = query.replace(/\$interval/g, (target.interval || options.interval));
|
||||
return query;
|
||||
|
||||
|
214
public/app/plugins/datasource/influxdb/influx_query.ts
Normal file
214
public/app/plugins/datasource/influxdb/influx_query.ts
Normal 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;
|
@ -1,4 +1,4 @@
|
||||
<div class="tight-form-container-no-item-borders">
|
||||
<div class="">
|
||||
<div class="tight-form">
|
||||
<ul class="tight-form-list pull-right">
|
||||
<li ng-show="parserError" class="tight-form-item">
|
||||
@ -48,98 +48,47 @@
|
||||
<li>
|
||||
<metric-segment segment="measurementSegment" get-options="getMeasurements()" on-change="measurementChanged()"></metric-segment>
|
||||
</li>
|
||||
<li class="tight-form-item query-keyword" style="padding-left: 15px; padding-right: 15px;">
|
||||
WHERE
|
||||
</li>
|
||||
<li ng-repeat="segment in tagSegments">
|
||||
<metric-segment segment="segment" get-options="getTagsOrValues(segment, $index)" on-change="tagSegmentUpdated(segment, $index)"></metric-segment>
|
||||
</li>
|
||||
</ul>
|
||||
<div class="clearfix"></div>
|
||||
|
||||
<div style="padding: 10px" ng-if="target.rawQuery">
|
||||
<textarea ng-model="target.query" rows="8" spellcheck="false" style="width: 100%; box-sizing: border-box;" ng-blur="get_data()"></textarea>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div ng-hide="target.rawQuery">
|
||||
<div class="tight-form">
|
||||
<ul class="tight-form-list">
|
||||
<li class="tight-form-item query-keyword tight-form-align" style="width: 75px;">
|
||||
WHERE
|
||||
</li>
|
||||
<li ng-repeat="segment in tagSegments">
|
||||
<metric-segment segment="segment" get-options="getTagsOrValues(segment, $index)" on-change="tagSegmentUpdated(segment, $index)"></metric-segment>
|
||||
</li>
|
||||
</ul>
|
||||
<div class="clearfix"></div>
|
||||
</div>
|
||||
|
||||
<div class="tight-form" ng-repeat="field in target.fields">
|
||||
<div class="tight-form" ng-repeat="selectParts in queryModel.selectModels">
|
||||
<ul class="tight-form-list">
|
||||
<li class="tight-form-item query-keyword tight-form-align" style="width: 75px;">
|
||||
<span ng-show="$index === 0">SELECT</span>
|
||||
</li>
|
||||
<li>
|
||||
<metric-segment-model property="field.func" get-options="getFunctions()" on-change="get_data()" css-class="tight-form-item-xlarge"></metric-segment>
|
||||
<li ng-repeat="part in selectParts">
|
||||
<influx-query-part-editor part="part" class="tight-form-item tight-form-func" remove-action="removeSelectPart(selectParts, part)" part-updated="selectPartUpdated(selectParts, part)" get-options="getPartOptions(part)"></influx-query-part-editor>
|
||||
</li>
|
||||
<li>
|
||||
<metric-segment-model property="field.name" get-options="getFields()" on-change="get_data()" css-class="tight-form-item-large"></metric-segment>
|
||||
</li>
|
||||
<li>
|
||||
<input type="text" class="tight-form-clear-input text-center" style="width: 70px;" ng-model="field.mathExpr" spellcheck='false' placeholder="math expr" ng-blur="get_data()">
|
||||
</li>
|
||||
<li class="tight-form-item query-keyword">
|
||||
AS
|
||||
</li>
|
||||
<li>
|
||||
<input type="text" class="tight-form-clear-input" style="width: 180px;" ng-model="field.asExpr" spellcheck='false' placeholder="as expr" ng-blur="get_data()">
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<ul class="tight-form-list pull-right">
|
||||
<li class="tight-form-item last" ng-show="$index === 0">
|
||||
<a class="pointer" ng-click="addSelect()"><i class="fa fa-plus"></i></a>
|
||||
</li>
|
||||
<li class="tight-form-item last" ng-show="target.fields.length > 1">
|
||||
<a class="pointer" ng-click="removeSelect($index)"><i class="fa fa-minus"></i></a>
|
||||
<li class="dropdown" dropdown-typeahead="selectMenu" dropdown-typeahead-on-select="addSelectPart(selectParts, $item, $subItem)">
|
||||
</li>
|
||||
</ul>
|
||||
<div class="clearfix"></div>
|
||||
</div>
|
||||
|
||||
<div class="tight-form" ng-repeat="groupBy in target.groupBy">
|
||||
<div class="tight-form">
|
||||
<ul class="tight-form-list">
|
||||
<li class="tight-form-item query-keyword tight-form-align" style="width: 75px;">
|
||||
<span ng-show="$index === 0">GROUP BY</span>
|
||||
</li>
|
||||
<li ng-if="groupBy.type === 'time'">
|
||||
<span class="tight-form-item">time</span>
|
||||
<metric-segment-model property="groupBy.interval" get-options="getGroupByTimeIntervals()" on-change="get_data()">
|
||||
</metric-segment>
|
||||
<li ng-repeat="part in queryModel.groupByParts">
|
||||
<influx-query-part-editor part="part" class="tight-form-item tight-form-func" remove-action="removeGroupByPart(part, $index)" part-updated="get_data();" get-options="getPartOptions(part)"></influx-query-part-editor>
|
||||
</li>
|
||||
<li class="dropdown" ng-if="groupBy.type === 'time'">
|
||||
<a class="tight-form-item pointer" data-toggle="dropdown" bs-tooltip="'Insert missing values, important when stacking'" data-placement="right">
|
||||
<span ng-show="target.fill">
|
||||
fill ({{target.fill}})
|
||||
</span>
|
||||
<span ng-show="!target.fill">
|
||||
no fill
|
||||
</span>
|
||||
</a>
|
||||
<ul class="dropdown-menu">
|
||||
<li><a ng-click="setFill('')">no fill</a></li>
|
||||
<li><a ng-click="setFill('0')">fill (0)</a></li>
|
||||
<li><a ng-click="setFill('null')">fill (null)</a></li>
|
||||
<li><a ng-click="setFill('none')">fill (none)</a></li>
|
||||
<li><a ng-click="setFill('previous')">fill (previous)</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li ng-if="groupBy.type === 'tag'">
|
||||
<metric-segment-model property="groupBy.key" get-options="getTagOptions()" on-change="get_data()"></metric-segment>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<ul class="tight-form-list pull-right">
|
||||
<li class="tight-form-item last" ng-show="$index === 0">
|
||||
<a class="pointer" ng-click="addGroupBy()"><i class="fa fa-plus"></i></a>
|
||||
</li>
|
||||
<li class="tight-form-item last" ng-show="$index > 0">
|
||||
<a class="pointer" ng-click="removeGroupBy($index)"><i class="fa fa-minus"></i></a>
|
||||
<li>
|
||||
<metric-segment segment="groupBySegment" get-options="getGroupByOptions()" on-change="groupByAction(part, $index)"></metric-segment>
|
||||
</li>
|
||||
</ul>
|
||||
<div class="clearfix"></div>
|
||||
|
@ -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>
|
@ -4,8 +4,9 @@ define([
|
||||
function (_) {
|
||||
'use strict';
|
||||
|
||||
function InfluxQueryBuilder(target) {
|
||||
function InfluxQueryBuilder(target, queryModel) {
|
||||
this.target = target;
|
||||
this.model = queryModel;
|
||||
|
||||
if (target.groupByTags) {
|
||||
target.groupBy = [{type: 'time', interval: 'auto'}];
|
||||
@ -92,78 +93,5 @@ function (_) {
|
||||
return query;
|
||||
};
|
||||
|
||||
p._getGroupByTimeInterval = function(interval) {
|
||||
if (interval === 'auto') {
|
||||
return '$interval';
|
||||
}
|
||||
return interval;
|
||||
};
|
||||
|
||||
p._buildQuery = function() {
|
||||
var target = this.target;
|
||||
|
||||
if (!target.measurement) {
|
||||
throw "Metric measurement is missing";
|
||||
}
|
||||
|
||||
if (!target.fields) {
|
||||
target.fields = [{name: 'value', func: target.function || 'mean'}];
|
||||
}
|
||||
|
||||
var query = 'SELECT ';
|
||||
var i;
|
||||
for (i = 0; i < target.fields.length; i++) {
|
||||
var field = target.fields[i];
|
||||
if (i > 0) {
|
||||
query += ', ';
|
||||
}
|
||||
query += field.func + '("' + field.name + '")';
|
||||
if (field.mathExpr) {
|
||||
query += field.mathExpr;
|
||||
}
|
||||
if (field.asExpr) {
|
||||
query += ' AS "' + field.asExpr + '"';
|
||||
} else {
|
||||
query += ' AS "' + field.name + '"';
|
||||
}
|
||||
}
|
||||
|
||||
var measurement = target.measurement;
|
||||
if (!measurement.match('^/.*/') && !measurement.match(/^merge\(.*\)/)) {
|
||||
measurement = '"' + measurement+ '"';
|
||||
}
|
||||
|
||||
query += ' FROM ' + measurement + ' WHERE ';
|
||||
var conditions = _.map(target.tags, function(tag, index) {
|
||||
return renderTagCondition(tag, index);
|
||||
});
|
||||
|
||||
query += conditions.join(' ');
|
||||
query += (conditions.length > 0 ? ' AND ' : '') + '$timeFilter';
|
||||
|
||||
query += ' GROUP BY';
|
||||
for (i = 0; i < target.groupBy.length; i++) {
|
||||
var group = target.groupBy[i];
|
||||
if (group.type === 'time') {
|
||||
query += ' time(' + this._getGroupByTimeInterval(group.interval) + ')';
|
||||
} else {
|
||||
query += ', "' + group.key + '"';
|
||||
}
|
||||
}
|
||||
|
||||
if (target.fill) {
|
||||
query += ' fill(' + target.fill + ')';
|
||||
}
|
||||
|
||||
target.query = query;
|
||||
|
||||
return query;
|
||||
};
|
||||
|
||||
p._modifyRawQuery = function () {
|
||||
var query = this.target.query.replace(";", "");
|
||||
return query;
|
||||
};
|
||||
|
||||
return InfluxQueryBuilder;
|
||||
});
|
||||
|
@ -2,32 +2,33 @@ define([
|
||||
'angular',
|
||||
'lodash',
|
||||
'./query_builder',
|
||||
'./influx_query',
|
||||
'./query_part',
|
||||
'./query_part_editor',
|
||||
],
|
||||
function (angular, _, InfluxQueryBuilder) {
|
||||
function (angular, _, InfluxQueryBuilder, InfluxQuery, queryPart) {
|
||||
'use strict';
|
||||
|
||||
var module = angular.module('grafana.controllers');
|
||||
|
||||
module.controller('InfluxQueryCtrl', function($scope, $timeout, $sce, templateSrv, $q, uiSegmentSrv) {
|
||||
module.controller('InfluxQueryCtrl', function($scope, templateSrv, $q, uiSegmentSrv) {
|
||||
|
||||
$scope.init = function() {
|
||||
if (!$scope.target) { return; }
|
||||
|
||||
var target = $scope.target;
|
||||
target.tags = target.tags || [];
|
||||
target.groupBy = target.groupBy || [{type: 'time', interval: 'auto'}];
|
||||
target.fields = target.fields || [{name: 'value', func: target.function || 'mean'}];
|
||||
$scope.target = $scope.target;
|
||||
$scope.queryModel = new InfluxQuery($scope.target);
|
||||
$scope.queryBuilder = new InfluxQueryBuilder($scope.target);
|
||||
$scope.groupBySegment = uiSegmentSrv.newPlusButton();
|
||||
|
||||
$scope.queryBuilder = new InfluxQueryBuilder(target);
|
||||
|
||||
if (!target.measurement) {
|
||||
if (!$scope.target.measurement) {
|
||||
$scope.measurementSegment = uiSegmentSrv.newSelectMeasurement();
|
||||
} else {
|
||||
$scope.measurementSegment = uiSegmentSrv.newSegment(target.measurement);
|
||||
$scope.measurementSegment = uiSegmentSrv.newSegment($scope.target.measurement);
|
||||
}
|
||||
|
||||
$scope.tagSegments = [];
|
||||
_.each(target.tags, function(tag) {
|
||||
_.each($scope.target.tags, function(tag) {
|
||||
if (!tag.operator) {
|
||||
if (/^\/.*\/$/.test(tag.value)) {
|
||||
tag.operator = "=~";
|
||||
@ -46,9 +47,69 @@ function (angular, _, InfluxQueryBuilder) {
|
||||
});
|
||||
|
||||
$scope.fixTagSegments();
|
||||
$scope.buildSelectMenu();
|
||||
$scope.removeTagFilterSegment = uiSegmentSrv.newSegment({fake: true, value: '-- remove tag filter --'});
|
||||
};
|
||||
|
||||
$scope.buildSelectMenu = function() {
|
||||
var categories = queryPart.getCategories();
|
||||
$scope.selectMenu = _.reduce(categories, function(memo, cat, key) {
|
||||
var menu = {text: key};
|
||||
menu.submenu = _.map(cat, function(item) {
|
||||
return {text: item.type, value: item.type};
|
||||
});
|
||||
memo.push(menu);
|
||||
return memo;
|
||||
}, []);
|
||||
};
|
||||
|
||||
$scope.getGroupByOptions = function() {
|
||||
var query = $scope.queryBuilder.buildExploreQuery('TAG_KEYS');
|
||||
|
||||
return $scope.datasource.metricFindQuery(query)
|
||||
.then(function(tags) {
|
||||
var options = [];
|
||||
if (!$scope.queryModel.hasFill()) {
|
||||
options.push(uiSegmentSrv.newSegment({value: 'fill(null)'}));
|
||||
}
|
||||
if (!$scope.queryModel.hasGroupByTime()) {
|
||||
options.push(uiSegmentSrv.newSegment({value: 'time($interval)'}));
|
||||
}
|
||||
_.each(tags, function(tag) {
|
||||
options.push(uiSegmentSrv.newSegment({value: 'tag(' + tag.text + ')'}));
|
||||
});
|
||||
return options;
|
||||
})
|
||||
.then(null, $scope.handleQueryError);
|
||||
};
|
||||
|
||||
$scope.groupByAction = function() {
|
||||
$scope.queryModel.addGroupBy($scope.groupBySegment.value);
|
||||
var plusButton = uiSegmentSrv.newPlusButton();
|
||||
$scope.groupBySegment.value = plusButton.value;
|
||||
$scope.groupBySegment.html = plusButton.html;
|
||||
$scope.get_data();
|
||||
};
|
||||
|
||||
$scope.removeGroupByPart = function(part, index) {
|
||||
$scope.queryModel.removeGroupByPart(part, index);
|
||||
$scope.get_data();
|
||||
};
|
||||
|
||||
$scope.addSelectPart = function(selectParts, cat, subitem) {
|
||||
$scope.queryModel.addSelectPart(selectParts, subitem.value);
|
||||
$scope.get_data();
|
||||
};
|
||||
|
||||
$scope.removeSelectPart = function(selectParts, part) {
|
||||
$scope.queryModel.removeSelectPart(selectParts, part);
|
||||
$scope.get_data();
|
||||
};
|
||||
|
||||
$scope.selectPartUpdated = function() {
|
||||
$scope.get_data();
|
||||
};
|
||||
|
||||
$scope.fixTagSegments = function() {
|
||||
var count = $scope.tagSegments.length;
|
||||
var lastSegment = $scope.tagSegments[Math.max(count-1, 0)];
|
||||
@ -58,38 +119,9 @@ function (angular, _, InfluxQueryBuilder) {
|
||||
}
|
||||
};
|
||||
|
||||
$scope.addGroupBy = function() {
|
||||
$scope.target.groupBy.push({type: 'tag', key: "select tag"});
|
||||
};
|
||||
|
||||
$scope.removeGroupBy = function(index) {
|
||||
$scope.target.groupBy.splice(index, 1);
|
||||
$scope.get_data();
|
||||
};
|
||||
|
||||
$scope.addSelect = function() {
|
||||
$scope.target.fields.push({name: "select field", func: 'mean'});
|
||||
};
|
||||
|
||||
$scope.removeSelect = function(index) {
|
||||
$scope.target.fields.splice(index, 1);
|
||||
$scope.get_data();
|
||||
};
|
||||
|
||||
$scope.changeFunction = function(func) {
|
||||
$scope.target.function = func;
|
||||
$scope.$parent.get_data();
|
||||
};
|
||||
|
||||
$scope.measurementChanged = function() {
|
||||
$scope.target.measurement = $scope.measurementSegment.value;
|
||||
$scope.$parent.get_data();
|
||||
};
|
||||
|
||||
$scope.getFields = function() {
|
||||
var fieldsQuery = $scope.queryBuilder.buildExploreQuery('FIELDS');
|
||||
return $scope.datasource.metricFindQuery(fieldsQuery)
|
||||
.then($scope.transformToSegments(false), $scope.handleQueryError);
|
||||
$scope.get_data();
|
||||
};
|
||||
|
||||
$scope.toggleQueryMode = function () {
|
||||
@ -102,20 +134,17 @@ function (angular, _, InfluxQueryBuilder) {
|
||||
.then($scope.transformToSegments(true), $scope.handleQueryError);
|
||||
};
|
||||
|
||||
$scope.getFunctions = function () {
|
||||
var functionList = ['count', 'mean', 'sum', 'min', 'max', 'mode', 'distinct', 'median',
|
||||
'stddev', 'first', 'last'
|
||||
];
|
||||
return $q.when(_.map(functionList, function(func) {
|
||||
return uiSegmentSrv.newSegment(func);
|
||||
}));
|
||||
};
|
||||
|
||||
$scope.getGroupByTimeIntervals = function () {
|
||||
var times = ['auto', '1s', '10s', '1m', '2m', '5m', '10m', '30m', '1h', '1d'];
|
||||
return $q.when(_.map(times, function(func) {
|
||||
return uiSegmentSrv.newSegment(func);
|
||||
}));
|
||||
$scope.getPartOptions = function(part) {
|
||||
if (part.def.type === 'field') {
|
||||
var fieldsQuery = $scope.queryBuilder.buildExploreQuery('FIELDS');
|
||||
return $scope.datasource.metricFindQuery(fieldsQuery)
|
||||
.then($scope.transformToSegments(true), $scope.handleQueryError);
|
||||
}
|
||||
if (part.def.type === 'tag') {
|
||||
var tagsQuery = $scope.queryBuilder.buildExploreQuery('TAG_KEYS');
|
||||
return $scope.datasource.metricFindQuery(tagsQuery)
|
||||
.then($scope.transformToSegments(true), $scope.handleQueryError);
|
||||
}
|
||||
};
|
||||
|
||||
$scope.handleQueryError = function(err) {
|
||||
@ -179,25 +208,8 @@ function (angular, _, InfluxQueryBuilder) {
|
||||
.then(null, $scope.handleQueryError);
|
||||
};
|
||||
|
||||
$scope.addField = function() {
|
||||
$scope.target.fields.push({name: $scope.addFieldSegment.value, func: 'mean'});
|
||||
_.extend($scope.addFieldSegment, uiSegmentSrv.newPlusButton());
|
||||
};
|
||||
|
||||
$scope.fieldChanged = function(field) {
|
||||
if (field.name === '-- remove from select --') {
|
||||
$scope.target.fields = _.without($scope.target.fields, field);
|
||||
}
|
||||
$scope.get_data();
|
||||
};
|
||||
|
||||
$scope.getTagOptions = function() {
|
||||
var query = $scope.queryBuilder.buildExploreQuery('TAG_KEYS');
|
||||
|
||||
return $scope.datasource.metricFindQuery(query)
|
||||
.then($scope.transformToSegments(false))
|
||||
.then(null, $scope.handleQueryError);
|
||||
};
|
||||
};
|
||||
|
||||
$scope.setFill = function(fill) {
|
||||
$scope.target.fill = fill;
|
||||
|
432
public/app/plugins/datasource/influxdb/query_part.ts
Normal file
432
public/app/plugins/datasource/influxdb/query_part.ts
Normal 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;
|
||||
}
|
||||
};
|
178
public/app/plugins/datasource/influxdb/query_part_editor.js
Normal file
178
public/app/plugins/datasource/influxdb/query_part_editor.js
Normal 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();
|
||||
}
|
||||
};
|
||||
|
||||
});
|
||||
|
||||
});
|
@ -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');
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
@ -6,116 +6,6 @@ declare var InfluxQueryBuilder: any;
|
||||
|
||||
describe('InfluxQueryBuilder', function() {
|
||||
|
||||
describe('series with mesurement only', function() {
|
||||
it('should generate correct query', function() {
|
||||
var builder = new InfluxQueryBuilder({
|
||||
measurement: 'cpu',
|
||||
groupBy: [{type: 'time', interval: 'auto'}]
|
||||
});
|
||||
|
||||
var query = builder.build();
|
||||
|
||||
expect(query).to.be('SELECT mean("value") AS "value" FROM "cpu" WHERE $timeFilter GROUP BY time($interval)');
|
||||
});
|
||||
});
|
||||
|
||||
describe('series with math expr and as expr', function() {
|
||||
it('should generate correct query', function() {
|
||||
var builder = new InfluxQueryBuilder({
|
||||
measurement: 'cpu',
|
||||
fields: [{name: 'test', func: 'max', mathExpr: '*2', asExpr: 'new_name'}],
|
||||
groupBy: [{type: 'time', interval: 'auto'}]
|
||||
});
|
||||
|
||||
var query = builder.build();
|
||||
|
||||
expect(query).to.be('SELECT max("test")*2 AS "new_name" FROM "cpu" WHERE $timeFilter GROUP BY time($interval)');
|
||||
});
|
||||
});
|
||||
|
||||
describe('series with single tag only', function() {
|
||||
it('should generate correct query', function() {
|
||||
var builder = new InfluxQueryBuilder({
|
||||
measurement: 'cpu',
|
||||
groupBy: [{type: 'time', interval: 'auto'}],
|
||||
tags: [{key: 'hostname', value: 'server1'}]
|
||||
});
|
||||
|
||||
var query = builder.build();
|
||||
|
||||
expect(query).to.be('SELECT mean("value") AS "value" FROM "cpu" WHERE "hostname" = \'server1\' AND $timeFilter'
|
||||
+ ' GROUP BY time($interval)');
|
||||
});
|
||||
|
||||
it('should switch regex operator with tag value is regex', function() {
|
||||
var builder = new InfluxQueryBuilder({
|
||||
measurement: 'cpu',
|
||||
groupBy: [{type: 'time', interval: 'auto'}],
|
||||
tags: [{key: 'app', value: '/e.*/'}]
|
||||
});
|
||||
|
||||
var query = builder.build();
|
||||
expect(query).to.be('SELECT mean("value") AS "value" FROM "cpu" WHERE "app" =~ /e.*/ AND $timeFilter GROUP BY time($interval)');
|
||||
});
|
||||
});
|
||||
|
||||
describe('series with multiple fields', function() {
|
||||
it('should generate correct query', function() {
|
||||
var builder = new InfluxQueryBuilder({
|
||||
measurement: 'cpu',
|
||||
tags: [],
|
||||
groupBy: [{type: 'time', interval: 'auto'}],
|
||||
fields: [{ name: 'tx_in', func: 'sum' }, { name: 'tx_out', func: 'mean' }]
|
||||
});
|
||||
|
||||
var query = builder.build();
|
||||
expect(query).to.be('SELECT sum("tx_in") AS "tx_in", mean("tx_out") AS "tx_out" ' +
|
||||
'FROM "cpu" WHERE $timeFilter GROUP BY time($interval)');
|
||||
});
|
||||
});
|
||||
|
||||
describe('series with multiple tags only', function() {
|
||||
it('should generate correct query', function() {
|
||||
var builder = new InfluxQueryBuilder({
|
||||
measurement: 'cpu',
|
||||
groupBy: [{type: 'time', interval: 'auto'}],
|
||||
tags: [{key: 'hostname', value: 'server1'}, {key: 'app', value: 'email', condition: "AND"}]
|
||||
});
|
||||
|
||||
var query = builder.build();
|
||||
expect(query).to.be('SELECT mean("value") AS "value" FROM "cpu" WHERE "hostname" = \'server1\' AND "app" = \'email\' AND ' +
|
||||
'$timeFilter GROUP BY time($interval)');
|
||||
});
|
||||
});
|
||||
|
||||
describe('series with tags OR condition', function() {
|
||||
it('should generate correct query', function() {
|
||||
var builder = new InfluxQueryBuilder({
|
||||
measurement: 'cpu',
|
||||
groupBy: [{type: 'time', interval: 'auto'}],
|
||||
tags: [{key: 'hostname', value: 'server1'}, {key: 'hostname', value: 'server2', condition: "OR"}]
|
||||
});
|
||||
|
||||
var query = builder.build();
|
||||
expect(query).to.be('SELECT mean("value") AS "value" FROM "cpu" WHERE "hostname" = \'server1\' OR "hostname" = \'server2\' AND ' +
|
||||
'$timeFilter GROUP BY time($interval)');
|
||||
});
|
||||
});
|
||||
|
||||
describe('series with groupByTag', function() {
|
||||
it('should generate correct query', function() {
|
||||
var builder = new InfluxQueryBuilder({
|
||||
measurement: 'cpu',
|
||||
tags: [],
|
||||
groupBy: [{type: 'time', interval: 'auto'}, {type: 'tag', key: 'host'}],
|
||||
});
|
||||
|
||||
var query = builder.build();
|
||||
expect(query).to.be('SELECT mean("value") AS "value" FROM "cpu" WHERE $timeFilter ' +
|
||||
'GROUP BY time($interval), "host"');
|
||||
});
|
||||
});
|
||||
|
||||
describe('when building explore queries', function() {
|
||||
|
||||
it('should only have measurement condition in tag keys query given query with measurement', function() {
|
||||
@ -126,8 +16,7 @@ describe('InfluxQueryBuilder', function() {
|
||||
|
||||
it('should handle regex measurement in tag keys query', function() {
|
||||
var builder = new InfluxQueryBuilder({
|
||||
measurement: '/.*/',
|
||||
tags: []
|
||||
measurement: '/.*/', tags: []
|
||||
});
|
||||
var query = builder.buildExploreQuery('TAG_KEYS');
|
||||
expect(query).to.be('SHOW TAG KEYS FROM /.*/');
|
||||
@ -170,7 +59,10 @@ describe('InfluxQueryBuilder', function() {
|
||||
});
|
||||
|
||||
it('should switch to regex operator in tag condition', function() {
|
||||
var builder = new InfluxQueryBuilder({measurement: 'cpu', tags: [{key: 'host', value: '/server.*/'}]});
|
||||
var builder = new InfluxQueryBuilder({
|
||||
measurement: 'cpu',
|
||||
tags: [{key: 'host', value: '/server.*/'}]
|
||||
});
|
||||
var query = builder.buildExploreQuery('TAG_VALUES', 'app');
|
||||
expect(query).to.be('SHOW TAG VALUES FROM "cpu" WITH KEY = "app" WHERE "host" =~ /server.*/');
|
||||
});
|
||||
|
@ -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"');
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
@ -111,11 +111,9 @@ function (angular, _, moment, dateMath) {
|
||||
var url = '/api/v1/label/__name__/values';
|
||||
|
||||
return this._request('GET', url).then(function(result) {
|
||||
var suggestData = _.filter(result.data.data, function(metricName) {
|
||||
return metricName.indexOf(query) !== 1;
|
||||
return _.filter(result.data.data, function (metricName) {
|
||||
return metricName.indexOf(query) !== 1;
|
||||
});
|
||||
|
||||
return suggestData;
|
||||
});
|
||||
};
|
||||
|
||||
|
@ -6,7 +6,7 @@
|
||||
<li class="tight-form-item" style="width: 80px">
|
||||
Left Y
|
||||
</li>
|
||||
<li class="tight-form-item">
|
||||
<li class="tight-form-item" style="width: 40px">
|
||||
Unit
|
||||
</li>
|
||||
<li class="dropdown" style="width: 140px;"
|
||||
@ -14,22 +14,6 @@
|
||||
dropdown-typeahead="unitFormats"
|
||||
dropdown-typeahead-on-select="setUnitFormat(0, $subItem)">
|
||||
</li>
|
||||
<li class="tight-form-item">
|
||||
Grid Max
|
||||
</li>
|
||||
<li>
|
||||
<input type="number" class="input-small tight-form-input" placeholder="auto"
|
||||
empty-to-null ng-model="panel.grid.leftMax"
|
||||
ng-change="render()" ng-model-onblur>
|
||||
</li>
|
||||
<li class="tight-form-item">
|
||||
Min
|
||||
</li>
|
||||
<li>
|
||||
<input type="number" class="input-small tight-form-input" placeholder="auto"
|
||||
empty-to-null ng-model="panel.grid.leftMin"
|
||||
ng-change="render()" ng-model-onblur>
|
||||
</li>
|
||||
<li class="tight-form-item">
|
||||
Scale type
|
||||
</li>
|
||||
@ -46,12 +30,36 @@
|
||||
</ul>
|
||||
<div class="clearfix"></div>
|
||||
</div>
|
||||
<div class="tight-form last">
|
||||
<div class="tight-form">
|
||||
<ul class="tight-form-list">
|
||||
<li class="tight-form-item" style="width: 80px">
|
||||
<i class="fa fa-remove invisible"></i>
|
||||
</li>
|
||||
<li class="tight-form-item">
|
||||
Y-Max
|
||||
</li>
|
||||
<li>
|
||||
<input type="number" class="input-small tight-form-input" placeholder="auto"
|
||||
empty-to-null ng-model="panel.grid.leftMax"
|
||||
ng-change="render()" ng-model-onblur>
|
||||
</li>
|
||||
<li class="tight-form-item" style="width: 115px; text-align: right;">
|
||||
Y-Min
|
||||
</li>
|
||||
<li>
|
||||
<input type="number" class="input-small tight-form-input" placeholder="auto" style="width: 113px;"
|
||||
empty-to-null ng-model="panel.grid.leftMin"
|
||||
ng-change="render()" ng-model-onblur>
|
||||
</li>
|
||||
</ul>
|
||||
<div class="clearfix"></div>
|
||||
</div>
|
||||
<div class="tight-form">
|
||||
<ul class="tight-form-list">
|
||||
<li class="tight-form-item" style="width: 80px">
|
||||
Right Y
|
||||
</li>
|
||||
<li class="tight-form-item">
|
||||
<li class="tight-form-item" style="width: 40px">
|
||||
Unit
|
||||
</li>
|
||||
<li class="dropdown" style="width: 140px"
|
||||
@ -59,22 +67,6 @@
|
||||
dropdown-typeahead="unitFormats"
|
||||
dropdown-typeahead-on-select="setUnitFormat(1, $subItem)">
|
||||
</li>
|
||||
<li class="tight-form-item">
|
||||
Grid Max
|
||||
</li>
|
||||
<li>
|
||||
<input type="number" class="input-small tight-form-input" placeholder="auto"
|
||||
empty-to-null ng-model="panel.grid.rightMax"
|
||||
ng-change="render()" ng-model-onblur>
|
||||
</li>
|
||||
<li class="tight-form-item">
|
||||
Min
|
||||
</li>
|
||||
<li>
|
||||
<input type="number" class="input-small tight-form-input" placeholder="auto"
|
||||
empty-to-null ng-model="panel.grid.rightMin"
|
||||
ng-change="render()" ng-model-onblur>
|
||||
</li>
|
||||
<li class="tight-form-item">
|
||||
Scale type
|
||||
</li>
|
||||
@ -91,6 +83,31 @@
|
||||
</ul>
|
||||
<div class="clearfix"></div>
|
||||
</div>
|
||||
<div class="tight-form last">
|
||||
<ul class="tight-form-list">
|
||||
<li class="tight-form-item" style="width: 80px">
|
||||
<i class="fa fa-remove invisible"></i>
|
||||
</li>
|
||||
<li class="tight-form-item">
|
||||
Y-Max
|
||||
</li>
|
||||
<li>
|
||||
<input type="number" class="input-small tight-form-input" placeholder="auto"
|
||||
empty-to-null ng-model="panel.grid.rightMax"
|
||||
ng-change="render()" ng-model-onblur>
|
||||
</li>
|
||||
<li class="tight-form-item" style="width: 115px; text-align: right;">
|
||||
Y-Min
|
||||
</li>
|
||||
<li>
|
||||
<input type="number" class="input-small tight-form-input" placeholder="auto" style="width: 113px;"
|
||||
empty-to-null ng-model="panel.grid.rightMin"
|
||||
ng-change="render()" ng-model-onblur>
|
||||
</li>
|
||||
</ul>
|
||||
<div class="clearfix"></div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="section" style="margin-bottom: 20px">
|
||||
@ -150,9 +167,9 @@
|
||||
|
||||
<div class="editor-row">
|
||||
<div class="section">
|
||||
<div class="tight-form last">
|
||||
<div class="tight-form">
|
||||
<ul class="tight-form-list">
|
||||
<li class="tight-form-item" style="width: 110px">
|
||||
<li class="tight-form-item" style="width: 80px">
|
||||
Legend
|
||||
</li>
|
||||
<li class="tight-form-item">
|
||||
@ -164,18 +181,28 @@
|
||||
<li class="tight-form-item">
|
||||
<editor-checkbox text="Right side" model="panel.legend.rightSide" change="render()"></editor-checkbox>
|
||||
</li>
|
||||
</ul>
|
||||
<div class="clearfix"></div>
|
||||
</div>
|
||||
<div class="tight-form">
|
||||
<ul class="tight-form-list">
|
||||
<li class="tight-form-item" style="width: 80px">
|
||||
Hide series
|
||||
</li>
|
||||
<li class="tight-form-item">
|
||||
<editor-checkbox text="With only nulls" model="panel.legend.hideEmpty" change="render()"></editor-checkbox>
|
||||
</li>
|
||||
<li class="tight-form-item last">
|
||||
<editor-checkbox text="Hide empty" model="panel.legend.hideEmpty" change="render()"></editor-checkbox>
|
||||
<editor-checkbox text="With only zeroes" model="panel.legend.hideZero" change="render()"></editor-checkbox>
|
||||
</li>
|
||||
</ul>
|
||||
<div class="clearfix"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="section">
|
||||
<div class="tight-form">
|
||||
|
||||
<div class="tight-form last">
|
||||
<ul class="tight-form-list">
|
||||
<li class="tight-form-item" style="width: 105px">
|
||||
Legend values
|
||||
<li class="tight-form-item" style="width: 80px">
|
||||
Values
|
||||
</li>
|
||||
<li class="tight-form-item">
|
||||
<editor-checkbox text="Min" model="panel.legend.min" change="legendValuesOptionChanged()"></editor-checkbox>
|
||||
@ -189,16 +216,11 @@
|
||||
<li class="tight-form-item">
|
||||
<editor-checkbox text="Current" model="panel.legend.current" change="legendValuesOptionChanged()"></editor-checkbox>
|
||||
</li>
|
||||
<li class="tight-form-item last">
|
||||
<li class="tight-form-item">
|
||||
<editor-checkbox text="Total" model="panel.legend.total" change="legendValuesOptionChanged()"></editor-checkbox>
|
||||
</li>
|
||||
</ul>
|
||||
<div class="clearfix"></div>
|
||||
</div>
|
||||
<div class="tight-form last">
|
||||
<ul class="tight-form-list">
|
||||
<li class="tight-form-item" style="width: 105px">
|
||||
<strong>Decimals</strong>
|
||||
<li class="tight-form-item">
|
||||
Decimals
|
||||
</li>
|
||||
<li style="width: 105px">
|
||||
<input type="number" class="input-small tight-form-input" placeholder="auto" bs-tooltip="'Override automatic decimal precision for legend and tooltips'" data-placement="right"
|
||||
@ -207,7 +229,5 @@
|
||||
</ul>
|
||||
<div class="clearfix"></div>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
@ -52,6 +52,11 @@ function ($) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!series.data.length || (scope.panel.legend.hideZero && series.allIsZero)) {
|
||||
results.push({ hidden: true });
|
||||
continue;
|
||||
}
|
||||
|
||||
hoverIndex = this.findHoverIndexFromData(pos.x, series);
|
||||
results.time = series.data[hoverIndex][0];
|
||||
|
||||
|
@ -137,6 +137,10 @@ function (angular, _, $) {
|
||||
if (!series.legend) {
|
||||
continue;
|
||||
}
|
||||
// ignore zero series
|
||||
if (panel.legend.hideZero && series.allIsZero) {
|
||||
continue;
|
||||
}
|
||||
|
||||
var html = '<div class="graph-legend-series';
|
||||
if (series.yaxis === 2) { html += ' pull-right'; }
|
||||
|
@ -60,7 +60,6 @@ export class TablePanelCtrl {
|
||||
}
|
||||
|
||||
_.defaults($scope.panel, panelDefaults);
|
||||
|
||||
panelSrv.init($scope);
|
||||
};
|
||||
|
||||
|
@ -16,7 +16,7 @@
|
||||
</ul>
|
||||
<div class="clearfix"></div>
|
||||
</div>
|
||||
<div class="tight-form" ng-if="showColumnOptions">
|
||||
<div class="tight-form">
|
||||
<ul class="tight-form-list">
|
||||
<li class="tight-form-item" style="width: 140px">
|
||||
Columns
|
||||
@ -27,7 +27,8 @@
|
||||
{{column.text}}
|
||||
</span>
|
||||
</li>
|
||||
<li class="dropdown" dropdown-typeahead="columnsMenu" dropdown-typeahead-on-select="addColumn($item, $subItem)">
|
||||
<li>
|
||||
<metric-segment segment="addColumnSegment" get-options="getColumnOptions()" on-change="addColumn()"></metric-segment>
|
||||
</li>
|
||||
</ul>
|
||||
<div class="clearfix"></div>
|
||||
@ -158,7 +159,7 @@
|
||||
</div>
|
||||
|
||||
<button class="btn btn-inverse" style="margin-top: 20px" ng-click="addColumnStyle()">
|
||||
Add style display rule
|
||||
Add column style rule
|
||||
</button>
|
||||
</div>
|
||||
|
||||
|
@ -8,93 +8,101 @@ import moment = require('moment');
|
||||
|
||||
import {transformers} from './transformers';
|
||||
|
||||
export function tablePanelEditor() {
|
||||
export class TablePanelEditorCtrl {
|
||||
|
||||
/** @ngInject */
|
||||
constructor($scope, $q, uiSegmentSrv) {
|
||||
$scope.transformers = transformers;
|
||||
$scope.unitFormats = kbn.getUnitFormats();
|
||||
$scope.colorModes = [
|
||||
{text: 'Disabled', value: null},
|
||||
{text: 'Cell', value: 'cell'},
|
||||
{text: 'Value', value: 'value'},
|
||||
{text: 'Row', value: 'row'},
|
||||
];
|
||||
$scope.columnTypes = [
|
||||
{text: 'Number', value: 'number'},
|
||||
{text: 'String', value: 'string'},
|
||||
{text: 'Date', value: 'date'},
|
||||
];
|
||||
$scope.fontSizes = ['80%', '90%', '100%', '110%', '120%', '130%', '150%', '160%', '180%', '200%', '220%', '250%'];
|
||||
$scope.dateFormats = [
|
||||
{text: 'YYYY-MM-DD HH:mm:ss', value: 'YYYY-MM-DD HH:mm:ss'},
|
||||
{text: 'MM/DD/YY h:mm:ss a', value: 'MM/DD/YY h:mm:ss a'},
|
||||
{text: 'MMMM D, YYYY LT', value: 'MMMM D, YYYY LT'},
|
||||
];
|
||||
|
||||
$scope.addColumnSegment = uiSegmentSrv.newPlusButton();
|
||||
|
||||
$scope.getColumnOptions = function() {
|
||||
if (!$scope.dataRaw) {
|
||||
return $q.when([]);
|
||||
}
|
||||
var columns = transformers[$scope.panel.transform].getColumns($scope.dataRaw);
|
||||
var segments = _.map(columns, (c: any) => uiSegmentSrv.newSegment({value: c.text}));
|
||||
return $q.when(segments);
|
||||
};
|
||||
|
||||
$scope.addColumn = function() {
|
||||
$scope.panel.columns.push({text: $scope.addColumnSegment.value, value: $scope.addColumnSegment.value});
|
||||
$scope.render();
|
||||
|
||||
var plusButton = uiSegmentSrv.newPlusButton();
|
||||
$scope.addColumnSegment.html = plusButton.html;
|
||||
};
|
||||
|
||||
$scope.transformChanged = function() {
|
||||
$scope.panel.columns = [];
|
||||
$scope.render();
|
||||
};
|
||||
|
||||
$scope.removeColumn = function(column) {
|
||||
$scope.panel.columns = _.without($scope.panel.columns, column);
|
||||
$scope.render();
|
||||
};
|
||||
|
||||
$scope.setUnitFormat = function(column, subItem) {
|
||||
column.unit = subItem.value;
|
||||
$scope.render();
|
||||
};
|
||||
|
||||
$scope.addColumnStyle = function() {
|
||||
var columnStyleDefaults = {
|
||||
unit: 'short',
|
||||
type: 'number',
|
||||
decimals: 2,
|
||||
colors: ["rgba(245, 54, 54, 0.9)", "rgba(237, 129, 40, 0.89)", "rgba(50, 172, 45, 0.97)"],
|
||||
colorMode: null,
|
||||
pattern: '/.*/',
|
||||
dateFormat: 'YYYY-MM-DD HH:mm:ss',
|
||||
thresholds: [],
|
||||
};
|
||||
|
||||
$scope.panel.styles.push(angular.copy(columnStyleDefaults));
|
||||
};
|
||||
|
||||
$scope.removeColumnStyle = function(style) {
|
||||
$scope.panel.styles = _.without($scope.panel.styles, style);
|
||||
};
|
||||
|
||||
$scope.getColumnNames = function() {
|
||||
if (!$scope.table) {
|
||||
return [];
|
||||
}
|
||||
return _.map($scope.table.columns, function(col: any) {
|
||||
return col.text;
|
||||
});
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export function tablePanelEditor($q, uiSegmentSrv) {
|
||||
'use strict';
|
||||
return {
|
||||
restrict: 'E',
|
||||
scope: true,
|
||||
templateUrl: 'app/plugins/panels/table/editor.html',
|
||||
link: function(scope, elem) {
|
||||
scope.transformers = transformers;
|
||||
scope.unitFormats = kbn.getUnitFormats();
|
||||
scope.colorModes = [
|
||||
{text: 'Disabled', value: null},
|
||||
{text: 'Cell', value: 'cell'},
|
||||
{text: 'Value', value: 'value'},
|
||||
{text: 'Row', value: 'row'},
|
||||
];
|
||||
scope.columnTypes = [
|
||||
{text: 'Number', value: 'number'},
|
||||
{text: 'String', value: 'string'},
|
||||
{text: 'Date', value: 'date'},
|
||||
];
|
||||
scope.fontSizes = ['80%', '90%', '100%', '110%', '120%', '130%', '150%', '160%', '180%', '200%', '220%', '250%'];
|
||||
scope.dateFormats = [
|
||||
{text: 'YYYY-MM-DD HH:mm:ss', value: 'YYYY-MM-DD HH:mm:ss'},
|
||||
{text: 'MM/DD/YY h:mm:ss a', value: 'MM/DD/YY h:mm:ss a'},
|
||||
{text: 'MMMM D, YYYY LT', value: 'MMMM D, YYYY LT'},
|
||||
];
|
||||
|
||||
scope.updateColumnsMenu = function(data) {
|
||||
scope.columnsMenu = transformers[scope.panel.transform].getColumns(data);
|
||||
scope.showColumnOptions = true;
|
||||
};
|
||||
|
||||
scope.$on('render', function(event, table, rawData) {
|
||||
scope.updateColumnsMenu(rawData);
|
||||
});
|
||||
|
||||
scope.addColumn = function(menuItem) {
|
||||
scope.panel.columns.push({text: menuItem.text, value: menuItem.value});
|
||||
scope.render();
|
||||
};
|
||||
|
||||
scope.transformChanged = function() {
|
||||
scope.panel.columns = [];
|
||||
scope.updateColumnsMenu();
|
||||
scope.render();
|
||||
};
|
||||
|
||||
scope.removeColumn = function(column) {
|
||||
scope.panel.columns = _.without(scope.panel.columns, column);
|
||||
scope.render();
|
||||
};
|
||||
|
||||
scope.setUnitFormat = function(column, subItem) {
|
||||
column.unit = subItem.value;
|
||||
scope.render();
|
||||
};
|
||||
|
||||
scope.addColumnStyle = function() {
|
||||
var columnStyleDefaults = {
|
||||
unit: 'short',
|
||||
type: 'number',
|
||||
decimals: 2,
|
||||
colors: ["rgba(245, 54, 54, 0.9)", "rgba(237, 129, 40, 0.89)", "rgba(50, 172, 45, 0.97)"],
|
||||
colorMode: null,
|
||||
pattern: '/.*/',
|
||||
dateFormat: 'YYYY-MM-DD HH:mm:ss',
|
||||
thresholds: [],
|
||||
};
|
||||
|
||||
scope.panel.styles.push(angular.copy(columnStyleDefaults));
|
||||
};
|
||||
|
||||
scope.removeColumnStyle = function(style) {
|
||||
scope.panel.styles = _.without(scope.panel.styles, style);
|
||||
};
|
||||
|
||||
scope.getColumnNames = function() {
|
||||
if (!scope.table) {
|
||||
return [];
|
||||
}
|
||||
return _.map(scope.table.columns, function(col: any) {
|
||||
return col.text;
|
||||
});
|
||||
};
|
||||
|
||||
scope.updateColumnsMenu(scope.dataRaw);
|
||||
}
|
||||
templateUrl: 'app/panels/table/editor.html',
|
||||
controller: TablePanelEditorCtrl,
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -19,6 +19,7 @@ export function tablePanel() {
|
||||
link: function(scope, elem) {
|
||||
var data;
|
||||
var panel = scope.panel;
|
||||
var pageCount = 0;
|
||||
var formaters = [];
|
||||
|
||||
function getTableHeight() {
|
||||
@ -26,8 +27,11 @@ export function tablePanel() {
|
||||
if (_.isString(panelHeight)) {
|
||||
panelHeight = parseInt(panelHeight.replace('px', ''), 10);
|
||||
}
|
||||
if (pageCount > 1) {
|
||||
panelHeight -= 28;
|
||||
}
|
||||
|
||||
return (panelHeight - 40) + 'px';
|
||||
return (panelHeight - 60) + 'px';
|
||||
}
|
||||
|
||||
function appendTableRows(tbodyElem) {
|
||||
@ -46,7 +50,7 @@ export function tablePanel() {
|
||||
footerElem.empty();
|
||||
|
||||
var pageSize = panel.pageSize || 100;
|
||||
var pageCount = Math.ceil(data.rows.length / pageSize);
|
||||
pageCount = Math.ceil(data.rows.length / pageSize);
|
||||
if (pageCount === 1) {
|
||||
return;
|
||||
}
|
||||
@ -73,12 +77,10 @@ export function tablePanel() {
|
||||
|
||||
appendTableRows(tbodyElem);
|
||||
|
||||
rootElem.css({
|
||||
'max-height': panel.scroll ? getTableHeight() : ''
|
||||
});
|
||||
|
||||
container.css({'font-size': panel.fontSize});
|
||||
appendPaginationControls(footerElem);
|
||||
|
||||
rootElem.css({'max-height': panel.scroll ? getTableHeight() : '' });
|
||||
}
|
||||
|
||||
elem.on('click', '.table-panel-page-link', switchPage);
|
||||
|
@ -1,6 +1,7 @@
|
||||
import {describe, beforeEach, it, sinon, expect} from 'test/lib/common';
|
||||
|
||||
import {TableModel} from '../table_model';
|
||||
import {transformers} from '../transformers';
|
||||
|
||||
describe('when transforming time series table', () => {
|
||||
var table;
|
||||
@ -100,7 +101,11 @@ describe('when transforming time series table', () => {
|
||||
describe('JSON Data', () => {
|
||||
var panel = {
|
||||
transform: 'json',
|
||||
columns: [{text: 'Timestamp', value: 'timestamp'}, {text: 'Message', value: 'message'}]
|
||||
columns: [
|
||||
{text: 'Timestamp', value: 'timestamp'},
|
||||
{text: 'Message', value: 'message'},
|
||||
{text: 'nested.level2', value: 'nested.level2'},
|
||||
]
|
||||
};
|
||||
var rawData = [
|
||||
{
|
||||
@ -108,26 +113,42 @@ describe('when transforming time series table', () => {
|
||||
datapoints: [
|
||||
{
|
||||
timestamp: 'time',
|
||||
message: 'message'
|
||||
message: 'message',
|
||||
nested: {
|
||||
level2: 'level2-value'
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
];
|
||||
|
||||
beforeEach(() => {
|
||||
table = TableModel.transform(rawData, panel);
|
||||
describe('getColumns', function() {
|
||||
it('should return nested properties', function() {
|
||||
var columns = transformers['json'].getColumns(rawData);
|
||||
expect(columns[0].text).to.be('timestamp');
|
||||
expect(columns[1].text).to.be('message');
|
||||
expect(columns[2].text).to.be('nested.level2');
|
||||
});
|
||||
});
|
||||
|
||||
it ('should return 2 columns', () => {
|
||||
expect(table.columns.length).to.be(2);
|
||||
expect(table.columns[0].text).to.be('Timestamp');
|
||||
expect(table.columns[1].text).to.be('Message');
|
||||
});
|
||||
describe('transform', function() {
|
||||
beforeEach(() => {
|
||||
table = TableModel.transform(rawData, panel);
|
||||
});
|
||||
|
||||
it ('should return 2 rows', () => {
|
||||
expect(table.rows.length).to.be(1);
|
||||
expect(table.rows[0][0]).to.be('time');
|
||||
expect(table.rows[0][1]).to.be('message');
|
||||
it ('should return 2 columns', () => {
|
||||
expect(table.columns.length).to.be(3);
|
||||
expect(table.columns[0].text).to.be('Timestamp');
|
||||
expect(table.columns[1].text).to.be('Message');
|
||||
expect(table.columns[2].text).to.be('nested.level2');
|
||||
});
|
||||
|
||||
it ('should return 2 rows', () => {
|
||||
expect(table.rows.length).to.be(1);
|
||||
expect(table.rows[0][0]).to.be('time');
|
||||
expect(table.rows[0][1]).to.be('message');
|
||||
expect(table.rows[0][2]).to.be('level2-value');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -2,6 +2,7 @@
|
||||
|
||||
import moment = require('moment');
|
||||
import _ = require('lodash');
|
||||
import flatten = require('app/core/utils/flatten');
|
||||
import TimeSeries = require('app/core/time_series');
|
||||
|
||||
var transformers = {};
|
||||
@ -149,9 +150,12 @@ transformers['json'] = {
|
||||
continue;
|
||||
}
|
||||
|
||||
for (var y = 0; y < series.datapoints.length; y++) {
|
||||
// only look at 100 docs
|
||||
var maxDocs = Math.min(series.datapoints.length, 100);
|
||||
for (var y = 0; y < maxDocs; y++) {
|
||||
var doc = series.datapoints[y];
|
||||
for (var propName in doc) {
|
||||
var flattened = flatten(doc, null);
|
||||
for (var propName in flattened) {
|
||||
names[propName] = true;
|
||||
}
|
||||
}
|
||||
@ -177,13 +181,16 @@ transformers['json'] = {
|
||||
for (y = 0; y < series.datapoints.length; y++) {
|
||||
var dp = series.datapoints[y];
|
||||
var values = [];
|
||||
for (z = 0; z < panel.columns.length; z++) {
|
||||
values.push(dp[panel.columns[z].value]);
|
||||
}
|
||||
|
||||
if (values.length === 0) {
|
||||
if (_.isObject(dp) && panel.columns.length > 0) {
|
||||
var flattened = flatten(dp, null);
|
||||
for (z = 0; z < panel.columns.length; z++) {
|
||||
values.push(flattened[panel.columns[z].value]);
|
||||
}
|
||||
} else {
|
||||
values.push(JSON.stringify(dp));
|
||||
}
|
||||
|
||||
model.rows.push(values);
|
||||
}
|
||||
}
|
||||
|
@ -241,7 +241,7 @@
|
||||
{
|
||||
"type": "query",
|
||||
"datasource": null,
|
||||
"refresh_on_load": false,
|
||||
"refresh": false,
|
||||
"name": "metric",
|
||||
"options": [],
|
||||
"includeAll": true,
|
||||
|
24
public/test/core/utils/flatten_specs.ts
Normal file
24
public/test/core/utils/flatten_specs.ts
Normal 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');
|
||||
});
|
||||
|
||||
});
|
||||
|
@ -68,6 +68,27 @@ define([
|
||||
describeValueFormat('wps', 789000000, 1000000, -1, '789M wps');
|
||||
describeValueFormat('iops', 11000000000, 1000000000, -1, '11B iops');
|
||||
|
||||
describeValueFormat('s', 24, 1, 0, '24 s');
|
||||
describeValueFormat('s', 246, 1, 0, '4.1 min');
|
||||
describeValueFormat('s', 24567, 100, 0, '6.82 hour');
|
||||
describeValueFormat('s', 24567890, 10000, 0, '40.62 week');
|
||||
describeValueFormat('s', 24567890000, 1000000, 0, '778.53 year');
|
||||
|
||||
describeValueFormat('m', 24, 1, 0, '24 min');
|
||||
describeValueFormat('m', 246, 10, 0, '4.1 hour');
|
||||
describeValueFormat('m', 6545, 10, 0, '4.55 day');
|
||||
describeValueFormat('m', 24567, 100, 0, '2.44 week');
|
||||
describeValueFormat('m', 24567892, 10000, 0, '46.7 year');
|
||||
|
||||
describeValueFormat('h', 21, 1, 0, '21 hour');
|
||||
describeValueFormat('h', 145, 1, 0, '6.04 day');
|
||||
describeValueFormat('h', 1234, 100, 0, '7.3 week');
|
||||
describeValueFormat('h', 9458, 1000, 0, '1.08 year');
|
||||
|
||||
describeValueFormat('d', 3, 1, 0, '3 day');
|
||||
describeValueFormat('d', 245, 100, 0, '35 week');
|
||||
describeValueFormat('d', 2456, 10, 0, '6.73 year');
|
||||
|
||||
describe('kbn.toFixed and negative decimals', function() {
|
||||
it('should treat as zero decimals', function() {
|
||||
var str = kbn.toFixed(186.123, -2);
|
||||
|
@ -204,7 +204,7 @@ define([
|
||||
});
|
||||
|
||||
it('dashboard schema version should be set to latest', function() {
|
||||
expect(model.schemaVersion).to.be(7);
|
||||
expect(model.schemaVersion).to.be(8);
|
||||
});
|
||||
|
||||
});
|
||||
@ -248,5 +248,90 @@ define([
|
||||
expect(clone.meta).to.be(undefined);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when loading dashboard with old influxdb query schema', function() {
|
||||
var model;
|
||||
var target;
|
||||
|
||||
beforeEach(function() {
|
||||
model = _dashboardSrv.create({
|
||||
rows: [{
|
||||
panels: [{
|
||||
type: 'graph',
|
||||
targets: [{
|
||||
"alias": "$tag_datacenter $tag_source $col",
|
||||
"column": "value",
|
||||
"measurement": "logins.count",
|
||||
"fields": [
|
||||
{
|
||||
"func": "mean",
|
||||
"name": "value",
|
||||
"mathExpr": "*2",
|
||||
"asExpr": "value"
|
||||
},
|
||||
{
|
||||
"name": "one-minute",
|
||||
"func": "mean",
|
||||
"mathExpr": "*3",
|
||||
"asExpr": "one-minute"
|
||||
}
|
||||
],
|
||||
"tags": [],
|
||||
"fill": "previous",
|
||||
"function": "mean",
|
||||
"groupBy": [
|
||||
{
|
||||
"interval": "auto",
|
||||
"type": "time"
|
||||
},
|
||||
{
|
||||
"key": "source",
|
||||
"type": "tag"
|
||||
},
|
||||
{
|
||||
"type": "tag",
|
||||
"key": "datacenter"
|
||||
}
|
||||
],
|
||||
}]
|
||||
}]
|
||||
}]
|
||||
});
|
||||
|
||||
target = model.rows[0].panels[0].targets[0];
|
||||
});
|
||||
|
||||
it('should update query schema', function() {
|
||||
expect(target.fields).to.be(undefined);
|
||||
expect(target.select.length).to.be(2);
|
||||
expect(target.select[0].length).to.be(4);
|
||||
expect(target.select[0][0].type).to.be('field');
|
||||
expect(target.select[0][1].type).to.be('mean');
|
||||
expect(target.select[0][2].type).to.be('math');
|
||||
expect(target.select[0][3].type).to.be('alias');
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('when creating dashboard model with missing list for annoations or templating', function() {
|
||||
var model;
|
||||
|
||||
beforeEach(function() {
|
||||
model = _dashboardSrv.create({
|
||||
annotations: {
|
||||
enable: true,
|
||||
},
|
||||
templating: {
|
||||
enable: true
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it('should add empty list', function() {
|
||||
expect(model.annotations.list.length).to.be(0);
|
||||
expect(model.templating.list.length).to.be(0);
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
|
@ -78,13 +78,20 @@ define([
|
||||
});
|
||||
|
||||
describe('setTime', function() {
|
||||
it('should return disable refresh for absolute times', function() {
|
||||
it('should return disable refresh if refresh is disabled for any range', function() {
|
||||
_dashboard.refresh = false;
|
||||
|
||||
ctx.service.setTime({from: '2011-01-01', to: '2015-01-01' });
|
||||
expect(_dashboard.refresh).to.be(false);
|
||||
});
|
||||
|
||||
it('should restore refresh for absolute time range', function() {
|
||||
_dashboard.refresh = '30s';
|
||||
|
||||
ctx.service.setTime({from: '2011-01-01', to: '2015-01-01' });
|
||||
expect(_dashboard.refresh).to.be('30s');
|
||||
});
|
||||
|
||||
it('should restore refresh after relative time range is set', function() {
|
||||
_dashboard.refresh = '10s';
|
||||
ctx.service.setTime({from: moment([2011,1,1]), to: moment([2015,1,1])});
|
||||
|
6
public/vendor/showdown.js
vendored
6
public/vendor/showdown.js
vendored
@ -855,7 +855,7 @@ var _DoLists = function(text) {
|
||||
|
||||
// Turn double returns into triple returns, so that we can make a
|
||||
// paragraph for the last item in a list, if necessary:
|
||||
list = list.replace(/\n{2,}/g,"\n\n\n");;
|
||||
list = list.replace(/\n{2,}/g,"\n\n\n");
|
||||
var result = _ProcessListItems(list);
|
||||
|
||||
// Trim any trailing whitespace, to put the closing `</$list_type>`
|
||||
@ -875,7 +875,7 @@ var _DoLists = function(text) {
|
||||
var list_type = (m3.search(/[*+-]/g)>-1) ? "ul" : "ol";
|
||||
// Turn double returns into triple returns, so that we can make a
|
||||
// paragraph for the last item in a list, if necessary:
|
||||
var list = list.replace(/\n{2,}/g,"\n\n\n");;
|
||||
list = list.replace(/\n{2,}/g,"\n\n\n");
|
||||
var result = _ProcessListItems(list);
|
||||
result = runup + "<"+list_type+">\n" + result + "</"+list_type+">\n";
|
||||
return result;
|
||||
@ -1451,4 +1451,4 @@ if (typeof define === 'function' && define.amd) {
|
||||
define(function() {
|
||||
return Showdown;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -9,12 +9,12 @@
|
||||
<title>Grafana</title>
|
||||
|
||||
[[if .User.LightTheme]]
|
||||
<link rel="stylesheet" href="[[.AppSubUrl]]/css/grafana.light.min.css">
|
||||
<link rel="stylesheet" href="[[.AppSubUrl]]/public/css/grafana.light.min.css">
|
||||
[[ range $css := .PluginCss ]]
|
||||
<link rel="stylesheet" href="[[$.AppSubUrl]]/[[ $css.Light ]]">
|
||||
[[ end ]]
|
||||
[[else]]
|
||||
<link rel="stylesheet" href="[[.AppSubUrl]]/css/grafana.dark.min.css">
|
||||
<link rel="stylesheet" href="[[.AppSubUrl]]/public/css/grafana.dark.min.css">
|
||||
[[ range $css := .PluginCss ]]
|
||||
<link rel="stylesheet" href="[[$.AppSubUrl]]/[[ $css.Dark ]]">
|
||||
[[ end ]]
|
||||
@ -22,10 +22,10 @@
|
||||
|
||||
|
||||
|
||||
<link rel="icon" type="image/png" href="[[.AppSubUrl]]/img/fav32.png">
|
||||
<link rel="icon" type="image/png" href="[[.AppSubUrl]]/public/img/fav32.png">
|
||||
<base href="[[.AppSubUrl]]/" />
|
||||
|
||||
<!-- build:js [[.AppSubUrl]]/app/app.js -->
|
||||
<!-- build:js [[.AppSubUrl]]/public/app/app.js -->
|
||||
<script src="[[.AppSubUrl]]/public/vendor/requirejs/require.js"></script>
|
||||
<script src="[[.AppSubUrl]]/public/app/require_config.js"></script>
|
||||
<!-- endbuild -->
|
||||
|
@ -13,7 +13,7 @@ module.exports = function(grunt) {
|
||||
'karma:test',
|
||||
'phantomjs',
|
||||
'css',
|
||||
'htmlmin:build',
|
||||
// 'htmlmin:build',
|
||||
'ngtemplates',
|
||||
'cssmin:build',
|
||||
'ngAnnotate:build',
|
||||
@ -34,8 +34,8 @@ module.exports = function(grunt) {
|
||||
|
||||
for(var key in summary){
|
||||
if(summary.hasOwnProperty(key)){
|
||||
var orig = key.replace(root, root+'/[[.AppSubUrl]]');
|
||||
var revved = summary[key].replace(root, root+'/[[.AppSubUrl]]');
|
||||
var orig = key.replace(root, root+'/[[.AppSubUrl]]/public');
|
||||
var revved = summary[key].replace(root, root+'/[[.AppSubUrl]]/public');
|
||||
fixed[orig] = revved;
|
||||
}
|
||||
}
|
||||
|
@ -27,7 +27,7 @@ module.exports = function(config) {
|
||||
js: {
|
||||
src: [
|
||||
'<%= tempDir %>/vendor/requirejs/require.js',
|
||||
'<%= tempDir %>/app/components/require.config.js',
|
||||
'<%= tempDir %>/app/require_config.js',
|
||||
'<%= tempDir %>/app/app.js',
|
||||
],
|
||||
dest: '<%= genDir %>/app/app.js'
|
||||
|
Loading…
Reference in New Issue
Block a user