Procházet zdrojové kódy

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

Conflicts:
	public/views/index.html
woodsaj před 10 roky
rodič
revize
fd392a2422
63 změnil soubory, kde provedl 1865 přidání a 630 odebrání
  1. 2 0
      CHANGELOG.md
  2. 5 3
      README.md
  3. 3 3
      build.go
  4. 1 1
      docs/sources/installation/debian.md
  5. 4 4
      docs/sources/reference/dashboard.md
  6. 32 34
      docs/sources/reference/http_api.md
  7. 10 0
      main.go
  8. 1 1
      package.json
  9. 15 3
      pkg/api/cloudwatch/cloudwatch.go
  10. 2 0
      pkg/api/dashboard.go
  11. 1 0
      pkg/api/dtos/models.go
  12. 20 16
      public/app/core/directives/metric_segment.js
  13. 19 0
      public/app/core/services/segment_srv.js
  14. 1 2
      public/app/core/settings.js
  15. 1 1
      public/app/core/store.js
  16. 6 0
      public/app/core/time_series.ts
  17. 39 0
      public/app/core/utils/flatten.ts
  18. 59 1
      public/app/core/utils/kbn.js
  19. 1 0
      public/app/features/admin/partials/orgs.html
  20. 50 8
      public/app/features/dashboard/dashboardSrv.js
  21. 1 1
      public/app/features/dashboard/partials/graphiteImport.html
  22. 3 3
      public/app/features/dashboard/timeSrv.js
  23. 1 1
      public/app/features/dashboard/timepicker/timepicker.ts
  24. 1 5
      public/app/features/dashboard/unsavedChangesSrv.js
  25. 1 1
      public/app/features/dashlinks/module.js
  26. 1 1
      public/app/features/org/partials/orgUsers.html
  27. 1 2
      public/app/features/panel/panel_menu.js
  28. 1 1
      public/app/features/templating/editorCtrl.js
  29. 0 11
      public/app/features/templating/templateValuesSrv.js
  30. 1 1
      public/app/plugins/datasource/cloudwatch/partials/query.editor.html
  31. 4 1
      public/app/plugins/datasource/elasticsearch/bucket_agg.js
  32. 1 1
      public/app/plugins/datasource/elasticsearch/partials/bucketAgg.html
  33. 3 19
      public/app/plugins/datasource/elasticsearch/query_ctrl.js
  34. 4 4
      public/app/plugins/datasource/influxdb/datasource.js
  35. 214 0
      public/app/plugins/datasource/influxdb/influx_query.ts
  36. 17 68
      public/app/plugins/datasource/influxdb/partials/query.editor.html
  37. 5 0
      public/app/plugins/datasource/influxdb/partials/query_part.html
  38. 2 74
      public/app/plugins/datasource/influxdb/query_builder.js
  39. 79 67
      public/app/plugins/datasource/influxdb/query_ctrl.js
  40. 432 0
      public/app/plugins/datasource/influxdb/query_part.ts
  41. 178 0
      public/app/plugins/datasource/influxdb/query_part_editor.js
  42. 216 0
      public/app/plugins/datasource/influxdb/specs/influx_query_specs.ts
  43. 5 113
      public/app/plugins/datasource/influxdb/specs/query_builder_specs.ts
  44. 41 0
      public/app/plugins/datasource/influxdb/specs/query_part_specs.ts
  45. 2 4
      public/app/plugins/datasource/prometheus/datasource.js
  46. 70 50
      public/app/plugins/panels/graph/axisEditor.html
  47. 5 0
      public/app/plugins/panels/graph/graph.tooltip.js
  48. 4 0
      public/app/plugins/panels/graph/legend.js
  49. 0 1
      public/app/plugins/panels/table/controller.ts
  50. 4 3
      public/app/plugins/panels/table/editor.html
  51. 87 79
      public/app/plugins/panels/table/editor.ts
  52. 8 6
      public/app/plugins/panels/table/module.ts
  53. 37 16
      public/app/plugins/panels/table/specs/transformers_specs.ts
  54. 13 6
      public/app/plugins/panels/table/transformers.ts
  55. 1 1
      public/dashboards/template_vars.json
  56. 24 0
      public/test/core/utils/flatten_specs.ts
  57. 21 0
      public/test/core/utils/kbn_specs.js
  58. 86 1
      public/test/specs/dashboardSrv-specs.js
  59. 8 1
      public/test/specs/time_srv_specs.js
  60. 3 3
      public/vendor/showdown.js
  61. 4 4
      public/views/index.html
  62. 3 3
      tasks/build_task.js
  63. 1 1
      tasks/options/concat.js

