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