Browse Source

Merge remote-tracking branch 'origin/master' into develop

Torkel Ödegaard 8 years ago
parent
commit
beb9f8ee74
42 changed files with 478 additions and 196 deletions
  1. 2 0
      .gitignore
  2. 1 1
      CHANGELOG.md
  3. 198 10
      LICENSE.md
  4. 3 13
      NOTICE.md
  5. 3 3
      build.go
  6. 1 0
      docker/blocks/mysql_tests/docker-compose.yaml
  7. 1 0
      docker/blocks/postgres_tests/docker-compose.yaml
  8. 2 1
      docs/sources/administration/provisioning.md
  9. 6 0
      docs/sources/features/datasources/mysql.md
  10. 6 0
      docs/sources/features/datasources/postgres.md
  11. 4 1
      docs/sources/installation/configuration.md
  12. 1 1
      packaging/rpm/systemd/grafana-server.service
  13. 5 1
      pkg/api/render.go
  14. 28 0
      pkg/cmd/grafana-server/server.go
  15. 1 0
      pkg/log/file_test.go
  16. 0 2
      pkg/services/alerting/extractor_test.go
  17. 2 1
      pkg/services/alerting/notifiers/slack.go
  18. 1 1
      pkg/services/sqlstore/user.go
  19. 2 2
      pkg/tsdb/graphite/graphite.go
  20. 2 2
      pkg/tsdb/graphite/graphite_test.go
  21. 0 2
      pkg/tsdb/influxdb/model_parser_test.go
  22. 1 0
      pkg/tsdb/influxdb/response_parser.go
  23. 32 1
      pkg/tsdb/postgres/postgres.go
  24. 8 3
      pkg/util/url.go
  25. 4 0
      public/app/core/config.ts
  26. 0 9
      public/app/core/controllers/all.js
  27. 7 0
      public/app/core/controllers/all.ts
  28. 8 11
      public/app/core/controllers/error_ctrl.ts
  29. 8 11
      public/app/core/controllers/invited_ctrl.ts
  30. 8 10
      public/app/core/controllers/json_editor_ctrl.ts
  31. 13 16
      public/app/core/controllers/login_ctrl.ts
  32. 9 11
      public/app/core/controllers/reset_password_ctrl.ts
  33. 1 1
      public/app/features/dashboard/export/export_modal.ts
  34. 9 16
      public/app/features/dashboard/shareModalCtrl.ts
  35. 0 1
      public/app/plugins/datasource/elasticsearch/query_builder.ts
  36. 0 1
      public/app/plugins/datasource/influxdb/influx_query.ts
  37. 12 3
      public/app/plugins/datasource/mysql/datasource.ts
  38. 12 3
      public/app/plugins/datasource/postgres/datasource.ts
  39. 2 2
      public/app/plugins/datasource/prometheus/metric_find_query.ts
  40. 53 56
      public/app/plugins/datasource/prometheus/partials/query.editor.html
  41. 4 0
      public/app/plugins/datasource/prometheus/plugin.json
  42. 18 0
      public/app/plugins/datasource/prometheus/specs/metric_find_query_specs.ts

+ 2 - 0
.gitignore

@@ -39,6 +39,8 @@ conf/custom.ini
 fig.yml
 docker-compose.yml
 docker-compose.yaml
+/conf/dashboards/custom.yaml
+/conf/datasources/custom.yaml
 profile.cov
 /grafana
 .notouch

+ 1 - 1
CHANGELOG.md