+ 2 - 0
CHANGELOG.md

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

+ 5 - 3
README.md

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

+ 3 - 3
build.go

@@ -328,9 +328,9 @@ func build(pkg string, tags []string) {
 func ldflags() string {
 func ldflags() string {
 	var b bytes.Buffer
 	var b bytes.Buffer
 	b.WriteString("-w")
 	b.WriteString("-w")
-	b.WriteString(fmt.Sprintf(" -X main.version %s", version))
-	b.WriteString(fmt.Sprintf(" -X main.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()
 	return b.String()
 }
 }
 
 

+ 1 - 1
docs/sources/installation/debian.md

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

+ 4 - 4
docs/sources/reference/dashboard.md

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

+ 32 - 34
docs/sources/reference/http_api.md

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

+ 10 - 0
main.go

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

+ 1 - 1
package.json

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

+ 15 - 3
pkg/api/cloudwatch/cloudwatch.go

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

+ 2 - 0
pkg/api/dashboard.go

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

+ 1 - 0
pkg/api/dtos/models.go

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

+ 20 - 16
public/app/core/directives/metric_segment.js

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

+ 19 - 0
public/app/core/services/segment_srv.js

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

+ 1 - 2
public/app/core/settings.js

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

+ 1 - 1
public/app/core/store.js

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

+ 6 - 0
public/app/core/time_series.ts

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

+ 39 - 0
public/app/core/utils/flatten.ts

@@ -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;

+ 59 - 1
public/app/core/utils/kbn.js

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

+ 1 - 0
public/app/features/admin/partials/orgs.html

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

+ 50 - 8
public/app/features/dashboard/dashboardSrv.js

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

+ 1 - 1
public/app/features/dashboard/partials/graphiteImport.html

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

+ 3 - 3
public/app/features/dashboard/timeSrv.js

@@ -90,11 +90,11 @@ define([
       timer.cancel(this.refresh_timer);
       timer.cancel(this.refresh_timer);
     };
     };
 
 
-    this.setTime = function(time) {
+    this.setTime = function(time, enableRefresh) {
       _.extend(this.time, time);
       _.extend(this.time, time);
 
 
-      // disable refresh if we have an absolute time
-      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.old_refresh = this.dashboard.refresh || this.old_refresh;
         this.setAutoRefresh(false);
         this.setAutoRefresh(false);
       }
       }

+ 1 - 1
public/app/features/dashboard/timepicker/timepicker.ts

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

+ 1 - 5
public/app/features/dashboard/unsavedChangesSrv.js

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

+ 1 - 1
public/app/features/dashlinks/module.js

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

+ 1 - 1
public/app/features/org/partials/orgUsers.html

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

+ 1 - 2
public/app/features/panel/panel_menu.js

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

+ 1 - 1
public/app/features/templating/editorCtrl.js

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

+ 0 - 11
public/app/features/templating/templateValuesSrv.js

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

+ 1 - 1
public/app/plugins/datasource/cloudwatch/partials/query.editor.html

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

+ 4 - 1
public/app/plugins/datasource/elasticsearch/bucket_agg.js

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

+ 1 - 1
public/app/plugins/datasource/elasticsearch/partials/bucketAgg.html

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

+ 3 - 19
public/app/plugins/datasource/elasticsearch/query_ctrl.js

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

+ 4 - 4
public/app/plugins/datasource/influxdb/datasource.js

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

+ 214 - 0
public/app/plugins/datasource/influxdb/influx_query.ts

@@ -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;

+ 17 - 68
public/app/plugins/datasource/influxdb/partials/query.editor.html

@@ -1,4 +1,4 @@
-<div class="tight-form-container-no-item-borders">
+<div class="">
 	<div  class="tight-form">
 	<div  class="tight-form">
 		<ul class="tight-form-list pull-right">
 		<ul class="tight-form-list pull-right">
 			<li ng-show="parserError" class="tight-form-item">
 			<li ng-show="parserError" class="tight-form-item">
@@ -48,98 +48,47 @@
 			<li>
 			<li>
 				<metric-segment segment="measurementSegment" get-options="getMeasurements()" on-change="measurementChanged()"></metric-segment>
 				<metric-segment segment="measurementSegment" get-options="getMeasurements()" on-change="measurementChanged()"></metric-segment>
 			</li>
 			</li>
+			<li class="tight-form-item query-keyword" style="padding-left: 15px; padding-right: 15px;">
+				WHERE
+			</li>
+			<li ng-repeat="segment in tagSegments">
+				<metric-segment segment="segment" get-options="getTagsOrValues(segment, $index)" on-change="tagSegmentUpdated(segment, $index)"></metric-segment>
+			</li>
 		</ul>
 		</ul>
 		<div class="clearfix"></div>
 		<div class="clearfix"></div>
 
 
 		<div style="padding: 10px" ng-if="target.rawQuery">
 		<div style="padding: 10px" ng-if="target.rawQuery">
 			<textarea ng-model="target.query" rows="8" spellcheck="false" style="width: 100%; box-sizing: border-box;" ng-blur="get_data()"></textarea>
 			<textarea ng-model="target.query" rows="8" spellcheck="false" style="width: 100%; box-sizing: border-box;" ng-blur="get_data()"></textarea>
 		</div>
 		</div>
+
 	</div>
 	</div>
 
 
 	<div ng-hide="target.rawQuery">
 	<div ng-hide="target.rawQuery">
-		<div class="tight-form">
-			<ul class="tight-form-list">
-				<li class="tight-form-item query-keyword tight-form-align" style="width: 75px;">
-					WHERE
-				</li>
-				<li ng-repeat="segment in tagSegments">
-					<metric-segment segment="segment" get-options="getTagsOrValues(segment, $index)" on-change="tagSegmentUpdated(segment, $index)"></metric-segment>
-				</li>
-			</ul>
-			<div class="clearfix"></div>
-		</div>
 
 
-		<div class="tight-form" ng-repeat="field in target.fields">
+		<div class="tight-form" ng-repeat="selectParts in queryModel.selectModels">
 			<ul class="tight-form-list">
 			<ul class="tight-form-list">
 				<li class="tight-form-item query-keyword tight-form-align" style="width: 75px;">
 				<li class="tight-form-item query-keyword tight-form-align" style="width: 75px;">
 					<span ng-show="$index === 0">SELECT</span>
 					<span ng-show="$index === 0">SELECT</span>
 				</li>
 				</li>
-				<li>
-					<metric-segment-model property="field.func" get-options="getFunctions()" on-change="get_data()" css-class="tight-form-item-xlarge"></metric-segment>
-				</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 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>
-				<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>
 				</li>
 			</ul>
 			</ul>
 			<div class="clearfix"></div>
 			<div class="clearfix"></div>
 		</div>
 		</div>
 
 
-		<div class="tight-form" ng-repeat="groupBy in target.groupBy">
+		<div class="tight-form">
 			<ul class="tight-form-list">
 			<ul class="tight-form-list">
 				<li class="tight-form-item query-keyword tight-form-align" style="width: 75px;">
 				<li class="tight-form-item query-keyword tight-form-align" style="width: 75px;">
 					<span ng-show="$index === 0">GROUP BY</span>
 					<span ng-show="$index === 0">GROUP BY</span>
 				</li>
 				</li>
-				<li ng-if="groupBy.type === 'time'">
-					<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>
-				<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>
 				</li>
 			</ul>
 			</ul>
 			<div class="clearfix"></div>
 			<div class="clearfix"></div>

+ 5 - 0
public/app/plugins/datasource/influxdb/partials/query_part.html

@@ -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>

+ 2 - 74
public/app/plugins/datasource/influxdb/query_builder.js

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

+ 79 - 67
public/app/plugins/datasource/influxdb/query_ctrl.js

@@ -2,32 +2,33 @@ define([
   'angular',
   'angular',
   'lodash',
   'lodash',
   './query_builder',
   './query_builder',
+  './influx_query',
+  './query_part',
+  './query_part_editor',
 ],
 ],
-function (angular, _, InfluxQueryBuilder) {
+function (angular, _, InfluxQueryBuilder, InfluxQuery, queryPart) {
   'use strict';
   'use strict';
 
 
   var module = angular.module('grafana.controllers');
   var module = angular.module('grafana.controllers');
 
 
-  module.controller('InfluxQueryCtrl', function($scope, $timeout, $sce, templateSrv, $q, uiSegmentSrv) {
+  module.controller('InfluxQueryCtrl', function($scope, templateSrv, $q, uiSegmentSrv) {
 
 
     $scope.init = function() {
     $scope.init = function() {
       if (!$scope.target) { return; }
       if (!$scope.target) { return; }
 
 
-      var target = $scope.target;
-      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();
         $scope.measurementSegment = uiSegmentSrv.newSelectMeasurement();
       } else {
       } else {
-        $scope.measurementSegment = uiSegmentSrv.newSegment(target.measurement);
+        $scope.measurementSegment = uiSegmentSrv.newSegment($scope.target.measurement);
       }
       }
 
 
       $scope.tagSegments = [];
       $scope.tagSegments = [];
-      _.each(target.tags, function(tag) {
+      _.each($scope.target.tags, function(tag) {
         if (!tag.operator) {
         if (!tag.operator) {
           if (/^\/.*\/$/.test(tag.value)) {
           if (/^\/.*\/$/.test(tag.value)) {
             tag.operator = "=~";
             tag.operator = "=~";
@@ -46,50 +47,81 @@ function (angular, _, InfluxQueryBuilder) {
       });
       });
 
 
       $scope.fixTagSegments();
       $scope.fixTagSegments();
+      $scope.buildSelectMenu();
       $scope.removeTagFilterSegment = uiSegmentSrv.newSegment({fake: true, value: '-- remove tag filter --'});
       $scope.removeTagFilterSegment = uiSegmentSrv.newSegment({fake: true, value: '-- remove tag filter --'});
     };
     };
 
 
-    $scope.fixTagSegments = function() {
-      var count = $scope.tagSegments.length;
-      var lastSegment = $scope.tagSegments[Math.max(count-1, 0)];
+    $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;
+      }, []);
+    };
 
 
-      if (!lastSegment || lastSegment.type !== 'plus-button') {
-        $scope.tagSegments.push(uiSegmentSrv.newPlusButton());
-      }
+    $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.addGroupBy = function() {
-      $scope.target.groupBy.push({type: 'tag', key: "select tag"});
+    $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.removeGroupBy = function(index) {
-      $scope.target.groupBy.splice(index, 1);
+    $scope.removeGroupByPart = function(part, index) {
+      $scope.queryModel.removeGroupByPart(part, index);
       $scope.get_data();
       $scope.get_data();
     };
     };
 
 
-    $scope.addSelect = function() {
-      $scope.target.fields.push({name: "select field", func: 'mean'});
+    $scope.addSelectPart = function(selectParts, cat, subitem) {
+      $scope.queryModel.addSelectPart(selectParts, subitem.value);
+      $scope.get_data();
     };
     };
 
 
-    $scope.removeSelect = function(index) {
-      $scope.target.fields.splice(index, 1);
+    $scope.removeSelectPart = function(selectParts, part) {
+      $scope.queryModel.removeSelectPart(selectParts, part);
       $scope.get_data();
       $scope.get_data();
     };
     };
 
 
-    $scope.changeFunction = function(func) {
-      $scope.target.function = func;
-      $scope.$parent.get_data();
+    $scope.selectPartUpdated = function() {
+      $scope.get_data();
     };
     };
 
 
-    $scope.measurementChanged = function() {
-      $scope.target.measurement = $scope.measurementSegment.value;
-      $scope.$parent.get_data();
+    $scope.fixTagSegments = function() {
+      var count = $scope.tagSegments.length;
+      var lastSegment = $scope.tagSegments[Math.max(count-1, 0)];
+
+      if (!lastSegment || lastSegment.type !== 'plus-button') {
+        $scope.tagSegments.push(uiSegmentSrv.newPlusButton());
+      }
     };
     };
 
 
-    $scope.getFields = function() {
-      var fieldsQuery = $scope.queryBuilder.buildExploreQuery('FIELDS');
-      return $scope.datasource.metricFindQuery(fieldsQuery)
-      .then($scope.transformToSegments(false), $scope.handleQueryError);
+    $scope.measurementChanged = function() {
+      $scope.target.measurement = $scope.measurementSegment.value;
+      $scope.get_data();
     };
     };
 
 
     $scope.toggleQueryMode = function () {
     $scope.toggleQueryMode = function () {
@@ -102,20 +134,17 @@ function (angular, _, InfluxQueryBuilder) {
       .then($scope.transformToSegments(true), $scope.handleQueryError);
       .then($scope.transformToSegments(true), $scope.handleQueryError);
     };
     };
 
 
-    $scope.getFunctions = function () {
-      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) {
     $scope.handleQueryError = function(err) {
@@ -179,25 +208,8 @@ function (angular, _, InfluxQueryBuilder) {
       .then(null, $scope.handleQueryError);
       .then(null, $scope.handleQueryError);
     };
     };
 
 
-    $scope.addField = function() {
-      $scope.target.fields.push({name: $scope.addFieldSegment.value, func: 'mean'});
-      _.extend($scope.addFieldSegment, uiSegmentSrv.newPlusButton());
-    };
-
-    $scope.fieldChanged = function(field) {
-      if (field.name === '-- remove from select --') {
-        $scope.target.fields = _.without($scope.target.fields, field);
-      }
-      $scope.get_data();
-    };
-
     $scope.getTagOptions = function() {
     $scope.getTagOptions = function() {
-      var query = $scope.queryBuilder.buildExploreQuery('TAG_KEYS');
-
-      return $scope.datasource.metricFindQuery(query)
-      .then($scope.transformToSegments(false))
-      .then(null, $scope.handleQueryError);
-    };
+   };
 
 
     $scope.setFill = function(fill) {
     $scope.setFill = function(fill) {
       $scope.target.fill = fill;
       $scope.target.fill = fill;

+ 432 - 0
public/app/plugins/datasource/influxdb/query_part.ts

@@ -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 - 0
public/app/plugins/datasource/influxdb/query_part_editor.js

@@ -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();
+        }
+      };
+
+    });
+
+});

+ 216 - 0
public/app/plugins/datasource/influxdb/specs/influx_query_specs.ts

@@ -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');
+    });
+
+  });
+
+});

+ 5 - 113
public/app/plugins/datasource/influxdb/specs/query_builder_specs.ts

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

+ 41 - 0
public/app/plugins/datasource/influxdb/specs/query_part_specs.ts

@@ -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"');
+    });
+
+  });
+
+});

