diff --git a/CHANGELOG.md b/CHANGELOG.md
index dcacc92e542..c0833f60e37 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -12,8 +12,21 @@
## New Features
* **Data Source Proxy**: Add support for whitelisting specified cookies that will be passed through to the data source when proxying data source requests [#5457](https://github.com/grafana/grafana/issues/5457), thanks [@robingustafsson](https://github.com/robingustafsson)
-## Fixes
+
+## Tech
+* **RabbitMq**: Remove support for publishing events to RabbitMQ [#9645](https://github.com/grafana/grafana/issues/9645)
+
+## Fixes
* **Sensu**: Send alert message to sensu output [#9551](https://github.com/grafana/grafana/issues/9551), thx [@cjchand](https://github.com/cjchand)
+* **Singlestat**: suppress error when result contains no datapoints [#9636](https://github.com/grafana/grafana/issues/9636), thx [@utkarshcmu](https://github.com/utkarshcmu)
+* **Postgres/MySQL**: Control quoting in SQL-queries when using template variables [#9030](https://github.com/grafana/grafana/issues/9030), thanks [@svenklemm](https://github.com/svenklemm)
+
+# 4.6.0 (2017-10-26)
+
+## Fixes
+* **Alerting**: Viewer can no longer pause alert rules [#9640](https://github.com/grafana/grafana/issues/9640)
+* **Playlist**: Bug where playlist controls was missing [#9639](https://github.com/grafana/grafana/issues/9639)
+* **Firefox**: Creating region annotations now work in firefox [#9638](https://github.com/grafana/grafana/issues/9638)
# 4.6.0-beta3 (2017-10-23)
@@ -56,11 +69,11 @@
* **OAuth**: Verify TLS during OAuth callback [#9373](https://github.com/grafana/grafana/issues/9373), thx [@mattbostock](https://github.com/mattbostock)
## Minor
-* **SMTP**: Make it possible to set specific EHLO for smtp client. [#9319](https://github.com/grafana/grafana/issues/9319)
-* **Dataproxy**: Allow grafan to renegotiate tls connection [#9250](https://github.com/grafana/grafana/issues/9250)
+* **SMTP**: Make it possible to set specific HELO for smtp client. [#9319](https://github.com/grafana/grafana/issues/9319)
+* **Dataproxy**: Allow grafana to renegotiate tls connection [#9250](https://github.com/grafana/grafana/issues/9250)
* **HTTP**: set net.Dialer.DualStack to true for all http clients [#9367](https://github.com/grafana/grafana/pull/9367)
* **Alerting**: Add diff and percent diff as series reducers [#9386](https://github.com/grafana/grafana/pull/9386), thx [@shanhuhai5739](https://github.com/shanhuhai5739)
-* **Slack**: Allow images to be uploaded to slack when Token is precent [#7175](https://github.com/grafana/grafana/issues/7175), thx [@xginn8](https://github.com/xginn8)
+* **Slack**: Allow images to be uploaded to slack when Token is present [#7175](https://github.com/grafana/grafana/issues/7175), thx [@xginn8](https://github.com/xginn8)
* **Opsgenie**: Use their latest API instead of old version [#9399](https://github.com/grafana/grafana/pull/9399), thx [@cglrkn](https://github.com/cglrkn)
* **Table**: Add support for displaying the timestamp with milliseconds [#9429](https://github.com/grafana/grafana/pull/9429), thx [@s1061123](https://github.com/s1061123)
* **Hipchat**: Add metrics, message and image to hipchat notifications [#9110](https://github.com/grafana/grafana/issues/9110), thx [@eloo](https://github.com/eloo)
diff --git a/codecov.yml b/codecov.yml
new file mode 100644
index 00000000000..3d764c1a5b1
--- /dev/null
+++ b/codecov.yml
@@ -0,0 +1,11 @@
+coverage:
+ precision: 2
+ round: down
+ range: "50...100"
+
+ status:
+ project: yes
+ patch: yes
+ changes: no
+
+comment: false
diff --git a/conf/defaults.ini b/conf/defaults.ini
index 14e77449241..404a7950bf3 100644
--- a/conf/defaults.ini
+++ b/conf/defaults.ini
@@ -381,13 +381,6 @@ facility =
# Syslog tag. By default, the process' argv[0] is used.
tag =
-
-#################################### AMQP Event Publisher ################
-[event_publisher]
-enabled = false
-rabbitmq_url = amqp://localhost/
-exchange = grafana_events
-
#################################### Dashboard JSON files ################
[dashboards.json]
enabled = false
diff --git a/conf/sample.ini b/conf/sample.ini
index 1aedfbf6532..c7d2d1d2695 100644
--- a/conf/sample.ini
+++ b/conf/sample.ini
@@ -360,12 +360,6 @@
;tag =
-#################################### AMQP Event Publisher ##########################
-[event_publisher]
-;enabled = false
-;rabbitmq_url = amqp://localhost/
-;exchange = grafana_events
-
;#################################### Dashboard JSON files ##########################
[dashboards.json]
;enabled = false
diff --git a/docs/sources/alerting/rules.md b/docs/sources/alerting/rules.md
index ead457066e3..bd5b95da856 100644
--- a/docs/sources/alerting/rules.md
+++ b/docs/sources/alerting/rules.md
@@ -27,8 +27,7 @@ and the conditions that need to be met for the alert to change state and trigger
## Execution
The alert rules are evaluated in the Grafana backend in a scheduler and query execution engine that is part
-of core Grafana. Only some data sources are supported right now. They include `Graphite`, `Prometheus`,
-`InfluxDB` and `OpenTSDB`.
+of core Grafana. Only some data sources are supported right now. They include `Graphite`, `Prometheus`, `InfluxDB`, `OpenTSDB`, `MySQL`, `Postgres` and `Cloudwatch`.
### Clustering
diff --git a/docs/sources/features/datasources/cloudwatch.md b/docs/sources/features/datasources/cloudwatch.md
index 1d98b2d16d1..bdf661dc4fc 100644
--- a/docs/sources/features/datasources/cloudwatch.md
+++ b/docs/sources/features/datasources/cloudwatch.md
@@ -169,5 +169,3 @@ Amazon provides 1 million CloudWatch API requests each month at no additional ch
it costs $0.01 per 1,000 GetMetricStatistics or ListMetrics requests. For each query Grafana will
issue a GetMetricStatistics request and every time you pick a dimension in the query editor
Grafana will issue a ListMetrics request.
-
-
diff --git a/docs/sources/features/datasources/index.md b/docs/sources/features/datasources/index.md
index 56501980c31..54606d20988 100644
--- a/docs/sources/features/datasources/index.md
+++ b/docs/sources/features/datasources/index.md
@@ -28,8 +28,9 @@ The following datasources are officially supported:
* [InfluxDB]({{< relref "influxdb.md" >}})
* [OpenTSDB]({{< relref "opentsdb.md" >}})
* [Prometheus]({{< relref "prometheus.md" >}})
+* [MySQL]({{< relref "mysql.md" >}})
+* [Postgres]({{< relref "postgres.md" >}})
## Data source plugins
Since grafana 3.0 you can install data sources as plugins. Checkout [Grafana.net](https://grafana.com/plugins) for more data sources.
-
diff --git a/docs/sources/features/datasources/influxdb.md b/docs/sources/features/datasources/influxdb.md
index 97e523cd151..6d0918a0d01 100644
--- a/docs/sources/features/datasources/influxdb.md
+++ b/docs/sources/features/datasources/influxdb.md
@@ -173,6 +173,4 @@ SELECT title, description from events WHERE $timeFilter order asc
For InfluxDB you need to enter a query like in the above example. You need to have the ```where $timeFilter```
part. If you only select one column you will not need to enter anything in the column mapping fields. The
-Tags field can be a comma seperated string.
-
-
+Tags field can be a comma separated string.
diff --git a/docs/sources/features/datasources/mysql.md b/docs/sources/features/datasources/mysql.md
index f277f45f3c8..69c6f667062 100644
--- a/docs/sources/features/datasources/mysql.md
+++ b/docs/sources/features/datasources/mysql.md
@@ -142,7 +142,11 @@ SELECT hostname FROM my_host WHERE region IN($region)
### Using Variables in Queries
-Template variables are quoted automatically so if it is a string value do not wrap them in quotes in where clauses. If the variable is a multi-value variable then use the `IN` comparison operator rather than `=` to match against multiple values.
+From Grafana 4.3.0 to 4.6.0, template variables are always quoted automatically so if it is a string value do not wrap them in quotes in where clauses.
+
+From Grafana 4.7.0, template variable values are only quoted when the template variable is a `multi-value`.
+
+If the variable is a multi-value variable then use the `IN` comparison operator rather than `=` to match against multiple values.
There are two syntaxes:
@@ -170,7 +174,28 @@ WHERE $__timeFilter(atimestamp) and hostname in([[hostname]])
ORDER BY atimestamp ASC
```
+## Annotations
+
+[Annotations]({{< relref "reference/annotations.md" >}}) allows you to overlay rich event information on top of graphs. You add annotation queries via the Dashboard menu / Annotations view.
+
+An example query:
+
+```sql
+SELECT
+ UNIX_TIMESTAMP(atimestamp) as time_sec,
+ value as text,
+ CONCAT(tag1, ',', tag2) as tags
+FROM my_table
+WHERE $__timeFilter(atimestamp)
+ORDER BY atimestamp ASC
+```
+
+Name | Description
+------------ | -------------
+time_sec | The name of the date/time field.
+text | Event description field.
+tags | Optional field name to use for event tags as a comma separated string.
+
## Alerting
-Time series queries should work in alerting conditions. Table formatted queries is not yet supported in alert rule
-conditions.
+Time series queries should work in alerting conditions. Table formatted queries is not yet supported in alert rule conditions.
diff --git a/docs/sources/features/datasources/postgres.md b/docs/sources/features/datasources/postgres.md
index a4ef8277cd1..154f822d9d0 100644
--- a/docs/sources/features/datasources/postgres.md
+++ b/docs/sources/features/datasources/postgres.md
@@ -154,7 +154,11 @@ SELECT hostname FROM host WHERE region IN($region)
### Using Variables in Queries
-Template variables are quoted automatically so if it is a string value do not wrap them in quotes in where clauses. If the variable is a multi-value variable then use the `IN` comparison operator rather than `=` to match against multiple values.
+From Grafana 4.3.0 to 4.6.0, template variables are always quoted automatically so if it is a string value do not wrap them in quotes in where clauses.
+
+From Grafana 4.7.0, template variable values are only quoted when the template variable is a `multi-value`.
+
+If the variable is a multi-value variable then use the `IN` comparison operator rather than `=` to match against multiple values.
There are two syntaxes:
@@ -180,6 +184,29 @@ WHERE $__timeFilter(atimestamp) and hostname in([[hostname]])
ORDER BY atimestamp ASC
```
+## Annotations
+
+[Annotations]({{< relref "reference/annotations.md" >}}) allows you to overlay rich event information on top of graphs. You add annotation queries via the Dashboard menu / Annotations view.
+
+An example query:
+
+```sql
+SELECT
+ extract(epoch from time_date_time) AS time,
+ metric1 as text,
+ concat_ws(', ', metric1::text, metric2::text) as tags
+FROM
+ public.test_data
+WHERE
+ $__timeFilter(time_date_time)
+```
+
+Name | Description
+------------ | -------------
+time | The name of the date/time field.
+text | Event description field.
+tags | Optional field name to use for event tags as a comma separated string.
+
## Alerting
Time series queries should work in alerting conditions. Table formatted queries is not yet supported in alert rule
diff --git a/docs/sources/http_api/admin.md b/docs/sources/http_api/admin.md
index 3ef5fb1136a..716246102bc 100644
--- a/docs/sources/http_api/admin.md
+++ b/docs/sources/http_api/admin.md
@@ -102,11 +102,6 @@ Content-Type: application/json
"templates_pattern":"emails/*.html",
"welcome_email_on_sign_up":"false"
},
- "event_publisher":{
- "enabled":"false",
- "exchange":"grafana_events",
- "rabbitmq_url":"amqp://localhost/"
- },
"log":{
"buffer_len":"10000",
"level":"Info",
diff --git a/docs/sources/http_api/org.md b/docs/sources/http_api/org.md
index 6542f00fd81..4c1dff904c8 100644
--- a/docs/sources/http_api/org.md
+++ b/docs/sources/http_api/org.md
@@ -14,12 +14,12 @@ parent = "http_api"
## Get current Organisation
-`GET /api/org`
+`GET /api/org/`
**Example Request**:
```http
-GET /api/org HTTP/1.1
+GET /api/org/ HTTP/1.1
Accept: application/json
Content-Type: application/json
Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
@@ -49,6 +49,8 @@ Accept: application/json
Content-Type: application/json
Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
```
+Note: The api will only work when you pass the admin name and password
+to the request http url, like http://admin:admin@localhost:3000/api/orgs/1
**Example Response**:
@@ -81,6 +83,8 @@ Accept: application/json
Content-Type: application/json
Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
```
+Note: The api will only work when you pass the admin name and password
+to the request http url, like http://admin:admin@localhost:3000/api/orgs/name/Main%20Org%2E
**Example Response**:
@@ -118,6 +122,9 @@ Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
"name":"New Org."
}
```
+Note: The api will work in the following two ways
+1) Need to set GF_USERS_ALLOW_ORG_CREATE=true
+2) Set the config users.allow_org_create to true in ini file
**Example Response**:
@@ -279,6 +286,8 @@ Accept: application/json
Content-Type: application/json
Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
```
+Note: The api will only work when you pass the admin name and password
+to the request http url, like http://admin:admin@localhost:3000/api/orgs
**Example Response**:
@@ -334,6 +343,9 @@ Accept: application/json
Content-Type: application/json
Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
```
+Note: The api will only work when you pass the admin name and password
+to the request http url, like http://admin:admin@localhost:3000/api/orgs/1/users
+
**Example Response**:
diff --git a/docs/sources/index.md b/docs/sources/index.md
index f22ee82cc5c..9226c842abc 100644
--- a/docs/sources/index.md
+++ b/docs/sources/index.md
@@ -35,17 +35,19 @@ The back-end web server has a number of configuration options. Go the
those options.
-## Getting started
+## Getting Started
- [Getting Started]({{< relref "guides/getting_started.md" >}})
- [Basic Concepts]({{< relref "guides/basic_concepts.md" >}})
- [Screencasts]({{< relref "tutorials/screencasts.md" >}})
-## Data sources guides
+## Data Source Guides
- [Graphite]({{< relref "features/datasources/graphite.md" >}})
- [Elasticsearch]({{< relref "features/datasources/elasticsearch.md" >}})
- [InfluxDB]({{< relref "features/datasources/influxdb.md" >}})
-- [OpenTSDB]({{< relref "features/datasources/opentsdb.md" >}})
-
-
+- [Prometheus]({{< relref "features/datasources/influxdb.md" >}})
+- [OpenTSDB]({{< relref "features/datasources/prometheus.md" >}})
+- [MySQL]({{< relref "features/datasources/mysql.md" >}})
+- [Postgres]({{< relref "features/datasources/postgres.md" >}})
+- [Cloudwatch]({{< relref "features/datasources/cloudwatch.md" >}})
diff --git a/docs/sources/installation/debian.md b/docs/sources/installation/debian.md
index 222a337855a..7ecb6d14b0c 100644
--- a/docs/sources/installation/debian.md
+++ b/docs/sources/installation/debian.md
@@ -15,7 +15,7 @@ weight = 1
Description | Download
------------ | -------------
-Stable for Debian-based Linux | [grafana_4.5.2_amd64.deb](https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana_4.5.2_amd64.deb)
+Stable for Debian-based Linux | [grafana_4.6.0_amd64.deb](https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana_4.6.0_amd64.deb)
@@ -26,9 +26,9 @@ installation.
```bash
-wget https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana_4.5.2_amd64.deb
+wget https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana_4.6.0_amd64.deb
sudo apt-get install -y adduser libfontconfig
-sudo dpkg -i grafana_4.5.2_amd64.deb
+sudo dpkg -i grafana_4.6.0_amd64.deb
```
@@ -27,7 +27,7 @@ installation.
You can install Grafana using Yum directly.
```bash
-$ sudo yum install https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana-4.5.2-1.x86_64.rpm
+$ sudo yum install https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana-4.6.0-1.x86_64.rpm
```
Or install manually using `rpm`.
@@ -35,15 +35,15 @@ Or install manually using `rpm`.
#### On CentOS / Fedora / Redhat:
```bash
-$ wget https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana-4.5.2-1.x86_64.rpm
+$ wget https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana-4.6.0-1.x86_64.rpm
$ sudo yum install initscripts fontconfig
-$ sudo rpm -Uvh grafana-4.5.2-1.x86_64.rpm
+$ sudo rpm -Uvh grafana-4.6.0-1.x86_64.rpm
```
#### On OpenSuse:
```bash
-$ sudo rpm -i --nodeps grafana-4.5.2-1.x86_64.rpm
+$ sudo rpm -i --nodeps grafana-4.6.0-1.x86_64.rpm
```
## Install via YUM Repository
diff --git a/docs/sources/installation/windows.md b/docs/sources/installation/windows.md
index ea9614c01be..181d26d694c 100644
--- a/docs/sources/installation/windows.md
+++ b/docs/sources/installation/windows.md
@@ -13,7 +13,7 @@ weight = 3
Description | Download
------------ | -------------
-Latest stable package for Windows | [grafana.4.5.2.windows-x64.zip](https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana-4.5.2.windows-x64.zip)
+Latest stable package for Windows | [grafana.4.6.0.windows-x64.zip](https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana-4.6.0.windows-x64.zip)
Read [Upgrading Grafana]({{< relref "installation/upgrading.md" >}}) for tips and guidance on updating an existing
installation.
diff --git a/docs/sources/plugins/developing/development.md b/docs/sources/plugins/developing/development.md
index 41cef41b653..f2e70a50c6a 100644
--- a/docs/sources/plugins/developing/development.md
+++ b/docs/sources/plugins/developing/development.md
@@ -16,7 +16,7 @@ You can extend Grafana by writing your own plugins and then share then with othe
1. [Setup grafana](http://docs.grafana.org/project/building_from_source/)
2. Clone an example plugin into ```/var/lib/grafana/plugins``` or `data/plugins` (relative to grafana git repo if you're running development version from source dir)
-3. You one of our example plugins as starting point
+3. Use one of our example plugins as starting point
Example plugins
diff --git a/docs/sources/reference/annotations.md b/docs/sources/reference/annotations.md
index 3852c2c727b..de118f37d46 100644
--- a/docs/sources/reference/annotations.md
+++ b/docs/sources/reference/annotations.md
@@ -69,5 +69,5 @@ The annotation query options are different for each data source.
- [Elasticsearch annotation queries]({{< relref "features/datasources/elasticsearch.md#annotations" >}})
- [InfluxDB annotation queries]({{< relref "features/datasources/influxdb.md#annotations" >}})
- [Prometheus annotation queries]({{< relref "features/datasources/prometheus.md#annotations" >}})
-
-
+- [MySQL annotation queries]({{< relref "features/datasources/mysql.md#annotations" >}})
+- [Postgres annotation queries]({{< relref "features/datasources/postgres.md#annotations" >}})
diff --git a/pkg/api/api.go b/pkg/api/api.go
index 957e2e06c0a..957ee1e23e6 100644
--- a/pkg/api/api.go
+++ b/pkg/api/api.go
@@ -287,7 +287,7 @@ func (hs *HttpServer) registerRoutes() {
apiRoute.Group("/alerts", func(alertsRoute RouteRegister) {
alertsRoute.Post("/test", bind(dtos.AlertTestCommand{}), wrap(AlertTest))
- alertsRoute.Post("/:alertId/pause", bind(dtos.PauseAlertCommand{}), wrap(PauseAlert), reqEditorRole)
+ alertsRoute.Post("/:alertId/pause", reqEditorRole, bind(dtos.PauseAlertCommand{}), wrap(PauseAlert))
alertsRoute.Get("/:alertId", ValidateOrgAlert, wrap(GetAlert))
alertsRoute.Get("/", wrap(GetAlerts))
alertsRoute.Get("/states-for-dashboard", wrap(GetAlertStatesForDashboard))
diff --git a/pkg/api/datasources.go b/pkg/api/datasources.go
index 9ffdc4a6d1b..747ef8f25e6 100644
--- a/pkg/api/datasources.go
+++ b/pkg/api/datasources.go
@@ -119,7 +119,13 @@ func AddDataSource(c *middleware.Context, cmd m.AddDataSourceCommand) {
return
}
- c.JSON(200, util.DynMap{"message": "Datasource added", "id": cmd.Result.Id, "name": cmd.Result.Name})
+ ds := convertModelToDtos(cmd.Result)
+ c.JSON(200, util.DynMap{
+ "message": "Datasource added",
+ "id": cmd.Result.Id,
+ "name": cmd.Result.Name,
+ "datasource": ds,
+ })
}
func UpdateDataSource(c *middleware.Context, cmd m.UpdateDataSourceCommand) Response {
@@ -133,10 +139,19 @@ func UpdateDataSource(c *middleware.Context, cmd m.UpdateDataSourceCommand) Resp
err = bus.Dispatch(&cmd)
if err != nil {
- return ApiError(500, "Failed to update datasource", err)
+ if err == m.ErrDataSourceUpdatingOldVersion {
+ return ApiError(500, "Failed to update datasource. Reload new version and try again", err)
+ } else {
+ return ApiError(500, "Failed to update datasource", err)
+ }
}
-
- return Json(200, util.DynMap{"message": "Datasource updated", "id": cmd.Id, "name": cmd.Name})
+ ds := convertModelToDtos(cmd.Result)
+ return Json(200, util.DynMap{
+ "message": "Datasource updated",
+ "id": cmd.Id,
+ "name": cmd.Name,
+ "datasource": ds,
+ })
}
func fillWithSecureJsonData(cmd *m.UpdateDataSourceCommand) error {
@@ -158,8 +173,6 @@ func fillWithSecureJsonData(cmd *m.UpdateDataSourceCommand) error {
}
}
- // set version from db
- cmd.Version = ds.Version
return nil
}
@@ -228,6 +241,7 @@ func convertModelToDtos(ds *m.DataSource) dtos.DataSource {
IsDefault: ds.IsDefault,
JsonData: ds.JsonData,
SecureJsonFields: map[string]bool{},
+ Version: ds.Version,
}
for k, v := range ds.SecureJsonData {
diff --git a/pkg/api/dtos/datasource.go b/pkg/api/dtos/datasource.go
new file mode 100644
index 00000000000..7cb36e61ab4
--- /dev/null
+++ b/pkg/api/dtos/datasource.go
@@ -0,0 +1,59 @@
+package dtos
+
+import (
+ "strings"
+
+ "github.com/grafana/grafana/pkg/components/simplejson"
+ m "github.com/grafana/grafana/pkg/models"
+)
+
+type DataSource struct {
+ Id int64 `json:"id"`
+ OrgId int64 `json:"orgId"`
+ Name string `json:"name"`
+ Type string `json:"type"`
+ TypeLogoUrl string `json:"typeLogoUrl"`
+ Access m.DsAccess `json:"access"`
+ Url string `json:"url"`
+ Password string `json:"password"`
+ User string `json:"user"`
+ Database string `json:"database"`
+ BasicAuth bool `json:"basicAuth"`
+ BasicAuthUser string `json:"basicAuthUser"`
+ BasicAuthPassword string `json:"basicAuthPassword"`
+ WithCredentials bool `json:"withCredentials"`
+ IsDefault bool `json:"isDefault"`
+ JsonData *simplejson.Json `json:"jsonData,omitempty"`
+ SecureJsonFields map[string]bool `json:"secureJsonFields"`
+ Version int `json:"version"`
+}
+
+type DataSourceListItemDTO struct {
+ Id int64 `json:"id"`
+ OrgId int64 `json:"orgId"`
+ Name string `json:"name"`
+ Type string `json:"type"`
+ TypeLogoUrl string `json:"typeLogoUrl"`
+ Access m.DsAccess `json:"access"`
+ Url string `json:"url"`
+ Password string `json:"password"`
+ User string `json:"user"`
+ Database string `json:"database"`
+ BasicAuth bool `json:"basicAuth"`
+ IsDefault bool `json:"isDefault"`
+ JsonData *simplejson.Json `json:"jsonData,omitempty"`
+}
+
+type DataSourceList []DataSourceListItemDTO
+
+func (slice DataSourceList) Len() int {
+ return len(slice)
+}
+
+func (slice DataSourceList) Less(i, j int) bool {
+ return strings.ToLower(slice[i].Name) < strings.ToLower(slice[j].Name)
+}
+
+func (slice DataSourceList) Swap(i, j int) {
+ slice[i], slice[j] = slice[j], slice[i]
+}
diff --git a/pkg/api/dtos/models.go b/pkg/api/dtos/models.go
index b155858c58c..2e9aa78d7d5 100644
--- a/pkg/api/dtos/models.go
+++ b/pkg/api/dtos/models.go
@@ -38,56 +38,6 @@ type CurrentUser struct {
HelpFlags1 m.HelpFlags1 `json:"helpFlags1"`
}
-type DataSource struct {
- Id int64 `json:"id"`
- OrgId int64 `json:"orgId"`
- Name string `json:"name"`
- Type string `json:"type"`
- TypeLogoUrl string `json:"typeLogoUrl"`
- Access m.DsAccess `json:"access"`
- Url string `json:"url"`
- Password string `json:"password"`
- User string `json:"user"`
- Database string `json:"database"`
- BasicAuth bool `json:"basicAuth"`
- BasicAuthUser string `json:"basicAuthUser"`
- BasicAuthPassword string `json:"basicAuthPassword"`
- WithCredentials bool `json:"withCredentials"`
- IsDefault bool `json:"isDefault"`
- JsonData *simplejson.Json `json:"jsonData,omitempty"`
- SecureJsonFields map[string]bool `json:"secureJsonFields"`
-}
-
-type DataSourceListItemDTO struct {
- Id int64 `json:"id"`
- OrgId int64 `json:"orgId"`
- Name string `json:"name"`
- Type string `json:"type"`
- TypeLogoUrl string `json:"typeLogoUrl"`
- Access m.DsAccess `json:"access"`
- Url string `json:"url"`
- Password string `json:"password"`
- User string `json:"user"`
- Database string `json:"database"`
- BasicAuth bool `json:"basicAuth"`
- IsDefault bool `json:"isDefault"`
- JsonData *simplejson.Json `json:"jsonData,omitempty"`
-}
-
-type DataSourceList []DataSourceListItemDTO
-
-func (slice DataSourceList) Len() int {
- return len(slice)
-}
-
-func (slice DataSourceList) Less(i, j int) bool {
- return strings.ToLower(slice[i].Name) < strings.ToLower(slice[j].Name)
-}
-
-func (slice DataSourceList) Swap(i, j int) {
- slice[i], slice[j] = slice[j], slice[i]
-}
-
type MetricRequest struct {
From string `json:"from"`
To string `json:"to"`
diff --git a/pkg/cmd/grafana-server/server.go b/pkg/cmd/grafana-server/server.go
index f6a94ac6ef0..4bbabbe3273 100644
--- a/pkg/cmd/grafana-server/server.go
+++ b/pkg/cmd/grafana-server/server.go
@@ -19,7 +19,6 @@ import (
"github.com/grafana/grafana/pkg/plugins"
"github.com/grafana/grafana/pkg/services/alerting"
"github.com/grafana/grafana/pkg/services/cleanup"
- "github.com/grafana/grafana/pkg/services/eventpublisher"
"github.com/grafana/grafana/pkg/services/notifications"
"github.com/grafana/grafana/pkg/services/search"
"github.com/grafana/grafana/pkg/setting"
@@ -59,7 +58,6 @@ func (g *GrafanaServerImpl) Start() {
search.Init()
login.Init()
social.NewOAuthService()
- eventpublisher.Init()
plugins.Init()
closer, err := tracing.Init(setting.Cfg)
diff --git a/pkg/components/renderer/renderer.go b/pkg/components/renderer/renderer.go
index e98172ba0cf..cc087106752 100644
--- a/pkg/components/renderer/renderer.go
+++ b/pkg/components/renderer/renderer.go
@@ -129,7 +129,9 @@ func RenderToPng(params *RenderOpts) (string, error) {
done := make(chan error)
go func() {
- cmd.Wait()
+ if err := cmd.Wait(); err != nil {
+ rendererLog.Error("failed to render an image", "error", err)
+ }
close(done)
}()
diff --git a/pkg/models/datasource.go b/pkg/models/datasource.go
index ee365582734..7fc8f935124 100644
--- a/pkg/models/datasource.go
+++ b/pkg/models/datasource.go
@@ -23,10 +23,10 @@ const (
DS_ACCESS_PROXY = "proxy"
)
-// Typed errors
var (
- ErrDataSourceNotFound = errors.New("Data source not found")
- ErrDataSourceNameExists = errors.New("Data source with same name already exists")
+ ErrDataSourceNotFound = errors.New("Data source not found")
+ ErrDataSourceNameExists = errors.New("Data source with same name already exists")
+ ErrDataSourceUpdatingOldVersion = errors.New("Trying to update old version of datasource")
)
type DsAccess string
@@ -131,10 +131,12 @@ type UpdateDataSourceCommand struct {
IsDefault bool `json:"isDefault"`
JsonData *simplejson.Json `json:"jsonData"`
SecureJsonData map[string]string `json:"secureJsonData"`
+ Version int `json:"version"`
- OrgId int64 `json:"-"`
- Id int64 `json:"-"`
- Version int `json:"-"`
+ OrgId int64 `json:"-"`
+ Id int64 `json:"-"`
+
+ Result *DataSource
}
type DeleteDataSourceByIdCommand struct {
diff --git a/pkg/services/alerting/eval_handler.go b/pkg/services/alerting/eval_handler.go
index 5c2861b9154..79b2f231b41 100644
--- a/pkg/services/alerting/eval_handler.go
+++ b/pkg/services/alerting/eval_handler.go
@@ -67,7 +67,7 @@ func (e *DefaultEvalHandler) Eval(context *EvalContext) {
metrics.M_Alerting_Execution_Time.Observe(float64(elapsedTime))
}
-// This should be move into evalContext once its been refactored.
+// This should be move into evalContext once its been refactored. (Carl Bergquist)
func (handler *DefaultEvalHandler) getNewState(evalContext *EvalContext) models.AlertStateType {
if evalContext.Error != nil {
handler.log.Error("Alert Rule Result Error",
diff --git a/pkg/services/eventpublisher/eventpublisher.go b/pkg/services/eventpublisher/eventpublisher.go
deleted file mode 100644
index 2854b63a9a5..00000000000
--- a/pkg/services/eventpublisher/eventpublisher.go
+++ /dev/null
@@ -1,150 +0,0 @@
-package eventpublisher
-
-import (
- "encoding/json"
- "fmt"
- "log"
- "time"
-
- "github.com/grafana/grafana/pkg/bus"
- "github.com/grafana/grafana/pkg/events"
- "github.com/grafana/grafana/pkg/setting"
- "github.com/streadway/amqp"
-)
-
-var (
- url string
- exchange string
- conn *amqp.Connection
- channel *amqp.Channel
-)
-
-func getConnection() (*amqp.Connection, error) {
- c, err := amqp.Dial(url)
- if err != nil {
- return nil, err
- }
- return c, err
-}
-
-func getChannel() (*amqp.Channel, error) {
- ch, err := conn.Channel()
- if err != nil {
- return nil, err
- }
-
- err = ch.ExchangeDeclare(
- exchange, // name
- "topic", // type
- true, // durable
- false, // auto-deleted
- false, // internal
- false, // no-wait
- nil, // arguments
- )
- if err != nil {
- return nil, err
- }
- return ch, err
-}
-
-func Init() {
- sec := setting.Cfg.Section("event_publisher")
-
- if !sec.Key("enabled").MustBool(false) {
- return
- }
-
- url = sec.Key("rabbitmq_url").String()
- exchange = sec.Key("exchange").String()
- bus.AddWildcardListener(eventListener)
-
- if err := Setup(); err != nil {
- log.Fatal(4, "Failed to connect to notification queue: %v", err)
- return
- }
-}
-
-// Every connection should declare the topology they expect
-func Setup() error {
- c, err := getConnection()
- if err != nil {
- return err
- }
- conn = c
- ch, err := getChannel()
- if err != nil {
- return err
- }
-
- channel = ch
-
- // listen for close events so we can reconnect.
- errChan := channel.NotifyClose(make(chan *amqp.Error))
- go func() {
- for e := range errChan {
- fmt.Println("connection to rabbitmq lost.")
- fmt.Println(e)
- fmt.Println("attempting to create new rabbitmq channel.")
- ch, err := getChannel()
- if err == nil {
- channel = ch
- break
- }
-
- //could not create channel, so lets close the connection
- // and re-create.
- _ = conn.Close()
-
- for err != nil {
- time.Sleep(2 * time.Second)
- fmt.Println("attempting to reconnect to rabbitmq.")
- err = Setup()
- }
- fmt.Println("Connected to rabbitmq again.")
- }
- }()
-
- return nil
-}
-
-func publish(routingKey string, msgString []byte) {
- for {
- err := channel.Publish(
- exchange, //exchange
- routingKey, // routing key
- false, // mandatory
- false, // immediate
- amqp.Publishing{
- ContentType: "application/json",
- Body: msgString,
- },
- )
- if err == nil {
- return
- }
- // failures are most likely because the connection was lost.
- // the connection will be re-established, so just keep
- // retrying every 2seconds until we successfully publish.
- time.Sleep(2 * time.Second)
- fmt.Println("publish failed, retrying.")
- }
-}
-
-func eventListener(event interface{}) error {
- wireEvent, err := events.ToOnWriteEvent(event)
- if err != nil {
- return err
- }
-
- msgString, err := json.Marshal(wireEvent)
- if err != nil {
- return err
- }
-
- routingKey := fmt.Sprintf("%s.%s", wireEvent.Priority, wireEvent.EventType)
- // this is run in a greenthread and we expect that publish will keep
- // retrying until the message gets sent.
- go publish(routingKey, msgString)
- return nil
-}
diff --git a/pkg/services/sqlstore/datasource.go b/pkg/services/sqlstore/datasource.go
index 88dc34fd196..7069990c476 100644
--- a/pkg/services/sqlstore/datasource.go
+++ b/pkg/services/sqlstore/datasource.go
@@ -3,6 +3,8 @@ package sqlstore
import (
"time"
+ "github.com/go-xorm/xorm"
+
"github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/components/securejsondata"
"github.com/grafana/grafana/pkg/metrics"
@@ -69,7 +71,6 @@ func DeleteDataSourceByName(cmd *m.DeleteDataSourceByNameCommand) error {
}
func AddDataSource(cmd *m.AddDataSourceCommand) error {
-
return inTransaction(func(sess *DBSession) error {
existing := m.DataSource{OrgId: cmd.OrgId, Name: cmd.Name}
has, _ := sess.Get(&existing)
@@ -96,6 +97,7 @@ func AddDataSource(cmd *m.AddDataSourceCommand) error {
SecureJsonData: securejsondata.GetEncryptedJsonData(cmd.SecureJsonData),
Created: time.Now(),
Updated: time.Now(),
+ Version: 1,
}
if _, err := sess.Insert(ds); err != nil {
@@ -122,7 +124,6 @@ func updateIsDefaultFlag(ds *m.DataSource, sess *DBSession) error {
}
func UpdateDataSource(cmd *m.UpdateDataSourceCommand) error {
-
return inTransaction(func(sess *DBSession) error {
ds := &m.DataSource{
Id: cmd.Id,
@@ -149,12 +150,29 @@ func UpdateDataSource(cmd *m.UpdateDataSourceCommand) error {
sess.UseBool("basic_auth")
sess.UseBool("with_credentials")
- _, err := sess.Where("id=? and org_id=?", ds.Id, ds.OrgId).Update(ds)
+ var updateSession *xorm.Session
+ if cmd.Version != 0 {
+ // the reason we allow cmd.version > db.version is make it possible for people to force
+ // updates to datasources using the datasource.yaml file without knowing exactly what version
+ // a datasource have in the db.
+ updateSession = sess.Where("id=? and org_id=? and version < ?", ds.Id, ds.OrgId, ds.Version)
+
+ } else {
+ updateSession = sess.Where("id=? and org_id=?", ds.Id, ds.OrgId)
+ }
+
+ affected, err := updateSession.Update(ds)
if err != nil {
return err
}
+ if affected == 0 {
+ return m.ErrDataSourceUpdatingOldVersion
+ }
+
err = updateIsDefaultFlag(ds, sess)
+
+ cmd.Result = ds
return err
})
}
diff --git a/pkg/services/sqlstore/datasource_test.go b/pkg/services/sqlstore/datasource_test.go
index 28957cb2096..de16b17f960 100644
--- a/pkg/services/sqlstore/datasource_test.go
+++ b/pkg/services/sqlstore/datasource_test.go
@@ -37,12 +37,9 @@ type Test struct {
}
func TestDataAccess(t *testing.T) {
-
Convey("Testing DB", t, func() {
InitTestDB(t)
-
Convey("Can add datasource", func() {
-
err := AddDataSource(&m.AddDataSourceCommand{
OrgId: 10,
Name: "laban",
@@ -67,7 +64,6 @@ func TestDataAccess(t *testing.T) {
})
Convey("Given a datasource", func() {
-
err := AddDataSource(&m.AddDataSourceCommand{
OrgId: 10,
Name: "nisse",
@@ -83,6 +79,89 @@ func TestDataAccess(t *testing.T) {
ds := query.Result[0]
+ Convey(" updated ", func() {
+ cmd := &m.UpdateDataSourceCommand{
+ Id: ds.Id,
+ OrgId: 10,
+ Name: "nisse",
+ Type: m.DS_GRAPHITE,
+ Access: m.DS_ACCESS_PROXY,
+ Url: "http://test",
+ Version: ds.Version,
+ }
+
+ Convey("with same version as source", func() {
+ err := UpdateDataSource(cmd)
+ So(err, ShouldBeNil)
+ })
+
+ Convey("when someone else updated between read and update", func() {
+ query := m.GetDataSourcesQuery{OrgId: 10}
+ err = GetDataSources(&query)
+ So(err, ShouldBeNil)
+
+ ds := query.Result[0]
+ intendedUpdate := &m.UpdateDataSourceCommand{
+ Id: ds.Id,
+ OrgId: 10,
+ Name: "nisse",
+ Type: m.DS_GRAPHITE,
+ Access: m.DS_ACCESS_PROXY,
+ Url: "http://test",
+ Version: ds.Version,
+ }
+
+ updateFromOtherUser := &m.UpdateDataSourceCommand{
+ Id: ds.Id,
+ OrgId: 10,
+ Name: "nisse",
+ Type: m.DS_GRAPHITE,
+ Access: m.DS_ACCESS_PROXY,
+ Url: "http://test",
+ Version: ds.Version,
+ }
+
+ err := UpdateDataSource(updateFromOtherUser)
+ So(err, ShouldBeNil)
+
+ err = UpdateDataSource(intendedUpdate)
+ So(err, ShouldNotBeNil)
+ })
+
+ Convey("updating datasource without version", func() {
+ cmd := &m.UpdateDataSourceCommand{
+ Id: ds.Id,
+ OrgId: 10,
+ Name: "nisse",
+ Type: m.DS_GRAPHITE,
+ Access: m.DS_ACCESS_PROXY,
+ Url: "http://test",
+ }
+
+ Convey("should not raise errors", func() {
+ err := UpdateDataSource(cmd)
+ So(err, ShouldBeNil)
+ })
+ })
+
+ Convey("updating datasource without higher version", func() {
+ cmd := &m.UpdateDataSourceCommand{
+ Id: ds.Id,
+ OrgId: 10,
+ Name: "nisse",
+ Type: m.DS_GRAPHITE,
+ Access: m.DS_ACCESS_PROXY,
+ Url: "http://test",
+ Version: 90000,
+ }
+
+ Convey("should not raise errors", func() {
+ err := UpdateDataSource(cmd)
+ So(err, ShouldBeNil)
+ })
+ })
+ })
+
Convey("Can delete datasource by id", func() {
err := DeleteDataSourceById(&m.DeleteDataSourceByIdCommand{Id: ds.Id, OrgId: ds.OrgId})
So(err, ShouldBeNil)
@@ -106,9 +185,6 @@ func TestDataAccess(t *testing.T) {
GetDataSources(&query)
So(len(query.Result), ShouldEqual, 1)
})
-
})
-
})
-
}
diff --git a/pkg/services/sqlstore/migrations/datasource_mig.go b/pkg/services/sqlstore/migrations/datasource_mig.go
index c694b5b7cfb..cc8a7f05177 100644
--- a/pkg/services/sqlstore/migrations/datasource_mig.go
+++ b/pkg/services/sqlstore/migrations/datasource_mig.go
@@ -120,4 +120,10 @@ func addDataSourceMigration(mg *Migrator) {
{Name: "json_data", Type: DB_Text, Nullable: true},
{Name: "secure_json_data", Type: DB_Text, Nullable: true},
}))
+
+ const setVersionToOneWhereZero = `UPDATE data_source SET version = 1 WHERE version = 0`
+ mg.AddMigration("Update initial version to 1", new(RawSqlMigration).
+ Sqlite(setVersionToOneWhereZero).
+ Postgres(setVersionToOneWhereZero).
+ Mysql(setVersionToOneWhereZero))
}
diff --git a/public/app/core/components/layout_selector/layout_selector.ts b/public/app/core/components/layout_selector/layout_selector.ts
index b2071723677..98f806cd63e 100644
--- a/public/app/core/components/layout_selector/layout_selector.ts
+++ b/public/app/core/components/layout_selector/layout_selector.ts
@@ -1,5 +1,3 @@
-///
-
import store from 'app/core/store';
import coreModule from 'app/core/core_module';
diff --git a/public/app/core/controllers/inspect_ctrl.js b/public/app/core/controllers/inspect_ctrl.ts
similarity index 65%
rename from public/app/core/controllers/inspect_ctrl.js
rename to public/app/core/controllers/inspect_ctrl.ts
index 39d550658f1..70516986099 100644
--- a/public/app/core/controllers/inspect_ctrl.js
+++ b/public/app/core/controllers/inspect_ctrl.ts
@@ -1,27 +1,14 @@
-define([
- 'angular',
- 'lodash',
- 'jquery',
- '../core_module',
-],
-function (angular, _, $, coreModule) {
- 'use strict';
+import angular from 'angular';
+import _ from 'lodash';
+import $ from 'jquery';
+import coreModule from '../core_module';
- coreModule.default.controller('InspectCtrl', function($scope, $sanitize) {
+export class InspectCtrl {
+
+ /** @ngInject */
+ constructor($scope, $sanitize) {
var model = $scope.inspector;
- function getParametersFromQueryString(queryString) {
- var result = [];
- var parameters = queryString.split("&");
- for (var i = 0; i < parameters.length; i++) {
- var keyValue = parameters[i].split("=");
- if (keyValue[1].length > 0) {
- result.push({ key: keyValue[0], value: window.unescape(keyValue[1]) });
- }
- }
- return result;
- }
-
$scope.init = function () {
$scope.editor = { index: 0 };
@@ -57,7 +44,7 @@ function (angular, _, $, coreModule) {
$scope.editor.index = 2;
if (_.isString(model.error.config.data)) {
- $scope.request_parameters = getParametersFromQueryString(model.error.config.data);
+ $scope.request_parameters = this.getParametersFromQueryString(model.error.config.data);
} else {
$scope.request_parameters = _.map(model.error.config.data, function(value, key) {
return {key: key, value: angular.toJson(value, true)};
@@ -65,7 +52,18 @@ function (angular, _, $, coreModule) {
}
}
};
+ }
+ getParametersFromQueryString(queryString) {
+ var result = [];
+ var parameters = queryString.split("&");
+ for (var i = 0; i < parameters.length; i++) {
+ var keyValue = parameters[i].split("=");
+ if (keyValue[1].length > 0) {
+ result.push({ key: keyValue[0], value: (window).unescape(keyValue[1]) });
+ }
+ }
+ return result;
+ }
+}
- });
-
-});
+coreModule.controller('InspectCtrl', InspectCtrl);
diff --git a/public/app/core/directives/diff-view.ts b/public/app/core/directives/diff-view.ts
index b82b23951b7..db76681498c 100644
--- a/public/app/core/directives/diff-view.ts
+++ b/public/app/core/directives/diff-view.ts
@@ -7,14 +7,15 @@ export class DeltaCtrl {
observer: any;
/** @ngInject */
- constructor($rootScope) {
- const waitForCompile = function(mutations) {
+ constructor(private $rootScope) {
+
+ const waitForCompile = (mutations) => {
if (mutations.length === 1) {
this.$rootScope.appEvent('json-diff-ready');
}
};
- this.observer = new MutationObserver(waitForCompile.bind(this));
+ this.observer = new MutationObserver(waitForCompile);
const observerConfig = {
attributes: true,
diff --git a/public/app/core/routes/dashboard_loaders.js b/public/app/core/routes/dashboard_loaders.ts
similarity index 70%
rename from public/app/core/routes/dashboard_loaders.js
rename to public/app/core/routes/dashboard_loaders.ts
index 91bbff84100..0512a3394ab 100644
--- a/public/app/core/routes/dashboard_loaders.js
+++ b/public/app/core/routes/dashboard_loaders.ts
@@ -1,10 +1,9 @@
-define([
- '../core_module',
-],
-function (coreModule) {
- "use strict";
+import coreModule from '../core_module';
- coreModule.default.controller('LoadDashboardCtrl', function($scope, $routeParams, dashboardLoaderSrv, backendSrv, $location) {
+export class LoadDashboardCtrl {
+
+ /** @ngInject */
+ constructor($scope, $routeParams, dashboardLoaderSrv, backendSrv, $location) {
$scope.appEvent("dashboard-fetch-start");
if (!$routeParams.slug) {
@@ -26,10 +25,13 @@ function (coreModule) {
}
$scope.initDashboard(result, $scope);
});
+ }
+}
- });
+export class NewDashboardCtrl {
- coreModule.default.controller('NewDashboardCtrl', function($scope) {
+ /** @ngInject */
+ constructor($scope) {
$scope.initDashboard({
meta: { canStar: false, canShare: false, isNew: true },
dashboard: {
@@ -38,12 +40,14 @@ function (coreModule) {
{
title: 'Dashboard Row',
height: '350px',
- panels:[],
+ panels: [],
isNew: true,
}
]
},
}, $scope);
- });
+ }
+}
-});
+coreModule.controller('LoadDashboardCtrl', LoadDashboardCtrl);
+coreModule.controller('NewDashboardCtrl', NewDashboardCtrl);
diff --git a/public/app/core/services/analytics.js b/public/app/core/services/analytics.js
deleted file mode 100644
index 9f71045b008..00000000000
--- a/public/app/core/services/analytics.js
+++ /dev/null
@@ -1,41 +0,0 @@
-define([
- 'angular',
- 'jquery',
- 'app/core/core_module',
- 'app/core/config',
-],
-function(angular, $, coreModule, config) {
- 'use strict';
-
- config = config.default;
-
- coreModule.default.service('googleAnalyticsSrv', function($rootScope, $location) {
-
- function gaInit() {
- $.getScript('https://www.google-analytics.com/analytics.js'); // jQuery shortcut
- var ga = window.ga = window.ga || function () { (ga.q = ga.q || []).push(arguments); }; ga.l = +new Date;
- ga('create', config.googleAnalyticsId, 'auto');
- return ga;
- }
-
- this.init = function() {
-
- $rootScope.$on('$viewContentLoaded', function() {
- var track = { page: $location.url() };
-
- var ga = window.ga || gaInit();
-
- ga('set', track);
- ga('send', 'pageview');
- });
-
- };
-
- }).run(function(googleAnalyticsSrv) {
-
- if (config.googleAnalyticsId) {
- googleAnalyticsSrv.init();
- }
-
- });
-});
diff --git a/public/app/core/services/analytics.ts b/public/app/core/services/analytics.ts
new file mode 100644
index 00000000000..87e84efa706
--- /dev/null
+++ b/public/app/core/services/analytics.ts
@@ -0,0 +1,36 @@
+import $ from 'jquery';
+import coreModule from 'app/core/core_module';
+import config from 'app/core/config';
+
+export class Analytics {
+
+ /** @ngInject */
+ constructor(private $rootScope, private $location) {
+ }
+
+ gaInit() {
+ $.getScript('https://www.google-analytics.com/analytics.js'); // jQuery shortcut
+ var ga = (window).ga = (window).ga || function () { (ga.q = ga.q || []).push(arguments); }; ga.l = +new Date;
+ ga('create', (config).googleAnalyticsId, 'auto');
+ return ga;
+ }
+
+ init() {
+ this.$rootScope.$on('$viewContentLoaded', () => {
+ var track = { page: this.$location.url() };
+ var ga = (window).ga || this.gaInit();
+ ga('set', track);
+ ga('send', 'pageview');
+ });
+ }
+}
+
+/** @ngInject */
+function startAnalytics(googleAnalyticsSrv) {
+ if ((config).googleAnalyticsId) {
+ googleAnalyticsSrv.init();
+ }
+}
+
+coreModule.service('googleAnalyticsSrv', Analytics).run(startAnalytics);
+
diff --git a/public/app/core/services/timer.js b/public/app/core/services/timer.js
deleted file mode 100644
index 67ad02367c0..00000000000
--- a/public/app/core/services/timer.js
+++ /dev/null
@@ -1,33 +0,0 @@
-define([
- 'angular',
- 'lodash',
- '../core_module',
-],
-function (angular, _, coreModule) {
- 'use strict';
-
- coreModule.default.service('timer', function($timeout) {
- // This service really just tracks a list of $timeout promises to give us a
- // method for cancelling them all when we need to
-
- var timers = [];
-
- this.register = function(promise) {
- timers.push(promise);
- return promise;
- };
-
- this.cancel = function(promise) {
- timers = _.without(timers,promise);
- $timeout.cancel(promise);
- };
-
- this.cancelAll = function() {
- _.each(timers, function(t) {
- $timeout.cancel(t);
- });
- timers = [];
- };
- });
-
-});
diff --git a/public/app/core/services/timer.ts b/public/app/core/services/timer.ts
new file mode 100644
index 00000000000..6356e1f2910
--- /dev/null
+++ b/public/app/core/services/timer.ts
@@ -0,0 +1,31 @@
+import _ from 'lodash';
+import coreModule from 'app/core/core_module';
+
+// This service really just tracks a list of $timeout promises to give us a
+// method for cancelling them all when we need to
+export class Timer {
+ timers = [];
+
+ /** @ngInject */
+ constructor(private $timeout) {
+ }
+
+ register(promise) {
+ this.timers.push(promise);
+ return promise;
+ }
+
+ cancel(promise) {
+ this.timers = _.without(this.timers, promise);
+ this.$timeout.cancel(promise);
+ }
+
+ cancelAll() {
+ _.each(this.timers, function (t) {
+ this.$timeout.cancel(t);
+ });
+ this.timers = [];
+ }
+}
+
+coreModule.service('timer', Timer);
diff --git a/public/app/core/specs/store.jest.ts b/public/app/core/specs/store.jest.ts
new file mode 100644
index 00000000000..44cb2a84959
--- /dev/null
+++ b/public/app/core/specs/store.jest.ts
@@ -0,0 +1,44 @@
+import store from '../store';
+
+Object.assign(window, {
+ localStorage: {
+ removeItem(key) {
+ delete window.localStorage[key];
+ }
+ }
+});
+
+describe('store', () => {
+
+ it("should store", ()=> {
+ store.set("key1", "123");
+ expect(store.get("key1")).toBe("123");
+ });
+
+ it("get key when undefined", ()=> {
+ expect(store.get("key2")).toBe(undefined);
+ });
+
+ it("check if key exixts", ()=> {
+ store.set("key3", "123");
+ expect(store.exists("key3")).toBe(true);
+ });
+
+ it("get boolean when no key", ()=> {
+ expect(store.getBool("key4", false)).toBe(false);
+ });
+
+ it("get boolean", ()=> {
+ store.set("key5", "true");
+ expect(store.getBool("key5", false)).toBe(true);
+ });
+
+ it("key should be deleted", ()=> {
+ store.set("key6", "123");
+ store.delete("key6");
+ expect(store.exists("key6")).toBe(false);
+ });
+
+});
+
+
diff --git a/public/app/core/store.js b/public/app/core/store.js
deleted file mode 100644
index 504b0e5aff5..00000000000
--- a/public/app/core/store.js
+++ /dev/null
@@ -1,26 +0,0 @@
-define([], function() {
- 'use strict';
-
- return {
- get: function(key) {
- return window.localStorage[key];
- },
- set: function(key, value) {
- window.localStorage[key] = value;
- },
- getBool: function(key, def) {
- if (def !== void 0 && !this.exists(key)) {
- return def;
- }
- return window.localStorage[key] === 'true';
- },
- exists: function(key) {
- return window.localStorage[key] !== void 0;
- },
- delete: function(key) {
- window.localStorage.removeItem(key);
- }
-
- };
-
-});
diff --git a/public/app/core/store.ts b/public/app/core/store.ts
new file mode 100644
index 00000000000..cbed16510ab
--- /dev/null
+++ b/public/app/core/store.ts
@@ -0,0 +1,29 @@
+export class Store {
+
+ get(key) {
+ return window.localStorage[key];
+ }
+
+ set(key, value) {
+ window.localStorage[key] = value;
+ }
+
+ getBool(key, def) {
+ if (def !== void 0 && !this.exists(key)) {
+ return def;
+ }
+ return window.localStorage[key] === 'true';
+ }
+
+ exists(key) {
+ return window.localStorage[key] !== void 0;
+ }
+
+ delete(key) {
+ window.localStorage.removeItem(key);
+ }
+
+}
+
+const store = new Store();
+export default store;
diff --git a/public/app/core/utils/outline.js b/public/app/core/utils/outline.js
deleted file mode 100644
index f968b3e51f9..00000000000
--- a/public/app/core/utils/outline.js
+++ /dev/null
@@ -1,32 +0,0 @@
-// outline.js
-// based on http://www.paciellogroup.com/blog/2012/04/how-to-remove-css-outlines-in-an-accessible-manner/
-(function(d) {
- "use strict";
-
- var style_element = d.createElement('STYLE'),
- dom_events = 'addEventListener' in d,
- add_event_listener = function(type, callback) {
- // Basic cross-browser event handling
- if(dom_events){
- d.addEventListener(type, callback);
- } else {
- d.attachEvent('on' + type, callback);
- }
- },
- set_css = function(css_text) {
- // Handle setting of