@@ -24,7 +24,7 @@
 * **Cloudwatch**: Fixes broken query inspector for cloudwatch [#9661](https://github.com/grafana/grafana/issues/9661), thx [@mtanda](https://github.com/mtanda)
 * **Dashboard**: Make it possible to start dashboards from search and dashboard list panel [#1871](https://github.com/grafana/grafana/issues/1871)
 * **Annotations**: Posting annotations now return the id of the annotation [#9798](https://github.com/grafana/grafana/issues/9798)
-
+* **Systemd**: Use systemd notification ready flag [#10024](https://github.com/grafana/grafana/issues/10024), thx [@jgrassler](https://github.com/jgrassler)
 ## Tech
 * **RabbitMq**: Remove support for publishing events to RabbitMQ [#9645](https://github.com/grafana/grafana/issues/9645)
 

+ 198 - 10
LICENSE.md

@@ -1,14 +1,202 @@
-Copyright 2014-2017 Torkel Ödegaard, Raintank Inc.
 
-Licensed under the Apache License, Version 2.0 (the "License"); you
-may not use this file except in compliance with the License. You may
-obtain a copy of the License at
+                                 Apache License
+                           Version 2.0, January 2004
+                        http://www.apache.org/licenses/
 
-    http://www.apache.org/licenses/LICENSE-2.0
+   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
 
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
-implied. See the License for the specific language governing
-permissions and limitations under the License.
+   1. Definitions.
 
+      "License" shall mean the terms and conditions for use, reproduction,
+      and distribution as defined by Sections 1 through 9 of this document.
+
+      "Licensor" shall mean the copyright owner or entity authorized by
+      the copyright owner that is granting the License.
+
+      "Legal Entity" shall mean the union of the acting entity and all
+      other entities that control, are controlled by, or are under common
+      control with that entity. For the purposes of this definition,
+      "control" means (i) the power, direct or indirect, to cause the
+      direction or management of such entity, whether by contract or
+      otherwise, or (ii) ownership of fifty percent (50%) or more of the
+      outstanding shares, or (iii) beneficial ownership of such entity.
+
+      "You" (or "Your") shall mean an individual or Legal Entity
+      exercising permissions granted by this License.
+
+      "Source" form shall mean the preferred form for making modifications,
+      including but not limited to software source code, documentation
+      source, and configuration files.
+
+      "Object" form shall mean any form resulting from mechanical
+      transformation or translation of a Source form, including but
+      not limited to compiled object code, generated documentation,
+      and conversions to other media types.
+
+      "Work" shall mean the work of authorship, whether in Source or
+      Object form, made available under the License, as indicated by a
+      copyright notice that is included in or attached to the work
+      (an example is provided in the Appendix below).
+
+      "Derivative Works" shall mean any work, whether in Source or Object
+      form, that is based on (or derived from) the Work and for which the
+      editorial revisions, annotations, elaborations, or other modifications
+      represent, as a whole, an original work of authorship. For the purposes
+      of this License, Derivative Works shall not include works that remain
+      separable from, or merely link (or bind by name) to the interfaces of,
+      the Work and Derivative Works thereof.
+
+      "Contribution" shall mean any work of authorship, including
+      the original version of the Work and any modifications or additions
+      to that Work or Derivative Works thereof, that is intentionally
+      submitted to Licensor for inclusion in the Work by the copyright owner
+      or by an individual or Legal Entity authorized to submit on behalf of
+      the copyright owner. For the purposes of this definition, "submitted"
+      means any form of electronic, verbal, or written communication sent
+      to the Licensor or its representatives, including but not limited to
+      communication on electronic mailing lists, source code control systems,
+      and issue tracking systems that are managed by, or on behalf of, the
+      Licensor for the purpose of discussing and improving the Work, but
+      excluding communication that is conspicuously marked or otherwise
+      designated in writing by the copyright owner as "Not a Contribution."
+
+      "Contributor" shall mean Licensor and any individual or Legal Entity
+      on behalf of whom a Contribution has been received by Licensor and
+      subsequently incorporated within the Work.
+
+   2. Grant of Copyright License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      copyright license to reproduce, prepare Derivative Works of,
+      publicly display, publicly perform, sublicense, and distribute the
+      Work and such Derivative Works in Source or Object form.
+
+   3. Grant of Patent License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      (except as stated in this section) patent license to make, have made,
+      use, offer to sell, sell, import, and otherwise transfer the Work,
+      where such license applies only to those patent claims licensable
+      by such Contributor that are necessarily infringed by their
+      Contribution(s) alone or by combination of their Contribution(s)
+      with the Work to which such Contribution(s) was submitted. If You
+      institute patent litigation against any entity (including a
+      cross-claim or counterclaim in a lawsuit) alleging that the Work
+      or a Contribution incorporated within the Work constitutes direct
+      or contributory patent infringement, then any patent licenses
+      granted to You under this License for that Work shall terminate
+      as of the date such litigation is filed.
+
+   4. Redistribution. You may reproduce and distribute copies of the
+      Work or Derivative Works thereof in any medium, with or without
+      modifications, and in Source or Object form, provided that You
+      meet the following conditions:
+
+      (a) You must give any other recipients of the Work or
+          Derivative Works a copy of this License; and
+
+      (b) You must cause any modified files to carry prominent notices
+          stating that You changed the files; and
+
+      (c) You must retain, in the Source form of any Derivative Works
+          that You distribute, all copyright, patent, trademark, and
+          attribution notices from the Source form of the Work,
+          excluding those notices that do not pertain to any part of
+          the Derivative Works; and
+
+      (d) If the Work includes a "NOTICE" text file as part of its
+          distribution, then any Derivative Works that You distribute must
+          include a readable copy of the attribution notices contained
+          within such NOTICE file, excluding those notices that do not
+          pertain to any part of the Derivative Works, in at least one
+          of the following places: within a NOTICE text file distributed
+          as part of the Derivative Works; within the Source form or
+          documentation, if provided along with the Derivative Works; or,
+          within a display generated by the Derivative Works, if and
+          wherever such third-party notices normally appear. The contents
+          of the NOTICE file are for informational purposes only and
+          do not modify the License. You may add Your own attribution
+          notices within Derivative Works that You distribute, alongside
+          or as an addendum to the NOTICE text from the Work, provided
+          that such additional attribution notices cannot be construed
+          as modifying the License.
+
+      You may add Your own copyright statement to Your modifications and
+      may provide additional or different license terms and conditions
+      for use, reproduction, or distribution of Your modifications, or
+      for any such Derivative Works as a whole, provided Your use,
+      reproduction, and distribution of the Work otherwise complies with
+      the conditions stated in this License.
+
+   5. Submission of Contributions. Unless You explicitly state otherwise,
+      any Contribution intentionally submitted for inclusion in the Work
+      by You to the Licensor shall be under the terms and conditions of
+      this License, without any additional terms or conditions.
+      Notwithstanding the above, nothing herein shall supersede or modify
+      the terms of any separate license agreement you may have executed
+      with Licensor regarding such Contributions.
+
+   6. Trademarks. This License does not grant permission to use the trade
+      names, trademarks, service marks, or product names of the Licensor,
+      except as required for reasonable and customary use in describing the
+      origin of the Work and reproducing the content of the NOTICE file.
+
+   7. Disclaimer of Warranty. Unless required by applicable law or
+      agreed to in writing, Licensor provides the Work (and each
+      Contributor provides its Contributions) on an "AS IS" BASIS,
+      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+      implied, including, without limitation, any warranties or conditions
+      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+      PARTICULAR PURPOSE. You are solely responsible for determining the
+      appropriateness of using or redistributing the Work and assume any
+      risks associated with Your exercise of permissions under this License.
+
+   8. Limitation of Liability. In no event and under no legal theory,
+      whether in tort (including negligence), contract, or otherwise,
+      unless required by applicable law (such as deliberate and grossly
+      negligent acts) or agreed to in writing, shall any Contributor be
+      liable to You for damages, including any direct, indirect, special,
+      incidental, or consequential damages of any character arising as a
+      result of this License or out of the use or inability to use the
+      Work (including but not limited to damages for loss of goodwill,
+      work stoppage, computer failure or malfunction, or any and all
+      other commercial damages or losses), even if such Contributor
+      has been advised of the possibility of such damages.
+
+   9. Accepting Warranty or Additional Liability. While redistributing
+      the Work or Derivative Works thereof, You may choose to offer,
+      and charge a fee for, acceptance of support, warranty, indemnity,
+      or other liability obligations and/or rights consistent with this
+      License. However, in accepting such obligations, You may act only
+      on Your own behalf and on Your sole responsibility, not on behalf
+      of any other Contributor, and only if You agree to indemnify,
+      defend, and hold each Contributor harmless for any liability
+      incurred by, or claims asserted against, such Contributor by reason
+      of your accepting any such warranty or additional liability.
+
+   END OF TERMS AND CONDITIONS
+
+   APPENDIX: How to apply the Apache License to your work.
+
+      To apply the Apache License to your work, attach the following
+      boilerplate notice, with the fields enclosed by brackets "[]"
+      replaced with your own identifying information. (Don't include
+      the brackets!)  The text should be enclosed in the appropriate
+      comment syntax for the file format. We also recommend that a
+      file or class name and description of purpose be included on the
+      same "printed page" as the copyright notice for easier
+      identification within third-party archives.
+
+   Copyright [yyyy] [name of copyright owner]
+
+   Licensed under the Apache License, Version 2.0 (the "License");
+   you may not use this file except in compliance with the License.
+   You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+   Unless required by applicable law or agreed to in writing, software
+   distributed under the License is distributed on an "AS IS" BASIS,
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+   See the License for the specific language governing permissions and
+   limitations under the License.

+ 3 - 13
NOTICE.md

@@ -1,16 +1,6 @@
 
-This software is based on Kibana:
-========================================
-Copyright 2012-2013 Elasticsearch BV
-
-Licensed under the Apache License, Version 2.0 (the "License"); you
-may not use this file except in compliance with the License. You may
-obtain a copy of the License at
+Copyright 2014-2017 Grafana Labs
 
-    http://www.apache.org/licenses/LICENSE-2.0
+This software is based on Kibana: 
+Copyright 2012-2013 Elasticsearch BV
 
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
-implied. See the License for the specific language governing
-permissions and limitations under the License.

+ 3 - 3
build.go

@@ -95,9 +95,9 @@ func main() {
 
 		case "package":
 			grunt(gruntBuildArg("release")...)
-      if runtime.GOOS != "windows" {
-			  createLinuxPackages()
-      }
+			if runtime.GOOS != "windows" {
+				createLinuxPackages()
+			}
 
 		case "pkg-rpm":
 			grunt(gruntBuildArg("release")...)

+ 1 - 0
docker/blocks/mysql_tests/docker-compose.yaml

@@ -7,3 +7,4 @@
       MYSQL_PASSWORD: password
     ports:
       - "3306:3306"
+    tmpfs: /var/lib/mysql:rw

+ 1 - 0
docker/blocks/postgres_tests/docker-compose.yaml

@@ -5,3 +5,4 @@
       POSTGRES_PASSWORD: grafanatest
     ports:
       - "5432:5432"
+    tmpfs: /var/lib/postgresql/data:rw

+ 2 - 1
docs/sources/administration/provisioning.md

@@ -65,13 +65,14 @@ Currently we do not provide any scripts/manifests for configuring Grafana. Rathe
 Tool | Project
 -----|------------
 Puppet | [https://forge.puppet.com/puppet/grafana](https://forge.puppet.com/puppet/grafana)
+Ansible | [https://github.com/cloudalchemy/ansible-grafana](https://github.com/cloudalchemy/ansible-grafana)
 Ansible | [https://github.com/picotrading/ansible-grafana](https://github.com/picotrading/ansible-grafana)
 Chef | [https://github.com/JonathanTron/chef-grafana](https://github.com/JonathanTron/chef-grafana)
 Saltstack | [https://github.com/salt-formulas/salt-formula-grafana](https://github.com/salt-formulas/salt-formula-grafana)
 
 ## Datasources 
 
-> This feature is available from v4.7
+> This feature is available from v5.0
 
 It's possible to manage datasources in Grafana by adding one or more yaml config files in the [`conf/datasources`](/installation/configuration/#datasources) directory. Each config file can contain a list of `datasources` that will be added or updated during start up. If the datasource already exists, Grafana will update it to match the configuration file. The config file can also contain a list of datasources that should be deleted. That list is called `delete_datasources`. Grafana will delete datasources listed in `delete_datasources` before inserting/updating those in the `datasource` list.
 

+ 6 - 0
docs/sources/features/datasources/mysql.md

@@ -127,6 +127,12 @@ A query can returns multiple columns and Grafana will automatically create a lis
 SELECT my_host.hostname, my_other_host.hostname2 FROM my_host JOIN my_other_host ON my_host.city = my_other_host.city
 ```
 
+To use time range dependent macros like `$__timeFilter(column)` in your query the refresh mode of the template variable needs to be set to *On Time Range Change*.
+
+```sql
+SELECT event_name FROM event_log WHERE $__timeFilter(time_column)
+```
+
 Another option is a query that can create a key/value variable. The query should return two columns that are named `__text` and `__value`. The `__text` column value should be unique (if it is not unique then the first value is used). The options in the dropdown will have a text and value that allows you to have a friendly name as text and an id as the value. An example query with `hostname` as the text and `id` as the value:
 
 ```sql

+ 6 - 0
docs/sources/features/datasources/postgres.md

@@ -139,6 +139,12 @@ A query can return multiple columns and Grafana will automatically create a list
 SELECT host.hostname, other_host.hostname2 FROM host JOIN other_host ON host.city = other_host.city
 ```
 
+To use time range dependent macros like `$__timeFilter(column)` in your query the refresh mode of the template variable needs to be set to *On Time Range Change*.
+
+```sql
+SELECT event_name FROM event_log WHERE $__timeFilter(time_column)
+```
+
 Another option is a query that can create a key/value variable. The query should return two columns that are named `__text` and `__value`. The `__text` column value should be unique (if it is not unique then the first value is used). The options in the dropdown will have a text and value that allows you to have a friendly name as text and an id as the value. An example query with `hostname` as the text and `id` as the value:
 
 ```sql

+ 4 - 1
docs/sources/installation/configuration.md

@@ -93,7 +93,10 @@ Directory where grafana will automatically scan and look for plugins
 
 ### datasources
 
-Config files containing datasources that will be configured at startup
+> This feature is available in 5.0+
+
+Config files containing datasources that will be configured at startup. 
+You can read more about the config files at the [provisioning page](/administration/provisioning/#datasources).
 
 ## [server]
 

+ 1 - 1
packaging/rpm/systemd/grafana-server.service

@@ -9,7 +9,7 @@ After=postgresql.service mariadb.service mysql.service
 EnvironmentFile=/etc/sysconfig/grafana-server
 User=grafana
 Group=grafana
-Type=simple
+Type=notify
 Restart=on-failure
 WorkingDirectory=/usr/share/grafana
 RuntimeDirectory=grafana

+ 5 - 1
pkg/api/render.go

@@ -10,7 +10,11 @@ import (
 )
 
 func RenderToPng(c *middleware.Context) {
-	queryReader := util.NewUrlQueryReader(c.Req.URL)
+	queryReader, err := util.NewUrlQueryReader(c.Req.URL)
+	if err != nil {
+		c.Handle(400, "Render parameters error", err)
+		return
+	}
 	queryParams := fmt.Sprintf("?%s", c.Req.URL.RawQuery)
 
 	renderOpts := &renderer.RenderOpts{

+ 28 - 0
pkg/cmd/grafana-server/server.go

@@ -3,7 +3,9 @@ package main
 import (
 	"context"
 	"flag"
+	"fmt"
 	"io/ioutil"
+	"net"
 	"os"
 	"path/filepath"
 	"strconv"
@@ -96,6 +98,7 @@ func (g *GrafanaServerImpl) Start() {
 		return
 	}
 
+	SendSystemdNotification("READY=1")
 	g.startHttpServer()
 }
 
@@ -169,3 +172,28 @@ func (g *GrafanaServerImpl) writePIDFile() {
 
 	g.log.Info("Writing PID file", "path", *pidFile, "pid", pid)
 }
+
+func SendSystemdNotification(state string) error {
+	notifySocket := os.Getenv("NOTIFY_SOCKET")
+
+	if notifySocket == "" {
+		return fmt.Errorf("NOTIFY_SOCKET environment variable empty or unset.")
+	}
+
+	socketAddr := &net.UnixAddr{
+		Name: notifySocket,
+		Net:  "unixgram",
+	}
+
+	conn, err := net.DialUnix(socketAddr.Net, nil, socketAddr)
+
+	if err != nil {
+		return err
+	}
+
+	_, err = conn.Write([]byte(state))
+
+	conn.Close()
+
+	return err
+}

+ 1 - 0
pkg/log/file_test.go

@@ -38,6 +38,7 @@ func TestLogFile(t *testing.T) {
 			So(fileLogWrite.maxlines_curlines, ShouldEqual, 3)
 		})
 
+		fileLogWrite.Close()
 		err = os.Remove(fileLogWrite.Filename)
 		So(err, ShouldBeNil)
 	})

+ 0 - 2
pkg/services/alerting/extractor_test.go

@@ -366,7 +366,6 @@ func TestAlertRuleExtraction(t *testing.T) {
 				          "steppedLine": false,
 				          "targets": [
 				            {
-				              "dsType": "influxdb",
 				              "groupBy": [
 				                {
 				                  "params": [
@@ -411,7 +410,6 @@ func TestAlertRuleExtraction(t *testing.T) {
 				              "tags": []
 				            },
 				            {
-				              "dsType": "influxdb",
 				              "groupBy": [
 				                {
 				                  "params": [

+ 2 - 1
pkg/services/alerting/notifiers/slack.go

@@ -6,6 +6,7 @@ import (
 	"io"
 	"mime/multipart"
 	"os"
+	"path/filepath"
 	"time"
 
 	"github.com/grafana/grafana/pkg/bus"
@@ -176,7 +177,7 @@ func (this *SlackNotifier) Notify(evalContext *alerting.EvalContext) error {
 
 func SlackFileUpload(evalContext *alerting.EvalContext, log log.Logger, url string, recipient string, token string) error {
 	if evalContext.ImageOnDiskPath == "" {
-		evalContext.ImageOnDiskPath = "public/img/mixed_styles.png"
+		evalContext.ImageOnDiskPath = filepath.Join(setting.HomePath, "public/img/mixed_styles.png")
 	}
 	log.Info("Uploading to slack via file.upload API")
 	headers, uploadBody, err := GenerateSlackBody(evalContext.ImageOnDiskPath, token, recipient)

+ 1 - 1
pkg/services/sqlstore/user.go

@@ -401,7 +401,7 @@ func SearchUsers(query *m.SearchUsersQuery) error {
 	}
 
 	if query.Query != "" {
-		whereConditions = append(whereConditions, "(email LIKE ? OR name LIKE ? OR login like ?)")
+		whereConditions = append(whereConditions, "(email "+dialect.LikeStr()+" ? OR name "+dialect.LikeStr()+" ? OR login "+dialect.LikeStr()+" ?)")
 		whereParams = append(whereParams, queryWithWildcards, queryWithWildcards, queryWithWildcards)
 	}
 

+ 2 - 2
pkg/tsdb/graphite/graphite.go

@@ -17,7 +17,7 @@ import (
 	"github.com/grafana/grafana/pkg/models"
 	"github.com/grafana/grafana/pkg/setting"
 	"github.com/grafana/grafana/pkg/tsdb"
-	opentracing "github.com/opentracing/opentracing-go"
+	"github.com/opentracing/opentracing-go"
 )
 
 type GraphiteExecutor struct {
@@ -158,7 +158,7 @@ func formatTimeRange(input string) string {
 	if input == "now" {
 		return input
 	}
-	return strings.Replace(strings.Replace(input, "m", "min", -1), "M", "mon", -1)
+	return strings.Replace(strings.Replace(strings.Replace(input, "now", "", -1), "m", "min", -1), "M", "mon", -1)
 }
 
 func fixIntervalFormat(target string) string {

+ 2 - 2
pkg/tsdb/graphite/graphite_test.go

@@ -18,14 +18,14 @@ func TestGraphiteFunctions(t *testing.T) {
 		Convey("formatting time range for now-1m", func() {
 
 			timeRange := formatTimeRange("now-1m")
-			So(timeRange, ShouldEqual, "now-1min")
+			So(timeRange, ShouldEqual, "-1min")
 
 		})
 
 		Convey("formatting time range for now-1M", func() {
 
 			timeRange := formatTimeRange("now-1M")
-			So(timeRange, ShouldEqual, "now-1mon")
+			So(timeRange, ShouldEqual, "-1mon")
 
 		})
 

+ 0 - 2
pkg/tsdb/influxdb/model_parser_test.go

@@ -20,7 +20,6 @@ func TestInfluxdbQueryParser(t *testing.T) {
 		Convey("can parse influxdb json model", func() {
 			json := `
         {
-        "dsType": "influxdb",
         "groupBy": [
           {
             "params": [
@@ -123,7 +122,6 @@ func TestInfluxdbQueryParser(t *testing.T) {
 		Convey("can part raw query json model", func() {
 			json := `
       {
-        "dsType": "influxdb",
         "groupBy": [
           {
             "params": [

+ 1 - 0
pkg/tsdb/influxdb/response_parser.go

@@ -50,6 +50,7 @@ func (rp *ResponseParser) transformRows(rows []Row, queryResult *tsdb.QueryResul
 			result = append(result, &tsdb.TimeSeries{
 				Name:   rp.formatSerieName(row, column, query),
 				Points: points,
+				Tags:   row.Tags,
 			})
 		}
 	}

+ 32 - 1
pkg/tsdb/postgres/postgres.go

@@ -78,6 +78,15 @@ func (e PostgresQueryEndpoint) transformToTable(query *tsdb.Query, rows *core.Ro
 
 	rowLimit := 1000000
 	rowCount := 0
+	timeIndex := -1
+
+	// check if there is a column named time
+	for i, col := range columnNames {
+		switch col {
+		case "time":
+			timeIndex = i
+		}
+	}
 
 	for ; rows.Next(); rowCount++ {
 		if rowCount > rowLimit {
@@ -89,6 +98,15 @@ func (e PostgresQueryEndpoint) transformToTable(query *tsdb.Query, rows *core.Ro
 			return err
 		}
 
+		// convert column named time to unix timestamp to make
+		// native datetime postgres types work in annotation queries
+		if timeIndex != -1 {
+			switch value := values[timeIndex].(type) {
+			case time.Time:
+				values[timeIndex] = float64(value.UnixNano() / 1e9)
+			}
+		}
+
 		table.Rows = append(table.Rows, values)
 	}
 
@@ -142,8 +160,13 @@ func (e PostgresQueryEndpoint) getTypedRowData(rows *core.Rows) (tsdb.RowValues,
 func (e PostgresQueryEndpoint) transformToTimeSeries(query *tsdb.Query, rows *core.Rows, result *tsdb.QueryResult) error {
 	pointsBySeries := make(map[string]*tsdb.TimeSeries)
 	seriesByQueryOrder := list.New()
+
 	columnNames, err := rows.Columns()
+	if err != nil {
+		return err
+	}
 
+	columnTypes, err := rows.ColumnTypes()
 	if err != nil {
 		return err
 	}
@@ -153,13 +176,21 @@ func (e PostgresQueryEndpoint) transformToTimeSeries(query *tsdb.Query, rows *co
 	timeIndex := -1
 	metricIndex := -1
 
-	// check columns of resultset
+	// check columns of resultset: a column named time is mandatory
+	// the first text column is treated as metric name unless a column named metric is present
 	for i, col := range columnNames {
 		switch col {
 		case "time":
 			timeIndex = i
 		case "metric":
 			metricIndex = i
+		default:
+			if metricIndex == -1 {
+				switch columnTypes[i].DatabaseTypeName() {
+				case "UNKNOWN", "TEXT", "VARCHAR", "CHAR":
+					metricIndex = i
+				}
+			}
 		}
 	}
 

+ 8 - 3
pkg/util/url.go

@@ -9,10 +9,15 @@ type UrlQueryReader struct {
 	values url.Values
 }
 
-func NewUrlQueryReader(url *url.URL) *UrlQueryReader {
-	return &UrlQueryReader{
-		values: url.Query(),
+func NewUrlQueryReader(urlInfo *url.URL) (*UrlQueryReader, error) {
+	u, err := url.ParseQuery(urlInfo.String())
+	if err != nil {
+		return nil, err
 	}
+
+	return &UrlQueryReader{
+		values: u,
+	}, nil
 }
 
 func (r *UrlQueryReader) Get(name string, def string) string {

+ 4 - 0
public/app/core/config.ts

@@ -17,6 +17,10 @@ class Settings {
     alertingEnabled: boolean;
     authProxyEnabled: boolean;
     ldapEnabled: boolean;
+    oauth: any;
+    disableUserSignUp: boolean;
+    loginHint: any;
+    loginError: any;
 
     constructor(options) {
         var defaults = {

+ 0 - 9
public/app/core/controllers/all.js

@@ -1,9 +0,0 @@
-define([
-  './inspect_ctrl',
-  './json_editor_ctrl',
-  './login_ctrl',
-  './invited_ctrl',
-  './signup_ctrl',
-  './reset_password_ctrl',
-  './error_ctrl',
-], function () {});

+ 7 - 0
public/app/core/controllers/all.ts

@@ -0,0 +1,7 @@
+import './inspect_ctrl';
+import './json_editor_ctrl';
+import './login_ctrl';
+import './invited_ctrl';
+import './signup_ctrl';
+import './reset_password_ctrl';
+import './error_ctrl';

+ 8 - 11
public/app/core/controllers/error_ctrl.js → public/app/core/controllers/error_ctrl.ts

@@ -1,13 +1,10 @@
-define([
-  'angular',
-  'app/core/config',
-  '../core_module',
-],
-function (angular, config, coreModule) {
-  'use strict';
+import config from 'app/core/config';
+import coreModule from '../core_module';
 
-  coreModule.default.controller('ErrorCtrl', function($scope, contextSrv, navModelSrv) {
+export class ErrorCtrl {
 
+  /** @ngInject */
+  constructor($scope, contextSrv, navModelSrv) {
     $scope.navModel = navModelSrv.getNotFoundNav();
     $scope.appSubUrl = config.appSubUrl;
 
@@ -17,7 +14,7 @@ function (angular, config, coreModule) {
     $scope.$on('$destroy', function() {
       contextSrv.sidemenu = showSideMenu;
     });
+  }
+}
 
-  });
-
-});
+coreModule.controller('ErrorCtrl', ErrorCtrl);

+ 8 - 11
public/app/core/controllers/invited_ctrl.js → public/app/core/controllers/invited_ctrl.ts

@@ -1,14 +1,10 @@
-define([
-  'angular',
-  '../core_module',
-  'app/core/config',
-],
-function (angular, coreModule, config) {
-  'use strict';
+import coreModule from '../core_module';
+import config from 'app/core/config';
 
-  config = config.default;
+export class InvitedCtrl {
 
-  coreModule.default.controller('InvitedCtrl', function($scope, $routeParams, contextSrv, backendSrv) {
+  /** @ngInject */
+  constructor($scope, $routeParams, contextSrv, backendSrv) {
     contextSrv.sidemenu = false;
     $scope.formModel = {};
 
@@ -35,6 +31,7 @@ function (angular, coreModule, config) {
     };
 
     $scope.init();
+  }
+}
 
-  });
-});
+coreModule.controller('InvitedCtrl', InvitedCtrl);

+ 8 - 10
public/app/core/controllers/json_editor_ctrl.js → public/app/core/controllers/json_editor_ctrl.ts

@@ -1,12 +1,10 @@
-define([
-  'angular',
-  '../core_module',
-],
-function (angular, coreModule) {
-  'use strict';
+import angular from 'angular';
+import coreModule from '../core_module';
 
-  coreModule.default.controller('JsonEditorCtrl', function($scope) {
+export class JsonEditorCtrl {
 
+  /** @ngInject */
+  constructor($scope) {
     $scope.json = angular.toJson($scope.object, true);
     $scope.canUpdate = $scope.updateHandler !== void 0 && $scope.contextSrv.isEditor;
 
@@ -14,7 +12,7 @@ function (angular, coreModule) {
       var newObject = angular.fromJson($scope.json);
       $scope.updateHandler(newObject, $scope.object);
     };
+  }
+}
 
-  });
-
-});
+coreModule.controller('JsonEditorCtrl', JsonEditorCtrl);

+ 13 - 16
public/app/core/controllers/login_ctrl.js → public/app/core/controllers/login_ctrl.ts

@@ -1,15 +1,11 @@
-define([
-  'angular',
-  'lodash',
-  '../core_module',
-  'app/core/config',
-],
-function (angular, _, coreModule, config) {
-  'use strict';
-
-  config = config.default;
-
-  coreModule.default.controller('LoginCtrl', function($scope, backendSrv, contextSrv, $location) {
+import _ from 'lodash';
+import coreModule from '../core_module';
+import config from 'app/core/config';
+
+export class LoginCtrl {
+
+  /** @ngInject */
+  constructor($scope, backendSrv, contextSrv, $location) {
     $scope.formModel = {
       user: '',
       email: '',
@@ -74,8 +70,7 @@ function (angular, _, coreModule, config) {
 
         if (params.redirect && params.redirect[0] === '/') {
           window.location.href = config.appSubUrl + params.redirect;
-        }
-        else if (result.redirectUrl) {
+        } else if (result.redirectUrl) {
           window.location.href = result.redirectUrl;
         } else {
           window.location.href = config.appSubUrl + '/';
@@ -84,5 +79,7 @@ function (angular, _, coreModule, config) {
     };
 
     $scope.init();
-  });
-});
+  }
+}
+
+coreModule.controller('LoginCtrl', LoginCtrl);

+ 9 - 11
public/app/core/controllers/reset_password_ctrl.js → public/app/core/controllers/reset_password_ctrl.ts

@@ -1,11 +1,9 @@
-define([
-  'angular',
-  '../core_module',
-],
-function (angular, coreModule) {
-  'use strict';
-
-  coreModule.default.controller('ResetPasswordCtrl', function($scope, contextSrv, backendSrv, $location) {
+import coreModule from '../core_module';
+
+export class ResetPasswordCtrl {
+
+  /** @ngInject */
+  constructor($scope, contextSrv, backendSrv, $location) {
     contextSrv.sidemenu = false;
     $scope.formModel = {};
     $scope.mode = 'send';
@@ -37,7 +35,7 @@ function (angular, coreModule) {
         $location.path('login');
       });
     };
+  }
+}
 
-  });
-
-});
+coreModule.controller('ResetPasswordCtrl', ResetPasswordCtrl);

+ 1 - 1
public/app/features/dashboard/export/export_modal.ts

@@ -26,7 +26,7 @@ export class DashExportCtrl {
   }
 
   saveJson() {
-    var clone = this.dashboardSrv.getCurrent().getSaveModelClone();
+    var clone = this.dash;
 
     this.$scope.$root.appEvent('show-json-editor', {
       object: clone,

+ 9 - 16
public/app/features/dashboard/shareModalCtrl.js → public/app/features/dashboard/shareModalCtrl.ts

@@ -1,18 +1,11 @@
-define(['angular',
-  'lodash',
-  'jquery',
-  'moment',
-  'app/core/config',
-],
-function (angular, _, $, moment, config) {
-  'use strict';
+import angular from 'angular';
+import moment from 'moment';
+import config from 'app/core/config';
 
-  config = config.default;
-
-  var module = angular.module('grafana.controllers');
-
-  module.controller('ShareModalCtrl', function($scope, $rootScope, $location, $timeout, timeSrv, templateSrv, linkSrv) {
+export class ShareModalCtrl {
 
+  /** @ngInject */
+  constructor($scope, $rootScope, $location, $timeout, timeSrv, templateSrv, linkSrv) {
     $scope.options = { forCurrent: true, includeTemplateVars: true, theme: 'current' };
     $scope.editor = { index: $scope.tabIndex || 0};
 
@@ -93,7 +86,7 @@ function (angular, _, $, moment, config) {
     $scope.getShareUrl = function() {
       return $scope.shareUrl;
     };
+  }
+}
 
-  });
-
-});
+angular.module('grafana.controllers').controller('ShareModalCtrl', ShareModalCtrl);

+ 0 - 1
public/app/plugins/datasource/elasticsearch/query_builder.ts

@@ -172,7 +172,6 @@ export class ElasticQueryBuilder {
   build(target, adhocFilters?, queryString?) {
     // make sure query has defaults;
     target.metrics = target.metrics || [{ type: 'count', id: '1' }];
-    target.dsType = 'elasticsearch';
     target.bucketAggs = target.bucketAggs || [{type: 'date_histogram', id: '2', settings: {interval: 'auto'}}];
     target.timeField =  this.timeField;
 

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

@@ -19,7 +19,6 @@ export default class InfluxQuery {
     this.scopedVars = scopedVars;
 
     target.policy = target.policy || 'default';
-    target.dsType = 'influxdb';
     target.resultFormat = target.resultFormat || 'time_series';
     target.orderByTime = target.orderByTime || 'ASC';
     target.tags = target.tags || [];

+ 12 - 3
public/app/plugins/datasource/mysql/datasource.ts

@@ -103,12 +103,21 @@ export class MysqlDatasource {
       format: 'table',
     };
 
+    var data = {
+        queries: [interpolatedQuery],
+    };
+
+    if (optionalOptions && optionalOptions.range && optionalOptions.range.from) {
+      data['from'] = optionalOptions.range.from.valueOf().toString();
+    }
+    if (optionalOptions && optionalOptions.range && optionalOptions.range.to) {
+      data['to'] = optionalOptions.range.to.valueOf().toString();
+    }
+
     return this.backendSrv.datasourceRequest({
       url: '/api/tsdb/query',
       method: 'POST',
-      data: {
-        queries: [interpolatedQuery],
-      }
+      data: data
     })
     .then(data => this.responseParser.parseMetricFindQueryResult(refId, data));
   }

+ 12 - 3
public/app/plugins/datasource/postgres/datasource.ts

@@ -99,12 +99,21 @@ export class PostgresDatasource {
       format: 'table',
     };
 
+    var data = {
+        queries: [interpolatedQuery],
+    };
+
+    if (optionalOptions && optionalOptions.range && optionalOptions.range.from) {
+      data['from'] = optionalOptions.range.from.valueOf().toString();
+    }
+    if (optionalOptions && optionalOptions.range && optionalOptions.range.to) {
+      data['to'] = optionalOptions.range.to.valueOf().toString();
+    }
+
     return this.backendSrv.datasourceRequest({
       url: '/api/tsdb/query',
       method: 'POST',
-      data: {
-        queries: [interpolatedQuery],
-      }
+      data: data
     })
     .then(data => this.responseParser.parseMetricFindQueryResult(refId, data));
   }

+ 2 - 2
public/app/plugins/datasource/prometheus/metric_find_query.ts

@@ -67,8 +67,8 @@ export default class PrometheusMetricFindQuery {
 
       return this.datasource._request("GET", url).then(function(result) {
         var _labels = _.map(result.data.data, function(metric) {
-          return metric[label];
-        });
+          return metric[label] || '';
+        }).filter(function(label) { return label !== ''; });
 
         return _.uniq(_labels).map(function(metric) {
           return {

+ 53 - 56
public/app/plugins/datasource/prometheus/partials/query.editor.html

@@ -1,63 +1,60 @@
 <query-editor-row query-ctrl="ctrl" can-collapse="true" has-text-edit-mode="false">
-	<div class="gf-form-inline">
-		<div class="gf-form gf-form--grow">
-			<code-editor content="ctrl.target.expr" datasource="ctrl.datasource" on-change="ctrl.refreshMetricData()"
-				get-completer="ctrl.getCompleter()" data-mode="prometheus" code-editor-focus="ctrl.isLastQuery">
-			</code-editor>
-		</div>
-	</div>
+  <div class="gf-form-inline">
+    <div class="gf-form gf-form--grow">
+      <code-editor content="ctrl.target.expr" datasource="ctrl.datasource" on-change="ctrl.refreshMetricData()" get-completer="ctrl.getCompleter()"
+        data-mode="prometheus" code-editor-focus="ctrl.isLastQuery">
+      </code-editor>
+    </div>
+  </div>
 
-	<div class="gf-form-inline">
-		<div class="gf-form max-width-26">
-			<label class="gf-form-label width-8">Legend format</label>
-			<input type="text" class="gf-form-input" ng-model="ctrl.target.legendFormat"
-			spellcheck='false' placeholder="legend format" data-min-length=0 data-items=1000
-			ng-model-onblur ng-change="ctrl.refreshMetricData()">
-			</input>
-		</div>
+  <div class="gf-form-inline">
+    <div class="gf-form max-width-26">
+      <label class="gf-form-label width-8">Legend format</label>
+      <input type="text" class="gf-form-input" ng-model="ctrl.target.legendFormat" spellcheck='false' placeholder="legend format"
+        data-min-length=0 data-items=1000 ng-model-onblur ng-change="ctrl.refreshMetricData()">
+      </input>
+      <info-popover mode="right-absolute">
+        Controls the name of the time series, using name or pattern. For example {{hostname}} will be replaced with label value for
+        the label hostname.
+      </info-popover>
+    </div>
 
-		<div class="gf-form">
-			<label class="gf-form-label width-6">Min step</label>
-			<input type="text" class="gf-form-input width-8" ng-model="ctrl.target.interval"
-					   data-placement="right"
-			       spellcheck='false'
-			       placeholder="{{ctrl.panelCtrl.interval}}"
-			       data-min-length=0 data-items=100
-			       ng-model-onblur
-			       ng-change="ctrl.refreshMetricData()"/>
-			<info-popover mode="right-absolute">
-				Leave blank for auto handling based on time range and panel width
-			</info-popover>
-		</div>
+    <div class="gf-form">
+      <label class="gf-form-label width-6">Min step</label>
+      <input type="text" class="gf-form-input width-8" ng-model="ctrl.target.interval" data-placement="right" spellcheck='false'
+        placeholder="{{ctrl.panelCtrl.interval}}" data-min-length=0 data-items=100 ng-model-onblur ng-change="ctrl.refreshMetricData()"
+      />
+      <info-popover mode="right-absolute">
+        Leave blank for auto handling based on time range and panel width
+      </info-popover>
+    </div>
 
-		<div class="gf-form">
-			<label class="gf-form-label">Resolution</label>
-			<div class="gf-form-select-wrapper max-width-15">
-				<select ng-model="ctrl.target.intervalFactor" class="gf-form-input"
-					ng-options="r.factor as r.label for r in ctrl.resolutions"
-					ng-change="ctrl.refreshMetricData()">
-				</select>
-			</div>
-		</div>
-
-		<div class="gf-form">
-			<label class="gf-form-label width-6">Format as</label>
-			<div class="gf-form-select-wrapper width-8">
-				<select class="gf-form-input gf-size-auto" ng-model="ctrl.target.format" ng-options="f.value as f.text for f in ctrl.formats" ng-change="ctrl.refresh()"></select>
-			</div>
-			<gf-form-switch class="gf-form" label="Instant" label-class="width-5" checked="ctrl.target.instant" on-change="ctrl.refresh()">
-			</gf-form-switch>
-			<label class="gf-form-label">
-				<a href="{{ctrl.linkToPrometheus}}" target="_blank" bs-tooltip="'Link to Graph in Prometheus'">
-					<i class="fa fa-share-square-o"></i>
-				</a>
-			</label>
-		</div>
-
-		<div class="gf-form gf-form--grow">
-			<div class="gf-form-label gf-form-label--grow"></div>
-		</div>
-	</div>
+    <div class="gf-form">
+      <label class="gf-form-label">Resolution</label>
+      <div class="gf-form-select-wrapper max-width-15">
+        <select ng-model="ctrl.target.intervalFactor" class="gf-form-input" ng-options="r.factor as r.label for r in ctrl.resolutions"
+          ng-change="ctrl.refreshMetricData()">
+        </select>
+      </div>
+    </div>
 
+    <div class="gf-form">
+      <label class="gf-form-label width-6">Format as</label>
+      <div class="gf-form-select-wrapper width-8">
+        <select class="gf-form-input gf-size-auto" ng-model="ctrl.target.format" ng-options="f.value as f.text for f in ctrl.formats"
+          ng-change="ctrl.refresh()"></select>
+      </div>
+      <gf-form-switch class="gf-form" label="Instant" label-class="width-5" checked="ctrl.target.instant" on-change="ctrl.refresh()">
+      </gf-form-switch>
+      <label class="gf-form-label">
+        <a href="{{ctrl.linkToPrometheus}}" target="_blank" bs-tooltip="'Link to Graph in Prometheus'">
+          <i class="fa fa-share-square-o"></i>
+        </a>
+      </label>
+    </div>
 
+    <div class="gf-form gf-form--grow">
+      <div class="gf-form-label gf-form-label--grow"></div>
+    </div>
+  </div>
 </query-editor-row>

+ 4 - 0
public/app/plugins/datasource/prometheus/plugin.json

@@ -13,6 +13,10 @@
   "alerting": true,
   "annotations": true,
 
+  "queryOptions": {
+    "minInterval": true
+  },
+
   "info": {
     "author": {
       "name": "Grafana Project",

+ 18 - 0
public/app/plugins/datasource/prometheus/specs/metric_find_query_specs.ts

@@ -76,6 +76,24 @@ describe('PrometheusMetricFindQuery', function() {
       ctx.$rootScope.$apply();
       expect(results.length).to.be(3);
     });
+    it('label_values(metric, resource) result should not contain empty string', function() {
+      response = {
+        status: "success",
+        data: [
+          {__name__: "metric", resource: "value1"},
+          {__name__: "metric", resource: "value2"},
+          {__name__: "metric", resource: ""}
+        ]
+      };
+      ctx.$httpBackend.expect('GET', /proxied\/api\/v1\/series\?match\[\]=metric&start=.*&end=.*/).respond(response);
+      var pm = new PrometheusMetricFindQuery(ctx.ds, 'label_values(metric, resource)', ctx.timeSrv);
+      pm.process().then(function(data) { results = data; });
+      ctx.$httpBackend.flush();
+      ctx.$rootScope.$apply();
+      expect(results.length).to.be(2);
+      expect(results[0].text).to.be("value1");
+      expect(results[1].text).to.be("value2");
+    });
     it('metrics(metric.*) should generate metric name query', function() {
       response = {
         status: "success",