+ 2 - 4
public/app/plugins/datasource/prometheus/datasource.js

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

+ 70 - 50
public/app/plugins/panels/graph/axisEditor.html

@@ -6,7 +6,7 @@
 				<li class="tight-form-item" style="width: 80px">
 				<li class="tight-form-item" style="width: 80px">
 					Left Y
 					Left Y
 				</li>
 				</li>
-				<li class="tight-form-item">
+				<li class="tight-form-item" style="width: 40px">
 					Unit
 					Unit
 				</li>
 				</li>
 				<li class="dropdown" style="width: 140px;"
 				<li class="dropdown" style="width: 140px;"
@@ -15,43 +15,51 @@
 					dropdown-typeahead-on-select="setUnitFormat(0, $subItem)">
 					dropdown-typeahead-on-select="setUnitFormat(0, $subItem)">
 				</li>
 				</li>
 				<li class="tight-form-item">
 				<li class="tight-form-item">
-					&nbsp;&nbsp; Grid Max
+					Scale type
 				</li>
 				</li>
 				<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>
+					<select class="input-small tight-form-input" style="width: 113px" ng-model="panel.grid.leftLogBase" ng-options="v as k for (k, v) in logScales" ng-change="render()"></select>
 				</li>
 				</li>
 				<li class="tight-form-item">
 				<li class="tight-form-item">
