diff --git a/.editorconfig b/.editorconfig index 146224e7330..84bbaf8a420 100644 --- a/.editorconfig +++ b/.editorconfig @@ -7,6 +7,8 @@ indent_size = 2 charset = utf-8 trim_trailing_whitespace = true insert_final_newline = true +max_line_length = 120 +insert_final_newline = true [*.go] indent_style = tab diff --git a/.gitignore b/.gitignore index 12e7bed3f46..deb1c1f882a 100644 --- a/.gitignore +++ b/.gitignore @@ -60,3 +60,4 @@ debug.test /vendor/**/*_test.go /vendor/**/.editorconfig /vendor/**/appengine* +*.orig \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index bb0d2e026b9..da4fe019959 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,6 +25,8 @@ Dashboard panels and rows are positioned using a gridPos object `{x: 0, y: 0, w: Config files for provisioning datasources as configuration have changed from `/conf/datasources` to `/conf/provisioning/datasources`. From `/etc/grafana/datasources` to `/etc/grafana/provisioning/datasources` when installed with deb/rpm packages. +The pagerduty notifier now defaults to not auto resolve incidents. More details at [#10222](https://github.com/grafana/grafana/issues/10222) + ## 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) * **Postgres/MySQL**: add __timeGroup macro for mysql [#9596](https://github.com/grafana/grafana/pull/9596), thanks [@svenklemm](https://github.com/svenklemm) @@ -36,6 +38,7 @@ From `/etc/grafana/datasources` to `/etc/grafana/provisioning/datasources` when * **Dashboard as cfg**: Load dashboards from file into Grafana on startup/change [#9654](https://github.com/grafana/grafana/issues/9654) [#5269](https://github.com/grafana/grafana/issues/5269) * **Prometheus**: Grafana can now send alerts to Prometheus Alertmanager while firing [#7481](https://github.com/grafana/grafana/issues/7481), thx [@Thib17](https://github.com/Thib17) and [@mtanda](https://github.com/mtanda) * **Table**: Support multiple table formated queries in table panel [#9170](https://github.com/grafana/grafana/issues/9170), thx [@davkal](https://github.com/davkal) + ## Minor * **Alert panel**: Adds placeholder text when no alerts are within the time range [#9624](https://github.com/grafana/grafana/issues/9624), thx [@straend](https://github.com/straend) * **Mysql**: MySQL enable MaxOpenCon and MaxIdleCon regards how constring is configured. [#9784](https://github.com/grafana/grafana/issues/9784), thx [@dfredell](https://github.com/dfredell) @@ -46,6 +49,8 @@ From `/etc/grafana/datasources` to `/etc/grafana/provisioning/datasources` when * **Github**: Use organizations_url provided from github to verify user belongs in org. [#10111](https://github.com/grafana/grafana/issues/10111), thx [@adiletmaratov](https://github.com/adiletmaratov) * **Backend**: Fixed bug where Grafana exited before all sub routines where finished [#10131](https://github.com/grafana/grafana/issues/10131) +* **Azure**: Adds support for Azure blob storage as external image stor [#8955](https://github.com/grafana/grafana/issues/8955), thx [@saada](https://github.com/saada) +* **Telegram**: Add support for inline image uploads to telegram notifier plugin [#9967](https://github.com/grafana/grafana/pull/9967), thx [@rburchell](https://github.com/rburchell) ## Tech * **RabbitMq**: Remove support for publishing events to RabbitMQ [#9645](https://github.com/grafana/grafana/issues/9645) @@ -55,6 +60,7 @@ From `/etc/grafana/datasources` to `/etc/grafana/provisioning/datasources` when * **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) +* **Pagerduty**: Pagerduty dont auto resolve incidents by default anymore. [#10222](https://github.com/grafana/grafana/issues/10222) # 4.6.3 (2017-12-14) diff --git a/conf/defaults.ini b/conf/defaults.ini index e16ef95178a..4e2929096a6 100644 --- a/conf/defaults.ini +++ b/conf/defaults.ini @@ -473,7 +473,7 @@ sampler_param = 1 #################################### External Image Storage ############## [external_image_storage] -# You can choose between (s3, webdav, gcs) +# You can choose between (s3, webdav, gcs, azure_blob) provider = [external_image_storage.s3] @@ -493,4 +493,9 @@ public_url = [external_image_storage.gcs] key_file = bucket = -path = \ No newline at end of file +path = + +[external_image_storage.azure_blob] +account_name = +account_key = +container_name = diff --git a/conf/sample.ini b/conf/sample.ini index 544fa7e0c7d..d297d2db66a 100644 --- a/conf/sample.ini +++ b/conf/sample.ini @@ -417,7 +417,7 @@ log_queries = #################################### External image storage ########################## [external_image_storage] # Used for uploading images to public servers so they can be included in slack/email messages. -# you can choose between (s3, webdav, gcs) +# you can choose between (s3, webdav, gcs, azure_blob) ;provider = [external_image_storage.s3] @@ -436,4 +436,9 @@ log_queries = [external_image_storage.gcs] ;key_file = ;bucket = -;path = \ No newline at end of file +;path = + +[external_image_storage.azure_blob] +;account_name = +;account_key = +;container_name = diff --git a/docker/blocks/apache_proxy/Dockerfile b/docker/blocks/apache_proxy/Dockerfile new file mode 100644 index 00000000000..37c2a4e493b --- /dev/null +++ b/docker/blocks/apache_proxy/Dockerfile @@ -0,0 +1,4 @@ +FROM jmferrer/apache2-reverse-proxy:latest + +COPY ports.conf /etc/apache2/sites-enabled +COPY proxy.conf /etc/apache2/sites-enabled \ No newline at end of file diff --git a/docker/blocks/apache_proxy/docker-compose.yaml b/docker/blocks/apache_proxy/docker-compose.yaml new file mode 100644 index 00000000000..2aec3d4bc4f --- /dev/null +++ b/docker/blocks/apache_proxy/docker-compose.yaml @@ -0,0 +1,9 @@ +# This will proxy all requests for http://localhost:10081/grafana/ to +# http://localhost:3000 (Grafana running locally) +# +# Please note that you'll need to change the root_url in the Grafana configuration: +# root_url = %(protocol)s://%(domain)s:/grafana/ + + apacheproxy: + build: blocks/apache_proxy + network_mode: host diff --git a/docker/blocks/apache_proxy/ports.conf b/docker/blocks/apache_proxy/ports.conf new file mode 100644 index 00000000000..e7a5f9712af --- /dev/null +++ b/docker/blocks/apache_proxy/ports.conf @@ -0,0 +1 @@ +Listen 10081 \ No newline at end of file diff --git a/docker/blocks/apache_proxy/proxy.conf b/docker/blocks/apache_proxy/proxy.conf new file mode 100644 index 00000000000..5eb35cbe8b9 --- /dev/null +++ b/docker/blocks/apache_proxy/proxy.conf @@ -0,0 +1,4 @@ + + ProxyPass /grafana/ http://localhost:3000/ + ProxyPassReverse /grafana/ http://localhost:3000/ + \ No newline at end of file diff --git a/docker/blocks/nginx_proxy/Dockerfile b/docker/blocks/nginx_proxy/Dockerfile new file mode 100644 index 00000000000..9ded20dfdda --- /dev/null +++ b/docker/blocks/nginx_proxy/Dockerfile @@ -0,0 +1,3 @@ +FROM nginx:alpine + +COPY nginx.conf /etc/nginx/nginx.conf \ No newline at end of file diff --git a/docker/blocks/nginx_proxy/docker-compose.yaml b/docker/blocks/nginx_proxy/docker-compose.yaml new file mode 100644 index 00000000000..7c3447ade5c --- /dev/null +++ b/docker/blocks/nginx_proxy/docker-compose.yaml @@ -0,0 +1,9 @@ +# This will proxy all requests for http://localhost:10080/grafana/ to +# http://localhost:3000 (Grafana running locally) +# +# Please note that you'll need to change the root_url in the Grafana configuration: +# root_url = %(protocol)s://%(domain)s:/grafana/ + + nginxproxy: + build: blocks/nginx_proxy + network_mode: host diff --git a/docker/blocks/nginx_proxy/nginx.conf b/docker/blocks/nginx_proxy/nginx.conf new file mode 100644 index 00000000000..18e27b3fb01 --- /dev/null +++ b/docker/blocks/nginx_proxy/nginx.conf @@ -0,0 +1,19 @@ +events { worker_connections 1024; } + +http { + sendfile on; + + proxy_redirect off; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Host $server_name; + + server { + listen 10080; + + location /grafana/ { + proxy_pass http://localhost:3000/; + } + } +} \ No newline at end of file diff --git a/docs/sources/administration/cli.md b/docs/sources/administration/cli.md index 2be1881cfab..3dc9b0cf3b4 100644 --- a/docs/sources/administration/cli.md +++ b/docs/sources/administration/cli.md @@ -10,17 +10,17 @@ weight = 8 # Grafana CLI -Grafana cli is a small executable that is bundled with grafana server and is suppose to be executed on the same machine as grafana runs. +Grafana cli is a small executable that is bundled with Grafana-server and is supposed to be executed on the same machine Grafana-server is running on. ## Plugins -The CLI helps you install, upgrade and manage your plugins on the same machine it CLI is running. -You can find more information about how to install and manage your plugins at the -[plugin page]({{< relref "plugins/installation.md" >}}). +The CLI allows you to install, upgrade and manage your plugins on the machine it is running on. +You can find more information about how to install and manage your plugins in the +[plugins page]({{< relref "plugins/installation.md" >}}). ## Admin -> This feature is only available in grafana 4.1 and above. +> This feature is only available in Grafana 4.1 and above. To show all admin commands: `grafana-cli admin` @@ -39,7 +39,7 @@ then there are two flags that can be used to set homepath and the config file pa `grafana-cli admin reset-admin-password --homepath "/usr/share/grafana" newpass` -If you have not lost the admin password then it is better to set in the Grafana UI. If you need to set the password in a script then the [Grafana API](http://docs.grafana.org/http_api/user/#change-password) can be used. Here is an example with curl using basic auth: +If you have not lost the admin password then it is better to set in the Grafana UI. If you need to set the password in a script then the [Grafana API](http://docs.grafana.org/http_api/user/#change-password) can be used. Here is an example using curl with basic auth: ```bash curl -X PUT -H "Content-Type: application/json" -d '{ diff --git a/docs/sources/administration/metrics.md b/docs/sources/administration/metrics.md index 56d0290bc82..5eed2b7ce15 100644 --- a/docs/sources/administration/metrics.md +++ b/docs/sources/administration/metrics.md @@ -10,6 +10,6 @@ weight = 8 # Internal metrics -Grafana collects some metrics about it self internally. Currently Grafana supports pushing metrics to graphite and exposing them to be scraped by Prometheus. +Grafana collects some metrics about itself internally. Currently, Grafana supports pushing metrics to Graphite or exposing them to be scraped by Prometheus. -To enabled internal metrics you have to enable it under the [metrics] section in your [grafana.ini](http://docs.grafana.org/installation/configuration/#enabled-6) config file.If you want to push metrics to graphite you have also have to configure the [metrics.graphite](http://docs.grafana.org/installation/configuration/#metrics-graphite) section. +To emit internal metrics you have to enable the option under the [metrics] section in your [grafana.ini](http://docs.grafana.org/installation/configuration/#enabled-6) config file. If you want to push metrics to Graphite, you must also configure the [metrics.graphite](http://docs.grafana.org/installation/configuration/#metrics-graphite) section. diff --git a/docs/sources/administration/provisioning.md b/docs/sources/administration/provisioning.md index 3dd5c5fd1d3..0750131d42f 100644 --- a/docs/sources/administration/provisioning.md +++ b/docs/sources/administration/provisioning.md @@ -12,7 +12,7 @@ weight = 8 ## Config file -Checkout the [configuration](/installation/configuration) page for more information about what you can configure in `grafana.ini` +Checkout the [configuration](/installation/configuration) page for more information on what you can configure in `grafana.ini` ### Config file locations @@ -35,7 +35,7 @@ GF__ ``` Where the section name is the text within the brackets. Everything -should be upper case, `.` should be replaced by `_`. For example, given these configuration settings: +should be upper case and `.` should be replaced by `_`. For example, given these configuration settings: ```bash # default section @@ -48,7 +48,7 @@ admin_user = admin client_secret = 0ldS3cretKey ``` -Then you can override them using: +Overriding will be done like so: ```bash export GF_DEFAULT_INSTANCE_NAME=my-instance @@ -60,7 +60,7 @@ export GF_AUTH_GOOGLE_CLIENT_SECRET=newS3cretKey ## Configuration management tools -Currently we do not provide any scripts/manifests for configuring Grafana. Rather then spending time learning and creating scripts/manifests for each tool, we think our time is better spent making Grafana easier to provision. Therefor, we heavily relay on the expertise of he community. +Currently we do not provide any scripts/manifests for configuring Grafana. Rather than spending time learning and creating scripts/manifests for each tool, we think our time is better spent making Grafana easier to provision. Therefore, we heavily relay on the expertise of the community. Tool | Project -----|------------ @@ -70,14 +70,14 @@ Ansible | [https://github.com/picotrading/ansible-grafana](https://github.com/pi Chef | [https://github.com/JonathanTron/chef-grafana](https://github.com/JonathanTron/chef-grafana) Saltstack | [https://github.com/salt-formulas/salt-formula-grafana](https://github.com/salt-formulas/salt-formula-grafana) -## Datasources +## Datasources > This feature is available from v5.0 It's possible to manage datasources in Grafana by adding one or more yaml config files in the [`provisioning/datasources`](/installation/configuration/#provisioning) directory. Each config file can contain a list of `datasources` that will be added or updated during start up. If the datasource already exists, Grafana will update it to match the configuration file. The config file can also contain a list of datasources that should be deleted. That list is called `delete_datasources`. Grafana will delete datasources listed in `delete_datasources` before inserting/updating those in the `datasource` list. -### Running multiple grafana instances. -If you are running multiple instances of Grafana you might run into problems if they have different versions of the datasource.yaml configuration file. The best way to solve this problem is to add a version number to each datasource in the configuration and increase it when you update the config. Grafana will only update datasources with the same or lower version number than specified in the config. That way old configs cannot overwrite newer configs if they restart at the same time. +### Running multiple Grafana instances. +If you are running multiple instances of Grafana you might run into problems if they have different versions of the `datasource.yaml` configuration file. The best way to solve this problem is to add a version number to each datasource in the configuration and increase it when you update the config. Grafana will only update datasources with the same or lower version number than specified in the config. That way, old configs cannot overwrite newer configs if they restart at the same time. ### Example datasource config file ```yaml @@ -86,7 +86,7 @@ delete_datasources: - name: Graphite org_id: 1 -# list of datasources to insert/update depending +# list of datasources to insert/update depending # whats available in the datbase datasources: # name of the datasource. Required @@ -116,7 +116,7 @@ datasources: # mark as default datasource. Max one per org is_default: # fields that will be converted to json and stored in json_data - json_data: + json_data: graphiteVersion: "1.1" tlsAuth: true tlsAuthWithCACert: true @@ -132,45 +132,46 @@ datasources: #### Json data -Since all datasources dont have the same configuration settings we only have the most common ones as fields. The rest should be stored as a json blob in the `json_data` field. Here are the most common settings that the core datasources use. +Since not all datasources have the same configuration settings we only have the most common ones as fields. The rest should be stored as a json blob in the `json_data` field. Here are the most common settings that the core datasources use. | Name | Type | Datasource |Description | | ----| ---- | ---- | --- | | tlsAuth | boolean | *All* | Enable TLS authentication using client cert configured in secure json data | | tlsAuthWithCACert | boolean | *All* | Enable TLS authtication using CA cert | +| tlsSkipVerify | boolean | *All* | Controls whether a client verifies the server's certificate chain and host name. | | graphiteVersion | string | Graphite | Graphite version | | timeInterval | string | Elastic, Influxdb & Prometheus | Lowest interval/step value that should be used for this data source | -| esVersion | string | Elastic | Elasticsearch version | -| timeField | string | Elastic | Which field that should be used as timestamp | +| esVersion | string | Elastic | Elasticsearch version | +| timeField | string | Elastic | Which field that should be used as timestamp | | interval | string | Elastic | Index date time format | | authType | string | Cloudwatch | Auth provider. keys/credentials/arn | -| assumeRoleArn | string | Cloudwatch | ARN of Assume Role | +| assumeRoleArn | string | Cloudwatch | ARN of Assume Role | | defaultRegion | string | Cloudwatch | AWS region | -| customMetricsNamespaces | string | Cloudwatch | Namespaces of Custom Metrics | +| customMetricsNamespaces | string | Cloudwatch | Namespaces of Custom Metrics | | tsdbVersion | string | OpenTsdb | Version | | tsdbResolution | string | OpenTsdb | Resolution | -| sslmode | string | Postgre | SSLmode. 'disable', 'require', 'verify-ca' or 'verify-full' | +| sslmode | string | Postgre | SSLmode. 'disable', 'require', 'verify-ca' or 'verify-full' | #### Secure Json data {"authType":"keys","defaultRegion":"us-west-2","timeField":"@timestamp"} -Secure json data is a map of settings that will be encrypted with [secret key](/installation/configuration/#secret-key) from the grafana config. The purpose of this is only to hide content from the users of the application. This should be used for storing TLS Cert and password that Grafana will append to request on the server side. All these settings are optional. +Secure json data is a map of settings that will be encrypted with [secret key](/installation/configuration/#secret-key) from the Grafana config. The purpose of this is only to hide content from the users of the application. This should be used for storing TLS Cert and password that Grafana will append to the request on the server side. All of these settings are optional. | Name | Type | Datasource | Description | | ----| ---- | ---- | --- | | tlsCACert | string | *All* |CA cert for out going requests | | tlsClientCert | string | *All* |TLS Client cert for outgoing requests | | tlsClientKey | string | *All* |TLS Client key for outgoing requests | -| password | string | Postgre | password | -| user | string | Postgre | user | +| password | string | Postgre | password | +| user | string | Postgre | user | ### Dashboards -It's possible to manage dashboards in Grafana by adding one or more yaml config files in the [`provisioning/dashboards`](/installation/configuration/#provisioning) directory. Each config file can contain a list of `dashboards providers` that will load dashboards into grafana. Currently we only support reading dashboards from file but we will add more providers in the future. +It's possible to manage dashboards in Grafana by adding one or more yaml config files in the [`provisioning/dashboards`](/installation/configuration/#provisioning) directory. Each config file can contain a list of `dashboards providers` that will load dashboards into Grafana. Currently we only support reading dashboards from file but we will add more providers in the future. -The dashboard provider config file looks like this +The dashboard provider config file looks somewhat like this: ```yaml - name: 'default' @@ -181,4 +182,4 @@ The dashboard provider config file looks like this folder: /var/lib/grafana/dashboards ``` -When grafana starts it will update/insert all dashboards available in the configured folders. If you modify the file the dashboard will also be updated. \ No newline at end of file +When Grafana starts, it will update/insert all dashboards available in the configured folders. If you modify the file, the dashboard will also be updated. diff --git a/docs/sources/alerting/metrics.md b/docs/sources/alerting/metrics.md index 02fee6a718a..271e43f4ecf 100644 --- a/docs/sources/alerting/metrics.md +++ b/docs/sources/alerting/metrics.md @@ -13,7 +13,7 @@ weight = 2 > Alerting is only available in Grafana v4.0 and above. -The alert engine publishes some internal metrics about itself. You can read more about how Grafana published [internal metrics](/installation/configuration/#metrics). +The alert engine publishes some internal metrics about itself. You can read more about how Grafana publishes [internal metrics](/installation/configuration/#metrics). Description | Type | Metric name ---------- | ----------- | ---------- diff --git a/docs/sources/alerting/notifications.md b/docs/sources/alerting/notifications.md index 0a233ac3577..9f94810ff4d 100644 --- a/docs/sources/alerting/notifications.md +++ b/docs/sources/alerting/notifications.md @@ -14,9 +14,9 @@ weight = 2 > Alerting is only available in Grafana v4.0 and above. -When an alert changes state it sends out notifications. Each alert rule can have -multiple notifications. But in order to add a notification to an alert rule you first need -to add and configure a `notification` channel (can be email, Pagerduty or other integration). This is done from the Notification Channels page. +When an alert changes state, it sends out notifications. Each alert rule can have +multiple notifications. In order to add a notification to an alert rule you first need +to add and configure a `notification` channel (can be email, PagerDuty or other integration). This is done from the Notification Channels page. ## Notification Channel Setup @@ -25,12 +25,12 @@ to add and configure a `notification` channel (can be email, Pagerduty or other On the Notification Channels page hit the `New Channel` button to go the page where you can configure and setup a new Notification Channel. -You specify name and type, and type specific options. You can also test the notification to make -sure it's working and setup correctly. +You specify a name and a type, and type specific options. You can also test the notification to make +sure it's setup correctly. ### Send on all alerts -When checked this option will make this notification used for all alert rules, existing and new. +When checked, this option will nofity for all alert rules - existing and new. ## Supported Notification Types @@ -38,39 +38,39 @@ Grafana ships with the following set of notification types: ### Email -To enable email notification you have to setup [SMTP settings](/installation/configuration/#smtp) -in the Grafana config. Email notification will upload an image of the alert graph to an -external image destination if available or fallback to attaching the image in the email. +To enable email notifications you have to setup [SMTP settings](/installation/configuration/#smtp) +in the Grafana config. Email notifications will upload an image of the alert graph to an +external image destination if available or fallback to attaching the image to the email. ### Slack {{< imgbox max-width="40%" img="/img/docs/v4/slack_notification.png" caption="Alerting Slack Notification" >}} -To set up slack you need to configure an incoming webhook url at slack. You can follow their guide for how -to do that https://api.slack.com/incoming-webhooks If you want to include screenshots of the firing alerts -in the slack messages you have to configure either the [external image destination](#external-image-store) in Grafana, +To set up slack you need to configure an incoming webhook url at slack. You can follow their guide on how +to do that [here](https://api.slack.com/incoming-webhooks). If you want to include screenshots of the firing alerts +in the Slack messages you have to configure either the [external image destination](#external-image-store) in Grafana, or a bot integration via Slack Apps. Follow Slack's guide to set up a bot integration and use the token provided -https://api.slack.com/bot-users, which starts with "xoxb". +(https://api.slack.com/bot-users), which starts with "xoxb". Setting | Description ---------- | ----------- -Recipient | allows you to override the slack recipient. -Mention | make it possible to include a mention in the slack notification sent by Grafana. Ex @here or @channel +Recipient | allows you to override the Slack recipient. +Mention | make it possible to include a mention in the Slack notification sent by Grafana. Ex @here or @channel Token | If provided, Grafana will upload the generated image via Slack's file.upload API method, not the external image destination. ### PagerDuty -To set up PagerDuty, all you have to do is to provide an api key. +To set up PagerDuty, all you have to do is to provide an API key. Setting | Description ---------- | ----------- -Integration Key | Integration key for pagerduty. -Auto resolve incidents | Resolve incidents in pagerduty once the alert goes back to ok +Integration Key | Integration key for PagerDuty. +Auto resolve incidents | Resolve incidents in PagerDuty once the alert goes back to ok ### Webhook -The webhook notification is a simple way to send information about an state change over HTTP to a custom endpoint. -Using this notification you could integrate Grafana into any system you choose, by yourself. +The webhook notification is a simple way to send information about a state change over HTTP to a custom endpoint. +Using this notification you could integrate Grafana into a system of your choosing. Example json body: @@ -117,19 +117,19 @@ Dingtalk supports the following "message type": `text`, `link` and `markdown`. O ### Kafka -Notifications can be sent to a Kafka topic from Grafana using [Kafka REST Proxy](https://docs.confluent.io/1.0/kafka-rest/docs/index.html). -There are couple of configurations options which need to be set in Grafana UI under Kafka Settings: +Notifications can be sent to a Kafka topic from Grafana using the [Kafka REST Proxy](https://docs.confluent.io/1.0/kafka-rest/docs/index.html). +There are a couple of configuration options which need to be set up in Grafana UI under Kafka Settings: 1. Kafka REST Proxy endpoint. 2. Kafka Topic. -Once these two properties are set, you can send the alerts to Kafka for further processing or throttling them. +Once these two properties are set, you can send the alerts to Kafka for further processing or throttling. ### All supported notifier -Name | Type |Support images ------|------------ | ------ +Name | Type |Support images +-----|------------ | ------ Slack | `slack` | yes Pagerduty | `pagerduty` | yes Email | `email` | yes @@ -150,13 +150,13 @@ Prometheus Alertmanager | `prometheus-alertmanager` | no # Enable images in notifications {#external-image-store} Grafana can render the panel associated with the alert rule and include that in the notification. Most Notification Channels require that this image be publicly accessible (Slack and PagerDuty for example). In order to include images in alert notifications, Grafana can upload the image to an image store. It currently supports -Amazon S3 and Webdav for this. So to set that up you need to configure the [external image uploader](/installation/configuration/#external-image-storage) in your grafana-server ini config file. +Amazon S3, Webdav, and Azure Blob Storage for this. So to set that up you need to configure the [external image uploader](/installation/configuration/#external-image-storage) in your grafana-server ini config file. Currently only the Email Channels attaches images if no external image store is specified. To include images in alert notifications for other channels then you need to set up an external image store. -This is an optional requirement, you can get Slack and email notifications without setting this up. +This is an optional requirement. You can get Slack and email notifications without setting this up. # Configure the link back to Grafana from alert notifications -All alert notifications contains a link back to the triggered alert in the Grafana instance. +All alert notifications contain a link back to the triggered alert in the Grafana instance. This url is based on the [domain](/installation/configuration/#domain) setting in Grafana. diff --git a/docs/sources/guides/whats-new-in-v4.md b/docs/sources/guides/whats-new-in-v4.md index 68a40ed7a1e..7b0d2259580 100644 --- a/docs/sources/guides/whats-new-in-v4.md +++ b/docs/sources/guides/whats-new-in-v4.md @@ -55,7 +55,7 @@ of another alert in your conditions, and `Time Of Day`. Alerting would not be very useful if there was no way to send notifications when rules trigger and change state. You can setup notifications of different types. We currently have `Slack`, `PagerDuty`, `Email` and `Webhook` with more in the pipe that will be added during beta period. The notifications can then be added to your alert rules. -If you have configured an external image store in the grafana.ini config file (s3 and webdav options available) +If you have configured an external image store in the grafana.ini config file (s3, webdav, and azure_blob options available) you can get very rich notifications with an image of the graph and the metric values all included in the notification. diff --git a/docs/sources/installation/behind_proxy.md b/docs/sources/installation/behind_proxy.md index 3d89d8b3c2c..e51a1d02697 100644 --- a/docs/sources/installation/behind_proxy.md +++ b/docs/sources/installation/behind_proxy.md @@ -68,5 +68,38 @@ server { } } ``` +### IIS URL Rewrite Rule (Windows) with Subpath +IIS requires that the URL Rewrite module is installed. +Given: + +- subpath `grafana` +- Grafana installed on `http://localhost:3000` +- server config: + ```bash + [server] + domain = localhost:8080 + root_url = %(protocol)s://%(domain)s:/grafana + ``` + +Create an Inbound Rule for the parent website (localhost:8080 in this example) with the following settings: + +- pattern: `grafana(/)?(.*)` +- check the `Ignore case` checkbox +- rewrite url set to `http://localhost:3000/{R:2}` +- check the `Append query string` checkbox +- check the `Stop processing of subsequent rules` checkbox + +The rewrite rule that is generated for the web.config: + +```xml + + + + + + + + +``` diff --git a/docs/sources/installation/configuration.md b/docs/sources/installation/configuration.md index 91094b3157d..01a26f38b9a 100644 --- a/docs/sources/installation/configuration.md +++ b/docs/sources/installation/configuration.md @@ -496,7 +496,7 @@ name = BitBucket enabled = true allow_sign_up = true client_id = -client_secret = +client_secret = scopes = account email auth_url = https://bitbucket.org/site/oauth2/authorize token_url = https://bitbucket.org/site/oauth2/access_token @@ -505,6 +505,41 @@ team_ids = allowed_organizations = ``` +### Set up oauth2 with OneLogin + +1. Create a new Custom Connector with the following settings: + - Name: Grafana + - Sign On Method: OpenID Connect + - Redirect URI: `https:///login/generic_oauth` + - Signing Algorithm: RS256 + - Login URL: `https:///login/generic_oauth` + + then: +2. Add an App to the Grafana Connector: + - Display Name: Grafana + + then: +3. Under the SSO tab on the Grafana App details page you'll find the Client ID and Client Secret. + + Your OneLogin Domain will match the url you use to access OneLogin. + + Configure Grafana as follows: + + ```bash + [auth.generic_oauth] + name = OneLogin + enabled = true + allow_sign_up = true + client_id = + client_secret = + scopes = openid email name + auth_url = https://.onelogin.com/oidc/auth + token_url = https://.onelogin.com/oidc/token + api_url = https://.onelogin.com/oidc/me + team_ids = + allowed_organizations = + ``` +
## [auth.basic] @@ -731,7 +766,7 @@ Time to live for snapshots. These options control how images should be made public so they can be shared on services like slack. ### provider -You can choose between (s3, webdav, gcs). If left empty Grafana will ignore the upload action. +You can choose between (s3, webdav, gcs, azure_blob). If left empty Grafana will ignore the upload action. ## [external_image_storage.s3] @@ -786,6 +821,17 @@ Bucket Name on Google Cloud Storage. ### path Optional extra path inside bucket +## [external_image_storage.azure_blob] + +### account_name +Storage account name + +### account_key +Storage account key + +### container_name +Container name where to store "Blob" images with random names. Creating the blob container beforehand is required. Only public containers are supported. + ## [alerting] ### enabled diff --git a/docs/sources/reference/dashboard.md b/docs/sources/reference/dashboard.md index d2da621a5a3..13f08a1ddaf 100644 --- a/docs/sources/reference/dashboard.md +++ b/docs/sources/reference/dashboard.md @@ -71,8 +71,8 @@ Each field in the dashboard JSON is explained below with its usage: | **timepicker** | timepicker metadata, see [timepicker section](#timepicker) for details | | **templating** | templating metadata, see [templating section](#templating) for details | | **annotations** | annotations metadata, see [annotations section](#annotations) for details | -| **schemaVersion** | TODO | -| **version** | TODO | +| **schemaVersion** | version of the JSON schema (integer), incremented each time a Grafana update brings changes to the said schema | +| **version** | version of the dashboard (integer), incremented each time the dashboard is updated | | **links** | TODO | ### rows diff --git a/docs/sources/tutorials/iis.md b/docs/sources/tutorials/iis.md new file mode 100644 index 00000000000..2dc38f78fae --- /dev/null +++ b/docs/sources/tutorials/iis.md @@ -0,0 +1,89 @@ ++++ +title = "Grafana with IIS Reverse Proxy on Windows" +type = "docs" +keywords = ["grafana", "tutorials", "proxy", "IIS", "windows"] +[menu.docs] +parent = "tutorials" +weight = 10 ++++ + +# How to Use IIS with URL Rewrite as a Reverse Proxy for Grafana on Windows + +If you want Grafana to be a subdomain under a website in IIS then the URL Rewrite module for ISS can be used to support this. + +Example: + +- Parent site: http://localhost:8080 +- Grafana: http://localhost:3000 + +Grafana as a subdomain: http://localhost:8080/grafana + +## Setup + +If you have not already done it, then a requirement is to install URL Rewrite module for IIS. + +Download and install the URL Rewrite module for IIS: https://www.iis.net/downloads/microsoft/url-rewrite + +## Grafana Config + +The Grafana config can be set by creating a file named `custom.ini` in the `conf` subdirectory of your Grafana installation. See the [installation instructions](http://docs.grafana.org/installation/windows/#configure) for more details. + +Given that the subpath should be `grafana` and the parent site is `localhost:8080` then add this to the `custom.ini` config file: + + ```bash +[server] +domain = localhost:8080 +root_url = %(protocol)s://%(domain)s:/grafana +``` + +Restart the Grafana server after changing the config file. + +## IIS Config + +1. Open the IIS Manager and click on the parent website +2. In the admin console for this website, double click on the Url Rewrite option: + {{< docs-imagebox img="/img/docs/tutorials/IIS_admin_console.png" max-width= "800px" >}} + +3. Click on the `Add Rule(s)...` action +4. Choose the Blank Rule template for an Inbound Rule + {{< docs-imagebox img="/img/docs/tutorials/IIS_add_inbound_rule.png" max-width= "800px" >}} + +5. Create an Inbound Rule for the parent website (localhost:8080 in this example) with the following settings: + - pattern: `grafana(/)?(.*)` + - check the `Ignore case` checkbox + - rewrite url set to `http://localhost:3000/{R:2}` + - check the `Append query string` checkbox + - check the `Stop processing of subsequent rules` checkbox + + {{< docs-imagebox img="/img/docs/tutorials/IIS_url_rewrite.png" max-width= "800px" >}} + +Finally, navigate to `http://localhost:8080/grafana` (replace `http://localhost:8080` with your parent domain) and you should come to the Grafana login page. + +## Troubleshooting + +### 404 error + +When navigating to the grafana url (`http://localhost:8080/grafana` in the example above) and a `HTTP Error 404.0 - Not Found` error is returned then either: + +- the pattern for the Inbound Rule is incorrect. Edit the rule, click on the `Test pattern...` button, test the part of the url after `http://localhost:8080/` and make sure it matches. For `grafana/login` the test should return 3 capture groups: {R:0}: `grafana` {R:1}: `/` and {R:2}: `login`. +- The `root_url` setting in the Grafana config file does not match the parent url with subpath. + +### Grafana Website only shows text with no images or css + +{{< docs-imagebox img="/img/docs/tutorials/IIS_proxy_error.png" max-width= "800px" >}} + +1. The `root_url` setting in the Grafana config file does not match the parent url with subpath. This could happen if the root_url is commented out by mistake (`;` is used for commenting out a line in .ini files): + + `; root_url = %(protocol)s://%(domain)s:/grafana` + +2. or if the subpath in the `root_url` setting does not match the subpath used in the pattern in the Inbound Rule in IIS: + + `root_url = %(protocol)s://%(domain)s:/grafana` + + pattern in Inbound Rule: `wrongsubpath(/)?(.*)` + +3. or if the Rewrite Url in the Inbound Rule is incorrect. + + The Rewrite Url should not include the subpath. + + The Rewrite Url should contain the capture group from the pattern matching that returns the part of the url after the subpath. The pattern used above returns 3 capture groups and the third one {R:2} returns the part of the url after `http://localhost:8080/grafana/`. diff --git a/jest.config.js b/jest.config.js index ead97e39dad..606465c9840 100644 --- a/jest.config.js +++ b/jest.config.js @@ -24,5 +24,6 @@ module.exports = { "setupFiles": [ "./public/test/jest-shim.ts", "./public/test/jest-setup.ts" - ] + ], + "snapshotSerializers": ["enzyme-to-json/serializer"], }; diff --git a/package.json b/package.json index 847f1738e45..01241637d46 100644 --- a/package.json +++ b/package.json @@ -25,6 +25,7 @@ "css-loader": "^0.28.7", "enzyme": "^3.1.0", "enzyme-adapter-react-16": "^1.0.1", + "enzyme-to-json": "^3.3.0", "es6-promise": "^3.0.2", "es6-shim": "^0.35.3", "expect.js": "~0.2.0", @@ -54,7 +55,7 @@ "html-loader": "^0.5.1", "html-webpack-plugin": "^2.30.1", "husky": "^0.14.3", - "jest": "^21.2.1", + "jest": "^22.0.4", "jshint-stylish": "~2.2.1", "json-loader": "^0.5.7", "karma": "1.7.0", @@ -83,14 +84,15 @@ "sinon": "1.17.6", "systemjs": "0.20.19", "systemjs-plugin-css": "^0.1.36", - "ts-jest": "^21.1.3", - "ts-loader": "^2.3.7", - "tslint": "^5.7.0", + "ts-jest": "^22.0.0", + "ts-loader": "^3.2.0", + "tslint": "^5.8.0", "tslint-loader": "^3.5.3", - "typescript": "^2.5.2", - "webpack": "^3.6.0", + "typescript": "^2.6.2", + "webpack": "^3.10.0", "webpack-bundle-analyzer": "^2.9.0", "webpack-cleanup-plugin": "^0.5.1", + "angular-mocks": "^1.6.6", "webpack-merge": "^4.1.0", "zone.js": "^0.7.2" }, @@ -107,19 +109,23 @@ }, "lint-staged": { "*.{ts,tsx}": [ - "prettier --single-quote --trailing-comma es5 --write", + "prettier --write", "git add" ], "*.scss": [ - "prettier --single-quote --write", + "prettier --write", "git add" ] }, + "prettier": { + "trailingComma": "es5", + "singleQuote": true, + "printWidth": 120 + }, "license": "Apache-2.0", "dependencies": { "angular": "^1.6.6", "angular-bindonce": "^0.3.1", - "angular-mocks": "^1.6.6", "angular-native-dragdrop": "^1.2.2", "angular-route": "^1.6.6", "angular-sanitize": "^1.6.6", @@ -133,13 +139,19 @@ "file-saver": "^1.3.3", "jquery": "^3.2.1", "lodash": "^4.17.4", + "mobx": "^3.4.1", + "mobx-react": "^4.3.5", + "mobx-state-tree": "^1.3.1", "moment": "^2.18.1", "mousetrap": "^1.6.0", "perfect-scrollbar": "^1.2.0", "prop-types": "^15.6.0", - "react": "^16.1.1", - "react-dom": "^16.1.1", + "react": "^16.2.0", + "react-dom": "^16.2.0", "react-grid-layout": "^0.16.1", + "react-popper": "^0.7.5", + "react-highlight-words": "^0.10.0", + "react-select": "^1.1.0", "react-sizeme": "^2.3.6", "remarkable": "^1.7.1", "rxjs": "^5.4.3", diff --git a/pkg/api/alerting.go b/pkg/api/alerting.go index 58ea56cc8a7..d11122016c4 100644 --- a/pkg/api/alerting.go +++ b/pkg/api/alerting.go @@ -278,7 +278,7 @@ func PauseAlert(c *middleware.Context, dto dtos.PauseAlertCommand) Response { } var response models.AlertStateType = models.AlertStatePending - pausedState := "un paused" + pausedState := "un-paused" if cmd.Paused { response = models.AlertStatePaused pausedState = "paused" @@ -287,7 +287,7 @@ func PauseAlert(c *middleware.Context, dto dtos.PauseAlertCommand) Response { result := map[string]interface{}{ "alertId": alertId, "state": response, - "message": "alert " + pausedState, + "message": "Alert " + pausedState, } return Json(200, result) diff --git a/pkg/api/api.go b/pkg/api/api.go index 3f6d8d4d954..ea082ff4741 100644 --- a/pkg/api/api.go +++ b/pkg/api/api.go @@ -40,8 +40,11 @@ func (hs *HttpServer) registerRoutes() { r.Get("/datasources/", reqSignedIn, Index) r.Get("/datasources/new", reqSignedIn, Index) r.Get("/datasources/edit/*", reqSignedIn, Index) + r.Get("/org/users", reqSignedIn, Index) r.Get("/org/users/new", reqSignedIn, Index) r.Get("/org/users/invite", reqSignedIn, Index) + r.Get("/org/teams", reqSignedIn, Index) + r.Get("/org/teams/*", reqSignedIn, Index) r.Get("/org/apikeys/", reqSignedIn, Index) r.Get("/dashboard/import/", reqSignedIn, Index) r.Get("/configuration", reqGrafanaAdmin, Index) diff --git a/pkg/api/avatar/avatar.go b/pkg/api/avatar/avatar.go index 6824e330f00..ce9da1e8790 100644 --- a/pkg/api/avatar/avatar.go +++ b/pkg/api/avatar/avatar.go @@ -157,11 +157,11 @@ func NewCacheServer() *CacheServer { func newNotFound() *Avatar { avatar := &Avatar{notFound: true} - // load transparent png into buffer - path := filepath.Join(setting.StaticRootPath, "img", "transparent.png") + // load user_profile png into buffer + path := filepath.Join(setting.StaticRootPath, "img", "user_profile.png") if data, err := ioutil.ReadFile(path); err != nil { - log.Error(3, "Failed to read transparent.png, %v", path) + log.Error(3, "Failed to read user_profile.png, %v", path) } else { avatar.data = bytes.NewBuffer(data) } diff --git a/pkg/api/dtos/models.go b/pkg/api/dtos/models.go index 2e9aa78d7d5..a702b06fad5 100644 --- a/pkg/api/dtos/models.go +++ b/pkg/api/dtos/models.go @@ -3,6 +3,7 @@ package dtos import ( "crypto/md5" "fmt" + "regexp" "strings" "github.com/grafana/grafana/pkg/components/simplejson" @@ -57,3 +58,19 @@ func GetGravatarUrl(text string) string { hasher.Write([]byte(strings.ToLower(text))) return fmt.Sprintf(setting.AppSubUrl+"/avatar/%x", hasher.Sum(nil)) } + +func GetGravatarUrlWithDefault(text string, defaultText string) string { + if text != "" { + return GetGravatarUrl(text) + } + + reg, err := regexp.Compile("[^a-zA-Z0-9]+") + + if err != nil { + return "" + } + + text = reg.ReplaceAllString(defaultText, "") + "@localhost" + + return GetGravatarUrl(text) +} diff --git a/pkg/api/login_oauth.go b/pkg/api/login_oauth.go index 847f09f0eb8..b63e3bfd89f 100644 --- a/pkg/api/login_oauth.go +++ b/pkg/api/login_oauth.go @@ -35,7 +35,7 @@ var ( func GenStateString() string { rnd := make([]byte, 32) rand.Read(rnd) - return base64.StdEncoding.EncodeToString(rnd) + return base64.URLEncoding.EncodeToString(rnd) } func OAuthLogin(ctx *middleware.Context) { diff --git a/pkg/api/render.go b/pkg/api/render.go index be85da35853..65733cfab15 100644 --- a/pkg/api/render.go +++ b/pkg/api/render.go @@ -31,11 +31,12 @@ func RenderToPng(c *middleware.Context) { pngPath, err := renderer.RenderToPng(renderOpts) - if err != nil { - if err == renderer.ErrTimeout { - c.Handle(500, err.Error(), err) - } + if err != nil && err == renderer.ErrTimeout { + c.Handle(500, err.Error(), err) + return + } + if err != nil { c.Handle(500, "Rendering failed.", err) return } diff --git a/pkg/api/team.go b/pkg/api/team.go index 31e465d3232..af537224d41 100644 --- a/pkg/api/team.go +++ b/pkg/api/team.go @@ -1,6 +1,7 @@ package api import ( + "github.com/grafana/grafana/pkg/api/dtos" "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/middleware" m "github.com/grafana/grafana/pkg/models" @@ -70,6 +71,10 @@ func SearchTeams(c *middleware.Context) Response { return ApiError(500, "Failed to search Teams", err) } + for _, team := range query.Result.Teams { + team.AvatarUrl = dtos.GetGravatarUrlWithDefault(team.Email, team.Name) + } + query.Result.Page = page query.Result.PerPage = perPage diff --git a/pkg/api/team_members.go b/pkg/api/team_members.go index 0999c9573a5..412e142edb7 100644 --- a/pkg/api/team_members.go +++ b/pkg/api/team_members.go @@ -1,6 +1,7 @@ package api import ( + "github.com/grafana/grafana/pkg/api/dtos" "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/middleware" m "github.com/grafana/grafana/pkg/models" @@ -15,6 +16,10 @@ func GetTeamMembers(c *middleware.Context) Response { return ApiError(500, "Failed to get Team Members", err) } + for _, member := range query.Result { + member.AvatarUrl = dtos.GetGravatarUrl(member.Email) + } + return Json(200, query.Result) } diff --git a/pkg/components/imguploader/azureblobuploader.go b/pkg/components/imguploader/azureblobuploader.go new file mode 100644 index 00000000000..40d2de836be --- /dev/null +++ b/pkg/components/imguploader/azureblobuploader.go @@ -0,0 +1,320 @@ +package imguploader + +import ( + "bytes" + "context" + "crypto/hmac" + "crypto/sha256" + "encoding/base64" + "encoding/xml" + "fmt" + "io" + "io/ioutil" + "mime" + "net/http" + "net/url" + "os" + "path" + "sort" + "strconv" + "strings" + "time" + + "github.com/grafana/grafana/pkg/log" + "github.com/grafana/grafana/pkg/util" +) + +type AzureBlobUploader struct { + account_name string + account_key string + container_name string + log log.Logger +} + +func NewAzureBlobUploader(account_name string, account_key string, container_name string) *AzureBlobUploader { + return &AzureBlobUploader{ + account_name: account_name, + account_key: account_key, + container_name: container_name, + log: log.New("azureBlobUploader"), + } +} + +// Receive path of image on disk and return azure blob url +func (az *AzureBlobUploader) Upload(ctx context.Context, imageDiskPath string) (string, error) { + // setup client + blob := NewStorageClient(az.account_name, az.account_key) + + file, err := os.Open(imageDiskPath) + + if err != nil { + return "", err + } + randomFileName := util.GetRandomString(30) + ".png" + // upload image + az.log.Debug("Uploading image to azure_blob", "conatiner_name", az.container_name, "blob_name", randomFileName) + resp, err := blob.FileUpload(az.container_name, randomFileName, file) + if err != nil { + return "", err + } + + if resp.StatusCode > 400 && resp.StatusCode < 600 { + body, _ := ioutil.ReadAll(io.LimitReader(resp.Body, 1<<20)) + aerr := &Error{ + Code: resp.StatusCode, + Status: resp.Status, + Body: body, + Header: resp.Header, + } + aerr.parseXML() + resp.Body.Close() + return "", aerr + } + + if err != nil { + return "", err + } + + url := fmt.Sprintf("https://%s.blob.core.windows.net/%s/%s", az.account_name, az.container_name, randomFileName) + return url, nil +} + +// --- AZURE LIBRARY +type Blobs struct { + XMLName xml.Name `xml:"EnumerationResults"` + Items []Blob `xml:"Blobs>Blob"` +} + +type Blob struct { + Name string `xml:"Name"` + Property Property `xml:"Properties"` +} + +type Property struct { + LastModified string `xml:"Last-Modified"` + Etag string `xml:"Etag"` + ContentLength int `xml:"Content-Length"` + ContentType string `xml:"Content-Type"` + BlobType string `xml:"BlobType"` + LeaseStatus string `xml:"LeaseStatus"` +} + +type Error struct { + Code int + Status string + Body []byte + Header http.Header + + AzureCode string +} + +func (e *Error) Error() string { + return fmt.Sprintf("status %d: %s", e.Code, e.Body) +} + +func (e *Error) parseXML() { + var xe xmlError + _ = xml.NewDecoder(bytes.NewReader(e.Body)).Decode(&xe) + e.AzureCode = xe.Code +} + +type xmlError struct { + XMLName xml.Name `xml:"Error"` + Code string + Message string +} + +const ms_date_layout = "Mon, 02 Jan 2006 15:04:05 GMT" +const version = "2017-04-17" + +var client = &http.Client{} + +type StorageClient struct { + Auth *Auth + Transport http.RoundTripper +} + +func (c *StorageClient) transport() http.RoundTripper { + if c.Transport != nil { + return c.Transport + } + return http.DefaultTransport +} + +func NewStorageClient(account, accessKey string) *StorageClient { + return &StorageClient{ + Auth: &Auth{ + account, + accessKey, + }, + Transport: nil, + } +} + +func (c *StorageClient) absUrl(format string, a ...interface{}) string { + part := fmt.Sprintf(format, a...) + return fmt.Sprintf("https://%s.blob.core.windows.net/%s", c.Auth.Account, part) +} + +func copyHeadersToRequest(req *http.Request, headers map[string]string) { + for k, v := range headers { + req.Header[k] = []string{v} + } +} + +func (c *StorageClient) FileUpload(container, blobName string, body io.Reader) (*http.Response, error) { + blobName = escape(blobName) + extension := strings.ToLower(path.Ext(blobName)) + contentType := mime.TypeByExtension(extension) + buf := new(bytes.Buffer) + buf.ReadFrom(body) + req, err := http.NewRequest( + "PUT", + c.absUrl("%s/%s", container, blobName), + buf, + ) + if err != nil { + return nil, err + } + + copyHeadersToRequest(req, map[string]string{ + "x-ms-blob-type": "BlockBlob", + "x-ms-date": time.Now().UTC().Format(ms_date_layout), + "x-ms-version": version, + "Accept-Charset": "UTF-8", + "Content-Type": contentType, + "Content-Length": strconv.Itoa(buf.Len()), + }) + + c.Auth.SignRequest(req) + + return c.transport().RoundTrip(req) +} + +func escape(content string) string { + content = url.QueryEscape(content) + // the Azure's behavior uses %20 to represent whitespace instead of + (plus) + content = strings.Replace(content, "+", "%20", -1) + // the Azure's behavior uses slash instead of + %2F + content = strings.Replace(content, "%2F", "/", -1) + + return content +} + +type Auth struct { + Account string + Key string +} + +func (a *Auth) SignRequest(req *http.Request) { + strToSign := fmt.Sprintf("%s\n%s\n%s\n%s\n%s\n%s\n%s\n%s\n%s\n%s\n%s\n%s\n%s\n%s", + strings.ToUpper(req.Method), + tryget(req.Header, "Content-Encoding"), + tryget(req.Header, "Content-Language"), + tryget(req.Header, "Content-Length"), + tryget(req.Header, "Content-MD5"), + tryget(req.Header, "Content-Type"), + tryget(req.Header, "Date"), + tryget(req.Header, "If-Modified-Since"), + tryget(req.Header, "If-Match"), + tryget(req.Header, "If-None-Match"), + tryget(req.Header, "If-Unmodified-Since"), + tryget(req.Header, "Range"), + a.canonicalizedHeaders(req), + a.canonicalizedResource(req), + ) + decodedKey, _ := base64.StdEncoding.DecodeString(a.Key) + + sha256 := hmac.New(sha256.New, []byte(decodedKey)) + sha256.Write([]byte(strToSign)) + + signature := base64.StdEncoding.EncodeToString(sha256.Sum(nil)) + + copyHeadersToRequest(req, map[string]string{ + "Authorization": fmt.Sprintf("SharedKey %s:%s", a.Account, signature), + }) +} + +func tryget(headers map[string][]string, key string) string { + // We default to empty string for "0" values to match server side behavior when generating signatures. + if len(headers[key]) > 0 { // && headers[key][0] != "0" { //&& key != "Content-Length" { + return headers[key][0] + } + return "" +} + +// +// The following is copied ~95% verbatim from: +// http://github.com/loldesign/azure/ -> core/core.go +// + +/* +Based on Azure docs: + Link: http://msdn.microsoft.com/en-us/library/windowsazure/dd179428.aspx#Constructing_Element + + 1) Retrieve all headers for the resource that begin with x-ms-, including the x-ms-date header. + 2) Convert each HTTP header name to lowercase. + 3) Sort the headers lexicographically by header name, in ascending order. Note that each header may appear only once in the string. + 4) Unfold the string by replacing any breaking white space with a single space. + 5) Trim any white space around the colon in the header. + 6) Finally, append a new line character to each canonicalized header in the resulting list. Construct the CanonicalizedHeaders string by concatenating all headers in this list into a single string. +*/ +func (a *Auth) canonicalizedHeaders(req *http.Request) string { + var buffer bytes.Buffer + + for key, value := range req.Header { + lowerKey := strings.ToLower(key) + + if strings.HasPrefix(lowerKey, "x-ms-") { + if buffer.Len() == 0 { + buffer.WriteString(fmt.Sprintf("%s:%s", lowerKey, value[0])) + } else { + buffer.WriteString(fmt.Sprintf("\n%s:%s", lowerKey, value[0])) + } + } + } + + splitted := strings.Split(buffer.String(), "\n") + sort.Strings(splitted) + + return strings.Join(splitted, "\n") +} + +/* +Based on Azure docs + Link: http://msdn.microsoft.com/en-us/library/windowsazure/dd179428.aspx#Constructing_Element + +1) Beginning with an empty string (""), append a forward slash (/), followed by the name of the account that owns the resource being accessed. +2) Append the resource's encoded URI path, without any query parameters. +3) Retrieve all query parameters on the resource URI, including the comp parameter if it exists. +4) Convert all parameter names to lowercase. +5) Sort the query parameters lexicographically by parameter name, in ascending order. +6) URL-decode each query parameter name and value. +7) Append each query parameter name and value to the string in the following format, making sure to include the colon (:) between the name and the value: + parameter-name:parameter-value + +8) If a query parameter has more than one value, sort all values lexicographically, then include them in a comma-separated list: + parameter-name:parameter-value-1,parameter-value-2,parameter-value-n + +9) Append a new line character (\n) after each name-value pair. + +Rules: + 1) Avoid using the new line character (\n) in values for query parameters. If it must be used, ensure that it does not affect the format of the canonicalized resource string. + 2) Avoid using commas in query parameter values. +*/ +func (a *Auth) canonicalizedResource(req *http.Request) string { + var buffer bytes.Buffer + + buffer.WriteString(fmt.Sprintf("/%s%s", a.Account, req.URL.Path)) + queries := req.URL.Query() + + for key, values := range queries { + sort.Strings(values) + buffer.WriteString(fmt.Sprintf("\n%s:%s", key, strings.Join(values, ","))) + } + + splitted := strings.Split(buffer.String(), "\n") + sort.Strings(splitted) + + return strings.Join(splitted, "\n") +} diff --git a/pkg/components/imguploader/azureblobuploader_test.go b/pkg/components/imguploader/azureblobuploader_test.go new file mode 100644 index 00000000000..570e105b321 --- /dev/null +++ b/pkg/components/imguploader/azureblobuploader_test.go @@ -0,0 +1,24 @@ +package imguploader + +import ( + "context" + "testing" + + "github.com/grafana/grafana/pkg/setting" + . "github.com/smartystreets/goconvey/convey" +) + +func TestUploadToAzureBlob(t *testing.T) { + SkipConvey("[Integration test] for external_image_store.azure_blob", t, func() { + err := setting.NewConfigContext(&setting.CommandLineArgs{ + HomePath: "../../../", + }) + + uploader, _ := NewImageUploader() + + path, err := uploader.Upload(context.Background(), "../../../public/img/logo_transparent_400x.png") + + So(err, ShouldBeNil) + So(path, ShouldNotEqual, "") + }) +} diff --git a/pkg/components/imguploader/imguploader.go b/pkg/components/imguploader/imguploader.go index fd14b5d6739..383d2c6d311 100644 --- a/pkg/components/imguploader/imguploader.go +++ b/pkg/components/imguploader/imguploader.go @@ -3,6 +3,7 @@ package imguploader import ( "context" "fmt" + "github.com/grafana/grafana/pkg/log" "regexp" "github.com/grafana/grafana/pkg/setting" @@ -76,6 +77,21 @@ func NewImageUploader() (ImageUploader, error) { path := gcssec.Key("path").MustString("") return NewGCSUploader(keyFile, bucketName, path), nil + case "azure_blob": + azureBlobSec, err := setting.Cfg.GetSection("external_image_storage.azure_blob") + if err != nil { + return nil, err + } + + account_name := azureBlobSec.Key("account_name").MustString("") + account_key := azureBlobSec.Key("account_key").MustString("") + container_name := azureBlobSec.Key("container_name").MustString("") + + return NewAzureBlobUploader(account_name, account_key, container_name), nil + } + + if setting.ImageUploadProvider != "" { + log.Error2("The external image storage configuration is invalid", "unsupported provider", setting.ImageUploadProvider) } return NopImageUploader{}, nil diff --git a/pkg/components/imguploader/imguploader_test.go b/pkg/components/imguploader/imguploader_test.go index 44b4c090008..d5008c9ae9f 100644 --- a/pkg/components/imguploader/imguploader_test.go +++ b/pkg/components/imguploader/imguploader_test.go @@ -119,5 +119,29 @@ func TestImageUploaderFactory(t *testing.T) { So(original.keyFile, ShouldEqual, "/etc/secrets/project-79a52befa3f6.json") So(original.bucket, ShouldEqual, "project-grafana-east") }) + + Convey("AzureBlobUploader config", func() { + setting.NewConfigContext(&setting.CommandLineArgs{ + HomePath: "../../../", + }) + setting.ImageUploadProvider = "azure_blob" + + Convey("with container name", func() { + azureBlobSec, err := setting.Cfg.GetSection("external_image_storage.azure_blob") + azureBlobSec.NewKey("account_name", "account_name") + azureBlobSec.NewKey("account_key", "account_key") + azureBlobSec.NewKey("container_name", "container_name") + + uploader, err := NewImageUploader() + + So(err, ShouldBeNil) + original, ok := uploader.(*AzureBlobUploader) + + So(ok, ShouldBeTrue) + So(original.account_name, ShouldEqual, "account_name") + So(original.account_key, ShouldEqual, "account_key") + So(original.container_name, ShouldEqual, "container_name") + }) + }) }) } diff --git a/pkg/models/dashboards.go b/pkg/models/dashboards.go index 51df2e55a57..091f27ec413 100644 --- a/pkg/models/dashboards.go +++ b/pkg/models/dashboards.go @@ -139,8 +139,12 @@ func (dash *Dashboard) GetString(prop string, defaultValue string) string { // UpdateSlug updates the slug func (dash *Dashboard) UpdateSlug() { - title := strings.ToLower(dash.Data.Get("title").MustString()) - dash.Slug = slug.Make(title) + title := dash.Data.Get("title").MustString() + dash.Slug = SlugifyTitle(title) +} + +func SlugifyTitle(title string) string { + return slug.Make(strings.ToLower(title)) } // diff --git a/pkg/models/dashboards_test.go b/pkg/models/dashboards_test.go index 0ec773dfb97..ad865b575bb 100644 --- a/pkg/models/dashboards_test.go +++ b/pkg/models/dashboards_test.go @@ -16,6 +16,12 @@ func TestDashboardModel(t *testing.T) { So(dashboard.Slug, ShouldEqual, "grafana-play-home") }) + Convey("Can slugify title", t, func() { + slug := SlugifyTitle("Grafana Play Home") + + So(slug, ShouldEqual, "grafana-play-home") + }) + Convey("Given a dashboard json", t, func() { json := simplejson.New() json.Set("title", "test dash") diff --git a/pkg/models/team.go b/pkg/models/team.go index b9759f059cf..d2912f431b8 100644 --- a/pkg/models/team.go +++ b/pkg/models/team.go @@ -16,6 +16,7 @@ type Team struct { Id int64 `json:"id"` OrgId int64 `json:"orgId"` Name string `json:"name"` + Email string `json:"email"` Created time.Time `json:"created"` Updated time.Time `json:"updated"` @@ -26,14 +27,16 @@ type Team struct { type CreateTeamCommand struct { Name string `json:"name" binding:"Required"` + Email string `json:"email"` OrgId int64 `json:"-"` Result Team `json:"-"` } type UpdateTeamCommand struct { - Id int64 - Name string + Id int64 + Name string + Email string } type DeleteTeamCommand struct { @@ -64,6 +67,8 @@ type SearchTeamDto struct { Id int64 `json:"id"` OrgId int64 `json:"orgId"` Name string `json:"name"` + Email string `json:"email"` + AvatarUrl string `json:"avatarUrl"` MemberCount int64 `json:"memberCount"` } diff --git a/pkg/models/team_member.go b/pkg/models/team_member.go index 71e5cd4ba12..9970678a1ae 100644 --- a/pkg/models/team_member.go +++ b/pkg/models/team_member.go @@ -47,9 +47,10 @@ type GetTeamMembersQuery struct { // Projections and DTOs type TeamMemberDTO struct { - OrgId int64 `json:"orgId"` - TeamId int64 `json:"teamId"` - UserId int64 `json:"userId"` - Email string `json:"email"` - Login string `json:"login"` + OrgId int64 `json:"orgId"` + TeamId int64 `json:"teamId"` + UserId int64 `json:"userId"` + Email string `json:"email"` + Login string `json:"login"` + AvatarUrl string `json:"avatarUrl"` } diff --git a/pkg/plugins/frontend_plugin.go b/pkg/plugins/frontend_plugin.go index 90c18d90044..04af4060169 100644 --- a/pkg/plugins/frontend_plugin.go +++ b/pkg/plugins/frontend_plugin.go @@ -40,7 +40,7 @@ func getPluginLogoUrl(pluginType, path, baseUrl string) string { } func (fp *FrontendPluginBase) setPathsBasedOnApp(app *AppPlugin) { - appSubPath := strings.Replace(strings.Replace(fp.PluginDir, app.PluginDir, "", 1), "\\", "/", 1) + appSubPath := strings.Replace(strings.Replace(fp.PluginDir, app.PluginDir, "", 1), "\\", "/", -1) fp.IncludedInAppId = app.Id fp.BaseUrl = app.BaseUrl diff --git a/pkg/plugins/frontend_plugin_test.go b/pkg/plugins/frontend_plugin_test.go index 304cd38deae..311f9fcc510 100644 --- a/pkg/plugins/frontend_plugin_test.go +++ b/pkg/plugins/frontend_plugin_test.go @@ -14,7 +14,7 @@ func TestFrontendPlugin(t *testing.T) { fp := &FrontendPluginBase{ PluginBase: PluginBase{ - PluginDir: "c:\\grafana\\public\\app\\plugins\\app\\testdata\\datasource", + PluginDir: "c:\\grafana\\public\\app\\plugins\\app\\testdata\\datasources\\datasource", BaseUrl: "fpbase", }, } @@ -29,6 +29,6 @@ func TestFrontendPlugin(t *testing.T) { } fp.setPathsBasedOnApp(app) - So(fp.Module, ShouldEqual, "app/plugins/app/testdata/datasource/module") + So(fp.Module, ShouldEqual, "app/plugins/app/testdata/datasources/datasource/module") }) } diff --git a/pkg/services/alerting/notifiers/pagerduty.go b/pkg/services/alerting/notifiers/pagerduty.go index 35f90a9e4b7..c4067abec3b 100644 --- a/pkg/services/alerting/notifiers/pagerduty.go +++ b/pkg/services/alerting/notifiers/pagerduty.go @@ -42,7 +42,7 @@ var ( ) func NewPagerdutyNotifier(model *m.AlertNotification) (alerting.Notifier, error) { - autoResolve := model.Settings.Get("autoResolve").MustBool(true) + autoResolve := model.Settings.Get("autoResolve").MustBool(false) key := model.Settings.Get("integrationKey").MustString() if key == "" { return nil, alerting.ValidationError{Reason: "Could not find integration key property in settings"} diff --git a/pkg/services/alerting/notifiers/pagerduty_test.go b/pkg/services/alerting/notifiers/pagerduty_test.go index 522bb133d77..1d2eeec4a52 100644 --- a/pkg/services/alerting/notifiers/pagerduty_test.go +++ b/pkg/services/alerting/notifiers/pagerduty_test.go @@ -10,7 +10,6 @@ import ( func TestPagerdutyNotifier(t *testing.T) { Convey("Pagerduty notifier tests", t, func() { - Convey("Parsing alert notification from settings", func() { Convey("empty settings should return error", func() { json := `{ }` @@ -26,10 +25,31 @@ func TestPagerdutyNotifier(t *testing.T) { So(err, ShouldNotBeNil) }) + Convey("auto resolve should default to false", func() { + json := `{ "integrationKey": "abcdefgh0123456789" }` + + settingsJSON, _ := simplejson.NewJson([]byte(json)) + model := &m.AlertNotification{ + Name: "pagerduty_testing", + Type: "pagerduty", + Settings: settingsJSON, + } + + not, err := NewPagerdutyNotifier(model) + pagerdutyNotifier := not.(*PagerdutyNotifier) + + So(err, ShouldBeNil) + So(pagerdutyNotifier.Name, ShouldEqual, "pagerduty_testing") + So(pagerdutyNotifier.Type, ShouldEqual, "pagerduty") + So(pagerdutyNotifier.Key, ShouldEqual, "abcdefgh0123456789") + So(pagerdutyNotifier.AutoResolve, ShouldBeFalse) + }) + Convey("settings should trigger incident", func() { json := ` { - "integrationKey": "abcdefgh0123456789" + "integrationKey": "abcdefgh0123456789", + "autoResolve": false }` settingsJSON, _ := simplejson.NewJson([]byte(json)) @@ -46,8 +66,8 @@ func TestPagerdutyNotifier(t *testing.T) { So(pagerdutyNotifier.Name, ShouldEqual, "pagerduty_testing") So(pagerdutyNotifier.Type, ShouldEqual, "pagerduty") So(pagerdutyNotifier.Key, ShouldEqual, "abcdefgh0123456789") + So(pagerdutyNotifier.AutoResolve, ShouldBeFalse) }) - }) }) } diff --git a/pkg/services/alerting/notifiers/telegram.go b/pkg/services/alerting/notifiers/telegram.go index 62da584d019..0ca1ad3dfe0 100644 --- a/pkg/services/alerting/notifiers/telegram.go +++ b/pkg/services/alerting/notifiers/telegram.go @@ -1,17 +1,19 @@ package notifiers import ( + "bytes" "fmt" - "github.com/grafana/grafana/pkg/bus" - "github.com/grafana/grafana/pkg/components/simplejson" "github.com/grafana/grafana/pkg/log" m "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/alerting" + "io" + "mime/multipart" + "os" ) var ( - telegeramApiUrl string = "https://api.telegram.org/bot%s/%s" + telegramApiUrl string = "https://api.telegram.org/bot%s/%s" ) func init() { @@ -47,9 +49,10 @@ func init() { type TelegramNotifier struct { NotifierBase - BotToken string - ChatID string - log log.Logger + BotToken string + ChatID string + UploadImage bool + log log.Logger } func NewTelegramNotifier(model *m.AlertNotification) (alerting.Notifier, error) { @@ -59,6 +62,7 @@ func NewTelegramNotifier(model *m.AlertNotification) (alerting.Notifier, error) botToken := model.Settings.Get("bottoken").MustString() chatId := model.Settings.Get("chatid").MustString() + uploadImage := model.Settings.Get("uploadImage").MustBool() if botToken == "" { return nil, alerting.ValidationError{Reason: "Could not find Bot Token in settings"} @@ -72,31 +76,42 @@ func NewTelegramNotifier(model *m.AlertNotification) (alerting.Notifier, error) NotifierBase: NewNotifierBase(model.Id, model.IsDefault, model.Name, model.Type, model.Settings), BotToken: botToken, ChatID: chatId, + UploadImage: uploadImage, log: log.New("alerting.notifier.telegram"), }, nil } -func (this *TelegramNotifier) ShouldNotify(context *alerting.EvalContext) bool { - return defaultShouldNotify(context) -} +func (this *TelegramNotifier) buildMessage(evalContext *alerting.EvalContext, sendImageInline bool) *m.SendWebhookSync { + var imageFile *os.File + var err error -func (this *TelegramNotifier) Notify(evalContext *alerting.EvalContext) error { - this.log.Info("Sending alert notification to", "bot_token", this.BotToken) - this.log.Info("Sending alert notification to", "chat_id", this.ChatID) + if sendImageInline { + imageFile, err = os.Open(evalContext.ImageOnDiskPath) + defer imageFile.Close() + if err != nil { + sendImageInline = false // fall back to text message + } + } - bodyJSON := simplejson.New() + message := "" - bodyJSON.Set("chat_id", this.ChatID) - bodyJSON.Set("parse_mode", "html") - - message := fmt.Sprintf("%s\nState: %s\nMessage: %s\n", evalContext.GetNotificationTitle(), evalContext.Rule.Name, evalContext.Rule.Message) + if sendImageInline { + // Telegram's API does not allow HTML formatting for image captions. + message = fmt.Sprintf("%s\nState: %s\nMessage: %s\n", evalContext.GetNotificationTitle(), evalContext.Rule.Name, evalContext.Rule.Message) + } else { + message = fmt.Sprintf("%s\nState: %s\nMessage: %s\n", evalContext.GetNotificationTitle(), evalContext.Rule.Name, evalContext.Rule.Message) + } ruleUrl, err := evalContext.GetRuleUrl() if err == nil { message = message + fmt.Sprintf("URL: %s\n", ruleUrl) } - if evalContext.ImagePublicUrl != "" { - message = message + fmt.Sprintf("Image: %s\n", evalContext.ImagePublicUrl) + + if !sendImageInline { + // only attach this if we are not sending it inline. + if evalContext.ImagePublicUrl != "" { + message = message + fmt.Sprintf("Image: %s\n", evalContext.ImagePublicUrl) + } } metrics := "" @@ -107,19 +122,69 @@ func (this *TelegramNotifier) Notify(evalContext *alerting.EvalContext) error { break } } + if metrics != "" { - message = message + fmt.Sprintf("\nMetrics:%s", metrics) + if sendImageInline { + // Telegram's API does not allow HTML formatting for image captions. + message = message + fmt.Sprintf("\nMetrics:%s", metrics) + } else { + message = message + fmt.Sprintf("\nMetrics:%s", metrics) + } } - bodyJSON.Set("text", message) + var body bytes.Buffer - url := fmt.Sprintf(telegeramApiUrl, this.BotToken, "sendMessage") - body, _ := bodyJSON.MarshalJSON() + w := multipart.NewWriter(&body) + fw, _ := w.CreateFormField("chat_id") + fw.Write([]byte(this.ChatID)) + if sendImageInline { + fw, _ = w.CreateFormField("caption") + fw.Write([]byte(message)) + + fw, _ = w.CreateFormFile("photo", evalContext.ImageOnDiskPath) + io.Copy(fw, imageFile) + } else { + fw, _ = w.CreateFormField("text") + fw.Write([]byte(message)) + + fw, _ = w.CreateFormField("parse_mode") + fw.Write([]byte("html")) + } + + w.Close() + + apiMethod := "" + if sendImageInline { + this.log.Info("Sending telegram image notification", "photo", evalContext.ImageOnDiskPath, "chat_id", this.ChatID, "bot_token", this.BotToken) + apiMethod = "sendPhoto" + } else { + this.log.Info("Sending telegram text notification", "chat_id", this.ChatID, "bot_token", this.BotToken) + apiMethod = "sendMessage" + } + + url := fmt.Sprintf(telegramApiUrl, this.BotToken, apiMethod) cmd := &m.SendWebhookSync{ Url: url, - Body: string(body), + Body: body.String(), HttpMethod: "POST", + HttpHeader: map[string]string{ + "Content-Type": w.FormDataContentType(), + }, + } + return cmd +} + +func (this *TelegramNotifier) ShouldNotify(context *alerting.EvalContext) bool { + return defaultShouldNotify(context) +} + +func (this *TelegramNotifier) Notify(evalContext *alerting.EvalContext) error { + var cmd *m.SendWebhookSync + if evalContext.ImagePublicUrl == "" && this.UploadImage == true { + cmd = this.buildMessage(evalContext, true) + } else { + cmd = this.buildMessage(evalContext, false) } if err := bus.DispatchCtx(evalContext.Ctx, cmd); err != nil { diff --git a/pkg/services/provisioning/dashboards/dashboard_cache.go b/pkg/services/provisioning/dashboards/dashboard_cache.go new file mode 100644 index 00000000000..da6b7e8a5e8 --- /dev/null +++ b/pkg/services/provisioning/dashboards/dashboard_cache.go @@ -0,0 +1,33 @@ +package dashboards + +import ( + "github.com/grafana/grafana/pkg/services/dashboards" + gocache "github.com/patrickmn/go-cache" + "time" +) + +type dashboardCache struct { + internalCache *gocache.Cache +} + +func NewDashboardCache() *dashboardCache { + return &dashboardCache{internalCache: gocache.New(5*time.Minute, 30*time.Minute)} +} + +func (fr *dashboardCache) addDashboardCache(key string, json *dashboards.SaveDashboardItem) { + fr.internalCache.Add(key, json, time.Minute*10) +} + +func (fr *dashboardCache) getCache(key string) (*dashboards.SaveDashboardItem, bool) { + obj, exist := fr.internalCache.Get(key) + if !exist { + return nil, exist + } + + dash, ok := obj.(*dashboards.SaveDashboardItem) + if !ok { + return nil, ok + } + + return dash, ok +} diff --git a/pkg/services/provisioning/dashboards/file_reader.go b/pkg/services/provisioning/dashboards/file_reader.go index 22fe11b45c1..eb3085296fd 100644 --- a/pkg/services/provisioning/dashboards/file_reader.go +++ b/pkg/services/provisioning/dashboards/file_reader.go @@ -2,6 +2,7 @@ package dashboards import ( "context" + "errors" "fmt" "os" "path/filepath" @@ -15,7 +16,12 @@ import ( "github.com/grafana/grafana/pkg/components/simplejson" "github.com/grafana/grafana/pkg/log" "github.com/grafana/grafana/pkg/models" - gocache "github.com/patrickmn/go-cache" +) + +var ( + checkDiskForChangesInterval time.Duration = time.Second * 3 + + ErrFolderNameMissing error = errors.New("Folder name missing") ) type fileReader struct { @@ -23,7 +29,8 @@ type fileReader struct { Path string log log.Logger dashboardRepo dashboards.Repository - cache *gocache.Cache + cache *dashboardCache + createWalk func(fr *fileReader, folderId int64) filepath.WalkFunc } func NewDashboardFileReader(cfg *DashboardsAsConfig, log log.Logger) (*fileReader, error) { @@ -41,32 +48,15 @@ func NewDashboardFileReader(cfg *DashboardsAsConfig, log log.Logger) (*fileReade Path: path, log: log, dashboardRepo: dashboards.GetRepository(), - cache: gocache.New(5*time.Minute, 30*time.Minute), + cache: NewDashboardCache(), + createWalk: createWalkFn, }, nil } -func (fr *fileReader) addCache(key string, json *dashboards.SaveDashboardItem) { - fr.cache.Add(key, json, time.Minute*10) -} - -func (fr *fileReader) getCache(key string) (*dashboards.SaveDashboardItem, bool) { - obj, exist := fr.cache.Get(key) - if !exist { - return nil, exist - } - - dash, ok := obj.(*dashboards.SaveDashboardItem) - if !ok { - return nil, ok - } - - return dash, ok -} - func (fr *fileReader) ReadAndListen(ctx context.Context) error { - ticker := time.NewTicker(time.Second * 3) + ticker := time.NewTicker(checkDiskForChangesInterval) - if err := fr.walkFolder(); err != nil { + if err := fr.startWalkingDisk(); err != nil { fr.log.Error("failed to search for dashboards", "error", err) } @@ -78,7 +68,9 @@ func (fr *fileReader) ReadAndListen(ctx context.Context) error { if !running { // avoid walking the filesystem in parallel. incase fs is very slow. running = true go func() { - fr.walkFolder() + if err := fr.startWalkingDisk(); err != nil { + fr.log.Error("failed to search for dashboards", "error", err) + } running = false }() } @@ -88,14 +80,57 @@ func (fr *fileReader) ReadAndListen(ctx context.Context) error { } } -func (fr *fileReader) walkFolder() error { +func (fr *fileReader) startWalkingDisk() error { if _, err := os.Stat(fr.Path); err != nil { if os.IsNotExist(err) { return err } } - return filepath.Walk(fr.Path, func(path string, fileInfo os.FileInfo, err error) error { + folderId, err := getOrCreateFolderId(fr.Cfg, fr.dashboardRepo) + if err != nil && err != ErrFolderNameMissing { + return err + } + + return filepath.Walk(fr.Path, fr.createWalk(fr, folderId)) +} + +func getOrCreateFolderId(cfg *DashboardsAsConfig, repo dashboards.Repository) (int64, error) { + if cfg.Folder == "" { + return 0, ErrFolderNameMissing + } + + cmd := &models.GetDashboardQuery{Slug: models.SlugifyTitle(cfg.Folder), OrgId: cfg.OrgId} + err := bus.Dispatch(cmd) + + if err != nil && err != models.ErrDashboardNotFound { + return 0, err + } + + // dashboard folder not found. create one. + if err == models.ErrDashboardNotFound { + dash := &dashboards.SaveDashboardItem{} + dash.Dashboard = models.NewDashboard(cfg.Folder) + dash.Dashboard.IsFolder = true + dash.Overwrite = true + dash.OrgId = cfg.OrgId + dbDash, err := repo.SaveDashboard(dash) + if err != nil { + return 0, err + } + + return dbDash.Id, nil + } + + if !cmd.Result.IsFolder { + return 0, fmt.Errorf("Got invalid response. Expected folder, found dashboard") + } + + return cmd.Result.Id, nil +} + +func createWalkFn(fr *fileReader, folderId int64) filepath.WalkFunc { + return func(path string, fileInfo os.FileInfo, err error) error { if err != nil { return err } @@ -110,12 +145,12 @@ func (fr *fileReader) walkFolder() error { return nil } - cachedDashboard, exist := fr.getCache(path) + cachedDashboard, exist := fr.cache.getCache(path) if exist && cachedDashboard.UpdatedAt == fileInfo.ModTime() { return nil } - dash, err := fr.readDashboardFromFile(path) + dash, err := fr.readDashboardFromFile(path, folderId) if err != nil { fr.log.Error("failed to load dashboard from ", "file", path, "error", err) return nil @@ -147,10 +182,10 @@ func (fr *fileReader) walkFolder() error { fr.log.Debug("loading dashboard from disk into database.", "file", path) _, err = fr.dashboardRepo.SaveDashboard(dash) return err - }) + } } -func (fr *fileReader) readDashboardFromFile(path string) (*dashboards.SaveDashboardItem, error) { +func (fr *fileReader) readDashboardFromFile(path string, folderId int64) (*dashboards.SaveDashboardItem, error) { reader, err := os.Open(path) if err != nil { return nil, err @@ -167,12 +202,12 @@ func (fr *fileReader) readDashboardFromFile(path string) (*dashboards.SaveDashbo return nil, err } - dash, err := createDashboardJson(data, stat.ModTime(), fr.Cfg) + dash, err := createDashboardJson(data, stat.ModTime(), fr.Cfg, folderId) if err != nil { return nil, err } - fr.addCache(path, dash) + fr.cache.addDashboardCache(path, dash) return dash, nil } diff --git a/pkg/services/provisioning/dashboards/file_reader_test.go b/pkg/services/provisioning/dashboards/file_reader_test.go index e91f278322a..16e3e1184b8 100644 --- a/pkg/services/provisioning/dashboards/file_reader_test.go +++ b/pkg/services/provisioning/dashboards/file_reader_test.go @@ -2,6 +2,7 @@ package dashboards import ( "os" + "path/filepath" "testing" "time" @@ -22,7 +23,7 @@ var ( ) func TestDashboardFileReader(t *testing.T) { - Convey("Reading dashboards from disk", t, func() { + Convey("Dashboard file reader", t, func() { bus.ClearBusHandlers() fakeRepo = &fakeDashboardRepo{} @@ -30,91 +31,191 @@ func TestDashboardFileReader(t *testing.T) { dashboards.SetRepository(fakeRepo) logger := log.New("test.logger") - cfg := &DashboardsAsConfig{ - Name: "Default", - Type: "file", - OrgId: 1, - Folder: "", - Options: map[string]interface{}{}, - } + Convey("Reading dashboards from disk", func() { - Convey("Can read default dashboard", func() { - cfg.Options["folder"] = defaultDashboards - - reader, err := NewDashboardFileReader(cfg, logger) - So(err, ShouldBeNil) - - err = reader.walkFolder() - So(err, ShouldBeNil) - - So(len(fakeRepo.inserted), ShouldEqual, 2) - }) - - Convey("Should not update dashboards when db is newer", func() { - cfg.Options["folder"] = oneDashboard - - fakeRepo.getDashboard = append(fakeRepo.getDashboard, &models.Dashboard{ - Updated: time.Now().Add(time.Hour), - Slug: "grafana", - }) - - reader, err := NewDashboardFileReader(cfg, logger) - So(err, ShouldBeNil) - - err = reader.walkFolder() - So(err, ShouldBeNil) - - So(len(fakeRepo.inserted), ShouldEqual, 0) - }) - - Convey("Can read default dashboard and replace old version in database", func() { - cfg.Options["folder"] = oneDashboard - - stat, _ := os.Stat(oneDashboard + "/dashboard1.json") - - fakeRepo.getDashboard = append(fakeRepo.getDashboard, &models.Dashboard{ - Updated: stat.ModTime().AddDate(0, 0, -1), - Slug: "grafana", - }) - - reader, err := NewDashboardFileReader(cfg, logger) - So(err, ShouldBeNil) - - err = reader.walkFolder() - So(err, ShouldBeNil) - - So(len(fakeRepo.inserted), ShouldEqual, 1) - }) - - Convey("Invalid configuration should return error", func() { cfg := &DashboardsAsConfig{ - Name: "Default", - Type: "file", - OrgId: 1, - Folder: "", + Name: "Default", + Type: "file", + OrgId: 1, + Folder: "", + Options: map[string]interface{}{}, } - _, err := NewDashboardFileReader(cfg, logger) - So(err, ShouldNotBeNil) + Convey("Can read default dashboard", func() { + cfg.Options["folder"] = defaultDashboards + cfg.Folder = "Team A" + + reader, err := NewDashboardFileReader(cfg, logger) + So(err, ShouldBeNil) + + err = reader.startWalkingDisk() + So(err, ShouldBeNil) + + folders := 0 + dashboards := 0 + + for _, i := range fakeRepo.inserted { + if i.Dashboard.IsFolder { + folders++ + } else { + dashboards++ + } + } + + So(dashboards, ShouldEqual, 2) + So(folders, ShouldEqual, 1) + }) + + Convey("Should not update dashboards when db is newer", func() { + cfg.Options["folder"] = oneDashboard + + fakeRepo.getDashboard = append(fakeRepo.getDashboard, &models.Dashboard{ + Updated: time.Now().Add(time.Hour), + Slug: "grafana", + }) + + reader, err := NewDashboardFileReader(cfg, logger) + So(err, ShouldBeNil) + + err = reader.startWalkingDisk() + So(err, ShouldBeNil) + + So(len(fakeRepo.inserted), ShouldEqual, 0) + }) + + Convey("Can read default dashboard and replace old version in database", func() { + cfg.Options["folder"] = oneDashboard + + stat, _ := os.Stat(oneDashboard + "/dashboard1.json") + + fakeRepo.getDashboard = append(fakeRepo.getDashboard, &models.Dashboard{ + Updated: stat.ModTime().AddDate(0, 0, -1), + Slug: "grafana", + }) + + reader, err := NewDashboardFileReader(cfg, logger) + So(err, ShouldBeNil) + + err = reader.startWalkingDisk() + So(err, ShouldBeNil) + + So(len(fakeRepo.inserted), ShouldEqual, 1) + }) + + Convey("Invalid configuration should return error", func() { + cfg := &DashboardsAsConfig{ + Name: "Default", + Type: "file", + OrgId: 1, + Folder: "", + } + + _, err := NewDashboardFileReader(cfg, logger) + So(err, ShouldNotBeNil) + }) + + Convey("Broken dashboards should not cause error", func() { + cfg.Options["folder"] = brokenDashboards + + _, err := NewDashboardFileReader(cfg, logger) + So(err, ShouldBeNil) + }) }) - Convey("Broken dashboards should not cause error", func() { + Convey("Should not create new folder if folder name is missing", func() { cfg := &DashboardsAsConfig{ Name: "Default", Type: "file", OrgId: 1, Folder: "", Options: map[string]interface{}{ - "folder": brokenDashboards, + "folder": defaultDashboards, }, } - _, err := NewDashboardFileReader(cfg, logger) + _, err := getOrCreateFolderId(cfg, fakeRepo) + So(err, ShouldEqual, ErrFolderNameMissing) + }) + + Convey("can get or Create dashboard folder", func() { + cfg := &DashboardsAsConfig{ + Name: "Default", + Type: "file", + OrgId: 1, + Folder: "TEAM A", + Options: map[string]interface{}{ + "folder": defaultDashboards, + }, + } + + folderId, err := getOrCreateFolderId(cfg, fakeRepo) So(err, ShouldBeNil) + inserted := false + for _, d := range fakeRepo.inserted { + if d.Dashboard.IsFolder && d.Dashboard.Id == folderId { + inserted = true + } + } + So(len(fakeRepo.inserted), ShouldEqual, 1) + So(inserted, ShouldBeTrue) + }) + + Convey("Walking the folder with dashboards", func() { + cfg := &DashboardsAsConfig{ + Name: "Default", + Type: "file", + OrgId: 1, + Folder: "", + Options: map[string]interface{}{ + "folder": defaultDashboards, + }, + } + + reader, err := NewDashboardFileReader(cfg, log.New("test-logger")) + So(err, ShouldBeNil) + + Convey("should skip dirs that starts with .", func() { + shouldSkip := reader.createWalk(reader, 0)("path", &FakeFileInfo{isDirectory: true, name: ".folder"}, nil) + So(shouldSkip, ShouldEqual, filepath.SkipDir) + }) + + Convey("should keep walking if file is not .json", func() { + shouldSkip := reader.createWalk(reader, 0)("path", &FakeFileInfo{isDirectory: true, name: "folder"}, nil) + So(shouldSkip, ShouldBeNil) + }) }) }) } +type FakeFileInfo struct { + isDirectory bool + name string +} + +func (ffi *FakeFileInfo) IsDir() bool { + return ffi.isDirectory +} + +func (ffi FakeFileInfo) Size() int64 { + return 1 +} + +func (ffi FakeFileInfo) Mode() os.FileMode { + return 0777 +} + +func (ffi FakeFileInfo) Name() string { + return ffi.name +} + +func (ffi FakeFileInfo) ModTime() time.Time { + return time.Time{} +} + +func (ffi FakeFileInfo) Sys() interface{} { + return nil +} + type fakeDashboardRepo struct { inserted []*dashboards.SaveDashboardItem getDashboard []*models.Dashboard diff --git a/pkg/services/provisioning/dashboards/types.go b/pkg/services/provisioning/dashboards/types.go index c4c4a67a755..46ca3c9246e 100644 --- a/pkg/services/provisioning/dashboards/types.go +++ b/pkg/services/provisioning/dashboards/types.go @@ -18,14 +18,16 @@ type DashboardsAsConfig struct { Options map[string]interface{} `json:"options" yaml:"options"` } -func createDashboardJson(data *simplejson.Json, lastModified time.Time, cfg *DashboardsAsConfig) (*dashboards.SaveDashboardItem, error) { - +func createDashboardJson(data *simplejson.Json, lastModified time.Time, cfg *DashboardsAsConfig, folderId int64) (*dashboards.SaveDashboardItem, error) { dash := &dashboards.SaveDashboardItem{} dash.Dashboard = models.NewDashboardFromJson(data) dash.UpdatedAt = lastModified dash.Overwrite = true dash.OrgId = cfg.OrgId - dash.Dashboard.Data.Set("editable", cfg.Editable) + dash.Dashboard.FolderId = folderId + if !cfg.Editable { + dash.Dashboard.Data.Set("editable", cfg.Editable) + } if dash.Dashboard.Title == "" { return nil, models.ErrDashboardTitleEmpty diff --git a/pkg/services/sqlstore/alert.go b/pkg/services/sqlstore/alert.go index 73be7d774fd..a34f56066e5 100644 --- a/pkg/services/sqlstore/alert.go +++ b/pkg/services/sqlstore/alert.go @@ -88,7 +88,7 @@ func HandleAlertsQuery(query *m.GetAlertsQuery) error { params = append(params, query.PanelId) } - if len(query.State) > 0 && query.State[0] != "ALL" { + if len(query.State) > 0 && query.State[0] != "all" { sql.WriteString(` AND (`) for i, v := range query.State { if i > 0 { diff --git a/pkg/services/sqlstore/migrations/team_mig.go b/pkg/services/sqlstore/migrations/team_mig.go index 374972e5449..eb0641fbc32 100644 --- a/pkg/services/sqlstore/migrations/team_mig.go +++ b/pkg/services/sqlstore/migrations/team_mig.go @@ -45,4 +45,9 @@ func addTeamMigrations(mg *Migrator) { //------- indexes ------------------ mg.AddMigration("add index team_member.org_id", NewAddIndexMigration(teamMemberV1, teamMemberV1.Indices[0])) mg.AddMigration("add unique index team_member_org_id_team_id_user_id", NewAddIndexMigration(teamMemberV1, teamMemberV1.Indices[1])) + + // add column email + mg.AddMigration("Add column email to team table", NewAddColumnMigration(teamV1, &Column{ + Name: "email", Type: DB_NVarchar, Nullable: true, Length: 190, + })) } diff --git a/pkg/services/sqlstore/stats.go b/pkg/services/sqlstore/stats.go index aa01c8a3761..7f631859a1d 100644 --- a/pkg/services/sqlstore/stats.go +++ b/pkg/services/sqlstore/stats.go @@ -13,7 +13,7 @@ func init() { bus.AddHandler("sql", GetAdminStats) } -var activeUserTimeLimit time.Duration = time.Hour * 24 * 14 +var activeUserTimeLimit time.Duration = time.Hour * 24 * 30 func GetDataSourceStats(query *m.GetDataSourceStatsQuery) error { var rawSql = `SELECT COUNT(*) as count, type FROM data_source GROUP BY type` diff --git a/pkg/services/sqlstore/team.go b/pkg/services/sqlstore/team.go index 3e9a6e6ec56..98bb1a36eb9 100644 --- a/pkg/services/sqlstore/team.go +++ b/pkg/services/sqlstore/team.go @@ -33,6 +33,7 @@ func CreateTeam(cmd *m.CreateTeamCommand) error { team := m.Team{ Name: cmd.Name, + Email: cmd.Email, OrgId: cmd.OrgId, Created: time.Now(), Updated: time.Now(), @@ -57,9 +58,12 @@ func UpdateTeam(cmd *m.UpdateTeamCommand) error { team := m.Team{ Name: cmd.Name, + Email: cmd.Email, Updated: time.Now(), } + sess.MustCols("email") + affectedRows, err := sess.Id(cmd.Id).Update(&team) if err != nil { @@ -125,6 +129,7 @@ func SearchTeams(query *m.SearchTeamsQuery) error { sql.WriteString(`select team.id as id, team.name as name, + team.email as email, (select count(*) from team_member where team_member.team_id = team.id) as member_count from team as team where team.org_id = ?`) diff --git a/pkg/services/sqlstore/team_test.go b/pkg/services/sqlstore/team_test.go index 4a099db14ff..dbae4545266 100644 --- a/pkg/services/sqlstore/team_test.go +++ b/pkg/services/sqlstore/team_test.go @@ -27,8 +27,8 @@ func TestTeamCommandsAndQueries(t *testing.T) { userIds = append(userIds, userCmd.Result.Id) } - group1 := m.CreateTeamCommand{Name: "group1 name"} - group2 := m.CreateTeamCommand{Name: "group2 name"} + group1 := m.CreateTeamCommand{Name: "group1 name", Email: "test1@test.com"} + group2 := m.CreateTeamCommand{Name: "group2 name", Email: "test2@test.com"} err := CreateTeam(&group1) So(err, ShouldBeNil) @@ -43,6 +43,7 @@ func TestTeamCommandsAndQueries(t *testing.T) { team1 := query.Result.Teams[0] So(team1.Name, ShouldEqual, "group1 name") + So(team1.Email, ShouldEqual, "test1@test.com") err = AddTeamMember(&m.AddTeamMemberCommand{OrgId: 1, TeamId: team1.Id, UserId: userIds[0]}) So(err, ShouldBeNil) @@ -76,6 +77,7 @@ func TestTeamCommandsAndQueries(t *testing.T) { So(err, ShouldBeNil) So(len(query.Result), ShouldEqual, 1) So(query.Result[0].Name, ShouldEqual, "group2 name") + So(query.Result[0].Email, ShouldEqual, "test2@test.com") }) Convey("Should be able to remove users from a group", func() { diff --git a/pkg/setting/setting.go b/pkg/setting/setting.go index 6a6df2b61fb..8cdb94bd413 100644 --- a/pkg/setting/setting.go +++ b/pkg/setting/setting.go @@ -600,7 +600,7 @@ func NewConfigContext(args *CommandLineArgs) error { readQuotaSettings() if VerifyEmailEnabled && !Smtp.Enabled { - log.Warn("require_email_validation is enabled but smpt is disabled") + log.Warn("require_email_validation is enabled but smtp is disabled") } // check old key name @@ -610,7 +610,7 @@ func NewConfigContext(args *CommandLineArgs) error { } imageUploadingSection := Cfg.Section("external_image_storage") - ImageUploadProvider = imageUploadingSection.Key("provider").MustString("internal") + ImageUploadProvider = imageUploadingSection.Key("provider").MustString("") return nil } diff --git a/pkg/tsdb/cloudwatch/metric_find_query.go b/pkg/tsdb/cloudwatch/metric_find_query.go index 1e1e855b123..befaf8e1276 100644 --- a/pkg/tsdb/cloudwatch/metric_find_query.go +++ b/pkg/tsdb/cloudwatch/metric_find_query.go @@ -37,6 +37,7 @@ var customMetricsDimensionsMap map[string]map[string]map[string]*CustomMetricsCa func init() { metricsMap = map[string][]string{ + "AWS/AmazonMQ": {"CpuUtilization", "HeapUsage", "NetworkIn", "NetworkOut", "TotalMessageCount", "ConsumerCount", "EnqueueCount", "EnqueueTime", "ExpiredCount", "InflightCount", "DispatchCount", "DequeueCount", "MemoryUsage", "ProducerCount", "QueueSize"}, "AWS/ApiGateway": {"4XXError", "5XXError", "CacheHitCount", "CacheMissCount", "Count", "IntegrationLatency", "Latency"}, "AWS/ApplicationELB": {"ActiveConnectionCount", "ClientTLSNegotiationErrorCount", "HealthyHostCount", "HTTPCode_ELB_4XX_Count", "HTTPCode_ELB_5XX_Count", "HTTPCode_Target_2XX_Count", "HTTPCode_Target_3XX_Count", "HTTPCode_Target_4XX_Count", "HTTPCode_Target_5XX_Count", "IPv6ProcessedBytes", "IPv6RequestCount", "NewConnectionCount", "ProcessedBytes", "RejectedConnectionCount", "RequestCount", "RequestCountPerTarget", "TargetConnectionErrorCount", "TargetResponseTime", "TargetTLSNegotiationErrorCount", "UnHealthyHostCount"}, "AWS/AutoScaling": {"GroupMinSize", "GroupMaxSize", "GroupDesiredCapacity", "GroupInServiceInstances", "GroupPendingInstances", "GroupStandbyInstances", "GroupTerminatingInstances", "GroupTotalInstances"}, @@ -106,6 +107,7 @@ func init() { "KMS": {"SecondsUntilKeyMaterialExpiration"}, } dimensionsMap = map[string][]string{ + "AWS/AmazonMQ": {"Broker", "Topic", "Queue"}, "AWS/ApiGateway": {"ApiName", "Method", "Resource", "Stage"}, "AWS/ApplicationELB": {"LoadBalancer", "TargetGroup", "AvailabilityZone"}, "AWS/AutoScaling": {"AutoScalingGroupName"}, diff --git a/pkg/tsdb/prometheus/prometheus.go b/pkg/tsdb/prometheus/prometheus.go index e798b92c6fe..1186fccbbf9 100644 --- a/pkg/tsdb/prometheus/prometheus.go +++ b/pkg/tsdb/prometheus/prometheus.go @@ -83,41 +83,48 @@ func (e *PrometheusExecutor) getClient(dsInfo *models.DataSource) (apiv1.API, er } func (e *PrometheusExecutor) Query(ctx context.Context, dsInfo *models.DataSource, tsdbQuery *tsdb.TsdbQuery) (*tsdb.Response, error) { - result := &tsdb.Response{} + result := &tsdb.Response{ + Results: map[string]*tsdb.QueryResult{}, + } client, err := e.getClient(dsInfo) if err != nil { return nil, err } - query, err := parseQuery(dsInfo, tsdbQuery.Queries, tsdbQuery) + querys, err := parseQuery(dsInfo, tsdbQuery.Queries, tsdbQuery) if err != nil { return nil, err } - timeRange := apiv1.Range{ - Start: query.Start, - End: query.End, - Step: query.Step, + for _, query := range querys { + timeRange := apiv1.Range{ + Start: query.Start, + End: query.End, + Step: query.Step, + } + + plog.Debug("Sending query", "start", timeRange.Start, "end", timeRange.End, "step", timeRange.Step, "query", query.Expr) + + span, ctx := opentracing.StartSpanFromContext(ctx, "alerting.prometheus") + span.SetTag("expr", query.Expr) + span.SetTag("start_unixnano", int64(query.Start.UnixNano())) + span.SetTag("stop_unixnano", int64(query.End.UnixNano())) + defer span.Finish() + + value, err := client.QueryRange(ctx, query.Expr, timeRange) + + if err != nil { + return nil, err + } + + queryResult, err := parseResponse(value, query) + if err != nil { + return nil, err + } + result.Results[query.RefId] = queryResult } - span, ctx := opentracing.StartSpanFromContext(ctx, "alerting.prometheus") - span.SetTag("expr", query.Expr) - span.SetTag("start_unixnano", int64(query.Start.UnixNano())) - span.SetTag("stop_unixnano", int64(query.End.UnixNano())) - defer span.Finish() - - value, err := client.QueryRange(ctx, query.Expr, timeRange) - - if err != nil { - return nil, err - } - - queryResult, err := parseResponse(value, query) - if err != nil { - return nil, err - } - result.Results = queryResult return result, nil } @@ -140,51 +147,54 @@ func formatLegend(metric model.Metric, query *PrometheusQuery) string { return string(result) } -func parseQuery(dsInfo *models.DataSource, queries []*tsdb.Query, queryContext *tsdb.TsdbQuery) (*PrometheusQuery, error) { - queryModel := queries[0] +func parseQuery(dsInfo *models.DataSource, queries []*tsdb.Query, queryContext *tsdb.TsdbQuery) ([]*PrometheusQuery, error) { + qs := []*PrometheusQuery{} + for _, queryModel := range queries { + expr, err := queryModel.Model.Get("expr").String() + if err != nil { + return nil, err + } - expr, err := queryModel.Model.Get("expr").String() - if err != nil { - return nil, err + format := queryModel.Model.Get("legendFormat").MustString("") + + start, err := queryContext.TimeRange.ParseFrom() + if err != nil { + return nil, err + } + + end, err := queryContext.TimeRange.ParseTo() + if err != nil { + return nil, err + } + + dsInterval, err := tsdb.GetIntervalFrom(dsInfo, queryModel.Model, time.Second*15) + if err != nil { + return nil, err + } + + intervalFactor := queryModel.Model.Get("intervalFactor").MustInt64(1) + interval := intervalCalculator.Calculate(queryContext.TimeRange, dsInterval) + step := time.Duration(int64(interval.Value) * intervalFactor) + + qs = append(qs, &PrometheusQuery{ + Expr: expr, + Step: step, + LegendFormat: format, + Start: start, + End: end, + RefId: queryModel.RefId, + }) } - format := queryModel.Model.Get("legendFormat").MustString("") - - start, err := queryContext.TimeRange.ParseFrom() - if err != nil { - return nil, err - } - - end, err := queryContext.TimeRange.ParseTo() - if err != nil { - return nil, err - } - - dsInterval, err := tsdb.GetIntervalFrom(dsInfo, queryModel.Model, time.Second*15) - if err != nil { - return nil, err - } - - intervalFactor := queryModel.Model.Get("intervalFactor").MustInt64(1) - interval := intervalCalculator.Calculate(queryContext.TimeRange, dsInterval) - step := time.Duration(int64(interval.Value) * intervalFactor) - - return &PrometheusQuery{ - Expr: expr, - Step: step, - LegendFormat: format, - Start: start, - End: end, - }, nil + return qs, nil } -func parseResponse(value model.Value, query *PrometheusQuery) (map[string]*tsdb.QueryResult, error) { - queryResults := make(map[string]*tsdb.QueryResult) +func parseResponse(value model.Value, query *PrometheusQuery) (*tsdb.QueryResult, error) { queryRes := tsdb.NewQueryResult() data, ok := value.(model.Matrix) if !ok { - return queryResults, fmt.Errorf("Unsupported result format: %s", value.Type().String()) + return queryRes, fmt.Errorf("Unsupported result format: %s", value.Type().String()) } for _, v := range data { @@ -204,6 +214,5 @@ func parseResponse(value model.Value, query *PrometheusQuery) (map[string]*tsdb. queryRes.Series = append(queryRes.Series, &series) } - queryResults["A"] = queryRes - return queryResults, nil + return queryRes, nil } diff --git a/pkg/tsdb/prometheus/prometheus_test.go b/pkg/tsdb/prometheus/prometheus_test.go index c551ab98112..efb42318214 100644 --- a/pkg/tsdb/prometheus/prometheus_test.go +++ b/pkg/tsdb/prometheus/prometheus_test.go @@ -60,9 +60,10 @@ func TestPrometheus(t *testing.T) { Convey("with 48h time range", func() { queryContext.TimeRange = tsdb.NewTimeRange("12h", "now") - model, err := parseQuery(dsInfo, queryModels, queryContext) - + models, err := parseQuery(dsInfo, queryModels, queryContext) So(err, ShouldBeNil) + + model := models[0] So(model.Step, ShouldEqual, time.Second*30) }) }) @@ -83,18 +84,22 @@ func TestPrometheus(t *testing.T) { Convey("with 48h time range", func() { queryContext.TimeRange = tsdb.NewTimeRange("48h", "now") - model, err := parseQuery(dsInfo, queryModels, queryContext) + models, err := parseQuery(dsInfo, queryModels, queryContext) So(err, ShouldBeNil) + + model := models[0] So(model.Step, ShouldEqual, time.Minute*2) }) Convey("with 1h time range", func() { queryContext.TimeRange = tsdb.NewTimeRange("1h", "now") - model, err := parseQuery(dsInfo, queryModels, queryContext) + models, err := parseQuery(dsInfo, queryModels, queryContext) So(err, ShouldBeNil) + + model := models[0] So(model.Step, ShouldEqual, time.Second*15) }) }) @@ -116,9 +121,11 @@ func TestPrometheus(t *testing.T) { Convey("with 48h time range", func() { queryContext.TimeRange = tsdb.NewTimeRange("48h", "now") - model, err := parseQuery(dsInfo, queryModels, queryContext) + models, err := parseQuery(dsInfo, queryModels, queryContext) So(err, ShouldBeNil) + + model := models[0] So(model.Step, ShouldEqual, time.Minute*20) }) }) @@ -139,9 +146,11 @@ func TestPrometheus(t *testing.T) { Convey("with 48h time range", func() { queryContext.TimeRange = tsdb.NewTimeRange("48h", "now") - model, err := parseQuery(dsInfo, queryModels, queryContext) + models, err := parseQuery(dsInfo, queryModels, queryContext) So(err, ShouldBeNil) + + model := models[0] So(model.Step, ShouldEqual, time.Minute*2) }) }) diff --git a/pkg/tsdb/prometheus/types.go b/pkg/tsdb/prometheus/types.go index 8ed665d0123..cf8c16682e8 100644 --- a/pkg/tsdb/prometheus/types.go +++ b/pkg/tsdb/prometheus/types.go @@ -8,4 +8,5 @@ type PrometheusQuery struct { LegendFormat string Start time.Time End time.Time + RefId string } diff --git a/public/app/app.ts b/public/app/app.ts index f3e1fa5133e..9ac76b8ec91 100644 --- a/public/app/app.ts +++ b/public/app/app.ts @@ -27,6 +27,9 @@ _.move = function(array, fromIndex, toIndex) { }; import { coreModule, registerAngularDirectives } from './core/core'; +import { setupAngularRoutes } from './routes/routes'; + +declare var System: any; export class GrafanaApp { registerFunctions: any; @@ -54,49 +57,40 @@ export class GrafanaApp { moment.locale(config.bootData.user.locale); - app.config( - ( - $locationProvider, - $controllerProvider, - $compileProvider, - $filterProvider, - $httpProvider, - $provide - ) => { - // pre assing bindings before constructor calls - $compileProvider.preAssignBindingsEnabled(true); + app.config(($locationProvider, $controllerProvider, $compileProvider, $filterProvider, $httpProvider, $provide) => { + // pre assing bindings before constructor calls + $compileProvider.preAssignBindingsEnabled(true); - if (config.buildInfo.env !== 'development') { - $compileProvider.debugInfoEnabled(false); - } - - $httpProvider.useApplyAsync(true); - - this.registerFunctions.controller = $controllerProvider.register; - this.registerFunctions.directive = $compileProvider.directive; - this.registerFunctions.factory = $provide.factory; - this.registerFunctions.service = $provide.service; - this.registerFunctions.filter = $filterProvider.register; - - $provide.decorator('$http', [ - '$delegate', - '$templateCache', - function($delegate, $templateCache) { - var get = $delegate.get; - $delegate.get = function(url, config) { - if (url.match(/\.html$/)) { - // some template's already exist in the cache - if (!$templateCache.get(url)) { - url += '?v=' + new Date().getTime(); - } - } - return get(url, config); - }; - return $delegate; - }, - ]); + if (config.buildInfo.env !== 'development') { + $compileProvider.debugInfoEnabled(false); } - ); + + $httpProvider.useApplyAsync(true); + + this.registerFunctions.controller = $controllerProvider.register; + this.registerFunctions.directive = $compileProvider.directive; + this.registerFunctions.factory = $provide.factory; + this.registerFunctions.service = $provide.service; + this.registerFunctions.filter = $filterProvider.register; + + $provide.decorator('$http', [ + '$delegate', + '$templateCache', + function($delegate, $templateCache) { + var get = $delegate.get; + $delegate.get = function(url, config) { + if (url.match(/\.html$/)) { + // some template's already exist in the cache + if (!$templateCache.get(url)) { + url += '?v=' + new Date().getTime(); + } + } + return get(url, config); + }; + return $delegate; + }, + ]); + }); this.ngModuleDependencies = [ 'grafana.core', @@ -111,14 +105,7 @@ export class GrafanaApp { 'react', ]; - var module_types = [ - 'controllers', - 'directives', - 'factories', - 'services', - 'filters', - 'routes', - ]; + var module_types = ['controllers', 'directives', 'factories', 'services', 'filters', 'routes']; _.each(module_types, type => { var moduleName = 'grafana.' + type; @@ -129,6 +116,7 @@ export class GrafanaApp { this.useModule(coreModule); // register react angular wrappers + coreModule.config(setupAngularRoutes); registerAngularDirectives(); var preBootRequires = [System.import('app/features/all')]; @@ -137,6 +125,7 @@ export class GrafanaApp { .then(() => { // disable tool tip animation $.fn.tooltip.defaults.animation = false; + // bootstrap the app angular.bootstrap(document, this.ngModuleDependencies).invoke(() => { _.each(this.preBootModules, module => { diff --git a/public/app/containers/AlertRuleList/AlertRuleList.jest.tsx b/public/app/containers/AlertRuleList/AlertRuleList.jest.tsx new file mode 100644 index 00000000000..a12340555e9 --- /dev/null +++ b/public/app/containers/AlertRuleList/AlertRuleList.jest.tsx @@ -0,0 +1,68 @@ +import React from 'react'; +import moment from 'moment'; +import { AlertRuleList } from './AlertRuleList'; +import { RootStore } from 'app/stores/RootStore/RootStore'; +import { backendSrv, createNavTree } from 'test/mocks/common'; +import { mount } from 'enzyme'; +import toJson from 'enzyme-to-json'; + +describe('AlertRuleList', () => { + let page, store; + + beforeAll(() => { + backendSrv.get.mockReturnValue( + Promise.resolve([ + { + id: 11, + dashboardId: 58, + panelId: 3, + name: 'Panel Title alert', + state: 'ok', + newStateDate: moment() + .subtract(5, 'minutes') + .format(), + evalData: {}, + executionError: '', + dashboardUri: 'db/mygool', + }, + ]) + ); + + store = RootStore.create( + {}, + { + backendSrv: backendSrv, + navTree: createNavTree('alerting', 'alert-list'), + } + ); + + page = mount(); + }); + + it('should call api to get rules', () => { + expect(backendSrv.get.mock.calls[0][0]).toEqual('/api/alerts'); + }); + + it('should render 1 rule', () => { + page.update(); + let ruleNode = page.find('.alert-rule-item'); + expect(toJson(ruleNode)).toMatchSnapshot(); + }); + + it('toggle state should change pause rule if not paused', async () => { + backendSrv.post.mockReturnValue( + Promise.resolve({ + state: 'paused', + }) + ); + + page.find('.fa-pause').simulate('click'); + + // wait for api call to resolve + await Promise.resolve(); + page.update(); + + expect(store.alertList.rules[0].state).toBe('paused'); + expect(page.find('.fa-play')).toHaveLength(1); + }); +}); diff --git a/public/app/containers/AlertRuleList/AlertRuleList.tsx b/public/app/containers/AlertRuleList/AlertRuleList.tsx new file mode 100644 index 00000000000..429504e46d9 --- /dev/null +++ b/public/app/containers/AlertRuleList/AlertRuleList.tsx @@ -0,0 +1,174 @@ +import React from 'react'; +import classNames from 'classnames'; +import { inject, observer } from 'mobx-react'; +import PageHeader from 'app/core/components/PageHeader/PageHeader'; +import { IAlertRule } from 'app/stores/AlertListStore/AlertListStore'; +import appEvents from 'app/core/app_events'; +import IContainerProps from 'app/containers/IContainerProps'; +import Highlighter from 'react-highlight-words'; + +@inject('view', 'nav', 'alertList') +@observer +export class AlertRuleList extends React.Component { + stateFilters = [ + { text: 'All', value: 'all' }, + { text: 'OK', value: 'ok' }, + { text: 'Not OK', value: 'not_ok' }, + { text: 'Alerting', value: 'alerting' }, + { text: 'No Data', value: 'no_data' }, + { text: 'Paused', value: 'paused' }, + ]; + + constructor(props) { + super(props); + + this.props.nav.load('alerting', 'alert-list'); + this.fetchRules(); + } + + onStateFilterChanged = evt => { + this.props.view.updateQuery({ state: evt.target.value }); + this.fetchRules(); + }; + + fetchRules() { + this.props.alertList.loadRules({ + state: this.props.view.query.get('state') || 'all', + }); + } + + onOpenHowTo = () => { + appEvents.emit('show-modal', { + src: 'public/app/features/alerting/partials/alert_howto.html', + modalClass: 'confirm-modal', + model: {}, + }); + }; + + onSearchQueryChange = evt => { + this.props.alertList.setSearchQuery(evt.target.value); + }; + + render() { + const { nav, alertList } = this.props; + + return ( +
+ +
+
+
+ +
+
+ + +
+ +
+
+ + + +
+
    + {alertList.filteredRules.map(rule => ( + + ))} +
+
+
+
+ ); + } +} + +function AlertStateFilterOption({ text, value }) { + return ( + + ); +} + +export interface AlertRuleItemProps { + rule: IAlertRule; + search: string; +} + +@observer +export class AlertRuleItem extends React.Component { + toggleState = () => { + this.props.rule.togglePaused(); + }; + + renderText(text: string) { + return ( + + ); + } + + render() { + const { rule } = this.props; + + let stateClass = classNames({ + fa: true, + 'fa-play': rule.isPaused, + 'fa-pause': !rule.isPaused, + }); + + let ruleUrl = `dashboard/${rule.dashboardUri}?panelId=${rule.panelId}&fullscreen&edit&tab=alert`; + + return ( +
  • + + + +
    +
    + +
    + {this.renderText(rule.stateText)} + for {rule.stateAge} +
    +
    + {rule.info &&
    {this.renderText(rule.info)}
    } +
    + +
  • + ); + } +} diff --git a/public/app/containers/AlertRuleList/__snapshots__/AlertRuleList.jest.tsx.snap b/public/app/containers/AlertRuleList/__snapshots__/AlertRuleList.jest.tsx.snap new file mode 100644 index 00000000000..571566503f8 --- /dev/null +++ b/public/app/containers/AlertRuleList/__snapshots__/AlertRuleList.jest.tsx.snap @@ -0,0 +1,103 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`AlertRuleList should render 1 rule 1`] = ` +
  • + + + +
    +
    + +
    + + + + + OK + + + + + + for + 5 minutes + +
    +
    +
    + +
  • +`; diff --git a/public/app/containers/IContainerProps.ts b/public/app/containers/IContainerProps.ts new file mode 100644 index 00000000000..aecba4e8ad3 --- /dev/null +++ b/public/app/containers/IContainerProps.ts @@ -0,0 +1,15 @@ +import { SearchStore } from './../stores/SearchStore/SearchStore'; +import { ServerStatsStore } from './../stores/ServerStatsStore/ServerStatsStore'; +import { NavStore } from './../stores/NavStore/NavStore'; +import { AlertListStore } from './../stores/AlertListStore/AlertListStore'; +import { ViewStore } from './../stores/ViewStore/ViewStore'; + +interface IContainerProps { + search: typeof SearchStore.Type; + serverStats: typeof ServerStatsStore.Type; + nav: typeof NavStore.Type; + alertList: typeof AlertListStore.Type; + view: typeof ViewStore.Type; +} + +export default IContainerProps; diff --git a/public/app/containers/ServerStats/ServerStats.jest.tsx b/public/app/containers/ServerStats/ServerStats.jest.tsx new file mode 100644 index 00000000000..ae5cb736ad6 --- /dev/null +++ b/public/app/containers/ServerStats/ServerStats.jest.tsx @@ -0,0 +1,30 @@ +import React from 'react'; +import renderer from 'react-test-renderer'; +import { ServerStats } from './ServerStats'; +import { RootStore } from 'app/stores/RootStore/RootStore'; +import { backendSrv, createNavTree } from 'test/mocks/common'; + +describe('ServerStats', () => { + it('Should render table with stats', done => { + backendSrv.get.mockReturnValue( + Promise.resolve({ + dashboards: 10, + }) + ); + + const store = RootStore.create( + {}, + { + backendSrv: backendSrv, + navTree: createNavTree('cfg', 'admin', 'server-stats'), + } + ); + + const page = renderer.create(); + + setTimeout(() => { + expect(page.toJSON()).toMatchSnapshot(); + done(); + }); + }); +}); diff --git a/public/app/containers/ServerStats/ServerStats.tsx b/public/app/containers/ServerStats/ServerStats.tsx new file mode 100644 index 00000000000..e40b441d967 --- /dev/null +++ b/public/app/containers/ServerStats/ServerStats.tsx @@ -0,0 +1,45 @@ +import React from 'react'; +import { inject, observer } from 'mobx-react'; +import PageHeader from 'app/core/components/PageHeader/PageHeader'; +import IContainerProps from 'app/containers/IContainerProps'; + +@inject('nav', 'serverStats') +@observer +export class ServerStats extends React.Component { + constructor(props) { + super(props); + const { nav, serverStats } = this.props; + + nav.load('cfg', 'admin', 'server-stats'); + serverStats.load(); + } + + render() { + const { nav, serverStats } = this.props; + return ( +
    + +
    + + + + + + + + {serverStats.stats.map(StatItem)} +
    NameValue
    +
    +
    + ); + } +} + +function StatItem(stat) { + return ( + + {stat.name} + {stat.value} + + ); +} diff --git a/public/app/containers/ServerStats/__snapshots__/ServerStats.jest.tsx.snap b/public/app/containers/ServerStats/__snapshots__/ServerStats.jest.tsx.snap new file mode 100644 index 00000000000..7e6f1d11c27 --- /dev/null +++ b/public/app/containers/ServerStats/__snapshots__/ServerStats.jest.tsx.snap @@ -0,0 +1,170 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`ServerStats Should render table with stats 1`] = ` +
    +
    +
    +
    +
    + + + + +
    +

    + admin-Text +

    + +
    +
    + +
    +
    +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + Name + + Value +
    + Total dashboards + + 10 +
    + Total users + + 0 +
    + Active users (seen last 30 days) + + 0 +
    + Total orgs + + 0 +
    + Total playlists + + 0 +
    + Total snapshots + + 0 +
    + Total dashboard tags + + 0 +
    + Total starred dashboards + + 0 +
    + Total alerts + + 0 +
    +
    +
    +`; diff --git a/public/app/core/angular_wrappers.ts b/public/app/core/angular_wrappers.ts index 83a70fa4c8a..6e68e7c8d2f 100644 --- a/public/app/core/angular_wrappers.ts +++ b/public/app/core/angular_wrappers.ts @@ -3,10 +3,14 @@ import { PasswordStrength } from './components/PasswordStrength'; import PageHeader from './components/PageHeader/PageHeader'; import EmptyListCTA from './components/EmptyListCTA/EmptyListCTA'; import LoginBackground from './components/Login/LoginBackground'; +import { SearchResult } from './components/search/SearchResult'; +import UserPicker from './components/UserPicker/UserPicker'; export function registerAngularDirectives() { react2AngularDirective('passwordStrength', PasswordStrength, ['password']); react2AngularDirective('pageHeader', PageHeader, ['model', 'noTabs']); react2AngularDirective('emptyListCta', EmptyListCTA, ['model']); react2AngularDirective('loginBackground', LoginBackground, []); + react2AngularDirective('searchResult', SearchResult, []); + react2AngularDirective('selectUserPicker', UserPicker, ['backendSrv', 'teamId', 'refreshList']); } diff --git a/public/app/core/components/EmptyListCTA/EmptyListCTA.jest.tsx b/public/app/core/components/EmptyListCTA/EmptyListCTA.jest.tsx index d62ae892a0a..4af60f3c839 100644 --- a/public/app/core/components/EmptyListCTA/EmptyListCTA.jest.tsx +++ b/public/app/core/components/EmptyListCTA/EmptyListCTA.jest.tsx @@ -3,19 +3,18 @@ import renderer from 'react-test-renderer'; import EmptyListCTA from './EmptyListCTA'; const model = { - title: 'Title', - buttonIcon: 'ga css class', - buttonLink: 'http://url/to/destination', - buttonTitle: 'Click me', - proTip: 'This is a tip', - proTipLink: 'http://url/to/tip/destination', - proTipLinkTitle: 'Learn more', - proTipTarget: '_blank' + title: 'Title', + buttonIcon: 'ga css class', + buttonLink: 'http://url/to/destination', + buttonTitle: 'Click me', + proTip: 'This is a tip', + proTipLink: 'http://url/to/tip/destination', + proTipLinkTitle: 'Learn more', + proTipTarget: '_blank', }; -describe('CollorPalette', () => { - - it('renders correctly', () => { +describe('EmptyListCTA', () => { + it('renders correctly', () => { const tree = renderer.create().toJSON(); expect(tree).toMatchSnapshot(); }); diff --git a/public/app/core/components/EmptyListCTA/__snapshots__/EmptyListCTA.jest.tsx.snap b/public/app/core/components/EmptyListCTA/__snapshots__/EmptyListCTA.jest.tsx.snap index 0da3d94aaa8..6d47c984d5e 100644 --- a/public/app/core/components/EmptyListCTA/__snapshots__/EmptyListCTA.jest.tsx.snap +++ b/public/app/core/components/EmptyListCTA/__snapshots__/EmptyListCTA.jest.tsx.snap @@ -1,6 +1,6 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`CollorPalette renders correctly 1`] = ` +exports[`EmptyListCTA renders correctly 1`] = `
    diff --git a/public/app/core/components/PageHeader/PageHeader.tsx b/public/app/core/components/PageHeader/PageHeader.tsx index 9b45267a8e5..b6b6511c04e 100644 --- a/public/app/core/components/PageHeader/PageHeader.tsx +++ b/public/app/core/components/PageHeader/PageHeader.tsx @@ -1,7 +1,7 @@ -import React from "react"; -import { NavModel, NavModelItem } from "../../nav_model_srv"; -import classNames from "classnames"; -import appEvents from "app/core/app_events"; +import React from 'react'; +import { NavModel, NavModelItem } from '../../nav_model_srv'; +import classNames from 'classnames'; +import appEvents from 'app/core/app_events'; export interface IProps { model: NavModel; @@ -13,8 +13,8 @@ function TabItem(tab: NavModelItem) { } let tabClasses = classNames({ - "gf-tabs-link": true, - active: tab.active + 'gf-tabs-link': true, + active: tab.active, }); return ( @@ -49,13 +49,7 @@ function Navigation({ main }: { main: NavModelItem }) { ); } -function SelectNav({ - main, - customCss -}: { - main: NavModelItem; - customCss: string; -}) { +function SelectNav({ main, customCss }: { main: NavModelItem; customCss: string }) { const defaultSelectedItem = main.children.find(navItem => { return navItem.active === true; }); @@ -63,15 +57,12 @@ function SelectNav({ const gotoUrl = evt => { var element = evt.target; var url = element.options[element.selectedIndex].value; - appEvents.emit("location-change", { href: url }); + appEvents.emit('location-change', { href: url }); }; return (
    -
    + +
    +
    + +`; diff --git a/public/app/core/components/UserPicker/__snapshots__/UserPickerOption.jest.tsx.snap b/public/app/core/components/UserPicker/__snapshots__/UserPickerOption.jest.tsx.snap new file mode 100644 index 00000000000..3b9351b2338 --- /dev/null +++ b/public/app/core/components/UserPicker/__snapshots__/UserPickerOption.jest.tsx.snap @@ -0,0 +1,17 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`UserPickerOption renders correctly 1`] = ` + +`; diff --git a/public/app/core/components/code_editor/code_editor.ts b/public/app/core/components/code_editor/code_editor.ts index 36d6c1b1020..2a0fe103bcf 100644 --- a/public/app/core/components/code_editor/code_editor.ts +++ b/public/app/core/components/code_editor/code_editor.ts @@ -56,9 +56,7 @@ function link(scope, elem, attrs) { let maxLines = attrs.maxLines || DEFAULT_MAX_LINES; let showGutter = attrs.showGutter !== undefined; let tabSize = attrs.tabSize || DEFAULT_TAB_SIZE; - let behavioursEnabled = attrs.behavioursEnabled - ? attrs.behavioursEnabled === 'true' - : DEFAULT_BEHAVIOURS; + let behavioursEnabled = attrs.behavioursEnabled ? attrs.behavioursEnabled === 'true' : DEFAULT_BEHAVIOURS; // Initialize editor let aceElem = elem.get(0); diff --git a/public/app/core/components/colorpicker/spectrum_picker.ts b/public/app/core/components/colorpicker/spectrum_picker.ts index 45d367bfbe8..6e93a4f39f4 100644 --- a/public/app/core/components/colorpicker/spectrum_picker.ts +++ b/public/app/core/components/colorpicker/spectrum_picker.ts @@ -12,8 +12,7 @@ export function spectrumPicker() { require: 'ngModel', scope: true, replace: true, - template: - '', + template: '', link: function(scope, element, attrs, ngModel) { scope.ngModel = ngModel; scope.onColorChange = color => { diff --git a/public/app/core/components/form_dropdown/form_dropdown.ts b/public/app/core/components/form_dropdown/form_dropdown.ts index f8d715579a5..1fa1dea4338 100644 --- a/public/app/core/components/form_dropdown/form_dropdown.ts +++ b/public/app/core/components/form_dropdown/form_dropdown.ts @@ -32,13 +32,7 @@ export class FormDropdownCtrl { lookupText: boolean; /** @ngInject **/ - constructor( - private $scope, - $element, - private $sce, - private templateSrv, - private $q - ) { + constructor(private $scope, $element, private $sce, private templateSrv, private $q) { this.inputElement = $element.find('input').first(); this.linkElement = $element.find('a').first(); this.linkMode = true; @@ -50,8 +44,7 @@ export class FormDropdownCtrl { if (this.labelMode) { this.cssClasses = 'gf-form-label ' + this.cssClass; } else { - this.cssClasses = - 'gf-form-input gf-form-input--dropdown ' + this.cssClass; + this.cssClasses = 'gf-form-input gf-form-input--dropdown ' + this.cssClass; } this.inputElement.attr('data-provide', 'typeahead'); @@ -207,16 +200,11 @@ export class FormDropdownCtrl { updateDisplay(text) { this.text = text; - this.display = this.$sce.trustAsHtml( - this.templateSrv.highlightVariablesAsHtml(text) - ); + this.display = this.$sce.trustAsHtml(this.templateSrv.highlightVariablesAsHtml(text)); } open() { - this.inputElement.css( - 'width', - Math.max(this.linkElement.width(), 80) + 16 + 'px' - ); + this.inputElement.css('width', Math.max(this.linkElement.width(), 80) + 16 + 'px'); this.inputElement.show(); this.inputElement.focus(); diff --git a/public/app/core/components/gf_page.ts b/public/app/core/components/gf_page.ts index 5df3bd89715..ad0770940ec 100644 --- a/public/app/core/components/gf_page.ts +++ b/public/app/core/components/gf_page.ts @@ -1,5 +1,3 @@ -/// - import coreModule from 'app/core/core_module'; const template = ` diff --git a/public/app/core/components/grafana_app.ts b/public/app/core/components/grafana_app.ts index 54685b5cf35..c00b7b2181b 100644 --- a/public/app/core/components/grafana_app.ts +++ b/public/app/core/components/grafana_app.ts @@ -6,32 +6,29 @@ import coreModule from 'app/core/core_module'; import { profiler } from 'app/core/profiler'; import appEvents from 'app/core/app_events'; import Drop from 'tether-drop'; +import { createStore } from 'app/stores/store'; +import colors from 'app/core/utils/colors'; export class GrafanaCtrl { /** @ngInject */ - constructor( - $scope, - alertSrv, - utilSrv, - $rootScope, - $controller, - contextSrv, - globalEventSrv - ) { + constructor($scope, alertSrv, utilSrv, $rootScope, $controller, contextSrv, bridgeSrv, backendSrv) { + createStore(backendSrv); + $scope.init = function() { $scope.contextSrv = contextSrv; - - $rootScope.appSubUrl = config.appSubUrl; + $scope.appSubUrl = config.appSubUrl; $scope._ = _; profiler.init(config, $rootScope); alertSrv.init(); utilSrv.init(); - globalEventSrv.init(); + bridgeSrv.init(); $scope.dashAlerts = alertSrv; }; + $rootScope.colors = colors; + $scope.initDashboard = function(dashboardData, viewScope) { $scope.appEvent('dashboard-fetch-end', dashboardData); $controller('DashboardCtrl', { $scope: viewScope }).init(dashboardData); @@ -54,76 +51,12 @@ export class GrafanaCtrl { appEvents.emit(name, payload); }; - $rootScope.colors = [ - '#7EB26D', - '#EAB839', - '#6ED0E0', - '#EF843C', - '#E24D42', - '#1F78C1', - '#BA43A9', - '#705DA0', - '#508642', - '#CCA300', - '#447EBC', - '#C15C17', - '#890F02', - '#0A437C', - '#6D1F62', - '#584477', - '#B7DBAB', - '#F4D598', - '#70DBED', - '#F9BA8F', - '#F29191', - '#82B5D8', - '#E5A8E2', - '#AEA2E0', - '#629E51', - '#E5AC0E', - '#64B0C8', - '#E0752D', - '#BF1B00', - '#0A50A1', - '#962D82', - '#614D93', - '#9AC48A', - '#F2C96D', - '#65C5DB', - '#F9934E', - '#EA6460', - '#5195CE', - '#D683CE', - '#806EB7', - '#3F6833', - '#967302', - '#2F575E', - '#99440A', - '#58140C', - '#052B51', - '#511749', - '#3F2B5B', - '#E0F9D7', - '#FCEACA', - '#CFFAFF', - '#F9E2D2', - '#FCE2DE', - '#BADFF4', - '#F9D9F9', - '#DEDAF7', - ]; - $scope.init(); } } /** @ngInject */ -export function grafanaAppDirective( - playlistSrv, - contextSrv, - $timeout, - $rootScope -) { +export function grafanaAppDirective(playlistSrv, contextSrv, $timeout, $rootScope, $location) { return { restrict: 'E', controller: GrafanaCtrl, @@ -269,10 +202,7 @@ export function grafanaAppDirective( // hide search if (body.find('.search-container').length > 0) { - if ( - target.parents('.search-results-container, .search-field-wrapper') - .length === 0 - ) { + if (target.parents('.search-results-container, .search-field-wrapper').length === 0) { scope.$apply(function() { scope.appEvent('hide-dash-search'); }); @@ -281,10 +211,7 @@ export function grafanaAppDirective( // hide popovers var popover = elem.find('.popover'); - if ( - popover.length > 0 && - target.parents('.graph-legend').length === 0 - ) { + if (popover.length > 0 && target.parents('.graph-legend').length === 0) { popover.hide(); } }); diff --git a/public/app/core/components/help/help.ts b/public/app/core/components/help/help.ts index a594b95bb4d..0676b4dae99 100644 --- a/public/app/core/components/help/help.ts +++ b/public/app/core/components/help/help.ts @@ -1,5 +1,3 @@ -/// - import coreModule from '../../core_module'; import appEvents from 'app/core/app_events'; diff --git a/public/app/core/components/info_popover.ts b/public/app/core/components/info_popover.ts index 9d2d13c9f01..59332a6f716 100644 --- a/public/app/core/components/info_popover.ts +++ b/public/app/core/components/info_popover.ts @@ -1,5 +1,3 @@ -/// - import _ from 'lodash'; import coreModule from 'app/core/core_module'; import Drop from 'tether-drop'; @@ -10,10 +8,10 @@ export function infoPopover() { template: '', transclude: true, link: function(scope, elem, attrs, ctrl, transclude) { - var offset = attrs.offset || '0 -10px'; - var position = attrs.position || 'right middle'; - var classes = 'drop-help drop-hide-out-of-bounds'; - var openOn = 'hover'; + let offset = attrs.offset || '0 -10px'; + let position = attrs.position || 'right middle'; + let classes = 'drop-help drop-hide-out-of-bounds'; + let openOn = 'hover'; elem.addClass('gf-form-help-icon'); @@ -26,14 +24,14 @@ export function infoPopover() { } transclude(function(clone, newScope) { - var content = document.createElement('div'); + let content = document.createElement('div'); content.className = 'markdown-html'; _.each(clone, node => { content.appendChild(node); }); - var drop = new Drop({ + let dropOptions = { target: elem[0], content: content, position: position, @@ -50,11 +48,16 @@ export function infoPopover() { }, ], }, - }); + }; - var unbind = scope.$on('$destroy', function() { - drop.destroy(); - unbind(); + // Create drop in next digest after directive content is rendered. + scope.$applyAsync(() => { + let drop = new Drop(dropOptions); + + let unbind = scope.$on('$destroy', function() { + drop.destroy(); + unbind(); + }); }); }); }, diff --git a/public/app/core/components/json_explorer/helpers.ts b/public/app/core/components/json_explorer/helpers.ts index 69fd422fff1..5b053792d73 100644 --- a/public/app/core/components/json_explorer/helpers.ts +++ b/public/app/core/components/json_explorer/helpers.ts @@ -103,11 +103,7 @@ export function cssClass(className: string): string { * Creates a new DOM element wiht given type and class * TODO: move me to helpers */ -export function createElement( - type: string, - className?: string, - content?: Element | string -): Element { +export function createElement(type: string, className?: string, content?: Element | string): Element { const el = document.createElement(type); if (className) { el.classList.add(cssClass(className)); diff --git a/public/app/core/components/json_explorer/json_explorer.ts b/public/app/core/components/json_explorer/json_explorer.ts index 5c879bcee0f..9cc1b53bc82 100644 --- a/public/app/core/components/json_explorer/json_explorer.ts +++ b/public/app/core/components/json_explorer/json_explorer.ts @@ -1,14 +1,7 @@ // Based on work https://github.com/mohsen1/json-formatter-js // Licence MIT, Copyright (c) 2015 Mohsen Azimi -import { - isObject, - getObjectName, - getType, - getValuePreview, - cssClass, - createElement, -} from './helpers'; +import { isObject, getObjectName, getType, getValuePreview, cssClass, createElement } from './helpers'; import _ from 'lodash'; @@ -112,9 +105,7 @@ export class JsonExplorer { private get isDate(): boolean { return ( this.type === 'string' && - (DATE_STRING_REGEX.test(this.json) || - JSON_DATE_REGEX.test(this.json) || - PARTIAL_DATE_REGEX.test(this.json)) + (DATE_STRING_REGEX.test(this.json) || JSON_DATE_REGEX.test(this.json) || PARTIAL_DATE_REGEX.test(this.json)) ); } @@ -151,9 +142,7 @@ export class JsonExplorer { * is this an empty object or array? */ private get isEmpty(): boolean { - return ( - this.isEmptyObject || (this.keys && !this.keys.length && this.isArray) - ); + return this.isEmptyObject || (this.keys && !this.keys.length && this.isArray); } /* @@ -234,11 +223,7 @@ export class JsonExplorer { } isNumberArray() { - return ( - this.json.length > 0 && - this.json.length < 4 && - (_.isNumber(this.json[0]) || _.isNumber(this.json[1])) - ); + return this.json.length > 0 && this.json.length < 4 && (_.isNumber(this.json[0]) || _.isNumber(this.json[1])); } renderArray() { @@ -249,17 +234,13 @@ export class JsonExplorer { if (this.isNumberArray()) { this.json.forEach((val, index) => { if (index > 0) { - arrayWrapperSpan.appendChild( - createElement('span', 'array-comma', ',') - ); + arrayWrapperSpan.appendChild(createElement('span', 'array-comma', ',')); } arrayWrapperSpan.appendChild(createElement('span', 'number', val)); }); this.skipChildren = true; } else { - arrayWrapperSpan.appendChild( - createElement('span', 'number', this.json.length) - ); + arrayWrapperSpan.appendChild(createElement('span', 'number', this.json.length)); } arrayWrapperSpan.appendChild(createElement('span', 'bracket', ']')); @@ -298,11 +279,7 @@ export class JsonExplorer { const objectWrapperSpan = createElement('span'); // get constructor name and append it to wrapper span - var constructorName = createElement( - 'span', - 'constructor-name', - this.constructorName - ); + var constructorName = createElement('span', 'constructor-name', this.constructorName); objectWrapperSpan.appendChild(constructorName); // if it's an array append the array specific elements like brackets and length @@ -399,12 +376,7 @@ export class JsonExplorer { let index = 0; const addAChild = () => { const key = this.keys[index]; - const formatter = new JsonExplorer( - this.json[key], - this.open - 1, - this.config, - key - ); + const formatter = new JsonExplorer(this.json[key], this.open - 1, this.config, key); children.appendChild(formatter.render()); index += 1; @@ -421,12 +393,7 @@ export class JsonExplorer { requestAnimationFrame(addAChild); } else { this.keys.forEach(key => { - const formatter = new JsonExplorer( - this.json[key], - this.open - 1, - this.config, - key - ); + const formatter = new JsonExplorer(this.json[key], this.open - 1, this.config, key); children.appendChild(formatter.render()); }); } @@ -437,9 +404,7 @@ export class JsonExplorer { * Animated option is used when user triggers this via a click */ removeChildren(animated = false) { - const childrenElement = this.element.querySelector( - `div.${cssClass('children')}` - ) as HTMLDivElement; + const childrenElement = this.element.querySelector(`div.${cssClass('children')}`) as HTMLDivElement; if (animated) { let childrenRemoved = 0; diff --git a/public/app/core/components/manage_dashboards/manage_dashboards.html b/public/app/core/components/manage_dashboards/manage_dashboards.html index 90091f3032c..e9d7d743063 100644 --- a/public/app/core/components/manage_dashboards/manage_dashboards.html +++ b/public/app/core/components/manage_dashboards/manage_dashboards.html @@ -5,11 +5,11 @@
    - + Dashboard - + Folder @@ -60,22 +60,20 @@ switch-class="gf-form-switch--transparent gf-form-switch--search-result-filter-row__checkbox" />
    -
    +
    @@ -110,11 +108,11 @@
    diff --git a/public/app/core/components/manage_dashboards/manage_dashboards.ts b/public/app/core/components/manage_dashboards/manage_dashboards.ts index b8132f6f643..011e020f588 100644 --- a/public/app/core/components/manage_dashboards/manage_dashboards.ts +++ b/public/app/core/components/manage_dashboards/manage_dashboards.ts @@ -13,11 +13,7 @@ export class ManageDashboardsCtrl { canMove = false; hasFilters = false; selectAllChecked = false; - starredFilterOptions = [ - { text: 'Filter by Starred', disabled: true }, - { text: 'Yes' }, - { text: 'No' }, - ]; + starredFilterOptions = [{ text: 'Filter by Starred', disabled: true }, { text: 'Yes' }, { text: 'No' }]; selectedStarredFilter: any; folderId?: number; @@ -53,10 +49,7 @@ export class ManageDashboardsCtrl { this.canMove = false; this.canDelete = false; this.selectAllChecked = false; - this.hasFilters = - this.query.query.length > 0 || - this.query.tag.length > 0 || - this.query.starred; + this.hasFilters = this.query.query.length > 0 || this.query.tag.length > 0 || this.query.starred; if (!result) { this.sections = []; @@ -126,16 +119,10 @@ export class ManageDashboardsCtrl { let text2; if (folderCount > 0 && dashCount > 0) { - text += `selected folder${folderCount === 1 ? '' : 's'} and dashboard${ - dashCount === 1 ? '' : 's' - }?`; - text2 = `All dashboards of the selected folder${ - folderCount === 1 ? '' : 's' - } will also be deleted`; + text += `selected folder${folderCount === 1 ? '' : 's'} and dashboard${dashCount === 1 ? '' : 's'}?`; + text2 = `All dashboards of the selected folder${folderCount === 1 ? '' : 's'} will also be deleted`; } else if (folderCount > 0) { - text += `selected folder${ - folderCount === 1 ? '' : 's' - } and all its dashboards?`; + text += `selected folder${folderCount === 1 ? '' : 's'} and all its dashboards?`; } else { text += `selected dashboard${dashCount === 1 ? '' : 's'}?`; } @@ -165,22 +152,16 @@ export class ManageDashboardsCtrl { let msg; if (folderCount > 0 && dashCount > 0) { - header = `Folder${folderCount === 1 ? '' : 's'} And Dashboard${ - dashCount === 1 ? '' : 's' - } Deleted`; + header = `Folder${folderCount === 1 ? '' : 's'} And Dashboard${dashCount === 1 ? '' : 's'} Deleted`; msg = `${folderCount} folder${folderCount === 1 ? '' : 's'} `; - msg += `and ${dashCount} dashboard${ - dashCount === 1 ? '' : 's' - } has been deleted`; + msg += `and ${dashCount} dashboard${dashCount === 1 ? '' : 's'} has been deleted`; } else if (folderCount > 0) { header = `Folder${folderCount === 1 ? '' : 's'} Deleted`; if (folderCount === 1) { msg = `${folders[0].dashboard.title} has been deleted`; } else { - msg = `${folderCount} folder${ - folderCount === 1 ? '' : 's' - } has been deleted`; + msg = `${folderCount} folder${folderCount === 1 ? '' : 's'} has been deleted`; } } else if (dashCount > 0) { header = `Dashboard${dashCount === 1 ? '' : 's'} Deleted`; @@ -188,9 +169,7 @@ export class ManageDashboardsCtrl { if (dashCount === 1) { msg = `${dashboards[0].dashboard.title} has been deleted`; } else { - msg = `${dashCount} dashboard${ - dashCount === 1 ? '' : 's' - } has been deleted`; + msg = `${dashCount} dashboard${dashCount === 1 ? '' : 's'} has been deleted`; } } @@ -231,9 +210,7 @@ export class ManageDashboardsCtrl { getTags() { return this.searchSrv.getDashboardTags().then(results => { - this.tagFilterOptions = [ - { term: 'Filter By Tag', disabled: true }, - ].concat(results); + this.tagFilterOptions = [{ term: 'Filter By Tag', disabled: true }].concat(results); this.selectedTagFilter = this.tagFilterOptions[0]; }); } @@ -297,13 +274,22 @@ export class ManageDashboardsCtrl { this.query.starred = false; this.getDashboards(); } + + createDashboardUrl() { + let url = 'dashboard/new'; + + if (this.folderId) { + url += `?folderId=${this.folderId}`; + } + + return url; + } } export function manageDashboardsDirective() { return { restrict: 'E', - templateUrl: - 'public/app/core/components/manage_dashboards/manage_dashboards.html', + templateUrl: 'public/app/core/components/manage_dashboards/manage_dashboards.html', controller: ManageDashboardsCtrl, bindToController: true, controllerAs: 'ctrl', diff --git a/public/app/core/components/org_switcher.ts b/public/app/core/components/org_switcher.ts index b682acfcad0..d6efeb51779 100644 --- a/public/app/core/components/org_switcher.ts +++ b/public/app/core/components/org_switcher.ts @@ -1,5 +1,3 @@ -/// - import coreModule from 'app/core/core_module'; import { contextSrv } from 'app/core/services/context_srv'; @@ -63,9 +61,7 @@ export class OrgSwitchCtrl { setUsingOrg(org) { return this.backendSrv.post('/api/user/using/' + org.orgId).then(() => { const re = /orgId=\d+/gi; - this.setWindowLocationHref( - this.getWindowLocationHref().replace(re, 'orgId=' + org.orgId) - ); + this.setWindowLocationHref(this.getWindowLocationHref().replace(re, 'orgId=' + org.orgId)); }); } diff --git a/public/app/core/components/query_part/query_part.ts b/public/app/core/components/query_part/query_part.ts index 033e2e450a2..93f28cbc04f 100644 --- a/public/app/core/components/query_part/query_part.ts +++ b/public/app/core/components/query_part/query_part.ts @@ -1,5 +1,3 @@ -/// - import _ from 'lodash'; export class QueryPartDef { diff --git a/public/app/core/components/query_part/query_part_editor.ts b/public/app/core/components/query_part/query_part_editor.ts index e993d4ce24b..138da186238 100644 --- a/public/app/core/components/query_part/query_part_editor.ts +++ b/public/app/core/components/query_part/query_part_editor.ts @@ -1,5 +1,3 @@ -/// - import _ from 'lodash'; import $ from 'jquery'; import coreModule from 'app/core/core_module'; @@ -17,8 +15,7 @@ var template = ` /** @ngInject */ export function queryPartEditorDirective($compile, templateSrv) { - var paramTemplate = - ''; + var paramTemplate = ''; return { restrict: 'E', @@ -102,14 +99,12 @@ export function queryPartEditorDirective($compile, templateSrv) { } $scope.$apply(function() { - $scope - .handleEvent({ $event: { name: 'get-param-options' } }) - .then(function(result) { - var dynamicOptions = _.map(result, function(op) { - return op.value; - }); - callback(dynamicOptions); + $scope.handleEvent({ $event: { name: 'get-param-options' } }).then(function(result) { + var dynamicOptions = _.map(result, function(op) { + return op.value; }); + callback(dynamicOptions); + }); }); }; @@ -136,11 +131,9 @@ export function queryPartEditorDirective($compile, templateSrv) { } $scope.showActionsMenu = function() { - $scope - .handleEvent({ $event: { name: 'get-part-actions' } }) - .then(res => { - $scope.partActions = res; - }); + $scope.handleEvent({ $event: { name: 'get-part-actions' } }).then(res => { + $scope.partActions = res; + }); }; $scope.triggerPartAction = function(action) { @@ -157,12 +150,8 @@ export function queryPartEditorDirective($compile, templateSrv) { $(', ').appendTo($paramsContainer); } - var paramValue = templateSrv.highlightVariablesAsHtml( - part.params[index] - ); - var $paramLink = $( - '' + paramValue + '' - ); + var paramValue = templateSrv.highlightVariablesAsHtml(part.params[index]); + var $paramLink = $('' + paramValue + ''); var $input = $(paramTemplate); $paramLink.appendTo($paramsContainer); diff --git a/public/app/core/components/scroll/scroll.ts b/public/app/core/components/scroll/scroll.ts index b625d917ca8..bf0bbfaec4b 100644 --- a/public/app/core/components/scroll/scroll.ts +++ b/public/app/core/components/scroll/scroll.ts @@ -1,5 +1,6 @@ import PerfectScrollbar from 'perfect-scrollbar'; import coreModule from 'app/core/core_module'; +import appEvents from 'app/core/app_events'; export function geminiScrollbar() { return { @@ -7,6 +8,19 @@ export function geminiScrollbar() { link: function(scope, elem, attrs) { let scrollbar = new PerfectScrollbar(elem[0]); + appEvents.on( + 'smooth-scroll-top', + () => { + elem.animate( + { + scrollTop: 0, + }, + 500 + ); + }, + scope + ); + scope.$on('$routeChangeSuccess', () => { elem[0].scrollTop = 0; }); diff --git a/public/app/core/components/search/SearchResult.tsx b/public/app/core/components/search/SearchResult.tsx new file mode 100644 index 00000000000..6d6b001cc1d --- /dev/null +++ b/public/app/core/components/search/SearchResult.tsx @@ -0,0 +1,86 @@ +import React from "react"; +import classNames from "classnames"; +import { observer } from "mobx-react"; +import { store } from "app/stores/store"; + +export interface SearchResultProps { + search: any; +} + +@observer +export class SearchResult extends React.Component { + constructor(props) { + super(props); + + this.state = { + search: store.search + }; + + store.search.query(); + } + + render() { + return this.state.search.sections.map(section => { + return ; + }); + } +} + +export interface SectionProps { + section: any; +} + +@observer +export class SearchResultSection extends React.Component { + constructor(props) { + super(props); + } + + renderItem(item) { + return ( + + + + + +
    {item.title}
    +
    +
    + ); + } + + toggleSection = () => { + this.props.section.toggle(); + }; + + render() { + let collapseClassNames = classNames({ + fa: true, + "fa-plus": !this.props.section.expanded, + "fa-minus": this.props.section.expanded, + "search-section__header__toggle": true + }); + + return ( +
    +
    + + + {this.props.section.title} + + +
    + {this.props.section.expanded && ( +
    + {this.props.section.items.map(this.renderItem)} +
    + )} +
    + ); + } +} diff --git a/public/app/core/components/search/search.ts b/public/app/core/components/search/search.ts index 632db4270f4..dcb88d7d32d 100644 --- a/public/app/core/components/search/search.ts +++ b/public/app/core/components/search/search.ts @@ -17,12 +17,7 @@ export class SearchCtrl { initialFolderFilterTitle: string; /** @ngInject */ - constructor( - $scope, - private $location, - private $timeout, - private searchSrv: SearchSrv - ) { + constructor($scope, private $location, private $timeout, private searchSrv: SearchSrv) { appEvents.on('show-dash-search', this.openSearch.bind(this), $scope); appEvents.on('hide-dash-search', this.closeSearch.bind(this), $scope); @@ -75,9 +70,7 @@ export class SearchCtrl { if (currentItem) { if (currentItem.dashboardIndex !== undefined) { - const selectedDash = this.results[currentItem.folderIndex].items[ - currentItem.dashboardIndex - ]; + const selectedDash = this.results[currentItem.folderIndex].items[currentItem.dashboardIndex]; if (selectedDash) { this.$location.search({}); @@ -105,9 +98,7 @@ export class SearchCtrl { if (currentItem) { if (currentItem.dashboardIndex !== undefined) { - this.results[currentItem.folderIndex].items[ - currentItem.dashboardIndex - ].selected = false; + this.results[currentItem.folderIndex].items[currentItem.dashboardIndex].selected = false; } else { this.results[currentItem.folderIndex].selected = false; } @@ -123,10 +114,7 @@ export class SearchCtrl { this.selectedIndex = (newIndex %= max) < 0 ? newIndex + max : newIndex; const selectedItem = flattenedResult[this.selectedIndex]; - if ( - selectedItem.dashboardIndex === undefined && - this.results[selectedItem.folderIndex].id === 0 - ) { + if (selectedItem.dashboardIndex === undefined && this.results[selectedItem.folderIndex].id === 0) { this.moveSelection(direction); return; } @@ -137,9 +125,7 @@ export class SearchCtrl { return; } - this.results[selectedItem.folderIndex].items[ - selectedItem.dashboardIndex - ].selected = true; + this.results[selectedItem.folderIndex].items[selectedItem.dashboardIndex].selected = true; return; } @@ -167,9 +153,7 @@ export class SearchCtrl { queryHasNoFilters() { var query = this.query; - return ( - query.query === '' && query.starred === false && query.tag.length === 0 - ); + return query.query === '' && query.starred === false && query.tag.length === 0; } filterByTag(tag) { diff --git a/public/app/core/components/sidemenu/sidemenu.ts b/public/app/core/components/sidemenu/sidemenu.ts index 20e8975de53..fb9d9be7f70 100644 --- a/public/app/core/components/sidemenu/sidemenu.ts +++ b/public/app/core/components/sidemenu/sidemenu.ts @@ -13,25 +13,14 @@ export class SideMenuCtrl { isOpenMobile: boolean; /** @ngInject */ - constructor( - private $scope, - private $rootScope, - private $location, - private contextSrv, - private $timeout - ) { + constructor(private $scope, private $rootScope, private $location, private contextSrv, private $timeout) { this.isSignedIn = contextSrv.isSignedIn; this.user = contextSrv.user; - this.mainLinks = _.filter( - config.bootData.navTree, - item => !item.hideFromMenu - ); - this.bottomNav = _.filter( - config.bootData.navTree, - item => item.hideFromMenu - ); - this.loginUrl = - 'login?redirect=' + encodeURIComponent(this.$location.path()); + + let navTree = _.cloneDeep(config.bootData.navTree); + this.mainLinks = _.filter(navTree, item => !item.hideFromMenu); + this.bottomNav = _.filter(navTree, item => item.hideFromMenu); + this.loginUrl = 'login?redirect=' + encodeURIComponent(this.$location.path()); if (contextSrv.user.orgCount > 1) { let profileNode = _.find(this.bottomNav, { id: 'profile' }); @@ -41,8 +30,7 @@ export class SideMenuCtrl { } this.$scope.$on('$routeChangeSuccess', () => { - this.loginUrl = - 'login?redirect=' + encodeURIComponent(this.$location.path()); + this.loginUrl = 'login?redirect=' + encodeURIComponent(this.$location.path()); }); } diff --git a/public/app/core/components/switch.ts b/public/app/core/components/switch.ts index 3203a42cd70..d2de09216cc 100644 --- a/public/app/core/components/switch.ts +++ b/public/app/core/components/switch.ts @@ -1,5 +1,3 @@ -/// - import coreModule from 'app/core/core_module'; var template = ` diff --git a/public/app/core/components/team_picker.ts b/public/app/core/components/team_picker.ts index a289b4075d4..228767a76c4 100644 --- a/public/app/core/components/team_picker.ts +++ b/public/app/core/components/team_picker.ts @@ -30,13 +30,11 @@ export class TeamPickerCtrl { searchGroups(query: string) { return Promise.resolve( - this.backendSrv - .get('/api/teams/search?perpage=10&page=1&query=' + query) - .then(result => { - return _.map(result.teams, ug => { - return { text: ug.name, value: ug }; - }); - }) + this.backendSrv.get('/api/teams/search?perpage=10&page=1&query=' + query).then(result => { + return _.map(result.teams, ug => { + return { text: ug.name, value: ug }; + }); + }) ); } diff --git a/public/app/core/components/user_picker.ts b/public/app/core/components/user_picker.ts index 36b7c34b1fa..606ded09885 100644 --- a/public/app/core/components/user_picker.ts +++ b/public/app/core/components/user_picker.ts @@ -26,13 +26,11 @@ export class UserPickerCtrl { searchUsers(query: string) { return Promise.resolve( - this.backendSrv - .get('/api/users/search?perpage=10&page=1&query=' + query) - .then(result => { - return _.map(result.users, user => { - return { text: user.login + ' - ' + user.email, value: user }; - }); - }) + this.backendSrv.get('/api/users/search?perpage=10&page=1&query=' + query).then(result => { + return _.map(result.users, user => { + return { text: user.login + ' - ' + user.email, value: user }; + }); + }) ); } diff --git a/public/app/core/constants.ts b/public/app/core/constants.ts index a45a14f1ea2..2642c5e400a 100644 --- a/public/app/core/constants.ts +++ b/public/app/core/constants.ts @@ -6,3 +6,5 @@ export const REPEAT_DIR_VERTICAL = 'v'; export const DEFAULT_PANEL_SPAN = 4; export const DEFAULT_ROW_HEIGHT = 250; export const MIN_PANEL_HEIGHT = GRID_CELL_HEIGHT * 3; + +export const LS_PANEL_COPY_KEY = 'panel-copy'; diff --git a/public/app/core/controllers/inspect_ctrl.ts b/public/app/core/controllers/inspect_ctrl.ts index 1e74bcf1968..5dd4cb3d06f 100644 --- a/public/app/core/controllers/inspect_ctrl.ts +++ b/public/app/core/controllers/inspect_ctrl.ts @@ -28,10 +28,7 @@ export class InspectCtrl { } if (model.error.config && model.error.config.params) { - $scope.request_parameters = _.map(model.error.config.params, function( - value, - key - ) { + $scope.request_parameters = _.map(model.error.config.params, function(value, key) { return { key: key, value: value }; }); } @@ -46,14 +43,9 @@ export class InspectCtrl { $scope.editor.index = 2; if (_.isString(model.error.config.data)) { - $scope.request_parameters = this.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 - ) { + $scope.request_parameters = _.map(model.error.config.data, function(value, key) { return { key: key, value: angular.toJson(value, true) }; }); } diff --git a/public/app/core/controllers/invited_ctrl.ts b/public/app/core/controllers/invited_ctrl.ts index f09c81522d9..e88c810b557 100644 --- a/public/app/core/controllers/invited_ctrl.ts +++ b/public/app/core/controllers/invited_ctrl.ts @@ -16,17 +16,15 @@ export class InvitedCtrl { }; $scope.init = function() { - backendSrv - .get('/api/user/invite/' + $routeParams.code) - .then(function(invite) { - $scope.formModel.name = invite.name; - $scope.formModel.email = invite.email; - $scope.formModel.username = invite.email; - $scope.formModel.inviteCode = $routeParams.code; + backendSrv.get('/api/user/invite/' + $routeParams.code).then(function(invite) { + $scope.formModel.name = invite.name; + $scope.formModel.email = invite.email; + $scope.formModel.username = invite.email; + $scope.formModel.inviteCode = $routeParams.code; - $scope.greeting = invite.name || invite.email || invite.username; - $scope.invitedBy = invite.invitedBy; - }); + $scope.greeting = invite.name || invite.email || invite.username; + $scope.invitedBy = invite.invitedBy; + }); }; $scope.submit = function() { @@ -34,11 +32,9 @@ export class InvitedCtrl { return; } - backendSrv - .post('/api/user/invite/complete', $scope.formModel) - .then(function() { - window.location.href = config.appSubUrl + '/'; - }); + backendSrv.post('/api/user/invite/complete', $scope.formModel).then(function() { + window.location.href = config.appSubUrl + '/'; + }); }; $scope.init(); diff --git a/public/app/core/controllers/json_editor_ctrl.ts b/public/app/core/controllers/json_editor_ctrl.ts index cedadc5370f..d369fe8b3c0 100644 --- a/public/app/core/controllers/json_editor_ctrl.ts +++ b/public/app/core/controllers/json_editor_ctrl.ts @@ -5,13 +5,15 @@ export class JsonEditorCtrl { /** @ngInject */ constructor($scope) { $scope.json = angular.toJson($scope.object, true); - $scope.canUpdate = - $scope.updateHandler !== void 0 && $scope.contextSrv.isEditor; + $scope.canUpdate = $scope.updateHandler !== void 0 && $scope.contextSrv.isEditor; + $scope.canCopy = $scope.enableCopy; $scope.update = function() { var newObject = angular.fromJson($scope.json); $scope.updateHandler(newObject, $scope.object); }; + + $scope.getContentForClipboard = () => $scope.json; } } diff --git a/public/app/core/controllers/login_ctrl.ts b/public/app/core/controllers/login_ctrl.ts index 79e5530ea91..313fc2efa1a 100644 --- a/public/app/core/controllers/login_ctrl.ts +++ b/public/app/core/controllers/login_ctrl.ts @@ -48,15 +48,13 @@ export class LoginCtrl { return; } - backendSrv - .post('/api/user/signup', $scope.formModel) - .then(function(result) { - if (result.status === 'SignUpCreated') { - $location.path('/signup').search({ email: $scope.formModel.email }); - } else { - window.location.href = config.appSubUrl + '/'; - } - }); + backendSrv.post('/api/user/signup', $scope.formModel).then(function(result) { + if (result.status === 'SignUpCreated') { + $location.path('/signup').search({ email: $scope.formModel.email }); + } else { + window.location.href = config.appSubUrl + '/'; + } + }); }; $scope.login = function() { diff --git a/public/app/core/controllers/reset_password_ctrl.ts b/public/app/core/controllers/reset_password_ctrl.ts index f2078a442c8..bc184838803 100644 --- a/public/app/core/controllers/reset_password_ctrl.ts +++ b/public/app/core/controllers/reset_password_ctrl.ts @@ -17,10 +17,7 @@ export class ResetPasswordCtrl { main: { icon: 'gicon gicon-branding', subTitle: 'Reset your Grafana password', - breadcrumbs: [ - { title: 'Login', url: '/login' }, - { title: 'Reset Password' }, - ], + breadcrumbs: [{ title: 'Login', url: 'login' }, { title: 'Reset Password' }], }, }; @@ -28,11 +25,9 @@ export class ResetPasswordCtrl { if (!$scope.sendResetForm.$valid) { return; } - backendSrv - .post('/api/user/password/send-reset-email', $scope.formModel) - .then(function() { - $scope.mode = 'email-sent'; - }); + backendSrv.post('/api/user/password/send-reset-email', $scope.formModel).then(function() { + $scope.mode = 'email-sent'; + }); }; $scope.submitReset = function() { @@ -45,11 +40,9 @@ export class ResetPasswordCtrl { return; } - backendSrv - .post('/api/user/password/reset', $scope.formModel) - .then(function() { - $location.path('login'); - }); + backendSrv.post('/api/user/password/reset', $scope.formModel).then(function() { + $location.path('login'); + }); }; } } diff --git a/public/app/core/controllers/signup_ctrl.ts b/public/app/core/controllers/signup_ctrl.ts index 38e30994dfa..dbaca216e30 100644 --- a/public/app/core/controllers/signup_ctrl.ts +++ b/public/app/core/controllers/signup_ctrl.ts @@ -1,16 +1,9 @@ -/// - import config from 'app/core/config'; import coreModule from '../core_module'; export class SignUpCtrl { /** @ngInject */ - constructor( - private $scope: any, - private backendSrv: any, - $location: any, - contextSrv: any - ) { + constructor(private $scope: any, private backendSrv: any, $location: any, contextSrv: any) { contextSrv.sidemenu = false; $scope.ctrl = this; @@ -29,7 +22,7 @@ export class SignUpCtrl { main: { icon: 'gicon gicon-branding', subTitle: 'Register your Grafana account', - breadcrumbs: [{ title: 'Login', url: '/login' }, { title: 'Sign Up' }], + breadcrumbs: [{ title: 'Login', url: 'login' }, { title: 'Sign Up' }], }, }; @@ -44,16 +37,13 @@ export class SignUpCtrl { return; } - this.backendSrv - .post('/api/user/signup/step2', this.$scope.formModel) - .then(rsp => { - if (rsp.code === 'redirect-to-select-org') { - window.location.href = - config.appSubUrl + '/profile/select-org?signup=1'; - } else { - window.location.href = config.appSubUrl + '/'; - } - }); + this.backendSrv.post('/api/user/signup/step2', this.$scope.formModel).then(rsp => { + if (rsp.code === 'redirect-to-select-org') { + window.location.href = config.appSubUrl + '/profile/select-org?signup=1'; + } else { + window.location.href = config.appSubUrl + '/'; + } + }); } } diff --git a/public/app/core/core.ts b/public/app/core/core.ts index bd9e0f7460d..bf041d608eb 100644 --- a/public/app/core/core.ts +++ b/public/app/core/core.ts @@ -35,7 +35,6 @@ import { queryPartEditorDirective } from './components/query_part/query_part_edi import { formDropdownDirective } from './components/form_dropdown/form_dropdown'; import 'app/core/controllers/all'; import 'app/core/services/all'; -import 'app/core/routes/routes'; import './filters/filters'; import coreModule from './core_module'; import appEvents from './app_events'; diff --git a/public/app/core/directives/array_join.ts b/public/app/core/directives/array_join.ts index 1bf1c6a9f53..f3416f43576 100644 --- a/public/app/core/directives/array_join.ts +++ b/public/app/core/directives/array_join.ts @@ -1,5 +1,3 @@ -/// - import _ from 'lodash'; import coreModule from '../core_module'; diff --git a/public/app/core/directives/diff-view.ts b/public/app/core/directives/diff-view.ts index 2770a09a00b..84cb597e543 100644 --- a/public/app/core/directives/diff-view.ts +++ b/public/app/core/directives/diff-view.ts @@ -1,5 +1,3 @@ -/// - import angular from 'angular'; import coreModule from '../core_module'; diff --git a/public/app/core/directives/give_focus.ts b/public/app/core/directives/give_focus.ts index 41767d55c0f..32e7205f84b 100644 --- a/public/app/core/directives/give_focus.ts +++ b/public/app/core/directives/give_focus.ts @@ -1,5 +1,3 @@ -/// - import coreModule from '../core_module'; coreModule.directive('giveFocus', function() { diff --git a/public/app/core/directives/misc.ts b/public/app/core/directives/misc.ts index 298d3ce5d3c..299de05f112 100644 --- a/public/app/core/directives/misc.ts +++ b/public/app/core/directives/misc.ts @@ -2,6 +2,7 @@ import angular from 'angular'; import Clipboard from 'clipboard'; import coreModule from '../core_module'; import kbn from 'app/core/utils/kbn'; +import { appEvents } from 'app/core/core'; /** @ngInject */ function tip($compile) { @@ -32,6 +33,10 @@ function clipboardButton() { }, }); + scope.clipboard.on('success', () => { + appEvents.emit('alert-success', ['Content copied to clipboard']); + }); + scope.$on('$destroy', function() { if (scope.clipboard) { scope.clipboard.destroy(); @@ -118,14 +123,7 @@ function editorCheckbox($compile, $interpolate) { var model = $interpolate(attrs.model)(scope); var ngchange = attrs.change ? ' ng-change="' + attrs.change + '"' : ''; var tip = attrs.tip ? ' ' + attrs.tip + '' : ''; - var label = - ''; + var label = ''; var template = '', - '', - ]; + var ul = ['']; for (let index = 0; index < items.length; index++) { let item = items[index]; @@ -172,9 +165,7 @@ function gfDropdown($parse, $compile, $timeout) { var li = '' + ' - import _ from 'lodash'; import angular from 'angular'; import moment from 'moment'; diff --git a/public/app/core/live/live_srv.ts b/public/app/core/live/live_srv.ts index 46e278aa749..e03e8296794 100644 --- a/public/app/core/live/live_srv.ts +++ b/public/app/core/live/live_srv.ts @@ -14,12 +14,7 @@ export class LiveSrv { getWebSocketUrl() { var l = window.location; - return ( - (l.protocol === 'https:' ? 'wss://' : 'ws://') + - l.host + - config.appSubUrl + - '/ws' - ); + return (l.protocol === 'https:' ? 'wss://' : 'ws://') + l.host + config.appSubUrl + '/ws'; } getConnection() { diff --git a/public/app/core/profiler.ts b/public/app/core/profiler.ts index 634a627899c..f459c5d4557 100644 --- a/public/app/core/profiler.ts +++ b/public/app/core/profiler.ts @@ -29,21 +29,9 @@ export class Profiler { ); $rootScope.onAppEvent('refresh', this.refresh.bind(this), $rootScope); - $rootScope.onAppEvent( - 'dashboard-fetch-end', - this.dashboardFetched.bind(this), - $rootScope - ); - $rootScope.onAppEvent( - 'dashboard-initialized', - this.dashboardInitialized.bind(this), - $rootScope - ); - $rootScope.onAppEvent( - 'panel-initialized', - this.panelInitialized.bind(this), - $rootScope - ); + $rootScope.onAppEvent('dashboard-fetch-end', this.dashboardFetched.bind(this), $rootScope); + $rootScope.onAppEvent('dashboard-initialized', this.dashboardInitialized.bind(this), $rootScope); + $rootScope.onAppEvent('panel-initialized', this.panelInitialized.bind(this), $rootScope); } refresh() { @@ -70,21 +58,12 @@ export class Profiler { dashboardInitialized() { setTimeout(() => { - console.log( - 'Dashboard::Performance Total Digests: ' + this.digestCounter - ); - console.log( - 'Dashboard::Performance Total Watchers: ' + this.getTotalWatcherCount() - ); - console.log( - 'Dashboard::Performance Total ScopeCount: ' + this.scopeCount - ); + console.log('Dashboard::Performance Total Digests: ' + this.digestCounter); + console.log('Dashboard::Performance Total Watchers: ' + this.getTotalWatcherCount()); + console.log('Dashboard::Performance Total ScopeCount: ' + this.scopeCount); - var timeTaken = - this.timings.lastPanelInitializedAt - this.timings.dashboardLoadStart; - console.log( - 'Dashboard::Performance All panels initialized in ' + timeTaken + ' ms' - ); + var timeTaken = this.timings.lastPanelInitializedAt - this.timings.dashboardLoadStart; + console.log('Dashboard::Performance All panels initialized in ' + timeTaken + ' ms'); // measure digest performance var rootDigestStart = window.performance.now(); @@ -92,10 +71,7 @@ export class Profiler { this.$rootScope.$apply(); } - console.log( - 'Dashboard::Performance Root Digest ' + - (window.performance.now() - rootDigestStart) / 30 - ); + console.log('Dashboard::Performance Root Digest ' + (window.performance.now() - rootDigestStart) / 30); }, 3000); } diff --git a/public/app/core/routes/bundle_loader.ts b/public/app/core/routes/bundle_loader.ts deleted file mode 100644 index e4e345b49fc..00000000000 --- a/public/app/core/routes/bundle_loader.ts +++ /dev/null @@ -1,26 +0,0 @@ -export class BundleLoader { - lazy: any; - - constructor(bundleName) { - var defer = null; - - this.lazy = [ - '$q', - '$route', - '$rootScope', - ($q, $route, $rootScope) => { - if (defer) { - return defer.promise; - } - - defer = $q.defer(); - - System.import(bundleName).then(() => { - defer.resolve(); - }); - - return defer.promise; - }, - ]; - } -} diff --git a/public/app/core/services/alert_srv.ts b/public/app/core/services/alert_srv.ts index 82c824a2b75..35e8e39804f 100644 --- a/public/app/core/services/alert_srv.ts +++ b/public/app/core/services/alert_srv.ts @@ -1,5 +1,3 @@ -/// - import angular from 'angular'; import _ from 'lodash'; import coreModule from 'app/core/core_module'; @@ -38,15 +36,9 @@ export class AlertSrv { this.$rootScope ); - appEvents.on('alert-warning', options => - this.set(options[0], options[1], 'warning', 5000) - ); - appEvents.on('alert-success', options => - this.set(options[0], options[1], 'success', 3000) - ); - appEvents.on('alert-error', options => - this.set(options[0], options[1], 'error', 7000) - ); + appEvents.on('alert-warning', options => this.set(options[0], options[1], 'warning', 5000)); + appEvents.on('alert-success', options => this.set(options[0], options[1], 'success', 3000)); + appEvents.on('alert-error', options => this.set(options[0], options[1], 'error', 7000)); appEvents.on('confirm-modal', this.showConfirmModal.bind(this)); } @@ -114,8 +106,7 @@ export class AlertSrv { }; scope.updateConfirmText = function(value) { - scope.confirmTextValid = - payload.confirmText.toLowerCase() === value.toLowerCase(); + scope.confirmTextValid = payload.confirmText.toLowerCase() === value.toLowerCase(); }; scope.title = payload.title; diff --git a/public/app/core/services/all.js b/public/app/core/services/all.js index 0053d789cbe..0a973440c5e 100644 --- a/public/app/core/services/all.js +++ b/public/app/core/services/all.js @@ -8,6 +8,6 @@ define([ './segment_srv', './backend_srv', './dynamic_directive_srv', - './global_event_srv' + './bridge_srv' ], function () {}); diff --git a/public/app/core/services/backend_srv.ts b/public/app/core/services/backend_srv.ts index 01ca5a70d70..3204db0ce5d 100644 --- a/public/app/core/services/backend_srv.ts +++ b/public/app/core/services/backend_srv.ts @@ -1,5 +1,3 @@ -/// - import _ from 'lodash'; import coreModule from 'app/core/core_module'; import appEvents from 'app/core/app_events'; @@ -11,13 +9,7 @@ export class BackendSrv { private noBackendCache: boolean; /** @ngInject */ - constructor( - private $http, - private alertSrv, - private $q, - private $timeout, - private contextSrv - ) {} + constructor(private $http, private alertSrv, private $q, private $timeout, private contextSrv) {} get(url, params?) { return this.request({ method: 'GET', url: url, params: params }); @@ -109,11 +101,7 @@ export class BackendSrv { }, err => { // handle unauthorized - if ( - err.status === 401 && - this.contextSrv.user.isSignedIn && - firstAttempt - ) { + if (err.status === 401 && this.contextSrv.user.isSignedIn && firstAttempt) { return this.loginPing().then(() => { options.retry = 1; return this.request(options); @@ -242,7 +230,7 @@ export class BackendSrv { return this.post('/api/dashboards/db/', { dashboard: dash, - folderId: dash.folderId, + folderId: options.folderId, overwrite: options.overwrite === true, message: options.message || '', }); @@ -295,17 +283,14 @@ export class BackendSrv { const tasks = []; for (let slug of dashboardSlugs) { - tasks.push( - this.createTask(this.moveDashboard.bind(this), true, slug, toFolder) - ); + tasks.push(this.createTask(this.moveDashboard.bind(this), true, slug, toFolder)); } return this.executeInOrder(tasks, []).then(result => { return { totalCount: result.length, successCount: _.filter(result, { succeeded: true }).length, - alreadyInFolderCount: _.filter(result, { alreadyInFolder: true }) - .length, + alreadyInFolderCount: _.filter(result, { alreadyInFolder: true }).length, }; }); } @@ -316,28 +301,27 @@ export class BackendSrv { this.getDashboard('db', slug).then(fullDash => { const model = new DashboardModel(fullDash.dashboard, fullDash.meta); - if ( - (!fullDash.meta.folderId && toFolder.id === 0) || - fullDash.meta.folderId === toFolder.id - ) { + if ((!fullDash.meta.folderId && toFolder.id === 0) || fullDash.meta.folderId === toFolder.id) { deferred.resolve({ alreadyInFolder: true }); return; } - model.folderId = toFolder.id; - model.meta.folderId = toFolder.id; - model.meta.folderTitle = toFolder.title; const clone = model.getSaveModelClone(); + let options = { + folderId: toFolder.id, + overwrite: false, + }; - this.saveDashboard(clone, {}) + this.saveDashboard(clone, options) .then(() => { deferred.resolve({ succeeded: true }); }) .catch(err => { if (err.data && err.data.status === 'plugin-dashboard') { err.isHandled = true; + options.overwrite = true; - this.saveDashboard(clone, { overwrite: true }) + this.saveDashboard(clone, options) .then(() => { deferred.resolve({ succeeded: true }); }) diff --git a/public/app/core/services/bridge_srv.ts b/public/app/core/services/bridge_srv.ts new file mode 100644 index 00000000000..561f651b089 --- /dev/null +++ b/public/app/core/services/bridge_srv.ts @@ -0,0 +1,69 @@ +import coreModule from 'app/core/core_module'; +import config from 'app/core/config'; +import appEvents from 'app/core/app_events'; +import { store } from 'app/stores/store'; +import { reaction } from 'mobx'; + +// Services that handles angular -> mobx store sync & other react <-> angular sync +export class BridgeSrv { + private appSubUrl; + private fullPageReloadRoutes; + + /** @ngInject */ + constructor(private $location, private $timeout, private $window, private $rootScope) { + this.appSubUrl = config.appSubUrl; + this.fullPageReloadRoutes = ['/logout']; + } + + // Angular's $location does not like and absolute urls + stripBaseFromUrl(url = '') { + const appSubUrl = this.appSubUrl; + const stripExtraChars = appSubUrl.endsWith('/') ? 1 : 0; + const urlWithoutBase = + url.length > 0 && url.indexOf(appSubUrl) === 0 ? url.slice(appSubUrl.length - stripExtraChars) : url; + + return urlWithoutBase; + } + + init() { + this.$rootScope.$on('$routeUpdate', (evt, data) => { + let angularUrl = this.$location.url(); + if (store.view.currentUrl !== angularUrl) { + store.view.updatePathAndQuery(this.$location.path(), this.$location.search()); + } + }); + + this.$rootScope.$on('$routeChangeSuccess', (evt, data) => { + let angularUrl = this.$location.url(); + if (store.view.currentUrl !== angularUrl) { + store.view.updatePathAndQuery(this.$location.path(), this.$location.search()); + } + }); + + reaction( + () => store.view.currentUrl, + currentUrl => { + let angularUrl = this.$location.url(); + if (angularUrl !== currentUrl) { + this.$location.url(currentUrl); + console.log('store updating angular $location.url', currentUrl); + } + } + ); + + appEvents.on('location-change', payload => { + const urlWithoutBase = this.stripBaseFromUrl(payload.href); + if (this.fullPageReloadRoutes.indexOf(urlWithoutBase) > -1) { + this.$window.location.href = payload.href; + return; + } + + this.$timeout(() => { + // A hack to use timeout when we're changing things (in this case the url) from outside of Angular. + this.$location.url(urlWithoutBase); + }); + }); + } +} + +coreModule.service('bridgeSrv', BridgeSrv); diff --git a/public/app/core/services/context_srv.ts b/public/app/core/services/context_srv.ts index 96c671ea4bc..5a879895267 100644 --- a/public/app/core/services/context_srv.ts +++ b/public/app/core/services/context_srv.ts @@ -51,10 +51,7 @@ export class ContextSrv { } isGrafanaVisible() { - return !!( - document.visibilityState === undefined || - document.visibilityState === 'visible' - ); + return !!(document.visibilityState === undefined || document.visibilityState === 'visible'); } toggleSideMenu() { diff --git a/public/app/core/services/dynamic_directive_srv.ts b/public/app/core/services/dynamic_directive_srv.ts index 86c2cbb40b4..086843b6f9a 100644 --- a/public/app/core/services/dynamic_directive_srv.ts +++ b/public/app/core/services/dynamic_directive_srv.ts @@ -1,5 +1,3 @@ -/// - import angular from 'angular'; import coreModule from '../core_module'; @@ -25,10 +23,7 @@ class DynamicDirectiveSrv { } if (!directiveInfo.fn.registered) { - coreModule.directive( - attrs.$normalize(directiveInfo.name), - directiveInfo.fn - ); + coreModule.directive(attrs.$normalize(directiveInfo.name), directiveInfo.fn); directiveInfo.fn.registered = true; } @@ -36,10 +31,7 @@ class DynamicDirectiveSrv { }) .catch(err => { console.log('Plugin load:', err); - this.$rootScope.appEvent('alert-error', [ - 'Plugin error', - err.toString(), - ]); + this.$rootScope.appEvent('alert-error', ['Plugin error', err.toString()]); }); } diff --git a/public/app/core/services/global_event_srv.ts b/public/app/core/services/global_event_srv.ts deleted file mode 100644 index ee78a7802ab..00000000000 --- a/public/app/core/services/global_event_srv.ts +++ /dev/null @@ -1,45 +0,0 @@ -import coreModule from 'app/core/core_module'; -import config from 'app/core/config'; -import appEvents from 'app/core/app_events'; - -// This service is for registering global events. -// Good for communication react > angular and vice verse -export class GlobalEventSrv { - private appSubUrl; - private fullPageReloadRoutes; - - /** @ngInject */ - constructor(private $location, private $timeout, private $window) { - this.appSubUrl = config.appSubUrl; - this.fullPageReloadRoutes = ['/logout']; - } - - // Angular's $location does not like and absolute urls - stripBaseFromUrl(url = '') { - const appSubUrl = this.appSubUrl; - const stripExtraChars = appSubUrl.endsWith('/') ? 1 : 0; - const urlWithoutBase = - url.length > 0 && url.indexOf(appSubUrl) === 0 - ? url.slice(appSubUrl.length - stripExtraChars) - : url; - - return urlWithoutBase; - } - - init() { - appEvents.on('location-change', payload => { - const urlWithoutBase = this.stripBaseFromUrl(payload.href); - if (this.fullPageReloadRoutes.indexOf(urlWithoutBase) > -1) { - this.$window.location.href = payload.href; - return; - } - - this.$timeout(() => { - // A hack to use timeout when we're changing things (in this case the url) from outside of Angular. - this.$location.url(urlWithoutBase); - }); - }); - } -} - -coreModule.service('globalEventSrv', GlobalEventSrv); diff --git a/public/app/core/services/ng_react.ts b/public/app/core/services/ng_react.ts index 55c1e19a52d..3c61412669e 100644 --- a/public/app/core/services/ng_react.ts +++ b/public/app/core/services/ng_react.ts @@ -88,10 +88,7 @@ function applyFunctions(obj, scope, propsConfig?) { * ensures that when function is called from a React component * the Angular digest cycle is run */ - prev[key] = - angular.isFunction(value) && config.wrapApply !== false - ? applied(value, scope) - : value; + prev[key] = angular.isFunction(value) && config.wrapApply !== false ? applied(value, scope) : value; return prev; }, {}); @@ -167,8 +164,7 @@ function findAttribute(attrs, propName) { // get watch depth of prop (string or array) function getPropWatchDepth(defaultWatch, prop) { - var customWatchDepth = - Array.isArray(prop) && angular.isObject(prop[1]) && prop[1].watchDepth; + var customWatchDepth = Array.isArray(prop) && angular.isObject(prop[1]) && prop[1].watchDepth; return customWatchDepth || defaultWatch; } @@ -205,9 +201,7 @@ var reactComponent = function($injector) { }; // If there are props, re-render when they change - attrs.props - ? watchProps(attrs.watchDepth, scope, [attrs.props], renderMyComponent) - : renderMyComponent(); + attrs.props ? watchProps(attrs.watchDepth, scope, [attrs.props], renderMyComponent) : renderMyComponent(); // cleanup when scope is destroyed scope.$on('$destroy', function() { @@ -215,10 +209,7 @@ var reactComponent = function($injector) { ReactDOM.unmountComponentAtNode(elem[0]); } else { scope.$eval(attrs.onScopeDestroy, { - unmountComponent: ReactDOM.unmountComponentAtNode.bind( - this, - elem[0] - ), + unmountComponent: ReactDOM.unmountComponentAtNode.bind(this, elem[0]), }); } }); @@ -282,20 +273,11 @@ var reactDirective = function($injector) { // watch each property name and trigger an update whenever something changes, // to update scope.props with new values var propExpressions = props.map(function(prop) { - return Array.isArray(prop) - ? [attrs[getPropName(prop)], getPropConfig(prop)] - : attrs[prop]; + return Array.isArray(prop) ? [attrs[getPropName(prop)], getPropConfig(prop)] : attrs[prop]; }); // If we don't have any props, then our watch statement won't fire. - props.length - ? watchProps( - attrs.watchDepth, - scope, - propExpressions, - renderMyComponent - ) - : renderMyComponent(); + props.length ? watchProps(attrs.watchDepth, scope, propExpressions, renderMyComponent) : renderMyComponent(); // cleanup when scope is destroyed scope.$on('$destroy', function() { @@ -303,10 +285,7 @@ var reactDirective = function($injector) { ReactDOM.unmountComponentAtNode(elem[0]); } else { scope.$eval(attrs.onScopeDestroy, { - unmountComponent: ReactDOM.unmountComponentAtNode.bind( - this, - elem[0] - ), + unmountComponent: ReactDOM.unmountComponentAtNode.bind(this, elem[0]), }); } }); diff --git a/public/app/core/services/popover_srv.ts b/public/app/core/services/popover_srv.ts index 7c1708b15a7..113e1e5fae7 100644 --- a/public/app/core/services/popover_srv.ts +++ b/public/app/core/services/popover_srv.ts @@ -1,5 +1,3 @@ -/// - import _ from 'lodash'; import coreModule from 'app/core/core_module'; import Drop from 'tether-drop'; diff --git a/public/app/core/services/util_srv.ts b/public/app/core/services/util_srv.ts index 2a7dbe3a684..52c934ee9ee 100644 --- a/public/app/core/services/util_srv.ts +++ b/public/app/core/services/util_srv.ts @@ -1,5 +1,3 @@ -/// - import coreModule from 'app/core/core_module'; import appEvents from 'app/core/app_events'; diff --git a/public/app/core/specs/backend_srv_specs.ts b/public/app/core/specs/backend_srv_specs.ts index eb228e01a72..74b058b98c8 100644 --- a/public/app/core/specs/backend_srv_specs.ts +++ b/public/app/core/specs/backend_srv_specs.ts @@ -1,10 +1,4 @@ -import { - describe, - beforeEach, - it, - expect, - angularMocks, -} from 'test/lib/common'; +import { describe, beforeEach, it, expect, angularMocks } from 'test/lib/common'; import 'app/core/services/backend_srv'; describe('backend_srv', function() { diff --git a/public/app/core/specs/global_event_srv.jest.ts b/public/app/core/specs/bridge_srv.jest.ts similarity index 64% rename from public/app/core/specs/global_event_srv.jest.ts rename to public/app/core/specs/bridge_srv.jest.ts index ba318b81cc7..f29c88d7a05 100644 --- a/public/app/core/specs/global_event_srv.jest.ts +++ b/public/app/core/specs/bridge_srv.jest.ts @@ -1,5 +1,4 @@ -import { GlobalEventSrv } from 'app/core/services/global_event_srv'; -import { beforeEach } from 'test/lib/common'; +import { BridgeSrv } from 'app/core/services/bridge_srv'; jest.mock('app/core/config', () => { return { @@ -7,11 +6,11 @@ jest.mock('app/core/config', () => { }; }); -describe('GlobalEventSrv', () => { +describe('BridgeSrv', () => { let searchSrv; beforeEach(() => { - searchSrv = new GlobalEventSrv(null, null, null); + searchSrv = new BridgeSrv(null, null, null, null); }); describe('With /subUrl as appSubUrl', () => { diff --git a/public/app/core/specs/datemath.jest.ts b/public/app/core/specs/datemath.jest.ts index 5bf79b1ca42..820c53486db 100644 --- a/public/app/core/specs/datemath.jest.ts +++ b/public/app/core/specs/datemath.jest.ts @@ -20,12 +20,9 @@ describe('DateMath', () => { expect(dateMath.parse('now&1d')).toBe(undefined); }); - it( - 'should return undefined if I pass a unit besides' + spans.toString(), - () => { - expect(dateMath.parse('now+5f')).toBe(undefined); - } - ); + it('should return undefined if I pass a unit besides' + spans.toString(), () => { + expect(dateMath.parse('now+5f')).toBe(undefined); + }); it('should return undefined if rounding unit is not 1', () => { expect(dateMath.parse('now/2y')).toBe(undefined); @@ -51,17 +48,7 @@ describe('DateMath', () => { it('now/d on a utc dashboard should be start of the current day in UTC time', () => { var today = new Date(); - var expected = new Date( - Date.UTC( - today.getUTCFullYear(), - today.getUTCMonth(), - today.getUTCDate(), - 0, - 0, - 0, - 0 - ) - ); + var expected = new Date(Date.UTC(today.getUTCFullYear(), today.getUTCMonth(), today.getUTCDate(), 0, 0, 0, 0)); var startOfDay = dateMath.parse('now/d', false, 'utc').valueOf(); expect(startOfDay).toBe(expected.getTime()); @@ -82,15 +69,11 @@ describe('DateMath', () => { var thenEx = anchor + '||-5' + span; it('should return 5' + span + ' ago', () => { - expect(dateMath.parse(nowEx).format(format)).toEqual( - now.subtract(5, span).format(format) - ); + expect(dateMath.parse(nowEx).format(format)).toEqual(now.subtract(5, span).format(format)); }); it('should return 5' + span + ' before ' + anchor, () => { - expect(dateMath.parse(thenEx).format(format)).toEqual( - anchored.subtract(5, span).format(format) - ); + expect(dateMath.parse(thenEx).format(format)).toEqual(anchored.subtract(5, span).format(format)); }); }); @@ -109,15 +92,11 @@ describe('DateMath', () => { _.each(spans, span => { it('should round now to the beginning of the ' + span, function() { - expect(dateMath.parse('now/' + span).format(format)).toEqual( - now.startOf(span).format(format) - ); + expect(dateMath.parse('now/' + span).format(format)).toEqual(now.startOf(span).format(format)); }); it('should round now to the end of the ' + span, function() { - expect(dateMath.parse('now/' + span, true).format(format)).toEqual( - now.endOf(span).format(format) - ); + expect(dateMath.parse('now/' + span, true).format(format)).toEqual(now.endOf(span).format(format)); }); }); diff --git a/public/app/core/specs/kbn.jest.ts b/public/app/core/specs/kbn.jest.ts index 490c66a90af..4ba0e623784 100644 --- a/public/app/core/specs/kbn.jest.ts +++ b/public/app/core/specs/kbn.jest.ts @@ -34,8 +34,7 @@ describe('unit format menu', function() { function describeValueFormat(desc, value, tickSize, tickDecimals, result) { describe('value format: ' + desc, function() { it('should translate ' + value + ' as ' + result, function() { - var scaledDecimals = - tickDecimals - Math.floor(Math.log(tickSize) / Math.LN10); + var scaledDecimals = tickDecimals - Math.floor(Math.log(tickSize) / Math.LN10); var str = kbn.valueFormats[desc](value, tickDecimals, scaledDecimals); expect(str).toBe(result); }); @@ -342,14 +341,10 @@ describe('duration', function() { }); it('too many decimals', function() { var str = kbn.toDuration(38898367008, 20, 'millisecond'); - expect(str).toBe( - '1 year, 2 months, 3 weeks, 4 days, 5 hours, 6 minutes, 7 seconds, 8 milliseconds' - ); + expect(str).toBe('1 year, 2 months, 3 weeks, 4 days, 5 hours, 6 minutes, 7 seconds, 8 milliseconds'); }); it('floating point error', function() { var str = kbn.toDuration(36993906007, 8, 'millisecond'); - expect(str).toBe( - '1 year, 2 months, 0 weeks, 3 days, 4 hours, 5 minutes, 6 seconds, 7 milliseconds' - ); + expect(str).toBe('1 year, 2 months, 0 weeks, 3 days, 4 hours, 5 minutes, 6 seconds, 7 milliseconds'); }); }); diff --git a/public/app/core/specs/manage_dashboards.jest.ts b/public/app/core/specs/manage_dashboards.jest.ts index 86b4a223a80..bd257c32f9b 100644 --- a/public/app/core/specs/manage_dashboards.jest.ts +++ b/public/app/core/specs/manage_dashboards.jest.ts @@ -567,9 +567,5 @@ function createCtrlWithStubs(searchResponse: any, tags?: any) { }, }; - return new ManageDashboardsCtrl( - {}, - { getNav: () => {} }, - searchSrvStub - ); + return new ManageDashboardsCtrl({}, { getNav: () => {} }, searchSrvStub); } diff --git a/public/app/core/specs/org_switcher.jest.ts b/public/app/core/specs/org_switcher.jest.ts index 3d9c40a4318..06172604069 100644 --- a/public/app/core/specs/org_switcher.jest.ts +++ b/public/app/core/specs/org_switcher.jest.ts @@ -25,8 +25,7 @@ describe('OrgSwitcher', () => { const orgSwitcherCtrl = new OrgSwitchCtrl(backendSrvStub); - orgSwitcherCtrl.getWindowLocationHref = () => - 'http://localhost:3000?orgId=1&from=now-3h&to=now'; + orgSwitcherCtrl.getWindowLocationHref = () => 'http://localhost:3000?orgId=1&from=now-3h&to=now'; orgSwitcherCtrl.setWindowLocationHref = href => (expectedHref = href); return orgSwitcherCtrl.setUsingOrg({ orgId: 2 }); @@ -37,9 +36,7 @@ describe('OrgSwitcher', () => { }); it('should switch orgId in url', () => { - expect(expectedHref).toBe( - 'http://localhost:3000?orgId=2&from=now-3h&to=now' - ); + expect(expectedHref).toBe('http://localhost:3000?orgId=2&from=now-3h&to=now'); }); }); }); diff --git a/public/app/core/specs/rangeutil.jest.ts b/public/app/core/specs/rangeutil.jest.ts index 535022b4493..6bfc0503900 100644 --- a/public/app/core/specs/rangeutil.jest.ts +++ b/public/app/core/specs/rangeutil.jest.ts @@ -5,10 +5,7 @@ import moment from 'moment'; describe('rangeUtil', () => { describe('Can get range grouped list of ranges', () => { it('when custom settings should return default range list', () => { - var groups = rangeUtil.getRelativeTimesList( - { time_options: [] }, - 'Last 5 minutes' - ); + var groups = rangeUtil.getRelativeTimesList({ time_options: [] }, 'Last 5 minutes'); expect(_.keys(groups).length).toBe(4); expect(groups[3][0].active).toBe(true); }); diff --git a/public/app/core/specs/search.jest.ts b/public/app/core/specs/search.jest.ts index d0b1bbd78d3..2457d71a48d 100644 --- a/public/app/core/specs/search.jest.ts +++ b/public/app/core/specs/search.jest.ts @@ -6,12 +6,7 @@ describe('SearchCtrl', () => { search: (options: any) => {}, getDashboardTags: () => {}, }; - let ctrl = new SearchCtrl( - { $on: () => {} }, - {}, - {}, - searchSrvStub - ); + let ctrl = new SearchCtrl({ $on: () => {} }, {}, {}, searchSrvStub); describe('Given an empty result', () => { beforeEach(() => { diff --git a/public/app/core/specs/search_srv.jest.ts b/public/app/core/specs/search_srv.jest.ts index a1470578331..444939d1f4a 100644 --- a/public/app/core/specs/search_srv.jest.ts +++ b/public/app/core/specs/search_srv.jest.ts @@ -35,10 +35,7 @@ describe('SearchSrv', () => { backendSrvMock.search = jest .fn() .mockReturnValueOnce( - Promise.resolve([ - { id: 2, title: 'second but first' }, - { id: 1, title: 'first but second' }, - ]) + Promise.resolve([{ id: 2, title: 'second but first' }, { id: 1, title: 'first but second' }]) ) .mockReturnValue(Promise.resolve([])); @@ -64,14 +61,10 @@ describe('SearchSrv', () => { beforeEach(() => { backendSrvMock.search = jest .fn() - .mockReturnValueOnce( - Promise.resolve([{ id: 2, title: 'two' }, { id: 1, title: 'one' }]) - ) + .mockReturnValueOnce(Promise.resolve([{ id: 2, title: 'two' }, { id: 1, title: 'one' }])) .mockReturnValue(Promise.resolve([])); - impressionSrv.getDashboardOpened = jest - .fn() - .mockReturnValue([4, 5, 1, 2, 3]); + impressionSrv.getDashboardOpened = jest.fn().mockReturnValue([4, 5, 1, 2, 3]); return searchSrv.search({ query: '' }).then(res => { results = res; @@ -90,9 +83,7 @@ describe('SearchSrv', () => { let results; beforeEach(() => { - backendSrvMock.search = jest - .fn() - .mockReturnValue(Promise.resolve([{ id: 1, title: 'starred' }])); + backendSrvMock.search = jest.fn().mockReturnValue(Promise.resolve([{ id: 1, title: 'starred' }])); return searchSrv.search({ query: '' }).then(res => { results = res; @@ -112,14 +103,9 @@ describe('SearchSrv', () => { backendSrvMock.search = jest .fn() .mockReturnValueOnce( - Promise.resolve([ - { id: 1, title: 'starred and recent', isStarred: true }, - { id: 2, title: 'recent' }, - ]) + Promise.resolve([{ id: 1, title: 'starred and recent', isStarred: true }, { id: 2, title: 'recent' }]) ) - .mockReturnValue( - Promise.resolve([{ id: 1, title: 'starred and recent' }]) - ); + .mockReturnValue(Promise.resolve([{ id: 1, title: 'starred and recent' }])); impressionSrv.getDashboardOpened = jest.fn().mockReturnValue([1, 2]); return searchSrv.search({ query: '' }).then(res => { diff --git a/public/app/core/specs/time_series.jest.ts b/public/app/core/specs/time_series.jest.ts index bbbfae90990..5043953071a 100644 --- a/public/app/core/specs/time_series.jest.ts +++ b/public/app/core/specs/time_series.jest.ts @@ -199,9 +199,7 @@ describe('TimeSeries', function() { describe('series option overrides, bars, true & lines false', function() { beforeEach(function() { series.alias = 'test'; - series.applySeriesOverrides([ - { alias: 'test', bars: true, lines: false }, - ]); + series.applySeriesOverrides([{ alias: 'test', bars: true, lines: false }]); }); it('should disable lines, and enable bars', function() { @@ -213,9 +211,7 @@ describe('TimeSeries', function() { describe('series option overrides, linewidth, stack', function() { beforeEach(function() { series.alias = 'test'; - series.applySeriesOverrides([ - { alias: 'test', linewidth: 5, stack: false }, - ]); + series.applySeriesOverrides([{ alias: 'test', linewidth: 5, stack: false }]); }); it('should disable stack, and set lineWidth', function() { @@ -227,9 +223,7 @@ describe('TimeSeries', function() { describe('series option overrides, dashes and lineWidth', function() { beforeEach(function() { series.alias = 'test'; - series.applySeriesOverrides([ - { alias: 'test', linewidth: 5, dashes: true }, - ]); + series.applySeriesOverrides([{ alias: 'test', linewidth: 5, dashes: true }]); }); it('should enable dashes, set dashes lineWidth to 5 and lines lineWidth to 0', function() { @@ -253,9 +247,7 @@ describe('TimeSeries', function() { describe('series option overrides, pointradius, steppedLine', function() { beforeEach(function() { series.alias = 'test'; - series.applySeriesOverrides([ - { alias: 'test', pointradius: 5, steppedLine: true }, - ]); + series.applySeriesOverrides([{ alias: 'test', pointradius: 5, steppedLine: true }]); }); it('should set pointradius, and set steppedLine', function() { diff --git a/public/app/core/specs/value_select_dropdown_specs.ts b/public/app/core/specs/value_select_dropdown_specs.ts index d51a5a374b2..8f6408fb389 100644 --- a/public/app/core/specs/value_select_dropdown_specs.ts +++ b/public/app/core/specs/value_select_dropdown_specs.ts @@ -1,11 +1,4 @@ -import { - describe, - beforeEach, - it, - expect, - angularMocks, - sinon, -} from 'test/lib/common'; +import { describe, beforeEach, it, expect, angularMocks, sinon } from 'test/lib/common'; import 'app/core/directives/value_select_dropdown'; describe('SelectDropdownCtrl', function() { diff --git a/public/app/core/time_series2.ts b/public/app/core/time_series2.ts index 5d9963977de..3e02b1ec939 100644 --- a/public/app/core/time_series2.ts +++ b/public/app/core/time_series2.ts @@ -28,9 +28,10 @@ export function updateLegendValues(data: TimeSeries[], panel) { for (let i = 0; i < data.length; i++) { let series = data[i]; let yaxes = panel.yaxes; - let axis = yaxes[series.yaxis - 1]; + const seriesYAxis = series.yaxis || 1; + let axis = yaxes[seriesYAxis - 1]; let { tickDecimals, scaledDecimals } = getFlotTickDecimals(data, axis); - let formater = kbn.valueFormats[panel.yaxes[series.yaxis - 1].format]; + let formater = kbn.valueFormats[panel.yaxes[seriesYAxis - 1].format]; // decimal override if (_.isNumber(panel.decimals)) { diff --git a/public/app/core/utils/css_loader.ts b/public/app/core/utils/css_loader.ts index 4ff03ec3c97..42f59a9c27c 100644 --- a/public/app/core/utils/css_loader.ts +++ b/public/app/core/utils/css_loader.ts @@ -1,5 +1,3 @@ -/// - var waitSeconds = 100; var head = document.getElementsByTagName('head')[0]; diff --git a/public/app/core/utils/datemath.ts b/public/app/core/utils/datemath.ts index b892d49f2d8..340591e5d9c 100644 --- a/public/app/core/utils/datemath.ts +++ b/public/app/core/utils/datemath.ts @@ -1,5 +1,3 @@ -/// - import _ from 'lodash'; import moment from 'moment'; diff --git a/public/app/core/utils/emitter.ts b/public/app/core/utils/emitter.ts index 280ccbdc14b..ae2489810a1 100644 --- a/public/app/core/utils/emitter.ts +++ b/public/app/core/utils/emitter.ts @@ -1,6 +1,4 @@ -/// - -import EventEmitter from 'eventemitter3'; +import { EventEmitter } from 'eventemitter3'; export class Emitter { emitter: any; diff --git a/public/app/core/utils/file_export.ts b/public/app/core/utils/file_export.ts index adbda50d829..1ebd5b90a86 100644 --- a/public/app/core/utils/file_export.ts +++ b/public/app/core/utils/file_export.ts @@ -4,31 +4,17 @@ import { saveAs } from 'file-saver'; const DEFAULT_DATETIME_FORMAT = 'YYYY-MM-DDTHH:mm:ssZ'; -export function exportSeriesListToCsv( - seriesList, - dateTimeFormat = DEFAULT_DATETIME_FORMAT, - excel = false -) { +export function exportSeriesListToCsv(seriesList, dateTimeFormat = DEFAULT_DATETIME_FORMAT, excel = false) { var text = (excel ? 'sep=;\n' : '') + 'Series;Time;Value\n'; _.each(seriesList, function(series) { _.each(series.datapoints, function(dp) { - text += - series.alias + - ';' + - moment(dp[1]).format(dateTimeFormat) + - ';' + - dp[0] + - '\n'; + text += series.alias + ';' + moment(dp[1]).format(dateTimeFormat) + ';' + dp[0] + '\n'; }); }); saveSaveBlob(text, 'grafana_data_export.csv'); } -export function exportSeriesListToCsvColumns( - seriesList, - dateTimeFormat = DEFAULT_DATETIME_FORMAT, - excel = false -) { +export function exportSeriesListToCsvColumns(seriesList, dateTimeFormat = DEFAULT_DATETIME_FORMAT, excel = false) { var text = (excel ? 'sep=;\n' : '') + 'Time;'; // add header _.each(seriesList, function(series) { diff --git a/public/app/core/utils/flatten.ts b/public/app/core/utils/flatten.ts index 66455cd0871..150017e34f8 100644 --- a/public/app/core/utils/flatten.ts +++ b/public/app/core/utils/flatten.ts @@ -22,12 +22,7 @@ export default function flatten(target, opts): any { maxDepth = currentDepth + 1; } - if ( - !isarray && - isobject && - Object.keys(value).length && - currentDepth < maxDepth - ) { + if (!isarray && isobject && Object.keys(value).length && currentDepth < maxDepth) { ++currentDepth; return step(value, newKey); } diff --git a/public/app/core/utils/kbn.ts b/public/app/core/utils/kbn.ts index d4111139a30..ec4837fe31d 100644 --- a/public/app/core/utils/kbn.ts +++ b/public/app/core/utils/kbn.ts @@ -168,9 +168,7 @@ kbn.calculateInterval = function(range, resolution, lowLimitInterval) { lowLimitMs = kbn.interval_to_ms(lowLimitInterval); } - intervalMs = kbn.round_interval( - (range.to.valueOf() - range.from.valueOf()) / resolution - ); + intervalMs = kbn.round_interval((range.to.valueOf() - range.from.valueOf()) / resolution); if (lowLimitMs > intervalMs) { intervalMs = lowLimitMs; } @@ -184,9 +182,7 @@ kbn.calculateInterval = function(range, resolution, lowLimitInterval) { kbn.describe_interval = function(str) { var matches = str.match(kbn.interval_regex); if (!matches || !_.has(kbn.intervals_in_seconds, matches[2])) { - throw new Error( - 'Invalid interval string, expecting a number followed by one of "Mwdhmsy"' - ); + throw new Error('Invalid interval string, expecting a number followed by one of "Mwdhmsy"'); } else { return { sec: kbn.intervals_in_seconds[matches[2]], @@ -209,11 +205,7 @@ kbn.interval_to_seconds = function(str) { kbn.query_color_dot = function(color, diameter) { return ( '
    ' ); }; @@ -253,23 +245,14 @@ kbn.toFixed = function(value, decimals) { var decimalPos = formatted.indexOf('.'); var precision = decimalPos === -1 ? 0 : formatted.length - decimalPos - 1; if (precision < decimals) { - return ( - (precision ? formatted : formatted + '.') + - String(factor).substr(1, decimals - precision) - ); + return (precision ? formatted : formatted + '.') + String(factor).substr(1, decimals - precision); } } return formatted; }; -kbn.toFixedScaled = function( - value, - decimals, - scaledDecimals, - additionalDecimals, - ext -) { +kbn.toFixedScaled = function(value, decimals, scaledDecimals, additionalDecimals, ext) { if (scaledDecimals === null) { return kbn.toFixed(value, decimals) + ext; } else { @@ -346,9 +329,7 @@ kbn.formatBuilders.decimalSIPrefix = function(unit, offset) { // offset is given, it starts the units at the given prefix; otherwise, the // offset defaults to zero and the initial unit is not prefixed. kbn.formatBuilders.binarySIPrefix = function(unit, offset) { - var prefixes = ['', 'Ki', 'Mi', 'Gi', 'Ti', 'Pi', 'Ei', 'Zi', 'Yi'].slice( - offset - ); + var prefixes = ['', 'Ki', 'Mi', 'Gi', 'Ti', 'Pi', 'Ei', 'Zi', 'Yi'].slice(offset); var units = prefixes.map(function(p) { return ' ' + p + unit; }); @@ -525,11 +506,7 @@ kbn.valueFormats.pressurembar = kbn.formatBuilders.decimalSIPrefix('bar', -1); kbn.valueFormats.pressurekbar = kbn.formatBuilders.decimalSIPrefix('bar', 1); kbn.valueFormats.pressurehpa = kbn.formatBuilders.fixedUnit('hPa'); kbn.valueFormats.pressurehg = kbn.formatBuilders.fixedUnit('"Hg'); -kbn.valueFormats.pressurepsi = kbn.formatBuilders.scaledUnits(1000, [ - ' psi', - ' ksi', - ' Mpsi', -]); +kbn.valueFormats.pressurepsi = kbn.formatBuilders.scaledUnits(1000, [' psi', ' ksi', ' Mpsi']); // Force kbn.valueFormats.forceNm = kbn.formatBuilders.decimalSIPrefix('Nm'); @@ -602,31 +579,13 @@ kbn.valueFormats.ms = function(size, decimals, scaledDecimals) { return kbn.toFixedScaled(size / 60000, decimals, scaledDecimals, 5, ' min'); } else if (Math.abs(size) < 86400000) { // Less than one day, devide in hours - return kbn.toFixedScaled( - size / 3600000, - decimals, - scaledDecimals, - 7, - ' hour' - ); + return kbn.toFixedScaled(size / 3600000, decimals, scaledDecimals, 7, ' hour'); } else if (Math.abs(size) < 31536000000) { // Less than one year, devide in days - return kbn.toFixedScaled( - size / 86400000, - decimals, - scaledDecimals, - 8, - ' day' - ); + return kbn.toFixedScaled(size / 86400000, decimals, scaledDecimals, 8, ' day'); } - return kbn.toFixedScaled( - size / 31536000000, - decimals, - scaledDecimals, - 10, - ' year' - ); + return kbn.toFixedScaled(size / 31536000000, decimals, scaledDecimals, 10, ' year'); }; kbn.valueFormats.s = function(size, decimals, scaledDecimals) { @@ -636,33 +595,15 @@ kbn.valueFormats.s = function(size, decimals, scaledDecimals) { // Less than 1 µs, devide in ns if (Math.abs(size) < 0.000001) { - return kbn.toFixedScaled( - size * 1e9, - decimals, - scaledDecimals - decimals, - -9, - ' ns' - ); + return kbn.toFixedScaled(size * 1e9, decimals, scaledDecimals - decimals, -9, ' ns'); } // Less than 1 ms, devide in µs if (Math.abs(size) < 0.001) { - return kbn.toFixedScaled( - size * 1e6, - decimals, - scaledDecimals - decimals, - -6, - ' µs' - ); + return kbn.toFixedScaled(size * 1e6, decimals, scaledDecimals - decimals, -6, ' µs'); } // Less than 1 second, devide in ms if (Math.abs(size) < 1) { - return kbn.toFixedScaled( - size * 1e3, - decimals, - scaledDecimals - decimals, - -3, - ' ms' - ); + return kbn.toFixedScaled(size * 1e3, decimals, scaledDecimals - decimals, -3, ' ms'); } if (Math.abs(size) < 60) { @@ -678,22 +619,10 @@ kbn.valueFormats.s = function(size, decimals, scaledDecimals) { return kbn.toFixedScaled(size / 86400, decimals, scaledDecimals, 5, ' day'); } else if (Math.abs(size) < 31536000) { // Less than one year, devide in week - return kbn.toFixedScaled( - size / 604800, - decimals, - scaledDecimals, - 6, - ' week' - ); + return kbn.toFixedScaled(size / 604800, decimals, scaledDecimals, 6, ' week'); } - return kbn.toFixedScaled( - size / 3.15569e7, - decimals, - scaledDecimals, - 7, - ' year' - ); + return kbn.toFixedScaled(size / 3.15569e7, decimals, scaledDecimals, 7, ' year'); }; kbn.valueFormats['µs'] = function(size, decimals, scaledDecimals) { @@ -720,29 +649,11 @@ kbn.valueFormats.ns = function(size, decimals, scaledDecimals) { } else if (Math.abs(size) < 1000000) { return kbn.toFixedScaled(size / 1000, decimals, scaledDecimals, 3, ' µs'); } else if (Math.abs(size) < 1000000000) { - return kbn.toFixedScaled( - size / 1000000, - decimals, - scaledDecimals, - 6, - ' ms' - ); + return kbn.toFixedScaled(size / 1000000, decimals, scaledDecimals, 6, ' ms'); } else if (Math.abs(size) < 60000000000) { - return kbn.toFixedScaled( - size / 1000000000, - decimals, - scaledDecimals, - 9, - ' s' - ); + return kbn.toFixedScaled(size / 1000000000, decimals, scaledDecimals, 9, ' s'); } else { - return kbn.toFixedScaled( - size / 60000000000, - decimals, - scaledDecimals, - 12, - ' min' - ); + return kbn.toFixedScaled(size / 60000000000, decimals, scaledDecimals, 12, ' min'); } }; @@ -758,21 +669,9 @@ kbn.valueFormats.m = function(size, decimals, scaledDecimals) { } else if (Math.abs(size) < 10080) { return kbn.toFixedScaled(size / 1440, decimals, scaledDecimals, 3, ' day'); } else if (Math.abs(size) < 604800) { - return kbn.toFixedScaled( - size / 10080, - decimals, - scaledDecimals, - 4, - ' week' - ); + return kbn.toFixedScaled(size / 10080, decimals, scaledDecimals, 4, ' week'); } else { - return kbn.toFixedScaled( - size / 5.25948e5, - decimals, - scaledDecimals, - 5, - ' year' - ); + return kbn.toFixedScaled(size / 5.25948e5, decimals, scaledDecimals, 5, ' year'); } }; diff --git a/public/app/core/utils/model_utils.ts b/public/app/core/utils/model_utils.ts index e2984aa7484..e595ada6e13 100644 --- a/public/app/core/utils/model_utils.ts +++ b/public/app/core/utils/model_utils.ts @@ -1,9 +1,4 @@ -export function assignModelProperties( - target, - source, - defaults, - removeDefaults? -) { +export function assignModelProperties(target, source, defaults, removeDefaults?) { for (var key in defaults) { if (!defaults.hasOwnProperty(key)) { continue; diff --git a/public/app/core/utils/outline.ts b/public/app/core/utils/outline.ts index dafe0b12914..94393e781e9 100644 --- a/public/app/core/utils/outline.ts +++ b/public/app/core/utils/outline.ts @@ -16,9 +16,7 @@ function outlineFixer() { var set_css = function(css_text) { // Handle setting of + + + + + + + + + + + + + + + + diff --git a/public/img/icons_light_theme/icon_add_notification_channel.svg b/public/img/icons_light_theme/icon_add_notification_channel.svg new file mode 100644 index 00000000000..9059310dcdf --- /dev/null +++ b/public/img/icons_light_theme/icon_add_notification_channel.svg @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + + diff --git a/public/img/tgr288gear_line6.pdf b/public/img/tgr288gear_line6.pdf deleted file mode 100644 index 21cc75e1e33..00000000000 Binary files a/public/img/tgr288gear_line6.pdf and /dev/null differ diff --git a/public/sass/_grafana.scss b/public/sass/_grafana.scss index 3232e9c7f8e..ceae2438533 100644 --- a/public/sass/_grafana.scss +++ b/public/sass/_grafana.scss @@ -1,104 +1,107 @@ // vendor -@import "../vendor/css/timepicker.css"; -@import "../vendor/css/spectrum.css"; +@import '../vendor/css/timepicker.css'; +@import '../vendor/css/spectrum.css'; // MIXINS -@import "mixins/mixins"; -@import "mixins/animations"; -@import "mixins/buttons"; -@import "mixins/breakpoints"; -@import "mixins/grid"; -@import "mixins/grid-framework"; -@import "mixins/hover"; -@import "mixins/forms"; -@import "mixins/drop_element"; +@import 'mixins/mixins'; +@import 'mixins/animations'; +@import 'mixins/buttons'; +@import 'mixins/breakpoints'; +@import 'mixins/grid'; +@import 'mixins/grid-framework'; +@import 'mixins/hover'; +@import 'mixins/forms'; +@import 'mixins/drop_element'; // BASE -@import "base/normalize"; -@import "base/reboot"; -@import "base/type"; -@import "base/forms"; -@import "base/grid"; -@import "base/fonts"; -@import "base/code"; -@import "base/icons"; +@import 'base/normalize'; +@import 'base/reboot'; +@import 'base/type'; +@import 'base/forms'; +@import 'base/grid'; +@import 'base/fonts'; +@import 'base/code'; +@import 'base/icons'; // UTILS -@import "utils/utils"; -@import "utils/validation"; -@import "utils/angular"; -@import "utils/spacings"; -@import "utils/widths"; +@import 'utils/utils'; +@import 'utils/validation'; +@import 'utils/angular'; +@import 'utils/spacings'; +@import 'utils/widths'; // LAYOUTS -@import "layout/lists"; -@import "layout/page"; +@import 'layout/lists'; +@import 'layout/page'; // COMPONENTS -@import "components/scrollbar"; -@import "components/cards"; -@import "components/buttons"; -@import "components/navs"; -@import "components/tabs"; -@import "components/alerts"; -@import "components/switch"; -@import "components/tooltip"; -@import "components/tags"; -@import "components/panel_graph"; -@import "components/submenu"; -@import "components/panel_alertlist"; -@import "components/panel_dashlist"; -@import "components/panel_gettingstarted"; -@import "components/panel_pluginlist"; -@import "components/panel_singlestat"; -@import "components/panel_table"; -@import "components/panel_text"; -@import "components/panel_heatmap"; -@import "components/panel_add_panel"; -@import "components/settings_permissions"; -@import "components/tagsinput"; -@import "components/tables_lists"; -@import "components/search"; -@import "components/gf-form"; -@import "components/sidemenu"; -@import "components/navbar"; -@import "components/timepicker"; -@import "components/filter-controls"; -@import "components/filter-list"; -@import "components/filter-table"; -@import "components/old_stuff"; -@import "components/typeahead"; -@import "components/modals"; -@import "components/dropdown"; -@import "components/color_picker"; -@import "components/footer"; -@import "components/infobox"; -@import "components/shortcuts"; -@import "components/drop"; -@import "components/query_editor"; -@import "components/tabbed_view"; -@import "components/query_part"; -@import "components/jsontree"; -@import "components/edit_sidemenu"; -@import "components/row.scss"; -@import "components/json_explorer"; -@import "components/code_editor"; -@import "components/dashboard_grid"; -@import "components/dashboard_list"; -@import "components/page_header"; -@import "components/dashboard_settings"; -@import "components/empty_list_cta"; +@import 'components/scrollbar'; +@import 'components/cards'; +@import 'components/buttons'; +@import 'components/navs'; +@import 'components/tabs'; +@import 'components/alerts'; +@import 'components/switch'; +@import 'components/tooltip'; +@import 'components/tags'; +@import 'components/panel_graph'; +@import 'components/submenu'; +@import 'components/panel_alertlist'; +@import 'components/panel_dashlist'; +@import 'components/panel_gettingstarted'; +@import 'components/panel_pluginlist'; +@import 'components/panel_singlestat'; +@import 'components/panel_table'; +@import 'components/panel_text'; +@import 'components/panel_heatmap'; +@import 'components/panel_add_panel'; +@import 'components/settings_permissions'; +@import 'components/tagsinput'; +@import 'components/tables_lists'; +@import 'components/search'; +@import 'components/gf-form'; +@import 'components/sidemenu'; +@import 'components/navbar'; +@import 'components/timepicker'; +@import 'components/filter-controls'; +@import 'components/filter-list'; +@import 'components/filter-table'; +@import 'components/old_stuff'; +@import 'components/typeahead'; +@import 'components/modals'; +@import 'components/dropdown'; +@import 'components/color_picker'; +@import 'components/footer'; +@import 'components/infobox'; +@import 'components/shortcuts'; +@import 'components/drop'; +@import 'components/query_editor'; +@import 'components/tabbed_view'; +@import 'components/query_part'; +@import 'components/jsontree'; +@import 'components/edit_sidemenu'; +@import 'components/row.scss'; +@import 'components/json_explorer'; +@import 'components/code_editor'; +@import 'components/dashboard_grid'; +@import 'components/dashboard_list'; +@import 'components/page_header'; +@import 'components/dashboard_settings'; +@import 'components/empty_list_cta'; +@import 'components/popper'; +@import 'components/form_select_box'; +@import 'components/user-picker'; // PAGES -@import "pages/login"; -@import "pages/dashboard"; -@import "pages/playlist"; -@import "pages/admin"; -@import "pages/alerting"; -@import "pages/history"; -@import "pages/plugins"; -@import "pages/signup"; -@import "pages/styleguide"; -@import "pages/errorpage"; -@import "old_responsive"; -@import "components/view_states.scss"; +@import 'pages/login'; +@import 'pages/dashboard'; +@import 'pages/playlist'; +@import 'pages/admin'; +@import 'pages/alerting'; +@import 'pages/history'; +@import 'pages/plugins'; +@import 'pages/signup'; +@import 'pages/styleguide'; +@import 'pages/errorpage'; +@import 'old_responsive'; +@import 'components/view_states.scss'; diff --git a/public/sass/base/_icons.scss b/public/sass/base/_icons.scss index f8bc0f08076..5925ab4a9ce 100644 --- a/public/sass/base/_icons.scss +++ b/public/sass/base/_icons.scss @@ -17,172 +17,176 @@ } .gicon-add-annotation { - background-image: url("../img/icons_#{$theme-name}_theme/icon_add_annotation.svg"); + background-image: url('../img/icons_#{$theme-name}_theme/icon_add_annotation.svg'); } .gicon-add-annotation-alt { - background-image: url("../img/icons_#{$theme-name}_theme/icon_add_annotation_alt.svg"); + background-image: url('../img/icons_#{$theme-name}_theme/icon_add_annotation_alt.svg'); } .gicon-add-datasources { - background-image: url("../img/icons_#{$theme-name}_theme/icon_add_data_sources.svg"); + background-image: url('../img/icons_#{$theme-name}_theme/icon_add_data_sources.svg'); } .gicon-add-user { - background-image: url("../img/icons_#{$theme-name}_theme/icon_add_user.svg"); + background-image: url('../img/icons_#{$theme-name}_theme/icon_add_user.svg'); } .gicon-add-team { - background-image: url("../img/icons_#{$theme-name}_theme/icon_add_team.svg"); + background-image: url('../img/icons_#{$theme-name}_theme/icon_add_team.svg'); } .gicon-add-panel { - background-image: url("../img/icons_#{$theme-name}_theme/icon_add_panel.svg"); + background-image: url('../img/icons_#{$theme-name}_theme/icon_add_panel.svg'); } .gicon-add-link { - background-image: url("../img/icons_#{$theme-name}_theme/icon_add_link.svg"); + background-image: url('../img/icons_#{$theme-name}_theme/icon_add_link.svg'); } .gicon-add-variable { - background-image: url("../img/icons_#{$theme-name}_theme/icon_add_variable.svg"); + background-image: url('../img/icons_#{$theme-name}_theme/icon_add_variable.svg'); +} + +.gicon-add-alert-notification-channel { + background-image: url('../img/icons_#{$theme-name}_theme/icon_add_notification_channel.svg'); } .gicon-alert { - background-image: url("../img/icons_#{$theme-name}_theme/icon_alert.svg"); + background-image: url('../img/icons_#{$theme-name}_theme/icon_alert.svg'); } .gicon-alert-alt { - background-image: url("../img/icons_#{$theme-name}_theme/icon_alert_alt.svg"); + background-image: url('../img/icons_#{$theme-name}_theme/icon_alert_alt.svg'); } .gicon-alert-rules { - background-image: url("../img/icons_#{$theme-name}_theme/icon_alert_rules.svg"); + background-image: url('../img/icons_#{$theme-name}_theme/icon_alert_rules.svg'); } .gicon-alert-notification-channel { - background-image: url("../img/icons_#{$theme-name}_theme/icon_notification_channels.svg"); + background-image: url('../img/icons_#{$theme-name}_theme/icon_notification_channels.svg'); } .gicon-annotation { - background-image: url("../img/icons_#{$theme-name}_theme/icon_annotation.svg"); + background-image: url('../img/icons_#{$theme-name}_theme/icon_annotation.svg'); } .gicon-annotation-alt { - background-image: url("../img/icons_#{$theme-name}_theme/icon_annotation_alt.svg"); + background-image: url('../img/icons_#{$theme-name}_theme/icon_annotation_alt.svg'); } .gicon-apikeys { - background-image: url("../img/icons_#{$theme-name}_theme/icon_apikeys.svg"); + background-image: url('../img/icons_#{$theme-name}_theme/icon_apikeys.svg'); } .gicon-branding { - background-image: url("../img/grafana_icon.svg"); + background-image: url('../img/grafana_icon.svg'); } .gicon-cog { - background-image: url("../img/icons_#{$theme-name}_theme/icon_cog.svg"); + background-image: url('../img/icons_#{$theme-name}_theme/icon_cog.svg'); } .gicon-dashboard { - background-image: url("../img/icons_#{$theme-name}_theme/icon_dashboard.svg"); + background-image: url('../img/icons_#{$theme-name}_theme/icon_dashboard.svg'); } .gicon-dashboard-starred { - background-image: url("../img/icons_#{$theme-name}_theme/icon_dashboard_fav.svg"); + background-image: url('../img/icons_#{$theme-name}_theme/icon_dashboard_fav.svg'); } .gicon-dashboard-list { - background-image: url("../img/icons_#{$theme-name}_theme/icon_dashboard_list.svg"); + background-image: url('../img/icons_#{$theme-name}_theme/icon_dashboard_list.svg'); } .gicon-dashboard-new { - background-image: url("../img/icons_#{$theme-name}_theme/icon_new_dashboard.svg"); + background-image: url('../img/icons_#{$theme-name}_theme/icon_new_dashboard.svg'); } .gicon-dashboard-import { - background-image: url("../img/icons_#{$theme-name}_theme/icon_import_dashboard.svg"); + background-image: url('../img/icons_#{$theme-name}_theme/icon_import_dashboard.svg'); } .gicon-datasources { - background-image: url("../img/icons_#{$theme-name}_theme/icon_data_sources.svg"); + background-image: url('../img/icons_#{$theme-name}_theme/icon_data_sources.svg'); } .gicon-folder-new { - background-image: url("../img/icons_#{$theme-name}_theme/icon_add_folder.svg"); + background-image: url('../img/icons_#{$theme-name}_theme/icon_add_folder.svg'); } .gicon-home { - background-image: url("../img/icons_#{$theme-name}_theme/icon_home.svg"); + background-image: url('../img/icons_#{$theme-name}_theme/icon_home.svg'); } .gicon-json { - background-image: url("../img/icons_#{$theme-name}_theme/icon_json.svg"); + background-image: url('../img/icons_#{$theme-name}_theme/icon_json.svg'); } .gicon-link { - background-image: url("../img/icons_#{$theme-name}_theme/icon_link.svg"); + background-image: url('../img/icons_#{$theme-name}_theme/icon_link.svg'); } .gicon-manage { - background-image: url("../img/icons_#{$theme-name}_theme/icon_sitemap.svg"); + background-image: url('../img/icons_#{$theme-name}_theme/icon_sitemap.svg'); } .gicon-org { - background-image: url("../img/icons_#{$theme-name}_theme/icon_org.svg"); + background-image: url('../img/icons_#{$theme-name}_theme/icon_org.svg'); } .gicon-playlists { - background-image: url("../img/icons_#{$theme-name}_theme/icon_playlist.svg"); + background-image: url('../img/icons_#{$theme-name}_theme/icon_playlist.svg'); } .gicon-plugins { - background-image: url("../img/icons_#{$theme-name}_theme/icon_plugins.svg"); + background-image: url('../img/icons_#{$theme-name}_theme/icon_plugins.svg'); } .gicon-preferences { - background-image: url("../img/icons_#{$theme-name}_theme/icon_preferences.svg"); + background-image: url('../img/icons_#{$theme-name}_theme/icon_preferences.svg'); } .gicon-question { - background-image: url("../img/icons_#{$theme-name}_theme/icon_question.svg"); + background-image: url('../img/icons_#{$theme-name}_theme/icon_question.svg'); } .gicon-shield { - background-image: url("../img/icons_#{$theme-name}_theme/icon_shield.svg"); + background-image: url('../img/icons_#{$theme-name}_theme/icon_shield.svg'); } .gicon-snapshots { - background-image: url("../img/icons_#{$theme-name}_theme/icon_snapshots.svg"); + background-image: url('../img/icons_#{$theme-name}_theme/icon_snapshots.svg'); } .gicon-team { - background-image: url("../img/icons_#{$theme-name}_theme/icon_team.svg"); + background-image: url('../img/icons_#{$theme-name}_theme/icon_team.svg'); } .gicon-user { - background-image: url("../img/icons_#{$theme-name}_theme/icon_user.svg"); + background-image: url('../img/icons_#{$theme-name}_theme/icon_user.svg'); } .gicon-variable { - background-image: url("../img/icons_#{$theme-name}_theme/icon_variable.svg"); + background-image: url('../img/icons_#{$theme-name}_theme/icon_variable.svg'); } .gicon-zoom-out { - background-image: url("../img/icons_#{$theme-name}_theme/icon_zoom_out.svg"); + background-image: url('../img/icons_#{$theme-name}_theme/icon_zoom_out.svg'); } .sidemenu { .gicon-dashboard { - background-image: url("../img/icons_dark_theme/icon_dashboard.svg"); + background-image: url('../img/icons_dark_theme/icon_dashboard.svg'); } .gicon-alert { - background-image: url("../img/icons_dark_theme/icon_alert.svg"); + background-image: url('../img/icons_dark_theme/icon_alert.svg'); } .gicon-cog { - background-image: url("../img/icons_dark_theme/icon_cog.svg"); + background-image: url('../img/icons_dark_theme/icon_cog.svg'); } .gicon-question { - background-image: url("../img/icons_dark_theme/icon_question.svg"); + background-image: url('../img/icons_dark_theme/icon_question.svg'); } } diff --git a/public/sass/base/_type.scss b/public/sass/base/_type.scss index aeddbc5ffb7..b69f7a0b439 100644 --- a/public/sass/base/_type.scss +++ b/public/sass/base/_type.scss @@ -299,7 +299,7 @@ blockquote { line-height: $line-height-base; color: $gray-2; &:before { - content: "\2014 \00A0"; + content: '\2014 \00A0'; } } @@ -316,10 +316,10 @@ blockquote { } small { &:before { - content: ""; + content: ''; } &:after { - content: "\00A0 \2014"; + content: '\00A0 \2014'; } } } @@ -330,7 +330,7 @@ q:before, q:after, blockquote:before, blockquote:after { - content: ""; + content: ''; } // Addresses @@ -409,3 +409,8 @@ a.external-link { .no-wrap { white-space: nowrap; } + +.highlight-search-match { + background: transparent; + color: $yellow; +} diff --git a/public/sass/base/font-awesome/_larger.scss b/public/sass/base/font-awesome/_larger.scss index 1efeef30392..5d7bebcda14 100644 --- a/public/sass/base/font-awesome/_larger.scss +++ b/public/sass/base/font-awesome/_larger.scss @@ -8,7 +8,7 @@ vertical-align: -15%; } .#{$fa-css-prefix}-2x { - font-size: 2em; + font-size: 2em !important; } .#{$fa-css-prefix}-3x { font-size: 3em; diff --git a/public/sass/components/_form_select_box.scss b/public/sass/components/_form_select_box.scss new file mode 100644 index 00000000000..880f6d4c647 --- /dev/null +++ b/public/sass/components/_form_select_box.scss @@ -0,0 +1,73 @@ +$select-input-height: 35px; +$select-menu-max-height: 300px; +$select-item-font-size: $font-size-base; +$select-item-bg: $dropdownBackground; +$select-item-fg: $input-color; +$select-option-bg: $dropdownBackground; +$select-option-color: $input-color; +$select-noresults-color: $text-color; +$select-input-bg: $input-bg; +$select-input-border-color: $input-border-color; +$select-menu-box-shadow: $menu-dropdown-shadow; + +@import '../../../node_modules/react-select/scss/default.scss'; + +@mixin select-control() { + width: 100%; + margin-right: $gf-form-margin; + @include border-radius($input-border-radius-sm); + background-color: $input-bg; +} + +@mixin select-control-focus() { + border-color: $input-border-focus; + outline: none; + $shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 8px $input-box-shadow-focus; + @include box-shadow($shadow); +} + +.gf-form-input--form-dropdown { + padding: 0; + border: 0; + overflow: visible; + + .Select-placeholder { + color: $gray-4; + } + + > .Select-control { + @include select-control(); + border-color: $dark-3; + } + + &.is-open > .Select-control { + background: transparent; + border-color: $dark-3; + } + + &.is-focused > .Select-control { + background-color: $input-bg; + @include select-control-focus(); + } + + .Select-menu-outer { + border: 0; + width: auto; + } + + .Select-option.is-focused { + background-color: $dropdownLinkBackgroundHover; + color: $dropdownLinkColorHover; + + &::before { + position: absolute; + left: 0; + top: 0; + height: 100%; + width: 2px; + display: block; + content: ''; + background-image: linear-gradient(to bottom, #ffd500 0%, #ff4400 99%, #ff4400 100%); + } + } +} diff --git a/public/sass/components/_gf-form.scss b/public/sass/components/_gf-form.scss index 6058e10dab3..599a3e32a21 100644 --- a/public/sass/components/_gf-form.scss +++ b/public/sass/components/_gf-form.scss @@ -33,6 +33,7 @@ $input-border: 1px solid $input-border-color; .gf-form--has-input-icon { position: relative; + margin-right: $gf-form-margin; .gf-form-input-icon { position: absolute; @@ -217,7 +218,7 @@ $input-border: 1px solid $input-border-color; background-color: transparent; color: $text-color; font: normal normal normal $font-size-sm/1 FontAwesome; - content: "\f0d7"; + content: '\f0d7'; pointer-events: none; } } @@ -270,7 +271,7 @@ $input-border: 1px solid $input-border-color; select.gf-form-input { text-indent: 0.01px; - text-overflow: ""; + text-overflow: ''; padding-right: $input-padding-x*3; appearance: none; @@ -292,7 +293,7 @@ $input-border: 1px solid $input-border-color; background-color: transparent; color: $text-color; font: normal normal normal $font-size-sm/1 FontAwesome; - content: "\f0d7"; + content: '\f0d7'; pointer-events: none; font-size: 11px; } @@ -319,7 +320,7 @@ $input-border: 1px solid $input-border-color; } .natural-language-input { - &input[type="number"] { + &input[type='number'] { font-size: $font-size-base; line-height: $input-line-height; margin: -6px -5px 0 5px; @@ -342,7 +343,7 @@ $input-border: 1px solid $input-border-color; background-color: transparent; color: $input-color; font: normal normal normal $font-size-sm/1 FontAwesome; - content: "\f0d7"; + content: '\f0d7'; pointer-events: none; } diff --git a/public/sass/components/_page_header.scss b/public/sass/components/_page_header.scss index 947aad11326..a7380327bd0 100644 --- a/public/sass/components/_page_header.scss +++ b/public/sass/components/_page_header.scss @@ -140,7 +140,7 @@ } &::after { - content: ""; + content: ''; position: absolute; top: 0; right: -14px; // half of square's length @@ -157,8 +157,7 @@ background: linear-gradient(135deg, $btn-inverse-bg, $btn-inverse-bg-hl); // stylish arrow design using box shadow - box-shadow: 2px -2px 0 2px rgb(35, 31, 31), - 3px -3px 0 2px rgba(255, 255, 255, 0.1); + box-shadow: 2px -2px 0 2px rgb(35, 31, 31), 3px -3px 0 2px rgba(255, 255, 255, 0.1); // 5px - for rounded arrows and // 50px - to prevent hover glitches on the border created using shadows*/ diff --git a/public/sass/components/_panel_add_panel.scss b/public/sass/components/_panel_add_panel.scss index a6b5aebd107..51754a54d92 100644 --- a/public/sass/components/_panel_add_panel.scss +++ b/public/sass/components/_panel_add_panel.scss @@ -7,12 +7,20 @@ display: flex; align-items: center; - i { + .gicon { font-size: 30px; margin-right: $spacer; } } +.add-panel__close { + margin-left: auto; + background-color: transparent; + border: 0; + font-size: 16px; + margin-right: -10px; +} + .add-panel__title { font-size: $font-size-md; margin-right: $spacer/2; @@ -65,3 +73,7 @@ .add-panel__item-img { height: calc(100% - 15px); } + +.add-panel__item-icon { + padding: 2px; +} diff --git a/public/sass/components/_panel_singlestat.scss b/public/sass/components/_panel_singlestat.scss index 0510c46cd47..33a956a0244 100644 --- a/public/sass/components/_panel_singlestat.scss +++ b/public/sass/components/_panel_singlestat.scss @@ -6,6 +6,7 @@ } .singlestat-panel-value-container { + line-height: 1; display: table-cell; vertical-align: middle; text-align: center; diff --git a/public/sass/components/_popper.scss b/public/sass/components/_popper.scss new file mode 100644 index 00000000000..a4809535feb --- /dev/null +++ b/public/sass/components/_popper.scss @@ -0,0 +1,79 @@ +.popper { + position: absolute; + background: $tooltipBackground; + color: $tooltipColor; + width: 150px; + border-radius: 3px; + box-shadow: 0 0 2px rgba(0, 0, 0, 0.5); + padding: 10px; + text-align: center; +} +.popper .popper__arrow { + width: 0; + height: 0; + border-style: solid; + position: absolute; + margin: 5px; +} + +.popper .popper__arrow { + border-color: $tooltipBackground; +} + +.popper[data-placement^='top'] { + margin-bottom: 5px; +} +.popper[data-placement^='top'] .popper__arrow { + border-width: 5px 5px 0 5px; + border-left-color: transparent; + border-right-color: transparent; + border-bottom-color: transparent; + bottom: -5px; + left: calc(50% - 5px); + margin-top: 0; + margin-bottom: 0; +} +.popper[data-placement^='bottom'] { + margin-top: 5px; +} +.popper[data-placement^='bottom'] .popper__arrow { + border-width: 0 5px 5px 5px; + border-left-color: transparent; + border-right-color: transparent; + border-top-color: transparent; + top: -5px; + left: calc(50% - 5px); + margin-top: 0; + margin-bottom: 0; +} +.popper[data-placement^='right'] { + margin-left: 5px; +} +.popper[data-placement^='right'] .popper__arrow { + border-width: 5px 5px 5px 0; + border-left-color: transparent; + border-top-color: transparent; + border-bottom-color: transparent; + left: -5px; + top: calc(50% - 5px); + margin-left: 0; + margin-right: 0; +} +.popper[data-placement^='left'] { + margin-right: 5px; +} +.popper[data-placement^='left'] .popper__arrow { + border-width: 5px 0 5px 5px; + border-top-color: transparent; + border-right-color: transparent; + border-bottom-color: transparent; + right: -5px; + top: calc(50% - 5px); + margin-left: 0; + margin-right: 0; +} + +.popper__target, +.popper__manager { + display: inline-block; +} diff --git a/public/sass/components/_submenu.scss b/public/sass/components/_submenu.scss index 0bf7f5b0a1c..0027e0b1999 100644 --- a/public/sass/components/_submenu.scss +++ b/public/sass/components/_submenu.scss @@ -68,7 +68,7 @@ overflow-x: hidden; background-color: $dropdownBackground; box-shadow: 0 0 25px 0 rgba(0, 0, 0, 0.4); - z-index: 1000; + z-index: $zindex-typeahead; font-size: $font-size-base; border-radius: 3px 3px 0 0; border: 1px solid $tight-form-func-bg; diff --git a/public/sass/components/_user-picker.scss b/public/sass/components/_user-picker.scss new file mode 100644 index 00000000000..35a2a242746 --- /dev/null +++ b/public/sass/components/_user-picker.scss @@ -0,0 +1,12 @@ +.user-picker-option__button { + position: relative; + text-align: left; + width: 100%; + display: block; + border-radius: 0; +} +.user-picker-option__avatar { + width: 20px; + display: inline-block; + margin-right: 10px; +} diff --git a/public/sass/pages/_alerting.scss b/public/sass/pages/_alerting.scss index 44c4fabbd5b..f44e26d5c20 100644 --- a/public/sass/pages/_alerting.scss +++ b/public/sass/pages/_alerting.scss @@ -28,73 +28,9 @@ border: 0; } -// Alert List - -.alert-list { - display: flex; - flex-direction: row; - justify-content: space-between; -} - -.alert-list-icon { - font-weight: bold; - display: flex; - justify-content: center; - align-items: center; - .icon-gf, - .fa { - font-size: 200%; - position: relative; - top: 2px; - } -} - -.alert-list-body { - display: flex; -} - -.alert-list-main { - padding: 0 2rem; - display: flex; - flex-direction: column; - justify-content: center; -} - -.alert-list-title { - font-size: $font-size-base; - margin: 0; - font-weight: 600; -} - -.alert-list-state { - font-weight: bold; -} - -.alert-list-text { - font-size: $font-size-sm; - margin: 0; - line-height: 1.5rem; - color: $text-color-weak; -} - -.alert-list-info { - color: $text-color; -} - -.alert-list-info-left { - padding-left: 2rem; -} - -.alert-list-footer { - display: flex; - justify-content: space-between; - flex-direction: column; - align-items: flex-end; -} - .panel-has-alert { .panel-alert-icon:before { - content: "\e611"; + content: '\e611'; position: relative; top: 1px; left: -3px; @@ -108,12 +44,12 @@ .panel-alert-icon:before { color: $critical; - content: "\e610"; + content: '\e610'; } } &--alerting::after { - content: ""; + content: ''; position: absolute; top: 0; z-index: -1; @@ -121,14 +57,13 @@ height: 100%; box-shadow: 0 0 10px rgba($critical, 1); opacity: 0; - animation: alerting-panel 1.6s cubic-bezier(1, 0.1, 0.73, 1) 0s infinite - alternate; + animation: alerting-panel 1.6s cubic-bezier(1, 0.1, 0.73, 1) 0s infinite alternate; } &--ok { .panel-alert-icon:before { color: $online; - content: "\e611"; + content: '\e611'; } } } @@ -138,3 +73,94 @@ opacity: 1; } } + +// Alert List +// Alert List + +.alert-rule-list { + display: flex; + flex-direction: row; + flex-wrap: wrap; + justify-content: space-between; + list-style-type: none; +} + +.alert-rule-item { + display: flex; + width: 100%; + height: 100%; + background: $card-background; + box-shadow: $card-shadow; + padding: 4px 8px; + border-radius: 4px; + margin-bottom: 4px; +} + +.alert-rule-item__body { + display: flex; + flex-direction: column; + flex-grow: 1; + overflow: hidden; +} + +.alert-rule-item__icon { + display: flex; + justify-content: center; + align-items: center; + width: 40px; + padding: 0 28px 0 16px; + .icon-gf, + .fa { + font-size: 200%; + position: relative; + top: 2px; + } +} + +.alert-rule-item__header { + display: flex; + flex-direction: column; + justify-content: space-between; +} + +.alert-rule-item__name { + font-size: $font-size-base; + margin: 0; + font-weight: $font-weight-semi-bold; +} + +.alert-list__btn { + margin: 0 2px; + display: flex; + align-items: center; + justify-content: center; +} + +.alert-rule-item__text { + font-weight: bold; + font-size: $font-size-sm; + margin: 0; +} + +.alert-rule-item__time { + color: $text-color-weak; + font-weight: normal; + white-space: nowrap; +} + +.alert-rule-item__info { + //color: $text-color; + font-weight: normal; + flex-grow: 2; + display: flex; + align-items: flex-end; +} + +.alert-rule-item__actions { + display: flex; + align-items: center; +} + +.alert-tesint { + display: flex; +} diff --git a/public/test/mocks/common.ts b/public/test/mocks/common.ts new file mode 100644 index 00000000000..0c0ae0aea6b --- /dev/null +++ b/public/test/mocks/common.ts @@ -0,0 +1,15 @@ +export const backendSrv = { + get: jest.fn(), + post: jest.fn(), +}; + +export function createNavTree(...args) { + let root = []; + let node = root; + for (let arg of args) { + let child = { id: arg, url: `/url/${arg}`, text: `${arg}-Text`, children: [] }; + node.push(child); + node = child.children; + } + return root; +} diff --git a/tsconfig.json b/tsconfig.json index bc9222ac87d..3596930a62f 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -13,7 +13,7 @@ "sourceMap": true, "noEmitOnError": false, "emitDecoratorMetadata": false, - "experimentalDecorators": false, + "experimentalDecorators": true, "noImplicitReturns": true, "noImplicitThis": false, "noImplicitUseStrict":false, diff --git a/vendor/github.com/go-stack/stack/.travis.yml b/vendor/github.com/go-stack/stack/.travis.yml deleted file mode 100644 index d5e5dd52da0..00000000000 --- a/vendor/github.com/go-stack/stack/.travis.yml +++ /dev/null @@ -1,16 +0,0 @@ -language: go -sudo: false -go: - - 1.2 - - 1.3 - - 1.4 - - 1.5 - - 1.6 - - tip - -before_install: - - go get github.com/mattn/goveralls - - go get golang.org/x/tools/cmd/cover - -script: - - goveralls -service=travis-ci diff --git a/vendor/github.com/go-stack/stack/LICENSE.md b/vendor/github.com/go-stack/stack/LICENSE.md index c8ca66c5ede..2abf98ea835 100644 --- a/vendor/github.com/go-stack/stack/LICENSE.md +++ b/vendor/github.com/go-stack/stack/LICENSE.md @@ -1,13 +1,21 @@ -Copyright 2014 Chris Hines +The MIT License (MIT) -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at +Copyright (c) 2014 Chris Hines - http://www.apache.org/licenses/LICENSE-2.0 +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/vendor/github.com/go-stack/stack/stack.go b/vendor/github.com/go-stack/stack/stack.go index a614eeebf16..1bc6971d889 100644 --- a/vendor/github.com/go-stack/stack/stack.go +++ b/vendor/github.com/go-stack/stack/stack.go @@ -1,3 +1,5 @@ +// +build go1.7 + // Package stack implements utilities to capture, manipulate, and format call // stacks. It provides a simpler API than package runtime. // @@ -21,29 +23,31 @@ import ( // Call records a single function invocation from a goroutine stack. type Call struct { - fn *runtime.Func - pc uintptr + frame runtime.Frame } // Caller returns a Call from the stack of the current goroutine. The argument // skip is the number of stack frames to ascend, with 0 identifying the // calling function. func Caller(skip int) Call { - var pcs [2]uintptr + // As of Go 1.9 we need room for up to three PC entries. + // + // 0. An entry for the stack frame prior to the target to check for + // special handling needed if that prior entry is runtime.sigpanic. + // 1. A possible second entry to hold metadata about skipped inlined + // functions. If inline functions were not skipped the target frame + // PC will be here. + // 2. A third entry for the target frame PC when the second entry + // is used for skipped inline functions. + var pcs [3]uintptr n := runtime.Callers(skip+1, pcs[:]) + frames := runtime.CallersFrames(pcs[:n]) + frame, _ := frames.Next() + frame, _ = frames.Next() - var c Call - - if n < 2 { - return c + return Call{ + frame: frame, } - - c.pc = pcs[1] - if runtime.FuncForPC(pcs[0]) != sigpanic { - c.pc-- - } - c.fn = runtime.FuncForPC(c.pc) - return c } // String implements fmt.Stinger. It is equivalent to fmt.Sprintf("%v", c). @@ -54,9 +58,10 @@ func (c Call) String() string { // MarshalText implements encoding.TextMarshaler. It formats the Call the same // as fmt.Sprintf("%v", c). func (c Call) MarshalText() ([]byte, error) { - if c.fn == nil { + if c.frame == (runtime.Frame{}) { return nil, ErrNoFunc } + buf := bytes.Buffer{} fmt.Fprint(&buf, c) return buf.Bytes(), nil @@ -71,6 +76,7 @@ var ErrNoFunc = errors.New("no call stack information") // %s source file // %d line number // %n function name +// %k last segment of the package path // %v equivalent to %s:%d // // It accepts the '+' and '#' flags for most of the verbs as follows. @@ -78,22 +84,23 @@ var ErrNoFunc = errors.New("no call stack information") // %+s path of source file relative to the compile time GOPATH // %#s full path of source file // %+n import path qualified function name +// %+k full package path // %+v equivalent to %+s:%d // %#v equivalent to %#s:%d func (c Call) Format(s fmt.State, verb rune) { - if c.fn == nil { + if c.frame == (runtime.Frame{}) { fmt.Fprintf(s, "%%!%c(NOFUNC)", verb) return } switch verb { case 's', 'v': - file, line := c.fn.FileLine(c.pc) + file := c.frame.File switch { case s.Flag('#'): // done case s.Flag('+'): - file = file[pkgIndex(file, c.fn.Name()):] + file = file[pkgIndex(file, c.frame.Function):] default: const sep = "/" if i := strings.LastIndex(file, sep); i != -1 { @@ -103,16 +110,31 @@ func (c Call) Format(s fmt.State, verb rune) { io.WriteString(s, file) if verb == 'v' { buf := [7]byte{':'} - s.Write(strconv.AppendInt(buf[:1], int64(line), 10)) + s.Write(strconv.AppendInt(buf[:1], int64(c.frame.Line), 10)) } case 'd': - _, line := c.fn.FileLine(c.pc) buf := [6]byte{} - s.Write(strconv.AppendInt(buf[:0], int64(line), 10)) + s.Write(strconv.AppendInt(buf[:0], int64(c.frame.Line), 10)) + + case 'k': + name := c.frame.Function + const pathSep = "/" + start, end := 0, len(name) + if i := strings.LastIndex(name, pathSep); i != -1 { + start = i + len(pathSep) + } + const pkgSep = "." + if i := strings.Index(name[start:], pkgSep); i != -1 { + end = start + i + } + if s.Flag('+') { + start = 0 + } + io.WriteString(s, name[start:end]) case 'n': - name := c.fn.Name() + name := c.frame.Function if !s.Flag('+') { const pathSep = "/" if i := strings.LastIndex(name, pathSep); i != -1 { @@ -127,35 +149,17 @@ func (c Call) Format(s fmt.State, verb rune) { } } +// Frame returns the call frame infomation for the Call. +func (c Call) Frame() runtime.Frame { + return c.frame +} + // PC returns the program counter for this call frame; multiple frames may // have the same PC value. +// +// Deprecated: Use Call.Frame instead. func (c Call) PC() uintptr { - return c.pc -} - -// name returns the import path qualified name of the function containing the -// call. -func (c Call) name() string { - if c.fn == nil { - return "???" - } - return c.fn.Name() -} - -func (c Call) file() string { - if c.fn == nil { - return "???" - } - file, _ := c.fn.FileLine(c.pc) - return file -} - -func (c Call) line() int { - if c.fn == nil { - return 0 - } - _, line := c.fn.FileLine(c.pc) - return line + return c.frame.PC } // CallStack records a sequence of function invocations from a goroutine @@ -179,9 +183,6 @@ func (cs CallStack) MarshalText() ([]byte, error) { buf := bytes.Buffer{} buf.Write(openBracketBytes) for i, pc := range cs { - if pc.fn == nil { - return nil, ErrNoFunc - } if i > 0 { buf.Write(spaceBytes) } @@ -205,49 +206,22 @@ func (cs CallStack) Format(s fmt.State, verb rune) { s.Write(closeBracketBytes) } -// findSigpanic intentionally executes faulting code to generate a stack trace -// containing an entry for runtime.sigpanic. -func findSigpanic() *runtime.Func { - var fn *runtime.Func - var p *int - func() int { - defer func() { - if p := recover(); p != nil { - var pcs [512]uintptr - n := runtime.Callers(2, pcs[:]) - for _, pc := range pcs[:n] { - f := runtime.FuncForPC(pc) - if f.Name() == "runtime.sigpanic" { - fn = f - break - } - } - } - }() - // intentional nil pointer dereference to trigger sigpanic - return *p - }() - return fn -} - -var sigpanic = findSigpanic() - // Trace returns a CallStack for the current goroutine with element 0 // identifying the calling function. func Trace() CallStack { var pcs [512]uintptr - n := runtime.Callers(2, pcs[:]) - cs := make([]Call, n) + n := runtime.Callers(1, pcs[:]) - for i, pc := range pcs[:n] { - pcFix := pc - if i > 0 && cs[i-1].fn != sigpanic { - pcFix-- - } - cs[i] = Call{ - fn: runtime.FuncForPC(pcFix), - pc: pcFix, - } + frames := runtime.CallersFrames(pcs[:n]) + cs := make(CallStack, 0, n) + + // Skip extra frame retrieved just to make sure the runtime.sigpanic + // special case is handled. + frame, more := frames.Next() + + for more { + frame, more = frames.Next() + cs = append(cs, Call{frame: frame}) } return cs @@ -256,7 +230,7 @@ func Trace() CallStack { // TrimBelow returns a slice of the CallStack with all entries below c // removed. func (cs CallStack) TrimBelow(c Call) CallStack { - for len(cs) > 0 && cs[0].pc != c.pc { + for len(cs) > 0 && cs[0] != c { cs = cs[1:] } return cs @@ -265,7 +239,7 @@ func (cs CallStack) TrimBelow(c Call) CallStack { // TrimAbove returns a slice of the CallStack with all entries above c // removed. func (cs CallStack) TrimAbove(c Call) CallStack { - for len(cs) > 0 && cs[len(cs)-1].pc != c.pc { + for len(cs) > 0 && cs[len(cs)-1] != c { cs = cs[:len(cs)-1] } return cs @@ -314,12 +288,13 @@ func pkgIndex(file, funcName string) int { var runtimePath string func init() { - var pcs [1]uintptr + var pcs [3]uintptr runtime.Callers(0, pcs[:]) - fn := runtime.FuncForPC(pcs[0]) - file, _ := fn.FileLine(pcs[0]) + frames := runtime.CallersFrames(pcs[:]) + frame, _ := frames.Next() + file := frame.File - idx := pkgIndex(file, fn.Name()) + idx := pkgIndex(frame.File, frame.Function) runtimePath = file[:idx] if runtime.GOOS == "windows" { @@ -328,7 +303,7 @@ func init() { } func inGoroot(c Call) bool { - file := c.file() + file := c.frame.File if len(file) == 0 || file[0] == '?' { return true } diff --git a/vendor/vendor.json b/vendor/vendor.json index db0774edf95..2f316b26d11 100644 --- a/vendor/vendor.json +++ b/vendor/vendor.json @@ -414,6 +414,14 @@ "revision": "9dee4ca50b83acdf57a35fb9e6fb4be640afa2f3", "revisionTime": "2017-03-27T11:30:21Z" }, + { + "checksumSHA1": "luu+ud4XYZhlCoNnSAwCJlbh0Bg=", + "path": "github.com/go-stack/stack", + "revision": "259ab82a6cad3992b4e21ff5cac294ccb06474bc", + "revisionTime": "2017-11-12T03:14:02Z", + "version": "v1.7.0", + "versionExact": "v1.7.0" + }, { "checksumSHA1": "HHB+Jna1wv0cXLxtCyOnQqFwvn4=", "path": "github.com/go-xorm/builder", @@ -540,7 +548,6 @@ }, { "checksumSHA1": "y/A5iuvwjytQE2CqVuphQRXR2nI=", - "origin": "github.com/grafana/grafana/vendor/github.com/mattn/go-isatty", "path": "github.com/mattn/go-isatty", "revision": "a5cdd64afdee435007ee3e9f6ed4684af949d568", "revisionTime": "2017-09-25T05:49:04Z" diff --git a/yarn.lock b/yarn.lock index a419e1020c9..feba691190e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2,6 +2,14 @@ # yarn lockfile v1 +"@babel/code-frame@^7.0.0-beta.35": + version "7.0.0-beta.36" + resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.0.0-beta.36.tgz#2349d7ec04b3a06945ae173280ef8579b63728e4" + dependencies: + chalk "^2.0.0" + esutils "^2.0.2" + js-tokens "^3.0.0" + "@types/cheerio@*": version "0.22.5" resolved "https://registry.yarnpkg.com/@types/cheerio/-/cheerio-0.22.5.tgz#db749e8470d98f103d51407db9bee5a8b9d20d45" @@ -197,7 +205,11 @@ version "21.1.8" resolved "https://registry.yarnpkg.com/@types/jest/-/jest-21.1.8.tgz#d497213725684f1e5a37900b17a47c9c018f1a97" -"@types/node@*", "@types/node@^8.0.31": +"@types/node@*": + version "8.5.2" + resolved "https://registry.yarnpkg.com/@types/node/-/node-8.5.2.tgz#83b8103fa9a2c2e83d78f701a9aa7c9539739aa5" + +"@types/node@^8.0.31": version "8.0.53" resolved "https://registry.yarnpkg.com/@types/node/-/node-8.0.53.tgz#396b35af826fa66aad472c8cb7b8d5e277f4e6d8" @@ -251,11 +263,11 @@ acorn-dynamic-import@^2.0.0: dependencies: acorn "^4.0.3" -acorn-globals@^3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/acorn-globals/-/acorn-globals-3.1.0.tgz#fd8270f71fbb4996b004fa880ee5d46573a731bf" +acorn-globals@^4.0.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/acorn-globals/-/acorn-globals-4.1.0.tgz#ab716025dbe17c54d3ef81d32ece2b2d99fe2538" dependencies: - acorn "^4.0.4" + acorn "^5.0.0" acorn-jsx@^3.0.0: version "3.0.1" @@ -267,7 +279,7 @@ acorn@^3.0.4: version "3.3.0" resolved "https://registry.yarnpkg.com/acorn/-/acorn-3.3.0.tgz#45e37fb39e8da3f25baee3ff5369e2bb5f22017a" -acorn@^4.0.3, acorn@^4.0.4: +acorn@^4.0.3: version "4.0.13" resolved "https://registry.yarnpkg.com/acorn/-/acorn-4.0.13.tgz#105495ae5361d697bd195c825192e1ad7f253787" @@ -275,6 +287,10 @@ acorn@^5.0.0, acorn@^5.1.1, acorn@^5.2.1: version "5.2.1" resolved "https://registry.yarnpkg.com/acorn/-/acorn-5.2.1.tgz#317ac7821826c22c702d66189ab8359675f135d7" +acorn@^5.1.2: + version "5.3.0" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-5.3.0.tgz#7446d39459c54fb49a80e6ee6478149b940ec822" + acorn@~2.6.4: version "2.6.4" resolved "https://registry.yarnpkg.com/acorn/-/acorn-2.6.4.tgz#eb1f45b4a43fa31d03701a5ec46f3b52673e90ee" @@ -795,12 +811,12 @@ babel-helpers@^6.24.1: babel-runtime "^6.22.0" babel-template "^6.24.1" -babel-jest@^21.2.0: - version "21.2.0" - resolved "https://registry.yarnpkg.com/babel-jest/-/babel-jest-21.2.0.tgz#2ce059519a9374a2c46f2455b6fbef5ad75d863e" +babel-jest@^22.0.4: + version "22.0.4" + resolved "https://registry.yarnpkg.com/babel-jest/-/babel-jest-22.0.4.tgz#533c46de37d7c9d7612f408c76314be9277e0c26" dependencies: - babel-plugin-istanbul "^4.0.0" - babel-preset-jest "^21.2.0" + babel-plugin-istanbul "^4.1.5" + babel-preset-jest "^22.0.3" babel-loader@^7.1.2: version "7.1.2" @@ -822,7 +838,7 @@ babel-plugin-check-es2015-constants@^6.22.0: dependencies: babel-runtime "^6.22.0" -babel-plugin-istanbul@^4.0.0, babel-plugin-istanbul@^4.1.4: +babel-plugin-istanbul@^4.1.4, babel-plugin-istanbul@^4.1.5: version "4.1.5" resolved "https://registry.yarnpkg.com/babel-plugin-istanbul/-/babel-plugin-istanbul-4.1.5.tgz#6760cdd977f411d3e175bb064f2bc327d99b2b6e" dependencies: @@ -830,9 +846,9 @@ babel-plugin-istanbul@^4.0.0, babel-plugin-istanbul@^4.1.4: istanbul-lib-instrument "^1.7.5" test-exclude "^4.1.1" -babel-plugin-jest-hoist@^21.2.0: - version "21.2.0" - resolved "https://registry.yarnpkg.com/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-21.2.0.tgz#2cef637259bd4b628a6cace039de5fcd14dbb006" +babel-plugin-jest-hoist@^22.0.3: + version "22.0.3" + resolved "https://registry.yarnpkg.com/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-22.0.3.tgz#62cde5fe962fd41ae89c119f481ca5cd7dd48bb4" babel-plugin-syntax-object-rest-spread@^6.13.0: version "6.13.0" @@ -1056,11 +1072,11 @@ babel-preset-es2015@^6.24.1: babel-plugin-transform-es2015-unicode-regex "^6.24.1" babel-plugin-transform-regenerator "^6.24.1" -babel-preset-jest@^21.2.0: - version "21.2.0" - resolved "https://registry.yarnpkg.com/babel-preset-jest/-/babel-preset-jest-21.2.0.tgz#ff9d2bce08abd98e8a36d9a8a5189b9173b85638" +babel-preset-jest@^22.0.1, babel-preset-jest@^22.0.3: + version "22.0.3" + resolved "https://registry.yarnpkg.com/babel-preset-jest/-/babel-preset-jest-22.0.3.tgz#e2bb6f6b4a509d3ea0931f013db78c5a84856693" dependencies: - babel-plugin-jest-hoist "^21.2.0" + babel-plugin-jest-hoist "^22.0.3" babel-plugin-syntax-object-rest-spread "^6.13.0" babel-register@^6.26.0: @@ -1183,6 +1199,10 @@ binary-extensions@^1.0.0: version "1.11.0" resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-1.11.0.tgz#46aa1751fb6a2f93ee5e689bb1087d4b14c6c205" +bindings@^1.2.1: + version "1.3.0" + resolved "https://registry.yarnpkg.com/bindings/-/bindings-1.3.0.tgz#b346f6ecf6a95f5a815c5839fc7cdb22502f1ed7" + bl@^1.0.0: version "1.2.1" resolved "https://registry.yarnpkg.com/bl/-/bl-1.2.1.tgz#cac328f7bee45730d404b692203fcb590e172d5e" @@ -1303,6 +1323,10 @@ brorand@^1.0.1: version "1.1.0" resolved "https://registry.yarnpkg.com/brorand/-/brorand-1.1.0.tgz#12c25efe40a45e3c323eb8675a0a0ce57b22371f" +browser-process-hrtime@^0.1.2: + version "0.1.2" + resolved "https://registry.yarnpkg.com/browser-process-hrtime/-/browser-process-hrtime-0.1.2.tgz#425d68a58d3447f02a04aa894187fce8af8b7b8e" + browser-resolve@^1.11.2: version "1.11.2" resolved "https://registry.yarnpkg.com/browser-resolve/-/browser-resolve-1.11.2.tgz#8ff09b0a2c421718a1051c260b32e48f442938ce" @@ -1578,7 +1602,7 @@ chalk@^1.0.0, chalk@^1.1.1, chalk@^1.1.3, chalk@~1.1.0, chalk@~1.1.1: strip-ansi "^3.0.0" supports-color "^2.0.0" -chalk@^2.0.1, chalk@^2.1.0, chalk@^2.3.0: +chalk@^2.0.0, chalk@^2.0.1, chalk@^2.1.0, chalk@^2.3.0: version "2.3.0" resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.3.0.tgz#b5ea48efc9c1793dccc9b4767c93914d3f2d52ba" dependencies: @@ -1682,7 +1706,7 @@ class-utils@^0.3.5: lazy-cache "^2.0.2" static-extend "^0.1.1" -classnames@2.x, classnames@^2.2.5: +classnames@2.x, classnames@^2.2.4, classnames@^2.2.5: version "2.2.5" resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.2.5.tgz#fb3801d453467649ef3603c7d61a02bd129bde6d" @@ -2705,6 +2729,10 @@ detect-libc@^1.0.2: version "1.0.3" resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-1.0.3.tgz#fa137c4bd698edf55cd5cd02ac559f91a4c4ba9b" +detect-newline@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/detect-newline/-/detect-newline-2.1.0.tgz#f41f1c10be4b00e87b5f13da680759f2c5bfd3e2" + dezalgo@^1.0.0, dezalgo@~1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/dezalgo/-/dezalgo-1.0.3.tgz#7f742de066fc748bc8db820569dddce49bf0d456" @@ -2781,6 +2809,10 @@ domelementtype@~1.1.1: version "1.1.3" resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-1.1.3.tgz#bd28773e2642881aec51544924299c5cd822185b" +domexception@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/domexception/-/domexception-1.0.0.tgz#81fe5df81b3f057052cde3a9fa9bf536a85b9ab0" + domhandler@2.1: version "2.1.0" resolved "https://registry.yarnpkg.com/domhandler/-/domhandler-2.1.0.tgz#d2646f5e57f6c3bab11cf6cb05d3c0acf7412594" @@ -3011,17 +3043,28 @@ enzyme-adapter-utils@^1.1.0: object.assign "^4.0.4" prop-types "^15.5.10" +enzyme-to-json@^3.3.0: + version "3.3.0" + resolved "https://registry.yarnpkg.com/enzyme-to-json/-/enzyme-to-json-3.3.0.tgz#553e23a09ffb4b0cf09287e2edf9c6539fddaa84" + dependencies: + lodash "^4.17.4" + enzyme@^3.1.0: - version "3.2.0" - resolved "https://registry.yarnpkg.com/enzyme/-/enzyme-3.2.0.tgz#998bdcda0fc71b8764a0017f7cc692c943f54a7a" + version "3.3.0" + resolved "https://registry.yarnpkg.com/enzyme/-/enzyme-3.3.0.tgz#0971abd167f2d4bf3f5bd508229e1c4b6dc50479" dependencies: cheerio "^1.0.0-rc.2" function.prototype.name "^1.0.3" has "^1.0.1" + is-boolean-object "^1.0.0" + is-callable "^1.1.3" + is-number-object "^1.0.3" + is-string "^1.0.4" is-subset "^0.1.1" lodash "^4.17.4" + object-inspect "^1.5.0" object-is "^1.0.1" - object.assign "^4.0.4" + object.assign "^4.1.0" object.entries "^1.0.4" object.values "^1.0.4" raf "^3.4.0" @@ -3043,7 +3086,7 @@ error-ex@^1.2.0, error-ex@^1.3.1: dependencies: is-arrayish "^0.2.1" -es-abstract@^1.6.1: +es-abstract@^1.5.1, es-abstract@^1.6.1: version "1.10.0" resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.10.0.tgz#1ecb36c197842a00d8ee4c2dfd8646bb97d60864" dependencies: @@ -3146,7 +3189,7 @@ escape-string-regexp@1.0.5, escape-string-regexp@^1.0.2, escape-string-regexp@^1 version "1.0.5" resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" -escodegen@^1.6.1: +escodegen@^1.9.0: version "1.9.0" resolved "https://registry.yarnpkg.com/escodegen/-/escodegen-1.9.0.tgz#9811a2f265dc1cd3894420ee3717064b632b8852" dependencies: @@ -3361,16 +3404,16 @@ expect.js@~0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/expect.js/-/expect.js-0.2.0.tgz#1028533d2c1c363f74a6796ff57ec0520ded2be1" -expect@^21.2.1: - version "21.2.1" - resolved "https://registry.yarnpkg.com/expect/-/expect-21.2.1.tgz#003ac2ac7005c3c29e73b38a272d4afadd6d1d7b" +expect@^22.0.3: + version "22.0.3" + resolved "https://registry.yarnpkg.com/expect/-/expect-22.0.3.tgz#bb486de7d41bf3eb60d3b16dfd1c158a4d91ddfa" dependencies: ansi-styles "^3.2.0" - jest-diff "^21.2.1" - jest-get-type "^21.2.0" - jest-matcher-utils "^21.2.1" - jest-message-util "^21.2.1" - jest-regex-util "^21.2.0" + jest-diff "^22.0.3" + jest-get-type "^22.0.3" + jest-matcher-utils "^22.0.3" + jest-message-util "^22.0.3" + jest-regex-util "^22.0.3" expose-loader@^0.7.3: version "0.7.4" @@ -3743,6 +3786,14 @@ fs-access@^1.0.0: dependencies: null-check "^1.0.0" +fs-extra@4.0.3: + version "4.0.3" + resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-4.0.3.tgz#0d852122e5bc5beb453fb028e9c0c9bf36340c94" + dependencies: + graceful-fs "^4.1.2" + jsonfile "^4.0.0" + universalify "^0.1.0" + fs-extra@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-1.0.0.tgz#cd3ce5f7e7cb6145883fcae3191e9877f8587950" @@ -3759,14 +3810,6 @@ fs-extra@^3.0.1: jsonfile "^3.0.0" universalify "^0.1.0" -fs-extra@^4.0.2: - version "4.0.2" - resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-4.0.2.tgz#f91704c53d1b461f893452b0c307d9997647ab6b" - dependencies: - graceful-fs "^4.1.2" - jsonfile "^4.0.0" - universalify "^0.1.0" - fs-vacuum@~1.2.10: version "1.2.10" resolved "https://registry.yarnpkg.com/fs-vacuum/-/fs-vacuum-1.2.10.tgz#b7629bec07a4031a2548fdf99f5ecf1cc8b31e36" @@ -3817,11 +3860,11 @@ function-bind@^1.0.2, function-bind@^1.1.0, function-bind@^1.1.1: resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d" function.prototype.name@^1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/function.prototype.name/-/function.prototype.name-1.0.3.tgz#0099ae5572e9dd6f03c97d023fd92bcc5e639eac" + version "1.1.0" + resolved "https://registry.yarnpkg.com/function.prototype.name/-/function.prototype.name-1.1.0.tgz#8bd763cc0af860a859cc5d49384d74b932cd2327" dependencies: define-properties "^1.1.2" - function-bind "^1.1.0" + function-bind "^1.1.1" is-callable "^1.1.3" gauge@~2.7.3: @@ -4290,6 +4333,10 @@ has-flag@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-2.0.0.tgz#e8207af1cc7b30d446cc70b734b5e8be18f88d51" +has-symbols@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.0.tgz#ba1a8f1af2a0fc39650f5c850367704122063b44" + has-unicode@^2.0.0, has-unicode@~2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/has-unicode/-/has-unicode-2.0.1.tgz#e0e6fe6a28cf51138855e086d1691e771de2a8b9" @@ -4383,6 +4430,10 @@ header-case@^1.0.0: no-case "^2.2.0" upper-case "^1.1.3" +highlight-words-core@^1.1.0: + version "1.1.2" + resolved "https://registry.yarnpkg.com/highlight-words-core/-/highlight-words-core-1.1.2.tgz#5c2717c4f6c6e7ea2462ab85b43ff8b24f58ec3e" + hmac-drbg@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/hmac-drbg/-/hmac-drbg-1.0.1.tgz#d2745701025a6c775a6c545793ed502fc0c649a1" @@ -4399,6 +4450,10 @@ hoek@4.x.x: version "4.2.0" resolved "https://registry.yarnpkg.com/hoek/-/hoek-4.2.0.tgz#72d9d0754f7fe25ca2d01ad8f8f9a9449a89526d" +hoist-non-react-statics@^2.3.1: + version "2.3.1" + resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-2.3.1.tgz#343db84c6018c650778898240135a1420ee22ce0" + home-or-tmp@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/home-or-tmp/-/home-or-tmp-2.0.0.tgz#e36c3f2d2cae7d746a857e38d18d5f32a7882db8" @@ -4743,6 +4798,10 @@ is-binary-path@^1.0.0: dependencies: binary-extensions "^1.0.0" +is-boolean-object@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-boolean-object/-/is-boolean-object-1.0.0.tgz#98f8b28030684219a95f375cfbd88ce3405dff93" + is-buffer@^1.0.2, is-buffer@^1.1.5: version "1.1.6" resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be" @@ -4881,6 +4940,10 @@ is-npm@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/is-npm/-/is-npm-1.0.0.tgz#f2fb63a65e4905b406c86072765a1a4dc793b9f4" +is-number-object@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/is-number-object/-/is-number-object-1.0.3.tgz#f265ab89a9f445034ef6aff15a8f00b00f551799" + is-number@^0.1.1: version "0.1.1" resolved "https://registry.yarnpkg.com/is-number/-/is-number-0.1.1.tgz#69a7af116963d47206ec9bd9b48a14216f1e3806" @@ -4983,6 +5046,10 @@ is-stream@^1.0.0, is-stream@^1.0.1, is-stream@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-1.1.0.tgz#12d4a3dd4e68e0b79ceb8dbc84173ae80d91ca44" +is-string@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/is-string/-/is-string-1.0.4.tgz#cc3a9b69857d621e963725a24caeec873b826e64" + is-subset@^0.1.1: version "0.1.1" resolved "https://registry.yarnpkg.com/is-subset/-/is-subset-0.1.1.tgz#8a59117d932de1de00f245fcdd39ce43f1e939a6" @@ -5048,7 +5115,7 @@ isstream@0.1.x, isstream@~0.1.2: version "0.1.2" resolved "https://registry.yarnpkg.com/isstream/-/isstream-0.1.2.tgz#47e63f7af55afa6f92e1500e690eb8b8529c099a" -istanbul-api@^1.1.1: +istanbul-api@^1.1.14: version "1.2.1" resolved "https://registry.yarnpkg.com/istanbul-api/-/istanbul-api-1.2.1.tgz#0c60a0515eb11c7d65c6b50bba2c6e999acd8620" dependencies: @@ -5064,7 +5131,7 @@ istanbul-api@^1.1.1: mkdirp "^0.5.1" once "^1.4.0" -istanbul-lib-coverage@^1.0.1, istanbul-lib-coverage@^1.1.1: +istanbul-lib-coverage@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/istanbul-lib-coverage/-/istanbul-lib-coverage-1.1.1.tgz#73bfb998885299415c93d38a3e9adf784a77a9da" @@ -5074,7 +5141,7 @@ istanbul-lib-hook@^1.1.0: dependencies: append-transform "^0.4.0" -istanbul-lib-instrument@^1.4.2, istanbul-lib-instrument@^1.7.5, istanbul-lib-instrument@^1.9.1: +istanbul-lib-instrument@^1.7.5, istanbul-lib-instrument@^1.8.0, istanbul-lib-instrument@^1.9.1: version "1.9.1" resolved "https://registry.yarnpkg.com/istanbul-lib-instrument/-/istanbul-lib-instrument-1.9.1.tgz#250b30b3531e5d3251299fdd64b0b2c9db6b558e" dependencies: @@ -5095,7 +5162,7 @@ istanbul-lib-report@^1.1.2: path-parse "^1.0.5" supports-color "^3.1.2" -istanbul-lib-source-maps@^1.1.0, istanbul-lib-source-maps@^1.2.2: +istanbul-lib-source-maps@^1.2.1, istanbul-lib-source-maps@^1.2.2: version "1.2.2" resolved "https://registry.yarnpkg.com/istanbul-lib-source-maps/-/istanbul-lib-source-maps-1.2.2.tgz#750578602435f28a0c04ee6d7d9e0f2960e62c1c" dependencies: @@ -5111,217 +5178,236 @@ istanbul-reports@^1.1.3: dependencies: handlebars "^4.0.3" -jest-changed-files@^21.2.0: - version "21.2.0" - resolved "https://registry.yarnpkg.com/jest-changed-files/-/jest-changed-files-21.2.0.tgz#5dbeecad42f5d88b482334902ce1cba6d9798d29" +jest-changed-files@^22.0.3: + version "22.0.3" + resolved "https://registry.yarnpkg.com/jest-changed-files/-/jest-changed-files-22.0.3.tgz#3771315acfa24a0ed7e6c545de620db6f1b2d164" dependencies: throat "^4.0.0" -jest-cli@^21.2.1: - version "21.2.1" - resolved "https://registry.yarnpkg.com/jest-cli/-/jest-cli-21.2.1.tgz#9c528b6629d651911138d228bdb033c157ec8c00" +jest-cli@^22.0.4: + version "22.0.4" + resolved "https://registry.yarnpkg.com/jest-cli/-/jest-cli-22.0.4.tgz#0052abaad45c57861c05da8ab5d27bad13ad224d" dependencies: ansi-escapes "^3.0.0" chalk "^2.0.1" glob "^7.1.2" graceful-fs "^4.1.11" is-ci "^1.0.10" - istanbul-api "^1.1.1" - istanbul-lib-coverage "^1.0.1" - istanbul-lib-instrument "^1.4.2" - istanbul-lib-source-maps "^1.1.0" - jest-changed-files "^21.2.0" - jest-config "^21.2.1" - jest-environment-jsdom "^21.2.1" - jest-haste-map "^21.2.0" - jest-message-util "^21.2.1" - jest-regex-util "^21.2.0" - jest-resolve-dependencies "^21.2.0" - jest-runner "^21.2.1" - jest-runtime "^21.2.1" - jest-snapshot "^21.2.1" - jest-util "^21.2.1" + istanbul-api "^1.1.14" + istanbul-lib-coverage "^1.1.1" + istanbul-lib-instrument "^1.8.0" + istanbul-lib-source-maps "^1.2.1" + jest-changed-files "^22.0.3" + jest-config "^22.0.4" + jest-environment-jsdom "^22.0.4" + jest-get-type "^22.0.3" + jest-haste-map "^22.0.3" + jest-message-util "^22.0.3" + jest-regex-util "^22.0.3" + jest-resolve-dependencies "^22.0.3" + jest-runner "^22.0.4" + jest-runtime "^22.0.4" + jest-snapshot "^22.0.3" + jest-util "^22.0.4" + jest-worker "^22.0.3" micromatch "^2.3.11" - node-notifier "^5.0.2" - pify "^3.0.0" + node-notifier "^5.1.2" + realpath-native "^1.0.0" + rimraf "^2.5.4" slash "^1.0.0" string-length "^2.0.0" strip-ansi "^4.0.0" which "^1.2.12" - worker-farm "^1.3.1" - yargs "^9.0.0" + yargs "^10.0.3" -jest-config@^21.2.1: - version "21.2.1" - resolved "https://registry.yarnpkg.com/jest-config/-/jest-config-21.2.1.tgz#c7586c79ead0bcc1f38c401e55f964f13bf2a480" +jest-config@^22.0.1, jest-config@^22.0.4: + version "22.0.4" + resolved "https://registry.yarnpkg.com/jest-config/-/jest-config-22.0.4.tgz#9c2a46c0907b1a1af54d9cdbf18e99b447034e11" dependencies: chalk "^2.0.1" glob "^7.1.1" - jest-environment-jsdom "^21.2.1" - jest-environment-node "^21.2.1" - jest-get-type "^21.2.0" - jest-jasmine2 "^21.2.1" - jest-regex-util "^21.2.0" - jest-resolve "^21.2.0" - jest-util "^21.2.1" - jest-validate "^21.2.1" - pretty-format "^21.2.1" + jest-environment-jsdom "^22.0.4" + jest-environment-node "^22.0.4" + jest-get-type "^22.0.3" + jest-jasmine2 "^22.0.4" + jest-regex-util "^22.0.3" + jest-resolve "^22.0.4" + jest-util "^22.0.4" + jest-validate "^22.0.3" + pretty-format "^22.0.3" -jest-diff@^21.2.1: - version "21.2.1" - resolved "https://registry.yarnpkg.com/jest-diff/-/jest-diff-21.2.1.tgz#46cccb6cab2d02ce98bc314011764bb95b065b4f" +jest-diff@^22.0.3: + version "22.0.3" + resolved "https://registry.yarnpkg.com/jest-diff/-/jest-diff-22.0.3.tgz#ffed5aba6beaf63bb77819ba44dd520168986321" dependencies: chalk "^2.0.1" diff "^3.2.0" - jest-get-type "^21.2.0" - pretty-format "^21.2.1" + jest-get-type "^22.0.3" + pretty-format "^22.0.3" -jest-docblock@^21.2.0: - version "21.2.0" - resolved "https://registry.yarnpkg.com/jest-docblock/-/jest-docblock-21.2.0.tgz#51529c3b30d5fd159da60c27ceedc195faf8d414" - -jest-environment-jsdom@^21.2.1: - version "21.2.1" - resolved "https://registry.yarnpkg.com/jest-environment-jsdom/-/jest-environment-jsdom-21.2.1.tgz#38d9980c8259b2a608ec232deee6289a60d9d5b4" +jest-docblock@^22.0.3: + version "22.0.3" + resolved "https://registry.yarnpkg.com/jest-docblock/-/jest-docblock-22.0.3.tgz#c33aa22682b9fc68a5373f5f82994428a2ded601" dependencies: - jest-mock "^21.2.0" - jest-util "^21.2.1" - jsdom "^9.12.0" + detect-newline "^2.1.0" -jest-environment-node@^21.2.1: - version "21.2.1" - resolved "https://registry.yarnpkg.com/jest-environment-node/-/jest-environment-node-21.2.1.tgz#98c67df5663c7fbe20f6e792ac2272c740d3b8c8" +jest-environment-jsdom@^22.0.4: + version "22.0.4" + resolved "https://registry.yarnpkg.com/jest-environment-jsdom/-/jest-environment-jsdom-22.0.4.tgz#5723d4e724775ed38948de792e62f2d6a7f452df" dependencies: - jest-mock "^21.2.0" - jest-util "^21.2.1" + jest-mock "^22.0.3" + jest-util "^22.0.4" + jsdom "^11.5.1" + +jest-environment-node@^22.0.4: + version "22.0.4" + resolved "https://registry.yarnpkg.com/jest-environment-node/-/jest-environment-node-22.0.4.tgz#068671f85a545f96a5469be3a3dd228fca79c709" + dependencies: + jest-mock "^22.0.3" + jest-util "^22.0.4" jest-get-type@^21.2.0: version "21.2.0" resolved "https://registry.yarnpkg.com/jest-get-type/-/jest-get-type-21.2.0.tgz#f6376ab9db4b60d81e39f30749c6c466f40d4a23" -jest-haste-map@^21.2.0: - version "21.2.0" - resolved "https://registry.yarnpkg.com/jest-haste-map/-/jest-haste-map-21.2.0.tgz#1363f0a8bb4338f24f001806571eff7a4b2ff3d8" +jest-get-type@^22.0.3: + version "22.0.3" + resolved "https://registry.yarnpkg.com/jest-get-type/-/jest-get-type-22.0.3.tgz#fa894b677c0fcd55eff3fd8ee28c7be942e32d36" + +jest-haste-map@^22.0.3: + version "22.0.3" + resolved "https://registry.yarnpkg.com/jest-haste-map/-/jest-haste-map-22.0.3.tgz#c9ecb5c871c5465d4bde4139e527fa0dc784aa2d" dependencies: fb-watchman "^2.0.0" graceful-fs "^4.1.11" - jest-docblock "^21.2.0" + jest-docblock "^22.0.3" + jest-worker "^22.0.3" micromatch "^2.3.11" sane "^2.0.0" - worker-farm "^1.3.1" -jest-jasmine2@^21.2.1: - version "21.2.1" - resolved "https://registry.yarnpkg.com/jest-jasmine2/-/jest-jasmine2-21.2.1.tgz#9cc6fc108accfa97efebce10c4308548a4ea7592" +jest-jasmine2@^22.0.4: + version "22.0.4" + resolved "https://registry.yarnpkg.com/jest-jasmine2/-/jest-jasmine2-22.0.4.tgz#f7c0965116efe831ec674dc954b0134639b3dcee" dependencies: + callsites "^2.0.0" chalk "^2.0.1" - expect "^21.2.1" + expect "^22.0.3" graceful-fs "^4.1.11" - jest-diff "^21.2.1" - jest-matcher-utils "^21.2.1" - jest-message-util "^21.2.1" - jest-snapshot "^21.2.1" - p-cancelable "^0.3.0" + jest-diff "^22.0.3" + jest-matcher-utils "^22.0.3" + jest-message-util "^22.0.3" + jest-snapshot "^22.0.3" + source-map-support "^0.5.0" -jest-matcher-utils@^21.2.1: - version "21.2.1" - resolved "https://registry.yarnpkg.com/jest-matcher-utils/-/jest-matcher-utils-21.2.1.tgz#72c826eaba41a093ac2b4565f865eb8475de0f64" +jest-leak-detector@^22.0.3: + version "22.0.3" + resolved "https://registry.yarnpkg.com/jest-leak-detector/-/jest-leak-detector-22.0.3.tgz#b64904f0e8954a11edb79b0809ff4717fa762d99" + dependencies: + pretty-format "^22.0.3" + optionalDependencies: + weak "^1.0.1" + +jest-matcher-utils@^22.0.3: + version "22.0.3" + resolved "https://registry.yarnpkg.com/jest-matcher-utils/-/jest-matcher-utils-22.0.3.tgz#2ec15ca1af7dcabf4daddc894ccce224b948674e" dependencies: chalk "^2.0.1" - jest-get-type "^21.2.0" - pretty-format "^21.2.1" + jest-get-type "^22.0.3" + pretty-format "^22.0.3" -jest-message-util@^21.2.1: - version "21.2.1" - resolved "https://registry.yarnpkg.com/jest-message-util/-/jest-message-util-21.2.1.tgz#bfe5d4692c84c827d1dcf41823795558f0a1acbe" +jest-message-util@^22.0.3: + version "22.0.3" + resolved "https://registry.yarnpkg.com/jest-message-util/-/jest-message-util-22.0.3.tgz#bf674b2762ef2dd53facf2136423fcca264976df" dependencies: + "@babel/code-frame" "^7.0.0-beta.35" chalk "^2.0.1" micromatch "^2.3.11" slash "^1.0.0" + stack-utils "^1.0.1" -jest-mock@^21.2.0: - version "21.2.0" - resolved "https://registry.yarnpkg.com/jest-mock/-/jest-mock-21.2.0.tgz#7eb0770e7317968165f61ea2a7281131534b3c0f" +jest-mock@^22.0.3: + version "22.0.3" + resolved "https://registry.yarnpkg.com/jest-mock/-/jest-mock-22.0.3.tgz#c875e47b5b729c6c020a2fab317b275c0cf88961" -jest-regex-util@^21.2.0: - version "21.2.0" - resolved "https://registry.yarnpkg.com/jest-regex-util/-/jest-regex-util-21.2.0.tgz#1b1e33e63143babc3e0f2e6c9b5ba1eb34b2d530" +jest-regex-util@^22.0.3: + version "22.0.3" + resolved "https://registry.yarnpkg.com/jest-regex-util/-/jest-regex-util-22.0.3.tgz#c5c10229de5ce2b27bf4347916d95b802ae9aa4d" -jest-resolve-dependencies@^21.2.0: - version "21.2.0" - resolved "https://registry.yarnpkg.com/jest-resolve-dependencies/-/jest-resolve-dependencies-21.2.0.tgz#9e231e371e1a736a1ad4e4b9a843bc72bfe03d09" +jest-resolve-dependencies@^22.0.3: + version "22.0.3" + resolved "https://registry.yarnpkg.com/jest-resolve-dependencies/-/jest-resolve-dependencies-22.0.3.tgz#202ddf370069702cd1865a1952fcc7e52c92720e" dependencies: - jest-regex-util "^21.2.0" + jest-regex-util "^22.0.3" -jest-resolve@^21.2.0: - version "21.2.0" - resolved "https://registry.yarnpkg.com/jest-resolve/-/jest-resolve-21.2.0.tgz#068913ad2ba6a20218e5fd32471f3874005de3a6" +jest-resolve@^22.0.4: + version "22.0.4" + resolved "https://registry.yarnpkg.com/jest-resolve/-/jest-resolve-22.0.4.tgz#a6e47f55e9388c7341b5e9732aedc6fe30906121" dependencies: browser-resolve "^1.11.2" chalk "^2.0.1" - is-builtin-module "^1.0.0" -jest-runner@^21.2.1: - version "21.2.1" - resolved "https://registry.yarnpkg.com/jest-runner/-/jest-runner-21.2.1.tgz#194732e3e518bfb3d7cbfc0fd5871246c7e1a467" +jest-runner@^22.0.4: + version "22.0.4" + resolved "https://registry.yarnpkg.com/jest-runner/-/jest-runner-22.0.4.tgz#3aa43a31b05ce8271539df580c2eb916023d3367" dependencies: - jest-config "^21.2.1" - jest-docblock "^21.2.0" - jest-haste-map "^21.2.0" - jest-jasmine2 "^21.2.1" - jest-message-util "^21.2.1" - jest-runtime "^21.2.1" - jest-util "^21.2.1" - pify "^3.0.0" + jest-config "^22.0.4" + jest-docblock "^22.0.3" + jest-haste-map "^22.0.3" + jest-jasmine2 "^22.0.4" + jest-leak-detector "^22.0.3" + jest-message-util "^22.0.3" + jest-runtime "^22.0.4" + jest-util "^22.0.4" + jest-worker "^22.0.3" throat "^4.0.0" - worker-farm "^1.3.1" -jest-runtime@^21.2.1: - version "21.2.1" - resolved "https://registry.yarnpkg.com/jest-runtime/-/jest-runtime-21.2.1.tgz#99dce15309c670442eee2ebe1ff53a3cbdbbb73e" +jest-runtime@^22.0.4: + version "22.0.4" + resolved "https://registry.yarnpkg.com/jest-runtime/-/jest-runtime-22.0.4.tgz#8f69aa7b5fbb3acd35dc262cbf654e563f69b7b4" dependencies: babel-core "^6.0.0" - babel-jest "^21.2.0" - babel-plugin-istanbul "^4.0.0" + babel-jest "^22.0.4" + babel-plugin-istanbul "^4.1.5" chalk "^2.0.1" convert-source-map "^1.4.0" graceful-fs "^4.1.11" - jest-config "^21.2.1" - jest-haste-map "^21.2.0" - jest-regex-util "^21.2.0" - jest-resolve "^21.2.0" - jest-util "^21.2.1" + jest-config "^22.0.4" + jest-haste-map "^22.0.3" + jest-regex-util "^22.0.3" + jest-resolve "^22.0.4" + jest-util "^22.0.4" json-stable-stringify "^1.0.1" micromatch "^2.3.11" + realpath-native "^1.0.0" slash "^1.0.0" strip-bom "3.0.0" write-file-atomic "^2.1.0" - yargs "^9.0.0" + yargs "^10.0.3" -jest-snapshot@^21.2.1: - version "21.2.1" - resolved "https://registry.yarnpkg.com/jest-snapshot/-/jest-snapshot-21.2.1.tgz#29e49f16202416e47343e757e5eff948c07fd7b0" +jest-snapshot@^22.0.3: + version "22.0.3" + resolved "https://registry.yarnpkg.com/jest-snapshot/-/jest-snapshot-22.0.3.tgz#a949b393781d2fdb4773f6ea765dd67ad1da291e" dependencies: chalk "^2.0.1" - jest-diff "^21.2.1" - jest-matcher-utils "^21.2.1" + jest-diff "^22.0.3" + jest-matcher-utils "^22.0.3" mkdirp "^0.5.1" natural-compare "^1.4.0" - pretty-format "^21.2.1" + pretty-format "^22.0.3" -jest-util@^21.2.1: - version "21.2.1" - resolved "https://registry.yarnpkg.com/jest-util/-/jest-util-21.2.1.tgz#a274b2f726b0897494d694a6c3d6a61ab819bb78" +jest-util@^22.0.4: + version "22.0.4" + resolved "https://registry.yarnpkg.com/jest-util/-/jest-util-22.0.4.tgz#d920a513e0645aaab030cee38e4fe7d5bed8bb6d" dependencies: callsites "^2.0.0" chalk "^2.0.1" graceful-fs "^4.1.11" - jest-message-util "^21.2.1" - jest-mock "^21.2.0" - jest-validate "^21.2.1" + is-ci "^1.0.10" + jest-message-util "^22.0.3" + jest-validate "^22.0.3" mkdirp "^0.5.1" -jest-validate@^21.1.0, jest-validate@^21.2.1: +jest-validate@^21.1.0: version "21.2.1" resolved "https://registry.yarnpkg.com/jest-validate/-/jest-validate-21.2.1.tgz#cc0cbca653cd54937ba4f2a111796774530dd3c7" dependencies: @@ -5330,11 +5416,26 @@ jest-validate@^21.1.0, jest-validate@^21.2.1: leven "^2.1.0" pretty-format "^21.2.1" -jest@^21.2.1: - version "21.2.1" - resolved "https://registry.yarnpkg.com/jest/-/jest-21.2.1.tgz#c964e0b47383768a1438e3ccf3c3d470327604e1" +jest-validate@^22.0.3: + version "22.0.3" + resolved "https://registry.yarnpkg.com/jest-validate/-/jest-validate-22.0.3.tgz#2850d949a36c48b1a40f7eebae1d8539126f7829" dependencies: - jest-cli "^21.2.1" + chalk "^2.0.1" + jest-get-type "^22.0.3" + leven "^2.1.0" + pretty-format "^22.0.3" + +jest-worker@^22.0.3: + version "22.0.3" + resolved "https://registry.yarnpkg.com/jest-worker/-/jest-worker-22.0.3.tgz#30433faca67814a8f80559f75ab2ceaa61332fd2" + dependencies: + merge-stream "^1.0.1" + +jest@^22.0.4: + version "22.0.4" + resolved "https://registry.yarnpkg.com/jest/-/jest-22.0.4.tgz#d3cf560ece6b825b115dce80b9826ceb40f87961" + dependencies: + jest-cli "^22.0.4" jquery@^3.2.1: version "3.2.1" @@ -5429,28 +5530,33 @@ jsdoctypeparser@~1.2.0: dependencies: lodash "^3.7.0" -jsdom@^9.12.0: - version "9.12.0" - resolved "https://registry.yarnpkg.com/jsdom/-/jsdom-9.12.0.tgz#e8c546fffcb06c00d4833ca84410fed7f8a097d4" +jsdom@^11.5.1: + version "11.5.1" + resolved "https://registry.yarnpkg.com/jsdom/-/jsdom-11.5.1.tgz#5df753b8d0bca20142ce21f4f6c039f99a992929" dependencies: abab "^1.0.3" - acorn "^4.0.4" - acorn-globals "^3.1.0" + acorn "^5.1.2" + acorn-globals "^4.0.0" array-equal "^1.0.0" + browser-process-hrtime "^0.1.2" content-type-parser "^1.0.1" cssom ">= 0.3.2 < 0.4.0" cssstyle ">= 0.2.37 < 0.3.0" - escodegen "^1.6.1" + domexception "^1.0.0" + escodegen "^1.9.0" html-encoding-sniffer "^1.0.1" - nwmatcher ">= 1.3.9 < 2.0.0" - parse5 "^1.5.1" - request "^2.79.0" + left-pad "^1.2.0" + nwmatcher "^1.4.3" + parse5 "^3.0.2" + pn "^1.0.0" + request "^2.83.0" + request-promise-native "^1.0.3" sax "^1.2.1" symbol-tree "^3.2.1" - tough-cookie "^2.3.2" - webidl-conversions "^4.0.0" + tough-cookie "^2.3.3" + webidl-conversions "^4.0.2" whatwg-encoding "^1.0.1" - whatwg-url "^4.3.0" + whatwg-url "^6.3.0" xml-name-validator "^2.0.1" jsesc@^0.5.0, jsesc@~0.5.0: @@ -5719,6 +5825,10 @@ lcid@^1.0.0: dependencies: invert-kv "^1.0.0" +left-pad@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/left-pad/-/left-pad-1.2.0.tgz#d30a73c6b8201d8f7d8e7956ba9616087a68e0ee" + leven@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/leven/-/leven-2.1.0.tgz#c2e7a9f772094dee9d34202ae8acce4687875580" @@ -5927,6 +6037,10 @@ lodash.mergewith@^4.6.0: version "4.6.0" resolved "https://registry.yarnpkg.com/lodash.mergewith/-/lodash.mergewith-4.6.0.tgz#150cf0a16791f5903b8891eab154609274bdea55" +lodash.sortby@^4.7.0: + version "4.7.0" + resolved "https://registry.yarnpkg.com/lodash.sortby/-/lodash.sortby-4.7.0.tgz#edd14c824e2cc9c1e0b0a1b42bb5210516a42438" + lodash.tail@^4.1.1: version "4.1.1" resolved "https://registry.yarnpkg.com/lodash.tail/-/lodash.tail-4.1.1.tgz#d2333a36d9e7717c8ad2f7cacafec7c32b444664" @@ -5951,7 +6065,7 @@ lodash@^3.10.1, lodash@^3.5.0, lodash@^3.6.0, lodash@^3.7.0, lodash@^3.8.0, loda version "3.10.1" resolved "https://registry.yarnpkg.com/lodash/-/lodash-3.10.1.tgz#5bf45e8e49ba4189e17d482789dfd15bd140b7b6" -lodash@^4.0.0, lodash@^4.0.1, lodash@^4.14.0, lodash@^4.15.0, lodash@^4.17.3, lodash@^4.17.4, lodash@^4.3.0, lodash@^4.5.0, lodash@^4.7.0, lodash@^4.8.0, lodash@~4.17.4: +lodash@^4.0.0, lodash@^4.0.1, lodash@^4.13.1, lodash@^4.14.0, lodash@^4.15.0, lodash@^4.17.3, lodash@^4.17.4, lodash@^4.3.0, lodash@^4.5.0, lodash@^4.7.0, lodash@^4.8.0, lodash@~4.17.4: version "4.17.4" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.4.tgz#78203a4d1c328ae1d86dca6460e369b57f4055ae" @@ -6141,6 +6255,12 @@ merge-descriptors@1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.1.tgz#b00aaa556dd8b44568150ec9d1b953f3f90cbb61" +merge-stream@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/merge-stream/-/merge-stream-1.0.1.tgz#4041202d508a342ba00174008df0c251b8c135e1" + dependencies: + readable-stream "^2.0.1" + merge@^1.1.3, merge@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/merge/-/merge-1.2.0.tgz#7531e39d4949c281a66b8c5a6e0265e8b05894da" @@ -6303,6 +6423,20 @@ mkdirp@0.5.1, mkdirp@0.x.x, "mkdirp@>=0.5 0", mkdirp@^0.5.0, mkdirp@^0.5.1, mkdi dependencies: minimist "0.0.8" +mobx-react@^4.3.5: + version "4.3.5" + resolved "https://registry.yarnpkg.com/mobx-react/-/mobx-react-4.3.5.tgz#76853f2f2ef4a6f960c374bcd9f01e875929c04c" + dependencies: + hoist-non-react-statics "^2.3.1" + +mobx-state-tree@^1.3.1: + version "1.3.1" + resolved "https://registry.yarnpkg.com/mobx-state-tree/-/mobx-state-tree-1.3.1.tgz#9e1ba9b8b6ea183f1a4a2ae1f67bfa8f2bcae4fe" + +mobx@^3.4.1: + version "3.4.1" + resolved "https://registry.yarnpkg.com/mobx/-/mobx-3.4.1.tgz#37abe5ee882d401828d9f26c6c1a2f47614bbbef" + mocha@^4.0.1: version "4.0.1" resolved "https://registry.yarnpkg.com/mocha/-/mocha-4.0.1.tgz#0aee5a95cf69a4618820f5e51fa31717117daf1b" @@ -6366,7 +6500,7 @@ mute-stream@~0.0.4: version "0.0.7" resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.7.tgz#3075ce93bc21b8fab43e1bc4da7e8115ed1e7bab" -nan@^2.3.0, nan@^2.3.2, nan@^2.6.2: +nan@^2.0.5, nan@^2.3.0, nan@^2.3.2, nan@^2.6.2: version "2.8.0" resolved "https://registry.yarnpkg.com/nan/-/nan-2.8.0.tgz#ed715f3fe9de02b57a5e6252d90a96675e1f085a" @@ -6551,7 +6685,7 @@ node-libs-browser@^2.0.0: util "^0.10.3" vm-browserify "0.0.4" -node-notifier@^5.0.2: +node-notifier@^5.1.2: version "5.1.2" resolved "https://registry.yarnpkg.com/node-notifier/-/node-notifier-5.1.2.tgz#2fa9e12605fa10009d44549d6fcd8a63dde0e4ff" dependencies: @@ -6888,7 +7022,7 @@ number-is-nan@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/number-is-nan/-/number-is-nan-1.0.1.tgz#097b602b53422a522c1afb8790318336941a011d" -"nwmatcher@>= 1.3.9 < 2.0.0": +nwmatcher@^1.4.3: version "1.4.3" resolved "https://registry.yarnpkg.com/nwmatcher/-/nwmatcher-1.4.3.tgz#64348e3b3d80f035b40ac11563d278f8b72db89c" @@ -6916,11 +7050,15 @@ object-copy@^0.1.0: define-property "^0.2.5" kind-of "^3.0.3" +object-inspect@^1.5.0: + version "1.5.0" + resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.5.0.tgz#9d876c11e40f485c79215670281b767488f9bfe3" + object-is@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/object-is/-/object-is-1.0.1.tgz#0aa60ec9989a0b3ed795cf4d06f62cf1ad6539b6" -object-keys@^1.0.10, object-keys@^1.0.8: +object-keys@^1.0.11, object-keys@^1.0.8: version "1.0.11" resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.0.11.tgz#c54601778ad560f1142ce0e01bcca8b56d13426d" @@ -6930,13 +7068,14 @@ object-visit@^1.0.0: dependencies: isobject "^3.0.0" -object.assign@^4.0.4: - version "4.0.4" - resolved "https://registry.yarnpkg.com/object.assign/-/object.assign-4.0.4.tgz#b1c9cc044ef1b9fe63606fc141abbb32e14730cc" +object.assign@^4.0.4, object.assign@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/object.assign/-/object.assign-4.1.0.tgz#968bf1100d7956bb3ca086f006f846b3bc4008da" dependencies: define-properties "^1.1.2" - function-bind "^1.1.0" - object-keys "^1.0.10" + function-bind "^1.1.1" + has-symbols "^1.0.0" + object-keys "^1.0.11" object.entries@^1.0.4: version "1.0.4" @@ -6947,6 +7086,13 @@ object.entries@^1.0.4: function-bind "^1.1.0" has "^1.0.1" +object.getownpropertydescriptors@^2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/object.getownpropertydescriptors/-/object.getownpropertydescriptors-2.0.3.tgz#8758c846f5b407adab0f236e0986f14b051caa16" + dependencies: + define-properties "^1.1.2" + es-abstract "^1.5.1" + object.omit@^2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/object.omit/-/object.omit-2.0.1.tgz#1a9c744829f39dbb858c76ca3579ae2a54ebd1fa" @@ -7063,10 +7209,6 @@ osenv@0, osenv@^0.1.4, osenv@~0.1.4: os-homedir "^1.0.0" os-tmpdir "^1.0.0" -p-cancelable@^0.3.0: - version "0.3.0" - resolved "https://registry.yarnpkg.com/p-cancelable/-/p-cancelable-0.3.0.tgz#b9e123800bcebb7ac13a479be195b507b98d30fa" - p-finally@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/p-finally/-/p-finally-1.0.0.tgz#3fbcfb15b899a44123b34b6dcc18b724336a2cae" @@ -7173,11 +7315,7 @@ parse-json@^3.0.0: dependencies: error-ex "^1.3.1" -parse5@^1.5.1: - version "1.5.1" - resolved "https://registry.yarnpkg.com/parse5/-/parse5-1.5.1.tgz#9b7f3b0de32be78dc2401b17573ccaf0f6f59d94" - -parse5@^3.0.1: +parse5@^3.0.1, parse5@^3.0.2: version "3.0.3" resolved "https://registry.yarnpkg.com/parse5/-/parse5-3.0.3.tgz#042f792ffdd36851551cf4e9e066b3874ab45b5c" dependencies: @@ -7366,6 +7504,14 @@ pluralize@^1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/pluralize/-/pluralize-1.2.1.tgz#d1a21483fd22bb41e58a12fa3421823140897c45" +pn@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/pn/-/pn-1.0.0.tgz#1cf5a30b0d806cd18f88fc41a6b5d4ad615b3ba9" + +popper.js@^1.12.5: + version "1.12.9" + resolved "https://registry.yarnpkg.com/popper.js/-/popper.js-1.12.9.tgz#0dfbc2dff96c451bb332edcfcfaaf566d331d5b3" + posix-character-classes@^0.1.0: version "0.1.1" resolved "https://registry.yarnpkg.com/posix-character-classes/-/posix-character-classes-0.1.1.tgz#01eac0fe3b5af71a2a6c02feabb8c1fef7e00eab" @@ -7723,6 +7869,13 @@ pretty-format@^21.2.1: ansi-regex "^3.0.0" ansi-styles "^3.2.0" +pretty-format@^22.0.3: + version "22.0.3" + resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-22.0.3.tgz#a2bfa59fc33ad24aa4429981bb52524b41ba5dd7" + dependencies: + ansi-regex "^3.0.0" + ansi-styles "^3.2.0" + private@^0.1.6, private@^0.1.7, private@~0.1.5: version "0.1.8" resolved "https://registry.yarnpkg.com/private/-/private-0.1.8.tgz#2381edb3689f7a53d653190060fcf822d2f368ff" @@ -7772,7 +7925,7 @@ promzard@^0.3.0: dependencies: read "1" -prop-types@15.x, prop-types@^15.5.10, prop-types@^15.6.0: +prop-types@15.x, prop-types@^15.5.10, prop-types@^15.5.8, prop-types@^15.6.0: version "15.6.0" resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.6.0.tgz#ceaf083022fc46b4a35f69e13ef75aed0d639856" dependencies: @@ -7838,6 +7991,10 @@ punycode@^1.2.4, punycode@^1.4.1: version "1.4.1" resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.4.1.tgz#c0d5a63b2718800ad8e1eb0fa5269c84dd41845e" +punycode@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.0.tgz#5f863edc89b96db09074bad7947bf09056ca4e7d" + q@^1.1.2: version "1.5.1" resolved "https://registry.yarnpkg.com/q/-/q-1.5.1.tgz#7e32f75b41381291d04611f1bf14109ac00651d7" @@ -7948,9 +8105,9 @@ rc@^1.0.1, rc@^1.1.6, rc@^1.1.7: minimist "^1.2.0" strip-json-comments "~2.0.1" -react-dom@^16.1.1: - version "16.1.1" - resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-16.1.1.tgz#b2e331b6d752faf1a2d31399969399a41d8d45f8" +react-dom@^16.2.0: + version "16.2.0" + resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-16.2.0.tgz#69003178601c0ca19b709b33a83369fe6124c044" dependencies: fbjs "^0.8.16" loose-envify "^1.1.0" @@ -7974,6 +8131,26 @@ react-grid-layout@^0.16.1: react-draggable "^3.0.3" react-resizable "^1.7.5" +react-highlight-words@^0.10.0: + version "0.10.0" + resolved "https://registry.yarnpkg.com/react-highlight-words/-/react-highlight-words-0.10.0.tgz#2e905c76c11635237f848ecad00600f1b6f6f4a8" + dependencies: + highlight-words-core "^1.1.0" + prop-types "^15.5.8" + +react-input-autosize@^2.1.2: + version "2.2.1" + resolved "https://registry.yarnpkg.com/react-input-autosize/-/react-input-autosize-2.2.1.tgz#ec428fa15b1592994fb5f9aa15bb1eb6baf420f8" + dependencies: + prop-types "^15.5.8" + +react-popper@^0.7.5: + version "0.7.5" + resolved "https://registry.yarnpkg.com/react-popper/-/react-popper-0.7.5.tgz#71c25946f291db381231281f6b95729e8b801596" + dependencies: + popper.js "^1.12.5" + prop-types "^15.5.10" + react-resizable@^1.7.5: version "1.7.5" resolved "https://registry.yarnpkg.com/react-resizable/-/react-resizable-1.7.5.tgz#83eb75bb3684da6989bbbf4f826e1470f0af902e" @@ -7981,6 +8158,14 @@ react-resizable@^1.7.5: prop-types "15.x" react-draggable "^2.2.6 || ^3.0.3" +react-select@^1.1.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/react-select/-/react-select-1.2.0.tgz#4f91df941c4ecdb94701faca2533b60e31d7508e" + dependencies: + classnames "^2.2.4" + prop-types "^15.5.8" + react-input-autosize "^2.1.2" + react-sizeme@^2.3.6: version "2.3.6" resolved "https://registry.yarnpkg.com/react-sizeme/-/react-sizeme-2.3.6.tgz#d60ea2634acc3fd827a3c7738d41eea0992fa678" @@ -7997,9 +8182,9 @@ react-test-renderer@^16.0.0, react-test-renderer@^16.0.0-0: object-assign "^4.1.1" prop-types "^15.6.0" -react@^16.1.1: - version "16.1.1" - resolved "https://registry.yarnpkg.com/react/-/react-16.1.1.tgz#d5c4ef795507e3012282dd51261ff9c0e824fe1f" +react@^16.2.0: + version "16.2.0" + resolved "https://registry.yarnpkg.com/react/-/react-16.2.0.tgz#a31bd2dab89bff65d42134fa187f24d054c273ba" dependencies: fbjs "^0.8.16" loose-envify "^1.1.0" @@ -8147,6 +8332,12 @@ readline2@^1.0.1: is-fullwidth-code-point "^1.0.0" mute-stream "0.0.5" +realpath-native@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/realpath-native/-/realpath-native-1.0.0.tgz#7885721a83b43bd5327609f0ddecb2482305fdf0" + dependencies: + util.promisify "^1.0.0" + recast@~0.11.12: version "0.11.23" resolved "https://registry.yarnpkg.com/recast/-/recast-0.11.23.tgz#451fd3004ab1e4df9b4e4b66376b2a21912462d3" @@ -8301,7 +8492,21 @@ request-progress@^2.0.1: dependencies: throttleit "^1.0.0" -request@2, request@^2.74.0, request@^2.79.0, request@^2.81.0, request@~2.83.0: +request-promise-core@1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/request-promise-core/-/request-promise-core-1.1.1.tgz#3eee00b2c5aa83239cfb04c5700da36f81cd08b6" + dependencies: + lodash "^4.13.1" + +request-promise-native@^1.0.3: + version "1.0.5" + resolved "https://registry.yarnpkg.com/request-promise-native/-/request-promise-native-1.0.5.tgz#5281770f68e0c9719e5163fd3fab482215f4fda5" + dependencies: + request-promise-core "1.1.1" + stealthy-require "^1.1.0" + tough-cookie ">=2.3.3" + +request@2, request@^2.74.0, request@^2.81.0, request@^2.83.0, request@~2.83.0: version "2.83.0" resolved "https://registry.yarnpkg.com/request/-/request-2.83.0.tgz#ca0b65da02ed62935887808e6f510381034e3356" dependencies: @@ -9034,6 +9239,10 @@ stack-trace@0.0.x: version "0.0.10" resolved "https://registry.yarnpkg.com/stack-trace/-/stack-trace-0.0.10.tgz#547c70b347e8d32b4e108ea1a2a159e5fdde19c0" +stack-utils@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/stack-utils/-/stack-utils-1.0.1.tgz#d4f33ab54e8e38778b0ca5cfd3b3afb12db68620" + staged-git-files@0.0.4: version "0.0.4" resolved "https://registry.yarnpkg.com/staged-git-files/-/staged-git-files-0.0.4.tgz#d797e1b551ca7a639dec0237dc6eb4bb9be17d35" @@ -9059,6 +9268,10 @@ stdout-stream@^1.4.0: dependencies: readable-stream "^2.0.1" +stealthy-require@^1.1.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/stealthy-require/-/stealthy-require-1.1.1.tgz#35b09875b4ff49f26a777e509b3090a3226bf24b" + stream-browserify@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/stream-browserify/-/stream-browserify-2.0.1.tgz#66266ee5f9bdb9940a4e4514cafb43bb71e5c9db" @@ -9488,15 +9701,17 @@ toposort@^1.0.0: version "1.0.6" resolved "https://registry.yarnpkg.com/toposort/-/toposort-1.0.6.tgz#c31748e55d210effc00fdcdc7d6e68d7d7bb9cec" -tough-cookie@^2.3.2, tough-cookie@~2.3.0, tough-cookie@~2.3.3: +tough-cookie@>=2.3.3, tough-cookie@^2.3.3, tough-cookie@~2.3.0, tough-cookie@~2.3.3: version "2.3.3" resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.3.3.tgz#0b618a5565b6dea90bf3425d04d55edc475a7561" dependencies: punycode "^1.4.1" -tr46@~0.0.3: - version "0.0.3" - resolved "https://registry.yarnpkg.com/tr46/-/tr46-0.0.3.tgz#8184fd347dac9cdc185992f3a6622e14b9d9ab6a" +tr46@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/tr46/-/tr46-1.0.1.tgz#a8b13fd6bfd2489519674ccde55ba3693b706d09" + dependencies: + punycode "^2.1.0" trim-newlines@^1.0.0: version "1.0.0" @@ -9520,27 +9735,26 @@ tryor@~0.1.2: version "0.1.2" resolved "https://registry.yarnpkg.com/tryor/-/tryor-0.1.2.tgz#8145e4ca7caff40acde3ccf946e8b8bb75b4172b" -ts-jest@^21.1.3: - version "21.2.3" - resolved "https://registry.yarnpkg.com/ts-jest/-/ts-jest-21.2.3.tgz#d90cd143c433e8dfd9b8df54921c7f3f1d8b9819" +ts-jest@^22.0.0: + version "22.0.0" + resolved "https://registry.yarnpkg.com/ts-jest/-/ts-jest-22.0.0.tgz#cce1a5f1106150ca002d09f7e85913355ceb5e8d" dependencies: babel-core "^6.24.1" babel-plugin-istanbul "^4.1.4" babel-plugin-transform-es2015-modules-commonjs "^6.24.1" - babel-preset-jest "^21.2.0" + babel-preset-jest "^22.0.1" cpx "^1.5.0" - fs-extra "^4.0.2" - jest-config "^21.2.1" - jest-util "^21.2.1" + fs-extra "4.0.3" + jest-config "^22.0.1" pkg-dir "^2.0.0" source-map-support "^0.5.0" yargs "^10.0.3" -ts-loader@^2.3.7: - version "2.3.7" - resolved "https://registry.yarnpkg.com/ts-loader/-/ts-loader-2.3.7.tgz#a9028ced473bee12f28a75f9c5b139979d33f2fc" +ts-loader@^3.2.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/ts-loader/-/ts-loader-3.2.0.tgz#23211922179b81f7448754b7fdfca45b8374a15a" dependencies: - chalk "^2.0.1" + chalk "^2.3.0" enhanced-resolve "^3.0.0" loader-utils "^1.0.2" semver "^5.0.1" @@ -9559,7 +9773,7 @@ tslint-loader@^3.5.3: rimraf "^2.4.4" semver "^5.3.0" -tslint@^5.7.0: +tslint@^5.8.0: version "5.8.0" resolved "https://registry.yarnpkg.com/tslint/-/tslint-5.8.0.tgz#1f49ad5b2e77c76c3af4ddcae552ae4e3612eb13" dependencies: @@ -9616,9 +9830,9 @@ typedarray@^0.0.6: version "0.0.6" resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777" -typescript@^2.5.2: - version "2.6.1" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-2.6.1.tgz#ef39cdea27abac0b500242d6726ab90e0c846631" +typescript@^2.6.2: + version "2.6.2" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-2.6.2.tgz#3c5b6fd7f6de0914269027f03c0946758f7673a4" ua-parser-js@^0.7.9: version "0.7.17" @@ -9844,6 +10058,13 @@ util-extend@^1.0.1: version "1.0.3" resolved "https://registry.yarnpkg.com/util-extend/-/util-extend-1.0.3.tgz#a7c216d267545169637b3b6edc6ca9119e2ff93f" +util.promisify@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/util.promisify/-/util.promisify-1.0.0.tgz#440f7165a459c9a16dc145eb8e72f35687097030" + dependencies: + define-properties "^1.1.2" + object.getownpropertydescriptors "^2.0.3" + util@0.10.3, "util@>=0.10.3 <1", util@^0.10.3: version "0.10.3" resolved "https://registry.yarnpkg.com/util/-/util-0.10.3.tgz#7afb1afe50805246489e3db7fe0ed379336ac0f9" @@ -9974,11 +10195,14 @@ wcwidth@^1.0.0: dependencies: defaults "^1.0.3" -webidl-conversions@^3.0.0: - version "3.0.1" - resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz#24534275e2a7bc6be7bc86611cc16ae0a5654871" +weak@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/weak/-/weak-1.0.1.tgz#ab99aab30706959aa0200cb8cf545bb9cb33b99e" + dependencies: + bindings "^1.2.1" + nan "^2.0.5" -webidl-conversions@^4.0.0: +webidl-conversions@^4.0.1, webidl-conversions@^4.0.2: version "4.0.2" resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-4.0.2.tgz#a855980b1f0b6b359ba1d5d9fb39ae941faa63ad" @@ -10036,9 +10260,9 @@ webpack-sources@^1.0.1: source-list-map "^2.0.0" source-map "~0.6.1" -webpack@^3.6.0: - version "3.8.1" - resolved "https://registry.yarnpkg.com/webpack/-/webpack-3.8.1.tgz#b16968a81100abe61608b0153c9159ef8bb2bd83" +webpack@^3.10.0: + version "3.10.0" + resolved "https://registry.yarnpkg.com/webpack/-/webpack-3.10.0.tgz#5291b875078cf2abf42bdd23afe3f8f96c17d725" dependencies: acorn "^5.0.0" acorn-dynamic-import "^2.0.0" @@ -10073,12 +10297,13 @@ whatwg-fetch@>=0.10.0: version "2.0.3" resolved "https://registry.yarnpkg.com/whatwg-fetch/-/whatwg-fetch-2.0.3.tgz#9c84ec2dcf68187ff00bc64e1274b442176e1c84" -whatwg-url@^4.3.0: - version "4.8.0" - resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-4.8.0.tgz#d2981aa9148c1e00a41c5a6131166ab4683bbcc0" +whatwg-url@^6.3.0: + version "6.4.0" + resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-6.4.0.tgz#08fdf2b9e872783a7a1f6216260a1d66cc722e08" dependencies: - tr46 "~0.0.3" - webidl-conversions "^3.0.0" + lodash.sortby "^4.7.0" + tr46 "^1.0.0" + webidl-conversions "^4.0.1" whet.extend@~0.9.9: version "0.9.9" @@ -10144,7 +10369,7 @@ wordwrap@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-1.0.0.tgz#27584810891456a4171c8d0226441ade90cbcaeb" -worker-farm@^1.3.1, worker-farm@~1.5.0: +worker-farm@~1.5.0: version "1.5.2" resolved "https://registry.yarnpkg.com/worker-farm/-/worker-farm-1.5.2.tgz#32b312e5dc3d5d45d79ef44acc2587491cd729ae" dependencies: @@ -10316,24 +10541,6 @@ yargs@^8.0.2: y18n "^3.2.1" yargs-parser "^7.0.0" -yargs@^9.0.0: - version "9.0.1" - resolved "https://registry.yarnpkg.com/yargs/-/yargs-9.0.1.tgz#52acc23feecac34042078ee78c0c007f5085db4c" - dependencies: - camelcase "^4.1.0" - cliui "^3.2.0" - decamelize "^1.1.1" - get-caller-file "^1.0.1" - os-locale "^2.0.0" - read-pkg-up "^2.0.0" - require-directory "^2.1.1" - require-main-filename "^1.0.1" - set-blocking "^2.0.0" - string-width "^2.0.0" - which-module "^2.0.0" - y18n "^3.2.1" - yargs-parser "^7.0.0" - yargs@~3.10.0: version "3.10.0" resolved "https://registry.yarnpkg.com/yargs/-/yargs-3.10.0.tgz#f7ee7bd857dd7c1d2d38c0e74efbd681d1431fd1"