-					Min
+					Label
 				</li>
 				</li>
 				<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>
+					<input type="text" class="input-small tight-form-input last"
+					ng-model="panel.leftYAxisLabel" 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">
+					<i class="fa fa-remove invisible"></i>
 				</li>
 				</li>
 				<li class="tight-form-item">
 				<li class="tight-form-item">
-					Scale type
+					Y-Max
 				</li>
 				</li>
 				<li>
 				<li>
-					<select class="input-small tight-form-input" style="width: 113px" ng-model="panel.grid.leftLogBase" ng-options="v as k for (k, v) in logScales" ng-change="render()"></select>
+					<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>
-				<li class="tight-form-item">
-					Label
+				<li class="tight-form-item" style="width: 115px; text-align: right;">
+					Y-Min
 				</li>
 				</li>
 				<li>
 				<li>
-					<input type="text" class="input-small tight-form-input last"
-					ng-model="panel.leftYAxisLabel" ng-change="render()" ng-model-onblur>
+					<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>
 				</li>
-			</ul>
+				</ul>
 			<div class="clearfix"></div>
 			<div class="clearfix"></div>
 		</div>
 		</div>
-		<div class="tight-form last">
+		<div class="tight-form">
 			<ul class="tight-form-list">
 			<ul class="tight-form-list">
 				<li class="tight-form-item" style="width: 80px">
 				<li class="tight-form-item" style="width: 80px">
 					Right Y
 					Right Y
 				</li>
 				</li>
-				<li class="tight-form-item">
+				<li class="tight-form-item" style="width: 40px">
 					Unit
 					Unit
 				</li>
 				</li>
 				<li class="dropdown" style="width: 140px"
 				<li class="dropdown" style="width: 140px"
@@ -60,37 +68,46 @@
 					dropdown-typeahead-on-select="setUnitFormat(1, $subItem)">
 					dropdown-typeahead-on-select="setUnitFormat(1, $subItem)">
 				</li>
 				</li>
 				<li class="tight-form-item">
 				<li class="tight-form-item">
-					&nbsp;&nbsp; Grid Max
+					Scale type
 				</li>
 				</li>
 				<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>
+					<select class="input-small tight-form-input" style="width: 113px" ng-model="panel.grid.rightLogBase" ng-options="v as k for (k, v) in logScales" ng-change="render()"></select>
 				</li>
 				</li>
 				<li class="tight-form-item">
 				<li class="tight-form-item">
-					Min
+					Label
 				</li>
 				</li>
 				<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>
+					<input type="text" class="input-small tight-form-input last"
+					ng-model="panel.rightYAxisLabel" ng-change="render()" ng-model-onblur>
+				</li>
+			</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>
 				<li class="tight-form-item">
 				<li class="tight-form-item">
-					Scale type
+					Y-Max
 				</li>
 				</li>
 				<li>
 				<li>
-					<select class="input-small tight-form-input" style="width: 113px" ng-model="panel.grid.rightLogBase" ng-options="v as k for (k, v) in logScales" ng-change="render()"></select>
+					<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>
-				<li class="tight-form-item">
-					Label
+				<li class="tight-form-item" style="width: 115px; text-align: right;">
+					Y-Min
 				</li>
 				</li>
 				<li>
 				<li>
-					<input type="text" class="input-small tight-form-input last"
-					ng-model="panel.rightYAxisLabel" ng-change="render()" ng-model-onblur>
+					<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>
 				</li>
 			</ul>
 			</ul>
 			<div class="clearfix"></div>
 			<div class="clearfix"></div>
 		</div>
 		</div>
+
 	</div>
 	</div>
 
 
 	<div class="section" style="margin-bottom: 20px">
 	<div class="section" style="margin-bottom: 20px">
@@ -150,9 +167,9 @@
 
 
 <div class="editor-row">
 <div class="editor-row">
 	<div class="section">
 	<div class="section">
-		<div class="tight-form last">
+		<div class="tight-form">
 			<ul class="tight-form-list">
 			<ul class="tight-form-list">
-				<li class="tight-form-item" style="width: 110px">
+				<li class="tight-form-item" style="width: 80px">
 					Legend
 					Legend
 				</li>
 				</li>
 				<li class="tight-form-item">
 				<li class="tight-form-item">
@@ -164,18 +181,28 @@
 				<li class="tight-form-item">
 				<li class="tight-form-item">
 					<editor-checkbox text="Right side" model="panel.legend.rightSide" change="render()"></editor-checkbox>
 					<editor-checkbox text="Right side" model="panel.legend.rightSide" change="render()"></editor-checkbox>
 				</li>
 				</li>
+			</ul>
+			<div class="clearfix"></div>
+		</div>
+		<div class="tight-form">
+			<ul class="tight-form-list">
+				<li class="tight-form-item" style="width: 80px">
+					Hide series
+				</li>
+				<li class="tight-form-item">
+					<editor-checkbox text="With only nulls" model="panel.legend.hideEmpty" change="render()"></editor-checkbox>
+				</li>
 				<li class="tight-form-item last">
 				<li class="tight-form-item last">
-					<editor-checkbox text="Hide empty" model="panel.legend.hideEmpty" change="render()"></editor-checkbox>
+					<editor-checkbox text="With only zeroes" model="panel.legend.hideZero" change="render()"></editor-checkbox>
 				</li>
 				</li>
 			</ul>
 			</ul>
 			<div class="clearfix"></div>
 			<div class="clearfix"></div>
 		</div>
 		</div>
-	</div>
-	<div class="section">
-		<div class="tight-form">
+
+		<div class="tight-form last">
 			<ul class="tight-form-list">
 			<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>
 				<li class="tight-form-item">
 				<li class="tight-form-item">
 					<editor-checkbox text="Min" model="panel.legend.min" change="legendValuesOptionChanged()"></editor-checkbox>
 					<editor-checkbox text="Min" model="panel.legend.min" change="legendValuesOptionChanged()"></editor-checkbox>
@@ -189,16 +216,11 @@
 				<li class="tight-form-item">
 				<li class="tight-form-item">
 					<editor-checkbox text="Current" model="panel.legend.current" change="legendValuesOptionChanged()"></editor-checkbox>
 					<editor-checkbox text="Current" model="panel.legend.current" change="legendValuesOptionChanged()"></editor-checkbox>
 				</li>
 				</li>
-				<li class="tight-form-item last">
+				<li class="tight-form-item">
 					<editor-checkbox text="Total" model="panel.legend.total" change="legendValuesOptionChanged()"></editor-checkbox>
 					<editor-checkbox text="Total" model="panel.legend.total" change="legendValuesOptionChanged()"></editor-checkbox>
 				</li>
 				</li>
-			</ul>
-			<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>
 				<li style="width: 105px">
 				<li style="width: 105px">
 					<input type="number" class="input-small tight-form-input" placeholder="auto" bs-tooltip="'Override automatic decimal precision for legend and tooltips'" data-placement="right"
 					<input type="number" class="input-small tight-form-input" placeholder="auto" bs-tooltip="'Override automatic decimal precision for legend and tooltips'" data-placement="right"
@@ -207,7 +229,5 @@
 			</ul>
 			</ul>
 			<div class="clearfix"></div>
 			<div class="clearfix"></div>
 		</div>
 		</div>
-
-
 	</div>
 	</div>
 </div>
 </div>

+ 5 - 0
public/app/plugins/panels/graph/graph.tooltip.js

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

+ 4 - 0
public/app/plugins/panels/graph/legend.js

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

+ 0 - 1
public/app/plugins/panels/table/controller.ts

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

+ 4 - 3
public/app/plugins/panels/table/editor.html

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

+ 87 - 79
public/app/plugins/panels/table/editor.ts

@@ -8,93 +8,101 @@ import moment = require('moment');
 
 
 import {transformers} from './transformers';
 import {transformers} from './transformers';
 
 
-export function tablePanelEditor() {
-  '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);
-      });
+export class TablePanelEditorCtrl {
 
 
-      scope.addColumn = function(menuItem) {
-        scope.panel.columns.push({text: menuItem.text, value: menuItem.value});
-        scope.render();
-      };
+  /** @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.transformChanged = function() {
-        scope.panel.columns = [];
-        scope.updateColumnsMenu();
-        scope.render();
-      };
+    $scope.addColumnSegment = uiSegmentSrv.newPlusButton();
 
 
-      scope.removeColumn = function(column) {
-        scope.panel.columns = _.without(scope.panel.columns, column);
-        scope.render();
-      };
+    $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.setUnitFormat = function(column, subItem) {
-        column.unit = subItem.value;
-        scope.render();
-      };
+    $scope.addColumn = function() {
+      $scope.panel.columns.push({text: $scope.addColumnSegment.value, value: $scope.addColumnSegment.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));
-      };
+      var plusButton = uiSegmentSrv.newPlusButton();
+      $scope.addColumnSegment.html = plusButton.html;
+    };
 
 
-      scope.removeColumnStyle = function(style) {
-        scope.panel.styles = _.without(scope.panel.styles, style);
-      };
+    $scope.transformChanged = function() {
+      $scope.panel.columns = [];
+      $scope.render();
+    };
+
+    $scope.removeColumn = function(column) {
+      $scope.panel.columns = _.without($scope.panel.columns, column);
+      $scope.render();
+    };
 
 
-      scope.getColumnNames = function() {
-        if (!scope.table) {
-          return [];
-        }
-        return _.map(scope.table.columns, function(col: any) {
-          return col.text;
-        });
+    $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.updateColumnsMenu(scope.dataRaw);
-    }
-  };
+      $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/panels/table/editor.html',
+    controller: TablePanelEditorCtrl,
+  };
+}

+ 8 - 6
public/app/plugins/panels/table/module.ts

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

+ 37 - 16
public/app/plugins/panels/table/specs/transformers_specs.ts

@@ -1,6 +1,7 @@
 import {describe, beforeEach, it, sinon, expect} from 'test/lib/common';
 import {describe, beforeEach, it, sinon, expect} from 'test/lib/common';
 
 
 import {TableModel} from '../table_model';
 import {TableModel} from '../table_model';
+import {transformers} from '../transformers';
 
 
 describe('when transforming time series table', () => {
 describe('when transforming time series table', () => {
   var table;
   var table;
@@ -100,7 +101,11 @@ describe('when transforming time series table', () => {
     describe('JSON Data', () => {
     describe('JSON Data', () => {
       var panel = {
       var panel = {
         transform: 'json',
         transform: 'json',
-        columns: [{text: 'Timestamp', value: 'timestamp'}, {text: 'Message', value: 'message'}]
+        columns: [
+          {text: 'Timestamp', value: 'timestamp'},
+          {text: 'Message', value: 'message'},
+          {text: 'nested.level2', value: 'nested.level2'},
+        ]
       };
       };
       var rawData = [
       var rawData = [
         {
         {
@@ -108,26 +113,42 @@ describe('when transforming time series table', () => {
           datapoints: [
           datapoints: [
             {
             {
               timestamp: 'time',
               timestamp: 'time',
-              message: 'message'
+              message: 'message',
+              nested: {
+                level2: 'level2-value'
+              }
             }
             }
           ]
           ]
         }
         }
       ];
       ];
 
 
-      beforeEach(() => {
-        table = TableModel.transform(rawData, panel);
-      });
-
-      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');
-      });
-
-      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');
+      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');
+        });
+      });
+
+      describe('transform', function() {
+        beforeEach(() => {
+          table = TableModel.transform(rawData, panel);
+        });
+
+        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');
+        });
       });
       });
     });
     });
 
 

+ 13 - 6
public/app/plugins/panels/table/transformers.ts

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

+ 1 - 1
public/dashboards/template_vars.json

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

+ 24 - 0
public/test/core/utils/flatten_specs.ts

@@ -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');
+  });
+
+});
+

+ 21 - 0
public/test/core/utils/kbn_specs.js

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

+ 86 - 1
public/test/specs/dashboardSrv-specs.js

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

+ 8 - 1
public/test/specs/time_srv_specs.js

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

+ 3 - 3
public/vendor/showdown.js

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

+ 4 - 4
public/views/index.html

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

+ 3 - 3
tasks/build_task.js

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

+ 1 - 1
tasks/options/concat.js

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